Why is transforming vectors different from transforming points?

This caused me a lot of confusion and headaches when I was writing my code.

Seems that if you translate a vector using Geom::Vector3d#transform(transform) or Geom::Transformation#*(vector), you get the equivalent of the matrix operation TV, but if you translate a point using Geom::Point3d#transform(transform) or Geom::Transformation#*(point), you get the equivalent of the matrix operation PT. What is the point of this?

I guess there are none of the method exist in Ruby API that you are referring to. You maybe wanted to mention e.g. Vector3d#transform -instance_method ā€¦ :wink:

Most probably Iā€™m not the best to explain, but there is what I recognised:

If you check both of the Geom::Vector3d and Geom::Point3d Constructor detailsā€¦

#initialize(x, y, z) ā‡’ Geom::Vector3d vs #initialize(x, y, z) ā‡’ Geom::Vector3d
Parameters:
x (Numeric)
y (Numeric)
z (Numeric)
OR:
#initialize(array3d) ā‡’ Geom::Point3d vs #initialize(array3d) ā‡’ Geom::Vector3d
Parameters:
array3d (Array(Numeric, Numeric, Numeric))

pt1 = Geom::Point3d.new(100,200,300)
vec1 = Geom::Vector3d.new(100,200,300)
pt2 = [100,200,300] #only the context will tell what it is
vec2 = [100,200,300] #only the context will tell what it is

ā€¦you should not surprise about the similarity of matrix operations.

I hope there will come another real expert who will explain betterā€¦ :innocent:

1 Like

Um, yeah. It was a long night last night fighting with the APIā€¦ :slight_smile: Iā€™ve updated the question.

The issue is that transformations using vectors are like when multiplying transformation matrix times a vector matrix (TV), but transformations using points are like when multiplying a point by a transformation matrix (PT). I would have expected they would be both like the latter, not different. That, or that the order could be controlled by a clearer idiom, such as to have a Vector3d#*(transformation)/Point3d#*(transformation) for VT/PT and Transformation#*(vector)/Transformation#*(point) for TV/TP and all vector/point transformations through a member function called transform being VT/PT.

Iā€™m not even sure what the point of multiplying a transformation matrix to a vector/point would even get you (but then again, my matrix algebra is pretty rusty).

For future reference and use with the Ruby language:

It is customary to refer to only module or class methods using the :: scope operator as the method prefix. Ie ā€¦
Sketchup::active_model()

ā€¦ and to use # in place of a dot or scope operator only for instance methods. Ie ā€¦
Geom::Vector3d#transform()

This way we know immediately what kind of method is being discussed. And we can go to the proper section in the documentation for the class to find the method definition. (Class methods are usually listed above instance methods.)

You will see this followed throughout the Ruby Core documentation.

What dezmo is getting at is with the SketchUp API, they wrote many of the methods in the classes to use Arrays, Vectors, Points and Transforms interchangeably. Often a simple literal array is used to represent a vector for a translational transform, and the API methods accept this. Ex:

# Where old_pt is a Geom::Point3d object:
new_pt = old_pt.transform([0,0,23.7])

ADD: The result is a new Geom::Point3d object that is 23.7 inches above (Z-wise) the old one.

This code might clear things up a little (or not).

pt1 = Geom::Point3d.new(100,200,300)
vc1 = Geom::Vector3d.new(100,200,300)
offset = Geom::Point3d.new(10,20,30)

# create a translating Transformation
puts "\nTransformation Matrix"
tr = Geom::Transformation.new(offset)
tr.to_a.each_slice(4) {|a| p a}

# translate the Point3d object by the Transformation and by an Array
puts "\nPoint operations"
puts  pt1.transform(tr)
puts  pt1.transform(offset.to_a)
puts  pt1 + offset.to_a


