Storing Custom Classes within a Model

attribute
attributedictionary

#1

Hello! I was curious if a .SKP file is able to store data on Custom Classes, so that my extension can access that data later?

In particular, I’m trying to create an extension that analyzes Face data for the purposes of estimating construction materials. Each face is processed and several basic rectangle “Panels” (my custom class) are created based on the face geometry. For every “Panel”, a corresponding Group with basic Edges is drawn in the model, and the Panel is attributed accordingly with that new group.

I’m developing a Tool as well, where a user picks a Group made by the Panel class, and ultimately the Panel class data is sent to an HTML dialog for visual interaction. I’ve tried using an AttributeDictionary to attribute the Panel class to the Group it created, but it appears that the AttributeDictionary can only store certain types of data (not my custom Panel class).

Is this a good approach for what I’m trying to do, or should I be doing something fundamentally different? (i.e. appying basic Attributes to my group, rather than going through my own Panel class)

Thanks!


#2

I think that AttributeDictionaries are an appropriate attack, but you must work within their capabilities. Because they are saved with the file not in a continuously running interpreter, you should think in terms of “serializing” (aka “marshaling”) your objects to and from an external representation and putting that serialized content into the AttributeDictionary. Take a look at the Ruby standard library’s Marshal module.


#3

Note that Ruby’s Marshal is version-sensitive: Newer versions of Marshal (= newer Ruby versions) can read data that was marshalled in older versions, but not vice versa. If a plugin user of SketchUp 2016 receives file of a colleague who used your plugin in SketchUp 2017, Marshal cannot read the class.

An alternative is JSON, which is versionless. You would implement in your class a method #to_json and .json_create which is given a JSON object and instantiates your class and sets the class’s state from the JSON attributes.

You can use Attribute Inspector which is coincidentially going to have JSON support in its next version.


#4

@Aerilius raises a good point. Because the output from Marshal is binary, an older version of Ruby may not be able to read it.

Yet another alternative is YAML (also in the standard Ruby library). Both it and JSON require you to do some work to specify what properties will be stored and to reconstitute your objects from the stored data.


#5

Steve & Aerilius - thanks for the prompt responses. I’ve gotta say this is new territory for me, externalizing data like this, but necessary for the task at hand. I’ll look into your suggestions and I’ll post if I come up with a working solution.

Thanks again.


#6

I would save the instance variables of the class as attributes and have a special factory method to later re-initialize the object from the element holding the attributes. You may want to later rename the class or make other changes to it, so it could be a good idea to try to somewhat de-couple it from the data in the model.


#7

That is a good point, a problem with Marshalling (or similar mechanisms like pickle in Python) is that the stored data is bound to the specific version of your class. But the data (model) has a longer lifetime then the software (your current plugin version) and should not depend on a specific software version. While it is more effort you have more control if you implement saving & restoring on your own (be it a factory method that reads individual attributes or JSON to restore an instance).


#8

I suppose the brick wall I run into is that my Panel class has attributes that reference other classes, rather than the basic data types that an AttributeDictionary can store. i.e. Panel.face_ref => Sketchup::Face:0x000... or Panel.panelgroup => PanelEst::PanelGroup:0x000...

Does needing these data types force me to use the Marshal way, or is it possible to use JSON to deal with these? Also (excuse the dumbness of the question), would I use the JSON techniques via my HTML dialog?


#9

I don’t think Marshal can serialize a reference to a face in the model. You probably need to store its PID as a integer and later query the model for an entity by that PID.

I’m not sure what you mean by via your HTML dialog.


#10

@eneroth3 makes a valid point. An Entity in the Ruby API is a bridge to an internal data structure of the non-Ruby SketchUp engine. Ruby can’t possibly serialize or store such a non-Ruby object because it has no way to fetch it from the engine. Said another way, a script can only see the Ruby representation of an Entity passed to it by the Ruby API, not the underlying C data.


#11

Currently, I’m quite unfamiliar with JSON, so I’m not sure how how one would even execute a JSON script from a Sketchup Ruby extension. I was assuming that JSON would work alongside Javascript & HTML, and that the way I’d access that is through an HtmlDialog object in my Ruby script…

Alas, I’ve got some way to go in wrapping my head around some of these fundamental concepts in app development… It makes a bit more sense now, so I appreciate the explanation.

