Can Entity Info be customized?

Continuing the discussion from Set up new entities to group or create a new entites list, because this post is on another topic than that thread:

No.

No.

Cutlist (and many other types of) plugins use custom Sketchup::AttributeDictionary objects that they attach to any model entity.

Any Sketchup::Entity subclass object within the model, can have multiple attribute dictionaries attached to it. They can be but do not need to be geometric (Sketchup::Drawingelement subclass) objects. You can also attach attribute dictionaries to the model itself, or any of it’s collection class objects (like the Pages, the Entities, the Layers collections, etc.)

But the model is a shared space, so you need to prefix your dictionary name the same as you would for your author namespace module and plugin submodule, but with any scope operator (ie, ::,) replaced with an underscore …

Example:

module YChen
  module SectionPlugin

    DICT_KEY ||= Module.nesting[0].name.gsub('::','_')
    #=> "YChen_SectionPlugin"

    extend self

    # Gets (and creates if needed) this plugin's attribute dictionary for
    # the argument entity object.
    # @param entity [Sketchup::Entity] any API entity subclass object.
    # @return false if argument cannot have attribute dictionaries.
    # @return [Sketchup::AttributeDictionary] attached to argument entity.
    def get_plugin_attr_dict(entity)
      return false unless entity.respond_to?(:attribute_dictionary)
      entity.attribute_dictionary(DICT_KEY,true)
    end

    # more methods and code ...

    def add_data(entity,key,value)
      dict = get_plugin_attr_dict(entity)
      dict[key]= value
    end

    def get_data(entity,key,fallback=nil)
      dict = get_plugin_attr_dict(entity)
      value = dict[key]
      return fallback if value.nil?
      value
    end

  end # this plugin submodule
end # outer namespace module
2 Likes

Does this mean that it’s better to create a hash class?

I created a hash like this and opened another file trying to load require and require_relative building#.rb.

class Building
  attr_reader :building_name, :built_year, :torn_year

  def initialize(building_name, built_year, torn_year)
    @building_name = building_name
    @built_year = built_year
    @torn_year = torn_year
  end

  def ==(other)
    self.class === other and
      other.building_name == @building_name and
      other.built_year == @built_year and
      other.torn_year == torn_year
  end

  alias eql? ==

  def hash
    @author.building_name ^ @built_year.hash ^ torn_year.hash # XOR
  end
  
end


However nothing works, so I tried it on intellij RubyMine. and it just works. is it the plugin, ruby code editor on sketchup, are causing this problem? or maybe I should just pack them all under one file.

require './building' #require_relative 'building'   #require 'building'  #load 'building'

book1 = Building.new 'London Eye', '2000' ,'nil'

puts "#{book1}"

My previous post had little to do with the Hash class.

That is not a “hash class”. It is just a custom class you wrote that contains properties saved into instance variables.

Be careful. In Ruby programming “hash” has 2 meanings. The Hash class, and it’s hash instances (returned by "to_h" and "to_hash" methods,) and a numeric hashcode returned by "hash" methods.

There are code errors in your class.

(1) in the == method last line should be…

      other.torn_year == @torn_year

(2) In the hash method …

2(a) A "to_h", "to_hash" method should return an instance of the Hash class.
A method named "hash" should return an Integer object.

2(b) @author is undefined and so would return nil. And the NilClass does not respond to a method named "building_name".
Error: #<NoMethodError: undefined method `building_name' for nil:NilClass>

2(c) String class objects do not respond to ^ (XOR) method
Error: #<NoMethodError: undefined method `^' for "London Eye":String>

2(d) Integer subclass objects use ^ method as a bitwise XOR.
This requires that any argument on the right of the ^ must be coercible into an Integer subclass.
The singleton instance (nil) of the NilClass cannot be coerced into an Integer (if you pass nil as the 3rd argument) into the Building class’ new constructor method …
Error: #<TypeError: nil can't be coerced into Fixnum>


What are you trying to do with the hash method ?

