Create an array of points from a face


#1

In my ruby API, I am trying to produce an array of points from the selected face. Basically I need a grid of points 1 foot apart vertically and horizontally. Ive tried ‘model.selection[0].mesh.points’ but that only gets me the corners. Any help would be greatly appreciated


Move 1.5" along edge from vertex?
#2

This returns an array of Geom::Point3d objects which represent the position of each vertex of the face.
It’s the same as face.vertices.map { |vertex| vertex.position }.

I’m not sure I exactly understand what you are trying to do.

So you are not looking for the vertices of a face. But a set of points across the surface boundary of a face? What faces are we talking about here? Plain squared - or any shape (irregular, with holes etc…)?

Could you post an image of the desired result you are looking for?


#3

As thomthom said - there are methods to get all of the points used by a face’s vertices.

If you want to apply a grid of points over the face you need to first find a few things.
bb=face.bounds
from that
min=bb.min
max=bb.max
Now iterate a test point.
test_point=min.clone
Set up two nested ‘do’ loops [or { } blocks] to step through the possible x values in 12" increments, and y the same.
The x will start at min.x and the y at min.y
The x will stop at max.x, the y at max.y
To get the plane=face.plane
[We’ll assume the face is not vertical etc]
Within a ‘y’ loop…
Doing the ‘x’ values, each time set
x=x.project_to_plane(plane)
test_point.x=x
Use face.classify_point(test_point) to see if it’s on the face, an edge or vertex - http://www.sketchup.com/intl/en/developer/docs/ourdoc/face#classify_point
1+2+4 == 7 ?
If it’s on the face do entities.add_cpoint(test_point)
then increment
x+=12
in that loop test for getting to max and also reset test_point
if x >= max.x
test_point=min.clone
break
end
Now increment the y loop and reset the test_point.y in a similar way…
Easier to write in code than to write the explanation…


#4

Thomthom,thanks for the reply, the shape could be any shape really, Im attaching a picture of the general idea (each arrow represents a point value Id like to receieve)


#5

Thanks - much clearer. I think you need to go with something like what TIG describe - however, I’m not sure if you get correct results by projecting the points. I think TIGs current code would create points according to the global model axes - when projected down the distance between the points on the face will be at different distance from each other. If you want the points to form a grid local to the plane of the face you need to compute the points and transform by the normal of the face - then check face.classify_point.


#6

Thanks for your responses guys, very helpful.

Thomthom, do you think you could go into a little more detail about this? Im very new to both Sketchup and Ruby.

specifically transforming by the normal of the face.

would be greatly appreciated, Thanks!


#7

So the idea here is that you take all the vertices of the face, and transform them such that their coordinates are local to ORIGIN flat on the ground. This makes it easy to compute the grid points - which you then transform back to the world position of the face.

model = Sketchup.active_model
face = model.selection.first

# Generate a transformation that will convert the face vertices to ORIGIN flat
# on the ground.
transform_local_to_face = Geom::Transformation.new(face.bounds.min, face.normal).inverse
local_points = face.mesh.transform!(transform_local_to_face).points
# To visualize what is going on uncomment the next line:
#local_points.each { |point| model.active_entities.add_cpoint(point) }

# Now get the width and height of their boundary so it can be used to determine
# how many grid points we need.
bounds = Geom::BoundingBox.new
bounds.add(local_points)

# We need to have a size for the grid we want. For this example it's picked
# relative to the size of the face.
grid_size = [bounds.width, bounds.height].max / 10.0

# Now create the point grid.
grid = []
transform_to_world = transform_local_to_face.inverse
0.step(bounds.width, grid_size).each { |x|
  0.step(bounds.height, grid_size).each { |y|
    local_point = Geom::Point3d.new(x, y, 0)
    # This point needs to be transformed into the world position of the face.
    world_point = local_point.transform(transform_to_world)
    # Push the point to the grid array.
    grid << world_point
  }
}

# The set of points are now distrobuted on the plane of the face. Next they must
# be filtered out such that only those within the boundary of the face remains.
points_on_face = grid.select { |point|
  result = face.classify_point(point)
  # Note that if you also want points that are on the edges and vertices of the
  # face you need to also check for Sketchup::Face::PointOnVertex and
  # Sketchup::Face::PointOnEdge.
  result == Sketchup::Face::PointInside
}

# The resulting set should be within the boundary of the face.
model.start_operation('Face Grid', true)
points_on_face.each { |point| model.active_entities.add_cpoint(point) }
model.commit_operation


#8

Just to illustrate what is going on:

# Generate a transformation that will convert the face vertices to ORIGIN flat
# on the ground.
transform_local_to_face = Geom::Transformation.new(face.bounds.min, face.normal).inverse
local_points = face.mesh.transform!(transform_local_to_face).points

(The face you see is just illustrative - we only transform a set of points derived from the vertex position of the face.)


# Now get the width and height of their boundary so it can be used to determine
# how many grid points we need.
bounds = Geom::BoundingBox.new
bounds.add(local_points)


# We need to have a size for the grid we want. For this example it's picked
# relative to the size of the face.
grid_size = [bounds.width, bounds.height].max / 10.0

