Fastest way to get all vertices

Hello,

After nobody here liked my extension ideas i decided how hard can it be. Well its pretty hard. I’m trying to get all the vertices in my model (global position not component origin positions). I thought it would be a simple task and its really become a nightmare. I keep rewriting the code. it works but its far too slow. Can anyone show me how to write it so that it can get them in a reasonable time. heres the fastest version I have so far. Thanks

def self.collect_all_vertices
  model = Sketchup.active_model
  puts "Collecting all vertices from the model..."

  # Step 1: Find all top-level component instances and free-floating faces/edges
  top_level_instances = model.active_entities.grep(Sketchup::ComponentInstance)
  free_floating_entities = model.active_entities.grep(Sketchup::Face) + model.active_entities.grep(Sketchup::Edge)

  # Step 2: Get a unique list of component definitions from top-level instances
  top_level_definitions = top_level_instances.map(&:definition).uniq
  puts "Found #{top_level_definitions.size} unique top-level component definitions."

  # Step 3: Build a complete template vertex dictionary for each top-level definition
  definition_templates = {}

  build_definition_template = lambda do |definition, parent_transformation = Geom::Transformation.new, depth = 0|
    template_vertices = {}
    puts "  Building template for definition: #{definition.name} at level #{depth}"

    definition.entities.each do |entity|
      if entity.is_a?(Sketchup::Face) || entity.is_a?(Sketchup::Edge)
        entity.vertices.each do |vertex|
		  vt = (parent_transformation * vertex.position)
          key = "#{vt.x},#{vt.y},#{vt.z}"
          template_vertices[key] = vt
        end
      elsif entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
        sub_transformation = parent_transformation * entity.transformation
        puts "    Subcomponent: #{entity.definition.name} at level #{depth + 1}"
        sub_vertices = build_definition_template.call(entity.definition, sub_transformation, depth + 1)
        template_vertices.merge!(sub_vertices)
      end
    end

    puts "  Template for #{definition.name} finished. Total vertices at this level: #{template_vertices.size}"
    template_vertices
  end

  top_level_definitions.each_with_index do |definition, idx|
    puts "Building template for top-level definition #{idx + 1}/#{top_level_definitions.size}..."
    definition_templates[definition] = build_definition_template.call(definition)
  end

  # Step 4: Process free-floating faces and edges
  vertices = {}

  free_floating_entities.each do |entity|
    if entity.is_a?(Sketchup::Face) || entity.is_a?(Sketchup::Edge)
      entity.vertices.each do |vertex|
        key = "#{vertex.position.x},#{vertex.position.y},#{vertex.position.z}"
        vertices[key] = vertex.position
      end
    end
  end

  # Step 5: Apply template vertices to all top-level component instances
  puts "Applying templates to top-level instances..."
  top_level_instances.each_with_index do |instance, idx|
    transformation = instance.transformation
    template_vertices = definition_templates[instance.definition]
    template_vertices.each_value do |v|
	  vt = transformation * v
      key = "#{vt.x},#{vt.y},#{vt.z}"
      vertices[key] = vt
    end
    puts "  Instance #{idx + 1}/#{top_level_instances.size}: #{instance.definition.name} - #{template_vertices.size} vertices"
  end

  # Step 6: Convert vertices dictionary to array
  unique_vertices = vertices.values

  puts "Found #{unique_vertices.size} unique vertices."
  unique_vertices
end

Clarification

You are actually getting 3D Point positions (Geom::Point3d) not Vertices (Sketchup::Vertex) which are a model dB object that can be shared among geometric entities such as edges and faces. Ie, the Geom::Point3d objects are not a model object, there are a virtual geometric helper class object.

And the return type for your collect_all_vertices() method in YARDoc commentary, had you included it, would be:

# return Array<Geom::Point3d>
def collect_all_vertices

Slowness

(a) Generally, I/O console output slow relative to other operations in Ruby. There have also been situations in recent versions (especially on Windows with the new Qt console window,) where the output sent by code to stdout does not arrive or be displayed in the execution order expected. This makes the console problematic for use in debugging.

