Observers and deleted entity information work around

After reading other topics, I understand that, within an entity observer’s onEraseEntity method, I cannot access information from within the erased entity.

Is there an observer (or other way) to get an event that something is about to be deleted?

What I do with my current software is make the user use my UI to delete one of my entities. When this happens, I check to see what the entity is, and is it associated (via attribute information) with another entity.

For example, when a user deletes a window, I check to see what wall it belongs to. And then I close the hole in the wall as well as deleting the window.

The problem is, of course, the user can just select the window and delete it the old fashion way in sketchup. This leaves a hole in the wall. What I need to do is:
-see that the window is going to be deleted
-get relevant information out of the window’s attributes
-allow the delete to happen
-close the hole in the wall

If I cannot do this with an observer, does anyone know how I might go about this?

Thanks in advance for your help. You guys always have great insight and ideas. My project would be stuck in the mud without you.

When you create an instance of the wall you will define its initial attributes e.g.

I’m a wall1
I have several attributes like color, height and so
There are no windows: yet

When you create an instance of the windows and put on to the wall1 you will define its attributes e.g.

I’m a window1
I’m belonging to wall1, in a position here and there
My sizes are this and that

Then last part of creating this window to inform the wall1, by modifying attributes of wall1 to:

I’m a wall1
I have several attributes like color, height and so
There is a window now: window1
The window located here and there
I opened a hole for him with a size of this and that.

Now you can attach an observer to the window1 instance to listen if it is deleted.

When user delete this window1 instance the observer onEraseEntity method will fire. In that method you still can read the attributes of the wall - containing the information of opening - and take care to close the opening of the wall1, and modify back its attributes to initials.

# Class: Sketchup::EntityObserver

2 Likes

Not at this time. There is an open request in the API Issue Tracker …


Generally, you attach the information you need to access to an object higher up in the entity hierarchy.
You can use a DefintionsObersever#onComponentInstanceRemoved callback to trigger the search through the “parent” object’s attributes.

@dezmo shows one scenario above.

What you might not realize is that an AttributeDictionary is an Entity sublcass object, and so can itself have an AttributeDictionaries collection of child dictionaries (say one for each window and/or opening.)

Thanks @dezmo and @DanRathbun.
My problem with these scenarios is the IFC export. Each of these items (wall, window, door, etc) is an IFC component. I need the information that I am saving in the component dictionaries to be exported to the IFC file when I do an export. If the information is not contained within the component’s dictionaries, it wont get exported to the IFC file.

Any thoughts?

This is is not the same topic as observers. Please search the forum for IFC discussions.

IFC export is slowly getting improvements.
Please fix your forum profile as there is no SketchUp Make 2021 edition.

No, not the same topic but related to the fix. Basically, I need to get the observer event before the item is erased so I can query the attributes. But I cannot remove the attributes from the element (as you and @desmo suggested) because the attributes need to be there for IFC. Its a conundrum.
And a good example for the need for the API request you mentioned earlier.

Create a custom observer, store important data in it and attach it to the window.

# class declaration, place inside your own namespace (modules)
class WindowDeletedObserver

	def initialize(wall_to_be_informed, other_information)
		@wall_to_be_informed = wall_to_be_informed
		@other_information = other_information
	end #def
	
	def onEraseEntity(entity)
		puts "We need to inform wall #{@wall_to_be_informed} that we just got deleted as well as some other information: #{@other_information}"
	end

end #class

# create an instance of it and store important information inside of it
observer = new WindowDeletedObserver("wall 123", "very usefull other data")

# attach it to the entity that can be deleted
# in this case, just the first entity in the model, in your case, the window you have created
Sketchup.active_model.entities[0].add_observer(observer)

Upon deletion this will put We need to inform wall wall 123 that we just got deleted as well as some other information: very usefull other data

Another one if you do not like classes (but classes are preferred however) but you want to capture context: use lambdas:

# image you have a reference to your wall and window (by using for example their persistent id)
wall_pid= 123
window_pid = 987

# procs capture context, which is great, so create proc
window_deleted_observer = ->() { puts "we should inform the wall: #{wall_pid } that window: #{window_pid} just got deleted" }

# declare a method on that proc that SketchUp expects to call on EntityObservers... 
def window_deleted_observer.onEraseEntity(entity)
    # just let it call itself
	self.call()
end #def

# attach to the window, or, in this case, the first entity in the active model
Sketchup.active_model.entities[0].add_observer(window_deleted_observer)

What makes Ruby so great (and at the same time a pain) is that you can “fake” things so easily:

# create a hash
data = {wall_pid_to_inform: 12345}
# declare a method on it that makes SketchUp think it is an observer
def data.onEraseEntity(e)
    # and use data that is available in the hash in the method
	puts "we should inform #{self[:wall_pid_to_inform]}"
end #def

# attach the hash as an observer to the first entity in the model, or the window you have
Sketchup.active_model.entities[0].add_observer(data)

As you see, think out of the box and you can get to more data than that DeletedEntity SketchUp gives you in the default observer.

What happens to the observer after the object is deleted? Is it deleted by sketchup or do I need to keep track of that?

