【SketchUp图元的族谱】附庸的附庸

Apiglio

共 10700字,需浏览 22分钟

 · 2024-03-21

本系列“图元族谱”的上一篇“对影成三人”介绍了 SketchUp 一个组件(或组件内的图元)的“影子”图元,提供了一个返回所有“影子”图元路径(InstancePath)的方法,通过一个组件实例或组件定义,向上查找包含该图元的父模型,从而查找所有在其他位置的同定义组件实例。这是一种自下而上的路径搜索逻辑,将关注另一种查找方向,即自上而下的路径搜索

501f98cdeaa682e8dc84998e16ad5672.webp

通过一个组件实例向下查找全部其包含的全部子模型

例如下图展示的一个群组,其内部是 20 个定义相同的组件,每个组件中有 6 个面 12 条边,所以群组中总计有 120 个面 240 条边。但是对于模型存储来说,群组中仅有 20 个位置信息(Transformation)不同的组件实例(ComponentInstance),所有组件共用 6 个面 12 条边的定义。如果现在需要知道群组中每一个平面和边线的位置,则需要从这个群组出发,向下查找所有 120 个面和 240 条边;如果群组内的组件实例内部还有组件,就继续向下查找。由于这 20 个组件内部的图元在模型定义中和上两层的群组没有直接的关联,需要借助中间层实现下一层的查找,就像欧洲中世纪的谚语“我的附庸的附庸不是我的附庸”一样,因此用“附庸的附庸”作为本期的题目。

ae62f76753d9aca3aaf29c9e9f89687c.webp

群组中包含20个相同定义的组件,每个组件中有6个面12条边

一、子模型的搜索

SketchUp 模型中呈现的每一个图元,都通过实例路径进行表示,无论组件复用多少次,每一个组件的每一个图形要素都有唯一的实例路径(InstancePath)用于空间定位(详见上一篇第二部分)。所以获取一个组件内所有图形的位置,关键在于逐层获取这些图形的实例路径,这同样需要使用到递归的方法,与上一篇查找“影子”图元的逻辑是相似的。只不过当时是通过组件定义(definition)去寻找所有同定义的实例(instance),而现在是通过组件实例去找所有定义中的图元,查找路径的逻辑可以用下图表示。

363987aa1c2ac06f9fae29a1f1d4a2b8.webp

上一篇文章中构造了一个 InstancePathTree 类,通过 add_child 方法将父模型记录为当前模型的子节点,从而实现组件嵌套树状结构的解析。这里同样可以使用 InstancePathTree 类,将子模型作为当前模型的子节点,即可反方向构建树状结构。

在本部分末,呈现了在 InstancePathTree 类中新增的几个类方法和实例方法第 6 行的 subordinates 方法通过调用先前的 recur_paths 方法递归生成树状结构的每一条路径。由于创建树的方向不同,需要用 .reverse 方法对先前自下而上的路径结果进行翻转。

第 15 行的 recur_find_subordinate 类方法,用于递归创建当前节点模型的子节点,当模型拥有 :definition 方法时,说明当前模型是群组或者组件,存在子模型,所以调用 add_child 方法将组件定义中的所有子模型创建为子节点,并分别对子节点进行相同的递归运算。如果当前节点的模型是 nil 则表示当前节点是世界模型,从 Sketchup .active_model .entities 中读取子模型信息。 如果不是组件或者群组,其他图元调用此方法不会有任何动作,也就不会进入下一层递归,不过更好的处理方式是先判断图元类型再决定是否递归,性能效果要好些。

第 32 行和第 48 行的两个类方法都是通过特定的条件返回所有子模型的实例路径。其中 check_subordinate_instancepath 方法通过一个路径返回所有次级路径,例如文章开头的案例中,如果双击打开群组,将当前路径作为参数执行该方法,即可返回 120 个平面和 240 条边线。

a56f1daef4b5b0f608b7f26ae6dba15e.webp

  check_subordinate_definition  方法则以一个组件定义为参数,判断组件定义内的所有图元路径,这个方法对于绘制临时的组件图形很有帮助。

