Find area of scaled faces?

Got a fun puzzle today:

I’m trying to find the area of the faces in a component that has been scaled. Naturally this distorts the materials on the component.

First I search the model to find the particular faces I’m interested in, then I check to see if the parent component has been scaled. It it’s been scaled, I can’t trust the measurements, such as face.area.

The parent of a Face is always a ComponentDefinition instead of a ComponentInstance.
There is no way to access the entities in a ComponentInstance except by doing something like:

my_component.definition.entities.each do |e|
  # Do something here
end

Normally this could get us any info about the component, but in the case of a scaled component, asking the ComponentDefinition for info reports incorrect information. Is there any way to get this information from the ComponentInstance?

Possible solutions I’ve thought of:

  1. Explode the scaled component, read info from it’s entities, and then reform it somehow.
  2. Make the component unique somehow?

I know this is “bad form” on the user’s part to have differently sized versions of the same component in the model, but I want to accomodate their bad behavior if possible.

The ComponentInstance’s Transformation has the information about how it has been scaled from the ComponentDefinition. Note, however, that if the ComponentInstance is nested in another Component, you will have to find the specific nesting path of that instance because it’s parent will be the ComponentDefinition in which it is nested! You can get the nesting path by various means when the user selects the instance via the GUI, but it is much messier when you identify the instance using Ruby code. In effect, you have to start with the model and work your way forward through instances, accumulating the total Transformation along the way.

But a Transformation can scale the x, y, and z directions differently. This makes calculating area a more complicated task in any case.

The face itself doesn’t belong to any specific component instance but to the component definition. If you open a component instance and select a face you’ll see that the face is also shown as selected in all other instances of the same component.

To calculate the area you need to specify for what instance, e.g. by using Model#active_path or get an instance path from an InputPoint object.

Once you have specified an instance path you can multiply all transformations in it to get the total transformation of the face’s context. You can also get such a transformation directly from InputPoint if that’s how you specify the face.

To get the area scale factor for the plane of the face once the Transformation is specified I typically find two normalized arbitrary vectors in the plane. This can be done by taking the vector of the first edge of the face, and by cross multiplying said vector with the normal of the face. Make sure both these vectors are normalized (have the length 1). Then transform both vectors by the Transformation and calculate their cross product. The scale factor in the plane is the length of the cross product.

There is probably a more idiomatic way using determinants and stuff but this is the approach i’ve found myself. I haven’t studied this kind of math officially, just learned it myself while making plugins.

1 Like

We left open the question of how you actually find the area. Here’s one way:

  • as in the previous posts, find the total Transformation of the ComponentInstance containing the Faces of interest
  • for each Face of interest, transform its vertex points into the model’s global coordinates using the Transformation
  • apply an algorithm such as given here to calculate the area from the vertex points.

And just to make it even more complicated, if you planned to specify the face instance using Model.active_path (the drawing context that is open for editing) you actually don’t need to do anything. When a drawing context is opened for editing everything in it is temporarily transformed into the model coordinate space, meaning Face#area will return the scaled area.

1 Like

Yes, I managed to get the scaling vectors from the Transformation like this:


# my_face is a Face inside a Component
my_face.parent.instances.each do |ins|
  transformation = ins.transformation
  arr = transformation.to_a

  # Find scalar component of 4x4 matrix
  v1 = Geom::Vector3d.new(arr[0], arr[1], arr[2])
  scale_x = v1.length.to_f
  v2 = Geom::Vector3d.new(arr[4], arr[5], arr[6])
  scale_y = v2.length.to_f
  v3 = Geom::Vector3d.new(arr[8], arr[9], arr[10])
  scale_z = v3.length.to_f
end

