Transformations consistency problem

Hi all !

I’m actually struggeling with SU regarding the transormations. I’m writing a scene clean up tool for my company that merge component instances geometries together into a new definition which is instanciated at the same previous instances positions.

Everything runs pretty well except that the final definition has the right pivot point but the geometries aren’t at the right place. Maybe I’m not doing it right but here are my steps :

  1. Here is for exemple a wall top plates set that I need to merge together in a single instance (have a look at the transformations).

  2. First I’m grouping the components instances together into a group and I’m moviving them to the origin (assuming that the definition pivot point will be at the origin when creating the new definition) in the way I want it to be. Then I’m exploding the meta group to keep only the instances at the right place at the model root (see also transformations.

  3. Then I’m targetting my merging code that process the boolean merge operation for each geometry LOD group of the components. The result is quite strange, the pivot point is right where I wanted it to be but hte geometry has moved (inside my merginig code I’m performing transormations but shouldn’t lead to that result and has been check many times on other cases).

  4. So I started to dig into what hapened and using a break point just before the merging operation I was checking the component transformations that was observed at step 2. And I was suprised and confused that the transformation weren’t the same…

The transformations observed at step 4 when analyzing code is consistant with the ones at step 1 (instances at their original positions) but unconsistant with the ones observed at step 2 in Sketchup. Tracking the instances transformation step by step in the debugger leads me observing that the instances transformations never change even if the meta group encapsulating them is moved and exploded.

So some few questions :

  • Does it feels normal to you ?
  • If Sketchup is working that way, what is the go way to perform the merging operation ?

Thanks for reading me so far and I hope someone will have a clue for me 'cause I’m really lost right now :slight_smile:

Please post code correctly, not images of code or the console.

1 Like

@DanRathbun The code is too big and there is many steps. The images are here to also show what happen with visual feedback and console outputs, ie : the code result. I prefered to explain the aproach but fine I’ll post the code below. I’m only posting the merge method, the part described is starting with the comment #Combine the set's item instances :wink:

  def dyn_merge
    #Start undo stack operation
    start_operation("Model Cleaning | Merge Dynamic Items", true)

    #Filter the scope to keep only component instances
    active_instances = active_scope.grep(Sketchup::ComponentInstance)

    #Merge the components for the current scope
    ITM_TO_MERGE.each do |current_tag|
      #Filter the component instances that match the item key
      item_instances = active_instances.select{|ci| ci.definition.name.start_with? "ITM_#{current_tag}"}
      active_instances -= item_instances

      #Retrieve all instance bounding box
      item_bounds = item_instances.collect{|cmp| bbox = cmp.bounds; (0..7).to_a.collect{|i| bbox.corner(i)}}
      item_data = Hash[*item_instances.zip(item_bounds).flatten(1)]

      #Build all sets of item instances connected to each other
      item_sets = []

      until item_data.empty?
        #Start a new item set
        current_item, current_corners = item_data.shift[0]
        item_set = [current_item]
        item_set_i = 0

        #Build the set by parsing each objects store progressively in the set
        until current_item.nil?
          #Parse all other items to check their connection to the current one using their bounding box
          current_bbox = current_item.bounds
          connected_items = []

          item_data.each_pair do |other_item, other_bounds|
            connected_items << other_item if other_bounds.count{|p| current_bbox.contains? p} == 4
          end

          #Store connected item and remove them from the list of items to merge
          connected_items.each{|other_item| item_data.delete(other_item)[0]}
          item_set += connected_items

          #Jump to next item in the set
          item_set_i += 1
          current_item = item_set[item_set_i]
        end

        #Store the item set
        item_sets << item_set unless item_set.length <= 1
      end

      #Combine the set's item instances
      item_sets.each do |current_set|
        #Create a set container
        set_container = Sketchup.active_model.entities.add_group()
        set_container_handler = set_container.entities.add_cpoint([0,0,0])
        set_name = current_set[0].definition.name
        set_container.name = set_name

        #Move the items into the set container
        current_set.each{|itm| set_container.insert(itm, arg_remove_origin:true)}
        # current_set.collect!{|itm| set_container.insert(itm, arg_remove_origin:true)}.flatten!
        set_container_handler.erase!
        current_set = set_container.entities.to_a


        #Compute the set container direction and pivot
        ##Extract compute data
        set_container_bbox = set_container.bounds
        set_container_vector = set_container_bbox.min.vector_to(set_container_bbox.max)
        set_container_length = set_container_vector.to_a.max

        ##Compute as array
        set_container_direction = set_container_vector.to_a.collect{|v| ((v/set_container_length).round(6) == 1 ? 1 : 0)}
        set_container_pivot = set_container_bbox.min.to_a.zip(set_container_bbox.center.to_a).each_with_index.collect do |v_set, v_index|
          if v_index == 2
            v_set[0]
          else
            ( v_set[0] * set_container_direction[v_index] + v_set[1] * (1-set_container_direction[v_index]) )
          end
        end

        ##Convert to Geom objects
        set_container_direction = Geom::Vector3d.new(set_container_direction)
        set_container_pivot = Geom::Point3d.new(set_container_pivot)

        #Move the set container pivot to the origin
        set_container_matrix = Geom::Transformation.translation(set_container_pivot) \
                             * Geom::Transformation.rotation(ORIGIN,
                                                             Geom::Vector3d.new(0,0,1),
                                                             Geom::Vector3d.new(1,0,0).angle_between(set_container_direction))
        set_container.transform!(set_container_matrix.inverse)

        #Merge the set items together
        set_container.explode
        set_instance = Sketchup::ComponentInstance.merge(current_set, set_name, :+, arg_as_component:true, arg_inplace:true)
        set_instance.transform!(set_container_matrix)
      end
    end

    #End undo stack operation
    commit_operation
  end

Thanks !

This indicates that you allow being within an editing context. The API and internal engine is written in a way that changes the transformations when not at the model’s top level. When in an edit context coordinates in transforms are in world coordinates not in the instance’s local coordinates.
(This has been explained here in this category multiple times.)

So you may be better off closing all edit contexts before your code does what it does.

model.close_active until model.active_entities == model.entities

EDIT: I see you’ve already been told this by Julia …

@DanRathbun active_scope is a custom method pluged onto the Sketchup::Model that alows me to retrieve either the user selection if one if define or the model content. This is used at many places in my code. It has nothing to do with the active_entities, I’m not working with it at all to avoid what you’re pointing

class Sketchup::Model
  def active_scope
    ( selection.empty? ? entities.to_a : selection.to_a)
  end
end

That’s exactly why I didn’t want to post some code because there is a lack of code context with only a part of it. Meaning why I was explaining the approach to check that it should be the good one or not.

It is a big no-no to modify base or API classes ... (click to expand.)

It is a big no-no to modify base or API classes.

You should use a refinement instead …

module Milmandre

  module ModifiedModel
    refine Sketchup::Model do
      def active_scope
        ( selection.empty? ? entities.to_a : selection.to_a)
      end
    end # refinement 
  end # refinement module

  module SomePlugin
    using ModifiedModel

    # you can now use model.active_scope() 
    # without effecting any other plugin

  end # plugin submodule

end # outermost namespace module

The Ruby Core doc: https://ruby-doc.org/core-2.5.5/doc/syntax/refinements_rdoc.html

I thought I’d look at your code to see if I could easily spot what went wrong, … then I notice that you are directly modifying multiple API classes.

All bets are off. Deal me out” …

Ok got it Dan (I was sure you’ll be yelling at me :wink: ), thank for it. This is a nice way to do it. As it was only for internal use I get the direct way but yours is much better for sure. I’ll make the update to get back to the best practices but that doesn’t tells me if the approach seems fine or not regarding the issue I’m facing. Any ideas ?

I can’t see what the problem is but could you isolate the code into a minimal example that shows this behavior and add a comment where you get a different result than expected?

1 Like

@eneroth3 Thanks for looking at this :slight_smile:

So I’m starting from scratch, I have a set of component instances that I’d like to merge together in a single component instance with a choosen pivot point. What I’m doing so far is :

  1. Grouping the instances together (the insert method a is custom method that reparent entities into another group)
    #Create a set container
    set_container = Sketchup.active_model.entities.add_group()
    set_container_handler = set_container.entities.add_cpoint([0,0,0])
    set_name = current_set[0].definition.name
    set_container.name = set_name

    #Move the items into the set container
    current_set.each{|itm| set_container.insert(itm, arg_remove_origin:true)}
    set_container_handler.erase!
    current_set = set_container.entities.to_a

  2. Moving the parent group to place the instances where I want them to be regarding the scene origin (assuming that creating a component definition will set its pivot at the scene origin).
    #Compute the set container direction and pivot
    ##Extract compute data
    set_container_bbox = set_container.bounds
    set_container_vector = set_container_bbox.min.vector_to(set_container_bbox.max)
    set_container_length = set_container_vector.to_a.max

    ##Compute as array
    set_container_direction = set_container_vector.to_a.collect{|v| ((v/set_container_length).round(6) == 1 ? 1 : 0)}
    set_container_pivot = set_container_bbox.min.to_a.zip(set_container_bbox.center.to_a).each_with_index.collect do |v_set, v_index|
    if v_index == 2
    v_set[0]
    else
    ( v_set[0] * set_container_direction[v_index] + v_set[1] * (1-set_container_direction[v_index]) )
    end
    end

    ##Convert to Geom objects
    set_container_direction = Geom::Vector3d.new(set_container_direction)
    set_container_pivot = Geom::Point3d.new(set_container_pivot)

    #Move the set container pivot to the origin
    set_container_matrix = Geom::Transformation.translation(set_container_pivot) * Geom::Transformation.rotation(ORIGIN, Geom::Vector3d.new(0,0,1), Geom::Vector3d.new(1,0,0).angle_between(set_container_direction))
    set_container.transform!(set_container_matrix.inverse)

  3. Exploding the parent group to keep the instances at the right place

  4. Merge the instances geometries by instacinating new copies of their definitions at the same place into a newly created group (the Sketchup::ComponentInstance.merge method is used to perform this task, I haven’t post it because its before this that I have something that I can’t understand/handle moreover as Dan said I shouldn’t extend directly SU Classes).

  5. Generate the defintion of the merged geometries from the group.

  6. Replace it at its original place
    #Merge the set items together
    set_container.explode
    set_instance = Sketchup::ComponentInstance.merge(current_set, set_name, :+, arg_as_component:true, arg_inplace:true)
    set_instance.transform!(set_container_matrix)

My problem is that I don’t understand why the component instances transfromations remain the same in my code althought they have been transformed as it isn’t when quering in the Sketchup Ruby Console.

  • At the step 3, when exploding the parent group and then quering the component instances transforms I get their original positions. So when performing 4 and 5 the instances copies aren’t place at the good position and everything runs dirty from then.

  • But if i’m exiting the script between the step 3 and 4 the instances transforms that I can inspect within the SU Ruby Console seems correct and non consistant with the one inspected with the breakpoint.

The reason you see different coordinates in the console could be that you change your drawing context when inspecting this way versus when the script runs. It could also be that exploding gives objects new transforms, as the exploded parent’s transform has to be incorporated into the transform of each direct child for it to stay in the same place in model space. Without a minimal example it is hard to see what goes wrong.

@DanRathbun @eneroth3 Thanks both of you but I found the problem when building a minimal test use case as requested by both of you too.

When calling the explode method on the component instances group, my array of instance wheren’t tagged as deleted so I didn’t refresh the list BUT in fact they where deleted and the list were pointing to others similar instances. I need to dig a bit more but as usual the mistake cames from a wrong postualte from mine.

Anyway @DanRathbun, even if the pulgin is for internal use only I will apply your recommandations that are really nice. Thanks for it :wink:

No problem. Internal use does not matter because it is the modification of the same API classes that all other extensions also use, that can cause issues. There have been several big cases of this where large extensions directly modified API and Ruby core classes which broke many other extensions.
And I’m sure you and your coworkers also run other extensions that y’all rely upon.

1 Like

don’t forget SU installs it’s own extensions on every startup and you could break those…

john