# The Vector3d class is used to represent vectors in a 3 dimensional space. 
# Vectors in SketchUp have a direction and a length, but not a starting point.
# https://ruby.sketchup.com/Geom/Vector3d.html#%2B-instance_method
#
# The implication here is that if you translate a vector via the tranform method
# you get back the vector unchanged, but, you can add an array to a vector to
# get the expected result as shown in the last example

# translate the vector3d object by 
puts "\nVector operations"
puts vc1.transform(tr)
puts vc1.transform(offset.to_a)
puts vc1 + offset.to_a

nil
point1 = [10, 0, 0]
point2 = [10, 0, 10]
line1 = [point1, point2]

I supposed line1 will be parallel to Z-axis but was not!!! Finally, I understood Sketchup considers point2 as a vector, not as a point.

1 Like

Yes because a line can be represented as either an Array of a point and a vector, as ā€œdefaultā€. (ā€œdefaultā€ is Not written in documentation, you have to discover it yourselfā€¦ :wink: )

line1 = [Geom::Point3d.new(10, 0, 0), Geom::Vector3d.new(10, 0, 10)]

Since both variable above (point1 , point2) are currently an Array the context here will interpret the second ā€œpoint2ā€ as vector. The line will cross a point1 and the direction is a 45Ā° vector.

But a ā€œsecond formā€ is as an Array of two points.

line1 = [Geom::Point3d.new(10, 0, 0), Geom::Point3d.new(10, 0, 10)]

Certainly, The line will different: cross a point1 and the direction is vector from point1 to point2 (0,0,10)

If you define more precisely in the begening

point1 = Geom::Point3d.new(10, 0, 0)
point2 = Geom::Point3d.new(10, 0, 10)
line1 = [point1, point2]

Then the expected result is clearerā€¦ :wink:

1 Like

Thanks. Iā€™m from a C++ background so this is new to me. Iā€™ll update my posts above.

Oh, thatā€™s an interesting undocumented operation. I thought that you needed to do it this way:

new_pt = old_pt.transform(Geom::Transformation.translation(Geom::Point3d.new(0,0,23.7)))

Can an array be used in all places a Geom::Transformation object is expected? Or is this specific to Geom::Point3d#transform? Iā€™ve noticed that arrays can be used in certain contexts to mean a point or vector, but in others it causes an error, making the use inconsistent, at least (perhaps) at first glance? Itā€™s definitely not fully documented.

Iā€™ve not seen lines yet used in the API. Iā€™ll keep this in mind, even though this isnā€™t exactly pertaining to my question.

Anyway. Looks like what Iā€™m actually asking is just flying over everyoneā€™s head. I understand these transforms via my matrix algebra that I took many moons ago. To transform a point, you would have the point/vector as a row matrix and multiply it against the transformation matrix. Thus PT, where P is the point in row form, and the T is the transformation matrix. However, matrix multiplication is not transitive, thus PT != TP.

The way the SU lib is designed, Geom::Transformation#* is specified in such a way as to make one think you are multiplying the transformation matrix against the point, which isnā€™t the case (youā€™re multiplying the point against the transformation). You are, however, actually multiplying a transformation against a vector, which is a further surprise. Coming from a CS and Math background, Iā€™m surprised at how this API was constructed. Contextually, this API is a bit all over the place.

Now, given what I just said, please reread what I said in my other posts with this as context and maybe youā€™ll see what I am getting at.

The SketchUp API extends the core Ruby Array class thus ā€¦

Class: Array ā€” SketchUp Ruby API Documentation

Overview


The SketchUp Array class adds additional methods to the standard Ruby Array class. Specifically, it contains methods allowing an array to behave just as a Geom::Vector3d or Geom::Point3d object (which can be thought of as arrays of 3 coordinate values). Therefore, you can use the Array class in place of a Geom::Point3d or Geom::Vector3d as a way to pass coordinate values.

Okay? Now many of the API method docs will list Point3ds, Vector3ds and Array as options. But it is not a hard and fast rule that the API team will do so in the documentation.(More about that later.)

When we find a method that expects a point or a vector, but will not take an array, we open an issue in the API Issue Tracker and usually it will get fixed.

