Displaying a Count of Component Instances


#1

Continuing the discussion from Question on an Animation and Counting visible copies of components:

I’m trying to display a real-time updating count of the number of instances of components, or the number of instances of related components, on the screen as a model note.

For example, if I have three distinct components, A, A#1, and B, I would like it to be able to display the instances of A and the instances of B; and to display the combined instances of A and A#1, and the instances of B.

I’ve tried my hand at writing something, but I’m only about 8 hours familiar with Ruby and using Ruby with SketchUp. I tried to clobber something together by referring to the two discussions linked above, and lots of googling. In the following code, the “General” mode does not work, and the “Strict” mode almost sort of works.

The dialog box asks for names for up to 5 components, and to choose either a general (attempts to count components and its made unique’s as one) or strict (must have exact spelling match).

Could someone help with this?

module DynamicUnitCount
    def self.add_unitcount(model)
        model = Sketchup.active_model
        definitions = Sketchup.active_model.definitions
        
        prompts = ["1","2","3","4","5","Type of Count:"]
        defaults = ["1BED", "STUDIO","","","","General"]
        list = ["","","","","","General|Strict"]
        
        input = UI.inputbox(prompts, defaults, list, "Which components to count?")
        if input
            for i in 0..4

                if input[i]
                    base_name = input[i]
                    note = model.add_note("component: #instances", 0.1, 0.1)
                    
                    if (input[5] == "General")  
                        general_counter = 0
                        
                        definitions.each do |defn|
                            if defn.name.start_with? base_name
                                defn.add_observer(GeneralObserver.new(note, base_name, general_counter))
                            end
                        end
                        
                    end
                    
                    if input[5] == "Strict" 
                        definitions.each do |defn|
                            if defn.name == base_name
                                defn.add_observer(StrictObserver.new(note, base_name))
                            end
                        end
                    end
                    
                    
                    
                end
            end
        end
    end
    
    class StrictObserver < Sketchup::DefinitionObserver      
        
        def initialize(entity, name)
            @entity = entity
	@name = name
        end
        
        def onComponentInstanceAdded(definition, instance)
            count = definition.count_instances
            @entity.text = "#{@name}: #{count}"
        end
        
        def onComponentInstanceRemoved(definition, instance)
            count = definition.count_instances
            @entity.text = "#{@name}: #{count}"
        end
    end
    

    class GeneralObserver < Sketchup::DefinitionObserver      
        def initialize(entity, name, general_count)
            @entity = entity
	@count = general_count
        end
        
        def onComponentInstanceAdded(definition, instance)
	count += definitions.count_intances
            @entity.text = "#{@name}: #{count}"
        end
        
        def onComponentInstanceAdded(definition, instance)
            count = count - definition.count_instances
            @entity.text = "#{@name}: #{count}"
        end
    end     #end class GeneralObserver
    
    unless file_loaded?(__FILE__)
        command = UI::Command.new("Unit Count"){
            self.add_unitcount(Sketchup.active_model)
        }
        
        UI.menu("Plugins").add_item(command)
        file_loaded(__FILE__)
    end
end # module DynamicUnitCount

#2

The general observer can not work because of undefined local variables (count, definitions), a typo in the method count_intances and a typo in the method for onComponentInstanceRemoved.

def onComponentInstanceAdded(definition, instance)
  # count += definitions.count_intances
  @count += definition.count_instances

This would raise an error. Since this method is neither invoked at load time nor from the Ruby Console, the errors would not be caught and reported. It is a silent error (unless you add to your observer methods your own error handling code during debugging:

  @entity.text = "#{@name}: #{@count}"
rescue Exception => e
  puts e.message
  puts e.backtrace.join("\n")
end

Then instead of using the name onComponentInstanceRemoved, this method is overridden, by another implementation that decreases the counter, that’s why you don’t see the desired effect.

#def onComponentInstanceAdded(definition, instance)
def onComponentInstanceRemoved(definition, instance)
  @count -= definition.count_instances
  @entity.text = "#{@name}: #{@count}"
end

P.S.:
At the third line, you shouldn’t do Sketchup.active_model if you already have a reference (method parameter) to a (potentially not the same) model.

definitions = model.definitions

#3

Thanks, I made the changes you pointed out. I didn’t realize I had so many typos. Why do we use @var = passed_in_variable, instead of using passed_in_variable directly?

For the GeneralObserver, is that the proper way to go about having a ‘shared’ count between similar components, but to have different sets of similar components to have different counts?

For StrictObserver, the count seems to display 0 when I am not actively copying it. For example, if I create a component and then Ctrl+Move to copy it several times, it might display 0, 1, 0, 2, 0, 3, 0, showing 0 when I select the component, showing the number when I ctrl+move, and going back to 0 when I place the new instance of the component. Furthermore, the same text note displays counts for different units (when the count is displayed at all), whether or not they strictly match the input name.


#4

There is another mistake I forgot: In GeneralObserver, you pass a name parameter into the constructor, but don’t use it. Later you use @name variable that hasn’t been initialized.

Because we are not doing anything with the (“local”) passed-in-parameter other than storing it for persistently so we can access it when we need it. Variable names starting with @ are instance variables, they persist outside of the scope of a specific method (class attributes in Java). Variables starting with @@ are class variables shared between all instances of a class and its child classes (corresponds somewhat to static attributes in Java). Variables starting with a Capital letter are constants.
Everything everything else is a local variable and dies at the end of the current method’s scope.

I had only looked at the language level, not yet the semantics. For me it’s not clear what a “General” observer and a Strict" one are specified to do, and what they actually do does not all make sense. If you would (try to) describe the data flow and the functioning of the plugin, and you would probably figure out what is inconsistent.

Can you explain me what the GeneralObserver should do, and how it tries to achieves that?


#5

Certainly.

GeneralObserver should keep track of the combined number of instances of a group of components.

For example, if there the following components: #instances:

A: 5
A#1: 2
A#2: 1
B: 1
C: 2
C#1: 2

and the user inputs “A”, “B”, and “C” and selects “General”, the GeneralObserver should display,
A: 8
B: 1
C: 4
in contrast, with the same user input, but with “Strict” selected instead, the display should be:
A: 5
B: 1
C: 2

GeneralObserver is a ‘general’ match, whereas StrictObserver is a ‘strict’ match to the given input name.

This is what I attempted to do with the GeneralObserver:

for each input,
for each definition which begins in the same way as the given input (input = A, matches = A, A#1, A#2, A#3), I create a new GeneralObserver. I pass in the same note entity to these different GeneralObservers. My thinking here was that all the observers would still modify the same note. The observers get the current count from the note, add (or subtract) their own specific component count to the note, and then redisplay the note.

I thought that there would probably be a better way to do this via a shared variable, such as a class variable, but I don’t want all the instances of the GeneralObserver class to share the same count, just the ones that were created in the same iteration over the definition list for a specific input. (So using the example input above, all the A’s would share a count, and all the C’s would share a count, but they are two separate counts).

if defn.name.start_with? base_name
  defn.add_observer(GeneralObserver.new(note, base_name, general_counter))
end

I thought a bit about inheritance but it seemed like Ruby only allows inheritance once, and GeneralObserver is already inheriting from Sketchup’s DefinitionObserver class – and I’m not too sure on how that would work.

Oh, I understand why the local value is assigned to an instance variable now. The value must persist in the instance of the class past the initialization of the class, so that its methods can use it. Obvious in retrospect. I was thinking about a class sort of like a struct, somehow.