If building a Hash from the Building instance variables, it should be named “to_h” …

  def to_h
    # Use the class [] constructor from list of key/value pairs:
    Hash::[
      "building_name", @building_name,
      "built_year", @built_year,
      "torn_year", @torn_year 
    ]
  end

If you are needing an numeric hashcode for comparison, the easiest thing to do is to leverage the core Hash class’ #hash method by using the to_h method I posted just above, like so …

  def hash
    # Get a hash of this class' data, then a numeric hashcode:
    to_h.hash
  end

If this information is to be saved into an attribute dictionary and then later retrieved and you want it in a hash data type, … keep in mind that AttributeDictionary class includes Enumerable, which implements a #to_h method that will output the dictionary as a Hash instance.

You can later iterate the hash and copy key/value pairs back into a dictionary …

# h1 is a hash of key/value pairs
dict = some_entity.attribute_dictionary(DICT_KEY)
h1.each_pair { |key,val| dict[key.to_s]= val }

And the hashcode approach can be used to quickly compare 2 attribute dictionaries, …

dict1.to_h.hash == dict2.to_h.hash

As said the class you posted above has errors. I use the plain native Ruby Console myself.
Yes sometimes there can be quirks with the web dialog-based Ruby code editor extensions.

You can split up your code into multiple files for easier maintenance, but be sure to wrap all files in the same author and plugin submodule namesapces. (Do not define custom classes at the top level of Ruby’s ObjectSpace. If you do they’ll propagate into everyone’s else’s namespaces.)

Thank you for you reply. I am trying to build a hash or dictionary attached to selected group. and display them in my plugin just like what they achieved in the cutlist plugin. and later on I will be trying to import or export these new entities to csv file to modify them. \

These is to support animation to manipulate building appearance like a construction scene.

    DICT_KEY ||= Module.nesting[0].name.gsub('::','_')

What does this line actually do? create a dictionary or attach the hash to a sketchup group or entities?

it only set a constants value…

run this from Ruby Console to see the results…

module A
  module B
    module C
      # from inside this nested module
p     nest_tree = Module.nesting # => reverse tree
      # so calling
p     deepest_nest = nest_tree[0 ] # => the first in the array
      # you convert it to a string by asking for the name
p     string_deepest_nest_name = deepest_nest.name
      # and using global sudtitution 
p     usable_string = string_deepest_nest_name.gsub('::','_')
      # => a string that can be used for a local constant
      # we only want to define it once we use the ' ||= ' operator
      DICT_KEY ||= usable_string
    end
  end
end
puts
# to call globally you use
A::B::C::DICT_KEY

john

1 Like

Short answer:
It defines a constant (DICT_KEY) that holds the string "OuterModuleName_InnerModuleName".

I actually had a comment in the code example above that showed the result …

#=> "YChen_SectionPlugin"

Neither. It just defines the constant string you’d use as the dictionary’s name.

And by the way, you can attach attribute dictionary to entity, but not a hash.
The dictionary is a SketchUp API class that comes from C++ core of SketchUp.
A hash is a core Ruby language class that is a memory data structure only.
But they both are a collection class object that holds key/value pairs, so are used the same way.
The main difference is that the attached dictionary is a perpetual object saved with the model file, whereas hashes are only in memory.

See the Sketchup::Entity class for methods used to create and access attribute dictionaries, in addition to the Sketchup::AttributeDictionary and Sketchup::AttributeDictionaries classes.

Thank you Dan.

I wrote a code using attributeDic and it seems to work well enough.

depth = 100
width = 100
model = Sketchup.active_model
entities = model.active_entities
pts = []
pts[0] = [0, 0, 0]
pts[1] = [width, 0, 0]
pts[2] = [width, depth, 0]
pts[3] = [0, depth, 0]

# Add the face to the entities in the model
face = entities.add_face pts


face1 = model.entities[4]