I can say that it can be used other places ā€¦such as:





ā€¦ etc., for Point2d, Vector2d, Vector3d classes.


Similarly, ā€¦

ā€¦ can take 3-element Arrays or Geom::Transformation objects, in the vectors array argument in place of Geom::Vector3d objects.


NOW, all that said, the method docs for all these methods do not specifically mention the variable argument types (ie, overloads.)

I asked that this be done in some way, recently in the official API tracker, but was met with resistance. Ie, they said it way too much work to doc all the overloads. I think I then asked that at least put a note in the overview for the Geom::Transformation class similar to the Overview blurb for Array. Ie, ā€¦ speaking to the interchangeability between transforms, arrays, vectors and points.

Anyway, I canā€™t find the exact issue where we discussed this. All I remember was (I think it was about a month ago,) and the idea did not go over well.

They are abstract (ie, no actual class.) The description is in the Geom module Overview.
Donā€™t overlook that module level. There are some nifty module methods there.

I havenā€™t talked specific to that as Iā€™ve not run into an issue.

Hmmmmā€¦ I think SketchUp transformation matrices may be in column major order.

Seems like a good time for you to install ThomThomā€™s Transformation Inspector plugin:

Whatā€™s this? A matrix/transformation calculator?

Thx for all the info. Itā€™s appreciated.

And manipulator. You can change individual matrix elements and see the results.
See itā€™s Extension Warehouse page for more information and more screenshots.

It still ticks me off that ā€˜multiplicationā€™ is specified as it is. Makes for confusing code as it doesnā€™t jive with the math. :frowning:

Vectors are a direction, and when a translation type of transformation matrix is applied to it (i.e. linear movement), the translation is not suppose to not affect the vector. Internally, Sketchup performs the multiplication differently depending on whether it is a Point3d or Vector3d to achieve the expect convention in geometry. Perhaps the code would be a bit less confusing with:

P_pt.transform( T )
V_vec.transform( T ) 

Someone looking at the code can expect different outcomes if the transformed object is of a different type.

Hi Bruce,

Yes, I understand that they are performed differently, but, why? What purpose would that serve except to confuse the transcription of matrix multiplication? When overloading operators, one shouldnā€™t stray from expected results (i.e. make it more confusing).

Further, I still donā€™t see what is the purpose for multiplying a vector on the right of a transformation matrix. Where would I use such an operation? Even so, if I wanted to right multiply, it would make more sense to do:

P_pt * T
T * V_vec

Which would explicitly show the multiplication order. Then, if I wanted to have a vector be operated on like a point, I could simply do:

V_vec * T

and it is immediately obvious as to what is expected (vector or point on left of transformation is treated as a row matrix, whereas on the right it is treated as a column matrix, and what is returned is whatever type was put in). Instead we have, T * P_pt really meaning P_pt * T and P_pt * T being undefined (as is also V_vec * T).

Why not? I myself have wanted to do such an operation on a vector and I have to do ugly conversions of a vector to a point and back again.

I just find the inconstancy between how matrix multiplication is portrayed and actual matrix multiplication annoying and confusing, with no real justification for it.

Then I suggest you open a documentation issue in the API Issue Tracker.

Please understand that the Geom module classes were implemented like 20 years ago and the authors are likely no longer SketchUp employees. So the reasons why things were done may be lost to history if there are no comments in the code.

I donā€™t know what you were asking, and you didnā€™t provide any example code of what you are trying to achieve, so I havenā€™t answered until recently. Mathematicians of some sort long ago decided that we needed separate constructs for the geometric concepts of position and direction. When operations are applied upon them, it is desirable that they behave differently, even though they both can be represented with a triplet of 3 numeric values. Vectors are not line segments on a piece of paper, not geometric lines at a specific position, and not a point. You can think of them like the wind blowing over a field:

That model is just a imperfect metaphor. A translation transformation changes the position of objects, but not direction. How can you change the position of the windā€™s direction blowing over the field? It doesnā€™t have a position to begin with. Its a direction, not a position.

