插件雏形
现在开始第一步,先建立shink_quick_naming文件夹作为根文件夹(也是版本控制的根文件夹,后文会用根文件夹指代),然后在其中建立shink_quick_naming文件夹来存放Ruby代码(后文会用后端文件夹指代),建立shink_quick_naming_front文件夹来存放前端代码(后文会用前端文件夹指代),新建shink_quick_naming.rb作为入口文件,它的内容如下:
require "sketchup.rb"
require "extensions.rb"
module ShinkQuickNaming
PluginName = 'shink_quick_naming'
PluginChineseName = '快速命名分层'
Version = '0.0.1'.freeze
load_path = File.join(File.dirname(__FILE__), "#{PluginName}/load.rb")
extension = SketchupExtension.new(PluginChineseName, load_path)
extension.version = Version
extension.creator = 'Shink'
Sketchup.register_extension(extension, true)
end
前面两行算是例行公事。第4行起开始正式内容,首先定义了一个命名空间,即一个全局模块,名称一般就是插件名称的驼峰式写法,因为Ruby的常量需要以大写字母开头,它会作为后面写的所有代码的容器,在名称上要尽量不和别的插件重复。后面三行通过定义常量的方式保存了插件的名称,中文名与版本号。第9行通过拼接方法获得了插件加载文件的路径,其中__FILE__的值是当前文件的路径。第10行使用插件的中文名以及插件加载文件路径通过SU提供的api创建了一个extension对象,给它设置了版本与创建人的信息后,我们使用SU的api来注册他并希望被立刻加载。这就是入口文件的意义,SU每次开启时都会加载所有插件的入口文件,但是插件的启用状态决定了它的加载文件是否会被执行。
然后,开始初始化后端代码的整体结构,首先在后端文件夹中将基础库的代码整个文件夹复制过来,然后新建插件加载文件load.rb,内容如下:
$LOAD_PATH.unshift(File.dirname(__FILE__))
load('shink_sketchup_library/load.rb')#加载基础库
SHINK_LIBRARY.inject_to_module(ShinkQuickNaming)#注入到模块
Sketchup::require('path')#路径相关方法
Sketchup::require('db')#数据定义
Sketchup::require('main')#按钮菜单定义
Sketchup::require('browser')#对话框定义
这里讲讲Ruby是如何找到代码文件来加载的,当你require一个文件时,Ruby会依次在$LOAD_PATH(这是Ruby提供的一个全局数组对象)中的路径里去寻找有这个名称的rb文件并执行。所以在加载文件的第一行通过调用$LOAD_PATH的unshift方法将当前的后端文件夹路径放在数组第一的位置,这样按照顺序来进行检索的话,即使其他插件中有和我们相同名称的rb文件也不会加载错误。第3第4行加载并把基础库注入了插件模块,第6行起就是把插件中的rb文件都通过Sketchup::require方法来加载,这是SU提供的,它扩展了Ruby自带的require方法,能够加载加密后的rb文件。
接下来讲讲插件中几个通用的rb文件,他们各自负责了一部分基础功能。
module ShinkQuickNaming
PATH = File.dirname(__FILE__)
module_function
def data_path
"#{DocumentsPath}/sketchup_plugin/#{PluginName}_#{SuVersion}"
end
def log_path(name = '')
"#{data_path}/log/#{name}"
end
def tmp_path(name = '')
"#{data_path}/tmp/#{name}"
end
def small_icon_path(icon_name)
"#{PATH}/icon/#{icon_name}_32.png"
end
def large_icon_path(icon_name)
"#{PATH}/icon/#{icon_name}_64.png"
end
def preset_data_path(file_name)
"#{PATH}/preset_data/#{file_name}"
end
def local_page_path(type)
"#{local_server_path}/index.html?type=#{type}&time=#{Time.now.to_i}"
end
def local_server_path
"http://localhost:#{local_server_port}"
end
def local_server_port
'38586'
end
def document_root_path
if IsDevelop
"#{PATH}/../shink_quick_naming_front/dist"
else
"#{PATH}/dist"
end
end
FileUtils.mkdir_p(log_path)
FileUtils.remove_dir(tmp_path) if Dir.exist?(tmp_path)
FileUtils.mkdir_p(tmp_path)
end
path.rb文件中定义了插件中和路径相关的方法,包括日志路径,图标路径,临时路径等都是在这里定义的。需要注意的是,这里还定义了服务器的地址,其中local_server_port即服务器挂载的端口,尽量选取不会冲突的端口号(端口上限是65535,最好选择五位数的端口号)。document_root_path方法则根据插件的开发状态返回服务器的文件根目录路径。
module ShinkQuickNaming
SuDb.set_section("#{PluginName}_DB")
Config = SuDb::Config.new_by_key('config')
end
db.rb文件中定义了SuDb使用的section(用来划分不同插件存储的数据位置),以及全局会使用到的数据结构,这里定义了一个全局配置对象,用来存贮插件相关的配置,基础库中的有些功能也会使用到这个常量。
module ShinkQuickNaming
module Main
module_function
def show_quick_naming_dialog
if check_browser
@quick_naming_dialog ||= Browser.new("快速命名分层", 'quick_naming')
@quick_naming_dialog.show
end
end
def show_plugin_settings_dialog
if check_browser
@plugin_settings_dialog ||= Browser.new("插件设置", 'plugin_settings')
@plugin_settings_dialog.show
end
end
def check_browser
if ShinkQuickNaming.const_defined?(:IEVersion) && IEVersion < 9
UI.messagebox('您的浏览器版本过低, 请至少升级至IE9')
false
else
true
end
end
end
unless file_loaded?(__FILE__)#保证代码块中代码只执行一次
toolbar = UI.toolbar(PluginName)#创建工具栏
cmd1 = UI::Command.new("Show Quick Naming Dialog") {
Main.show_quick_naming_dialog
}
cmd1.small_icon = small_icon_path('quick_naming')#设置小图标
cmd1.large_icon = large_icon_path('quick_naming')#设置大图标
cmd1.tooltip = '快速命名分层'#设置提示文字
toolbar = toolbar.add_item(cmd1)#添加按钮
cmd99 = UI::Command.new("Show Plugin Settings Dialog") {
Main.show_plugin_settings_dialog
}
cmd99.small_icon = small_icon_path('plugin_settings')
cmd99.large_icon = large_icon_path('plugin_settings')
cmd99.tooltip = '插件设置'
toolbar = toolbar.add_item(cmd99)
toolbar.show#显示工具栏
file_loaded(__FILE__)
end
end
main.rb文件中定义了显示两个窗口的方法,并创建了一个工具栏,设置了两个按钮与窗口调用的关联。其中@quick_naming_dialog ||= Browser.new("快速命名分层", 'quick_naming')这行用到了||=运算符,它的意思是当@quick_naming_dialog为nil时,将一个新建的窗口对象赋给@quick_naming_dialog,当@quick_naming_dialog不为nil时,则不进行操作。因此当这行代码被多次调用时,@quick_naming_dialog只会被赋值一次,这样每次调用这个方法就会打开同一个窗口对象。因为Vue框架需要浏览器版本至少为IE9,check_browser方法会在打开窗口前调用,检查当前的IE浏览器版本。
module ShinkQuickNaming
class Browser < ShinkBrowser
def initialize(title, type)
@type = type
super(title, true, type, 0, 0, 500, 150, true)
set_background_color(get_default_dialog_color)
set_callback
end
def set_callback
add_callback("alert"){|d, param| UI.messagebox(param.message)}
end
def show
if visible?
bring_to_front
else
set_url(index_path)
set_window_min_size_by_type
SuRunJs.set_web_dialog(@type, self)#将窗口类型与窗口实例绑定
case @type
when 'quick_naming'
RefreshEntityObserver.open(@type) do#设置选中对象改变时调用的代码
run_js(@type, "quick_naming.refresh_entity_info()")
end
after_close{ RefreshEntityObserver.close(@type) }#设置窗口关闭时调用的代码
end
super
end
end
def set_window_min_size_by_type
case @type
when 'quick_naming'
set_window_min_size(500, 460)
when 'plugin_settings'
set_window_min_size(1200, 670)
end
end
def index_path
ShinkQuickNaming.local_page_path(@type)
end
end
class RefreshEntityObserver < Sketchup::SelectionObserver
def self.open(type, &block)
@type_observer_hash ||= {}
observer = RefreshEntityObserver.new(&block)
@type_observer_hash[type] = observer
Sketchup.active_model.selection.add_observer(observer)
ModelChangeObserver.open(type){ RefreshEntityObserver.open(type, &block) }
end
def self.close(type)
if @type_observer_hash[type]
Sketchup.active_model.selection.remove_observer(@type_observer_hash[type])
@type_observer_hash[type] = nil
ModelChangeObserver.close(type)
end
end
def initialize(&block);@block = block end
def onSelectionBulkChange(selection);@block.call end
def onSelectionCleared(selection);@block.call end
end
class ModelChangeObserver < Sketchup::AppObserver
def self.open(type, &block)
@type_observer_hash ||= {}
if @type_observer_hash[type].nil?
observer = ModelChangeObserver.new(&block)
@type_observer_hash[type] = observer
Sketchup.add_observer(observer)
end
end
def self.close(type)
if @type_observer_hash[type]
Sketchup.remove_observer(@type_observer_hash[type])
@type_observer_hash[type] = nil
end
end
def initialize(&block);@block = block end
def onNewModel(model);@block.call end
def onOpenModel(model);@block.call end
end
end
browser.rb文件包含了两个部分的内容。第一部分以基础库提供的ShinkBrowser类作为基类定义了窗口类,添加了初始化方法,需要窗口标题与窗口类型两个参数。窗口类型用于区分窗口的作用,它影响到窗口的最小长宽、打开时使用的路径以及作为某些方法的索引参数。其中set_callback方法中设置的都是对前端的回调处理,这里使用的是基类提供的add_callback方法,它与前端的方法一起作用,压缩并优化了传入参数,使得后端可以以对象的形式来操作数据,我们在后面的章节中还会继续根据要实现的功能增加回调。第二部分则定义了两个观察者类,分别用来在选中对象改变与当前打开模型变化时执行预设的回调,用于根据用户的操作来刷新前端页面上的数据。可看到RefreshEntityObserver中有对ModelChangeObserver的调用,这是因为当打开的模型改变时,之前设置的SelectionObserver会无效,所以需要监测这个变化并再次开启SelectionObserver。
在添加了这些代码文件以及两种图标后,打开SU我们就能看到有两个图标的工具栏出现了,现在由于尚未构建前端,点击按钮后出现的窗口还是没有内容的,在下个章节中将重点讲述前端如何构建。