Grepping for curves & Parametric horn generator

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 }

or shorter

edge_lists = ents.grep(Sketchup::Edge).select(&:curve).group_by(&:curve)

If all have the same curve, edge_lists must have length 1 and edge_lists.first gives these edges and edge_lists.first.curve that curve.

edge_lists.first returns [curve,[edge,…,edge]] while edge_lists[0] returns nil! Shouldn’t they be the same? What am I missing?

Thank you Aerilius for a very thoughtful and informative answer!

I have it working now thanks to you and some extra help from sdmitch, I will post the code up here when it’s fully done.

Finished the script, here it is -

################# 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

I think you’re wrong there sdmitch. If the selection is a curve, the first if statement fails and the second one is tested.

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.

1 Like

On second thought, I think you are right. My apologies.

1 Like

( brain fart. Happens all the time to me when not enough :coffee: )