attdict = face1.attribute_dictionary "test_dict", create_if_nil
attrdict["attr_one"] = "one"
attrdict["attr_two"] = "two"

attrdict = model.attribute_dictionaries['test_dict']
values = attrdict.values

Why not use face ?
Assuming a needed object is at a particular position in the entities collection is bound to bite you later.


Please be aware that the attribute dictionaries collections for any entity is a shared space.
This is why I showed you (above) how to create a DICT_KEY that is unique to your plugin.
Using plain simple dictionary names may clash if another extension does the same.

Secondly, the 2nd parameter creates the dictionary if true. In Ruby nil evaluates as FALSE.


A tip for faster programming …

pts = [
  [0, 0, 0] ,
  [width, 0, 0] ,
  [width, depth, 0] ,
  [0, depth, 0]
]

Hi ,Dan.

I am trying to add attirbute dictionary to the group. What I want to achieve is that:

if my_group doesn’t have any attribute_dictionaries, create one named as “Animation_Info”, or if there are attribute_dictionaries there, but not “Animation_Info”, create one named as it. or just puts Already exist.
But the console gives me a wrong message: I suppose that is because the statement “if !variable” just take no argument but i’m giving one. I deleted the second statedment " elsif !my_group.attribute_dictionaries"Animation_Info" then everything works fine.

I think this would prevent me from modifying it later after created the attribute dictionary. I want to use the second statement to modify some attribute, if there another option for me to take?

"Nil result (no result returned or run failed)"
"wrong number of arguments (given 1, expected 0)"
Error: #<TypeError: no implicit conversion of ArgumentError into String>

model = Sketchup.active_model
entities = Sketchup.active_model.entities

all_groups = entities.grep(Sketchup::Group)
my_group = all_groups.find { |g| g.name == "Rome" } # nil if not found
if !my_group.attribute_dictionaries
  puts "This dictionaries is not created"
  create_if_nil = true
  attdict = my_group.attribute_dictionary("Animation_Info",   create_if_nil)
  attdict["att_one"] = "one"
  attdict["att_two"] = "two"
  elsif !my_group.attribute_dictionaries"Animation_Info"
    create_if_nil = true
    attdict = my_group.attribute_dictionary("Animation_Info",   create_if_nil)
    attdict["att_one"] = "1"
    attdict["att_two"] = "2"
  else
    puts "Already exist"
end

Thanks for the tip Dan.

You are overly complicating such simple tasks.

  1. Break your code up into more manageable method definitions.

  2. It does not matter to YOUR plugin if OTEHR plugins also have attribute dictionaries attached to entities. YOUR plugin just ignores them and ONLY uses IT’S dictionaries.

  3. And again, You keep using dictionary keys that are not unique to YOU and YOUR plugin:

module YChen
  module SectionPlugin

    DICT_KEY ||= Module.nesting[0].name.gsub('::','_') << "_Animation_Info"
    #=> Sets DICT_KEY to be: "YChen_SectionPlugin_Animation_Info"

    extend self

    def find_group(name)
      model = Sketchup.active_model
      entities = model.entities
      all_groups = entities.grep(Sketchup::Group)
      all_groups.find { |g| g.name == name } # nil if not found
    end

    def get_dictionary(entity)
      if !entity.attribute_dictionaries || !entity.attribute_dictionaries[DICT_KEY]
        puts "The #{DICT_KEY} dictionary is not created."
        create = true
        entity.attribute_dictionary(DICT_KEY, create)
      else
        puts "The #{DICT_KEY} dictionary already exists."
        entity.attribute_dictionary(DICT_KEY)
      end
    end

    def add_attrs(group_name)
      my_group = find_group(group_name)
      if my_group
        attdict = get_dictionary(my_group)
        attdict["att_one"] = "one"
        attdict["att_two"] = "two"
        attdict["att_three"] = "3"
        attdict["att_four"]  = "4"
      end
    end

    def mark_rome_group
      add_attrs("Rome")
    end

  end # plugin submodule
end # namespace module