Best way to iterate all nested entities
-
In reality,... instances (Group or Component,) do NOT really have entities collections.
It is their
definition
that has theentities
collection. (Thegroup.entities()
method is a shortcut that often deceives novices into thinking that thegroup
instance has entities. It actually is a wrapper method forgroup.entities.parent.entities
)Also a the
Group
class is a special kind ofComponentInstance
class.Both have a parent
ComponentDefinition
So it is much more efficient to iterate the model's
DefinitionList
collection, and access the instances through a definitionsinstances
collection.
EDIT: Fixed second paragraph,
group.entities.parent.entities
wasgroup.parent.entities
in error. -
@dan rathbun said:
@david. said:
I've been using recursion ...
The bad news is that recursion is bad in Ruby, and worse in embedded Ruby.
It can cause a stack overflow, which crashes Ruby, and in the case of SketchUp embedded Ruby, will then crash SketchUp.
The good news is that you can write a method to collect the definitions whose instances are present in the selection.
I use recursion for digging into nested components levels - because the recursion level is not going to max out the call stack unless you have a model that was designed to crash it. Nobody nests geometry that deep.
Recursing when traversing over connected geometry on the other hand will quickly hit the limit of the call stack.
-
@tig said:
> elsif e.is_a?(Sketchup;;ComponentInstance) > e.parent.entities.each{|face| > next unless face.is_a?(Sketchup;;Face) > ### check for compliance with some 'property' and then > matching_faces << face if match > } > end > }
One comment: This section of the code for components didn't work for me. It wasn't giving any error messages, it just wasn't making any changed to the components in my model. I changed the second line to e.definition.entities.each .... and that worked.
One question: I'm not processing faces like this example, but edges. I'm modifying another script from TIG to delete all vertical edges in my model. I need it to also recurse into groups and components, so I've combined it with the script here.
So ... the first time I run the script, it deletes all vertical lines that are not within groups/components. From groups/components, however, it leaves one vertical line in each. (This is run on a simple test file that has three cubes, one is ungrouped, one is a group, the last in a component.) If I run the script a second time, it deletes the remaining vertical lines from the groups/components. I'd appreciate if anyone could explain what's going on here, and if there is a way to write the code such that each vertical edge is dealt with in the first go.
Many thanks,
Shannon -
The API's
Entities
collection(s), is/are actually thinly-exposed (to Ruby,) C++ collection(s).You CANNOT safely both iterate such a set AND delete items from the set AT THE SAME TIME.
Doing so creates a "fence post error" in which the iteration reference skips one (or more) items in the set.
This has been covered so many times before. (Here and in other forums.)
Seems every newbie must fall for this boo boo.
Use the standard Ruby
to_a()
orgrep()
method to take a "snapshot" Ruby array copy of API collections.
Then iterate THAT Ruby copy, viz:
entities.grep(Sketchup::Edge).each {|e| e.erase! if is_vertical?(e) }
is_vertical?()
is a hypothetical utility query method within your plugin's class or module.Would be something like:
def is_vertical?(edge) vec = edge.start.position.vector_to(edge.end.position).normalize vec == [0,0,1] || vec == [0,0,-1] end
Note that some of those old examples were written before we knew how very fast the
grep()
method was.
USE it to filter collections when you want only one class of object. It is FAST!
(This method comes from the mixin moduleEnumerable
, which is mixed into many collection classes.) -
When "drilling down" into the entities of component or groups, it is so very much faster to go through the model's
definitions
collection, checking each definition if it'sinstances
collection hassize > 0
, (and possibly if it isimage? == false
,) and if so...... delete from the definitions entities collection, and all instances are changed.
-
.grep
is an iterator itself - so no need for.each
:entities.grep(Sketchup::Edge) {|edge| edge.erase! if is_vertical?(edge) }
-
Saying that - bulk methods is much faster than individual actions. Use entities.erase_entities when you erase multiple entities - it also avoids the pitfall of erasing the collection you are erasing from.
-
Yea so you can also do this:
verts = entities.grep(Sketchup::Edge).find_all {|edge| is_vertical?(edge) } entities.erase_entities(verts) unless verts.empty?
P.S. @TT Yea I knew
grep()
is an iterator, but I usually avoid using a block with it, as it returns an array of block results which is a bit weird. (Especially when you expect a smaller subset than the whole.)
I tend to just use it as a filter, and then call another method on the filtered results. IMHO the code is more readable. (.. and I don't confuse myself as much.) -
@dan rathbun said:
Use the standard Ruby
to_a()
orgrep()
method to take a "snapshot" Ruby array copy of API collections.
Then iterate THAT Ruby copy, viz:
entities.grep(Sketchup::Edge).each {|e| e.erase! if is_vertical?(e) }
Thanks Dan! The grep method worked great!
@dan rathbun said:
def is_vertical?(edge) > vec = edge.start.position.vector_to(edge.end.position).normalize > vec == [0,0,1] || vec == [0,0,-1] > end
Your example for how to test for a vertical edge did not work for me, which could be entirely my fault. TIG's code from the other forum I mentioned did.
edge.line[1].z.abs==1
-
To use the same tolerance as SketchUp does, use the methods built into the Ruby API:
vector = edge.line[1] vector.samedirection?(Z_AXIS)
http://www.sketchup.com/intl/en/developer/docs/ourdoc/vector3d.php#samedirection?
The components of a vector are floating point values so they should never be compared without a tolerance. For more information about floating point precision: http://floating-point-gui.de/
-
@tt_su said:
To use the same tolerance as SketchUp does, use the methods built into the Ruby API:
vector = edge.line[1] vector.samedirection?(Z_AXIS)
http://www.sketchup.com/intl/en/developer/docs/ourdoc/vector3d.php#samedirection?
The components of a vector are floating point values so they should never be compared without a tolerance. For more information about floating point precision: http://floating-point-gui.de/
Excellent advice! Do you know whether Point3d#on_line? and #on_plane? also include the tolerance? The API docs don't say.
-
Yes it does. Do does Geom::Point3d, Geom::Vector3d and Length - which is why it's recommended you use those types when doing calculations, instead of using arrays and floats.
Also note that Length + Length == Float (annoyingly). So you need to ensure you have a Length before outputting that to a string.
-
definition.instances.empty? won't give you an accurate indicator of the usefulness of a definition unfortunately. It can cut out some obvious ones to skip, but not all of the useless ones. If an instance is used in a definition, and that parent definition isn't instanced anywhere, the first definition will still say it has one instance. It's not wrong... but it's not helpful either. It would be nice to know if a definition is actually used in your model somewhere. Of course if you purge unused first, you should be fine. This can be especially frustrating when working with a model in which you are not allowed to purge unused definitions.
Example:
- Make a cube
- Make the cube a component
- Make a copy of the component instance
- Make a new component out of the two instances
- Delete the resulting component instance
- Type this in the ruby console: Sketchup.active_model.definitions.each{|df| puts "definition #{df.name} instance count: #{df.instances.size}"};nil
-
Ah yes - very good point.
-
To check if a defn with instances actually has one or more if these inserted in the model, or also it is inside a 'container' that is itself inserted in the model, OR inside something else that is inserted in the model... etc...
Start count=0.
Look at each instance in turn and get its parent.
If its parent is the model then it is inserted (count+=1).
Elsif its parent is another component-defn you'll need to check if that definition has instances and iterate those and if their parent is the model (count+=1 and break) but if its parent is another component-defn repeat the nested testing of that definition having instances etc...With an instance of Component#1 inside Component#2, and two instances of both in model.
` instance_counter()
Component Name: Component#1
All Instances: 4
Inserted Instances: 2
Nested Instances: 2Component Name: Component#2
All Instances: 1
Inserted Instances: 1
Nested Instances: 0true`
With only 2 instances of Component#1
` instance_counter()
Component Name: Component#1
All Instances: 2
Inserted Instances: 0
Nested Instances: 2Component Name: Component#2
All Instances: 1
Inserted Instances: 1
Nested Instances: 0true`
This is only to demonstrate the principal - clearly you'd want to hae a proper model/method that returned the counts for you...
require('sketchup.rb') def instance_counter() def instances?(d) model=Sketchup.active_model cont=0 d.instances.each{|i| parent=i.parent if parent==model cont+=1 else cont+=instances?(parent) end } return cont end model=Sketchup.active_model puts model.definitions.each{|d| next if d.image? || d.group? puts "Component Name; \t#{d.name}" puts "All Instances; \t#{d.instances.length}" count=0 ncount=0 d.instances.each{|i| parent=i.parent if parent==model count+=1 else ncount+=instances?(parent) end } puts "Inserted Instances; \t#{count}" puts "Nested Instances; \t#{ncount}" puts } return true end
Perhaps a method that takes the defn as its argument and returns the instances count in three arrayed integers [all, model, nested] ... [4, 2, 2] - like this:
def instance_count(d=nil) #d=defintion return nil if !d || !d.is_a?(Sketchup;;ComponentDefinition) return false if d.image? || d.group? def instances?(d) model=Sketchup.active_model cont=0 d.instances.each{|i| parent=i.parent if parent==model cont+=1 else cont+=instances?(parent) end } return cont end model=Sketchup.active_model count_array=[d.instances.length] count=0 ncount=0 d.instances.each{|i| parent=i.parent if parent==model count+=1 else ncount+=instances?(parent) end } count_array << count count_array << ncount return count_array end
-
yes, generally this tends to do the trick, but we found that it was a tad faster to have your own lookup table that keeps track of definition relevance. The only downside, is that you are at the mercy of the observer system in that case, but the observers these days seem fairly stable, so that isn't currently an issue.
Advertisement