Observing changes in Component Definitions

I’m wondering what is the best practice is for detecting changes in a component definition. All I need to know is that the definition changed, I’m not watching any particular edge or face inside of it.

  • I know the EntitiesObserver can be used to watch changes in the definition’s entities collection, but it triggers for every single entity that changed inside the component, which seems inefficient to me.

  • I tried attaching EntityObserver to the definition but it doesn’t react, probably as it’s designed for actual drawing elements.

  • DefinitionObserver only has methods for adding or removing instances. It supposedly inherits onChangeEntity from EntityObserver, but this doesn’t trigger when the definition is modified.

  • I’ve also tried an InstanceObserver: onOpen notes the initial GUID, onClose compares it with the stored GUID, and if they’re different then the observer calls my script’s method. But this is not really observing the definition; if any other instances are placed, they don’t react to changes. I would have to create a whole other system to add the instance observer to any new instances of that component.

Any ideas would be appreciated!

Please be aware that ALL observer superclasses are actually abstract since ~ the 2016 release.
File: Release Notes — SketchUp Ruby API Documentation
https://assets.sketchup.com/files/ewh/Observers2016.pdf
(This means that they have no real callback methods and are defined only so that the YARD documenter can produce class documentation.)

Prior to that release the observer classes had empty callback methods that would get inherited by subclasses (including developer’s custom observer subclasses.) The engine would call these empty methods regardless which takes time, slows things down. For that release, the engine was changed to poll (“duck-type”) attached observer objects using #respond_to? before actually calling any callback method.

This also means that you can write test observers. See example here …
Observers not fired when importing files · Issue #101 · SketchUp/api-issue-tracker · GitHub

Also note that the attachment methods do not class type nor duck type the object attached as an observer.

The result of all this is that observer objects need not even be class instances (and of course not even subclasses of anything in particular.)
I often implement hybrid observers (those having a mix of callback methods from the various API observer classes,) as a module. Very often this observer module is actually the plugin submodule itself. This makes it easier to pass data and share references (which become difficult if you have many different observer classes.)
In addition, an observer object can be an instance of anything, and have singleton observer callback methods attached to it.
The only “rules of thumb” for observer callback methods is that they be publicly accessible to the SketchUp core so they can be called, and that they accept the correct number of arguments.

It should work, as a Sketchup::ComponentDefinition is a subclass of Sketchup::Entity.
IF it does not work the way you need it to, suggest you log an issue.
A bulk change callback might be helpful.


FYI, The docs state that you need to use Sketchup::DefinitionsObserver#onComponentPropertiesChanged if you need to know when a definition’s name or description changes.

However, EntityObserver#onChangeEntity will also fire when the user changes the definition name via the GUI, or any of the behaviors (shadows face sun, casts shadows, glue to) of the definition change.

Yes this is the only means right now.

I would use either 1 hybrid observer class, or just use the plugin module as the hybrid observer* …


  def attach_observers_to_model(model)
    model.add_observer(self)
    model.definitions.add_observer(self)
    # attach observer(s) to any other model collections HERE
  end

  def expectsStartupModelNotifications
    return true
  end

  def onActivateModel(model)
    attach_observers_to_model(model)
  end

  def onNewModel(model)
    attach_observers_to_model(model)
  end

  def onComponentAdded(dlist,cdef)
    cdef.add_observer(self)
  end

  def onComponentInstanceAdded(cdef,instance)
    instance.add_observer(self)
  end

  def onClose(instance)
    @editing = false
    do_something(instance) if @guid != instance.definition.guid
  end

  def onOpen(instance)
    @editing = true
    @guid = instance.definition.guid
  end

* - If you use a module you must extend it with itself. Ie at the top of the module …

   extend self

And then attach the observer object to the application …

  Skecthup.add_observer(self) # within a module

… or …

  Skecthup.add_observer(MySpy::new) # for a class

Now, when you need to keep unique state for specific models (like on Mac where multiple models may be open at the same time,) then this is where you’d use a observer class and create a separate observer instance (that has unique instance variables) for each model.

3 Likes

Have you tested and seen any noticeable performance impact? I’d start with this, and if it doesn’t impact the usage of your extension to any noticeable degree - stick with the simple way of using the EntitiesObserver observer. Avoid adding complexity to premature optimizations.

Your observer can also debounce the event, which is often a good idea with observers. Using a timer to throttle how often the observer trigger logic further down the pipe in your extension.

1 Like

Ultimately I think the GUID comparison test is the the best solution as it only triggers when the user closes the component from editing, avoiding debounce code entirely. And actually it’s pretty trivial to have a definition observer assign the instance observer to new copies.

That hybrid observer class is a great implementation strategy, I did not know that was possible. My project relies on component references (i.e. an ‘extrusion’ object that references a ‘profile’ component) so observers will need access to a fair bit of shared data…

Thank you both!

1 Like