Explode and Re-Group/Component objects imported from a 3D STEP (.stp) file

Hi,

I am a newbie to SketchUp development. I am using SketchUP for some time, and have many years of programming experience in multiple languages.

I got a complex 3D model as a .stp file (STEP) with thousands of objects. I have used the SimLab Step Importer extension to import them into SketchUp. The model comes with about 500 groups in a hierarchy each has 10-20 components of geometry objects. The issue is the axis of the model, but also each of the thousands of objects is off, and that causes each of these objects to be rounded by a boxer in a weird direction, that doesn’t change, unless the object is exploded and regroup. Doing it manually is almost impossible due to the numbers. I was looking to write a Ruby code to do that.

I found a skeleton, which I modified to run recursively over the entire hierarchy of a selected object, but when it comes to creating the new group I am confused.

The basic concept is:

  1. Run down into each of the groups/components until you get to one that is just geometry.
  2. Move back to the parent and explode.
  3. Get the exploded children and regroup them into the parent-parent. I don’t need the components as they are all 1 instance components.

My issue is that entity doesn’t have add_group, only entities.

Anyone can help? To focus my issue, it’s in 3 lines of explode_deeply. Thanks in advance, Shai :pray:

require 'Sketchup'

module REGROUP
	
  def self.container_type(entity)
	result = 0
	if(entity.is_a?(Sketchup::Group))
	  result = 3
	elsif (entity.is_a?(Sketchup::ComponentInstance))
	  result = 2
	end
	if (result == 2)
	  definitionA = entity.definition
	  number = definitionA.count_used_instances
	  if (number > 1)
		result = 1
	  end
	end
	return result
  end

  def self.is_geometry(entity)
	  return entity.is_a?(Sketchup::Edge) || 
		  entity.is_a?(Sketchup::Face)
  end

  def self.get_children(entity)
	  result = nil
	  if (entity.is_a?(Sketchup::Group))
		  result = entity.entities
	  elsif (entity.is_a?(Sketchup::ComponentInstance))
		  result = entity.definition.entities
	  end
	  return result
  end

  def self.explode_deeply(entity)
	result = !(is_geometry(entity))
	if (result)
	  children = get_children(entity)
	  if (children)
		children.each do |child|
		  if explode_deeply(child)
			nameA = child.name
			type = container_type(child)
			if (type >= 2) # if type is component with 1 instance of group
##!!!!!  Here is teh code which is uncorrect, that explode and add to new group of the parent 
			  members = child.explode
			  group = entity.add_group(members)
			  group.name = nameA
##!!!!!
			end
		  end
		end
	  end
	end
	return result
  end
	
  def self.regroup_selected_hierarchy
	puts "Regroup Start1"
	mod = Sketchup.active_model
	sel = mod.selection
	unless sel.empty?
	  sel.each do |entity|
		puts "Testing: #{entity.name}"
		if (container_type(entity) > 0)
		  result = explode_deeply(entity)
		end
	  end
	  mod.commit_operation
	end
	puts "Regroup End1"
  end	
end

SKETCHUP_CONSOLE.show
SKETCHUP_CONSOLE.clear

date_a = DateTime.now() 
puts "Regroup Start: #{date_a}"
REGROUP.regroup_selected_hierarchy()
puts "Regroup End"

You still need to do things the Ruby way, rather than say the JavaScript or Python way.

A few thoughts …

(1) In Ruby, constants are ALL_UPPERCASE.
Module and Class identifiers are CamelCase (aka SnakeCase.)
Method identifiers and Reference names are lower_case_divided_by_underscore.

(2) All of your extensions should be wrapped within a single toplevel author or company namespace module. So …

module REGROUP

… should be …

module ShaisNamepace # <--<<< Change to suit your needs
  module Regroup

    # code here

  end
end

(3) Ruby uses 2 space indentation. Set your editor to replace tabs with 2 spaces so that code is indented properly when pasted into the forum.

(4) Encode your Ruby files as UTF-8 and insert a magic comment as the first line of each .rb file …

# encoding: UTF-8

(5) For best results (especially copy and pasting into forum or SketchUp’s Ruby Console,) … use Windows style Line endings (CR+LF).

(6) Ruby’s main paradigm is “readability”. Do not use frivolous parenthesis around conditional expressions. JavaScript and C requires it, Ruby does not.

if result == 2

not

if (result == 2)

Only use parenthesis if there are multiple terms and you must control the order of evaluation.

(7) Use modifier position for one-line conditional blocks to reduce lines and improve readability …

      result = 1 if number > 1

