Copying faces using their polygon mesh

Hey all! I’ve seen a couple of different threads and discussions about the best way to make copies of Sketchup::Faces using the Ruby API. Most of the advice seems to be to create the new face using the outer loop, and then punch holes using the inner loop. I tried playing around with various versions of that, including adding all the inner loop and outer loop edges (not using add_face) and then invoking Edge.find_faces.

None of them worked well with some of my faces, which had several holes punched in them. (And to be fair to the API, the Sketchup UI itself often struggles to find and punch them when I try to do so manually.)

Anyway, I digress: after playing around with a bunch of different methods, I came up with this one, which is based on using the face’s polygon mesh.

# Returns an array with the given edge's points sorted in a standard order for easy equality checks
# @param edge [Sketchup::Edge] the edge to normalize
# @return [Array<Geom::Point3d>] the start and edge points of the edge in normalized order
def self.normalized_edge(edge)
  [edge.start.position, edge.end.position].sort_by { |pt| pt.x + pt.y + pt.z }
end

# Copies the edges and faces for the given face element to the given group
# @param face [Sketchup::Face] the face to clone
# @param entities [Sketchup::Entities] the entities list to clone the face into
# @return [Array<Sketchup::Face>] the cloned face(s) within `entities`; will generally be a single face
def self.clone_face(face, entities)
  # Create a temporary group
  temp_group = entities.add_group

  # Remember the original edges so we can compare the new mesh edges with them later
  original_edges = face.edges.map { |edge| normalized_edge(edge) }

  # Use the face's mesh to add a bunch of connected polygons (triangles) to the temporary group
  mesh = face.mesh
  mesh.polygons.each do |polygon|
    temp_group.entities.add_face(*(polygon.map { |index| mesh.point_at index }))
  end

  # Now we've got a whole bunch of interior edges for the polygons that don't belong on the new face. So we'll
  # delete all the excess edges and call the remaining face(s) the result. There should generally only be one face
  # left over, but it depends on lots of things going right ;)
  (entities.grep(Sketchup::Edge).find_all { |edge| !(original_edges.include? normalized_edge(edge)) }).each do |edge|
    edge.erase! unless edge.deleted?
  end
  new_faces = temp_group.entities.grep(Sketchup::Face)

  # Explode the group to drop all the faces into the desired entity list
  temp_group.explode

  new_faces
end

So, as both a Ruby and a Sketchup API plugin n00b, my question is: does this make sense? Am I missing some significant gotchas?

Thanks for any insights y’all can provide :grin:

(1) Besides the frivolous parenthesis that only clutter the code, …

(2) It may be simpler and less error prone as …

temp_ents = temp_group.entities
# Add polygons ...
edges_unneeded = temp_ents.grep(Sketchup::Edge).select {|edge|
  edge.faces.size > 1
}
temp_ents.erase_entities(edges_unneeded)

… and then you need not keep track of the normalized edges.
(Generally seek to use the batch erase methods from the Entities collection class.)

(3) There are versions that do not keep the face references after exploding a group.
For these SketchUp versions you need to take a snapshot of the entities’ faces and then subtract it from the faces collection afterward. Ie …

before = entities.grep(Sketchup::Face)
#   explode group
new_faces = entities.grep(Sketchup::Face) - before
new_faces.keep_if {|face| face.valid? }

This is actually leveraging the Array#- method.

(4) You might wish to note the face’s normal and be sure afterward that it is pointed correctly. (ie, that the face isn’t reversed.)

(5) There may be other properties to clone (ie, material & texture, attribute dictionaries.)

1 Like

Thanks for the excellent (and wow super-rapid on a Sunday) response, Dan!

Besides the frivolous parenthesis that only clutter the code

Haha, okay fair point :sweat_smile:. I’ll admit that with my c-style background Ruby’s syntax and precedence rules terrify me :stuck_out_tongue:

edge.faces.size > 1

Ah, okay I think I get why this works. But to clarify–faces touching at only a shared vertex won’t share the edges connected to that same vertex? E.g., in the diagram below, all edges are shared by just one face even though the faces touch at point D? (I did test this specific example in Sketchup and that’s how it worked.)

+-------+
| A   B |
|       |
| C     | D
+-------+-------+
        |     F |
        |       |
        | G   H |
        +-------+

(Generally seek to use the batch erase methods from the Entities collection class.)

Perfect, thank you :slight_smile:

For these SketchUp versions

Would it be better to implement this the “universal” way, or to prefer the approach I’ve shown and use a version check to ensure backwards compatibility?

entities.grep(Sketchup::Face) - before

I imagine this operation could actually be quite expensive if the entities list is large? (Although I guess the same is true of my normalized edge test if the number of edges is very large.) Anyway I could probably do the same thing with Sets?

You might wish to note the face’s normal and be sure afterward that it is pointed correctly. (ie, that the face isn’t reversed.)

D’oh! I actually had that in there in a previous version but deleted it by accident. Nice catch, thank you!

(5) There may be other properties to clone (ie, material & texture, attribute dictionaries.)

Noted. Thanks again!

SketchUp

At the risk of opening a can of worms, what’s the convention for spelling SketchUp in code? Camel case like this or the way it is in the Ruby API?

Thanks again, Dan!

Oh, nevermind, I see what you mean. It’s good to make sure the faces still exist after exploding it for backwards-compatibility.

Yes we often must do version check. I cannot remember when this happened. Check release notes.

Now we also have persistent_id (since SU7.0) that can be saved (to an array) before the explode.

Before this, coders might attach temporary attribute dictionary to the faces or edges to find them after the explode.

EDIT: I think I am confounding another situation that used to occur. It could have had to do with working around old explode bugs in the API. See my comment below.

Yes, but we have found that Enumerable#grep is very fast. (Psst! Ruby is implemented in C. ;))

In this situation a set won’t help you. Most API collection access methods return an array of unique members. Sometimes reordering is needed.

Other times when collecting references of say points from multiple edges, and then you flatten the nested arrays into one, you will need to run #uniq on the result array.

In code? I have no idea why the original authors of the API decided to use Sketchup as the reference identifier for the API application module. (They are long gone and on to other endeavors.)

But referring to SketchUp will raise a NameError exception.

Within your own module(s) you could define a local constant that points at the toplevel one, ie …

module MarkB
  module SomePlugin
    SketchUp ||= ::Sketchup
  end
end

But I don’t know anyone who does this.

The word “SketchUp” is a trademarked product name and should be used whenever displaying text to the user. IE, messageboxes, inputbox, statusbar text, etc.

1 Like

Oh! I missed something basic.

In your example above, you had …

  new_faces = temp_group.entities.grep(Sketchup::Face)

  # Explode the group to drop all the faces into the desired entity list
  temp_group.explode

  new_faces
end # method

The explode returns the exploded entities, so it should be much simpler as …

  # Explode the group to drop all the faces into the desired entity list
  begin
    new_stuff = temp_group.explode
  rescue => err
    puts err.inspect
    nil
  else
    new_stuff ? new_stuff.grep(Sketchup::Face) : nil 
  end
end # method
1 Like

In this case, I was figuring that the comparison of two lists would be O(n^2) whereas a hash-backed set would presumably be O(n).