Finding All Exterior Faces

Hey there.

I’m trying to draw a 3D helix. Specifically, the helix is a staircase stringer. So it has width. Part of the script that draws this is attached below:

step = step-(1.0/@detail)
entities.add_line(Geom::Point3d.new(-p3, p4, rise1-stringerz+(@rise/@detail)),Geom::Point3d.new(-p1, p2, rise1-stringerz+(@rise/@detail)))
rise1 = rise1-(@rise/@detail)
rise2 = rise2-(@rise/@detail)
p1 = stringe1
p2 = stringe2
p3 = stringe3
p4 = stringe4
end
entities.add_line(Geom::Point3d.new(-irad+@stringt, 0, rise1),Geom::Point3d.new(-irad+@stringt, 0, rise2-stringerz))
entities.add_line(Geom::Point3d.new(-lp3, lp4, rise2-stringerz+(@rise/@detail)), Geom::Point3d.new(-irad, 0, rise1-stringerz+(@rise/@detail)))
entities.add_line(Geom::Point3d.new(-irad, 0, rise1-stringerz+(@rise/@detail)), Geom::Point3d.new(-lp1, lp2, rise2-stringerz+(@rise/@detail)))
entities.add_line(Geom::Point3d.new(-irad+@stringt, 0, rise1-stringerz+(@rise/@detail)), Geom::Point3d.new(-lp3, lp4, rise2-stringerz+(@rise/@detail)))
model.active_layer = threed
add = group.entities.add_instance(new_comp_def, IDENTITY)

Please note, I have referred to this as a ‘script’. I use the Ruby Code Editor to automate my drawings, and this is not an extension.

Anyways, at the end of the code, I have a wireframe stringer. I’m having difficulty with find the exact code I need to create the faces on this. I’ve tried a variety of different things already, including progressively drawing the faces as the lines are drawn, but there’s issues with everything I’ve tried so far. I know the answer lies in grepping the lines and the selection tools, but the exact answer escapes me.

Using Eneroth’s Face Creator seems like a simple solution, but it’s not ideal as it creates too many faces (finding all the internal ones as well), which slows things down and you can’t soften the lines.

Thanks.

We would really like to help you with details, but your snippet does not help us help you:

(Snippet issues ... click to expand)
  • Excessive indentation (beyond 2 space per level)
  • Line length more than 80 characters causing horizontal scroll to read each line
  • Numerous variables in the snippet whose purpose and value must be guessed at
  • No comments within the code telling readers what the code is doing at strategic places or what variables represent

So, generally speaking, you should create a empty group with a construction point at it’s origin, then within it draw a wireframe spiral path from the origin. Draw a face at the origin representing the stringer’s profile and use face.followme(path) to extrude the 3D stringer. (Delete the cpoint afterward if no longer needed. It is just used to prevent the group from getting garbage collected.)

It has been suggested that the profile face should be perpendicular to the path before the followme operation. (You could always trim the bottom and top of the stringer later with boolean operations. But these are Pro only methods. If still running SU Make, you’ll need to use Eneroth’s Boolean Tools extension.)

1 Like

Well. :exploding_head:

That is certainly a paradigm shift in my thinking. Certainly a lot less code to write. Thank you.

# Draw front cross section face of stringer
entities.add_line(Geom::Point3d.new(-irad, 0, rise1-stringerz+(@rise/@detail)),Geom::Point3d.new(-irad+@stringt, 0, rise1-stringerz+(@rise/@detail)))
entities.add_line(Geom::Point3d.new(-irad, 0, rise1),Geom::Point3d.new(-irad+@stringt, 0, rise1))
entities.add_line(Geom::Point3d.new(-irad, 0, rise1),Geom::Point3d.new(-irad, 0, rise2-stringerz))
closer = entities.add_line(Geom::Point3d.new(-irad+@stringt, 0, rise1),Geom::Point3d.new(-irad+@stringt, 0, rise2-stringerz))

# Find face of stringer
face = closer.find_faces

# Draw helical path
for t in 0...(@numrise-1)*4
  angle = 0.0
  string = angle+(@angle/(@numrise-1)*step) #/
  stringe1 = Math::sin(string.degrees)*irad/Math::tan(string.degrees) #/
  stringe2 = Math::sin(string.degrees)*stringe1/Math::cos(string.degrees) #/
  stringf = angle+(@angle/(@numrise-1)*step) #/
  stringf1 = Math::sin(stringf.degrees)*irad/Math::tan(stringf.degrees) #/
  stringf2 = Math::sin(stringf.degrees)*stringf1/Math::cos(stringf.degrees) #/
  edge = entities.add_line(Geom::Point3d.new(-p1, p2, rise1),Geom::Point3d.new(-stringf1, stringf2, rise2))
  step = step+(1.0/@detail) #/
  rise1 = rise1+(@rise/@detail) #/          
  rise2 = rise2+(@rise/@detail) #/
  p1 = stringe1
  p2 = stringe2