I need to do everything without user input by clicking around, and opening/closing components, or by selecting. I have a searching tool that finds the correct Face (example as my_face above.

Will have to look into this.

Ah yes, that’s what I’m trying to avoid actually.

The actual end goal is to automatically find the sum area of a material in the model. I’ve got everything working until a user scales one ComponentInstance (thus making a unique, non-unique Component.

This will iterate all the instances of the component containing the face. Is this what you want or do you want to specify a single instance of the face, e.g. the one you have clicked with the mouse? In any case there can be numerous levels of nested components, all being scaled in various ways.

No clicking. Some components might be scaled, some might not be.

scaling

For instance, I want to add together all of this yellow material areas. They are in the same component, but the larger one has a scale factor. I can extract the scale factor, but no way to get face.area for the larger, scaled component.

In that case I would create a method that iterates an entities collection, with a local transformation as parameter. When the method is initially called (typically in the root entities) the transformation is the IDENTITY transformation. When the method finds a Group/ComponentInstance it calls itself recursively, but with the cross product of the local transformation and the Group/ComponentInstance own transformation as argument (I can’t remember the correct order of the Transformations).

This way you always have a reference to the local Transformation whenever a face is found and each instance of the same face will be found, regardless of how components are nested and scaled.

I cleaned up my old project and published it on GitHub. This can be used as a reference for other plugin developers.

1 Like
    # REVIEW: What happens when user is not in the model root? That messes up
    # the Transformations and coordinates reported, doesn't it?

Wouldn’t you leverage Sketchup::Model#edit_transform()

This allows one to correctly calculate “local” transformations of a given entity regardless of whether the user is in edit mode.

:question:

At the root of a new model …

t = Sketchup.active_model.edit_transform
#=> <Geom::Transformation:0x0000000f6d9ac0>
t.identity?
#=> true

But if inside a component edit context that is at the origin and not been scaled nor rotated, it’s transform will also be equal to identity.

So … always need to check active edit path:

Sketchup.active_model.active_path.nil? # true if at model root

It seems there is no need to adjust for this though. The CompoinentInstance/Group#transformation and Face#area seem to cancel each other out as they both depend on what is the active drawing context.

1 Like

This is excellent! Thank you @eneroth3
Found one bug in your implementation. This gives incorrect values for faces that don’t lie in the XY, YZ, or XZ plane.

Example:

I realize it’s counting backface material as well (so should be double the sum of all faces, but is off because of the 0.217m face.

Here’s what I came up with (basic implementation ignoring transformations):

# Get determinant of 3x3 matrix a
def determinant(a)
  determ = a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]
  determ
end

# Get unit normal vector of plane defined by points a, b, and c
def unit_normal(a, b, c)
  x = determinant([[1, a[1], a[2]], [1, b[1], b[2]],
                   [1, c[1], c[2]]])
  y = determinant([[a[0], 1, a[2]],
                   [b[0], 1, b[2]],
                   [c[0], 1, c[2]]])
  z = determinant([[a[0], a[1], 1],
                   [b[0], b[1], 1],
                   [c[0], c[1], 1]])
  magnitude = (x ** 2 + y ** 2 + z ** 2) ** 0.5
  normal = [x / magnitude, y / magnitude, z / magnitude]
  normal
end

def polygon_area(poly)
  if poly.length < 3
    return 0
  end

  total = [0, 0, 0]
  poly.each_with_index do |vertex, index|
    v1 = Geom::Vector3d.new(vertex)
    v2 = Geom::Vector3d.new(poly[index - 1])

    prod = v1.cross(v2)
    total[0] += prod[0]
    total[1] += prod[1]
    total[2] += prod[2]
  end

  # General formula for area of polygon in 3D space
  sum_of_crosses_vector = Geom::Vector3d.new(total)
  unit_normal_vector = Geom::Vector3d.new(unit_normal(poly[0], poly[1], poly[2]))
  double_area = sum_of_crosses_vector.dot(unit_normal_vector)
  area = (double_area / 2).abs
  area
end

And here’s an abbreviated test case with no walking the model tree, and no unit conversion:


area_sum = 0
Sketchup.active_model.entities.each do |e|
  next unless e.is_a? Sketchup::Face
  array_of_vertices = []
  e.vertices.each { |v| array_of_vertices << v.position.to_a }
  face_area = polygon_area(array_of_vertices)
  area_sum += face_area
end

puts area_sum

This + your example should give the complete solution. Thanks so much everyone.

Good catch! I had missed the parenthesis in arbitrary_perpendicular_vector which prevented it from returning a unit vector when the vector supplied wasn’t along any of the coordinate axes.

Updated the GithUb repo: Fix bug for faces in tilted planes · Eneroth3/eneroth-face-area-counter@8aee158 · GitHub.

Regarding your own code a lot of its functionality already exists in the API. If you want to measure faces that already exists in the model you can save some work by relying on Face#area, Face#normal and so on. However doing it yourself can of course be a good way to learn about geometry and math (everything I know about matrices, cross products and dot products I’ve learned by writing plugins). If you need to get the area of a face that doesn’t yet exist in the model, with an array of points as your only reference, your only option is to code it yourself though as you are doing now.

Just tested it out, it’s perfect.

Until I saw this code I thought it was impossible to use face.area on scaled Components and we had to apply a transformation to the individual points. You proved me wrong! Thanks.

1 Like

@somtum, please use ```ruby as your opening code line, so the code is lexed correctly as Ruby code.

It appears like this when you add the language name …

# Get determinant of 3x3 matrix a
def determinant(a)
  determ = a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]
  determ
end

# Get unit normal vector of plane defined by points a, b, and c
def unit_normal(a, b, c)
  x = determinant([[1, a[1], a[2]], [1, b[1], b[2]],
                   [1, c[1], c[2]]])
  y = determinant([[a[0], 1, a[2]],
                   [b[0], 1, b[2]],
                   [c[0], 1, c[2]]])
  z = determinant([[a[0], a[1], 1],
                   [b[0], b[1], 1],
                   [c[0], c[1], 1]])
  magnitude = (x ** 2 + y ** 2 + z ** 2) ** 0.5
  normal = [x / magnitude, y / magnitude, z / magnitude]
  normal
end

def polygon_area(poly)
  if poly.length < 3
    return 0
  end

  total = [0, 0, 0]
  poly.each_with_index do |vertex, index|
    v1 = Geom::Vector3d.new(vertex)
    v2 = Geom::Vector3d.new(poly[index - 1])

    prod = v1.cross(v2)
    total[0] += prod[0]
    total[1] += prod[1]
    total[2] += prod[2]
  end

  # General formula for area of polygon in 3D space
  sum_of_crosses_vector = Geom::Vector3d.new(total)
  unit_normal_vector = Geom::Vector3d.new(unit_normal(poly[0], poly[1], poly[2]))
  double_area = sum_of_crosses_vector.dot(unit_normal_vector)
  area = (double_area / 2).abs
  area
end

(Off-topic) Alternate indents for method calls, using unit_normal method as an example ... (click to view)

Basically all style guides encourage only 2 space indents, so indenting all the way (many spaces) over to a method call’s first argument is poor form. (But I’ve even seen some of the core Ruby code that does use these “giant” indents.)

Here is an example of adhering to the 2 space indent rule:

# Get unit normal vector of plane defined by points a, b, and c
def unit_normal(a, b, c)
  x = determinant([
    [1, a[1], a[2]],
    [1, b[1], b[2]],
    [1, c[1], c[2]]
  ])
  y = determinant([
    [a[0], 1, a[2]],
    [b[0], 1, b[2]],
    [c[0], 1, c[2]]
  ])
  z = determinant([
    [a[0], a[1], 1],
    [b[0], b[1], 1],
    [c[0], c[1], 1]
  ])
  magnitude = (x ** 2 + y ** 2 + z ** 2) ** 0.5
  normal = [x / magnitude, y / magnitude, z / magnitude]
  normal
end

Some might say that even this example is wrong, that the outer array should be the first indent and the inner array one indent more like:

  x = determinant(
    [
      [1, a[1], a[2]],
      [1, b[1], b[2]],
      [1, c[1], c[2]]
    ]
  )

Yea, I do this but only if it is a much more complex or very large array.

1 Like

Yes, it appears I was having some problems with my formatter. Do you have a preference of rufo or rubocop?

I’ve just started using Rubocop. You can configure the Array indentation by adding this to your rubocop.yml.

Layout/IndentArray:
  EnforcedStyle: consistent
1 Like