What I have done myself is write a method named emit that can alternatively either send to the global puts() method, OR on Windows use a Fiddle function to call the Windows system’s OutputDebugString function and then while debugging I will open the DebugView64 applet which will display any string messages sent from the aforementioned function. It is also likely that MS Visual Studio and MS Visual Studio Code’s internal console could display these messages. (I prefix them with "SketchUp:" and the name of the extension so I can filter the list in DebugView64.)

(b) Ruby comparison of text strings is also rather slow. So, using Hash objects with string keys I believe will actually take away the speed advantage of a hash.
I understand (looking at your code) that you are attempting to uniquify the vertex positions by using string keys of their coordinates, but I myself would not do this. Since Sketchup::Vertex objects are good Ruby objects, and their comparison == method is inherited from BasicObject they will be compared with one another based upon object identity which is relatively “screamin’ fast” in Ruby.
I would uniquify a geometric context’s vertices based on direct comparison of the collected Sketchup::Vertex objects, and then afterward &map! the vertices array to point objects.

(c) Another thing that can eat time is unnecessary reference assignments and repeated method calls to the same method. This is a tradeoff between the former and latter.

Thoughts

There is just no way to slow down the walking of the component hierarchy. The bigger the model and the more components, the longer it will take.

The only alternative to the way you’ve done it, is to walk every instance chain and build InstancePaths as you go and use the path’s #transformation method.

What is the time it is taking now?

3 Likes

Now it works on small test models almost instantly but on the model i actually need to use it on its never completed. The console goes unresponsive and no matter how long i wait nothing happens.

it gets to 3/9 top level defintions before freezing every time. I’m surprised there isn’t a way to do this directly with the API.

How many levels deep is the component hierarchy?

You can see if the console is the issue, by redefining the puts method in your module as a no-op, ie …

module MyNamespace
  module MyExtension

    extent self

    @debug = true

    def debug(arg)
      @debug =( arg ? true : false )
    end

    def puts(text)
      Kernel.puts(text) if @debug
    end

    def collect_all_vertices
      # method code
    end

    # Etc., ... etc., ...

  end # extension submodule
end # top-level namespace

Of course, you’d need to call debug(false) to switch off the console output.

I removed all the output and its not making a difference. I don’t think tinkering with the syntax can really help in this situation. The entire structure of how the model is accessed needs to be changed. And it seems like you are right that there is no fast way to do it. I got it to finish on a 30,000 vertex model with 7 top level components and about 4-5 subcomponents in each in about 15 seconds so is that pretty decent?

I suppose so. How does that compare to outputting all those IO text to the console?

Rhetorically, then why ask if there is a better way?

I am going to disagree with you as I explained above using string keyed hashes is not necessary and can be avoided which also avoids creating all those interpolated coordinate strings.

It also just dawned on me that the way you’ve done this using the comparison of the string keys is poor practice. SketchUp has an internal coordinate tolerance which is built into the Geom::Point3d#== method. This is a numerical comparison which is much faster and more precise than comparing text characters.

Only the author of the model can change the structure.
It is what it is and is usually organized hierarchically for a reason.

You could explode all instances, but that also causes a very slow operation where the SketchUp engine checks for geometric merging of edges and faces each time explode() is called. Subsequent calls take longer and longer each time as the amount of geometry in the context grows.

This is a subjective statement. The “live” API was designed originally as a means of enhancing internal SketchUp tasks.

There are other 3D geometry formats that use all global coordinates, but their data is larger because they do not use transformed instances. I think that the OBJ format is one of these.

SketchUp’s SKP is not one of these global coordinate formats, which I prefer myself. We have no choice but to walk the component hierarchy and apply cumulative transforms. It is the way it is.

The Pro editions have file format exporters which can be called from the API to write out into another format. If you are still using Make 2017 you’ll not be able do this.

The only faster way than using interpretive Ruby is to use compiled C using SketchUp’s C SDK. But there is no “magic function” in the C SDK either. You must also walk the component hierarchy and apply cumulative transforms. (There may be examples in the SDK’s "samples" folder.)