# Now create the point grid.
grid = []
transform_to_world = transform_local_to_face.inverse
0.step(bounds.width, grid_size).each { |x|
  0.step(bounds.height, grid_size).each { |y|
    local_point = Geom::Point3d.new(x, y, 0)
    # This point needs to be transformed into the world position of the face.
    world_point = local_point.transform(transform_to_world)
    # Push the point to the grid array.
    grid << world_point
  }
}

Note that the last image illustrate the last block minus the .transform(transform_to_world) part. When we add that the points transform into world space:


# The set of points are now distrobuted on the plane of the face. Next they must
# be filtered out such that only those within the boundary of the face remains.
points_on_face = grid.select { |point|
  result = face.classify_point(point)
  # Note that if you also want points that are on the edges and vertices of the
  # face you need to also check for Sketchup::Face::PointOnVertex and
  # Sketchup::Face::PointOnEdge.
  result == Sketchup::Face::PointInside
}

# The resulting set should be within the boundary of the face.
model.start_operation('Face Grid', true)
points_on_face.each { |point| model.active_entities.add_cpoint(point) }
model.commit_operation


#9

Thank you very much Thomthom, this is TREMENDOUSLY helpful!


#10

Have you assumed that the transformed face always belongs to the positive quadrant (x>0, y>0)? If face.normal.z is negative then the transformed face will be in the x<0, y>0 quadrant and no point will pass the test.

I have modified the loop as follows:

bounds.min.x.step(bounds.max.x, grid_size).each { |x|
  bounds.min.y.step(bounds.max.y, grid_size).each { |y|

and it seems to work for any orientation. I have not tested all the possible edge cases though.


#11

Yea, I didn’t go in depth to test all edge cases. Just assumed a slanted face with facing upwards. There is probably issues if the face is completely vertical as well.


#12

Interesting use of the Transformation constructor of the type “point, axis”:

transform_local_to_face = Geom::Transformation.new( 
                                  face.bounds.min, face.normal).inverse

I can see that you could create a local transformation coordinate system for the face similar to a grouped objects local transformation, and the local “zaxis” can be parallel to the face normal. I’m not fully understanding it though.


#13

It is just for ease of calculating the width and height of the face - by using Geom::BoundingBox. That class however is always aligned to the world axes.
If you obtained the width and height of the face differently you could skip that part and go straight to generating the grid points.


#14

This method of creating a transform from (point, vector) is tremendously useful, but very difficult to discover from the API documentation. It talks about (point), (point, axis, angle) and (vector) but no (point, vector). I spent a week last summer developing a complex method that could have been a single line. (I didn’t guess given all the other places in the API where vectors are specified that here it would use axis as a synonym.)

So here’s my question to thomthom:
The transform Geom::Transformation.new(point, face.normal) puts the face parallel to the XY plane with the front of the face pointing up and the back pointing down. The orientation follows a heads up rule where the Z slope of the face (if any) is transformed to be parallel to the Y axis. What’s the easiest way to ensure that the visible side of the face from the current viewpoint is transformed to face up? (Again, I’ve already solved this probably very inefficiently, but there might be a one line function that I’m not smart enough to see.)

Thanks


#15

I’m not sure we can ever expect to see detailed examples of transformation usage in the API documention. Things like coordinate systems, points, vectors and planes are taught in the school system, but transformations are not. I took a matrices course in university, but it did not get into the 3D graphics side of things. University courses specifically for them are very theoretical. You just need to be doing this stuff as long as @thomthom. I did manage to do something similar with the position of the constructor at the ORIGIN instead of “face.bounds.min”. It provided a transformation that rotated the face’s plane to be parallel with the X-Y plane, but it doesn’t translate it down onto the X-Y plane.


#16

http://www.sketchup.com/intl/en/developer/docs/ourdoc/transformation#new
The docs use zaxis as argument name to indicate it’s purpose:
Geom::Transformation.new(origin, zaxis)

There are new docs on their way that will make it clearer the type of argument in addition to its name.

Feed Transformation.new(point, vector) the normal of the face. The direction of the vector becomes the new “up”.


#17

Yea, it’s probably enough - as you only need Geom::BoundingBox to give you the width and height. You could probably get away by not doing the transformating and instead calculate the width and height in the plane of the face directly, but you’d probably have to do custom calculations and if you do that in Ruby you will probably be slower than using the API methods that’s written in C. There are potential for precision errors though - if that’s of a concern and performance you might have to drop into C yourself to crunch the numbers. In most cases once can get away by wielding the API methods.


#18

Your answered: “Feed Transformation.new(point, vector) the normal of the face. The direction of the vector becomes the new “up”.”

You lost me there, so I’ll rephrase the question:
I want the VISIBLE side of the face (which might be the back) to be facing up after the transform. How do I easily determine which side of the face is visible from the current eyepoint. If its the back side I’ll use face,normal.reverse.


#19

compute the dot product of the camera’s direction and the face’s normal. If the result is positive, you are looking at the back, if negative you are looking at the front, and if zero you are looking at the edge.


#20

Ah - I’m used to 3d applications normally not displaying the backside. So i read that as you where looking for the front side of a face. SketchUp is rather unique in this respect that it shows the backside by default.