Ruby Copy Entity Workaround - Loop finds Deleted Entities

This question is in reference to this post by @barry_milliken_droid addressing the quirky challenge that the API does not have a method to copy a group.

Here is a slightly modified version of Barry’s code that successfully “copies” all root-level groups into a new container group and moves it slightly to show that it worked.

array_of_ents_to_copy = Sketchup.active_model.entities.to_a

if array_of_ents_to_copy.length == 0
	UI.messagebox "nothing to copy"
	return
end

original_groups_temp_container = Sketchup.active_model.entities.add_group array_of_ents_to_copy
temp_transformation = original_groups_temp_container.transformation
temp_definition = original_groups_temp_container.definition

container_ent = Sketchup.active_model.entities.add_instance temp_definition, temp_transformation
container_ent.name = "CONTAINER_ENT"

vector = Geom::Vector3d.new(80, 40, 0)
tr = Geom::Transformation.translation(vector)
Sketchup.active_model.entities.transform_entities(tr, container_ent)
#container_ent.explode # don't explode if you want to keep everything in one group
original_groups_temp_container.explode # explode to return original groups to normal

The problem comes when I then try to loop the new container’s contents. I want to get rid of anything that is not a comp or group, not on a visible layer, or named certain things. When I run .each() on the new container’s entities, and try to erase! certain qualifying objects, I get a bunch of Deleted Entity warnings.

Here is the loop…

container_ent.entities.each{ |this_ent|

	print "inspecting #{this_ent}\n"

	# prune unwanted ents
	if (!this_ent.is_a?(Sketchup::Group) && !this_ent.is_a?(Sketchup::ComponentInstance))
		if this_ent.erase!
			print "erased #{this_ent} - because it was not a comp or group\n"
		else
			print "erase failed for #{this_ent}	\n"
		end
	elsif this_ent.layer.visible? != true
		if this_ent.erase!
			print "erased #{this_ent.name} - because its layer was not visible\n"
		else
			print "erase failed for #{this_ent}\n"
		end
	elsif this_ent.name.include?("_CIRCLE")
		if this_ent.erase!
			print "erased #{this_ent.name} - because it was a cutting circle\n"
		else
			print "erase failed for #{this_ent}\n"
		end
	elsif this_ent.name.include?("_CUT")
		if this_ent.erase!
			print "erased #{this_ent.name} - because it was a _CUT result\n"
		else
			print "erase failed for #{this_ent}\n"
		end
	else
		print "keeping #{this_ent.name}\n"
	end

}

And here is the console output…

inspecting #<Sketchup::ComponentInstance:0x007fdfeaa8dd48>
keeping 
inspecting #<Sketchup::Group:0x007fdfeaa8dd20>
erase failed for #<Deleted Entity:0x7fdfeaa8dd20>
inspecting #<Sketchup::Group:0x007fdfeaa8dcf8>
erase failed for #<Deleted Entity:0x7fdfeaa8dcf8>
inspecting #<Sketchup::Group:0x007fdfeaa8dca8>
erase failed for #<Deleted Entity:0x7fdfeaa8dca8>
inspecting #<Sketchup::Group:0x007fdfeaa8dc80>
keeping PROP_GROUP
inspecting #<Sketchup::Group:0x007fdfeaa8dc58>
erase failed for #<Deleted Entity:0x7fdfeaa8dc58>
inspecting #<Sketchup::Group:0x007fdfeaa8dbe0>
keeping EXIST_GROUP
inspecting #<Sketchup::Group:0x007fdfeaa8dbb8>
keeping DEMO_GROUP
inspecting #<Sketchup::SectionPlane:0x007fdfeaa8db90>
erase failed for #<Deleted Entity:0x7fdfeaa8db90>	
inspecting #<Sketchup::SectionPlane:0x007fdfeaa8db40>
erase failed for #<Deleted Entity:0x7fdfeaa8db40>	
inspecting #<Sketchup::SectionPlane:0x007fdfeaa8db18>
erase failed for #<Deleted Entity:0x7fdfeaa8db18>	
inspecting #<Sketchup::SectionPlane:0x007fdfeaa8dac8>
erase failed for #<Deleted Entity:0x7fdfeaa8dac8>	
#<Sketchup::Entities:0x007fdfe8d0a550>

Somehow, between starting the loop and inspecting the entities, they have changed/been deleted? It must have something to do with the handles being changed?

Your help would be greatly appreciated.

next if this_ent.deleted?

john

Don’t delete from the collection you are iterating over. Doing so changes what elements are contained in the collection, shifts the remaining ones to new indices, and risks iterating over the same element multiple times while missing others.

instead of entities.each, use entities.to_a.each. The array created from to_a retains elements even while they are deleted in model, meaning it can be safely iterated over.

Btw, if you follow the Ruby style guide (2 spaces for indentation among other things), your code is easier to read and give feedback on.

2 Likes

Thanks but…

a) I WANT to delete those entities.

b) my real question is why do they appear to be deleted already?

Hmmm… strangely if I change the line

