
简介这份 PDF 文档是 SketchUp Ruby API 的中文整理版参考手册面向使用 Ruby 进行 SketchUp 插件与扩展开发的程序员、建模工具开发者及有二次开发需求的设计人员解决官方英文文档查阅不便、API 条目分散难以速查的问题适合具备一定 Ruby 基础、希望从建模操作进阶到脚本自动化的中高级用户。资源包共 1 个文件为单个 PDF整体约 2.67MB篇幅近 274 页按类目逐条编排便于打印或本地全文检索。文档从 App Level Classes 入手依次讲解 Sketchup、Model、AttributeDictionary、Axes、Animation、Camera、Color、ExtensionLicense、Importer、LanguageHandler 等模块覆盖模型创建与管理、属性词典的键值操作与遍历、坐标轴建立、动画播放控制、摄像机视角设置以及扩展授权等常见开发场景每个类给出方法说明与调用要点可作为日常开发的速查索引。目前已有 475 人学习适合作为插件开发入门到实战阶段的案头参考资料。1. 一份 274 页的 SketchUp Ruby API 手册该从哪一页翻起接手的 sketchup插件 想改两行参数解压后全是.rb文件团队要做批量建模、自动出图选型会上绕不开 SketchUp 二次开发。和很多三维软件同时开放多套脚本语言不同SketchUp 官方只认 Ruby没有可视化脚本编辑器这直接决定了插件代码的组织方式也决定了插件作者必须对 API 的对象层级有整体印象。《Sketch Up Ruby API by Sugar》把官方文档整理了 274 页按 App Level Classes、Entity Classes、Collection Classes 三块分开从 Sketchup、Model、AttributeDictionary、Axes、Animation、Camera 一路排到 Tools 这类集合类。它的定位是查询手册而不是教程顺着目录翻比从头读一遍有效得多。真正卡住人的地方从来不是 Ruby 语法本身而是 SketchUp 把“当前编辑上下文”“撤销栈”“属性持久化”这几件事揉进了 api接口 的调用习惯里。这份资料适合已经有 Ruby 基础、能把它当字典查的人完全没碰过 Ruby 的话先补清楚类、模块、块这几个概念再来翻手册会省不少时间。2. Sketchup.active_model 与 App Level 层插件加载和撤销栈怎么写2.1 三层类结构各自的职责边界手册目录的分组本身就是一张架构图。App Level Classes 是应用级的单例和上下文对象Entity Classes 是模型里真实存在的几何对象Collection Classes 则是容纳这些对象的容器。三者的生命周期完全不在一个量级上混用是插件出问题最常见的原因。分层代表类获取方式生命周期App LevelSketchup、Model、Camera、ViewSketchup.active_model、model.active_view随文档开关EntityFace、Edge、Group、ComponentInstance由 Entities 构造返回或遍历得到随几何增删CollectionEntities、Selection、Materials、LayersModel 上的属性随 ModelSketchup本身是模块提供的是模块方法比如Sketchup.active_model、Sketchup.version、Sketchup.status_textModel才是能持有实体和事务的实例对象。刚上手时最容易写错的是active_entities和entities的区别model Sketchup.active_model raise 没有打开的模型 if model.nil? # entities 永远指向最顶层active_entities 会随“进入组或组件编辑”而改变 top model.entities ctx model.active_entities puts 顶层实体 #{top.count} 个当前上下文 #{ctx.count} 个 # 当前正在编辑某个组件时两者不相等 puts 编辑上下文与顶层不同 if top ! ctxmodel在没有打开文档的极端场景下会是nil所以入口处判空是必须的尤其是在启动时自动执行的代码里。active_entities拿到的是当前编辑上下文的实体集合写“把选中的面挤出一层楼板”这类操作时用它是对的写“遍历整个模型统计面积”时用它就会漏掉组件内部的几何。2.2 插件加载入口loader 文件与 SketchupExtension插件目录是固定位置Windows 在%AppData%\SketchUp\SketchUp 20xx\SketchUp\PluginsmacOS 在~/Library/Application Support/SketchUp 20xx/SketchUp/Plugins。常见做法是一个 loader 文件放在 Plugins 根目录负责注册真正的业务代码放进一个同名子目录。# sugar_tools.rb —— 放在 Plugins 根目录文件名即加载入口 require sketchup.rb require extensions.rb ext SketchupExtension.new(Sugar Tools, sugar_tools/main.rb) ext.version 1.0.0 ext.creator sugar ext.description 按标高批量生成楼板并写入房间属性 # 第二个参数为 true表示启动 SketchUp 时默认加载该插件 Sketchup.register_extension(ext, true)SketchupExtension.new的第二个参数是相对 Plugins 目录的主文件路径写成绝对路径在换机器后必然翻车。version只接受字符串写1.0这种浮点数在部分版本上会直接抛类型错误。loader 文件名和子目录名保持一致、只用字母和下划线可以避开旧版本对中文路径的解析问题。2.3 Model 上真正高频的属性翻第 18 页开始的 Model 部分会发现方法很多但日常真正反复用的就那么几个entities决定往哪写几何selection决定用户当前选了谁materials、layers、pages、definitions分别对应材质、图层、场景页和组件定义active_view管视角和出图options和rendering_options则要区分开——前者偏向应用设置后者才是边线显示、剖面填充这类渲染开关。model.selection返回的是Sketchup::Selection本质上是个有序集合。遍历时用each但要批量改动前先判断empty?否则一个空选择会让后续的first返回nil错误会推迟到很远的地方才暴露。2.4 撤销栈start_operation 的位置决定了插件能不能用所有会改动模型的操作都必须包在事务里否则用户在 SketchUp 里按一次 CtrlZ要么撤不掉要么把插件的中间状态一起撤掉留下半成品几何。model Sketchup.active_model model.start_operation(生成楼板, true) # 第二个参数 disable_ui true begin # 一批几何操作集中写在这里 model.commit_operation rescue StandardError e model.abort_operation puts 操作失败已回滚: #{e.message} endstart_operation的完整签名是start_operation(op_name, disable_ui false, next_transparent false, transparent false)。批量生成几百个面时如果不把disable_ui打开界面会在整个过程中不停重绘卡到像假死。另一个坑是嵌套在一次未提交的start_operation里再调一次会抛异常所以事务要尽量放在最外层方法里内层函数只负责算几何不碰事务。3. Entities 建模链路add_face、pushpull 与组件实例的层级关系3.1 点、向量、变换三件套几何部分的所有输入输出都围绕Geom::Point3d、Geom::Vector3d、Geom::Transformation这三个类。坐标可以直接写数组SketchUp 的 API 会自动转换但一旦涉及向量运算就必须显式构造否则cross、dot这类方法根本不存在。ents model.active_entities # 显式构造点避免后续向量运算时报 NoMethodError pts [[0, 0, 0], [3000, 0, 0], [3000, 4000, 0], [0, 4000, 0]].map do |a| Geom::Point3d.new(a[0].mm, a[1].mm, a[2].mm) end face ents.add_face(pts) if face.nil? puts 建面失败点集不共面或存在自相交 else puts 生成面面积 #{face.area.to_mm.round(0)} mm² end长度单位这块要注意SketchUp 内部统一按英寸存储1000.mm这类写法来自sketchup.rb给 Numeric 加的单位扩展直接写裸数字会被当成英寸。做建筑相关的插件时把毫米换算写进输入层是最省事的做法。3.2 共面判断与容差add_face返回nil的绝大多数原因只有一个传入的点不共面。四个点看起来在一个平面上实际坐标里混了浮点误差或者本来就是一组扭曲的四点都会导致建面失败。稳妥的写法是在调用前自己用叉积判一次。# 用叉积判断四点是否共面容差按 1mm 给 def coplanar?(p1, p2, p3, p4, tol 1.mm) n (p2 - p1).cross(p3 - p1) return false if n.length 1e-9 # 前三点共线法向量退化 n.normalize! n.dot(p4 - p1).abs tol endcross是向量叉积结果方向就是平面法向量normalize!原地归一化返回自身dot做点积得到第四点在法向量方向上的投影距离。容差不能给太小SketchUp 内部对共面的判定本身就有一定宽容度插件侧判得比内核还严会出现“我判不共面但手动拉面能拉出来”的尴尬。3.3 从面到体pushpull 的方向与复制拿到Face之后拉出厚度用的是pushpull。距离正负决定方向正值沿面法向负值反向做楼板从上往下沉或者从下往上长全靠这个符号。face.pushpull(-120.mm) # 反向拉伸 120mm常用于楼板下沉 face.pushpull(3000.mm, true) # 第二个参数为 true保留原面并复制结果pushpull在面被其它几何切割时会只影响其中一部分判断依据是调用前后face.area的变化而不是返回值的真假——这个方法在多数情况下没有有意义的返回值。做批量挤出时养成“先记录面积、再调用、再比对”的习惯能提前发现几何被切碎的情况。3.4 Group 与 ComponentInstance先分组再变换把一堆实体打成组入口是Entities#add_group注意它接收的是实体集合而不是单个实体。sel model.selection.to_a group model.active_entities.add_group(sel) # 返回 Sketchup::Group group.name SUGAR_SLAB_L1 # 拿到组句柄后先 make_unique避免改到共享定义 group group.make_unique if group.respond_to?(:make_unique) inner group.entities inner.add_face(...) if false # 组内实体要通过 group.entities 写入直接操作group.entities在旧版本里会改到共享定义导致模型里其它同名组跟着一起变这个 bug 往往要到用户反馈才被发现。我一般拿到组句柄后先调一次make_unique代价可以忽略省掉的排查时间很值。组件那边则完全不同path File.join(__dir__, components, chair.skp) definition model.definitions.load(path) # 加载外部 skp 作为组件定义 definition.name SUGAR_CHAIR model.start_operation(布置椅子, true) begin [[0, 0, 0], [1500, 0, 0], [3000, 0, 0]].each do |p| tr Geom::Transformation.new(Geom::Point3d.new(p[0].mm, p[1].mm, 0)) model.active_entities.add_instance(definition, tr) end model.commit_operation rescue StandardError e model.abort_operation puts 布置失败#{e.message} enddefinitions.load内部会按文件名查重同名定义不会重复加载所以循环里可以放心调用。add_instance(definition, transformation)返回ComponentInstance实例本身没有几何几何在定义里改实例的尺寸要动transformation或者definition。3.5 常用建模方法的失败边界方法返回值常见失败原因Entities#add_faceFace 或 nil点集不共面、自相交、存在重复点Entities#add_lineEdge 或 nil两点重合Entities#add_groupGroup传入空集合或 nilEntities#add_instanceComponentInstance定义加载失败、变换矩阵为 nilFace#pushpull无明确返回值面已失效、被其它几何切割筛选实体时优先用grep比手写is_a?判断更短也更快model.entities.grep(Sketchup::Face)直接拿到所有顶层面。要递归组件内部就得自己写栈式遍历因为entities只覆盖当前这一层。4. AttributeDictionary 写入与 Tool 类拾取交互的完整实现4.1 属性词典的结构与写入方式SketchUp 允许在任意实体上挂多层命名属性词典每层是一组键值对用来存插件自己的业务数据。这套机制比外挂一个 JSON 文件靠谱得多因为数据跟着模型文件走复制、另存、发同事都不会丢。ent model.selection.first dict ent.attribute_dictionary(SUGAR_META, true) # 第二个参数为 true 时不存在就创建 dict[room_no] A-101 dict[level] 3 dict[checked] true # 等价写法少取一次字典 ent.set_attribute(SUGAR_META, area_mm2, ent.area.to_mm.round(0)) # 读取第三个参数是取不到时的默认值 lv ent.get_attribute(SUGAR_META, level, 0) puts 房间 #{dict[room_no]} 位于 #{lv} 层字典名建议全大写下划线和内置的dynamic_attributes以及动态组件系统生成的键区分开。往内置字典里塞同名键轻则被组件系统覆盖重则让动态组件属性面板显示异常。4.2 属性值的类型边界与遍历写入值类型读回类型备注Integer / Float原类型单位是英寸长度要自己换算StringString支持中文注意源文件编码true / falseTrueClassArrayArray元素类型不做递归校验Geom::Point3dGeom::Point3d坐标同样按英寸存其它对象String内部调用to_s最后一行是踩坑重灾区往里塞Time或者自定义对象读出来是一坨字符串反序列化得自己写。遍历和清理的写法如下# attribute_dictionaries 在没有任何字典时返回 nil不是空集合 ent.attribute_dictionaries.each do |d| puts #{d.name} - #{d.keys.inspect} end dict ent.attribute_dictionary(SUGAR_META) dict.delete(level) if dict # 删单个键整本字典的删除要走attribute_dictionaries集合链式调用前必须判空这一点和很多语言里“空集合不为 nil”的直觉相反。4.3 自定义 ToolonLButtonDown 与 PickHelper 拾取交互式插件的骨架是Sketchup::Tools的子类通过model.select_tool挂上去。屏幕点击到模型实体的映射靠View#pick_helper完成。class SugarPickTool def activate Sketchup.status_text 点击模型读取 SUGAR_META end def onLButtonDown(_flags, x, y, view) ph view.pick_helper ph.do_pick(x, y) # 在视图坐标做一次拾取 ent ph.best_picked # 命中的最靠前实体 return if ent.nil? room ent.get_attribute(SUGAR_META, room_no, 未标注) puts 命中 #{ent.typename} / 房间 #{room} view.model.selection.clear view.model.selection.add(ent) end end model.select_tool(SugarPickTool.new)do_pick(x, y)接收的是视图坐标而不是模型坐标这个参数顺序和很多图形库的习惯相反写反了会表现为“点哪都拾取不到”。best_picked返回最上层实体count配合picked_at(index)能拿到完整的拾取深度列表做“穿透选择”功能时用得上。工具结束时调model.select_tool(nil)退回系统默认工具。4.4 工具里的撤销栈与状态同步工具中一次点击就可能改模型事务要开在实际改动之前而不是放在activate里。用户中途按 ESC 会触发onCancel这时候未提交的事务必须回滚。def onLButtonDown(_flags, x, y, view) model view.model model.start_operation(标注房间, true) begin # 写入属性的具体逻辑 model.commit_operation rescue StandardError e model.abort_operation UI.messagebox(写入失败#{e.message}) end end def onCancel(_reason, view) view.model.abort_operation if view.model endonCancel里的abort_operation只在确实存在未提交事务时才安全所以要么加状态标记要么用respond_to?之类的方式兜一层。工具状态和界面提示要同步更新Sketchup.status_text设一次就够反复设置在拖动过程中会拖慢响应。5. Camera 出图、异常兜底与版本差异的速查技巧5.1 View 与 Camera 的批量出图写法做自动出图时镜头要先用坐标定死再截靠手动调整视角没法复现。view model.active_view cam view.camera cam.set(Geom::Point3d.new(8000.mm, -6000.mm, 5000.mm), # 相机位置 Geom::Point3d.new(0, 0, 0), # 目标点 Geom::Vector3d.new(0, 0, 1)) # 朝上方向 cam.perspective true cam.fov 45 view.zoom_extents view.write_image(out/level1.png, 1600, 900, true) # 最后一位开启抗锯齿cam.set的三个参数分别对应眼睛、目标、上方向上方向写反会导致画面倒转。fov只在透视模式下有效正交视图下设置它不报错但也不生效。write_image的文件名要用绝对路径相对路径的基准目录在不同平台上不一致。5.2 异常兜底别让一个插件拖垮整个加载链插件加载时抛出的异常会中断后续加载用户看到的是“另一个插件突然不见了”。主文件入口处统一包一层 rescue 是成本最低的防御手段。# main.rb 里统一兜底避免插件异常影响其它插件加载 begin require File.join(File.dirname(__FILE__), sugar_tools, core) rescue LoadError, StandardError e puts [Sugar Tools] 加载失败: #{e.class}: #{e.message} puts e.backtrace.first(5) end调试阶段把puts输出到 Ruby 控制台Window 菜单下的 Ruby Console就够了比UI.messagebox好用因为消息不会阻塞界面。要区分的是e.class和e.messageLoadError多半是路径写错或文件缺失NoMethodError一般是实体已经被其它操作删掉了这时候先判断valid?再操作更稳妥。5.3 版本差异与批量删除的正确姿势变更点旧写法新写法影响图层语义model.layers下默认图层名Layer0model.tagsUI 显示为 Tag2020 后 API 两者都在混杂使用会漏改渲染选项model.rendering_options同名词典部分键名随版本调整键名写错不报错静默失效渲染选项这块要特别注意键名拼错不会有任何异常只是设置不生效排查时优先怀疑键名而不是逻辑。最后是一个批量处理的通用技巧# 错误示范边遍历边删迭代器会失效 model.entities.each { |e| e.erase! if e.is_a?(Sketchup::Edge) } # 正确做法先取快照再统一处理 targets model.entities.to_a.select do |e| e.is_a?(Sketchup::Edge) e.length 1.mm end model.start_operation(清理碎边, true) targets.each { |e| e.erase! if e.valid? } model.commit_operationvalid?这一步不能省删掉一条边之后与它相连的孤立面会被内核连带清除快照里排在前面的实体可能已经不存在了这时候再调erase!会直接抛异常。把valid?判断和start_operation配合使用几百个实体的批量清理可以稳定跑完而不留残渣。本文还有配套的精品资源点击获取