Understanding objects and sub-classes


#1

I’m very much a beginner at Ruby and the SketchUp API, and one fundamental idea has me quite confused. I understand that nearly everything in Ruby is an object. In the context of SketchUp, this is actually really easy to relate to since SketchUp is used to model “things”, so it’s quite natural to make the connection.

I know that SketchUp entities are organized into subclasses, like Entity > DrawingElement > Group, for example. Meaning, a Group is a “type of” DrawingElement, and a DrawingElement is a “type of” Entity.

Now what confuses me is when I want to create my own class. For example, let’s say I want to create a Class for Studs, and I might include such attributes like lumber_species, grade, pressure_treated, etc, and I might include methods to check for a minimum or maximum length allowed… whatever.

Now, in my mind, I would think that I should create my Stud class as a subclass of Group, but something tells me I’m not right in that way of thinking. But I feel like the “Stud” is a “type of” group, no?

Otherwise, it’s almost like I have redundant objects. One object is the “representation” of the object with all my defined attributes, and the other copy of the object is the “physical” object that is displayed in SketchUp. Shouldn’t there just be a single object that inherits everything and includes my custom attributes and methods and IS the actual group object in SketchUp?

Hopefully that makes sense.
What am I misunderstanding?


#2

I wouldn’t try to subclass SketchUp’s entity classes. The API methods that gives you a reference to a group, e.g. from the selection, gives a reference to an object of the type defined in the API.

What you can do is to create your own stud class, and have an instance variable pointing to the corresponding Sketchup::Group. The stud object represent the stud as an idea, and defines all behavior related to the stud concept. The corresponding group is the physical/visual representation of the stud, but not the stud per se. This gives a nice separation of your conceptual object (stud) and SketchUp’s “physical” object (a group containing some faces and edges).

I typically have a superclass for all classes that represents objects that can be drawn in the model. This class defines methods for serializing custom objects and save them to SketchUp entities, as well as to later revive these custom objects from said entities.

Here’s an example of a scalable implementation:

class Serializable
  # In this example all serializable objects are represented as groups in the
  # model.
  attr_accessor :group, :name

  # Edit this. I typically reference a PLUGIN_ID constant defined in plugin
  # loader as the basename of the plugin loader file (same as the plugin
  # dirname).
  DICT_NAME = "my_extension".freeze

  ATTR_CLASS_NAME = "type".freeze

  def initialize(group = nil)
    @group = group
    read_attributes if group
  end

  # Write instance variables as attributes to @group.
  #
  # @return [Void]
  def write_attributes
    class_base_name = self.class.name.split("::").last
    @group.set_attribute(DICT_NAME, ATTR_CLASS_NAME, class_base_name)
    @group.name = @name

    instance_variables.each do |var_name|
      # Purge leading @ from key.
      key = var_name.to_s[1..-1].to_sym
      next if key == :name
      next if key == :group
      @group.set_attribute(DICT_NAME, key.to_s, instance_variable_get(var_name))
    end

    nil
  end

  # Read attributes saved to @group and assign them as instance variables.
  #
  # @return [Void]
  def read_attributes
    @group.attribute_dictionaries[DICT_NAME].each_pair do |key, value|
      next if key == ATTR_CLASS_NAME
      # Prefer public attribute writer method over just setting the instance
      # variable, as setting some attributes may have side affects, like
      # updating other values or parsing into objects used internally.
      setter_name = "#{key}=".to_sym
      send(setter_name, value) if respond_to?(setter_name)
    end

    @name = @group.name
  end
end

class Stud < Serializable
  # Sett attr_accessors for length, width, height, start, end etc.

  def draw
    # Create a @group if not defined, or deleted.
    # Set group transformation according to instance variables for position.
    # Set group scale flag (if needed).
    # Call write_attributes.
    # Clear group.
    # Draw anew into group according to the instance variables.
  end
end

You can then create a new stud, give it some attributes, draw it, and at a later time read it, change some attributes and then redraw it.

stud = Stud.new
stud.length = 3.m
stud.width = 5.cm
stud.draw

# Later...
stud = Stud.new(Sketchup.active_model.selection.first)
stud.length = 7.m
stud.draw

#3

