本篇文章介绍如何使用Ruby枚举SU模型中的图元,并且提供一个自定义的统计方式。全文分为三个部分,分别介绍这个功能的使用场景、实现这些功能的原理和功能实现的代码。
第一部分:图元枚举与统计的使用场合
①枚举
在建模过程中可能需要对所有图元进行判断,并且进行某些操作,例如删除、隐藏或者是其他修改。以隐藏举例,这些需求可以是翻转所有平面、隐藏所有垂直于地面的边线或者隐藏所有文字标注。
(左侧为原始状态,右侧为脚本执行后状态)
我们知道可以通过使用Sketchup::Entities类中的迭代器“.each”对模型的图元进行迭代枚举,所以以上效果是通过如下的代码来实现的:
#翻转所有平面Sketchup.active_model.entities.each{|ent| if ent.typename=="Face" then ent.reverse! end}#隐藏所有垂直于地面的边线Sketchup.active_model.entities.each{|ent| if ent.typename=="Edge" then if ent.line.line[1].parallel? [0,0,1] then ent.visible=false end end}#隐藏所有文字标注Sketchup.active_model.entities.each{|ent| if ent.typename=="Text" then ent.visible=false end}
以上代码中,“.each”方法是ruby中容器类的默认枚举方式,其重要程度实际上远超于for、while这样经典的循环结构。但是以上方法在涉及组件和群组内的图元时就不能有效地枚举,因此需要有一个更好的方式来枚举所有组件(以及群组)内外的所有图元。
②统计
除了对模型进行批量处理,有的时候也需要对模型的图元进行统计。这个功能在建模编辑的过程实用性不及之前的枚举功能,但是在检查模型和管理模型的过程中是一个不错的工具。
这里的统计不仅仅是图元总数的统计,而是更为详细的统计方法,包括图层、图元类型、定义名称材质选择等各种图元属性的统计。
(统计效果1:组件名称统计)
(统计效果2:带结果筛选的组件名称统计)
(统计效果3:图层情况和显隐情况统计)
第二部分:枚举和统计组件的细节或难点
①枚举的细节或难点
回顾之前第一部分翻转面的代码:
#翻转所有平面Sketchup.active_model.entities.each{|ent| if ent.typename=="Face" then ent.reverse! end}#这段还可以有其他更简洁的表述,见文末
这部分代码可以将当前打开的SU模型中所有未打组的图元进行一定的筛选,并翻转所有的面图元,但是对于组件或者群组内的图元则不能翻转(如下图的效果一般)。这是因为“Sketchup .active_model .entities”返回的Entities类作为一个迭代的对象,被认为是一个一维的数组,数组中包括相同地位的边线、平面、文字标注、群组和组件实例。所以在迭代循环的过程中,其中的迭代变量“ent”可能会是Sketchup::Edge、Sketchup::Face或是其他类的对象,群组内的平面并不在这个数组之中。
(组件和群组中的平面并没有翻转)
如果需要让群组内的平面翻转,则需要使用如下的代码:
grp = Sketchup.active_model.selection[0]#这里需要在仅选择有一个群组之后才会执行以下if之内的内容#grp表示的就是唯一被选择的图元,即“第0个被选图元”#此时grp.class == Sketchup::Groupgrp.entities.each{|sub_ent| if sub_ent.typename=="Face" then sub_ent.reverse! end}
如果需要让某个组件中的平面翻转,则需要使用如下的代码:
cpn = Sketchup.active_model.selection[0]#这里需要在仅选择有一个群组之后才会执行以下if之内的内容#cpn表示的就是唯一被选择的图元,即“第0个被选图元”#此时cpn.class == Sketchup::ComponentInstancecpn.definition.entities.each{|sub_ent| if sub_ent.typename=="Face" then sub_ent.reverse! end}
访问群组和组件中的图元所用的方法似乎略有区别,在这里需要单独说明一下,其中组件实例的图元访问一定要通过对应的组件定义实现,这部分内容在上一篇“【如何将SU组件玩出花?】合并和更新组件”中已经提到。群组虽然每一个相互独立,但是它们的定义一样由“组件定义类”来实现。只不过通常情况下一个群组对应一个组件定义。所以群组不同于组件实例,它有两种访问实体的方法。
grp.entities#返回群组的图元列表#(在2015版本之后不推荐使用)#(但是出于向下兼容性的考虑依然保留在高版本中)cpn.definition.entities#返回组件实例相对应的组件定义的图元列表grp.definition.entities#返回群组所对应的组件定义的图元列表#(这是从SketchUp 2015 以后推荐的表示)grp.entities != grp.definition.entities#对于拥有两种群组图元访问方法的版本而言#以上表达式结果为true
至于群组的特殊情况,即一个组件定义(ComponentDefinition)对应多个群组(Group)的情况出现在对群组的简单复制中,这种处理方法可以在大量复制群组的模型中减小模型大小。所以一次性复制100个群组但是不修改他们中的任何一个,模型中会有100Group类,而他们的定义都仅仅是最初那个群组的定义,即只有1个ComponentDefinition类。如果用组编辑器打开其中一个,那么原有的定义(定义1)将会复制出新的定义(定义2),并且另外99个群组的定义将会被修改为定义2,这样一来,对于这个群组的修改将只会影响到这一个群组。这个过程的变化见下图:
两个群组实际上共用一个组件定义,图元信息中也可以看到这个组件定义的实例数量
双击其中一个群组(但是并不进行任何修改)之后,两个群组就不属于同一个组件定义了
回到之前需要给组内平面翻面的问题,如果群组之内还有群组,这个群组中的群组内的平面是依然不会翻转,这同样是因为群组定义中的Entities返回的依然有可能是群组,这个群组中的平面依然没有被枚举出来,而且并不能知道到底群组中套群组这个样的情况有多少层。
多层嵌套的群组或者组件能够很好地对模型进行结构化管理,但是在这里,如果需要枚举就稍微有一点麻烦。如果想要不受上述情况的掣肘,枚举任意层次的群组中的图元,一种比较有效的方法是使用递归,这里我使用函数递归。以下是一个简单的例子:
module Apiglio @list=[] def Apiglio.all_ents(arr) #deprecated @list=[] arr.each{|i| case i.typename when "ComponentInstance","Group" Apiglio.all_ents(i.definition.entities) @list< else @list< end } return @list.uniq endend
以上代码需要简单介绍一下,这里在模块Apiglio中增加一个“.all_ents”的方法,参数“arr”需要是Entity类的容器,具体可以是Entities类或者Selection类。以上这些类都可以通过“.each”方法枚举每一个符合要求的Entity类实例。列举的每一个图元都被添加至“@list”这一数组中。如果这个图元是群组或者组件实例,在添加进数组的同时单独以其自身的定义作为参数立刻执行新的一个“all_ents”方法。这样一来,组内的图元也会被新的方法添加进“@list”数组。新的“all_ents”方法执行后会重新回到老的“all_ents”方法中,如此的过程可以被认为是一种函数递归。
例子中的“@list”是模块的类变量,充当的是全局变量的作用。也可以直接用“$list”这是真正的全局变量表示方法,但是这样就有和其他功能的全局变量冲突的可能性,所以使用模块类变量相当于是一个命名空间,相对而言更安全些。(虽然全局变量大概率不会是这样一个容易重复的样子,虽然其他成熟的功能通常不会使用全局变量。)
有了以上方法之后,就可以通过以下代码来实现所有平面的翻面:
load "apiglio.rb"#需要将代码文件储存在Tools文件夹中,随后用“load”+文件名的方法载入#之后在控制台中输入以下内容,换行可用空格代替Apiglio.all_ents.each{|ent| if ent.typename=="Face" then ent.reverse! end}
需要指出的是,以上方法其实是非常粗糙的,存在很多能导致错误或是影响执行效率的因素。例如:没有进行类型检查,也没有排除掉同样继承自Entity类的其他非DrawingElement类实例(例如Loop类、Vertex类等)。除此两点之外,枚举过程的重复和冗余是这个方法最大的问题,以至于最后需要使用数组去重复的“.uniq”方法善后。因为在这样粗糙的枚举过程中,大量组件定义中的图元被重复枚举之后又被额外的去重复,对于很小的枚举而言或许还勉强可以使用,一旦结构类型增加,运行速度将大受影响。
上图三个立方体组件为相同的定义,因此在枚举过程中会先得到3×6个平面和3×12个边线。然而ruby引用这些图元时他们都指向同一个组件定义中的图元,并且额外重复了两遍。因此图中控制台的第三句指令加入了Array类“.uniq”方法去除重复,才得到六面十二棱。(图中,S.ss = Skectchup .active_model .selection)
因此需要一个更有效率的枚举方法,这个方法要能供尽可能的避免重复的图元。不过在开始实现更有效率的方法之前,还需要关注一件事情:枚举的具体目的是什么?
这个问题相当重要,因为涉及组件和群组,是否需要枚举群组和组件内部的图元需要根据枚举的目的确定,例如用于移动的枚举完全没有必要枚举组件和群组内部,只需要移动组件实例即可,如果同时移动组件实例和组件定义,就会出现组件内的图元在世界坐标系中移动了多倍的距离。但如果你需要将所有的材质替换回默认材质,这个时候枚举结果就需要同时包含组件实例、群组和组内的其他图元。
也就是说枚举并非仅有一种方法,它的返回结果可以分为以下几个部分,不同的目的需要的枚举结果可能包含其中的部分或全部:
群组 | 群组内 | 组件 实例 | 组件内 | 其它 图元 | |
目的1 | √ | √ | √ | √ | √ |
目的2 | √ | ○ | √ | ○ | ○ |
目的3 | √ | √ | √ | ○ | √ |
众多的枚举方式不是本文的重点,本文只想统计各类组件有多少个罢了。因此,使用了表格中的“目的2”方法。与上面不同的还有,这个枚举方法不避讳重复,反而需要了解一个Entities类中相同的组件实例有多少个,这也就意味着之前的例子中的“.uniq”方法并不需要。这些目的上的差异是枚举过程中必须关注的细节。
以下是具体的实现方法:
module Apiglio #定义一个简写方法 def Apiglio.sels Sketchup.active_model.selection end #all_groups(arr,mode) 返回所有选中的所有层次的群组和元组实例 #mode:0完全穷尽,1到组件为止,2到群组为止,3无层级 def Apiglio.all_groups(arr=Apiglio.sels,mode=1) list=[] arr.each{|i| case i.typename when "ComponentInstance" list< if mode &1 == 0 then list+=all_groups(i.definition.entities,mode) end when "Group" list< if mode &2 == 0 then list+=all_groups(i.definition.entities,mode) end end } return list endend
以上方法相比于上一种方法而言有两个明显的不同。第一是没有使用类似全局变量的概念,完全使用递归返回。第二是对递归进行了一定的限制,只统计组件实例,并不去处理其他的图元;同时设置了一个模式参数,可以具体指定是否深入统计群组或者组件实例中的图元。函数返回的是一个图元列表,返回当前层次下找到的符合条件的图元,同时在找到每一个图元时都判断是否需要对它进行展开,如果当前模式需要深入一层,就单独以新的参数运行一次,并将这一次函数运行返回的图元列表追加到当前运行的函数,从而实现遍历全部组件实例。
“mode”是限定函数执行内容的参数,默认值为0。应当以二进制数来解读,解读方法如代码中的注释。函数执行过程中通过判断mode是否符合要求来选择性的跳过一些没有必要的过程以提高效率。其中诸如“mode & 1 == 0”和“mode & 2 == 0”之类的表达是对mode的逐位判断,在这里不详细展开了。
②统计的细节或难点
在思考清除枚举具体的工作之后,就可以将这个功能封装在一个方法之中,枚举方法的实质就是在给定的条件下返回一个图元列表,而这个图元列表是一维的、扁平的,可以线性查找的。统计的基本套路就是用循环,当然在ruby中更地道的是用迭代器:
list = Apiglio.all_groups(Apiglio.sels)acc=0 #设置一个累加器list.each{|ent| #在这里就可以是用ent进行针对列表中所有图元的操作 #例如下面的代码统计list中有多少个平面图元 if ent.typename=="Face" then acc+=1 end}puts "一共有#{acc}个平面"#以上是输出结果,#{var}表示将表达式var转换为字符串输出#在控制台中不能输入中文字符#之前包括教材在内的很多文章都提到过
以上只是对于符合某一个条件的图元进行计数,这并不是真正的统计,统计起码也要是一个表格,自动罗列出所有出现的值。这就需要使用的一个新的数据结构,ruby中称“哈希”,python中称“字典”。这个结构专门用来表示名义变量(键,Key)与其他形式变量(键值,Value)之间的关系:
h=Hash.new#>> {}h["Face"]=3#>> {"Face"=>3}h["Edge"]=8#>> {"Face"=>3, "Edge"=>8}h["Edge"]+=12#>> {"Face"=>3, "Edge"=>20}h["Group"]=12#>> h={“Face”=>3,“Edge”=>20,“Group”=>12}h.each{|i|puts "键名:#{i[0]},\t键值:#{i[1]}."}#>> 键名:Face, 键值:3.#>> 键名:Edge, 键值:20.#>> 键名:Group, 键值:12.
这中数据结构天生可以用来储存统计结果,所以统计图元类型就可以按照如下的代码来实现,其中“.key?”方法用来判断哈希是否有相对应的键:
output=Hash.newlist.each{|ent| tmp=ent.typename if output.key? tmp then output[tmp]+=1 else output[tmp]=1 end}puts output
不过统计并不是说统计图元的个数,或者是别的什么单一的功能,而是应该同时满足图元不同属性的统计。例如图元列表中的图层分布情况、可见性情况。如果有图元是组件实例的话还有组件名称之类的属性,如果是边线还有长度,方向等属性,平面也有面积、方向等属性。如果每次统计不一样的内容就要重新修改代码这实在是不太聪明,因此这个统计功能需要能够设置具体的统计内容。
现在我们都知道枚举返回的图元都是Sketchup ::Entity类的实例,具体又是它的Sketchup::Drawingelement子类的实例,根据图元形态不同又划分为边线(Sketchup ::Edge)、平面(Sketchup ::Face)、文本注记(Sketchup ::Text)、群组(Sketchup ::Group)和组件实例(Sketchup ::ComponentInstance)等等。
如果要判断这些图元的不同属性,在代码上相当的多元:
ent.typename # 返回图元类型ent.length # 返回边线长度ent.area # 返回面域面积ent.definition.name # 返回组件的定义名称ent.layer.name # 返回所属图层的名称ent.hidden? # 返回是否隐藏
这里只是一些例子,统计的项目可能要比这些要古怪得多,尤其是每一个图元可以有自己的参数表(.attribute_dictionaries)。所以现在需要一种方法将上面的表述变成统计方法的一个参数,这样就可以更加方便的使用这个功能。这就涉及按照字符串的要求执行特定的方法的问题。Ruby中有这样一种替代方法:
ent.erase!# 等同于(ent.method :erase!).call# 或者(ent.method "erase!").call
利用上面的方法,可以简单地构建了一个这样的方法:
def Apiglio.method_rank(obj,mtd) mtd=mtd.to_s mtd_list= mtd.split "." res=obj mtd_list.each{|i| res=(res.method i).call } return res end
例如调用Apiglio .method_rank (ent,"definition.name")时,首先会先将“definition.name”转化为["definition","name"],之后建一个临时的变量res储存ent。迭代器执行第1个元素“definition”时,res=res .definition,因此res=ent .definition;迭代器执行第2个元素“name”时,res=res .name,因此res=ent .definition .name;最后res得以返回。
这样一来统计方法将可以自由地适用于各种类型的图元,但是伴随“自由”的是“不安全”。如果ent不是群组(Sketchup ::Group)或者组件实例(Sketchup ::ComponentInstance),图元不存在对应的“.definition”方法,因此这个时候ruby就会报错,并且停止执行这个控制台的所有未执行指令。这种情况使得我们需要考虑筛选枚举结果的图元类型,一种做法是直接在枚举结果之上添加一个筛选过程:
list = Apiglio.all_groups(Apiglio.sels)list.grep(Sketchup::Edge).each{|ent|......}
但是很显然这一行代码还可以进一步封装,直接在控制台中输入这些内容不是明智之举,尤其是这个方法功能固定且比较常用之时。如果继续封装,也不能保证实际使用时需要筛选出什么图元类型,更何况有可能同时需要多种类型呢。所以为了更普遍地使用统计功能,我推荐另一种做法。
Ruby报错并不一定就要停止执行,也可以使用try - except结构来进行故障处理,Ruby中的故障处理的其中一种方式长这个样子:
mtd="definition.name"output=Hash.newoutput["~Error"]=0# ...begin tmp=Apiglio.method_rank(ent,mtd) if output.key? tmp then output[tmp]+=1 else output[tmp]=1 endrescue output["~Error"]+=1end
形如“begin A rescue B end”的语法表示:执行A的过程中如果出现错误,停止执行A转而执行B,随后继续执行“end”之后的内容。这里我在哈希中增加了一个故障个案的统计,替换成N/A也是一样的,这样就能最大程度展示统计结果了。
第三部分:代码实现
实现代码需要封装在一个模块中,这里以名为“Apiglio”的模块定义为例:
module Apiglio# 中间是模块的实现代码end
之后将底下的实现代码填充进这两行之间,以“apiglio.rb”的名称保存在SketchUp安装目录中的Tools文件夹下,打开Ruby控制台输入以下内容即可在后续的工作中使用模块中定义的方法:
load "apiglio.rb"
本篇文章的代码其实大部分在前文大多已经零散的列出用于演示技术细节,这里将它们完整的罗列出来,实现功能的代码如下:
module Apiglio def Apiglio.sels Sketchup.active_model.selection end def Apiglio.all_groups(arr=Apiglio.sels,mode=1) list=[] arr.each{|i| case i.typename when "ComponentInstance" list< if mode&1==0 then list+=all_groups(i.definition.entities,mode) end when "Group" list< if mode&2==0 then list+=all_groups(i.definition.entities,mode) end end } return list end def Apiglio.groups_count(label="typename",reg=//,arr=Sel.sels) if not reg.kind_of? ::Regexp then puts "第二个参数应为正则表达式//" return nil end ha=Hash["~Error"=>0] Apiglio.all_groups(arr,0).each{|i| begin stmp=Apiglio.method_rank(i,label).to_s if ha.key? stmp then ha[stmp]+=1 else ha[stmp]=1 end rescue ha["~Error"]+=1 end } puts "" puts " Cout \t| Name" puts "--------+--------------" acc=0 add=0 ha.sort{|a,b|a[1]<=>b[1]}.each{|k,v| acc+=v if k=~ reg then add+=v puts " #{v}\t| #{k}" end } puts "--------+--------------" puts " #{acc} \t| Total" puts " #{add} \t| Shown" if acc!=add return nil endendApiglio.groups_count("definition.name",//)
最后再Ruby控制台输入Apiglio .groups_count ("definition.name",//)就可以看到选取的模型所使用的组件分别有多少了。
最后依照惯例放一些相关的、补充的并且有趣的内容:
①一些简短的写法:
#对于第一部分的例子Sketchup.active_model.entities.each{|ent| if ent.typename=="Face" then ent.reverse! end}#还有更简便的写法ents=Sketchup.active_model.entitiesents.grep(Sketchup::Face).each{|ent| ent.reverse!}#又或者这样ents.grep(Sketchup::Face).each(&:reverse!)
②深入组件定义的枚举可能导致这样的问题:
③最后一部分出现了正则表达式的内容,这是一种专门又来查找、匹配、筛选字符串集合的语法,展示效果图片中的/\A\[/表示以字符“[”开头的字符串。
本篇文章是“SketchUp小功能与灵感”的其中一篇,编号为“SU-2020-08”,本系列的其他文章可以在公众号后台输入“SU-”+年份+“-”+两位数编号查看,或者直接点击菜单中的“SU Ruby”选项获得全部有关SketchUp文章的目录。