not

      if number > 1
        result = 1
      end

(8) The return reserved-keyword is not actually required as methods in Ruby always return the value of the last expression (or statement) evaluated.

    def container_type(entity)
      if entity.is_a?(Sketchup::Group)
        3
      elsif entity.is_a?(Sketchup::ComponentInstance)
        number = entity.definition.count_used_instances
        number > 1 ? 1 : 2
      else
        0
      end
    end

In the above example, the last statement evaluated is the if statement which returns the result of it’s evaluation, so the method will return the result of the if statement. There is no need to short-circuit the if statement using return statements.
The above would also be the same as …

    def container_type(entity)
      return if entity.is_a?(Sketchup::Group)
        3
      elsif entity.is_a?(Sketchup::ComponentInstance)
        number = entity.definition.count_used_instances
        number > 1 ? 1 : 2
      else
        0
      end
    end

Which I’m sure we’d all agree is a frivolous use of a return ?

(9) You can avoid the “self.” method qualification inside your module, by extending it with itself using extend. Ie (removing the unneeded number = reference assignment) …

module ShaisNamepace # <--<<< Change to suit your needs
  module Regroup

    extend self

    def container_type(entity)
      if entity.is_a?(Sketchup::Group)
        3
      elsif entity.is_a?(Sketchup::ComponentInstance)
        entity.definition.count_used_instances > 1 ? 1 : 2
      else
        0
      end
    end

    # more code here .. etc.

  end
end

You might not (looking at the container_type method) realize that class Sketchup::Group is just a special subkind of ComponentInstance, so group’s also must have a ComponentDefinition that “owns” the entities collection.
This means that a group also has a #definition getter method.
But the API also defined a short-cut #entities method for group’s to get their definition’s entities collection.
For a group only …

group.entities == group.definition.entities

What issue are you having with the model’s axis ?

How are they “off” ? Coordinates ? or are they rotated away from the model axis ?

What does “rounded” mean ? We usually use this word with respect to decimal precision of coordinates.

What does “boxer” mean ? Do you mean the instance’s BoundingBox ?


You might need to post annotated images to explain what you mean if you do not yet know SketchUp “buzzwords” or terminology.

I’m not really sure what you want as regards reorganising the groups and components. This is what I think you mean. Take a hierarchy like this:

before

and make it into this:

after

With the bounding boxes reset by exploding.

    def explode_deeply(entity)
      result = !(is_geometry(entity))
      if result
        children = get_children(entity)
        if children
          children.each do |child|
            if explode_deeply(child)
              nameA = child.name
              type = container_type(child)
              if (type >= 2)
                # Create new empty group in child's parent's parent
                group = entity.parent.entities.add_group
                
                # Add instance of child to its parent's parent
                inst = group.entities.add_instance(child.definition,
                  entity.transformation * child.transformation)
                
                # Explode instance so it's just the geometry 
                # inside new group with reset axes
                inst.explode
                
                child.erase!
                group.name = nameA
              end
            end
          end
        end
      end #if result
      result
    end #explode_deeply

I haven’t edited everything, you still need to do what Dan said. I’ve just made it work the way I think you wanted

Edit: get version 2, I had multiplied the transformations in the wrong order.
Shais2.rb (2.7 KB)

1 Like

Thank you Dan for all your comments. It was very helpful, and I believe I implemented all of them.

I apologize for my poor terminology. You are correct, I meant the instance’s BoundingBox, but also the components axis which is aligned to the axis the entire import was created, that is out of my control. By exploding and recreated the group/component they are both defined aligned with the global axis. Here is a small example I have done manually:

  1. The original
  2. And after recreating the first 2 components (3DGeom-1 & 3DGeom-2)

I understand that I would need to copy some of the other attributes, such as the material from the old to the new. As a matter of fact, in my case, I probably will not need to recreate components, but rather groups, as these components are all single instance and are just using as a frame to the underlined geometry.

so to clear to McGordon question, I am trying to leave the hierarchy as it is, just explode and create groups that are aligned with the global axis.

Perhaps my code needs to be:

def explode_deeply(entity)
  result = !is_geometry(entity)
  if !is_geometry(entity)
	children = get_children(entity)
	if children
	  children.each do |child|
		if explode_deeply(child)
		  name_a = child.name
		  type = container_type(child)
		  if type >= 2 # if type is component with 1 instance of group
			group = child.parent.entities.add_group
			group.name = name_a
			members = child.explode
			members.each do |member|
			  status = group.add_observer member
			end
		  end
		end
	  end
	end
	true
  else
	false
  end
end