Some extra methods in the superclass can be handy to re-initialize elements of any subclass, or test if an entity represents an objects of a certain class.

  # Create serializable objects represented by active entities (those in the active
  # drawing context).
  #
  # When called on a subclass of Serializable, only objects of that class are
  # returned.
  #
  # @return [Array<Serializable>]
  def self.from_active
    from_entities(Sketchup.active_model.active_entities)
  end

  # Create serializable objects from Entities.
  #
  # When called on a subclass of Serializable, only objects of that class are
  # returned.
  #
  # @param entities [Array<Sketchup::Entity>, Sketchup::Entities,
  #   Sketchup::Selection]
  #
  # @return [Array<Serializable>]
  def self.from_entities(entities)
    entities.map { |e| from_entity(e) }.compact
  end

  # Create serializable object from entity, or return nil if entity doesn't
  # represent a serializable element.
  #
  # When called on a subclass of Serializable, only objects of that class are
  # returned.
  #
  # @param entity [Sketchup::Entity]
  #
  # @return [Serializable, nil]
  def self.from_entity(entity)
    return unless entity.is_a?(Sketchup::Group)
    class_name = entity.get_attribute(DICT_NAME, ATTR_CLASS_NAME)
    return unless class_name
    return unless Serializable.constants.include?(class_name.to_sym)
    klass = Serializable.const_get(class_name.to_sym)
    return unless klass <= self

    klass.new(entity)
  end

  # Create serialsable object represented by selected entities.
  #
  # When called on a subclass of Serializable, only objects of that class are
  # returned.
  #
  # @return [Array<Serializable>]
  def self.from_selection
    from_entities(Sketchup.active_model.selection)
  end

  # Create serializable represented by selected entities, or active
  # entities (those in the active drawing context) if selection is empty.
  # Can be used for exporters that export selection only if there is one, or
  # otherwise fall back on all active entities.
  #
  # When called on a subclass of Serializable, only objects of that class are
  # returned.
  #
  # @return [Array<Serializable>]
  def self.from_selection_or_active
    Sketchup.active_model.selection.empty? ? from_active : from_selection
  end

  # Check if entity represents a Serializable.
  #
  # When called on a subclass of Serializable, it is checked if entity
  # represents an instance of that class.
  #
  # @param entity [Sketchup::Entity]
  #
  # @return [Boolean]
  def self.represented_by?(entity)
    return false unless entity.is_a?(Sketchup::Group)
    class_name = entity.get_attribute(DICT_NAME, ATTR_CLASS_NAME)
    return false unless class_name
    return false unless self.constants.include?(class_name.to_sym)
    klass = self.const_get(class_name.to_sym) 
    return false unless klass <= self

    true
  end

With this you can use Stud.from_selection to get all selected studs. Or FrameElement.from_active to get all Studs, Joists or whatever FrameElements there might be in the active drawing context. Or Joist.represented_by?(entity) to test if an entity represents a joist.

Having short and simple method calls for these things allow for more elegant and easy to work with code all over the extension.


#4

Hey Christina,

Thank you for the insight. Makes a lot of sense. I knew it seemed weird to try and sub-class a SketchUp entity. I love your idea of creating a superclass for all things that will get drawn in SketchUp. There are definitely common attributes and methods that can apply to all. I’ll try to think in that context some more.


#5

You are correct. SketchUp API classes are not “true” Ruby objects, but instead thinly wrapped C++ objects. The API Entity classes have no ::new() constructor method like normal Ruby classes.

Instead their creation (on the C++ side) is exposed to Ruby via “factory methods” of the entity collection classes (ie Entities, Layers, Pages, Materials, etc.) So because they are instantiated from a “factory method” of the “owning” collection, their is no normal means of passing down initialization behavior from ancestor to descendant classes (ie, inheritance,) as their is for normal / real Ruby classes.

Again, correct. Because of how the SketchUp API are exposed to Ruby, you need to create what is called a “wrapper class”. In effect you are “wrapping” the use of an API class instance inside the instance of your custom class. This means that your custom class’ initialize() method will create the API object via a factory method and assign an instance variable (@var) to reference it. (Hence the Examples above by Christina.)

There are some issues, however. Some geometric editing operations can divide objects into new objects leaving your wrapped reference pointing at a DeletedEntity. So you need to often test your reference with the #valid?() method.


Your alternative is to use “plain Jane” API objects and store your attributes in AttributeDictionary collections.


#6

An alternative to inheritance (subclassing) is composition or interfaces.