Anyway, I’ve given my advice. Use it or not. Your choice.
We’ll see if anyone else has any other bright ideas.

okay I wanted to test your theory about the keys. You were right. Its significantly faster. So it appears the string keys did contribute a lot more to the time it takes than I thought. But its still not fast enough to be useable for the purpose I wanted unfortunately. I noticed when sketchup is too slow it initially renders your stuff as a wireframe box that bounds your component. I could use this wireframe box to know the area the vertices are contained in if there is a way to access it quickly without needing to load the component and recurse through it.

What is the purpose of getting all vertices? Do you want to draw a wireframe (if so, you should use Edges instead)? or another purpose?

This is known as the bounds. There are two kinds of bounds:

  • untransformed (unscaled) comp_inst.defintion.bounds
  • transformed (usually scaled) comp_inst.bounds

The bounds() method is inherited by all Drawingelement subclasses and returns a Geom::BoundingBox object which is aligned with the axes.

Getting the corner points of a component instance’s bounds:

bbox = comp_inst.bounds
corners = (0..7).map { |i| bbox.corner(i) }

Ref:


However, be aware that if the object is rotated, it’s bounds may be larger than it would if it was aligned with the axes.

:bulb: You can also check the .positions method, the last one in TT_Lib2\entities.rb …

This is perfect. The reason I wanted to get all vertices was to check their position against the vertex the user just created. But if I can filter them by the bounding box first I can eliminate the vast majority that are not near the point the user just made. Hopefully these bounding boxes can be obtained quickly. Thanks

Basic comparison then might use BoundingBox#contains?

bbox = inst.bounds
if bbox.contains?(user_pt)
  # do something
else
  # do otherwise
end

Also be aware that most SketchUp API collection classes mix in the Ruby core Enumerable module which bring in filtering methods. (In Ruby, when a library module is mixed in, the module is inserted into the class’ ancestor chain as a pseudo-superclass at the point it is prepended or appended.)

bboxes = entities.map(&:bounds)

# Boolean test if contained:
contained = bboxes.any? { |bbox| bbox.contains?(user_pt) }

# Boolean test if NOT contained:
non_contained = bboxes.none? { |bbox| bbox.contains?(user_pt) }

# Find the first bounding box that contains the user entered point:
# (returns a BoundingBox or nil)
within = bboxes.find { |bbox| bbox.contains?(user_pt) }

# Find ALL of the bounding boxes that contains the user entered point:
# (returns an array which might be empty)
within = bboxes.find_all { |bbox| bbox.contains?(user_pt) }

# etc.
1 Like

So i am trying to do the bounds but i am running into problems. The line in the picture is within the tolerance of a vertex in a subcomponent of the main component. You can see the first bounding box check passes but the next level down, the subcomponents bounding box check fails. Which seems to indicate I am doing something wrong with subcomponent instances bounding boxes’ transformations. Are subcomponents bounds in local coordinates or global coordinates? if so how can I transform the bounds to the same coordinate space as “point”

class VertexChecker
  def self.check_for_point(entities, point, distance_tolerance, parent_transformation = Geom::Transformation.new, position_set = {})
    entities.each do |entity|
      if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
        entity_bb = entity.bounds
        expanded_bb = Geom::BoundingBox.new
        expanded_bb.add(Geom::Point3d.new(entity_bb.min.x - distance_tolerance, entity_bb.min.y - distance_tolerance, entity_bb.min.z - distance_tolerance))
        expanded_bb.add(Geom::Point3d.new(entity_bb.max.x + distance_tolerance, entity_bb.max.y + distance_tolerance, entity_bb.max.z + distance_tolerance))
        
        unless expanded_bb.contains?(point)
          puts "#{entity} bounding box did not contain point!"
          next  # Skip this entity if it doesn't contain the point within the tolerance
        end
		puts "#{entity} bounding box contained point!"
        child_entities = entity.definition.entities
        child_transformation = parent_transformation * entity.transformation
        result = self.check_for_point(child_entities, point, distance_tolerance, child_transformation, position_set)
        return result if result
      elsif entity.is_a?(Sketchup::Face) || entity.is_a?(Sketchup::Edge)
        entity.vertices.each do |vertex|
          transformed_position = vertex.position.transform(parent_transformation)
          dist = transformed_position.distance(point)
          
          if dist < distance_tolerance && dist > 0.0
            puts "Closest vertex found at #{transformed_position} with distance #{dist}"
            return transformed_position  # Return the vertex immediately if it's within tolerance
          end

          key = [transformed_position.x.to_f, transformed_position.y.to_f, transformed_position.z.to_f]
          position_set[key] = transformed_position
        end
      end
    end

    puts "Nothing found"
    return false
  end

