geom.curveoffset
drafting: Q = geom.curveoffset (P, D)
drafting: Q = geom.curveoffset (P, D, CLOSED)
drafting: [Q, MARGIN] = geom.curveoffset (…)
Equidistant offset of a smooth sampled curve.
Q = geom.curveoffset (P, D) moves every point of the
curve P a distance D along the curve’s own normal and returns the
result Q, with one point per point of P. P is an
-by-2 matrix of points and D is a real scalar in the units of
P, so millimetres by package convention.
Q = geom.curveoffset (P, D, CLOSED) treats the
curve as closed when CLOSED is true. CLOSED defaults to false.
A positive D offsets inward on a closed curve, shrinking it,
whichever way round its points run; on an open curve, which has no inside, a
positive D offsets to the left of the direction of travel. A
negative D reverses this, and D = 0 returns P
unchanged.
[Q, MARGIN] = geom.curveoffset (…) additionally
returns how much further the curve could have been offset before the result
degenerated. MARGIN is the smallest radius of curvature on the side
being offset towards, less D; it is Inf for a curve that never
turns that way. A design that must not approach the limit can test
MARGIN rather than wait for the error.
Validity is decided by a criterion, not detected afterwards.
Offsetting inward by more than the radius of curvature at a point folds the
curve through its own centre of curvature, and the criterion is simply that
D must not exceed 1 / K wherever the curvature K is
positive on the offset side. This is the undercut condition of a cycloidal
disc, and of any roller running in a groove. Violating it raises an error
rather than returning a folded curve.
Offsetting exactly to the centre of curvature is admitted: it yields a cusp, which touches rather than folds. That boundary is tested to a relative tolerance of of the limiting radius, because a three-point curvature estimate loses precision as the sampling tightens and cannot resolve the boundary exactly. The tolerance is far below any distance a manufactured part could care about, and a real violation exceeds it by orders of magnitude.
Accuracy, and when not to use this. The normal is estimated from each point’s two neighbours, weighted by the chord lengths so that the estimate stays second order even where the spacing changes abruptly, as it does at every refinement boundary of an adaptively sampled curve. On points taken from a circle the offset is exact to rounding at any spacing. On a curve whose curvature varies sharply the residual is a few microns at a chordal tolerance of mm and falls with the sampling.
Where the offset curve has a closed form — as a cycloidal disc profile does — evaluate that instead and keep this function to check it. A few microns is a fraction of a wire-EDM tolerance, not a comfortable margin inside it.
A polygon is offset by passing its vertices. With one point per corner each normal is the angle bisector, so a closed outline of straight edges — rectilinear or not — offsets exactly, giving the mitred result. Do not pass a dense sampling of the same outline: a corner resolved by sampling has a vanishing radius of curvature, and the criterion above rejects it as an undercut for an offset the polygon absorbs without difficulty. Same shape, different representation, opposite answer.
For a rectilinear outline geom.offset is the better route: it tests
exactly whether the outline can absorb the offset at all, which a criterion
evaluated point by point cannot do.
The criterion is local. It guarantees that no neighbourhood folds
on itself, which is what makes an offset well defined point by point. It
does not guarantee that two distant parts of the curve stay apart: a shape
with a narrow neck can pinch closed under an offset that every point accepts.
Where that is possible, check the result with geom.selfintersects.
See also: geom.curvature, geom.selfintersects, geom.offset
Source Code: geom.curveoffset
An equidistant offset moves every point along the curve's own normal. On a closed curve a positive distance goes inward, whichever way round the points happen to run.
t = linspace (0, 2*pi, 241)(1:240)';
P = (30 + 4 * cos (7 * t)) .* [cos(t), sin(t)];
D = draw.Drawing ().polyline (P, true);
D.Colour = 'red';
D = D.polyline (geom.curveoffset (P, 4, true), true);
D.Colour = 'blue';
D = D.polyline (geom.curveoffset (P, -4, true), true);
plot (D);
title ('a lobed profile offset 4 mm in (red) and out (blue)');
The margin says how much further the curve could be offset before it folds through its own centre of curvature. Exceeding it raises rather than returning a folded curve that looks plausible and cannot be cut.
t = linspace (0, 2*pi, 241)(1:240)';
P = (30 + 4 * cos (7 * t)) .* [cos(t), sin(t)];
[Q, MARGIN] = geom.curveoffset (P, 4, true);
printf ('4 mm in, with %.2f mm still in hand\n', MARGIN);
4 mm in, with 1.06 mm still in hand
try geom.curveoffset (P, 4 + MARGIN + 1, true); catch err disp (err.message); end_try_catch
geom.curveoffset: D exceeds the radius of curvature by 1; the offset would fold through its own centre of curvature.