以下是 InstancePathTree 类新增的代码部分:

      
        
          class InstancePathTree
        
      
      
          # 此处省略了部分上一篇文章中呈现的代码
      
      
          # 只展示新增的代码
      
      
          # 因此直接复制粘贴是无法运行的
      
      
          # 如需复制完整代码可以见文末的链接
      
      
          def subordinates
      
      
            root_path = @instpath.to_a
      
      
            result=[]
      
      
            recur_paths(result,[])
      
      
            return result.map{|path|Sketchup::InstancePath.new(root_path+path.compact.reverse)}
      
      
          end
      
      
      
          class << self
      
      
            # 此处省略了部分上一篇文章中呈现的代码
      
      
            def recur_find_subordinate(node)
      
      
              entity = node.instance
      
      
              if entity.respond_to?(:definition) then
      
      
                entity.definition.entities.each{|ent|
      
      
                  child = node.add_child(ent)
      
      
                  recur_find_subordinate(child)
      
      
                }
      
      
              elsif node.instance.nilthen
      
      
                Sketchup.active_model.entities.each{|ent|
      
      
                  child = node.add_child(ent)
      
      
                  recur_find_subordinate(child)
      
      
                }
      
      
              end
      
      
            end
      
      
            private :recur_find_subordinate
      
      
      
            # 返回path之下的所有path
      
      
            def check_subordinate_instancepath(instance_path)
      
      
              instance_path=[] if instance_path.nil?
      
      
              instpath = Sketchup::InstancePath.new(instance_path)
      
      
              if instpath.empty? then
      
      
                result = InstancePathTree.new(nil)
      
      
                result.instpath = []
      
      
              else
      
      
                instpath_ary = instpath.to_a
      
      
                result = InstancePathTree.new(instpath_ary.pop)
      
      
                result.instpath = instpath_ary
      
      
              end
      
      
              recur_find_subordinate(result)
      
      
              return result
      
      
            end
      
      
      
            # 返回definition之下的所有path
      
      
            def check_subordinate_definition(definition)
      
      
              instpath=[] if instpath.nil?
      
      
              raise ArgumentError.new("参数definition必须要有entities成员。") unless definition.respond_to?(:entities)
      
      
              result = InstancePathTree.new(nil)
      
      
              result.instpath = []  
      
      
              definition.entities.each{|ent|
      
      
                child = result.add_child(ent)
      
      
                recur_find_subordinate(child)
      
      
              }
      
      
              return result
      
      
            end
      
      
      
            # 根据参数选择性调用前两个方法
      
      
            def check_subordinate(instpath_or_definition)
      
      
              if instpath_or_definition.respond_to?(:entities) then
      
      
                check_subordinate_definition(instpath_or_definition)
      
      
              else
      
      
                check_subordinate_instancepath(instpath_or_definition)
      
      
              end
      
      
            end
      
      
          end
      
      
        
          end
        
      
    


二、具体的使用示例

首先将整个 InstancePathTree 类的代码复制到控制台中执行,或者直接用 load 方法加载 “instpath_helper.rb” 文件(在本人github/gitee主页中可以找到,链接在文末)。之后就可以在控制台中测试前文介绍的方法,以下提供几个案例。

①返回整个模型的全部路径

其中第 1 行最后的 ;nil 用于避免将 paths 结果打印在控制台输出中。这是因为,如果模型很大,打印结果将会消耗大量时间,造成软件假死。

      
        paths = InstancePathTree.check_subordinate([]).subordinates;nil
      
      
        paths.length
      
      
        
          #=> 1319
        
      
    


②返回当前路径之下的全部路径

其中第 1 行将当前路径保存在变量 apath 中,在第 2 行作为参数返回此路径下的结果。

      
        apath = Sketchup.active_model.active_path
      
      
        paths = InstancePathTree.check_subordinate(apath).subordinates;nil
      
      
        paths.length
      
      
        
          #=> 348
        
      
    


③返回某个组件内部的全部路径

其中第 1~2 行将名称为 "组件#1" 的组件的定义保存在变量 cpmdef 中,在第 4 行作为参数返回该组件定义之下的全部路径。

      
        defs = Sketchup.active_model.definitions
      
      
        cpmdef = defs.select{|d|d.name=="组件#1"}[0]
      
      
        
          #=> #<Sketchup::ComponentDefinition:0x000001bac7641300>
        
      
      
        paths = InstancePathTree.check_subordinate(cpmdef).subordinates;nil
      
      
        paths.length
      
      
        
          #=> 35
        
      
    


④仅返回边线的路径

借用上一个案例中返回的 paths 结果,可以进一步筛选组件中的所有以边线图元为末梢的路径。其中 .select 方法表示筛选,其后的花括号内条件中的 .leaf 是 InstancePath 的方法,返回最末梢的图元。

      
        spath = paths.select{|path|path.leaf.is_a? Sketchup::Edge}
      
      
        p spaths
      
      
        
          #=> [#<Sketchup::InstancePath:0x000001bacf8a35f0>, 
        
      
      
        
          #=> ...,
        
      
      
        
          #=> #<Sketchup::InstancePath:0x000001bacf8a1ae8>]
        
      
    


