IDK Compilation

So my initial set up did not require @dialog = UI::HtmlDialog.new. But I went down a path of @dialog at first trying to get the selection observer to work. So I had:

@dialog instance variable creep
module IDK_Programming
    module Dc_Ac

        PLUGIN_PATH ||= File.dirname(__FILE__)

        # Module-level variable to hold the dialog reference
        @dialog = nil

        # Method to access the module-level variable
        def self.dialog
            @dialog
        end

        class DcSelectionObserver < Sketchup::SelectionObserver #But should this be a class or a local observer in the create_html_dialog method?
            def onSelectionBulkChange(selection)
                puts "Selection changed"
                if selection.single_object?
                    entity = selection[0]
                    if entity.is_a?(Sketchup::ComponentInstance) && entity.attribute_dictionary("dynamic_attributes")
                        puts "DC selected, updating attributes"
                        IDK_Programming::Dc_Ac.update_dc_attributes_display(IDK_Programming::Dc_Ac.dialog, entity)
                    else
                        # Clear the attributes display if the selection is not a DC
                        IDK_Programming::Dc_Ac.clear_dc_attributes_display(IDK_Programming::Dc_Ac.dialog)
                    end
                else
                    # Clear the attributes display if there's no single object selected
                    IDK_Programming::Dc_Ac.clear_dc_attributes_display(IDK_Programming::Dc_Ac.dialog)
                end
            end
        end

        def self.create_html_dialog
            @dialog = UI::HtmlDialog.new(
            {

#### Removed below.

There was also another little problem with the Html Dialog not opening in the previous state (that I’ve consistently bumped into with Html Dialogs). I thought I needed:

@dialog = dialog

Within the method:

def self.show_dialog

Maybe this was not a real problem that I needed to solve. It seems right to tuck the instance variable away into the class. But I can’t justify that.

You could keep the @dialog as an extension module variable and pass it into the observer constructor, ie:

    # within create_html_dialog()
    @observer = DcSelectionObserver.new(@dialog)

… and in the observer …

    def initialize(dialog)
        @dialog = dialog
    end

… but read on.

First of all, there is no requirement for observers to be implemented as a class (or subclass) and then instantiated.
The only requirement is that observers be an object (which is easy because everything in Ruby is an object and a subclass of class Object,) … and that any defined callbacks be publicly accessible.

Secondly, since the SU2016 observer overhaul 99% of observer superclasses are for documentation only and actually have no ancestor callback methods to pass down to child subclasses. NONE of the API’s various #add_observer methods do any type checking to verify if an observer object is any particular subclass or has any particular ancestor class(es).

This means that any Ruby object can act as an observer. A weird example could be adding a singleton observer callback method to a Sketchup::Face object and then attaching this face as an EntityObserver to itself. (It’s not a very practical solution however.)
A more practical example (which I myself use often) is to use the extension submodule itself as an AppObserver. Since there is always only one application object, there really is never any need to instantiate multiple AppObserver objects within SketchUp’s running process.

This leads to the main question a coder should ask when implementing an observer. How many observers will be needed? If more than one, then the observer should be implemented as a class and instantiated as needed. Okay, when is more than one needed? The answer usually is if the observer needs to hold state within it that is unique to any given model object that may be open (think multiple models on the Mac platform) then normally a separate observer instance will be required per model.
If there was not to be any unique state (or data) held by the observer, then it is possible to implement a shared observer that can be attached to multiple models (or multiple objects owned by multiple models.)

So, in your case you have one dialog. This dialog would need to know when the active model was changed on the Mac so that the SelectionObserver acts upon the selection collection of the active model. It may be possible then (if you stay with one single dialog,) to use the extension submodule as the selection observer object that acts upon the single dialog no matter which model is the active model.
This will mean that you also will need to implement some AppObserver callbacks and attach the extension submodule to the application at the bottom of your files (probably within the “run once” block where you create UI objects.) Ie …

            unless defined?(@loaded)
                # UI commands, menu and toolbars
                Sketchup.add_observer(self) # attach module as an AppObsever
                @loaded = true
            end

Then in the AppObserver callbacks, you again attach the submodule as a selection observer to each model as it’s opened or created.

The beauty of this approach is that everything is in the same submodule and there is only one shared @dialog reference. Also all the methods are at the same level and can be called without the clunky IDK_Programming::Dc_Ac full qualification.

When you have a single object like a submodule acting as more than one kind of observer, this is called a “hybrid observer”. Recall I said that there really is no functionality in the API observer superclasses so there is no need to subclass them as there is no methods to inherit.
So you can have your extension submodule act as both an AppObserver and a common SelectionObserver.


For more information, often the best flavor of hybrid observers is a class that serves as the main observer object for model wide collections and is instantiated for each model that is created or opened. In the initialize method for that class receives the model object as a parameter (from AppObsever callbacks) and then proceeds to attach itself to each one of the model’s collection objects (selection, definitions, layers, materials, etc., etc.)


  • P.S. - Try to keep code lines below 81 characters so we do not have to scroll horizontally.
    Ruby uses 2 space indentation. Using larger indents contributes to excessive line length.

So one problem fixed. The selection wasn’t clearing on model space clicks.

Tried modifying the dynamic_attributes dictionary with input fields… but removed for now.

Tried displaying dynamic attributes:

@DanRathbun - I attempted your suggestion by trying to create a hybrid observer approach. App Observer is just a stub. The code should be wrapped and spaced correctly now as well.

Attach Observers
unless defined?(@loaded)
      create_toolbar()
      add_menu_item()

      # Attach the module as a SelectionObserver
      attach_selection_observer
  
      # Attach the module as an AppObserver
      Sketchup.add_observer(self)
  
      @loaded = true
    end
Unused App Observers
def self.onNewModel(model)
      # Code to execute when a new model is created
    end
    
    def self.onOpenModel(model)
      # Code to execute when a model is opened
    end
    
    def self.onSaveModel(model)
      # Code to execute when a model is saved
    end
Attach Module as Selection Observer
#Attach the module as a SelectionObserver
    def self.attach_selection_observer
      Sketchup.active_model.selection.add_observer(self)
    end
Observer Callback Methods
def self.onSelectionBulkChange(selection)
      handle_selection_change(selection)
    end

    def self.onSelectionCleared(_selection)
      clear_dc_attributes_display
    end
Selection Handlers
    def self.handle_selection_change(selection)
      return if selection.empty? || @dialog.nil?
  
      selected_entity = selection[0]
  
      
      if selected_entity.is_a?(Sketchup::ComponentInstance) || selected_entity.is_a?(Sketchup::Group)
        display_name = selected_entity.definition.name
       
        dynamic_name = ""

        if selected_entity.definition.attribute_dictionary("dynamic_attributes")
          attributes = selected_entity.definition.attribute_dictionary("dynamic_attributes")

          dynamic_name = attributes["_name"] if attributes.include?("_name")

          name = attributes["name"] || "No Name Provided"
          lengthunits = attributes["_lengthunits"] || ""

          name_label = attributes["_name_label"] || "Name"
          summary_label = attributes["_summary_label"] || "Summary"
          description_label = attributes["_description_label"] || "Description"
          itemcode_label = attributes["_itemcode_label"] || "Item Code"
          name = attributes["name"] || ""
          summary = attributes["summary"] || ""
          description = attributes["description"] || ""
          itemcode = attributes["itemcode"] || ""

          inst_x = attributes["_inst_x"] || ""
          inst_y = attributes["_inst_y"] || ""
          inst_z = attributes["_inst_z"] || ""
          x_label = attributes["_inst__x_label"] || "X"
          y_label = attributes["_inst__y_label"] || "Y"
          z_label = attributes["_inst__z_label"] || "Z"

          lenx = attributes["lenx"] || ""
          leny = attributes["leny"] || ""
          lenz = attributes["lenz"] || ""
          lenx_label = attributes["_lenx_label"] || "LenX"
          leny_label = attributes["_leny_label"] || "LenY"
          lenz_label = attributes["_lenz_label"] || "LenZ"

          inst_rotx = attributes["_inst_rotx"] || ""
          inst_roty = attributes["_inst_roty"] || ""
          inst_rotz = attributes["_inst_rotz"] || ""
          rotx_label = attributes["_inst__rotx_label"] || "RotX"
          roty_label = attributes["_inst__roty_label"] || "RotY"
          rotz_label = attributes["_inst__rotz_label"] || "RotZ"

          hidden = attributes["hidden"] || ""
          material = attributes["material"] || ""
          onclick = attributes["onclick"] || ""
          scaletool = attributes["scaletool"] || ""
          hidden_label = attributes["_hidden_label"] || "Hidden"
          material_label = attributes["_material_label"] || "Material"
          onclick_label = attributes["_onclick_label"] || "onClick"
          scaletool_label = attributes["_scaletool_label"] || "ScaleTool"

          excluded_keys = []

          formatted_attributes = attributes.map do |key, value| 
              next if excluded_keys.include?(key)
              "#{key}: #{value}"
          end.compact.join(",")  

          script = "updateDcAttributesDisplay(
          '#{display_name}', 
          '#{lengthunits}', 
          '#{lenx_label}', 
          '#{leny_label}', 
          '#{lenz_label}', 
          '#{lenx}', 
          '#{leny}', 
          '#{lenz}', 
          '#{formatted_attributes}', 
          '#{name_label}', 
          '#{summary_label}', 
          '#{description_label}', 
          '#{itemcode_label}', 
          '#{name}', 
          '#{summary}', 
          '#{description}', 
          '#{itemcode}', 
          '#{dynamic_name}', 
          '#{x_label}', 
          '#{inst_x}', 
          '#{y_label}', 
          '#{inst_y}', 
          '#{z_label}', 
          '#{inst_z}', 
          '#{rotx_label}', 
          '#{inst_rotx}', 
          '#{roty_label}', 
          '#{inst_roty}', 
          '#{rotz_label}', 
          '#{inst_rotz}', 
          '#{hidden_label}', 
          '#{hidden}', 
          '#{material_label}', 
          '#{material}', 
          '#{onclick_label}', 
          '#{onclick}', 
          '#{scaletool_label}', 
          '#{scaletool}'
          )"
          
          @dialog.execute_script(script)

        else
          script = "updateDcAttributesDisplay('#{display_name}')"
          @dialog.execute_script(script)
        end
        else
        puts "Selected entity is not a component or group."
      end
    end
    
    
###    

    def self.clear_dc_attributes_display
      return if @dialog.nil?
  
      @dialog.execute_script("clearDcAttributesDisplay()")
    end

I noticed that I get the Ruby Console message, “Dialog instance is not initialized.”, a lot with this last attempt. Previously I had thought the instance variable was the culprit. But I couldn’t get rid of it.

The call to attach_selection_observer() should be within the AppObserver callbacks:
onNewModel and onOpenModel like so:

    def self.onNewModel(model)
      # Code to execute when a new model is created
      attach_selection_observer(model)
    end
    
    def self.onOpenModel(model)
      # Code to execute when a model is opened
      attach_selection_observer(model)
    end

… and also need to have this callback so that when SketchUp starts up that the AppOberver callbacks are called with the startup model as an argument:

    def self.expectsStartupModelNotifications
      return true
    end

… and the attach_selection_observer() method should be:

    # Attach the module as a SelectionObserver
    def self.attach_selection_observer(model)
      model.selection.add_observer(self)
    end

This is a Core Ruby warning for newer Ruby versions.

They do not think it is good practice to reference an @var instance variable before it has been initialized.
In the past Ruby versions, the interpreter would just use nil as the evaluated value for uninitialized @vars.

So, now if using an @var at the module level, you need to have an initialization statements near the top of the module before any statements that try to access (read) it’s value, such as the 1st statement in the handle_selection_change() method.

The soultion is to put:

        @dialog = nil

… near the top of the module so the variable is initialized before it can possibly be accessed.

Oh, good grief! I thought instance variables might be causing some kind of problem… and that’s why I didn’t want to have ruby @dialog = UI::HtmlDialog.new previously.

But maybe someone also neglected to disable other poor-behaving extensions in their Extension Manager? I scattered ruby @dialog = nil all over until it finally dawned on me (ahem, ‘someone’) that the solution wasn’t working because it wasn’t being applied in the right place.

Banished!

Thank you.

1 Like

Getting dynamic attributes, updating them. Creating new attribute dictionary populated with selected DC attributes.

I’ve got another Behavior problem: no inclusion check box appearing for Behaviors. But added option to copy and display all of the dynamic attributes into the custom attributes dictionary.

Tried copying DC attributes to custom attributes and modifying DC/Component with custom attribute values. Custom attribute values can be used even if not present in DC.

Hmmm … just checking. Do you realize that there can be 2 “dynamic_attibutes” dictionaries?
One is attached to the definition and has all the default values. If there are more than 1 instance and an instance’s attribute values differ from the default (in the definition’s dictionary,) then the instance will have it’s own “dynamic_attibutes” dictionary whose values override the definition’s.

So, in this sense, any dynamic component instance can have custom dynamic attributes that apply to it alone.

Yes. I’ve been told that there are two dictionaries. One I found (“dynamic_attributes”). The other I’m told can be found. But I’m not sure how. I noticed that attributes are added to the dynamic attributes dictionary when it is updated.

So, for example, I display the attributes similarly to how they are displayed in the Component Options and also display all of the attributes in the dynamic attributes dictionary in a togglable container (Show DC attributes) just to investigate what is there. Let’s say I add all of the Size attributes. I’ll have:

_lengthunits: INCHES
_lenx_label: LenX
_leny_label: LenY
_lenz_label: LenZ
_name: Thing
lenx: 86.0
leny: 129.0
lenz: 71.0

If I enter a new size value in Component Attributes, then new attributes appear in the dynamic attributes dictionary:

_formatversion: 1.0
_has_movetool_behaviors: 0.0
_lastmodified: 2023-12-17 16:52
_lengthunits: INCHES
_lenx_label: LenX
_leny_label: LenY
_lenz_label: LenZ
_name: Thing
lenx: 90
leny: 129.0
lenz: 71.0

Of course, the SU DC extension is adding those (and can add many others). I don’t really know what those do. In the previous version of this extension (DC AC), I tried to write values directly to the dynamic attributes dictionary. There were some problems. So, this extension version (AC DC) is just investigating creating a custom attributes dictionary and modifying the component using those. It doesn’t need to access the dynamic attributes. But by modifying something like size x using the custom attributes and then looking at the dynamic attributes dictionary I can see that x in the dynamic attributes dictionary is not changed. Which tells me something. I supposed that if I can’t control the functions of a DC with the dynamic attributes dictionary because of the unknowns about ‘structure’ of the code in the ‘black box’ of the DC extension, I could try writing the code to perform the same functions using a custom attributes dictionary. For fun and practice. I’m poking around and doing what some might describe as a sort of hacky ‘reverse engineering’. But really I’m trying to employ principles of biology and trying to create a DC extension “homoplasy”. Sir Richard Owen first described a homology as, “the same organ in different animals under every variety of form and function.”. Only the holders of the original DC extension code could further modify it. A homoplasy is ‘a different structure with the same function’.

So if I’m already using the definitions’ dynamic_attributes dictionary, do I need to check again for the instances’ “dynamic_attributes” dictionary?

Goodness gracious! I found something. So far it looks like the values are going to be the same in the instance as they are in the definition. I’m going to look into this now.

YES

Everytime in fact.

Write yourself an attribute reader method that always checks for an instance “dynamic_attributes” dictionary first (for a component instance and a group instance,) then (if it is not a nested dynamic group) check the definition’s “dynamic_attributes” dictionary.

Changing dynamic attributes is a trickier proposition. If there is only one instance, then changes are (if I remember correctly) made to the definition’s dictionary (if it is not a group. Ie, nested dynamic groups only have a dynamic dictionary upon the instance.)
If there are more than one instance and their (or any of them) have attributes that differ from the definition dictionary, then the DC extension will create a dictionary upon the instance, that only has the attributes that differ (and a couple of general attributes like _formatversion.)
Also, when an instance gets a differenced attribute value there is a “last used value” attribute in the definition’s dictionary that also gets that new value. These “last used” values in the definition are used when a new instance is made by grabbing the component again from the Components panel. (I suppose it is thought that the user will want any new instances to be “like” the last one modified.)

There is no manual for the DC extension. The only thing to do is use a attribute inspector like @Aerilius’ and play with DCs, watching what happens.

Thanks Dan. I started moving forward with that.

Pre-DC state:

DC created by the addition of all Position attributes in Component Attributes dialog. The AC DC extension is simulating DC extension by showing attributes from the newly added DC dynamic_attributes dictionary on the component definition:

A display of all of the attributes in the dynamic_attributes dictionary as well as the (also newly added) dynamic attributes on the dictionary attached to the component instance. Lo, the instance attributes!:

And now we encounter the first zone of incomprehensibility. The DC position x is changed to 10" in the Component Attributes dialog and the definition does not reflect this. but the instance does:

But maybe not so befuddling? Changing values in the dynamic_attributes dictionary on the definition didn’t work because the attached instance dictionary was the one that was needed in the first place.

More drama. Adding and changing size was immediately reflected in both dictionaries:
EDIT: Oops, wrong picture. But I’m leaving it.

So I created a new DC by adding size first. That was updated in both dictionaries upon a change. I’m going to have to fiddle around.

Yes, it is a very complex extension.

It will also create unique definitions “on the fly” for nested objects as needed.

Went back to the Draper extension to try a few things. Got a drape with no rotation added so I could begin adding swapping. @mihai.s for some of his latest vids ;^).

The Drop ‘N’ Swap:

1 Like

MultiSwap. Added preview thumbnails. Trying to use Modus.

1 Like

Multi Swap ‘n’ Drop:

New toy. A Grid Generator.

1 Like

Guides and Guide Points, or Guides or Guide Points.

Auto elevate contour lines.

1 Like