end

# Follow helical path
face.followme(edge)
model.active_layer = threed
add = group.entities.add_instance(new_comp_def, IDENTITY)

So that simplifies the code a lot! However, now I’m getting an error:

undefined method ‘followme’ for 1:Integer (Line 1159)

The solution is probably as simple as it obvious to everyone except me.

Your error is that Edge#find_faces returns the number of faces created, not the faces themselves. You need to first query edge.faces to see what pre-exists, then again after find_faces, and get the new face(s) from the difference.

Ah. Learning every minute (but not enough yet!)

Like this?

# Draw front cross section face of stringer
entities.add_line(Geom::Point3d.new(-irad, 0, rise1-stringerz+(@rise/@detail)),Geom::Point3d.new(-irad+@stringt, 0, rise1-stringerz+(@rise/@detail)))
entities.add_line(Geom::Point3d.new(-irad, 0, rise1),Geom::Point3d.new(-irad+@stringt, 0, rise1))
entities.add_line(Geom::Point3d.new(-irad, 0, rise1),Geom::Point3d.new(-irad, 0, rise2-stringerz))
closer = entities.add_line(Geom::Point3d.new(-irad+@stringt, 0, rise1),Geom::Point3d.new(-irad+@stringt, 0, rise2-stringerz))

# Find face of stringer
faces_before = entities.grep(Sketchup::Face)
closer.find_faces
new_faces = entities.grep(Sketchup::Face)-faces_before

# Draw helical path
for t in 0...(@numrise-1)*4
  angle = 0.0
  string = angle+(@angle/(@numrise-1)*step) #/
  stringe1 = Math::sin(string.degrees)*irad/Math::tan(string.degrees) #/
  stringe2 = Math::sin(string.degrees)*stringe1/Math::cos(string.degrees) #/
  stringf = angle+(@angle/(@numrise-1)*step) #/
  stringf1 = Math::sin(stringf.degrees)*irad/Math::tan(stringf.degrees) #/
  stringf2 = Math::sin(stringf.degrees)*stringf1/Math::cos(stringf.degrees) #/
  edge = entities.add_line(Geom::Point3d.new(-p1, p2, rise1),Geom::Point3d.new(-stringf1, stringf2, rise2))
  step = step+(1.0/@detail) #/
  rise1 = rise1+(@rise/@detail) #/          
  rise2 = rise2+(@rise/@detail) #/
  p1 = stringe1
  p2 = stringe2
end

# Follow helical path
new_faces.followme(edge)
model.active_layer = threed
add = group.entities.add_instance(new_comp_def, IDENTITY)

Now I’m getting an undefined method for Deleted Entity. So very close.

Perhaps instead of using multiple #add_line calls, use the points with one call to #add_face which should return the reference for your profile face ?

At what line are you getting that error? The error message should identify the offending variable. My guess would be new_comp_def.

A Deleted Entity is a stub that SketchUp hangs onto when an Entity is erased but there is still a Ruby variable holding a reference to it. The interpreter can’t get rid of the stub until the reference releases it, as every Ruby variable must refer to some object. So, the likely cause of the error is that some Entity still referenced by a variable in your code has been erased, either implicitly or explicitly.

One way you can run into an implicit erasure is if you create a Group with no contents. SketchUp detects empty Groups fairly quickly and erases them. Unlike for a Component, the Group’s ComponentDefinition is erased along with the instance. Since it wasn’t created or populated within the code excerpt you shared, I can’t tell where new_comp_def came from or what it may or may not contain. It is common practice to immediately add a placeholder Entity, often a ConstructionPoint, to a Group so as to prevent SketchUp from reaping the empty Group. A ConstructionPoint won’t interact with other geometry, so it is harmless and can be erased after you add some actual geometry to the Group.

The error is actually on new_faces call.

