Ruby scene serialization

Hi guys, i am very new on sketchup and ruby , i have worked with java and c# but this is the first time with ruby.
Now i have one problem , i need to serialize all scene in one json (scene hierarchy and object name , object material and position of single object) how can i do ? i have done this but something tell me that is wrong

def main()

avr_entities = Sketchup.active_model.active_entities; # all objects

dictionary = {};
list = Array.new();


avr_entities.each { |root|
 
    if root.is_a?(Sketchup::Group)
    if root.name == ""
      UI.messagebox("this is a group #{root.definition.name}")
    else
      UI.messagebox("this is a leaf #{root.name}")
    end
  
    if root.entities.count > 0
      root.entities.each { |leaf|
        if leaf.is_a?(Sketchup::Group)
        UI.messagebox("#{leaf.name}")
        end            
      }
    end
  end
}

end

What tells you that there is something wrong? At the moment this code has little to do with serialization but rather with tree traversal.

  • Do you have a specific question about traversing the SketchUp scene graph?
  • What is your use case, do you want to export all types of entities or just specific ones (here you only consider groups)?

So far I can only give some remarks:

  • Puts triple backticks ``` on the empty lines before and after your code in this forum to make the forum format it properly.

  • Use puts instead of UI.messagebox to avoid pain in the fingers and mouse abrasion.

  • You do not need to construct an array with Array.new(), just write [].

  • Semicolons are not wrong, but the prefered style is not to use them.

  • The name says nothing about whether an entity is a leaf. The term “leaf” is actually means a node that has no children, so for example a (non-empty) group would never be a leaf.

  • You can iterate entities.each{…} without first checking how many elements it has. If it is empty, the iteration will finish immediately. Also prefer length or size in Ruby, because count is often slower (linear complexity).

  • You can require "json" from the standard library provided by SketchUp and then serialize Ruby Hash and Array objects to JSON.

  • Conceptionally, tree traversal works like this (here recursive depth-search, pre-order):

def traverse(node)
  puts "Visiting node #{node}"
  node.children.each{ |child|
    traverse(child)
  }
end

def main
  # Start the traversal.
  traverse(root)
end

Transfering this to the SketchUp API, a node is either the model (root) or a group or a component instance. We use a Sketchup::Entities collection to access children.

  • A special complexity is that a component definition can have many instances, that means several nodes in the tree are based on the same definition (so this primitive approach would visit the same definition multiple times). Alternatively, one can traverse the definitions list.
  • Another specialty is that coordinates refer to local coordinate systems (defined by a Geom::Transformation of the parent group or component instance).
def traverse(entity, transformation = IDENTITY)
  # Here, do something with this entity. Check its type when calling methods that not all entities have.
  # To convert points to global coordinates, transform them with `point.transform(transformation)`
  puts("Visiting node #{entity.inspect}")
  
  # If this node can have children, traverse its children.
  if entity.is_a?(Sketchup::Model)
    entity.entities.each{ |child_entity|
      traverse(child_entity, transformation)
    }
  elsif entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance)
    # Multiply the outer coordinate system with the group's/component's local coordinate system.
    transformation *= entity.transformation
    entity.definition.entities.each{ |child_entity|
      traverse(child_entity, transformation.clone)
    }
  end
end

def main
  # Start the traversal.
  traverse(Sketchup.active_model)
end

As an exercise, you can implement a nicer, more reusable variation of the above example, where traverse takes a block &visitor as last argument and calls it in place of puts("Visiting node..."). Another argument passed to the block can be the entity’s instance path.

traverse(Sketchup.active_model){ |entity, transformation, instance_path| 
  puts("Visiting node #{entity.inspect} at #{instance_path}")
}
3 Likes

Thank you for your reply , yesterday i have re-write my class and i write the json with the same hierachy on sketchup , but your method is better :wink: , you really clear my ideas on ruby and sketchup . can i ask other question in future ? :slight_smile: thank you for your time man

Don’t you want to copy the transformation for each instance here? Otherwise you accumulate the transformation for an instance’s siblings as well.

Well spotted! Ruby is not pass-by-value.
But the Transformation#*= method does not modify the receiver, it returns a new transformation? (So we are allowed to do t1 *= t2 short for t1 = t1 * t2?)

In any case, whatever code is inserted in place of “Visiting node”, it may not only read but also modify the given transformation, so it is better to copy it for every method invocation.

That it does, but you are reassigning it to the same `transformation` variable within your loop over the entities. So after processing the first instance then `transformation` will now accumulate the transformation for the next instance as well. You want to preserve the original `transformation` passed into the method - and combine the sub-instance's transformation into a new temp variable:
    # Multiply the outer coordinate system with the group's/component's local coordinate system.
    instance_tr = transformation * entity.transformation
    entity.definition.entities.each{ |child_entity|
      traverse(child_entity, instance_tr)
    }

Edit: Never mind. I read your method wrong. I didn’t realize that traverse processed only one entity, then dug into it’s sub entities. I’m used to these loops taking a collection of entities and iterating that.

The clone you added isn’t needed. Sorry for the confusion.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.