How to determine normal of a face in a rotated object

Here is a bit of code which draws the shape shown here.

This is just sample code, but in my actual project, I am going to be making a lot of these shapes and rotating them various ways…

I would like to be able to determine which direction (front, back, left, right, top, bottom) the flap is facing after the rotation. But I’m finding that after the rotation, the face still retains the normal it had in the original shape.

module JJones_example

  def self.draw_shape(entities, points, flap_points, material)
    sgroup = draw_edges(points, entities)
    flap = draw_flap(flap_points, sgroup.entities, material)
    [sgroup, flap]
  end
  
  def self.draw_edges(points, entities, with_callouts=false)
    print("draw_edges")
    g = entities.add_group
    last_point = points[points.length - 1] #.shift
    points.each_with_index{ |point, i|
      if with_callouts
        self.make_text(g.entities, point, i)
      end
      g.entities.add_edges(last_point, point)
      last_point = point
    }
    g
  end
  
  def self.draw_flap(flap, entities, material)
    print("draw_flap")
    face = entities.add_face(flap)
    face.material = material
    face.back_material = material
    face
  end
  
  def self.move_thing(obj, distance, axis)
    print("move_thing")
    tr = Geom::Transformation.new([distance,0,0]) if axis == "x"
    tr = Geom::Transformation.new([0,distance,0]) if axis == "y"
    tr = Geom::Transformation.new([0,0,distance]) if axis == "z"
    obj.transform! tr
  end
  
  def self.get_rotation(obj, axis, angle)
    bounds = obj.bounds
    ctr = bounds.center
    rv = obj.transformation.zaxis if axis == "z"
    rv = obj.transformation.yaxis if axis == "y"
    rv = obj.transformation.xaxis if axis == "x"
    ro = Geom::Transformation.rotation(ctr, rv, angle.degrees)
    ro
  end
  
  def self.rotate_thing(obj, axis, angle)
    print("rotate_thing")
    ro = get_rotation(obj, axis, angle)
    obj.transform! ro
  end
  
  def self.copy_and_transform(shape, distance, angle, axis)
    print("copy_and_transform")
    cp = shape.copy
    self.move_thing(cp, distance, axis)
    self.rotate_thing(cp, axis, angle)
    cp
  end
  
  def self.get_material(model)
    color = Sketchup::Color.new(255,255,255)
    material = model.materials.add("translucent")
    material.color = color
    material.alpha = 0.5
    material
  end

end #module

model = Sketchup.active_model
entities = model.active_entities

model.start_operation("copy and rotate shape", true)