Going forward, I’ll rethink what kind of attribute data I really need and their purpose. i.e. the Sketchup face that I want to link my Panel object to is only needed for its geometry, which I can dumb down to an array of Point3d arrays and a Vector 3d array, which I believe can work with an AttributeDictionary, and reconstitute them with @eneroth3’s factory concept. If I’m able to keep all the data within AttributeDictionary’s, then hopefully I can avoid fumbling my way through externalizing data, and just keep it all within the .skp file.


#12

JSON is not scrting but a way of serializing data. By default it supports integers, floats, strings and booleans. Maybe some kind of nil/null/undefined too, I’m not sure.


#13

The Ruby reference to nil is translated to the string "NULL" in the JSON dataset by the Ruby JSON library.

CORRECTED by Aerilius BELOW


#14

How would you store your data in a database? Exactly, tables and records. How do you store references between objects in a table? By keeping an id to the reference as a value in the referrer.
So you have to find a way to decouple your direct references in your business object. I’d suggest making a hard distinction between your data-models and your business-models. data-models have just plain valuetypes as properties, preferably also parameter-less constructors etc, while your business-models can have real references to other objects.

Also, have a look at the repository pattern: https://8thlight.com/blog/mike-ebert/2013/03/23/the-repository-pattern.html
Except, you don’t have a database as a repository, but an AttributeDictionary. But, also with AttributeDictionary you can create an implementation of the repository described in the url. But this url should help you to decouple things.

If I have some time I would like to come with a good ruby example.


#15

Null is part of the JSON specification. However, keys can only be strings, not objects.
JSON.generate({nil => nil}) # '{"": null}'


#16

To store data, inside of Sketchup, or elsewhere, a few steps have to be taken. First, you should be aware of the difference between your business objects that have all the functionality and references to other objects, and their purest state representation, which is what you want to make persistent. Now, the example I am going to write here is far from complete, as it lacks business objects, but it will give you an example of how to make data persistent in sketchup. In this case, by storing ruby Hash objects.

First, a generic repository is created to handle basic CRUD operations against a store. In this case, the store must be a key-value-store. Since it is generic, all dependencies should be injected. The repository should not be aware that it is running inside of sketchup, it should not be aware what kind of key-value-store it uses (AttributeDictionary, or a regular ruby Hash, or…), it should not be aware which key is used as an ID and it should not be aware of how it is serialized exactly.


module Developer
  module Product

    # this class should not be aware that it is running inside of SketchUp
    # inject dependencies
    class GenericHashRepositoryKeyValueStore

      def initialize(id_key, key_value_store, key_value_store_id_provider, hash_serializer)
        # TODO: check if passed dependencies implement the needed methods
        @id_key = id_key
        @key_value_store = key_value_store
        @key_value_store_id_provider = key_value_store_id_provider
        @hash_serializer = hash_serializer
      end #def

      def add(hash)
        # get a new id for thekey value store we have here
        id = @key_value_store_id_provider.next_id(@key_value_store)
        hash[@id_key] = id
        # serialize it
        serialized = @hash_serializer.serialize(hash)
        # and store it
        @key_value_store[id] = serialized
        # and finally, return it
        return hash
      end #def

      def get(id)
        # get the entry at the given key
        serialized = @key_value_store[id]
        return false if not serialized
        # deserialize it
        hash = @hash_serializer.deserialize(serialized)
        return hash
      end #def

      def all()
        hashes = []
        @key_value_store.values do |serialized|
          hash = @hash_serializer.deserialize(serialized)
          hashes << hash
        end #def
        return hashes
      end #def

      def update(hash)
        # get the id
        id = hash[@id_key]
        # check if the given id exists, if not return false
        return false if not @key_value_store[id]
        # serialize
        serialized = @hash_serializer.serialize(hash)
        # store
        @key_value_store[id] = serialized
        return true
      end #def

      def delete(id)
        # check if the given id exists, if not return false
        return false if not @key_value_store[id]
        # delete the entry
        value = @key_value_store.delete(id)
        return value ? true : false
      end #def


    end #class

  end #module
end #module

Because dependencies are injected, different implementations can be interchanged. for example, the serializer could be a JSON serializer, or YAML, or XML, or binary, or whatever you like… I have created 2 small implementations:

require 'json'