Thank you McGodon for your reply. No, I would like to meet the hierarchy as is, just to explode and re-create the groups / components to have them aligned with the global axis. I hope my modified code, I placed above in my reply to Dan is correct now.

Try something like this to avoid excessive nesting of if blocks…

def explode_deeply(entity)
  return false if is_geometry(entity)
  children = get_children(entity)
  return true unless children && !children.empty?
  children.each do |child|
    next unless explode_deeply(child)
    # if type is component with 1 instance of group
    next if container_type(child) < 2
    # if type is component with 1 instance of group
    group = child.parent.entities.add_group(child)
    group.name = child.name
    group.material = child.material if child.material
    members = child.explode
    members.each do |member|
      # !! member is not an observer object / status is unused !!
      status = group.add_observer( member )
    end
  end
  true
end

Thanks again Dan. Just a few more comments if I may:

  1. When executing this snippet, there is an error on “empty?” at the second return in the snippet (line 36 in my code), which I can’t figure out:
    "undefined method empty?’ for #Sketchup::Entities:0x007ffae9daa160 (Line 36)"`
  2. Your example adds the same child into a new group, when my goal is to explode the group/component, in order to lose the wrong axis and the wrong BoundingBox.
  3. You say “member is not an observer object”, so how can I add these exploded members to the new group?

Here is Attached is my code, which works, but add empty groups: regroup.rb (1.8 KB)

Thank you again :pray:

@DanRathbun, I got to a dead-end either way I go. I have a feeling I am asking the wrong questions.

Perhaps what I need to do is to align each component axis with the global one, and not try to rebuild the hierarchy. But I learn this is also no simple. From looking at many posts, I understand @TIG is a master with much experience in this area. I hope @TIG would be able to help here.

The error message …
"undefined method 'empty?' for #Sketchup::Entities:0x007ffae9daa160 (Line 36)"
means exactly what it says.
I made an incorrect assumption that children would be an array and so then respond to an #empty? method call.
It isn’t an array, and the API writers did not define an #empty? instance method for the Sketchup::Entities collection class.

So this method call needs to be changed to a more brute force method call and comparison …
children.size == 0 (instead of) children.empty?
children.size > 0 (instead of) !children.empty?

Ie …

  return true unless children && children.size > 0

Another trick is to just test the first item of an entities collection for nilness which will be nil if the collection is empty. Ie …

children[0].nil? # true if collection is empty

Or for a conditional (because in Ruby only nil and false evaluate falsely)…

  if children[0]
    # only do if collection is not empty
  end

Never mind what the example did. I haven’t really looked deep into what you are trying to do.

I was really attempting to show how to write easy to read code without the frivolous nesting of conditional blocks. (Meaning … when it is okay to shortcircuit using return and next keywords.)

You need to read the documentation for the methods before you try to use them.
The docs for all the various #add_observer methods clearly state that they take an observer object as the argument (not an entity). So these methods really “attach” an observer object (given as the passed argument) to “watch” the receiver entity.
There are also complementary #remove_observer methods to detach observers so as to stop “watching” watched entities or collections (or the application itself.)

You can only add exploded entities into another entities context if the loose entities and the new group are in the same context (entities collection.)

Well, my idea was to insert the child group into a new group before exploding the child group. The main reason is that exploding can cause primitive geometry (faces and edges) to merge with existing primitive geometry.

If there is no other primitive geometry, then perhaps something like …

parent = child.parent
material = child.material
members = child.explode
group = parent.entities.add_group(members)
group.material = material unless material.nil?

Another option might be to add a new instance of the child group with a different transformation, where you want it. Then copy materials, attributes etc. Then delete the old group.

Are you still using SketchUp 2018 ?

@eneroth3 Isn’t their an open issue with skewed bounding boxes ?

I would suggest to get in touch with SimLab to see if there are any open issues with their importer extension.

Or rename the file to an ".ifc" extension and try SketchUp’s native IFC importer.

I have a macbook Pro 2010 with High Sierra. Both 2020 and 2019 are crashing right of the gate after opening the model. With 2018, I still have issues sometime with the display, when I move the cursor drawinf or moving tools, and the display doesn’t earse the drawn lines…

The most stable is 2017, but the pro is not supported.

IFC Import fail. It looks like there is no native way to import STEP (.stp) files.

@DanRathbun, thanks for your ideas. Any idea of how to go over each group / component in the hierarchy and set the axis to align with the the global?

Please post the imported SKP file that is crashing.

You might also analyze the .stp file with …

STEP and IFC are quite different beasts…

1 Like

… mea culpa.