Reverting the Y axis direction of a group via Ruby API, while preserving location and orientation of group geometry

I created an extension that renders and imports quite complex 2D geometry (edges and faces) into Sketchup, in a multilevel group structure. The different groups in this structure may have been transformed in multiple ways (scaling, moving, rotating, skewing, mirroring) and ‘painted’, to finally arrive at the desired design. Ultimately, all grouped geometry is collected in a single top level group. This top level group (as well as the childs) have their y-axis oriented downwards (as a consequence of how the source geometry data is supplied). I would like to have the group axis pointing upwards, and positioned at the overall origin. The picture below shows a simple example of the current situation (no faces involved here, but that’s not relevant for the issue at hand). The top group axis system is near the upper left corner of the group (in the center of the red circle, and the Y axis points DOWNWARDS. The geometry in the group is shown in the selected group (blue rectangle), and is oriented upwards, and this should stay. The geometry is also positioned as wanted (i.e. the bottom left corner is at the global origin).

What I would like to achieve, via Ruby code, is that the group axis system moves to the global origin (at the center of the green circle at the left bottom), and that the y-axis points UPWARDS!, while the geometry stays where it is (bottom left of the bounding box at the origin, and oriented upside up, as is the case already). I’m aware that I can set the axis manually as desired via the UI, but I would like to add this as a final step of the extension, because y-axis pointing downwards doesn’t feel ‘natural’ in Sketchup and may result e.g. in upside down glueing when converting the top group to a component, and then trying to glue it (unless the component axes are manually defined). I’ve researched this forum, and although there are a number of posts dealing with axis management via the Ruby API, I could not find a solution that solves my issue (i.e. reverting the direction of a GROUP axis (not the global axis), while not changing the location (at world origin) and the upside orientation of the geometry).

I think you will need to apply a transformation several times.

To all the entities inside the group’s definition.
See Geom::Transformation::axes() and Sketchup::Entities#transform_entities()

You’ll need to flip the entities inside:
Ex:

ents = grp.definition.entities
#Vertically flip the entities in grp's definition:
flip = Geom::Transformation.axes(ORIGIN, X_AXIS, Y_AXIS.reverse, Z_AXIS)
ents = grp.definition.entities
ents.transform_entities(flip, *ents)

Then move the entities by the bounds height …
See Geom::BoundingBox and Geom::Transformation::translation()
Ex

# Move the entities by the height:
box = Geom::BoundingBox.new
box.add( ents.map(&:bounds) )
height = box.height
t = Geom::Transformation::translation([0, height, 0])
ents.transform_entities(t, *ents)

… and afterward flip the instance itself …
See: Sketchup::Group.transform!()
Ex:

# Flip the grp instance as a whole vertically:
grp.transform!(flip)

… and then applying a translation to move the insertion point to the model ORIGIN.
Ex:

# Move the grp to the model ORIGIN:
pt = grp.transformation.origin
dist = pt.vector_to(ORIGIN)
grp.transform!( Geom::Transformation::translation(dist) )

Putting it all together in an undoable method ... (click to expand) ...
def correct_y
  model = Sketchup.active_model
  if model.selection.empty?
    UI.messagebox("Select a group or component!")
    return
  end
  grp = model.selection[0]
  model.start_operation("Correct Y", true)
    #
    ents = grp.definition.entities
    # Vertically flip the entities in grp's definition:
    flip = Geom::Transformation.axes(ORIGIN, X_AXIS, Y_AXIS.reverse, Z_AXIS)
    ents = grp.definition.entities
    ents.transform_entities(flip, *ents)
    # Move the entities by the height:
    box = Geom::BoundingBox.new
    box.add( ents.map(&:bounds) )
    height = box.height
    t = Geom::Transformation::translation([0, height, 0])
    ents.transform_entities(t, *ents)
    # Flip the grp instance as a whole vertically:
    grp.transform!(flip)
    # Move the grp to the model ORIGIN:
    pt = grp.transformation.origin
    dist = pt.vector_to(ORIGIN)
    grp.transform!( Geom::Transformation::translation(dist) )
  #
  model.commit_operation
end

Thanks a lot for this reply. Unfortunately, it doesn’t fix the issue yet completely. What it actually gives as result:

  • It points the group Y axis upwards: GOOD
  • It moves the group axis to the model origin: GOOD
  • But, it also moves the group bounding box AWAY from the model origin where it was previously, and where it should stay: BAD

See also the graphical outcome of the updated code in the screenshot below (for the same model as in my original post)

The group (blue rectangle) should have its bounding box minimum at the global origin (i.e. shift by the red arrow). Applying an additional move transformation to do so, seems to move the group origin as well (–> BAD). As far as I have been able to verify, your solution seems to work when the initial group axis is located on the upper left of the group bounding box (Y axis pointing downwards). In these cases, the result of your code snippet moves the group axis to the lower left corner of the bounding box (GOOD), and turns the Y axis upwards (GOOD), while the group’s bounding box min vertex stays at the global origin (GOOD). But, in my extension, this prerequisite for the initial group axis isn’t guaranteed… Any further suggestions to fix this are highly appreciated. I’ll keep trying from my end as well, but this becomes more of a ‘trial and error’ attempt..

A small update on my last post, which was not really correct. Actually, the fix presented in the code snippet did the following:

  • It reverts the direction of the group’s Y axis (GOOD)
  • It keeps the bounding box of the group at the global world origin (GOOD)
  • But, it does NOT place the origin of the group axis at the global origin (BAD).

The confusion in my previous post comes from a wrong interpretation of where the global origin actually was…
Thus: the correction that is still needed is to move the group origin to the lower left corner of the blue bounding box (which also corresponds to the global origin). Thus: a move in the opposite direction of the red arrow.

Sorry for the confusion…

flipmoveaxes

def correct_y
  model = Sketchup.active_model
  if model.selection.empty?
    UI.messagebox("Select a group or component!")
    return
  end
  grp = model.selection[0]
  orig = grp.transformation.origin
  flip = Geom::Transformation.axes(orig, X_AXIS, Y_AXIS.reverse, Z_AXIS.reverse)
  gents = grp.definition.entities
  model.start_operation("Correct Y", true)
    gents.transform_entities( flip, gents.to_a )
    grp.transformation = grp.transformation * flip.inverse
  model.commit_operation
end

Wow, dezmo’s solution seems indeed to do what I was expecting. Still need to integrate and test in my code.

For what it’s worth, as said, I also continued the ‘trial and error’ approach on my side, and I was able to find something that worked for my case, based on Dan’s input. I basically had to change the origin of the flip transformation.

My current code, that ‘seems’ to work correctly (don’t ask me why, the Ruby API documentation is basically ‘worthless’ in this area), goes as follows:

          ents = su_group.definition.entities
          flip = Geom::Transformation.axes(Geom::Point3d.new(-bb.min.x, bb.min.y, 0.0), X_AXIS, Y_AXIS.reverse, Z_AXIS)
          ents.transform_entities(flip, *ents)                                  # Vertically flip the entities in grp's definition
          move = Geom::Transformation::translation([0, bb.height, 0])
          ents.transform_entities(move, *ents)                                  # Move the entities by the bounding box height
          su_group.transform!(flip)                                             # Vertically flip the grp instance as a whole
          pt = su_group.transformation.origin
          dist = pt.vector_to(ORIGIN)
          su_group.transform!( Geom::Transformation::translation(dist) )        # Move the grp to the model ORIGIN

The core of my 'trick' was in setting the origin of the flip transformation.
Please notice that in my code, the top level group is called su_group and it's bounding box is defined as bb. 

But, I think that dezmo's solution is better, as it seems to have only 3 transformations, and also, it seems to be more generic, which might make it feasible to apply this solution recursively to all multilevel child groups of the toplevel group. So, I'll certainly need to integrate and test that version.
Thanks a lot!

Sorry dezmo, I integrated your version, it worked in a number of cases, but not always. Anyway, I had to remove the Z-axis inversion in your code, because in my code I ensure upfront that my faces are created in a plane above the XY plane (and thus don’t face automatically downwards). But, with that correction, your solution did not work all the time. I can’t see it very good in your film with a perspective camera, but I think your case works (like in Dan’s case) when both axis systems (global and group) are in the same x plane (and y plane - difficult to see in perspective view). The issue is that in my code pipeline that is often not the case (see e.g. the example in my earlier posts). Therefore, I believe that I really need the translation transformation, and flipping alone doesn’t solve all cases. I’m also not sure that my code version solves all cases (for my pipeline) but so far, I haven’t found any counter indications (trial and error).

What if you create a new group with the existing group (su_group ) as a parameter, then explode the original top level group. That way the new group will be the top level group and - since it is placed to the model origin by default - you do not need to brother with the transformations… :wink:

  model = Sketchup.active_model
  su_group_new = model.entities.add_group(su_group)
  su_group.explode

regroup

Hi dezmo, you must have been reading my mind… While my ‘solution’ seemed to cover all cases, I suddenly discovered an even more bizar phenomenom with one specific test case. Group axis was placed and oriented correctly, and the group stayed at the world origin. BUT: all of a sudden, the bounding box of the group was no longer aligned with the boundaries of the group’s geometry??? (i.e. there was a shift up and to the right). I’ve never encountered such a group and bounding box in many years of using Sketchup. I assume it must be a bug in Sketchup…

So, I was searching for alternatives and recalled that somewhere else in my code, I had ‘moved’ child components from their parent to a newly created parent, after exploding the old parent. That code was a bit ‘special’ because it was actually a ‘move’ operation, and worked with persistentID’s, because it was not guaranteed that the move would happen during the same session. But from that piece of code, I remembered that the explode action seemed to ‘erase’ the transformation info at the new parent. So, I wanted to start testing that alternative, and then your reply arrived… Your code is actually even simpler, as it does’t require persistent_ids. The only extra thing I need to foresee (for my needs) is a transfer of group name and attributes, but that’s much simpler than the Sketchup transformation labyrinth. I ran a first couple of tests with your last code snippet, and it seems to work, including avoiding the extremely bizar ‘out-of-sync’ between the group geometry and the group bounding box.

I could not test your model because you did not post it in the original post. It it good to give a test model when asking for help.

Bounding boxes are determined by the extents of an object’s entities, NOT by it’s axes origin! (I.e., the origin can be and often is outside the bounds of it’s entities collection.)

Well, sorry, but again I was going off of the image you originally posted, lacking an actual model to test.

ADD: I had to quickly draw my own model for testing, which of course (as you surmised) had the group origin at the top left of the entities bounding box.

That’s okay because I am was confused about what you wanted anyway.

Take what I gave as a general pointer to using transformations and play with it. Consider it a learning experience.

Yea, this is fine, I just did not understand this was what you needed.
But it looks like our snippets have gotten you way down the path to success.

:clap:

A purge of the old group definitions will be needed so as not to bloat the model.

Yes it is a known bug in older SketchUp versions. Are you still using SU 2017?

I think @colin worked on tracking this bug. Sometimes calling Sketchup::Definition#invalidate_bounds helped and sometimes not.