undefined method ‘followme’ for [#<Deleted Entity:0xa1eeeda8>]:Array (Line 1160)

Ah. Check what new_faces contains.

It should be an array, which would not have a #followme method defined.

Again …

Also the argument passed to #followme needs to be all of the edges of the path.

Yes, it’s an Array. Its first element is supposed to be the profile Face, I think. That’s why I asked to check what it contains. But anyway the call should be on its first element, not the Array itself. I don’t know why the code yields a deleted entity error, even with this mistake.

Instead of …

Do something like this …

x1 = -irad
x2 = x1 + @stringt
y = 0
z1 = rise1
z2 = rise2-stringerz
z3 = rise1-stringerz+(@rise/@detail)

# Create vertex points for front cross section face of stringer:
points = [
  [x1, y, z1],
  [x2, y, z1],
  [x2, y, z2],
  [x1, y, z2] # last point
]

# Create face of stringer:
face = entities.add_face(points)

However because of the way your code is written I’m having a difficult time figuring out what the points are for the profile face. Your code had 4 edges being drawn but there was 6 points involved.

Perhaps the edges never really closed a loop ?


Also, you should draw in one direction if drawing with edges. Either clockwise or counter-clockwise. This is called “winding order” and it controls the direction of the face’s normal vector.

You were drawing the right side edge first, then the bottom, then the left side. I could not figure out what the other edge(s) were.


The Ruby API allows simple arrays to act as vectors and points. (In other words on the C side they are passed as arrays of numerics, so most Ruby API methods will accept arrays of 3 member arrays.)
So you need not have such long argument lists with Geom::Point3d.new(expression1, exprssion2, expression3), … etc.

Ha, I got it working! Hurray for points!

I don’t think that this is the solution I’m looking for. I switched to points, as per Dan’s suggestion, and added my edges to an array, and now the #followme works. Unfortunately, the extrusion does not stay upright, which has always been a Sketchup limitation when working with inclined and twisting lines and Follow Me. The functionality I need is akin to the Upright Extrude extension, which I use all the time. Functionally, it evolved from drawing a single side of the stringer in full, and then using JointPushPull to widen it to my desired thickness.

I love how simple the code got, however. Thanks for that insight. Now to find time to go back and simplify everything else…

p1 = irad
p2 = 0.0
points = [ # Draw cross section of stringer
  [-irad+width, 0, rise1-stringerz+(@rise/@detail)], # Bottom inside
  [-irad, 0, rise1-stringerz+(@rise/@detail)], # Bottom outside
  [-irad, 0, rise1], # Top outside
  [-irad+width, 0, rise1] # Top inside
]

faces_before = entities.grep(Sketchup::Face)
face = entities.add_face(points) # Find face of stringer
new_faces = entities.grep(Sketchup::Face)-faces_before

helix = [] # Empty edge array
for t in 0...(@numrise-1)*4 # Draw helix
  angle = 0.0
  string = angle+(@angle/(@numrise-1)*step) 
  stringe1 = Math::sin(string.degrees)*irad/Math::tan(string.degrees) 
  stringe2 = Math::sin(string.degrees)*stringe1/Math::cos(string.degrees) 
  stringf = angle+(@angle/(@numrise-1)*step) 
  stringf1 = Math::sin(stringf.degrees)*irad/Math::tan(stringf.degrees) 
  stringf2 = Math::sin(stringf.degrees)*stringf1/Math::cos(stringf.degrees) 
  helix << entities.add_line(Geom::Point3d.new(-p1, p2, rise1),Geom::Point3d.new(-stringf1, stringf2, rise2))
  step = step+(1.0/@detail) 
  rise1 = rise1+(@rise/@detail)         
  rise2 = rise2+(@rise/@detail) 
  p1 = stringe1
  p2 = stringe2
end

face.followme(helix)
model.active_layer = threed
add = group.entities.add_instance(new_comp_def, IDENTITY)

Makes for a rather trippy staircase! Not sure I’d want to walk up it. :stuck_out_tongue:

Is there a way to find all the exterior faces on a wirecage?

Ah, I see what you mean. It is twisting like a “Fun House” staircase.

I didn’t think it would do that. Is the face perpendicular to the first helix segment?

Writing clear concise code with comments will help you when you come back to tweak it months from now.

Anytime you see multiple expressions that are the same, they can be set to a single reference to make the code easier to read and maintain.

Is the face perpendicular to the first helix segment?

No it is not. The face is the vertical cross section of the stringer. As in, say the height of the stringer is 16". Incline that on the angle of the staircase, and the vertical cross section of the 16" stringer becomes larger, say 18". The face is the 18" section. followme takes that vertical section, inclines it to the path, and then twists it along as it goes, without regard to the ‘up’ vector that the section represent. Sorry, bunch of terms getting confused and use haphazardly there.

I don’t think that would particularly help, either, as you’d have to define the ‘up’ vector for each segment, which (I think) is what the Upright Extrude extension does?

This is what the drawing currently looks like before applying the followme call:

This is what it looks like after applying follow me to one segment of the helix:

As you showed, follow-me projected the profile to be perpendicular to the start edge before doing the extrusion along that edge. That is how follow-me always operates when the profile is not perpendicular to the edge. However, because the path is a full 3-D curve, the algorithm follow-me uses is going to rotate the profile as it curves both upward and to the side, causing the twist you showed in your earlier image.

That is the reason Eneroth3 created the upright extrusion extension. You will need similar logic in your code. Her code says it is free to reuse, though you might want to consult her before copying it outright.

1 Like