There is no public actionable observer callback for the click on another author’s extension web interface (ie a button,) unless that author provides it and explicitly publishes the means for extensions by other authors to use it. The DC extension is proprietary and the only public interfaces are the manual use of it’s two web dialogs and the Interact tool.
At one time I thought I had filed an API feature request for a Dictionary observer class, but I cannot find it at the moment.
If the change in attributes cause a change to the dynamic instance, then some other observer callbacks may fire.
So test to see if EntityObserver#onChangeEntity fires for either the dictionary (which I think is a bug that does not happen,) or the component instance itself. This means you’ll need to attach this class of observer to all those DC instances you wish to “watch”.
EDIT: I think I posted a few years ago that this is possible. See this topic thread:
Sometimes making a DC property change causes the entities collection to change (such as adding or deleting copies of subcomponent instances such as pickets in a fence.) This means you’ll need to also monitor the DC definition’s entities collection with an EntitiesObserver.
To make it more difficult, if more than 1 instance of a DC component exists, and the change involves changes to the entities (as described above,) the DC extension will transparently clone the definition and make changes to the clone and reassign the “changed” instance as a child of the new cloned definition.
So simply “watching” the instance’s definition’s entities collection will not catch the change. You may also need to use InstanceObserver, DefinitionObserver and/or DefinitionsObserver to catch when a new definition is cloned from an old one.
Hacking a closed source extension is difficult and problematic in many ways. Foremost is the possibility that this extension could be changed at any time, or withdrawn in the future in preference to the newer Live Component feature.
One possible workaround is to try to change your thinking about when your extension will “retrieve the values of attributes” from “every time the user clicks Apply” to just before your extension does what it needs to do with the latest attribute values.
Using your example, I have created my own EntityObserver class which is activated when the selection at the “My_componant” definition.
Here is my code:
module JulienDufren
class Dynamic_componant_change
# Saves all dynamic attributes in an array with their values
def save_all_dynamic_attributes(all_attribut)
mod = Sketchup.active_model
sel = mod.selection
sel.grep(Sketchup::ComponentInstance).each do |s|
s.definition.instances.each do |inst|
inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
all_attribut << "#{key} = #{value}"
end
end
end
end
# Remove unnecessary dynamic attributes from the array
def remove_useless_dynamic_attributes(all_attribut, user_attribut)
exclude = []
["_formula","_formlabel","_options","_formulaunits","_access","_formulaunits",
"_units","_label","_has_movetool_behaviors","_hasbehaviors","_iscollapsed","_name"].each do |ex|
all_attribut.each do |att|
if att.include?(ex)
exclude << att
end
end
end
user_attribut << all_attribut - exclude
end
# Saves all dynamic attributes of the selection with their values in a file external to SketchUp always accessible!
def save_dynamic_attributes
all_attribut = []
save_all_dynamic_attributes(all_attribut)
user_attribut = []
remove_useless_dynamic_attributes(all_attribut,user_attribut)
user_attribut.flatten!
user_attribut.each do |att|
key = att.split(" =")[0]
value = att.split("= ")[1]
Sketchup.write_default("Plugin_name", "#{key}", "#{value}")
end
end
# Activate the observer and delete it to prevent it from remaining constantly active.
def onChangeEntity(entity)
save_dynamic_attributes
lenx = Sketchup.read_default("Plugin_name", "lenx")
leny = Sketchup.read_default("Plugin_name", "leny")
lenz = Sketchup.read_default("Plugin_name", "lenz")
p "My component: Height = #{lenz.to_f.to_mm} , Width = #{lenx.to_f.to_mm} , Depth = #{leny.to_f.to_mm}"
end
end
class My_class
# Trigger the observer if the selected component definition name is = "My_componant"
def self.save_dynamic_attribut
mod = Sketchup.active_model
sel = mod.selection
sel.grep(Sketchup::ComponentInstance) do |s|
if s.definition.name.include?("My_componant")
s.add_observer(Dynamic_componant_change.new)
s.attribute_dictionary("dynamic_attributes").add_observer(Dynamic_componant_change.new)
s.definition.attribute_dictionary("dynamic_attributes").add_observer(Dynamic_componant_change.new)
end
end
end
save_dynamic_attribut
end
end
Here is a dynamic component attachment that works with my code. My_componant.skp (51.8 KB)
In my example, I didn’t remove the watcher! Should it be removed to avoid problems?
Why does my code send me several times the dimensions of my dynamic component? Is this an error on my part? Do you have any suggestions for me on my code so that I can improve it?
Try to keep your code lines to 80 characters or less, especially if you will be posting to a forum. Very long lines causing line wrap and make the code hard to read.
Please use correct indentation. Many of your code blocks are not indented correctly which cause a reader to need to copy the code into an editor and correct the indents just to read it.
Use space not tabs. (Set your editor to replace TAB characters with 2 spaces.)
Many of your code lines have blank space(s) at the end. This is not needed.
Usability
Use a Hash rather than an Array to copy dictionary data. The Sketchup::AttributeDictionary class has the Ruby Core Enumerable module mixed in, which gives dictionaries a #to_h method that makes using a hash “easy as pie”.
* If you look at the top of the Sketchup::AttributeDictionary class documentation page (or any class page,) you will see a listing labeled “Includes” which tells you what libraries have been mixed into the class.
Using a hash via #to_h will replace your whole
inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
all_attribut << "#{key} = #{value}"
end
block of code.
You are not filtering only dynamic components from your selection.
Your code is assuming every instance is dynamic. You need to select only those whose definition has a "dynamic_attributes" dictionary.
def is_dynamic?(inst)
inst.definition.attribute_dictionary("dynamic_attributes")
end ###
# Gets all dynamic instances from selection
def get_all_dynamic_instances
mod = Sketchup.active_model
sel = mod.selection
sel.grep(Sketchup::ComponentInstance).select do |inst|
is_dynamic?(inst)
end
end ###
Your code is saving all of the attributes in ONE collection without separating them. Each set of an instance’s attributes need to be kept separate and stored by the instance name property which is not a dynamic attribute. It is a Ruby API property (ie, inst.name).
This means you need a collection of attribute sets. This means a hash of nested hashes.
Also, your code is assuming that all dynamic attributes will be attached to the instance. This is not true. Only those that differ from the definition default values will exist in the instance’s dictionary.
(In fact, if none of the attributes differ, then the instance itself will not even have a dynamic dictionary.)
This kind of model specific data does not belong in the application / plugin defaults files.
Save such data in the model’s file folder as a JSON file. We have covered this in this forum category quite a few times. (I myself have posted method snippets to do this.)
Your list of attributes to exclude is missing quite a few of the DC “system” hidden attributes (those that begin with an underscore.)
The SketchUp engine in some cases calls observer callbacks multiple times.
There are likely to be open issues in the official issue tracker for these cases.
Also the DC extension code might make several changes to an instance or it’s definition’s entities which might be the cause of multiple observer callbacks.
You will need to code defensively. But if you use a hash as said above, then the attributes will just overwrite the same key/value in the hash and you will not get multiple entries as you do when just appending an array.
You remove an observer attachment when you no longer need to watch something.
However, the API will automatically remove the observer attachment if the object is deleted, or the model is closed.
I suggest using Aerilius’ Attribute Inspector extension if you are going to be coding an extension that looks at dynamic dictionaries. It is in the Warehouse.
I am preparing a response to all your remarks and I will post it as soon as I have finished analyzing all your comments.
Before that, I would like to share a problem that I do not understand!
Why does the method below not automatically exclude keys with the values indicated in the table?
def save_all_dynamic_attributes(all_attribut)
mod = Sketchup.active_model
sel = mod.selection
exclude = ["_value","_formula","_formulaunits","_access","_formlabel","_units",
"_options","_has_movetool_behaviors", "_hasbehaviors", "_lengthunits",
"_name", "_last_","_lastmodified","_formatversion"
]
sel.grep(Sketchup::ComponentInstance).each do |s|
s.definition.instances.each do |inst|
inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
exclude.each do |ex|
all_attribut[key] = value unless key.include?(ex)
end
end
end
end
end
all_attribut = {}
save_all_dynamic_attributes(all_attribut)
p all_attribut
This would save me from creating a second array to subtract useless keys.
Readability
I use Notepad++ and would like to know if there is something better?
1.Is it possible to force a line break after 80 characters?
2.How do I change the indentation without having to manually rearrange all my lines of code?
3.I can’t see the empty spaces at the end of the lines! How do you see them?
Usability
In my new example I use a hash.
I also filtered instances only with dynamic attributes.
I don’t need to order attributes with instance name because there is only one instance where i need to copy the dynamic attributes.
I don’t understand your remark because the dynamic attributes created manually are indeed attached to the instance. Here is an example that displays all manually created dynamic attributes:
mod = Sketchup.active_model
sel = mod.selection
sel.grep(Sketchup::ComponentInstance).each do |s|
s.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
p "#{key} = #{value}"
end
end
The goal is to retain attributes and their values all the time even on new projects.
5a. JSON file allow to do it and if so how?
5b.Does using the “Sketchup.write_defaultwrite_default” method cause technical problems?
5c. Is it illegal to use “Sketchup.write_defaultwrite_default” for extension information?
Thank you I will use the Inspector extension to know the complete list of hidden attributes to remove.
Code corrected according to the remarks that I think I understood:
module JulienDufren
class Dynamic_cp_change
# Create a hash of the dynamic attributes of the selection with the values
def save_all_dynamic_attributes(attribute)
mod = Sketchup.active_model
sel = mod.selection
exclude = ["_value","_formula","_formulaunits","_access","_formlabel","_units",
"_options","_label","_has_movetool_behaviors", "_hasbehaviors", "_lengthunits",
"_name", "_last_","_lastmodified","_formatversion","_iscollapsed"
]
all_attribute = {}
delete_attribute = {}
sel.grep(Sketchup::ComponentInstance).each do |s|
s.definition.instances.each do |inst|
if inst.definition.attribute_dictionary("dynamic_attributes")
inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
all_attribute[key] = value
exclude.each do |ex|
delete_attribute[key] = value if key.include?(ex)
end
end
end
end
end
att = delete_attribute.each_with_object(all_attribute.dup){|(k, v), h| h.delete(k)}
attribute.merge!(att)
end
# Saves all dynamic attributes of the selection with their values in a file external
# to SketchUp always accessible!
def save_dynamic_attributes
attribute = {}
save_all_dynamic_attributes(attribute)
attribute.each do |key, value|
Sketchup.write_default("Plugin_name", "#{key}", "#{value}")
end
end
# Activate the observer and delete it to prevent it from remaining constantly active.
def onChangeEntity(entity)
save_dynamic_attributes
lenx = Sketchup.read_default("Plugin_name", "lenx")
leny = Sketchup.read_default("Plugin_name", "leny")
lenz = Sketchup.read_default("Plugin_name", "lenz")
p "My component: Height = #{lenz.to_f.to_mm} , Width = #{lenx.to_f.to_mm}
, Depth = #{leny.to_f.to_mm}"
end
end
class My_class
# Trigger the observer if the selected component definition name is = "My_componant"
def self.save_dynamic_attribut
mod = Sketchup.active_model
sel = mod.selection
sel.grep(Sketchup::ComponentInstance) do |s|
defi_attribut = s.definition.attribute_dictionary("dynamic_attributes")
inst_attribut = s.attribute_dictionary("dynamic_attributes")
if s.definition.name.include?("My_componant")
s.add_observer(Dynamic_cp_change.new)
inst_attribut.add_observer(Dynamic_cp_change.new)
defi_attribut.add_observer(Dynamic_cp_change.new)
end
end
end
save_dynamic_attribut
end
end
I like to post the complete codes to find them in the future.
I also do it to share my progress with others and to be corrected by experienced programmers.
Don’t know. Don’t really care as it’s not the best way. Try …
h = inst.attribute_dictionary('dynamic_attributes').to_h
h.delete_if { |key,val| key.start_with('_') }
The hash h now contains only attributes that do not begin with an underscore.
You can merge with the hash passed into the method like:
all_attribut.merge(h)
I also still mostly use Notepad++. Microsoft VS Code is also good.
If NP++ has already replaced the TABs with spaces then either do it manually or you might search for a NP++ Ruby cleanup plugin.
You can manually change the indents for a whole block by highlighting the block and click TAB (to move the block to the right 1 indent) or SHIFT+TAB (to move the block 1 indent to the left.)
Click the cursor to the far left of the lines and this will move the caret to the end of the line.
When you see it has space between the caret and the end of the line, you have unneeded spaces.
This can happen when you accidentally type a TAB at the end of the line.
Then why is your code going through the definition and iterating it’s instances collection ?
Again, this does not belong in the application defaults, unless this is somehow a default for every model that will ever be opened.
As I said, I (and others) have already posted this. Learn how to search the category.
By compiling all the tips in this thread and my research, I think I found the best solution to my problem.
Basic objective:
Purge definitions without instances produced by my dynamic components.
Best solution:
Sketchup::EntitiesObserver.onElementModified
Why?
This watcher is the only one able to fire when a nested component instance is modified!
Unlike Sketchup::EntityObserver, this observer doesn’t require you to browse all instances to watch them!
METHOD
Add an attribute to all definitions of the selection to watch:
def self.set_custom_attribute_all_definitions( ents )
ents.grep(Sketchup::ComponentInstance) do |i|
i.definition.set_attribute "my_dictionary", "my_attribute", "my_value"
set_custom_attribute_all_definitions( i.definition.entities )
end
end
mod = Sketchup.active_model
sel = mod.selection
set_custom_attribute_all_definitions( sel )
Add the EntitiesObserver to purge my definitions without instances in the model:
class MyEntitiesObserver < Sketchup::EntitiesObserver
def onElementModified(entities, entity)
if entity.is_a? (Sketchup::ComponentDefinition)
if entity.count_instances == 0
if entity.attribute_dictionaries
val = entity.get_attribute "my_dictionary","my_attribute"
if val == "my_value"
entity.entities.each do |i|
mod = Sketchup.active_model
mod.start_operation("Purge Definition", true, true, true)
i.parent.entities.clear! if i.valid?
entities.remove_observer(self)
mod.commit_operation
end
end
end
end
end
end
end
mod = Sketchup.active_model
@change_entities ||= MyEntitiesObserver.new
mod.entities.add_observer(@change_entities)
These code examples are a very simplified version of the methods I’m going to use!
The goal is to show the direction I have chosen.
First of all, the crazy excessive nesting is a signal of poor coding practice.
It makes the code hard to read and therefore difficult to understand.
You can use test conditional statements to “bailout” of the method early, …
and sometimes compound boolean expression to combine statements:
class MyEntitiesObserver < Sketchup::EntitiesObserver
def onElementModified(entities, entity)
return unless entity.is_a?(Sketchup::ComponentDefinition)
return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
val = entity.get_attribute("my_dictionary", "my_attribute")
return unless val == "my_value"
mod = entity.model
entity.entities.each do |i|
mod.start_operation("Purge Definition", true, true, true)
i.parent.entities.clear! if i.valid?
entities.remove_observer(self)
mod.commit_operation
end # each ... do
end ### onElementModified()
end # class
However this block does not make sense to me:
mod = entity.model
entity.entities.each do |i|
mod.start_operation("Purge Definition", true, true, true)
i.parent.entities.clear! if i.valid?
entities.remove_observer(self)
mod.commit_operation
end # each ... do
It is iterating entity.entities, but inside if any of the children are valid, then it “seems to be” clearing the same collection that it is iterating. This is a big no-no in programming which leads to errors.
I mean … i.parent is the same as entity, is it not?
And so, entity.entities could be cleared whilst it is being iterated.
I’m not sure when SketchUp will do the definition cleanup and remove empty definitions. We would hope that this does not happen immediately because if entity becomes invalid and more observers are waiting in the queue and will receive the entity reference which they assume would be valid, errors might occur. When errors occur in observer callback there have been crashes in the past.
By publishing my code I suspected that many constructive remarks were going to be given to me.
That’s why it does it every time.
Certainly I’ve gotten into bad habits, but I understand nested code better!
As I am self-directed and Ruby is my first language, I trust you, I will opt for the return method.
I’m going to use Sketchup::ToolsObserver to monitor when the user activates the Interacts with dynamic components icon.
As long as this tool is active, the observer will be too.
dezmo you’re right so I rewrote the code following your advice and that of Dan:
class MyEntitiesObserver < Sketchup::EntitiesObserver
def onElementModified(entities, entity)
return unless entity.is_a?(Sketchup::ComponentDefinition)
return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
val = entity.get_attribute "my_dictionary","my_attribute"
return unless val == "my_value"
mod = entity.model
mod.start_operation("Purge Definition", true, true, true)
entity.entities.clear! if entity.valid?
entities.remove_observer(self)
mod.commit_operation
end
end
mod = Sketchup.active_model
@change_entities ||= MyEntitiesObserver.new
mod.entities.add_observer(@change_entities)
I am currently unable to reproduce the error on a tested component that I could share.
I will continue my research following your advice and I will post a more complete code later with a component producing the error if possible.
As promised here is my final method to purge entities without instances generated by my dynamic components and the “Interact with dynamic components” tool:
# Purge entities without instances that have "my_attribute" attribute!
class MyEntitiesObserver < Sketchup::EntitiesObserver
def all_instances_in_entities( ents, array )
ents.grep(Sketchup::ComponentInstance) do |i|
array << i
all_instances_in_entities( i.definition.entities, array )
end
end
def onElementModified(entities, entity)
return unless entity.is_a?(Sketchup::ComponentDefinition)
return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
val = entity.get_attribute "my_dictionary","my_attribute"
return unless val == "my_value"
return unless entity.valid?
mod = entity.model
mod.start_operation("Purge Definition", true, true, true)
inst = []
all_instances_in_entities( entity.entities, inst )
inst.each{ |i| i.parent.entities.clear! if i.valid? }
mod.commit_operation
end
end
# Active Tools Observer whenever the "Interact DC" is activated
# Or removes it if another tool is selected!
class MyToolsObserver < Sketchup::ToolsObserver
def onActiveToolChanged(tools, tool_name, tool_id)
mod = tools.model
if tool_name == "RubyTool"
@change_entities ||= MyEntitiesObserver.new
mod.entities.add_observer(@change_entities)
else
mod.entities.remove_observer(@change_entities)
end
end
end
# Attach "ToolsObserver" when opening a project or a new model
class MyAppObserver < Sketchup::AppObserver
def activate_observer_tools( model )
@mytoolsobserver ||= MyToolsObserver.new
model.tools.add_observer(@mytoolsobserver)
end
def onNewModel(model)
activate_observer_tools( model )
end
def onOpenModel(model)
activate_observer_tools( model )
end
end
Sketchup.add_observer(MyAppObserver.new)
I had to adapt the methods in order to purge the entities in depth by looking for all the nested instances with their entities.
Otherwise some entities without instances continued to exist in the model.
Ps:Sorry I failed to create a dynamic component that create the same error.
You made an error of judgment on me because I test my codes before posting them!
However, I can understand your confusion with code that is out of context that you can’t test.
Try your last code, you will see that there are errors with the “count_used_instances” method!
You also use “purge_unused” which is a method that removes all user definitions.
And I don’t want to remove any definitions that aren’t mine.
Thank you for this information.
I ended up creating a component that reproduces almost the same problem: DEFINITIONS ERROR BOX.skp (188.3 KB)
Since the component is much simpler, so is the solution:
class MyEntitiesObserver < Sketchup::EntitiesObserver
def onElementModified(entities, entity)
return unless entity.is_a?(Sketchup::ComponentDefinition)
return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
val = entity.get_attribute "my_dictionary","my_attribute"
return unless val == "my_value"
return unless entity.valid?
entities.model.start_operation("Purge Definition", true, true, true)
p "DEFINITION PURGED : #{entity.name}"
entity.entities.clear!
entities.model.commit_operation
end
end
mod = Sketchup.active_model
@myentitiesobserver ||= MyEntitiesObserver.new
mod.entities.add_observer(@myentitiesobserver)
Ps: I was wrong in answering on this topic because all my last posts concern the other topic.
Is it possible to move all the latest posts to the “Create an array of all modified entities” topic?