Is it possible to find the entities that have just been painted?

Is there a way using materials observer #0nMaterialRefChange to grab the entities that were just painted with the paint tool? Any help is appreciated.

Edit: Maybe there is a way to pull all the entities painted with a material into an array when #onMaterialSetCurrent is called, and then again after #onMarteialRefChanged is called, then compare the arrays to get the new entities?

Edit 2-5: It seems that #onMaterialSetCurrent actually fires when you apply a material to an object, not when you select a new material in the material dialog as stated here. After a closer look it appears that #onMaterialSetCurrent fires and then #onMaterialRefChange fires twice afterwards when a material is applied.

Edit 6: When #onMaterialRefChange fires twice it gives the old material first and then the new material.

def onMaterialSetCurrent(materials, material)
  puts "onMaterialSetCurrent: #{material}"
  puts
end

def onMaterialRefChange(materials, material)
  puts "onMaterialRefChange: #{material}"
  puts
end

By ā€œentitiesā€ do you mean group, component definition, face, and edge? Or maybe nested entities too?

Do you mean cumulatively each time the material is changed?

???

1 Like

Looking for groups and components, but thats easy after you get all the entities. I want to get the groups and components that were just painted, not cumulative. Looking to run a code snippet when a certain group or component gets a paint jobā€¦

You write ā€œa certain group or componentā€. Does that mean you could pre-select them? If so you could monitor them alone for changes.

1 Like

Hey thanks for the interest! What I am doing is looking to redraw a DC after it gets a material applied to it, if it has an attribute, letā€™s say ā€˜paintedā€™.

Store the current material as a variable. onChangeEntity compare it with the current material, when it differs redraw and update the variable. The onChangeEntity function seems volatile after a bit of messing around with it. Is this how would you would recommend watching the instance for changes or is there another route I am missing? Maybe a timer to push the redraw outside the onChangeEntity execution, otherwise redraw may just cause a loop?

Have to go install some new lights for mom, but I will play with this later. Thanks!

This is what I get when testing what happens when painting a face ā€¦

# An observer class that announces what callbacks SketchUp is polling for.
class Spy
  def respond_to?(meth)
    puts "SketchUp is polling observer for a :#{meth} callback."
    super # <- always provide default behavior when overriding core Ruby methods
  end
end


sel = Sketchup.active_model.selection
#=> #<Sketchup::Selection:0x000001bd10f8c518>
face = sel[0]
#=> #<Sketchup::Face:0x000001bd10f88c60>
face.add_observer(Spy.new)
#=> true

# I paint the selected face here ...
#=> SketchUp is polling observer for a :onChangeEntity callback.

I get the same result when a Spy observer is attached to a group instance.

2 Likes

OK, I have a working solution thanks for the help guys! :tada: Attach an observer to my dc, store the currently applied material as a custom attribute ā€˜_matā€™ in my dc, then compare it with the actual material onChangeEntity. When they do not match, I update the attribute and redraw the dc.

Dan would you care to elaborate a bit on SketchUp polling for callbacks? I am currently reading about ā€˜superā€™ā€¦

def onChangeEntity(dc)
  actual = dc.material.to_s
  saved = get_att(dc, '_mat')
  unless actual == saved
    dc.set_attribute('dynamic_attributes', '_mat', actual.to_s)
    $dc_observers.get_latest_class.redraw_with_undo(dc)
  end
end

Prior to SketchUp 2016 its engine just bruteforce called callbacks which were attached to objects making the assumption that any observer object so attached would have been defined as a subclass of an appropriate API observer superclass.
It mostly worked because the APi observer superclasses had empty callback methods as shown in the API documentation and the assumption (incorrectly) that all use of observers would use a subclassing pattern. Ie, if an extensionā€™s observer subclass did not locally override the callback being called, then the empty callback method in the superclass was called (with no effect other than to unnecessarily waste time.)

But with the 2016 release, the observers were overhauled because they were fragile and there were too many crashes. Also, we (coders) pointed out that none of the #add_observer() methods for API objects did any type checking and that the subclass pattern was not the only valid way of using observers.
Ie, an observer need only be a Ruby object which has publicly accessible callback methods which can be called by the SketchUp engine. It can be an instance of a certain observer subclass, or an instance of a hybrid observer class containing all the collection observer callbacks all-in-one, or it could be a module instance, or it could be the entity being watched itself if a singleton observer callback were to be defined upon the entity and the entity was attached as its own observer. (The last is a quirky example not likely to be used much if at all. But it is possible, Iā€™ve tested it.)
For example, the extension submodule itself can most times act as the extensionā€™s AppObserver instance object since there is only ever one single instance of the application within the running process. There is no need to have it be a class since only one observer instance is needed. And since an extension has a submodule (which is an instance of class Module) then it can serve as the observer object.

One of the major changes with the 2016 observer overhaul was that all the API observer classes were made abstract classes. Meaning that they exist in name only and no longer have any functionality to pass on down to descendant subclasses. All their empty superclass observer callback methods were removed. (It could be argued that they never had any functionality that subclasses could inherit, but technically the subclasses did previously inherit the ability to call the empty superclass methods.)
To replace the blind calling of callback methods upon observer objects, the SketchUp engine was changed to use a more defensive protocol by first polling the attached observer object(s) to ask if they respond to the callback method being called. If the object does not have the named callback method defined, it does not now get called.

Another major change was when observer callbacks get called. Post overhaul, callbacks are queued up and dispatched after the operation is complete.

From a link in the API Release Notes at ā€¦

ā€¦ the following extensive explanation is published as an PDF document:

I suggest consumers of observers save the document locally to their computer and read it carefully. Keep the copy handy for future reference. It also discusses some best practices and safer use of object references passed into callback methods.

1 Like

See:

1 Like

Thanks Dan!

1 Like

So the onChangeEntity method is too wild for what I want to do here, it causes all kinds of other behaviours with my extension. Care to post the code for this, skimmed over it too quick last look, want to try what you are doing here but canā€™t see the all of the code in the video.

Hey, Iā€™m not sure I understand what you are trying to do. Could you describe it a little more? Would you ideally want and html dialog that displays things and has options relevant to what you want to accomplish?

Check your PMs - I donā€™t want to post misleading/bad code!

1 Like

If I were doing this, Iā€™d operate thusly:

  • Iā€™d only deal with certain objects ( a filtered subset ) ā€¦
  • ā€¦ within the current active edit context (#model.active_entities)
  • only when the Paint Bucket tool is the active tool

It seems that:

  • the certain objects are dynamic component instances, filtered by type and whose definition contains an "dynamic_attributes" dictionary, ā€¦
  • which should only be grepā€™d from the active entities ā€¦
  • only when a ToolsObserver attached to the model signals that the Paint Bucket (id # 21074) has been activated
  • and only then:
    • a MaterialsObserver is attached to the modelā€™s materials collection, and ā€¦
    • an EntityObserver instance is attached to the dc objects which makes a reference to the material being ā€œwatchedā€

When the #onMaterialsRefChange fires you can keep track on how many materials were changed. When the #onEntityChanged callback fires, check the material to see if the reference changed comparing against what you referenced when you first attached the observer to it, likely in the observerā€™s initialize method.

Probably youā€™ll also need to attach a ModelObserver (whilst the Paint Bucket is active) to watch for #onActivePath changed. When it does your code would detach observers from the dc instances and re-grep the current entities context, and attach new EntityObserver instances (whose initialize would cache the references to the materials.)

Then when the ToolsObserver detects that another tool besides the Pant Bucket is activated, all the observers would be detached except for the one watching the modelā€™s toolstack.

1 Like