Maintaining reference to old face after pushpull

A general question for the correct way to do something: I have noticed that if I have a Face (named f), and I call:

f.pushpull(1.0, false)

If I then subsequently attempt to reference that face, I get the following error:

Error: #<TypeError: reference to deleted DrawingElement>

Why does this happen? And what is the proper way to then reference this face?

For a simple example:

model = Sketchup.active_model
face = model.entities.add_face([0, 0], [20, 0], [20, 20], [0, 20])
face.pushpull(1.0, false)
face.material

in a script will trigger the error. Doing this manually line by line in the command line, interestingly, does not. (Might this have something to do with cleanup?)

To solve my own problem: If I add the face to a group, and then later in my code, I try to access it by iterating over entities in the group, that solves my problem - the face is still in that group.

However, I am unsure as to why that would be.

SketchUp creates all new face objects during the push-pull operation.
You cannot hold onto the old face object reference.
The error message tells you the old reference is now invalid.

Save the face’s normal

old_norm = face.normal

Get an array collection of the current faces:

ents = Sketchup.active_model.active_entities
faces_before = ents.grep(Sketchup::Face)

Then after your push-pull operation, remove the invalid references from the old set, and subtract the old set from a new set:

faces_before.delete_if {|f| !f.valid? }
faces_after = ents.grep(Sketchup::Face)
new_faces = faces_after - faces_before

Now you have a set of the faces added by the push-pull operation.
Iterate and compare normals:

old_face = new_faces.find {|f| f.normal == old_norm }

If unsuccessful, Enumerable#find returns nil.

When adding new geometry it’s difficult to retain entities in SketchUp because of the “sticky” nature of SketchUp that constantly merge vertices, edges and faces. This means entities some times are regenerated when you perform operations that modify the geometry.

1 Like

Thanks for the info, but what is the difference between doing it line-by-line and doing it in a script? Why should it work one way but not another? What are the rules for when references are deleted and when they are maintained?

This is not really determinable. When you model something by hand and eye-sight, you don’t see when SketchUp does merging or intersecting or replacing entities by new entities. And it has no consequences because you don’t keep hard references in your mind, but your eyes see the (new) entities and recognize them as if they were retained.

Doing it in a script is different: You can not know how many entities result from a merge/intersect operation and you can not know which are new (sides of the pushpull) and which relate to previous faces (top of pushpull). A SketchUp API method could only return one return value…

Your current approach was translating manual actions into the respective Ruby API methods (pushpull). The alternative approach requires a bit of math and would calculate the points of the final geometry using vector math, and then just draw the edges and faces of the final result.

3 Likes

I’m also having this problem. I tried using an attribute to keep track of the face but all the newly created faces on the sides of the extrusion also got the same attribute. Here’s the code I came up with that seems to work quite well. I haven’t bothered about the edge cases when the face meets another face.

#Push-pull a face and return reference to the push-pulled face.
#
# face - A Face.
# distance - Length.
#
# Returns a Face.
def push_pull_with_reference(face, distance)
  pts = face.vertices.map { |v| v.position }
  pts.each { |pt| pt.offset!(face.normal, distance) }
  entities = face.parent.entities
  face.pushpull(distance)
  
  entities.find { |e| e.is_a?(Sketchup::Face) && pts.all? { |pt| e.vertices.map(&:position).include?(pt)} }
end

Edit: I completely missed Dan’s response. That’s probably handles edge cases better.

1 Like

Make the face inside a group.
Get an array of all current faces in that entities context.
Get norm=face.normal
Do the pushpull.
Iterate the entities context again and collect the new faces into an array.
Collect the new face[s] sharing a normal == norm

Late to the party TIG. Basicaly what I posted above a year and a half ago.

Although, the scenario does not assume that the face is created in this operation.
It may already be existing, and attached to other geometry.