module Developer
  module Product

    class HashSerializerJson

      def serialize(hash)
        return JSON.dump(hash)
      end #def

      def deserialize(json)
        return JSON.parse(json)
      end #def

    end #class

  end #module
end #module

and

require 'yaml'

module Developer
  module Product

    class HashSerializerYaml

      def serialize(hash)
        return YAML::dump(hash)
      end #def

      def deserialize(yaml)
        return YAML::load(yaml)
      end #def

    end #class

  end #module
end #module

Because I don’t know if I want the ID’s to be numbers, or strings, or guids, or colors, or… I have made the id provider also an injected dependecy. For now I only have created the implementation that gives a new number as an id. The id provider also has to store its state in the key-value-store, for that reason, the key where the state is stored is also injected.

module Developer
  module Product

    class KeyValueStoreIdProviderIncrementalNumber

      def initialize(key)
        @key = key      
      end #def

      def next_id(key_value_store)
        # get the last id
        last_id = key_value_store[@key] || 0
        # get next
        next_id = last_id + 1
        # store it
        key_value_store[@key] = next_id
        # return nex id
        return next_id
      end #def

    end #class

  end #module
end #module

Finally, the key-value-store itself is free to select. Only want to make a repository in-memory? Fine, pass a regular Hash to it and it will work. Want to make it persistent inside an AttributeDictionary? Fine, you can pass an AttributeDictionary as well, except, it lacks a deletemethod which a regular Hash has. For that reason, I made a wrapper:


require 'sketchup'

module Developer
  module Product

    class KeyValueStoreSketchupAttributeDictionaryBased

      def initialize(attribute_dictionary)
        # TODO: check if passed dependencies implement the needed methods, or, in this case, I'd check if it truely is an AttributeDictionary
        @attribute_dictionary = attribute_dictionary
      end #def

      def delete(key)
        return @attribute_dictionary.delete_key(key)
      end #def

      def method_missing(method, *args)
        if @attribute_dictionary.respond_to?(method)
          @attribute_dictionary.send(method, *args)
        else
          super
        end
      end

    end #class

  end #module
end #module

With all these classes, not aware of what their dependencies are, one can create multiple, slightly different implementations. For example:


# create a repository that:
# - stores hashes in a Hash
# - serializes them to json
# - uses a Number as a key
# - uses "id" to store the id

# create dependencies first
serializer = Developer::Product::HashSerializerJson.new()
id_provider_key = "LAST_ID"
id_provider = Developer::Product::KeyValueStoreIdProviderIncrementalNumber.new(id_provider_key)
store = {} # simple hash for now
id_key = "id"

# now the repository
repository = Developer::Product::GenericHashRepositoryKeyValueStore.new(id_key, store, id_provider, serializer)

Another implementation:


# create a repository that:
# - stores hashes in an AttributeDictionary
# - serializes them to yaml
# - uses a Number as a key
# - uses "the-cool-id" to store the id

# create dependencies first
serializer = Developer::Product::HashSerializerYaml.new()
id_provider_key = "LAST_ID"
id_provider = Developer::Product::KeyValueStoreIdProviderIncrementalNumber.new(id_provider_key)
attribute_dictionary = Sketchup.active_model.attribute_dictionary('name', true)
store = Developer::Product::KeyValueStoreSketchupAttributeDictionaryBased.new(attribute_dictionary)
id_key = "the-cool-id"

# now the repository
repository = Developer::Product::GenericHashRepositoryKeyValueStore.new(id_key, store, id_provider, serializer)

Regardles of the implementations, both example repositories` input and output should be the same (except for the id key). to use the repository:

# add a new entry
entry= repository.add({"name" => "Kenny", "country" => "Belgium"})

# get an existing entry
entry = repository.get(id)

# update an entry
entry["country"] = "Scotland"
repository.update(entry)

# delete an entry
repository.delete(id)

This code is partially written during the creation of this post. I’m sure there will be typos. But it should give you an idea of how to make data persistent and at the same time an idea of dependecy injection.

The next step should be to create Controllers, that create business objects based on one or mulitple Hash repositories. But, that is for a next time I have some spare time…
In the meantime, this code is also on github: https://github.com/kengey/sketchup-dev-tools-and-examples


#17

Wow, thanks for the sample code and your thoroughness in explaining the concepts behind it. It’ll take me a while to digest it and actually use it, but I think it could benefit the many who are unsure of how to approach data collections.