Sometimes it is not practical to inherit from a parent class. Either because the objects are related to each other but still so different that there is not much code to share. Or there is a conflict of what should be the highest parent class. (Or as Dan says, for SketchUp’s native classes which are not designed for subclassing.)

Sometimes you later discover you did a bad choice for the parent class and then its hard to unscramble the inheritence mess again.

A composed class holds references to one (or more) instance of the other class and calls methods on it (like a proxy) without directly modifying their behavior (e.g. through overriding). You can add extra methods or hide (not implement) methods that are not relevant for your class. This is usually cleaner, reduces complexity and avoids conflicts with other developers’ code because nobody else uses your class.

Interfaces don’t really exist in Ruby but are only interesting for the concept. It’s a contract of what methods a class must implement without imposing a specific parent class. (Usually behaviors or Serializable are interfaces that are added to an existing class hierarchy where the root class cannot be changed anymore)


#7

You are right that interfaces don’t exist as a formal construct in Ruby. They can’t because Ruby is weakly typed (or perhaps untyped); there are no static declarations of what an Object is or can do. In fact, Ruby code can modify the methods any Object provides at any time during a run. A formal contract is impossible under these circumstances!

However, the informal interface (or protocol) concept of “duck typing” applies in Ruby: if an Object provides the required methods to act like a Duck, it can be treated as a Duck without any necessary inheritance, collaboration, reference or other relationship to something formally known as “Duck”.

There are multiple ways an Object could come to provide the Duck methods. It could inherit them from a parent class that provides defaults (this assures that none are missing, which eases the burden on clients because they don’t have to probe to verify what is or isn’t provided). It could mix them in from some Module and thus avoid inheritance. Usually mixins themselves expect the receiving class to implement some protocol to let the mixin methods work, for example comparison operations are needed for some enumeration methods such as sorting and selecting. Or the Object’s class could simply implement the required methods directly.

SketchUp’s Ruby API Observers are an example of inheriting an interface. The various specific Classes of Observers make sure that derived Classes provide the methods essential to standing in that role, though most of the defaults do nothing. Derived Observer classes override selected methods and accept the defaults for others depending on what they want to do.

The SketchUp Tool is a good example of a non-inheritance-based interface, and the fact that the SketchUp Ruby API declares a Tool Class is misleading. To be a Tool, an object doesn’t inherit from the Tool Class, it just needs to implement some subset of the callback methods described in the Tool interface. The active model can then be told to select the object to activate it, i.e. to start invoking the callback methods from the Tool interface. The SketchUp engine probes the Object to see which Tool methods it provides, invokes those when the corresponding GUI events occur, and ignores any that are missing. The engine doesn’t care about methods that aren’t part of the Tool callback protocol.


#8

We need to be clear that since the observer overhaul (for the 2016 version) most of the API observer classes are now just empty abstract classes. Ie, the empty callback (interface) methods were removed so that the SketchUp engine could duck-type observer objects to know which callbacks were implemented.

Example …

Sketchup::ModelObserver.instance_methods(false)
#=> []

Previous to this subclasses would inherit empty callback methods and these empty methods would get called all the time.


#9

Dan, thank you for reaffirming my thoughts. I appreciate you reminding me that the Ruby API is is a sort of “interface” to the C++ core of SketchUp, so things aren’t always going to make sense in a “Ruby context.” A lot of the following conversation went over my head a bit, but it was good to read, regardless.


#10

Aerilius, I think I may have done something like you’ve described. If I understand correctly, it would be like instead of referring to a specific SketchUp API class directly, I would wrap it inside of my own “parent” class, and refer to my parent class throughout my code wherever I needed to access the associated SketchUp class. This way, if the API changes in the future and there is a better implementation of a specific function, I can simply update my parent class instead of searching through all my code and updating the direct references to the deprecated class.


#11

A wrapper or proxy class doesn’t need a parent or doesn’t need to be used as parent class. (I’m not sure if you used “parent” with the meaning in an inheritence context.)
But in any case, if you every want feedback on some code, you can reduce it to a minimal example and post it in the forum for review.


#12

Yeah I appreciate that. So far, I’ve been able to figure things out. Sometimes I wonder if I’m reinventing the wheel with some of the classes and functions I build. It would be interesting to get some feedback to see if there’s an easier way.