#           0        1      2       3       4       5       6      7
pts = [[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
shape_pts = [pts[0],pts[1],pts[3],pts[2],pts[6],pts[7],pts[5],pts[4]]
flap_pts = [pts[3],pts[2],pts[6],pts[7]]

material = JJones_example.get_material(model)

sgroup, flap = JJones_example.draw_shape(entities, shape_pts, flap_pts, material)
print("flap normal: #{flap.normal}")

# now copy and transform the object, rotating it and moving it nearby
copy = JJones_example.copy_and_transform(sgroup, 1.5, 90, 'x')
  
#is flap on left, right, top, bottom, back, front?
faces = copy.entities.grep(Sketchup::Face)
flap2 = faces[0]
print("flap2 normal: #{flap2.normal}")

model.commit_operation

Here’s an image of the rotated shape, highlighting the face normal. As you can see, it still thinks the face is in the original orientation, before the object was rotated.

How can I find the normal of the face in the rotated shape?

Thank you for any clues
-j_jones

The groups (and components) have its own coordinate system, which is determined by the transformation applied to them. Therefore when you inspecting the entities inside the group you will see the coordinates according to this local coordinate system. If you want to get in a model (global) system you need to transform it… (If you have a more nested group/components level you will need to walk through on all and combine their transformation)

So, you need to determine the Group #transfomation of new "copy" , - which is containing the face - then apply it to the vector in question, using Vector3d #transform-instance_method.
e.g.:

tr_group = copy.transformation
print("flap2 normal: #{flap2.normal.transform(tr_group)}")

__

Some advise:
__

  • you can use puts instead of print

__

  • When you are creating a face the edges - which bound the face - is automatically crated
    or/and:
  • You can use the the Edge #find_faces-instance_method to create all of the Faces that can be created with this edge.
2 Likes

Thank you that worked!

Maybe this should be a separate question, but now, if I rotate a rotated copy, the normal to the face makes sense, but the rotation is unexpected.

In this follow-on example I create three copies of the original shape and displace them along x, y and z as well as rotate each one 90 degrees along the respective displacement axis. So far so good.

module JJones_example

  def self.draw_shape(entities, points, flap_points, material)
    #print("draw_shape")
    sgroup = draw_edges(points, entities)
    flap = draw_flap(flap_points, sgroup.entities, material)
    [sgroup, flap]
  end
  
  def self.draw_edges(points, entities, with_callouts=false)
    #print("draw_edges")
    g = entities.add_group
    last_point = points[points.length - 1] #.shift
    points.each_with_index{ |point, i|
      if with_callouts
        self.make_text(g.entities, point, i)
      end
      g.entities.add_edges(last_point, point)
      last_point = point
    }
    g
  end
  
  def self.draw_flap(flap, entities, material)
    #print("draw_flap")
    face = entities.add_face(flap)
    face.material = material
    face.back_material = material
    face
  end

  def self.get_rotation(obj, axis, angle)
    #print("get_rotation")
    bounds = obj.bounds
    ctr = bounds.center
    rv = obj.transformation.zaxis if axis == "z"
    rv = obj.transformation.yaxis if axis == "y"
    rv = obj.transformation.xaxis if axis == "x"
    ro = Geom::Transformation.rotation(ctr, rv, angle.degrees)
    ro
  end

  def self.rotate_thing(obj, axis, angle)
    #print("rotate_thing")
    ro = get_rotation(obj, axis, angle)
    obj.transform! ro
  end
  
  def self.move_thing(obj, distance, axis)
    #print("move_thing")
    tr = Geom::Transformation.new([distance,0,0]) if axis == "x"
    tr = Geom::Transformation.new([0,distance,0]) if axis == "y"
    tr = Geom::Transformation.new([0,0,distance]) if axis == "z"
    obj.transform! tr
  end
  
  def self.copy_and_transform(shape, distance, angle, axis)
    #print("copy_and_transform")
    cp = shape.copy
    self.move_thing(cp, distance, axis)
    self.rotate_thing(cp, axis, angle)
    cp
  end
  
  def self.get_material(model)
    #print("get_material")
    color = Sketchup::Color.new(255,255,255)
    material = model.materials.add("translucent")
    material.color = color
    material.alpha = 0.5
    material
  end
  
  def self.get_normal(xgroup, face)
    #print("get_normal")
    # get the normal from a face in a transformed group
    tr_group = xgroup.transformation
    norm = face.normal.transform(tr_group)
    norm
  end
  
  def self.make_text(group, txt)
    #print("make_text")
    tgroup = group.entities.add_group
    # rotate text so it appears facing you instead of lying down
    txt = tgroup.entities.add_3d_text(txt, TextAlignCenter, "Arial", true, false, 0.8, 0.0, 0.0, true, 0.25)
    x_rotate = Geom::Transformation.rotation(ORIGIN, X_AXIS, 90.degrees)
    tgroup.transform!(x_rotate) 
  
    # move text to center of cube
    target = Geom::Point3d.new(0.5,0.5,0.5)
    bounds = tgroup.bounds
    xt = Geom::Transformation.translation(bounds.center.vector_to(target))
    tgroup.transform!(xt) 
    tgroup
  end  
  
  def self.delete_text(group)
    #print("delete_text")
    gz = group.entities.grep(Sketchup::Group)
    if gz.length > 0
      tg = gz[0]
      group.entities.erase_entities(tg)
      #print("gz: #{gz}")
    end
  end
  
  def self.copy_test(orig, dist, angle, axis, txt)
    print("copy_test: #{orig}, #{dist}, #{angle}, #{axis}, #{txt}")
    copy = JJones_example.copy_and_transform(orig, dist, angle, axis)
    JJones_example.delete_text(copy)
    tgroup = JJones_example.make_text(copy,txt)
    # identify the flap's new normal
    faces = copy.entities.grep(Sketchup::Face)
    flap = faces[0]
    tr_group = copy.transformation
    flap_normal = flap.normal.transform(tr_group)
    print("#{txt} flap normal: #{flap_normal}")
    copy
  end
  
end #module

model = Sketchup.active_model
entities = model.active_entities

model.start_operation("copy and rotate shape", true)

#           0        1      2       3       4       5       6      7
pts = [[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
shape_pts = [pts[0],pts[1],pts[3],pts[2],pts[6],pts[7],pts[5],pts[4]]
flap_pts = [pts[3],pts[2],pts[6],pts[7]]

material = JJones_example.get_material(model)

sgroup, flap = JJones_example.draw_shape(entities, shape_pts, flap_pts, material)
txt = 'O'
tgroup = JJones_example.make_text(sgroup,txt)
print("flap normal: #{flap.normal}")

copy1x = JJones_example.copy_test(sgroup, 3.0, 90, 'x', "1x")

copy1y = JJones_example.copy_test(sgroup, 3.0, 90, 'y', "1y")

copy1z = JJones_example.copy_test(sgroup, 3.0, 90, 'z', "1z")

#now copy a copy and rotate it, does the normal make sense? 
#copy1zx = JJones_example.copy_test(copy1z, 1.5, 90, 'x', "1zx")

But then if I copy and rotate a copy. (uncomment last line). Here I copy the 1z copy and displace it along ‘x’ and rotate it along ‘x’. The rotation of the copied copy winds up actually being along the ‘real world y’ axis instead of ‘real world x’ as I expected.

I would have expected the 1zx block to be rotated along the ‘x’ axis, but it is now rotated along ‘y’. This is so confusing.

Expected result rotation for 1zx (first z then x) didn’t get this though

Thanks for any additional clues
-j_jones

When you set the axis of transformation you are using the object (aka local) axis. Until the object not rotated these axes are same direction as the global axes… but after rotation it will change.
The global axes defined in a Top Level Namespace as a constant : X_ASIS, Y_AXIS, Z_AXIS

Switch on the Show Component axes in Model Info>>Component menu, to see the orientation axes of groups and components when you are teasing.

Other references to read:

1 Like

Ah! Thank you! That’s what I get for copy/pasting code from other examples without truly understanding it. I found the rotate logic in another topic (Rotate object in ruby script - #6 by McGordon) and didn’t realize what I was doing. Thank you

replacing it this way makes it work as one would expect

    rv = Z_AXIS if axis == "z"
    rv = Y_AXIS if axis == "y"
    rv = X_AXIS if axis == "x"