Hi there, I’m trying to write a Ruby script that automatically completes the steps in this howto.

The idea is to create a hollow horn along an arbitrary path by 1. first doing a followme, then 2. locating the individual circular curves along the resultant extruded shape, and 3. scaling those down sequentially.

I’m having trouble with step 2 - when I do a curves = ent.grep(Sketchup::Curve), the result is an empty array. But if I grep for Edges, all the edges are present in the array including the ones that are part of the the curves I’m looking for. I have no idea where to go from here!

Here is my code. I am using it via the Ruby code editor extension by Alexander C. Schreyer:

print "-----------------"
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection
mod.start_operation('Create Horn', true)
if !sel.is_curve?
print "Please select a curve\n"
else
outerradius = 0.6
innerradius = 0.5
# Make inner circle
centerpoint = Geom::Point3d.new
vector = Geom::Vector3d.new 0,0,1
vector2 = vector.normalize!
circle = ent.add_circle centerpoint, vector2, innerradius
innerface = ent.add_face(circle)
# Make outer circle
circle = ent.add_circle centerpoint, vector2, outerradius
face = ent.add_face(circle)
# Delete inner face to make a doughnut shape
innerface.erase!
# Get selected curve as ordered array of edges
edges = sel[1].curve.edges
# Extrude along guide curve
face.followme(edges)
# Delete the guide curve
ent.erase_entities(edges)
# Search for curves created by followme
curves = ent.grep(Sketchup::Curve)
print curves # Empty array???
sel.add(curves) # Nothing selected...
mod.commit_operation
end

Curves are “virtual aggregate” entities. Since the curve’s edges are also real entities, the entities collection does not contain both because then there were duplicates. But every edge that is part of a curve is linked to a curve entity.

When exploring how to achieve something (that you are going to program in a script), it is better to go through it step by step instead of trying to evaluate and debug a huge block of code at once. You can explore and visualize this very easily with the autocompletion and highlighter in Ruby Console+. When you evaluate each line at once, you get the return value and can inspect what it is and verify that it is what you want, and explore what methods you can do on it (with the autocompleter).

So you grep for edges and filter those which have a curve (the same curve to be precise):

all_edges = ents.grep(Sketchup::Edge)
curve_edges = all_edges.select{ |edge| edge.curve }
# Creates arrays of items for which the code block returns the same value (same curve).
edge_lists = curve_edges.group_by{ |edge| edge.curve }

################# Hyperbolic Horn Generator ###################
#
# For loudspeaker horn design.
# Creates a hyperbolic horn along a selected curve.
# (Use "Weld" extension to join multiple curves together).
# The length of the horn is determined by the length of this curve
# Radius at the throat of the horn
throatradius = 100.mm
# Circumference at the open end of the horn
mouthcircumference = 8000.mm
# T factor.
t = 0.5
# T = 1 - Exponential horn
# 0<T<1 - Hyperbolic horn
# T->infinity - Conical horn
# Number of segments
numsegs = 8
################################################################
SKETCHUP_CONSOLE.clear
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection
# Check for bad input
if !sel.is_curve?
puts "Please select a curve to follow"
abort
elsif mouthcircumference <= 2*Math::PI*throatradius
puts "Mouth circumference too small!"
abort
end
mod.start_operation('Create Horn', true)
# Get selected curve as ordered array of edges
edges = sel[1].curve.edges
# Get length of selected curve
hornlength = sel[1].curve.length
# Calibrate rate of expansion to match mouth circumference with horn length
# https://www.wolframalpha.com/input/?i=C+%3D+2*pi*R*+cosh(L+%2F+d0)+%2B+t*sinh(L+%2F+d0)+solve+for+d0
pi = Math::PI
l = hornlength
c = mouthcircumference
r = throatradius
d0 = l / Math.acosh((Math.sqrt(c*c*r*r*t*t+4*pi*pi*r**4*t**4-4*pi*pi*r**4*t*t) - c*r) / (2*pi*r*r*(t*t - 1)))
# Hyperbolic horn equation, expressed as a radius
radiusfn = Proc.new {|d| throatradius * (Math.cosh(d.to_f / d0) + t*Math.sinh(d.to_f / d0))}
# Create shape to be extruded
# Get starting point and tangent vector of path
centerpoint = sel[1].curve.edges[0].start
vector = centerpoint.position.vector_to(sel[1].curve.edges[0].end)
vector2 = vector.normalize!
# Make inner circle
circle = ent.add_circle centerpoint, vector2, 1, numsegs
innerface = ent.add_face(circle)
# Make outer circle
circle = ent.add_circle centerpoint, vector2, 1.1, numsegs
face = ent.add_face(circle)
# Delete inner face to make a doughnut shape
innerface.erase!
# Extrude shape along selected guide curve
face.followme(edges)
# Delete the guide curve
ent.erase_entities(edges)
# Search for the circles created by followme
all_edges = ent.grep(Sketchup::Edge)
curve_edges = all_edges.select{ |edge| edge.curve }
edge_lists = curve_edges.group_by{ |edge| edge.curve }
# Prepare for iteration
dist = 0
bb = Geom::BoundingBox.new
edge_lists.first[1].each{|e|bb.add e.bounds}
prevpoint = bb.center;
# Iterate over circles, scaling them accordingly
edge_lists.each_pair{|curve,edges|
if curve.is_a?(Sketchup::Curve)
bb = Geom::BoundingBox.new
edges.each{|e|bb.add e.bounds}
center = bb.center; #todo wall thickness?
dist += center.distance(prevpoint)
trans = Geom::Transformation.scaling(center,radiusfn.call(dist))
ent.transform_entities(trans,curve)
prevpoint = center
else
p curve
end
}
mod.commit_operation

I think in the “check for bad input” logic, the elsif is never going to be checked. If the selection is not a curve, it aborts and, if it is a curve, it is ignored.

# Check for bad input
if !sel.is_curve?
puts "Please select a curve to follow"
abort
elsif mouthcircumference <= 2*Math::PI*throatradius
puts "Mouth circumference too small!"
abort
end

Need to make it two if statements

# Check for bad input
if !sel.is_curve?
puts "Please select a curve to follow"
abort
end
if mouthcircumference <= 2*Math::PI*throatradius
puts "Mouth circumference too small!"
abort
end

Ruby’s abort corresponds somewhat to exit which is supposed to quit the Ruby interpreter, but in SketchUp we are in a shared environment and it is not our responsibility to control the interpreter (the interpreter lives as long as the SketchUp process lives, much longer than an individual extension’s execution time). While exit has not really an effect in SketchUp, it is not best practice. I see that you used it because return and break only work in methods or loop procs and not in dynamic interpreter evaluation.

The more your script finalizes, the more would it be worth to consider wrapping it into a reusable method (def) and namespace (module). Readers of this forum are not aware that you are pasting and evaluating it in a dynamic Ruby console/editor, but they may just blindly copy-paste it into files that they intend to publish as “extensions”.

This line takes all edges from the model. It is better to limit the scope of a script to entities that were passed as parameters into the script or created by it because otherwise the script might do unexpected things. This line includes edges that were there before invoking the script (does the script assume the model is empty?) and the selected curve and the added circle. That means curve_edges does not only contain the created circles but also the selected curve and any preexisting curves.

But a BoundingBox’s center only then precisely coincides with a curve’s centerpoint if the curve is a full circle and has an even amount of segments.
It is better if you filter those edges whose curve is not just any type of Sketchup::Curve but a Sketchup::ArcCurve (API’s name for circles or parts of circles, I don’t know how to easily exclude those that are affine deformed circles). Then you can easily get the precise center point from the center method in the API.