【如何将SU组件玩出花?】原位置替换
共 8738字,需浏览 18分钟
·
2021-11-23 20:26
当修改 SketchUp 模型其中一个组件内部的内容时,模型中其他组件也会相应地更新修改,这使得很多重复的建模操作得以避免。而替换组件的功能可以在不改变组件位置和方向的情况下对组件进行替换,对于模型的版本更新和团队合作来说,有特殊的意义。SU 自带的组件替换方式是:选定要替换的组件,而后在“组件”窗口中找到需要替换的组件类型(组件定义),右键单击选择“替换选定项”。
这个操作完全等同于以下一段代码:
d=Sketchup.active_model.definitions["3D Printer Build Volume"]
sels=Sketchup.active_model.selection
sels.grep(Sketchup::ComponentInstance).each{|ins|
ins.definition=d
}
#此段代码仅用于说明替换组件的原理
#实际测试时需要先在当前模型中导入“3D打印机构造体积”这一组件
#否则将找不到此组件定义
其中,最核心的是第4行的 .definition= 方法,就是通过此方法将选定的所有组件依次替换的。而替换的依据来自第1行声明的 d,其在此处表示模型中名为 "3D Printer Build Volume" 的组件定义。
不过这种替换是非常粗糙的,如果替换前后的组件在建模过程中存在继承关系(例如其中一个是另一个组件“设定为唯一”之后稍微编辑细节的组件),那么替换的效果还不错;如果是尺寸存在差异的两个模型,那么这种替换有时就不那么尽如人意了:
这样的替换结果可以保证没有缩放过的组件在替换后,呈现的效果就是组件设计时的尺寸,这样能够保证模型尺寸和长宽高比例的真实性。但是作为“草图大师”,有时替换前后的模型尺寸差异不大,且只需要一个大概的视觉感受,这时替换组件需要额外变形以适应实际的需求,就比较麻烦了。
那么,能不能在替换组件时,保留原有组件的尺寸边界和方向?答案是肯定的,可以通过 SU Ruby 代码来实现。以下视频为效果展示:
为了实现视频中的功能,首先需要了解 SketchUp 的组件是如何定位和替换的,完整的代码实现在本文的第三部分。
【本期目录】 | |
一、组件的定位 (1) 组件定义与实例 (2) 组件内的坐标轴 (3) 组件位置的定义 | 二、原位置替换的原理 (1) 变换过程的组合 (2) 变换的因式分解 三、代码实现 |
一、组件的定位
(1) 组件定义与实例
SketchUp 的组件(Component)具体可以分为两个部分:一个是组件的定义(ComponentDefinition),用于储存组件中的图元。另一个是组件的实例(ComponentInstance),用于储存具体一个组件的空间位置和其他诸如图层、材质的属性。
复制一个组件后,会得到两个组件的实例(ComponentInstance),而两个实例同时关联在原有的组件定义(ComponentDefinition)上,因此复制前后,组件定义的数量没有变化。但是如果右键其中一个组件实例,并选择“设定为唯一”,就会新建一个相同的定义,并将选中的实例关联给这个新的定义(如下图)。
每一个组件定义都可以理解成一个模型(Model),这一点可以从图元类 .parent 方法中寻得一些端倪。这个方法的返回值是图元的“父节点”,可能存在 Model 类或者 ComponentDefinition 类的返回值。SU 模型中任何一个图元都存在一个父节点,可以是代表“根节点”的 Model 类(用Sketchup.active_model调用),也可以是组件定义 ComponentDefinition。
编辑组件定义中的图元,所有与之关联的组件实例的图元都会据此更新显示,可以尝试以下代码:
# 选择一个组件(选择的是组件实例)
sels=Sketchup.active_model.selection
sels[0].definition.entities.grep(Sketchup::Face).each{|ent|
ent.erase!
}
以上代码中, sels[0] 表示选中的图元,也就是组件的实例。使用 .definition 方法获得选中的组件的定义,而后通过 .entities 方法调用定义中的图元列表。在其后通过 .grep 方法加 .each 迭代器删除组件定义中的全部平面图元。
总之,组件分为定义和实例两个部分。当我们说点击或者选择组件时,所说的组件指的是组件的实例;而当我们双击实例打开组件编辑器后,编辑的是组件定义内的图元。
(2) 组件内的坐标轴
双击编辑一个组件,可以发现其有自己的坐标轴,这个坐标轴通常与模型的坐标轴位置不同。例如以下两个两张对比图中,前后两个组件有不相同的空间位置,但是其左下角的顶点都是组件定义内坐标轴的原点。这两个顶点在模型中有不同的坐标,但是其在组件定义中的坐标均为 (0, 0, 0)。
模型内 | 组编辑器内 |
模型中的坐标轴 | 组件定义内的坐标轴 |
所以,一个组件实例中的图元位置是由实例各自的坐标轴和定义内的相对位置两个部分计算而成。如果选中模型中的图元创建的组件实例,其坐标轴与模型坐标轴相同,而坐标原点为模型的最小点(即 BoundingBox #min,参见教程 [SU-R07])。
模型中的立方体 | 创建组件后 |
最小点在原点 | 定义原点与模型原点重合 |
最小点不在原点 | 定义原点与模型原点不重合 |
如果只考虑平移和轴向的缩放,组件原位替换将是一个很轻松的过程。只需要用 .bounds 方法获得替换前后组件实例的外接轴向立方体范围(BoundingBox)长宽高的比例关系,进行相应的轴向缩放即可。但是这种情况太过于特殊,多数情况不适用。毕竟只要涉及旋转,只接受轴方向范围的 BoundingBox 就完全无法表示组件的位置:
如上图,一旦涉及旋转,贸然固定长宽高范围还可能导致组件出现非正交变换(即两条线的夹角在变换前后可能不一致),组件的形状发生扭曲,这在绝大多数建模任务中是不能接受的。因此我们至少需要获得的是组件定义中的 BoundingBox 位置(下图右侧的蓝色立方体)。下文中出现这个范围,将用 EBCD 来代称,表示“组件定义内图元的范围(Entities BoundingBox of Component Definition)”。
获取 EBCD 的位置信息,需要其相对于原点的距离和方向、其自身的缩放情况和旋转情况,具体来说需要:
①模型坐标轴原点与组件坐标轴原点的相对位置关系 (x, y, z);
②模型坐标轴方向上的缩放比例 (w, h, d) ;
③空间旋转角度 (φ, θ)。
但是这些信息太过于冗杂,几组截然不同的数据组合可能代表的是完全相同的位置,因此 SketchUp 不会采用这种方式来定位组件位置,取而代之的是一个变换实例(Geom:: Transformation),其本质是一个4×4的齐次变换矩阵。
(3) 组件位置的定义
有关变换类和齐次变换矩阵的概念,在 SU Ruby 教程系列中的相关篇目 [SU-R08] 中已经进行了比较详尽的介绍了,此处不再赘述。一言以蔽之就是:一个变换类(Geom:: Transformation)实例代表一个变换操作,它可以表示一切平移、旋转和缩放变换(缩放变换包括对称变换)及其组合。
对于组件而言,一个变换可以用来表示组件定义坐标系内一点坐标到组件实例所在坐标系点坐标的转换。
上图中红色的公式就分别表示两个组件实例的位置,其右侧分别为齐次变换矩阵的形式。
对于一个具体的组件实例,其变换可以通过 .transformation 方法访问,可以通过 .to_a 方法获得其矩阵形式。试着选择几个组件实例,而后执行以下代码:
# 选择多个组件(选择的是组件实例)
sels=Sketchup.active_model.selection
sels.each{|ent|p ent.transformation.to_a}
就可以得到以下效果:
以上的组件是通过平移复制生成的,因此可以明显发现每一个组件实例的第13~15个数值正好是该图元其中一个顶点的坐标。这是因为这个顶点正好是组件定义中的坐标原点,即 .definition .entities 的坐标原点。
平移的情况显然过于特殊,如果加上旋转,这16个数值就没有那么容易理解了,这也足以见包含旋转的变换之复杂:
通过 .transformation= 方法可以将一个变换所代表的空间位置赋予具体的组件实例,这也就是组件位置更改的方法。例如以下例子中,将所有组件的位置都改成了第一个组件的位置,因此五个实例重合在了一起:
如果将一个位置为 t_ori 的组件进行依次进行 tt1 和 tt2 两个变换,变换后的组件位置为 t_new。那么会存在 tt2 * tt1 * t_ori = t_new 的关系,由此可见变换的先后反映在乘法公式中为从内至外(应用的变换始终在乘号的左边),并且组件自身的 t_ori 最先变换。可以通过以下代码自行测试:
tt1=Geom::Transformation.rotation([0,0,0],[0,0,1],15.degrees)
tt2=Geom::Transformation.scaling([0,0,100],1.2,1.5,2.0)
sels=Sketchup.active_model.selection
t_ori=sels[0].transformation
sels[0].transform!(tt1)
sels[0].transform!(tt2)
t_new=sels[0].transformation
(tt2*tt1*t_ori).to_a.map{|i|i.round(6)}==t_new.to_a.map{|i|i.round(6)}
#>> true
以上代码中第1行创建了一个绕z轴正半轴旋转15°的变换实例,第2行创建了一个以 (0, 0, 100) 为中心的非等比例缩放,并通过第5、6行将两个变换依次应用在选中的第一个图元(即下图中的组件实例)上,效果如下:
旋转变换前,位置为t_ori | 旋转变换后,位置为t_new |
最后一行代码中的 .to_a .map{|i|i .round( 6)} 表示将4×4齐次矩阵的每一个元素值保留6位小数[注]。
由此可见,一个组件变换前后的位置关系就是一个变换组合计算而已,这就体现了使用变换类作为组件定位数据形式的优点:尽管对于一些特殊点而言不那么直观,却有着极简洁的数学表达。
下文中,我们将像对待矩阵那样,用粗体大写字母来表示一个组件的位置;即 .transformation 方法的返回值,亦即一个变换类实例(Geom:: Transformation)。另外,单位变换用 O 表示,而不是按照习惯记作 E,符号 O 也不表示习惯中的零矩阵。
二、原位置替换的原理
在明确了组件定位数据的形式后,就可以具体计划如何实现组件的原位置替换了。由于不同组件定义中的图元可能有不同的大小尺度,因此需要对替换前后的组件实例进行缩放。而前文已经说明了涉及旋转变换以后不能使用简单的范围来判断缩放,因此需要对组件经历的旋转操作进行还原。
(1) 变换过程的组合
一般而言,组件的位置是通过缩放、旋转和平移三类变换组合而成的。因此,代表组件位置的变换类可以视为是这三个变换类的组合。
在下文的描述中,会使用 T 表示平移变换, R 表示旋转变换, S 表示缩放变换。如果将其中的 R 限定为绕坐标原点的旋转, S 限定为以坐标原点为中心的缩放,那么 T R S 的矩阵形式将会比较简洁。这样一来,一个组件的位置 P 可以表述为 T * R * S,等同于为先缩放、再旋转、然后平移。
这么做的目的是为了了解 EBCD (即上文的“组件定义内图元的范围”)的变换情况,因为我们需要让一个组件在任意一个位置上根据定义替换前后的大小进行缩放变换,因此 S 需要单独分离出来,并且要在第一步就先完成这个变换。为了形象的说明这个过程,这需要用以下这个树形结构来描述:
一个组件在原位置替换前后,其位置分别为 TRS1O 和 TRS2O,二者在旋转和平移部分是完全相同的(因为不会导致组件形状的变化,因此称为刚体变换);由于组件定义中图元大小和形状的不同,其缩放变换是不相同的,即 S1 和 S2 的区别;O 表示单位变换对象(即无变换)。缩放变换 S 就是典型的非刚体变换。而在替换定义后保持组件定义中图元的范围(EBCD)的前后一致,需要建立替换前后 S1 和 S2 的联系,通过变换矩阵的除法(即与逆矩阵相乘)可以得到 S1O 和 S2O 两个位置之间的联系,即变换 S_1^{-1}·S_2。通过此变换就可以在组件定义实际大小不相同的情况下,保证定义替换前后 EBCD 的空间位置相同。
(2) 变换的因式分解
根据上文变换过程的组合,可以对任意一个组件位置(Geom:: Transformation)进行组合的逆推,由于变换组合为乘法,因此很自然地,这种逆向的拆分可以称为因式分解。
因式分解的思路是按照上文变换组合的规则逆序分解出缩放、旋转和平移三个变换,每一次分离出一个因子后,用因子的逆矩阵乘原矩阵以达到“除法求商”的目的。
将给定的组件位置记为 P,之后首先析出平移变换 T。由于 SketchUp 中的变换是仿射变换可以分解成一个线性变换和一个平移变换,因此余下的变换不再存在平移变换,原点坐标将保持不变。所以只需要以齐次变换矩阵中的平移分块 v = (n, p, q) 表示的向量作为平移依据创建新的变换即可。
表述起来就是 P = TRSO,将 T 析出后 P' = T^{-1}·P = RSO。此时剩余的变换 P' 为线性变换,即满足以上公式中 v=0 且 p=0。
接下来是旋转变换的析出。旋转变换的析出稍微复杂,需要通过轴变换实现。通过计算x轴和y轴通过 P' 变换后的向量重新确定一个三轴相互垂直的结果。将三个轴向量标准化后使用轴变换创建旋转变换 R。于是有P'' = R^{-1}·P' = SO。
这里没有直接将x轴、y轴和z轴的变换后向量直接进行轴变换定义是因为线性变换后的轴向量间的角度并不一定保持不变,旋转变换属于正交变换,因此轴向量之间的夹角应保持不变。因此为了最大程度地适应非正交变换的情况,需要对三个轴并非两两垂直的情况进行方向的取舍。此处的处理方式是:保留x轴变换后方向,在此基础上保留xy平面方向,z轴变换后方向始终与xy平面垂直。
再之后就是析出缩放变换。缩放变换采用的是轴向的缩放,这是为了和 EBCD 更好地协调,因此其矩阵形式为主对角线不为零其他元素均为 0。类似于平移变换的计算,缩放变换也只需要“摘抄数字”即可。于是摘抄剩余变换的主对角线作为 S,而后有 P''' = S^{-1}·P'' = O。
但是实际情况通常剩余的结果 P''' 并不为单位变换 O,这是因为以上的因式分解规则是基于正交变换的特点设计的,对于轴方向角度的变异在计算旋转变换时会保留在剩余变换中。所以我们将 P''' 用 E 表示,表示 “Extra” 剩余或 “Error” 错误(并不如习惯那样用于表示单位矩阵)。
因此之前的因式分解公式可以修改成 P = TRSE,最后的 E 可以理解成是一个“余数”,当 E 为单位阵时表示 P 为正交变换。
三、代码实现
以下是完整的代码实现过程,必要的解释在注释中:
module Geom
class Transformation
def to_trse
tmp=Transformation.new(self)
t=Geom::Transformation.new([1,0,0,0,0,1,0,0,0,0,1,0]+tmp.to_a[12..15])
tmp=t.inverse*tmp
#分解出平移分量,并从变换组合中移除此变换
xx=tmp.xaxis #获取剩余变换后的x轴
yy=tmp.yaxis #获取剩余变换后的y轴
xx.length=1 #标准化x轴方向长度
yy.length=1 #标准化y轴方向长度
unless xx.perpendicular?(yy) then
xtmp=xx
xtmp.length=yy.dot(xx)
yy=yy-xtmp
end
#在xy平面中重新确定y轴方向以保证其与x轴垂直
zz=xx.cross(yy) #计算垂直于xy平面的z轴方向
zz.length=1 #标准化z轴方向长度
#这里没有判断轴方向是否符合右手系
r=Geom::Transformation.axes(tmp.origin,xx,yy,zz)
#分解出旋转分量
tmp=r.inverse*tmp #移除旋转分量
arr=tmp.to_a
s=Geom::Transformation.new([arr[0],0,0,0,0,arr[5],0,0,0,0,arr[10],0,0,0,0,arr[15]])
#分解出轴线缩放分量
tmp=s.inverse*tmp
#剩余的变换直接作为最后一个参数输出
#若为正交变换则tmp.identity?为真。
return [t,r,s,tmp]
end
end
end
def replace_defintion_bb(aIns,aDef)
#保持aIns的BoundingBox属性替换定义为aDef
raise ArgumentError unless aIns.is_a?(Sketchup::ComponentInstance)
raise ArgumentError unless aDef.is_a?(Sketchup::ComponentDefinition)
#判断参数类型是否正确
Sketchup.active_model.start_operation("ReplaceDef_BB",true)
#开始一个新的可撤销操作,用于撤销
b1=aIns.definition.bounds
b2=aDef.bounds
sw=b1.width/b2.width
sh=b1.height/b2.height
sd=b1.depth/b2.depth
#获取定义内图元范围的尺度比例
#即替换前后定义的EBCD的长高宽比例
ts=Geom::Transformation.scaling(sw,sh,sd) #计算用于调整组件大小的缩放变换
trse=aIns.transformation.to_trse #因式分解组件实例的位置
aIns.definition=aDef #替换组件实例的定义
aIns.transformation=trse[0]*trse[1]*trse[2]*trse[3]*ts
#将新的组合变换赋值给组件实例
Sketchup.active_model.commit_operation
#提交可撤销操作,用于撤销
end
以上定义的 to_trse 方法直接追加在 Geom:: Transformation 类之中,能够更方便地对变换类实例进行操作,而 replace_defintion_bb 方法是原位置替换的核心,通过对组件实例位置的因式分解和替换前后组件定义内图元的大小变化,计算出替换定义之后的组件实例新的位置,以实现图形本身的空间范围不变。具体的使用方式如下:
sels=Sketchup.active_model.selection
#仅选择需要替换的目标组件
dt=sels[0].definition
#此时dt表示的就是替换的目标组件定义
#选中需要原位置替换的组件,可以选择多个
sels.grep(Sketchup::ComponentInstance).each{|ins|
replace_defintion_bb(ins,dt)
}
(完)
注:保留6位小数是为了忽略变换矩阵计算的误差。这是因为例子中的 15° 旋转变换的矩阵元素中存在无理数,用有限的数位无法准确表述。当然通常情况下这个误差很小以至于难以被察觉,但是如果使用代码比较两个矩阵就有可能因为微小的差别而得出不相等的结果。
本篇为了解释组件替换前后的位置信息,导致原理部分的篇幅有点过长了,尤其是 SU Ruby 教程还未进行到组件部分,所以无法对组件定位的内容进行引用和回避,因此本篇甚至可以认为是教程系列中关于组件的篇目的一个引子了,之后教程会尽可能再精炼一些。
本文编号:SU-2021-10