We are building an extension that combines a 3D model catalog with the power of our e-commerce website. The issue we are facing is as follows, when the user adds a light fitting model to the model a few things need to happen. First thing is to store a product slug in an array so we can track the products that have been added this is so we can send these slugs to our api and build a basket in our web application which is working fine. When the user deletes a light fitting model we track this and need to remove the correct slug from the array so the basket will always be correct. How can we add a slug to the model when we import it so we have something to retrieve when we want to update the array if a light model is deleted. We have had no luck adding attributes to the imported model and at the time of import don’t seem to have access to entity info.
# JavaScript callback to add light
dialog.add_action_callback("add_light") do |_, light_file, slug|
puts "Light file requested: #{light_file}"
@@added_slugs << slug
puts "Slug added: #{slug}. Current slugs: #{@@added_slugs.inspect}"
save_slugs_to_model # Save slugs to model after adding
add_light_to_model(light_file, slug)
slugs_json = @@added_slugs.to_json
dialog.execute_script("updateAddedSlugs(#{slugs_json})")
end
# Method to add the light fitting model to SketchUp
def self.add_light_to_model(light_file, slug)
model = Sketchup.active_model
folder_path = File.join(__dir__, 'models', slug) # Path to the folder named after the slug
file_path = File.join(folder_path, light_file) # Path to the .dae file inside the folder
puts "Model Folder Path: #{folder_path}"
puts "Model File Path: #{file_path}"
puts "Model File exists? #{File.exist?(file_path)}"
if File.exist?(file_path)
begin
# Import the model
result = model.import(file_path)
if result
UI.messagebox("Light fitting added to the model.")
model.attribute_dictionary("liteworld", true)["last_slug"] = slug
else
UI.messagebox("Failed to import the light fitting.")
end
rescue => e
UI.messagebox("Failed to add light fitting: #{e.message}")
puts "Error: #{e.message}"
end
else
UI.messagebox("Light fitting model not found for slug: #{slug}.")
end
end
(1) The attributes_dictionaries
collection of a model and any of it’s Entity
subclass objects is a shared space. So the name of any dictionary should be globally unique. Usually this means a prefix that matches the top-level Ruby module namespace for your extension which also must be unique.
Ex: if you company (or domain) name is “LightWorld
” then all your code would be wrapped up in a module of the same name, and within this an extension submodule, say (for argument’s sake this is your “LightingWidget
” extension …
module LightWorld
module LightingWidget
# Local constant for dictionary name:
DICT ||= Module.nesting[0].name.gsub('::','_')
end
end
Then any attribute dictionaries that this extension creates would use this module nesting qualification as its name, resulting in: "LightWorld_LightingWidget"
(2) An attribute dictionary is more like a Ruby Hash
than an array, Meaning, each member is a kay/value pair. So, it would make sense to store them using unique keys, say a stock number?
Also, what many new SketchUp coders do not notice is that an AttributeDictionary
is itself a subclass of Entity
and therefore itself can have an AttributeDictionaries
collection. Ie, nested dictionaries.
So, for example, your extension dictionary could have a subdictionary for each light stock number, whose keys could be the object’s persistent_id
which allows each instance to be located in a later session and either modified or deleted.
Thank you for your valuable insights. I have now corrected the extension name and set up the local constant for dictionary name. Please could you provide an example of how i could add a stock number and persistant id for the model after importing it so i have something to locate when deleting a model?
# Method to add the light fitting model to SketchUp
def self.add_light_to_model(light_file, slug)
model = Sketchup.active_model
folder_path = File.join(__dir__, 'models', slug) # Path to the folder named after the slug
file_path = File.join(folder_path, light_file) # Path to the .dae file inside the folder
puts "Model Folder Path: #{folder_path}"
puts "Model File Path: #{file_path}"
puts "Model File exists? #{File.exist?(file_path)}"
if File.exist?(file_path)
begin
# Import the model
result = model.import(file_path)
if result
UI.messagebox("Light fitting added to the model.")
# add attributes at this point
else
UI.messagebox("Failed to import the light fitting.")
end
rescue => e
UI.messagebox("Failed to add light fitting: #{e.message}")
puts "Error: #{e.message}"
end
else
UI.messagebox("Light fitting model not found for slug: #{slug}.")
end
end
Here’s something i tried and the results i got
def self.add_light_to_model(light_file, slug)
model = Sketchup.active_model
file_path = File.join(__dir__, 'models', slug, light_file)
if File.exist?(file_path)
begin
result = model.import(file_path)
if result
new_component = model.active_entities.grep(Sketchup::ComponentInstance).last
if new_component
add_slug_attribute(new_component, slug)
else
puts "No new component instance found after import."
end
else
UI.messagebox("Failed to import the light fitting.")
end
rescue => e
UI.messagebox("Failed to add light fitting: #{e.message}")
end
else
UI.messagebox("Light fitting model not found: #{file_path}")
end
end
# Add a slug attribute to a component
def self.add_slug_attribute(component, slug)
return unless component.is_a?(Sketchup::ComponentInstance)
# Create or access the dictionary for this slug
main_dict = component.attribute_dictionary(DICT, true)
slug_dict = main_dict.attribute_dictionary(slug, true)
slug_dict["persistent_id"] = component.persistent_id
puts "Added slug attribute: #{slug} to component #{component.persistent_id}"
end
# Globally unique dictionary name
DICT ||= Module.nesting[0].name.gsub('::', '_')
class EntitiesChangeObserver < Sketchup::EntitiesObserver
def initialize(dialog)
@dialog = dialog
end
def onElementRemoved(entities, entity_id)
puts "Entity removed with ID: #{entity_id}"
model = Sketchup.active_model
removed_entity = model.find_entity_by_persistent_id(entity_id)
if removed_entity && removed_entity.is_a?(Sketchup::ComponentInstance)
removed_slug = Liteworld::DesignPlugin.get_slug_attribute(removed_entity, slug)
if removed_slug
Liteworld::DesignPlugin.remove_slug(removed_slug)
puts "Removed slug: #{removed_slug}"
slugs_json = Liteworld::DesignPlugin.get_added_slugs.to_json
@dialog.execute_script("updateAddedSlugs(#{slugs_json})")
else
puts "No slug found for component with ID: #{entity_id}"
end
else
puts "No valid entity found for ID: #{entity_id}."
end
end
here is the log i get when import the first model and another model to follow that, the first import we get no component instance found and the second import we do get a component instance found, however this id does now match when we delete a model.
No new component instance found after import.
Added slug attribute: ceiling-lamp-merida-4-white to component 44315
Entity removed with ID: 76245
No valid entity found for ID: 76245.
Entity removed with ID: 76245
No valid entity found for ID: 76245.
Entity removed with ID: 19419
No valid entity found for ID: 19419.
Entity removed with ID: 19419
No valid entity found for ID: 19419.
…
Sorry, you will need to figure this out yourself as this is commercial project.
Regardless, I do not have a test DAE file and do not know what a slug
reference represents. Nor do I have a test SKP model. I cannot debug your extensions from a set of snippets cut out of the whole.
Notes:
When an import finishes successfully, a new component definition will be added to the model’s DefintionList
collection. So, the temporary attachment of a DefinitionsObserver
may be useful. Once the import is handled the observer can be dettached.
It is common to need to explode the component wrapped instance which then makes the defintion no longer needed. (It can be later purged from the DefintionList
collection.)
If you are calling a method in the same scope you should not need to explicitly qualify the method calls as in:
Liteworld::DesignPlugin.remove_slug(removed_slug)
See calling_methods - Documentation for Ruby 3.2