Normally local. But if the active editing context (model.active_path) is within the parent the I think that the API returns global coordinates.

Also be aware of the Sketchup::Model#edit_transform method.

Ok i got it working! the bounding box had needed to be transformed.

def self.check_for_point(entities, point, distance_tolerance, parent_transformation = Geom::Transformation.new, position_set = {})
  entities.each do |entity|
    if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
      entity_bb = entity.bounds
      expanded_bb = Geom::BoundingBox.new
      expanded_bb.add(Geom::Point3d.new(entity_bb.min.x - distance_tolerance, entity_bb.min.y - distance_tolerance, entity_bb.min.z - distance_tolerance).transform(parent_transformation))
      expanded_bb.add(Geom::Point3d.new(entity_bb.max.x + distance_tolerance, entity_bb.max.y + distance_tolerance, entity_bb.max.z + distance_tolerance).transform(parent_transformation))
      
      unless expanded_bb.contains?(point)
        puts "#{entity} bounding box did not contain point!"
        next  # Skip this entity if it doesn't contain the point within the tolerance
      end
puts "#{entity} bounding box contained point!"
      child_entities = entity.definition.entities
      child_transformation = parent_transformation * entity.transformation
      result = self.check_for_point(child_entities, point, distance_tolerance, child_transformation, position_set)
      return result if result
    elsif entity.is_a?(Sketchup::Face) || entity.is_a?(Sketchup::Edge)
      entity.vertices.each do |vertex|
        transformed_position = vertex.position.transform(parent_transformation)
        dist = transformed_position.distance(point)
        
        if dist < distance_tolerance && dist > 0.0
          puts "Closest vertex found at #{transformed_position} with distance #{dist}"
          return transformed_position  # Return the vertex immediately if it's within tolerance
        end

        key = [transformed_position.x.to_f, transformed_position.y.to_f, transformed_position.z.to_f]
        position_set[key] = transformed_position
      end
    end
  end

  puts "Nothing found"
  return false
end
1 Like

You also might have the option to transform the point (or a clone of it) into the boundingbox’s local coordinates.

oddly enough this didn’t work, i first tried applying parent transformation to the point which hadn’t fixed it. So i tried applying the parent transformation to the bounding box which worked. Now I have a new issue. When i pass the point along with entities I am in the points coordinate space and search for entities there. But if the point is on an edge in the geometry of a subcomponent, then it won’t search outward to the component that the point is in. So I need to figure out how to get this function to search in the other direction too, from subcomponents out to components. Or perhaps start the search by getting all top level geometry in the model.

If I do model.entities instead of active_entities when i first call the function that will start me from the top level of the model right? but If thats the case the point will be in local coordinates, I need to get the point to global coordinates. So if i have a point and and the transformation that was applied to it to get it to its current coordinatesm how can I reverse that? Inverse transform?

Edit:
I have solved the problem. Big thank you to everyone that provided feedback. I will hopefully release my first extension in a few days.

1 Like

Correct. But if the editing context is the top-level, then
model.entities == model.active_entities
and
model.active_path.nil? is true.

The model.entities are in global coordinates from the global ORIGIN which cannot be moved. (And please do not reassign the ORIGIN constant as it could affect other extensions.)

It always better for your brain if you solve the challenge yourself.

1 Like