container_ent.entities.each{ |this_ent|

to

container_ent.entities.to_a.each{ |this_ent|

I still get a slew of error messages like

erase failed for Deleted Entity:0x7fdfeb119888

That’s odd. Perhaps some entities were already deleted before the loop even started. What happens if you run p container_ent.entities.to_a? Any sign of deleted entities there?

You should never iterate an .entities collection [or a .selection] and delete items as you go because that changes what you are iterating !
The .entities.to_a etc avoids that issue.
BUT it’s quite possible to iterate a ‘to_a’ collection and still find some items are already deleted !
For example, if your collection consists of a few edges and a face supported by those edges, then when you delete an edge the face is also deleted at that step, so later on in the iteration you get to the face and it’s already gone !
Hence the tip about adding in next if this_ent.deleted? at the start of the block…
This then skips the entity in question if it’s already been deleted.
Avoiding the error message !!

2 Likes

look at a rectangle,

model = Sketchup.active_model
ents  = model.active_entities.to_a

#[#<Sketchup::Edge:0x00007fab1dc87330>, #<Sketchup::Edge:0x00007fab1dc87308>, #<Sketchup::Edge:0x00007fab1dc872e0>, #<Sketchup::Edge:0x00007fab1dc87290>, #<Sketchup::Face:0x00007fab1dc7cbb0>]

# erase one edge and call your array again...
ents 

#[#<Deleted Entity:0x7fab1dc87330>, #<Sketchup::Edge:0x00007fab1dc87308>, #<Sketchup::Edge:0x00007fab1dc872e0>, #<Sketchup::Edge:0x00007fab1dc87290>, #<Deleted Entity:0x7fab1dc7cbb0>]

# re-populate your array and the deleted entries are gone...
ents = model.active_entities.to_a

# [#<Sketchup::Edge:0x00007fab1dc87308>, #<Sketchup::Edge:0x00007fab1dc872e0>, #<Sketchup::Edge:0x00007fab1dc87290>]

john

Sorry, where should that line be placed… you are dealing with a moron here sorry.

and…

OK, I get that Sketchup’s cleaning directive will erase things if any partner geometry is erased but I am getting this on EVERY entity. Look at this erase criteria…

elsif this_ent.name.include?("_CIRCLE")

This is a pretty specific criteria for a group or comp with a specific name. How could this have been erased when is no partner geometry at the active context that could have already been deleted?

As far as I can tell

next if this_ent.deleted?

Just avoids the error message associated with trying to delete something that isn’t there. Still doesn’t address why it’s not there in there first place, which is the whole conundrum here.

It is usually safer to queue up references to drawing elements that you will delete in an array, and then pass that array to the Sketchup::Entities#erase_entities() method to erase them in one operation. It is faster and should be easier on the undo stack.

This method has been part of the API since v 6.0 …


Also for topic threads that were locked when that autolock feature was active, you can PM one of the admins like Aaron to unlock it. Admins are listed on the forum’s About page (accessible via the hamburger menu, top right on the toolbar.)

2 Likes

wait, what? Oh shoot then why are we even talking about this. :upside_down_face:

I thought barry’s method was still the preferred method. Does this method also avoid the cross-threading challenge?

Thank you re unlocking a topic!

I will play around with erase_entities() that sounds promising!

@DanRathbun That worked!

@TIG and @john_drivenupthewall I also used next if this_ent.deleted? per your recommendation.

So I am not sure if we figured out why some ents were deleted but I suspect it had to do with using the “old” copy method rather than group.copy()

Here is the updated code that works as desired.

array_of_ents_to_copy = Sketchup.active_model.entities.to_a

if array_of_ents_to_copy.length == 0
  UI.messagebox "nothing to copy"
  return
end

original_groups_temp_container = Sketchup.active_model.entities.add_group array_of_ents_to_copy
container_ent = original_groups_temp_container.copy
#temp_transformation = original_groups_temp_container.transformation
#temp_definition = original_groups_temp_container.definition

#container_ent = Sketchup.active_model.entities.add_instance temp_definition, temp_transformation
container_ent.name = "CONTAINER_ENT"

vector = Geom::Vector3d.new(80, 40, 0)
tr = Geom::Transformation.translation(vector)
Sketchup.active_model.entities.transform_entities(tr, container_ent)
#container_ent.explode # don't explode if you want to keep everything in one group
original_groups_temp_container.explode # explode to return original groups to normal

ents_to_erase = []

container_ent.entities.to_a.each{ |this_ent|

  print "inspecting #{this_ent}\n"

  next if this_ent.deleted?

  # prune unwanted ents
  if (!this_ent.is_a?(Sketchup::Group) && !this_ent.is_a?(Sketchup::ComponentInstance))
    ents_to_erase << this_ent
    print "added #{this_ent} to ents_to_erase - because it was not a comp or group\n"
  elsif this_ent.layer.visible? != true
    ents_to_erase << this_ent
    print "added #{this_ent} to ents_to_erase - because its layer was not visible\n"
  elsif this_ent.name.include?("_CIRCLE")
    ents_to_erase << this_ent
    print "added #{this_ent} to ents_to_erase - because it was a cutting circle\n"
  elsif this_ent.name.include?("_CUT")
    ents_to_erase << this_ent
    print "added #{this_ent} to ents_to_erase - because it was a _CUT result\n"
  else
    print "keeping #{this_ent.name}\n"
  end

}

Sketchup.active_model.active_entities.erase_entities ents_to_erase

Thank you all!

1 Like