【SU Ruby教程】图元定义(1):图元类与图元容器
从本篇开始,就进入了教程最核心的部分。这一部分会直接和各类 SketchUp 模型中的图元打交道,包括创建图元以及相应的一些基础设置的修改。
本篇重点关注 Sketchup:: Drawingelement 类(图元类)和 Sketchup:: Entities (图元容器类)两个类型,并简要罗列图元类的各种子类。
图元定义(1):图元类与图元容器
【本期目录】 | |
(1)图元类 ①Drawingelement 类 ②实例方法 | (3)图元容器 ①Entities 的调用方式 ②容器的可枚举特性 ③新建图元 ④移动和删除图元 |
(2)图元类的继承 ①何谓继承 ②图元类的子类 |
(1)图元类
①Drawingelement 类
图元类(Drawingelement)是实体类(Entity)的子类,SU 模型中的线、面、群组、组件、文本标记和剖切面等都属于这个大类,所有的图元实例都可以通过图元容器 Entities 类实例访问。
图元类是与模型文件紧密相关的,因此所有图元实例都是对应模型文件中的具体图元,每一个图元有其唯一的编号。其中使用 .persistent_id 方法可以获得图元在模型文件中的唯一标识:
此处的 .persistent_id 方法定义在 Entity 类中,但并不是所有的实体类实例都有有效的值。不过作为实体类子类的一个重要类群,所有图元类的实例都有有效的 Persistent ID 值。这些值随文件保存,重新打开文件或者跨越版本,都不会改变。
与 .persistent_id 相对的还有两个类似的序号概念,分别是 .entityID 和 .object_id。前者也定义在 Entity 类中,后者则定义在 ruby 的基类 Object 类中。 .entityID 在同一个版本中,重启 SketchUp 之后仍能保持不变,而跨越版本时不能保证前后一致。 .object_id 方法(以及相同效果的 .__id__ 方法)每次重启程序都会重新分配。
其中, .object_id 的值乘2,再转为十六进制即对象 .inspect 方法的默认返回结果。
因为图元类与模型文件的高度关联,它不像上一部分教程中 Geom 模块里的对象那样,可以使用 .new 方法来定义,而要通过 SketchUp Ruby API 中提供的方法来创建,这样才能正确地创建图元。
de=Sketchup::Drawingelement.new
nf=Sketchup::Face.new
de.valid? #>> false
nf.valid? #>> false
②图元类实例方法
图元实例拥有非常丰富的属性,其中所有图元都包括以下几个属性的设置:空间范围、显隐设置、阴影设置、图层设置和材质设置。图元类需要提供方法来进行这些操作。在教程的第一篇,曾经提到过其中的一部分,现在这里完整地罗列出来:
(i) 范围
通过 .bounds 方法可以返回一个范围类,来表示图元的空间范围。这个类在之前的章节 [SU-R07] 中有详细的解释,此处不重复介绍。
#令当前模型至少有一个图元
ents=Sketchup.active_model.entities
ents[0].bounds
#>> #<Geom::BoundingBox:0x0000000be5eb28>
(ii) 显隐
通过 “hidden” 和 “visible” 两个单词分别接问号和等号,组合成以下四个方法,分别用于修改或者返回一个控制图元显隐的布尔值;“hidden” 与 “visible” 在相同显隐性的情况下结果总是相反。
#令当前模型有且只有一个未隐藏的群组
ents=Sketchup.active_model.entities
ents[0].hidden? #>> false
ents[0].visible? #>> true
ents[0].visible=false
# 此时群组被隐藏
ents[0].hidden? #>> true
ents[0].visible? #>> false
ents[0].hidden=false
# 此时群组重新显示
能出现在 Entities 容器中的所有图元都可以使用以上方法控制显隐性。同属于图元类子类的 ComponentDefinition 类,虽然也有以上方法,但通过这些方法对组件定义的属性进行修改不会体现在绘图区中。
(iii) 阴影
类似于控制图元显隐性的方法,也可以通过 “casts_shadows” 和 “receives_shadows” 两个单词控制图元是否投射或接受投影。
#令当前模型有且只有一个群组
ents=Sketchup.active_model.entities
p ents[0].casts_shadows? #>> true
p ents[0].receives_shadows? #>> true
ents[0].casts_shadows=false
ents[0].receives_shadows=false
# 令群组既不接收投影,也不投射投影
p ents[0].casts_shadows? #>> false
p ents[0].receives_shadows? #>> false
所有图元类实例都有以上四个方法,也都有记录是否投射/接受投影的属性。但是,由于并不是所有图元的阴影都有意义,设置的结果是否有效取决于绘图区的判断。能够同时投射投影和接受投影的只有平面图元(Face)和组件图元(ComponentInstance 和 Group,群组属于特殊的组件实例)两种;除此之外图像图元(Image)也可以接受投影。
(iv) 图层与材质
图层和材质作为非布尔型的图元属性,因此不能使用以“?”为后缀的方法名,不过同样可以使用以“=”为后缀的方法。
“图层(Layer)”在最新的版本中已经改称为“标签(Tag)”,不过此处暂时还是使用旧名称。可以使用 layer 方法获得特定图元所在的图层,其返回结果为 Layer 类。这个类在之后的篇幅中再详细介绍,以下例子中使用 Layer 类的 .name 方法,返回图层类的名称:
ents=Sketchup.active_model.entities
puts ents[0].layer.name
#>> Layer0
ents[0].layer=Sketchup.active_model.layers.add("Apiglio")
#>> #<Sketchup::Layer:0x0000000b882ee8>
ents[0].layer="Layer0"
#>> Layer0
ents[0].layer="not_exist"
#>> Error: #<ArgumentError: Cannot find layer named "not_exist">
#>> <main>:in `layer='
#>> <main>:in `<main>'
#>> SketchUp:1:in `eval'
同样可以使用 .layer= 方法设置图层,等号右边可以接受 Layer 类实例,也可以接受字符串。使用字符串参数时,如果没有名为此参数的图层,则会报错(上例中第9行开始为错误提示的展示)。
材质的设置与图层设置相同,只是将“layer”替换为“material”,此处粗略展示一个例子:
ents=Sketchup.active_model.entities
puts ents[0].material
#>> nil
ents[0].material="red"
#>> red
ents[0].material="not_exist"
#>> Error: #<ArgumentError: Cannot find material named not_exist>
#>> <main>:in `material='
#>> <main>:in `
#>> SketchUp:1:in `eval'
如果给不符合要求的图元设置投影和显隐性,其属性也会保存。例如,将一个组件定义设置为不可见,或者将图像图元设置为投射投影,这两种情况在绘图区虽然不会有任何改变,但图元本身属性确实有修改。
而材质设置的情况则不同于这两个设置,对于不支持材质的图元,调用 .material= 方法并不会对属性造成修改。这是一个微小的区别:
ents=Sketchup.active_model.entities
# 令模型仅有一个辅助线图元
puts ents[0]
#>> #<Sketchup::ConstructionLine:0x0000000bb1c640>
ents[0].material="red"
p ents[0].material
#>> nil
# 如果是平面图元则会是类似以下输出:
#>> #<Sketchup::Material:0x0000000b480cf0>
支持材质的图元类型有平面图元(Face)、组件图元(ComponentInstance & Group)、文本图元(Text)和标注图元(Dimension)。另外,边线图元(Edge)和组件定义(ComponentDefinition)虽然不会根据材质改变显示状态,但是会保留材质属性。
(v) 删除
删除涉及到图元容器的改变,因此方法名带有“!”的后缀,删除后再次引用该图元就会得到 Deleted Entity,此时的图元实例仍然存在于内存之中,但是并不在 Model 类的任何容器之中。因此依然可以调用 .valid? 方法判断图元是否有效,而不是出现无法调用方法的错误(NoMethodError)。
ents=Sketchup.active_model.entities
ent=ents[0]
p ent.valid?
#>> true
ent.erase!
# 图元被删除
p ent.valid?
#>> false
p ent
#>> #<Deleted Entity:0xb5fd178>
(2)图元类的继承
①何谓继承
当一个类型A属于一个更大的类型B,且类型B有属性P时,就构成了一个典型的三段论推理逻辑,也就是“B有属性P,A是B,所以A有属性P”。这时就可以说A是B的子类,A继承了B的属性。
在 SketchUp 中,这个例子就变成了:f 是一个平面(即Face类实例),Face 类是 Drawingelement 类的子类,而 Drawingelement 类有 hidden? 方法,所以 f 也有 hidden? 方法。这个 hidden? 方法就是 Face 类从 Drawingelement 类中继承而来。
图元类(Drawingelement)就是实体类(Entity)的子类,因此继承了 Entity 类的属性和方法,例如 valid? 和 deleted? 方法。之后图元定义部分会分别介绍各类图元类,而这些具体的图元类型(例如边线类、平面类或者群组类)都继承自图元类,因此也都拥有图元类的方法,例如 .hidden? 和 .layer= 方法。
同理,图元实例同样也包含诸如 .object_id 这样的方法,这些方法则继承自 Entity 类的父类 Object 类,这是所有 ruby 类的父类(或者应该说是祖先类了)。
类继承设计能够显著提高代码的复用率,也可以使代码结构更加清晰。在之后的一些场景中还需要自行构造一些自定义类,需要继承 SU Ruby API 中的某些预设类,来实现特定的预设功能。但目前暂时还只需要了解 API 中提供的类之间存在的继承关系。
(SketchUp Ruby API 中的Entity类及其子类的继承关系)
上图中,可以分成三个层次,最完整的圆圈为基本的实体要素,其外围一圈为对应的容器类或者相关的辅助实体。圈内的个别零星节点表示抽象类,它们不能直接实例化,而要通过其子类实现。
根据图中原点 Object 类可以区分出四个象限:右下角为图元类,左下角为图元底层的拓扑关系,左上角为常用的图元属性,右上角为其他设置与自定义属性。
而从这一期开始的“图元定义”部分,会同时涉及右下角和左下角两大门类;之后的“图元属性”部分则会涉及左上角和部分右上角的概念。当然还有很多 API 提供的类直接继承自 Object 类,在这两个部分之后也会涉及。
②图元类的子类
当我们需要了解一个类的子类有哪些时,可以通过如下的代码实现:
puts ObjectSpace.each_object.to_a.grep(::Class).select{|c|
c.ancestors.include?(Sketchup::Drawingelement)
}
其中, ObjectSpace. each_object. to_a. grep( ::Class) 可以返回一个包含内存中已加载的所有类的数组; .ancestors 方法可以返回每一个类的继承关系组,包括其本身和所有祖先类(父类、父类的父类……)。
通过上述的代码或者直接通过上文的继承关系图可以得知,图元类有以下几个类型:
类名 | 说明 | 容器 |
ComponentDefinition 组件定义 | 所有群组和组件的定义,唯一一个可实例化但不在 Entities 容器中的图元类 | DefinitionList |
ComponentInstance 组件实例图元 | 引用组件定义的一种形式,修改实例时会修改组件定义 | Entities |
ConstructionLine 辅助线图元 | 构造线,分为有限长度和无限长度两种 | Entities |
ConstructionPoint 辅助点图元 | 构造辅助点 | Entities |
Dimension 尺寸 | 抽象类,是以下两个图元类型的共同父类 | - |
DimensionLinear 线型尺寸图元 | 标注长度的尺寸线 | Entities |
DimensionRadial 圆弧尺寸图元 | 标注直径的尺寸线 | Entities |
Drawingelement 图元基类 | 抽象类,是所有图元类的父类或祖先类 | - |
Edge 边线图元 | 边线图元,连接两个端点 | Entities |
Face 平面图元 | 平面图元,由一组闭合的边线确定 | Entities |
Group 群组 | 引用组件定义的另一种形式,修改实例时会复制新建组件定义 | Entities |
Image 图像图元 | 图像图元,本质上是平面图元,但是不投射阴影 | Entities |
SectionPlane 剖切面图元 | 创建剖视图的图元,也可以认为是平面图元的一种 | Entities |
Text 文本标注图元 | 文本标注,与尺寸标注类似 | Entities |
这些图元类,大多可以通过 Entities 类实例访问,因此可以称 Entities 为图元容器。
(3)图元容器
①Entities 的调用方式
图元容器有两种调用方式,分别是通过 Model 类和 ComponentDefinition 类的方法来调用。
Model 类可以通过 .entities 或者 .active_entities 方法来调用图元容器。两者区别在于前者是返回当前模型的图元容器,而后者返回的是绘图区当前正在编辑的图元容器。正在编辑的可以是整个模型的图元容器,也可以是某个正在编辑的群组或者组件的图元容器。
# 新建模型,并保留自带的主创人员模型
p Sketchup.active_model.entities
#>> #<Sketchup::Entities:0x0000000b9aee48>
p Sketchup.active_model.active_entities
#>> #<Sketchup::Entities:0x0000000b9aee48>
# 双击组件编辑组件内部
p Sketchup.active_model.active_entities
#>> #<Sketchup::Entities:0x0000000bd206f8>
对于一个没有在绘图区打开的群组或组件,需要修改其图元就需要直接使用 ComponentDefinition 类的 .entities 方法。以下例子中通过 .definition 方法返回组件实例的组件定义:
cpm=Sketchup.active_model.entities[0]
p cpm.definition.entities
#>> #<Sketchup::Entities:0x0000000bd206f8>
另外对于群组(Group)而言,可以直接使用 .entities 方法,而不需要额外通过 .definition 方法返回图元容器。使用这两种方法返回的图元容器作为对象而言,并不相同(指object_id不同);但是其包含的图元信息可以认为是完全一致的,这在群组和组件部分会详细区分。
从调用方式可以得知,Entities实例可以直接从属于模型,或者从属于某个组件定义。当需要判断绘图区是否正在编辑组件时,可以通过判断当前模型的图元容器和当前激活的图元容器是否相同来确定:
model_ents=Sketchup.active_model.entities
active_ents=Sketchup.active_model.active_entities
if model_ents == active_ents then
puts "当前没有编辑组件"
else
puts "当前正在编辑组件"
end
同样还可以直接通过 .model 和 .parent 方法返回一个 Entities 实例从属的 Model 实例或者 ComponentDefinition 实例:
# 在绘图区中双击一个组件进入编辑状态
active_ents=Sketchup.active_model.active_entities
puts "p=#{active_ents.parent}"
puts "m=#{active_ents.model}"
#>> p=#<Sketchup::ComponentDefinition:0x0000000be35c00>
#>> m=#<Sketchup::Model:0x0000000bcdf6f8>
# 退出组件编辑状态后
active_ents=Sketchup.active_model.active_entities
puts "p=#{active_ents.parent}"
puts "m=#{active_ents.model}"
#>> p=#<Sketchup::Model:0x0000000bcdf6f8>
#>> m=#<Sketchup::Model:0x0000000bcdf6f8>
②容器的可枚举特性
在之前的篇幅 [SU-R05] 中就提到过,容器类就像是数组,可以通过 .[] 方法访问某个具体元素。例如:
p Sketchup.active_model.entities[0]
#>> #<Sketchup::Group:0x0000000be36088>
p Sketchup.active_model.entities[1]
#>> #<Sketchup::Group:0x0000000be35e80>
除了使用方括号的形式,还可以使用以下两种方法:
Sketchup.active_model.entities.at 7
#<Sketchup::ComponentInstance:0x0000000be35b88>
Sketchup.active_model.entities.[] 12
#<Sketchup::ConstructionLine:0x0000000be35958>
图元容器的总大小可以用 .length、 .size 或者 .count 方法返回,三个方法的返回结果相同。总大小即容器中有多少个图元要素,无论是简单的边线,还是极其复杂且多层嵌套的组件实例,都只贡献 1 的大小。而描述这些组件的内部结构将由其他 Entities 实例来完成,与当前图元容器无关。
不过这三个方法也并非完全相同,它们在运行速度上存在一定的差异。下图展示了三种方法各自执行100万次的所需要的时间:
(图中的 Usf.ents 相当于 Sketchup.active_model.entities)
可以发现, .length 方法最快, .size 方法稍微落后,而 .count 方法速度比前两者相差较大。可以从官方文档中发现这三者的区别:
The #length method is used to retrieve the number of entities in the collection of entities.
ruby.sketchup.com
The #size method is an alias for the #length method.
ruby.sketchup.com
Since SketchUp 2014 the (#)count method is inherited from Ruby's Enum(er)able mix-in module. Prior to that the #count method is an alias for #length.
ruby.sketchup.com
可以得知, .length 方法是最“正宗”的方法, .size 方法则是前者的别名(alias)。调用方法别名需要额外一个定位动作,因此相对而言会更慢一些。 .count 方法是最特殊的,它通过 Enumerable 模块实现,因此和前两个方法实现方式不同。
Entities 类中包含有 Enumerable 模块的方法,可以使用以下代码验证:
puts Sketchup::Entities.included_modules
#>> Enumerable
#>> JSON::Ext::Generator::GeneratorMethods::Object
#>> Kernel
而这个 Enumerable 模块会提供大量遍历、查找和排序的方法。所有包含了这个模块的类,实例都会拥有以下这些方法:
而所有的这些方法都需要在这个类拥有 .each 方法时才能够有效调用,因为它们的实现需要调用这个方法;也因此 .count 方法会比另两个方法慢很多。
而 .each 方法是最基础的迭代器,在之前的篇幅中已经多次出现了,例如下图中在控制台中输出每一个图元的类型:
有了 .each 方法之后,直接引入 Enumerable 模块就可以获得功能丰富的各类与遍历或者查找有关的方法,也就包括了上文提到的 .count 方法。
图元容器经常需要筛选部分图元或者查找批量编辑某些图元,这时就需要使用 Enumerable 模块中的这一众方法,以下是一些常见的例子:
ents = Sketchup.active_model.entities
# 将容器中的图元保存到数组中以供他用
arr = ents.to_a
# 返回容器中所有边线类
ents.grep(Sketchup::Edge)
ents.find_all{|i|i.is_a? Sketchup::Edge}
# 将容器中的图元根据高度排序
ents.sort_by{|e|e.bounds.height}
ents.sort{|a,b|a.bounds.height<=>b.bounds.height}
# 判断图元容器中图元是否都在图层Layer0
ents.all?{|i|i.layer.name=="Layer0"}
# 判断图元容器中是否至少有一个图元在图层LAY1
ents.any?{|i|i.layer.name=="LAY1"}
# 判断图元容器中图元是否全部不在图层LAY2
ents.none?{|i|i.layer.name=="LAY2"}
# 将容器中的图元根据类型名称分组成哈希结构
ents.group_by(&:typename)
# 选择容器中有definition方法的图元实例(也就是群组和组件两类)
ents.select{|i|i.respond_to? :definition}
关于这个模块的更多方法的使用功能,可以通过搜索引擎查找,也可以直接查看官方文档(https://ruby-doc.org/core-2.7.1/Enumerable.html)。
③新建图元
Entities 类提供了一系列“add”开头的实例方法,用于往容器中添加新的图元:
其中有11个方法用于创建最基本的图元:
方法 | 创建图元的类型 |
.add_line | Edge |
.add_face | Face |
.add_group | Group |
.add_instance | ComponentInstance |
.add_cline | ConstructionLine |
.add_cpoint | ConstructionPoint |
.add_image | Image |
.add_text | Text |
.add_dimension_linear | DimensionLinear |
.add_dimension_radial | DimensionRadial |
.add_section_plane | SectionPlane |
另外8个方法则用于创建复杂或更特定的某些图元,甚至不是用于新建图元:
方法 | 创建图元的类型 |
.add_edges | 一次性创建多段Edge |
.add_curve | 一次性创建多段Edge作为曲线 |
.add_arc | 一次性创建多段Edge作为圆弧 |
.add_circle | 一次性创建多段Edge作为圆 |
.add_ngon | 一次性创建多段Edge作为正多边形 |
.add_3d_text | 用于创建三维文字 |
.add_faces_from_mesh | 用于创建三角形镶嵌表面 |
.add_observer | 用于创建图元容器监控,这并不是一个图元类型 |
这些方法所需要的参数各有不同,会在各类图元的篇幅中详细介绍。它们中的大部分都会返回新创建的图元作为结果,因此可以在新创建图元后立刻对其进行其他修改:
ents = Sketchup.active_model.entities
pt1=[0,0,0]
pt2=[0,0,1000.mm]
pt3=[0,1000.mm,1000.mm]
pt4=[0,1000.mm,0]
edges=ents.add_edges pt1,pt2,pt3,pt4,pt1
face=ents.add_face pt1,pt2,pt3,pt4,pt1
dl=ents.add_dimension_linear pt2,pt3,[0,0,500.mm]
puts dl.class #>> Sketchup::DimensionLinear
以上代码的执行效果如下:
④移动与删除图元
(i)移动
图元的移动其实在“几何与变换(3)”的“应用变换”部分中就已经介绍过了,即通过 .transform_entities 方法和一个变换对象参数来实现。例如,以下代码能够使图元容器中的所有群组图元向Z轴正方向移动5米:
t = Geom::Transformation.translation([0,0,5000.mm])
ents.transform_entities(t,ents.grep(Sketchup::Group))
有意思的是, .transform_entities 方法需要的参数可以不是图元类,它同样接受诸如Vertex、Loop、Curve这些抽象的空间实体。不过变换这些实体可能产生一些与想象不同的结果,这个在之后的篇幅中会部分展开。
除了以上这个方法,Entities 类实例还有一个与变换有关的方法,即 .transform_by_vectors 方法。不同于前者,这个方法更像是为端点类(Vertex)量身定做的,通过一个端点与向量的对照表,一次性移动所有端点。端点空间位置的改变也就导致了与之关联的图元的空间形态改变。
以下这个方法的例子:
vertexs=ents.grep(Sketchup::Edge).map(&:vertices).flatten
l=vertexs.length
vectors=(l/2-l+1 .. l-l/2).to_a
vectors.sort_by!{rand()}
vectors.map!{|i|[0,0,i]}
ents.transform_by_vectors(vertexs,vectors)
其中, vertexs 为这个图形的所有端点, vectors 则是与之对应的一组向量。这里使用了一系列语句使得 vectors 中的向量方向垂直于地面,并且高度方向上随机。于是得到下图的效果:
(ii)删除
图元类实例有 .erase! 方法,用于从模型中删除自身,如果使用这个方法则需要一个一个地删除图元。但是当删除一个参与成面的边线时,平面也一并删除,如果一个一个删除就会出现如下问题:
为了避免这个问题当然可以这样处理:
ents.to_a.each{|i|i.erase! unless i.deleted?}
不过这样显得过于麻烦,因为可以直接使用 .erase_entities 这个方法:
ents.erase_entities(ents.to_a)
#也可以用以下代码代替
ents.clear!
由于本例是删除容器中的所有图元,所以还可以用 .clear! 这个方法,这与图元类的 .erase! 方法的命名逻辑相同,同时避免了与之混淆。这个方法可以一次性清空整个图元容器。
文中在介绍 .transform_by_vectors 方法时,已经提到了端点类(Vertex)。端点类并非图元,但它是边线和平面拓扑关系的重要桥梁实体(与之类似的还有 Loop 类和 EdgeUse 类等)。而在下一篇,就会在介绍边线类(Edge)这个相对简单的图元类时,涉及这个相对简单的拓扑实体。
(完)
本文编号:SU-R10