What do you do with your current observer?

It’s your observer object. Your code can keep a persistent reference to it …ie, @data.
But the action of attaching it also means SketchUp’s observer queues are also keeping a reference to it.

What happens when any API object that has observers attached is deleted ? Simple, they are removed from the observer callback queues by the SketchUp engine. If there no other reference, then in the next GC cycle they’ll get garbage collected.

But (as you have found,) the EntityObserver#onEraseEntity callback gets called after the deletion, but before things get cleaned up. I think we all agree it almost makes that callback worthless. (It might have been a bit better if DeletedEntity objects could be checked for pid.)

Final (hopefully) question about observers: Is there a way to get a list of observers attached to an entity? There are some instances where I don’t want the observer to do its thing because it would become circular. For example: Wall’s observer deletes the window but the window’s observer wants to tell the wall to remove it. I would like to, when deleting a wall, “turn off” the window’s observer and let the wall observer do everything.

But unless I keep a separate list of observers, I do not see a way to do this. It would be nice to either be able to access the observers (entity.observers) or simply remove any attached observers (entity.remove_observers()). But I do not think that is possible.

If I am wrong, please let me know. Thanks!

Not globally, unless you create such a utility. I started myself but never finished it.

If we ae only talking about your observers, then same as before. You are responsible for keeping track of your own objects.

Well if the wall (or it’s observer) has a collection of it’s child windows (or you could iterate and find the windows,) … then you use the collection to detach the window observers.

It is likely you are attaching attribute dictionaries to windows. Add an “observer” attribute that has the observer object as it’s value.

The other thing is you are likely keeping track of window PIDs, so when the wall goes to delete a window it pushes the window PID into an array … @delete_in_process << win_pid.
If the window observer sends a message back to the wall, the wall can check this array … if @delete_in_process.include?(pid) and act accordingly. Ie, do nothing if true or react otherwise.

I moved my data from the entities to a dictionary on the active model. Basically I have the dictionary named “MyIFCDictionary” with an attribute for each of my component instances. It is a guid. So…

Sketchup.active_model.set_attribute “MyIFCDictionary”, “myguid”, “myDataString”.

My observer has the guid associated with the entity. So in onEraseEntity I do all of the stuff that I was having trouble with at the beginning of this thread. The only problem I have now is getting rid of the data once the entity is deleted.

I have this line, but it doesnt seem to do anything:

Sketchup.active_model.delete_attribute “MyIFCDictionary”, “myguid”

I cannot get this to work in the event or even in the Sketchup console.

I feel like this is the last piece of my puzzle. Am I doing something wrong? Is this a bug or am I not understanding how to delete an attribute?

Let me know. Thanks for all of your help!

create_if_nil = true
model = Sketchup.active_model
attrdict = model.attribute_dictionary("test_dict", create_if_nil)
attrdict["attr_one"] = "one"
attrdict["attr_two"] = "two"

# Delete a key/value pair and get the deleted value.
attrdict = model.attribute_dictionaries['test_dict']
value = attrdict.delete_key("attr_one")
1 Like

It works for me in SU2021.1 when dealing with a dictionary attached to an object.

Checking behavior for model dictionaries …
Ooo! I see the same unexpected behavior.

This rings a bell in memory. I wonder if this has been reported ?

There are “special” dictionaries that are not allowed to be deleted, but this should not happen with non-Trimble dictionaries.

Thanks Ken, yes using #delete_key on the dictionary object does work correctly.


A few notes:

(1) The model space is a shared space same a Ruby’s ObjectSpace and SketchUp’s “plugins” filespace, so dictionary names should be qualified similar to modules and extension folders. We often suggest that the dictionary for an extension use the same name as it’s extension subfolder.

Ie, if the folder name is "Acme_RoadRunnerKiller" it’d be best if the extension’s dictionary uses the same name. This will prevent clashes and weird things happening.

(2) Since an extension is likely to use it’s dictionary name many places, rather than using string literals everywhere, define a constant at the top of the extension module …

DICT ||= "Acme_RoadRunnerKiller"

… and use this whenever the name must be referenced. This helps avoid typo errors in dictionary access method calls.

Your code can also use the same name for saving defaults to the json preference files, (See: Sketchup::read_default and Sketchup::write_default methods.)

Thanks Dan! I do have my company name but I didnt use an underscore after it. And yes, I use constants. I just didnt want to cause any confusion when typing out the example.

Good(?) to see that you see the issue too. It looks like @kengey 's suggestion to use delete_key worked.

The only thing (hopefully) left is to attach my observers to the entities when the model is opened or when my plugin is loaded. But that is just a matter of looping through the entities.

I have a lot of code to change to implement these fixes throughout my plugin. But in the long run, the observers are going to cut out a lot of repetitive code I had in my original cut at this.

Thanks for the help!

The only thin

Thanks! That seemed to be the right way to do it. Not sure what the difference is between delete_attribute and delete_key. But the desired result has been achieved.

Thanks!

If it were my choice, I’d save the newer #persistent_id rather than the old #guid.