Merge vertices by distance in a complex mesh

Hi there,

I’m using chat to try to help me write a short script that will evaluate the distances of all the vertices in a selected mesh. Vertices that sit at a distance that is lower than a threshold distance set by the user are automatically merged to a single point at the centroid they form.

This comes in really handy when dealing with complex topo meshes that have draped cad lines greatly increasing complexity. Very often in such cases, the large mesh will have a lot of vertices that are extremely close together. Across acres and acres of land lie hundreds of tiny lines too small to be seen - which show up in Sketchup with <~0" measurements.

Here is the condition of the script when I finally gave up on Chat:
Do you see where I’m going wrong here?

require 'sketchup.rb'

module Example
  module CollapsePoints

    def self.activate_main_window
      Sketchup.send_action("activateMainWindow:")
    end

    def self.get_user_input
      prompts = ["Collapse Distance:"]
      defaults = ["1.0"]
      input = UI.inputbox(prompts, defaults, "Enter Collapse Distance")
      return nil unless input
      input.first.to_f
    end

    def self.create_merged_point(entities, points)
      # Find the centroid of the points
      centroid = points.reduce(ORIGIN) { |sum, point| sum + point.vector_to(ORIGIN) } / points.size
      # Merge the points to the centroid
      merged_point = entities.add_cpoint(centroid)
      points.each do |point|
        entities.add_cline(point, centroid) unless point == centroid
      end
      merged_point
    end

    def self.collapse_points(distance)
      model = Sketchup.active_model
      entities = model.active_entities
      selection = model.selection

      if selection.empty?
        UI.messagebox("Please select some points or edges.")
        return nil
      end

      # Start an operation so this can be undone in one step
      model.start_operation('Collapse Points', true)

      # Get all the unique vertices from the selected edges
      vertices = selection.grep(Sketchup::Edge).map(&:vertices).flatten.uniq
      # Create a hash to associate each vertex with a group (initially, itself)
      vertex_groups = vertices.map { |v| [v, [v]] }.to_h

      # Group vertices within the specified distance
      vertices.combination(2) do |v1, v2|
        if v1.position.distance(v2.position) <= distance
          group1 = vertex_groups[v1]
          group2 = vertex_groups[v2]
          # Merge groups if they are different
          if group1 != group2
            merged_group = group1 | group2
            merged_group.each { |v| vertex_groups[v] = merged_group }
          end
        end
      end

      # Collapse the points of each group to their centroid
      vertex_groups.values.uniq.each do |group|
        points = group.map(&:position)
        create_merged_point(entities, points)
      end

      # Commit the operation
      model.commit_operation
      nil
    end

    unless file_loaded?(__FILE__)
      UI.menu("Plugins").add_item("Collapse Points") {
        activate_main_window
        distance = get_user_input
        collapse_points(distance) if distance
      }
      file_loaded(__FILE__)
    end
  end
end

Please post code correctly with proper indentation:


Sketchup.send_action("activateMainWindow:")

This is an unknown (undocumented) action string. It does not work on Windows platform.
It could be an AI “invented” solution that really does not exist.

Use the Sketchup.focus module method instead.


The code does not actually move the vertices. To do this, vertex objects must be moved with either the Entities#transform_by_vectors or Entities#transform_entities methods. The first method using vectors is likely your need.

There is no real point in drawing clines from the old point to the new centroid. Just collect the vectors into an array whose members match the position of each vertex in the vertices array. See Geom::Point3d#vector_to method.
Ie:
vectors = vertices.map { |vertex| vertex.position.vector_to(centroid) }

1 Like

:wink:

Recently, there was a topic too where the AI invented methods that seemed formally true but do not exist.

1 Like

With vertex tools you can merge vertices

You can do that with vertex tools if you don’t know how to write a script.

Thanks - that’s a great solution for problems where I’m just collapsing a single tangle. In this case, I have acres of topology with a lot of little tangles - most of which I can’t even see until I’m trying to manipulate the terrain in some way. So I need something that can evaluate the whole mesh and auto-collapse little constellations of points that are super close to one another.

In a way it’s nice to know how dumb AI still is at this point - but I suppose that will change eventually. At this point, for someone like me with basically no experience thinking about these kinds of problems, I’m unable to tell the difference between what look like good ideas and reasonable syntax (but which is entirely hallucinated) from actual workable code.

Dan - thank you so much for your help. It is very generous of you to suffer along with me. In a way I am glad that AI is still bad enough (for the time being) that our need for fellow human beings is still quite vital. When I told Chat what you said, it readily agreed, and then tried its best to fix the code per your advice. However, being Chat, it still failed to create a workable script. Here is what it created, and following is the error message that Ruby generated.

require 'sketchup.rb'

