【SketchUp图元的族谱】包含与嵌套
一、引入:图元族谱
SketchUp 通 过组件或者群组的方式可以将图元打包在一起,以便隔离编辑和管理图元。 组件和群组可 以任意深度地嵌套,实现复杂的模型逻辑。 一个组件内部可以有其他组件,除了不能在组件或子组件中插入其本身,基本没有层数上的限制。
就像管理目录的窗口中呈现的那样,组件的嵌套可以描述成一个树状的目录结构,不存在环形的结构。其实不止组件,SketchUp 中的所有图元都被囊括在这个体系之中,树状结构的根节点对应的就是整个模型(Model),模型中包含最浅表的图元,即未打组的边线、平面、标注等图元,当然也包括组件和群组。而从组件和群组往下展开,是其包含的下一层的图元。这样一层一层的树状结构就像族谱一样,所以这一系列的名称就叫 SketchUp 图元的族谱。
由于组件的复用特点,整个模型并不是一个简单的树状结构,而是每一个组件定义独立的一个树结构,如果组件中包含其他组件,会存储为组件实例(Component Instance),树状结构也就到此为止。如果需要往下查找下一层,则需要通过这里“树枝”上的“组件实例”,找到对应的组件定义(Component Definition)作为“树根”查找它所包含的图元。如下图所呈现的组件树状结构,第一层模型中有五个图元,其中两个图元有下一层的子图元,如果两个图元是群组或不相同的组件,则树状结构相对简单,各自存储其子图元即可。如果两个图元是相同的组件,则他们共用一个组件定义,所以虽然三维视图中显示有两个“一样的组件”,但实际只存储了一个组件定义,修改其中任何一个组件实例,另一个也会一并修改,可以认为两个实例都是同一个组件定义的“影子”。
简易的组件嵌套关系及对应的组件、群组两种情况
这个系列文章所关注的就是以上这种共用定义的现象,目前计划了三个章节,分别是“包含与嵌套”、“对影成三人”和“附庸的附庸”。原谅我在标题上打了个哑谜,具体来说:“包含与嵌套”关注图元是如何存储在组件中,又是如何在多层嵌套中定位的;“对影成三人”关注修改一个组件实例后,整个模型中会有多少处修改,分别在哪里修改;“附庸的附庸”则关注一个组件实例往下每一层的图元都在具体哪个位置上。
本系列文章关注的三种嵌套问题
二、包含
当说一个组件包含某个图元时,实际上是说该组件的组件定义的图元容器中包含了这个图元。用 ruby 代码来表示图元 a 在组件实例 b 中可以写作:
b.definition.entities.include?(a)
如果需要在组件 b 中新建一个边线图元,则可以通过组件定义的 .entities 方法访问相应的图元容器,从而新建图元:
# 组件实例 b 中新绘制一条从组件坐标原点向上长度为1的边线
b.definition.entities.add_line([0,0,0],[0,0,1])
这种包含关系是基于组件定义的,而非基于组件实例。正如通过用户界面打开一个组件,并在其内部改动具体的图形,其修改都会更新到所有组件实例中,无论是从哪一个组件实例修改图元,都会应用到每一个组件实例,就像以下代码所呈现的。
# 在当前模型中仅保留复制的三个立方体组件
ins_1 = Sketchup.active_model.entities[0]
ins_2 = Sketchup.active_model.entities[1]
ins_3 = Sketchup.active_model.entities[2]
line = ins_1.definition.entities.add_line([0,0,0],[0,0,100])
p ins_3.definition.entities.include?(line)
#=> true
可以发现,在组件实例 ins_1 中增加了一条边线后,三个组件实例都更新出了边线,同时在代码中的判断也证实了,组件实例 ins_3 内部也包含了刚创建的边线 line。
|
|
修改内部图形前 | 修改内部图形后 |
上述例子中的编辑图元方式已经包含了一个获取组件定义的过程,无论是修改 ins_1、 ins_2 还是 ins_3,都要首先通过 .definition 方法获取其组件定义,而三个组件实例共用同一个定义,因此无论选取哪一个组件实例,都是在修改同一个组件定义。以下代码对比了三个组件实例的定义,结果表明三个实例共用一个定义:
p ins_1.definition == ins_2.definition
#=> true
p ins_1.definition == ins_3.definition
#=> true
类似的效果同样适用于移动、删除和其他形式的图元修改。例如以下代码将组件中所有图元向上平移了 100cm。
# 在当前模型中仅保留复制的三个立方体组件
ins_1 = Sketchup.active_model.entities[0]
ins_2 = Sketchup.active_model.entities[1]
ins_3 = Sketchup.active_model.entities[2]
all_ents = ins_1.definition.entities
all_ents.transform_entities([0, 0, 100.cm], all_ents.to_a)
其中第 5 行定义一个变量 all_ents 用于指代组件定义的图元容器,第 6 行通过 Entities 类的 .transform_entities 方法,对图元容器中的所有图元进行平移操作。其中,第 1 个参数 [0, 0, 100.cm] 表示沿 z 轴正方向平移100cm(这里包含了一个隐藏的类型转换,将向量转换成了平移变换对象,详细介绍见[SU-R08]),第 2 个参数中的 .to_a 表示将容器中的图元转换为一个数组,即容器中的全部图元。
|
|
修改内部图形前 | 修改内部图形后 |
可以看到,平移了组件定义内部的图元,三个实例都表现出平移的效果。在以上例子中,如果不在组件定义内部平移图元,而是直接平移三个组件实例,也能达到一样的效果,但是对于模型来说是有区别的。以下通过代码的方式直接移动三个组件实例:
# 在当前模型中仅保留复制的三个立方体组件
ins_1 = Sketchup.active_model.entities[0]
ins_2 = Sketchup.active_model.entities[1]
ins_3 = Sketchup.active_model.entities[2]
Sketchup.active_model.entities.transform_entities([0,0,100.cm],[ins_1,ins_2,ins_3])
执行代码后可以发现,结果在视觉效果上与之前定义内平移图元是相同的。但是当双击组件进入内部时,会发现两种情况的组件坐标轴不一样:组件定义内平移时,组件内部的坐标原点没有变化,内部图元与组件坐标原点发生了平移;直接平移组件实例时,组件内部图元和组件内的坐标原点之间没有发生平移,而组件坐标原点在上一层的坐标系中发生了平移。这两种平移方式引出了组件内外坐标系的区分。
在组件定义中平移 | 直接移动组件实例 |
如前所述,组件内的图元是存储在组件定义之中,其所包含的图元无论有多少个组件实例,分别位于什么地方,不影响内部图元使用固定的坐标系,即组件坐标系来存储图元空间位置;组件上一层用来描述组件实例所在位置的坐标系为父坐标系;最表层的、不属于任何组件的图元(包括最表层的组件实例)所使用的坐标系是整个模型(Model)使用的坐标系,称为世界坐标系。
需要注意的是,世界坐标系在模型中是唯一的,组件坐标系对于一个组件定义来说也是唯一的;而父坐标系对于一个组件定义来说是动态的、不确定的,具体一个组件定义的每一个组件实例都有自身所处的位置,也就拥有各自的父坐标系。
三、嵌套
在多层的组件嵌套结构中,最末端的图元通过每一层组件实例的位置信息依次计算确定位置。具体来说,一个组件内的图元在模型中的位置可以表示为组件坐标系的位置和组件实例在父坐标系中的位置的组合,如果父坐标系不是世界坐标系,则可以重复这一过程,直到追溯到最上层的世界坐标系。
将组件坐标系转换为世界坐标系
组件内的一个图元,其组件坐标系中的位置与其在父坐标系的位置可以用一个变换(Transformation)来记录,即组件内的坐标经过一次坐标变换得到相应的父坐标系坐标(组件内图元位置的保存及其在世界坐标系中坐标的计算详解[SU-2021-10])。以下代码案例有助于理解组件嵌套中的坐标变换:
# 选中一个内部有图元的组件实例
# 并确保不进入编辑任何一个组件
sels = Sketchup.active_model.selection
ins = sels[0]
# 获取组件内第一个图元的第一个顶点坐标
p1 = ins.definition.entities[0].vertices[0].position
p p1
#=> Point3d(0, 64.9606, 0)
p ins.transformation * p1
#=> Point3d(144.208, 210.416, 0)
对于以上代码,其中的 ins 指向模型中唯一的一个组件实例,变量 p1 表示该组件内部的某一个端点的空间位置。对于组件内的端点,此处使用 .position 方法返回的是组件坐标系下的坐标,即上图中的 p。输出坐标可知,在组件坐标系中端点坐标为 [0, 64.96, 0];在该坐标基础上追加一个组件的位置信息 ins .transformation,即上图中的 T,即可得到 p' 的值,具体是 [144.208, 210.416, 0]。由下图可知组件实例的位置可以表示为在 x 轴和 y 轴上平移一段距离,所以组件坐标系下的坐标与世界坐标系坐标之间正好相差平移的距离。
上述例子只涉及组件的平移,如果还有旋转、缩放等位置变化,组件内外坐标的转换前后的数值就不会那么直观了,但都是 .transformation 和 .position 的组合计算结果。
使用 ruby 脚本在组件或群组内创建图元时,既可以使用组件坐标系,也可以使用世界坐标系,分别对应两种不同的方法。使用组件坐标系的方法,即前文案例中使用的方法,通过组件定义的 .entities 方法返回定义内的图元容器,再具体使用诸如 .add_line 的创建图元:
# 第一种方法
# 选中一个内部有图元的组件实例
# 并确保不进入编辑任何一个组件
sels = Sketchup.active_model.selection
ins = sels[0]
ins.definition.entities.add_line([0,0,0],[0,0,100])
在组件内使用世界坐标系创建图元,则需要配合 SketchUp 的编辑界面操作,通过双击组件进入组件内部,从而编辑组件内的图元。这种方法需要通过当前模型( Sketchup .active_model )的 .active_entities 方法实现,此方法返回编辑界面当前正在编辑的图元容器,如果双击编辑某个组件实例,那么此时这个在编辑的组件内图元的坐标系与世界坐标系一致。具体案例如下:
# 第二种方法
# 在编辑窗口双击组件实例进入其内部
aes = Sketchup.active_model.active_entities
aes.add_line([0,0,0],[0,0,100])
第二种方法与第一种方法使用相同的坐标,不同之处在于它根据世界坐标创建图元。这是因为进入组件内部编辑时,对处在不同嵌套位置的图元的坐标计算是不同的,对于正在编辑的组件的定义以及上层组件的定义内图元,坐标会按照世界坐标系来处理,其余情况坐标会按照组件坐标系来处理。是一个相对比较复杂的情况分析,也因此前文案例中会在注释中要求“ 确保不进入编辑任何一个组件 ”。通过ruby代码可以知晓目前正在编辑哪一个组件,也可以将编辑界面修改到其他嵌套层次,这就涉及到具体组件实例路径(InstancePath)的问题,是之后两篇内容需要专门考虑的概念了。
(完)
从SU-2022-01期开始,本人的 SketchUp Ruby “小功能与灵感” 的文章中涉及的功能会在 Gitee/GitHub 中相应更新。这样一来,如果日后发现存在bug需要修正或者是想追加相关的功能,都有极大的便利。
这个代码仓库同样也包含大部分之前文章的代码,可以访问以下网址查看,也欢迎大家共同提交修改:
https://gitee.com/apiglio/sketchup-script-tool
https://github.com/Apiglio/SketchupScriptTool
之前更新的教程到组件部分之后一直就没有再更新,总觉得需要一个比较好的讲述逻辑,又希望能够统一组件、群组和图像三种图元,又要兼顾组件定义和定义列表这两个实体,所以总显得杂乱无章。所以打算先在这个系列大概聊一聊组件的一些特点,也是给详细的组件教程进行一个预热。
SketchUp ruby的中文资料相对较少,并且相对小众,欢迎加入 SketchUp Ruby 自学俱乐部 Q群(群号:368842817)。
本文编号:SU-2023-02