For the position or point, a similar situation occurs with the rotation transformation. The rotation constructor needs a position, vector and angle. The position and vector define a geometric line that is the rotation axis. If you try to rotate a point that lies on that line, its position wonā€™t change. The same triplet that is a vector will be rotated by the rotation (unless the vector is parallel to the axis of rotation). Matrix operations affect position and direction differently intentionally.

You said:

You can limit some, but not all ugly conversions with duck typing, including the ā€œ.to_aā€ method. But, if you start using an array as a Point3d or Vector3d, you need to test to see how the array is being treated. It would be nice to have the Vector3d class have a ā€œto_ptā€ cast and the Point3d have a ā€œto_vecā€ cast.

A refinement might be used ā€¦

# SomeAuthor_RefinedGeom.rb
module SomeAuthor
  module RefinedGeom

    refine ::Geom::Vector3d do
      def to_pt
        ORIGIN.transform(self)
      end
    end

    refine ::Geom::Point3d do
      def to_vec
        ORIGIN.vector_to(self)
      end
    end

  end
end
require File.join(__dir__,"SomeAuthor_RefinedGeom")

# Must be called at toplevel for older Ruby versions
# and refinement only applies in the current file:
using SomeAuthor::RefinedGeom

module SomeAuthor
  module SomeExtension

    # module code using the refinement

  end
end

Refinements. Interesting. Iā€™ll have to look into that further when I have time. Thx.

I know some other APIs like that of Unity use a single vector class to represent both directions and positions. In those cases you make the choice of whether the translation part of the transformation should be taken into account when you multiply.

In the SketchUp Ruby API different classes represents directions and positions. The choice of how multiplications are done is made already when you create the object.

I think both designs are valid but both can be confusing if you are used to the other. SketchUp is originally created as a 3D modeler ā€œfor the rest of usā€. The program was designed to be more intuitive and require less technical knowledge to be used than existing programs, and I think this can be seen also in the API design. If you took linear algebra in college, thinking in matrices and N-dimensional vectors as abstract mathematical concepts makes sense, but if you have little or no professional math knowledge, thinking in visual concepts like positions, directions and ā€œdifferences in coordinate systemsā€ makes more sense.

My recommendation is to be explicit when you create an object about what it represent. Use Vector3d for all directions and Point3ds for all positions and transformations should just ā€œmagicallyā€ work.

Yes, I agree. Having the transformationā€™s multiply operator * accept point/vector/plane is strange.

Iā€™ve personally never used those overloads. I use the * operator only to multiply against other matrices. And I use the explicit transform method on point and vector objects.

Iā€™m not sure what the reasons behind these overloads are. They are Old Code, from long before anyone still on the SU dev team, so that is lost to history. We are alas stuck with it now.

Another thing I have a gripe with is that we overload the meaning of Array a lot. It can represent point, vector, line, plane, transformation etc. It can be convenient some times, but can also lead to unfortunate ambiguity. And for the concepts of Line and Plane you have to use arrays. I would have preferred dedicated types. But at this point there is so much existing code and logic that Iā€™m doubtful how valuable introducing new types would be.

It might be that the original implementers added the * transformation overloads as a way to allow planes and lines to be transformed. Since they donā€™t have a type of their own the only way is this backward syntax (transform * plane).

It might also be a case of misplaced code re-use. Internally the point and vector class have no transform method. And the API layer reaches out for transform.*(point) and transform.*(vector). Maybe the original implementer of the API wasnā€™t aware this wasnā€™t aware that the order of operation mattered. Unknown at this point.

Implementation looks like this:

By this point weā€™re unable to do anything about it. It would break almost every extension out there. But we could improve documentation.

Btw, I would also recommend being explicit in using Geom::Vector3d, Geom::Point3d, Geom::Transformation instead of using the Array shorthand notation. Eliminates ambiguity.

2 Likes