module Example
  module CollapsePoints

    def self.get_user_input
      prompts = ["Collapse Distance:"]
      defaults = ["1.0"]
      input = UI.inputbox(prompts, defaults, "Enter Collapse Distance")
      return nil unless input
      input.first.to_f
    end

    def self.collapse_points(distance)
      model = Sketchup.active_model
      entities = model.active_entities
      selection = model.selection

      Sketchup.focus

      if selection.empty?
        UI.messagebox("Please select some points or edges.")
        return nil
      end

      model.start_operation('Collapse Points', true)

      vertices = selection.grep(Sketchup::Edge).map(&:vertices).flatten.uniq
      vertex_groups = vertices.map { |v| [v, [v.position]] }.to_h

      vertices.combination(2) do |v1, v2|
        if v1.position.distance(v2.position) <= distance
          group1 = vertex_groups[v1]
          group2 = vertex_groups[v2]
          if group1 != group2
            group1.concat(group2).uniq!
            group2.each { |v| vertex_groups[v] = group1 }
          end
        end
      end

      vertex_groups.values.uniq.each do |group|
        centroid = Geom::Point3d.new(0, 0, 0)
        group.each { |point| centroid = centroid + point.vector_to(ORIGIN) }
        centroid = centroid / group.size

        vectors = group.map { |vertex| vertex.position.vector_to(centroid) }
        model.active_entities.transform_by_vectors(group, vectors)
      end

      model.commit_operation
      nil
    end

    unless file_loaded?(__FILE__)
      UI.menu("Plugins").add_item("Collapse Points") {
        distance = get_user_input
        collapse_points(distance) if distance
      }
      file_loaded(__FILE__)
    end
  end
end

error message following:

Error: #<NoMethodError: undefined method `/' for Point3d(5.83727, -14.9898, 0):Geom::Point3d>
<main>:44:in `block in collapse_points'
<main>:41:in `each'
<main>:41:in `collapse_points'
<main>:57:in `block in <module:CollapsePoints>'

This error message appeared after I ran the script on a very simple piece of geometry - a simple 24" x 24" square with a 1" square at its center. With everything selected and the collapse distance set to 1 and also to other values, it returned variations on this basic message. Looks like Chat didn’t define a method properly.

For what it is worth - here is the latest state of the script, after trying repeatedly to get Chat to think through its logic and pay attention to Dan’s feedback.


require 'sketchup.rb'

module Example
  module CollapsePoints

    def self.get_user_input
      prompts = ["Collapse Distance:"]
      defaults = ["1.0"]
      input = UI.inputbox(prompts, defaults, "Enter Collapse Distance")
      return nil unless input
      input.first.to_f
    end

    def self.collapse_points(distance)
      model = Sketchup.active_model
      entities = model.active_entities
      selection = model.selection

      Sketchup.focus  # Corrected to use Sketchup.focus

      if selection.empty?
        UI.messagebox("Please select some points or edges.")
        return nil
      end

      model.start_operation('Collapse Points', true)

      vertices = selection.grep(Sketchup::Edge).map(&:vertices).flatten.uniq
      vertex_groups = vertices.map { |v| [v, [v]] }.to_h

      vertices.combination(2) do |v1, v2|
        if v1.position.distance(v2.position) <= distance
          group1 = vertex_groups[v1]
          group2 = vertex_groups[v2]
          if group1 != group2
            merged_group = group1 | group2
            merged_group.each { |v| vertex_groups[v] = merged_group }
          end
        end
      end

      vertex_groups.values.uniq.each do |group|
        total_vector = Geom::Vector3d.new(0, 0, 0)
        group.each { |vertex| total_vector += vertex.position.vector_to(ORIGIN) }
        average_vector = Geom::Vector3d.new(
          total_vector.x / group.size,
          total_vector.y / group.size,
          total_vector.z / group.size
        )
        centroid = ORIGIN.offset(average_vector)

        vectors = group.map { |vertex| vertex.position.vector_to(centroid) }
        entities.transform_by_vectors(group, vectors)
      end

      model.commit_operation
      nil
    end

    unless file_loaded?(__FILE__)
      UI.menu("Plugins").add_item("Collapse Points") {
        distance = get_user_input
        collapse_points(distance) if distance
      }
      file_loaded(__FILE__)
    end
  end
end

Do you have an example .skp you’ve been using with this that you’d share?

You are collapsing the points but forgeting that those points belong to edges… You need to move the edges points to the center position.

1 Like

When I run the script, it deletes the points below the distance threshold - instead of merging them. It also offsets the remaining geometry -1" along the green axis.

The little squares were each 1" x 1". Instead of collapsing them, the script basically removed the lines connecting the points. I think the points are still there - invisible to my eye, but they show up as elements when I select the empty space.

Woah! It kind of worked. The script didn’t like the floating 1x1 square, but when I connected it to other points with lines, it collapsed them down to a single point. Unfortunately, it isn’t really a point. It still has faces and lines - of zero dimension. And it moves the whole mesh down an inch on the green axis. Why?



image

image
image