Function Reference: 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, but for the last point of a closed curve that repeats the first, which is dropped. P is an N-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 the distance abs (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 by more than the radius of curvature at a point, towards its centre, folds the curve through that centre, and the criterion is simply that abs (D) must not exceed 1 / K wherever the curvature K turns towards 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 10^{-9} 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 10^{-4} 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 (geom.Polyline (P, 'Closed', true));
 D.Colour = 'red';
 D = D.polyline (geom.Polyline (geom.curveoffset (P, 4, true), ...
                                'Closed', true));
 D.Colour = 'blue';
 D = D.polyline (geom.Polyline (geom.curveoffset (P, -4, true), ...
                                'Closed', true));
 plot (D);
 title ('a lobed profile offset 4 mm in (red) and out (blue)');
plotted figure

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.