Paths
A path is the tube’s centreline. There are four ways to describe one, and all of
them produce the same thing: a spine of exact lines, arcs and sampled chains
(reference/ir.md). Everything downstream sees only the
spine, so the frontends can be mixed freely in one model.
| frontend | what it is for |
|---|---|
ut_polyline(pts, r=) |
waypoints plus fillet radii |
ut_turtle([...]) |
a bender program: feed, bend, roll |
ut_curve(f) |
a parametric centreline: helices, coils, generated routes |
ut_spine([...]) |
the spine itself, built from segments |
ut_polyline([[0,0,0], [50,0,0], [50,50,0]], r = 15)
ut_turtle([["feed", 60], ["bend", 90, "r", 24], ["feed", 40],
["roll", 90], ["bend", 45, "r", 24], ["feed", 30]])
ut_curve(function(t) [80*cos(360*t), 80*sin(360*t), 60*t], n = 180)
ut_spine([ ut_line([0,0,0], [35,0,0]),
ut_arc_from(c = [35,15,0], from = [35,0,0], to = [50,15,0], axis = [0,0,1]),
ut_line([50,15,0], [50,50,0]) ])
Choosing one
- Known absolute coordinates?
ut_polyline. - Describing how the tube is bent rather than where it goes?
ut_turtle. It is how a CNC tube bender is programmed. - A formula?
ut_curve. - Anything else — a solver’s output, surveyed points, another library —
build a spine directly with
ut_spine.
Every frontend also takes frame, normal and twist; see
frames.md.
ut_polyline — waypoints and radii
ut_polyline(pts, r = 0, frame = "transport", normal = undef, twist = 0)
pts is a list of 3D points. Each interior waypoint is rounded by an arc tangent
to both legs. r is one of:
- a number, applied to every interior waypoint;
- a list with one entry per waypoint (the two end entries are ignored);
- a list with one entry per interior waypoint.
A collinear waypoint adds no arc. Two bends that are exactly tangent leave no straight between them, which is fine.
These inputs are errors, each naming the waypoint or leg:
| input | why |
|---|---|
| a turning waypoint with radius 0 | a sharp corner cannot be swept (ADR 0004). r defaults to 0, so a path with a corner needs an explicit radius. |
| radii that do not fit on a leg | see below |
| a 180° reversal | a path cannot double back on itself |
| two identical consecutive waypoints | the leg has no direction |
| 2D points | every waypoint must be [x, y, z] |
closed = true |
closed paths go through ut_spine |
Radii that do not fit are an error, never scaled to fit. A leg must be at
least as long as the two tangent lengths that trim it, r * tan(angle/2) at each
end. Shrinking the radius until it fits looks forgiving and is the opposite: it
hides the problem at exactly the input that most needs a message. On a doubleback
it shrinks the radius to exactly zero, which is a sharp corner and no tube at
all.
ut_turtle — a bender program
ut_turtle(cmds, start = [0,0,0], dir = [1,0,0], up = [0,0,1],
frame = "transport", normal = undef, twist = 0)
| command | effect |
|---|---|
["feed", d] |
straight, d mm along the current heading |
["bend", ang, "r", r] or ["bend", ang, r] |
bend by ang degrees at radius r, toward up. A negative angle bends away from up. |
["roll", ang] |
rotate up about the heading |
up must be perpendicular to dir. A bend over 180° is split into equal arcs
for you. An unknown command is an error.
roll emits no geometry. It turns the turtle, which changes the plane of every
later bend and therefore the shape of the path. It does not twist the tube, just
as rotating the workpiece in a bender does not; for twist see
frames.md.
ut_curve — a parametric centreline
ut_curve(f, t = [0, 1], n = 100, dfdt = undef,
frame = "transport", normal = undef, twist = 0)
f is a function literal returning [x, y, z]. The curve is sampled at n + 1
values of t, and coincident consecutive samples are dropped.
Tangents come from finite differences, second order in the interior and at both
ends. Supply dfdt to make them exact.
Foot-gun. Only the direction of
dfdtis used, but the relative size of its components matters, and OpenSCAD’s trig is in degrees:d/dt cos(360*k*t) = -2*PI*k * sin(360*k*t). Dropping the2*PIleaves a helix’s tangents about 6° out at every sample while looking plausible. When in doubt, omitdfdt.
Known issue (#36). A
dfdtthat is the zero vector at a sample — an eased parametrisation stops at its ends — aborts with across()NaN warning that namesut_mathrather than your curve.
What a sampled path costs
ut_curve produces one sampled segment, which gives up things arcs provide:
arc-based (ut_polyline, ut_turtle) |
sampled (ut_curve) |
|
|---|---|---|
| arclength | exact | chord sum, slightly under |
| frame transport | exact | discrete, second order |
| minimum bend radius check | exact | estimated from three-point curvature |
| resolution | follows $fa/$fs |
fixed by n |
Measured on a 2-turn helix at n=160: the length is 0.025% under the analytic
value, and interior tangents agree with the analytic tangent to 0.006°. The end
tangents, which are where port frames come from, without dfdt:
| n | end-tangent error |
|---|---|
| 40 | 0.460° |
| 160 | 0.0141° |
| 320 | 0.0032° |
Two of these deserve attention:
- The bend-radius check fails open. The estimate is accurate (11.014 against an analytic 11.013 on a helix) but over-reads on fast-varying curves, so a marginal self-intersection can slip through. Leave margin.
$faand$fsdo nothing. An n=160 curve has 161 stations at any setting, where an arc path measured 7 at$fa=24and 183 at$fa=0.5. Choosenfor the finest resolution you need.
Fitting arcs to the samples would restore all four. It is prototyped in
spikes/biarc_fitter.scad and deferred (#17).
ut_spine — segments directly
ut_spine(segs, closed = false, opts = [])
| constructor | segment |
|---|---|
ut_line(p0, p1) |
a straight |
ut_arc(c, u, v, r, ang) |
an arc about c, starting at c + r*u, sweeping ang degrees toward v. u and v are orthonormal; 0 < ang <= 180. |
ut_arc_from(c, from, to, axis) |
the same arc, from its centre, endpoints and plane normal |
ut_sampled(pts, tans) |
a sampled chain with one tangent per point |
ut_spine asserts that each segment ends where the next begins and that the
tangent is continuous across every joint. A sharp corner is an error here too.
closed = true is the only way to make a closed path. A closed tube’s bore is a
sealed cavity until something breaches it.
Writing your own frontend
A frontend is a function that returns ut_spine([...]) and never touches frames:
function my_frontend(...) =
ut_spine([ ut_line(a, b), ut_arc(c, u, v, r, ang), ... ],
false,
[["frame", "transport"]]);
Two jobs belong to the frontend rather than the spine:
- Split bends over 180°. A single arc is capped at 180° so its tangent sign is never ambiguous.
- Drop coincident samples before calling
ut_sampled.
If you build lists by slicing, guard the empty case: OpenSCAD silently reverses
a descending range, so [0:-1] is [-1, 0], not empty.
Not shipped
Arc-line route (G-code / SVG-A semantics) |
Later. The turtle covers most of it, and in 3D two endpoints and a radius do not fix the arc’s plane. |
| Bezier / Catmull-Rom | Later, after arc fitting. A continuously varying radius suits a part with a minimum bend radius badly. |
| Port-to-port routing | Later. Its main use needs terminations, which are a separate library (ADR 0005). |
| SVG path strings | Rejected. SVG is 2D, and OpenSCAD has no string parsing worth the name. Preprocess outside and feed ut_spine. |
| Waypoint + tangent list | Rejected. It is the spine with information removed. |
More frontends are tracked in #23.