⑤具体实例路径下所有边线的坐标显示

多层的嵌套会让组件中的坐标处理变得复杂而不易想象,通过路径的获取可以简化这个坐标转换的过程,因为每一个路径都能直接返回对应的相对位置变换矩阵,给多层嵌套的模型坐标计算提供便利。例如以下代码返回一个具体路径的组件内所有边线的端点坐标。

      
        
          # 先打开要测试的组件实例路径
        
      
      
        test_path = Sketchup.active_model.active_path
      
      
        Sketchup.active_model.active_path = []
      
      
        
          # 自动退出组件编辑,避免图元所在entities与active_entities一致而返回世界坐标
        
      
      
        paths = InstancePathTree.check_subordinate(test_path).subordinates;nil
      
      
        paths.select!{|path|path.leaf.is_a? Sketchup::Edge}
      
      
        paths.each{|path|
      
      
          trans = path.transformation
      
      
          v1 = path.leaf.vertices[0]
      
      
          v2 = path.leaf.vertices[1]
      
      
          p1 = trans*v1.position
      
      
          p2 = trans*v2.position
      
      
          puts "Line #{p1} To #{p2}"
      
      
        };nil
      
      
        
          #=> Line (254 m, 254 m, 0 m) To (254 m, 254 m, 168.4105 m)
        
      
      
        
          #=> Line (254 m, 254 m, 0 m) To (254 m, 508 m, 0 m)
        
      
      
        
          #=> Line (508 m, 508 m, 0 m) To (508 m, 254 m, 0 m)
        
      
      
        
          #=> ...
        
      
    


三、图元族谱的总结

到目前为止,“SketchUp 图元的族谱”系列的三篇文章已经全部结束,第一篇“包含与嵌套”介绍 SketchUp 中群组和组件的嵌套方式,区分了组件实例与组件定义的区别,并对世界坐标系和组件坐标系进行了辨析,为后两篇内容提供了一些背景内容。而第二篇“对影成三人”和本篇“附庸的附庸”则分别从向上和向下两个搜索方向介绍如何通过模型中一个确切的结构位置,计算出与之相关的所有图元实例路径。两个功能都需要借助递归的方式构建模型结构树,因此都需要使用 InstancePathTree 类实现。

e33787d6438c33658991ede21d07e85a.webp

三篇文章对应的模型嵌套结构

而两篇如此不厌其烦地 构造 自定义类 现路径的查找,都是为了更便捷地对嵌套模型的坐标转换进行描述。模型嵌套往往会让人对不同层图元的坐标产生困惑,而 InstancePath 是模型坐标定位的本质,直接计算符合条件的 InstancePath 就可以通过 .transformation 方法便捷地获取代表相对位置的变换矩阵 ,只需要将路径末端图元 .leaf 的坐标与相对位置变换矩阵相乘,就能轻易得到这个末端图元的世界坐标,这也就是两篇文章中 InstancePathTree 类的意义。

(完)



从SU-2022-01期开始,本人的 SketchUp Ruby “小功能与灵感” 的文章中涉及的功能会在 Gitee/GitHub 中相应更新。这样一来,如果日后发现存在bug需要修正或者是想追加相关的功能,都有极大的便利。

这个代码仓库同样也包含大部分之前文章的代码,可以访问以下网址查看,也欢迎大家共同提交修改:

https://gitee.com/apiglio/sketchup-script-tool

https://github.com/Apiglio/SketchupScriptTool

本期代码在 class/ instpath_helper.rb  文件中,文中的 InstancePathTree 类 按如下方式调用:

      
        
          #选择一个组件
        
      
      
        sels = Sketchup.active_model.selection
      
      
        inst = sels[0]
      
      
        
          # 如果查找当前组件定义中的所有路径:
        
      
      
        n = InstancePathTree.check_subordinate(inst.definition)
      
      
        
          # 如果查找整个模型的所有路径:
        
      
      
        n = InstancePathTree.check_definition(nil)
      
      
        p n.subordinates
      
      
        
          #=> [...]
        
      
    

之前更新的教程到组件部分之后一直就没有再更新,总觉得需要一个比较好的讲述逻辑,又希望能够统一组件、群组和图像三种图元,又要兼顾组件定义和定义列表这两个实体,所以总显得杂乱无章。因此打算先在这个系列大概聊一聊组件的一些特点,也是给详细的组件教程进行一个预热。

SketchUp ruby的中文资料相对较少,并且相对小众,欢迎加入 SketchUp Ruby 自学俱乐部 Q群(群号:368842817)。



本文编号:SU-2023-04


浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报