%D \module %D [ file=mp-para.mpxl, %D version=2026.06.23, %D title=\CONTEXT\ \METAPOST\ graphics, %D subtitle=Parallel Paths, %D author=Hans Hagen & Mikael Sundqvist & The Internet, %D date=\currentdate, %D copyright={PRAGMA ADE \& \CONTEXT\ Development Team}] %C %C This module is part of the \CONTEXT\ macro||package and is %C therefore copyrighted by \PRAGMA. See mreadme.pdf for %C details. % vardef firstpoint p = message(p) ; point 0 of p enddef ; if known metafun_loaded_para : endinput ; fi ; newinternal boolean metafun_loaded_para ; metafun_loaded_para := true ; immutable metafun_loaded_para ; % Robust adaptive cubic Bezier offsets for MetaPost. % % Exact offsets of cubic Beziers are not cubic Beziers. This code therefore % approximates the offset by cubic pieces, using de Casteljau subdivision and % accepting a piece only after checking it against sampled normal-offset points. % There are various sources on the internet that one can query with a search engine % or an \LLM\ as we did here as part of an experiment to see how well these models % / frameworks actually perform when you want to get info about something like % this. Snippets of code oen can find on the web vary in quality and performance. % % Here are few resources one runs into from various angles: % % An offset spline approximation for plane cubic splines, % Reinhold Klass, 1983 % Cubic spline approximation of offset curves of planar cubic splines, % Manabu Sakai and Katsumasa Suenaga, 2001 % Offsets of Two-Dimensional Profiles, % Wayne Tiller and Eric G. Hanson, 1984 % A New Offsetting Algorithm Based On Tracing Technique, % Shi-Nine Yang and Ming-Liang Huang, 1993 % Fast Cubic Bezier curve offsetting, % Aurimas Gasilius % % Below is an implementation we arrived at after various experiments. The basics % were slapped together with Chat 5.5 (thanks to Lund) but we had to do quite a bit % of cleanup. Apart from curious code (likely a side effect of mapping onto % \METAPOST) we also took the opportunity to use some of the path manipulation % primitives in \LUAMETATEX. An interesting observation was that instead is using % subpath complex splitters were suggested. Actually one could do some conveniently % in \LUA\ (the pseudo arrays) and we did some prototyping but in the end didn't % want to waste time on it and stuck to primitive \METAPOST\ path manipulation. % After all it's not that this will be used much. Again our conclusion was that % because in the end the code is different one can best stick to using an \LLM\ % for gathering information and not so much for coding. In the end the code below % is still kind of ugly and not something to be proud of. Let's say: it works. % % tolerance is the maximum accepted sampled distance between an offset candidate % and the true normal-offset samples, in MetaPost units. newinternal mfun_parallel_tolerance ; mfun_parallel_tolerance := .20 ; % max_depth is the recursion limit for adaptive subdivision of one source cubic % segment. newinternal mfun_parallel_max_depth ; mfun_parallel_max_depth := 13 ; % samples is the number of interior sample points used when estimating the error of % one candidate offset cubic. newinternal mfun_parallel_samples ; mfun_parallel_samples := 5 ; % eps is the general geometric zero tolerance used for degenerate segments, % coincident endpoints, and nearly zero algebraic quantities. newinternal mfun_parallel_epsilon ; mfun_parallel_epsilon := 1/65536 ; % cusp_epsilon is the tolerance for deciding that 1 - dist * curvature is close % enough to zero that the true offset may have a cusp. newinternal mfun_parallel_cusp_epsilon ; mfun_parallel_cusp_epsilon := .060 ; % min_speed_factor is the smallest allowed absolute endpoint speed scale when the % candidate offset handles are computed from 1 - dist * curvature. newinternal mfun_parallel_min_speed_factor ; mfun_parallel_min_speed_factor := .002 ; % join_miter is the numeric code for using a miter join at a corner. % newinternal mfun_parallel_join_miter ; mfun_paralleljoin_miter := mitered ; % join_bevel is the numeric code for using a straight bevel join at a corner. % newinternal mfun_parallel_join_bevel ; mfun_parallel_join_bevel := beveled ; % join_mode selects the preferred corner join style before miter-length and % numerical tests are applied. newinternal mfun_parallel_join_mode ; mfun_parallel_join_mode := mitered ; % miter_limit is the maximum miter length as a multiple of the absolute offset % distance. newinternal mfun_parallel_miter_limit ; mfun_parallel_miter_limit := 8 ; % join_epsilon is the denominator tolerance for intersecting the two tangent rays % that define a miter join. newinternal mfun_parallel_join_epsilon ; mfun_parallel_join_epsilon := .00001 ; % smooth_epsilon is the cross-product tolerance for treating consecutive source % tangents as a smooth continuation rather than a corner. newinternal mfun_parallel_smooth_epsilon ; mfun_parallel_smooth_epsilon := .0005 ; % Return the first derivative of a single cubic Bezier segment. The input c is % expected to be a one-segment path, and t is the local parameter, normally in the % interval [0,1]. The result is a tangent vector in the same coordinate system as % the path. vardef mfun_parallel_cubic_derivative(expr c, t) = 3 * (1 - t) * (1 - t) * (firstpostcontrol c - firstpoint c) + 6 * (1 - t) * t * (lastprecontrol c - firstpostcontrol c) + 3 * t * t * (lastpoint c - lastprecontrol c) enddef ; % Return the second derivative of a single cubic Bezier segment. The input c is % again a one-segment path and t is its local parameter. This is used together with % the first derivative to compute signed curvature. vardef mfun_parallel_cubic_second_derivative(expr c, t) = 6 * (1 - t) * (lastprecontrol c - 2 * firstpostcontrol c + firstpoint c) + 6 * t * (lastpoint c - 2 * lastprecontrol c + firstpostcontrol c) enddef ; % Compute signed curvature for one cubic segment at local parameter t. Positive and % negative values distinguish the two turning directions of the oriented path. If % the derivative is numerically too small, the function returns zero to avoid % division by a nearly singular speed. vardef mfun_parallel_signed_curvature(expr c, t) = save d, dd, s ; pair d, dd ; numeric s ; d := mfun_parallel_cubic_derivative(c, t) ; dd := mfun_parallel_cubic_second_derivative(c, t) ; s := length d ; if s > mfun_parallel_epsilon : (d crossprod dd) / (s * s * s) else : 0 fi enddef ; % Reconstruct the current segment while iterating with "for i within p". The macro % has no explicit arguments: it reads MetaPost's path iteration variables % pathpoint, pathpostcontrol, deltaprecontrol, and deltapoint. The result is a % one-segment cubic path, even when the original segment was written as a straight % line. % vardef cubic_path_segment = % inlined % pathpoint .. controls pathpostcontrol and (deltaprecontrol 1) .. (deltapoint 1) % enddef ; % Measure the current iterated segment by the length of its cubic control polygon. % Like cubic_path_segment, this is meant to be called only inside a "for i within % p" loop. It is used to recognize zero-length closing segments and other % degenerate pieces. vardef cubic_path_segment_length = abs(pathpostcontrol - pathpoint ) + abs(deltaprecontrol 1 - pathpostcontrol ) + abs(deltapoint 1 - deltaprecontrol 1) enddef ; % Return a path with degenerate segments removed while preserving open or cyclic % status. The input is any MetaPost path; straight segments are kept as cubic % segments with coincident controls as supplied by MetaPost. This prevents an % explicit "point && cycle" style closing segment from being offset or sampled. vardef normalized_path(expr p) = for i within p : if i < pathlength if not cycle p : - 1 fi : if cubic_path_segment_length > mfun_parallel_epsilon : (pathpoint .. controls pathpostcontrol and (deltaprecontrol 1) .. (deltapoint 1)) & fi fi endfor if cycle p : cycle else : nocycle fi enddef ; % Test whether a cyclic path ends with an explicit zero-length closing segment. % This is a representation detail of inputs like "... (0,0) && cycle": the segment % is ignored for offset computation, but the final closing knot can be preserved in % the returned offset path for clearer point labels. vardef mfun_parallel_has_degenerate_cyclic_tail(expr p) = save found ; boolean found ; found := false ; if cycle p : for i within p : if i = pathlength - 1 : found := cubic_path_segment_length <= mfun_parallel_epsilon ; fi endfor fi found enddef ; % Return the last point of a path. This is a small readability helper for join % code, where the tail of an offset piece is repeatedly compared with the start of % the next piece. For cyclic paths this is MetaPost's point at length p. (inlined) % Move the first point of a path to new_start while preserving its outgoing tangent % handle by the same translation. The input p must have at least one segment. Later % segments are appended unchanged. vardef mfun_parallel_set_path_start(expr p, new_start) = new_start .. controls (firstpostcontrol p + new_start - firstpoint p) and (precontrol 1 of p) .. (point 1 of p) & subpath (1,length p) of p enddef ; % Move the last point of a path to new_end while preserving its incoming tangent % handle by the same translation. The input p must have at least one segment. % Earlier segments are appended unchanged. vardef mfun_parallel_set_path_end(expr p, new_end) = save lp ; numeric lp ; lp := length p ; subpath(0, lp - 1) of p & point (lp - 1) of p .. controls (postcontrol lp - 1 of p) and ((precontrol lp of p) + (new_end - point lp of p)) .. new_end enddef ; % Append path q after path p without creating duplicate knots when their % endpoints already match. Tiny numerical gaps are snapped by moving the start % of q; genuine gaps are joined by a straight connector. This keeps adaptive % subdivision boundaries from producing visible double points. vardef mfun_parallel_append_path_piece(expr p, q) = p if lastpoint p = firstpoint q : & q elseif abs(lastpoint p - firstpoint q) <= mfun_parallel_epsilon : & mfun_parallel_set_path_start(q, lastpoint p) else : -- firstpoint q & q fi enddef ; % Decide whether two source segments meet with a smooth tangent continuation. The % inputs are consecutive one-segment source cubics, not their offset pieces. A % smooth join can be assembled directly; a corner gets a miter or bevel join. vardef mfun_parallel_join_is_smooth(expr prev_src, next_src) = save incoming, outgoing ; pair incoming, outgoing ; incoming := lastunitdirection prev_src ; outgoing := firstunitdirection next_src ; ((incoming dotprod outgoing) > 0) and (abs(incoming crossprod outgoing) < mfun_parallel_smooth_epsilon) enddef ; % Compute the miter point for a corner between two offset pieces. prev_src and % next_src provide the incoming and outgoing source tangents, while prev_piece and % next_piece provide the two offset endpoints to be joined. The result is the % intersection of the two tangent rays in offset space. vardef mfun_parallel_corner_miter_point(expr prev_src, next_src, prev_piece, next_piece) = save join_start, join_end, incoming, outgoing ; save denom, along_prev ; pair join_start, join_end, incoming, outgoing ; numeric denom, along_prev ; join_start := lastpoint prev_piece ; join_end := firstpoint next_piece ; incoming := lastunitdirection prev_src ; outgoing := firstunitdirection next_src ; denom := incoming crossprod outgoing ; if denom <> 0 : along_prev := ((join_end - join_start) crossprod outgoing) / denom ; join_start + along_prev * incoming else : % message("SOME ERROR"); join_start fi enddef ; % Check whether the miter for a corner is numerically usable and not too long. The % inputs are the same as corner_miter_point, with dist used to scale the miter % limit. The function also respects join_mode, so setting the mode away from miters % forces bevels. vardef mfun_parallel_corner_miter_usable(expr prev_src, next_src, prev_piece, next_piece, dist) = save join_start, join_end, incoming, outgoing, miter ; save denom, base, limit ; pair join_start, join_end, incoming, outgoing, miter ; numeric denom, base, limit ; join_start := lastpoint prev_piece ; join_end := firstpoint next_piece ; incoming := lastunitdirection prev_src ; outgoing := firstunitdirection next_src ; denom := incoming crossprod outgoing ; base := abs(dist) ; if base < mfun_parallel_tolerance : base := mfun_parallel_tolerance ; fi limit := mfun_parallel_miter_limit * base ; if (mfun_parallel_join_mode = mitered) and (abs(denom) > mfun_parallel_join_epsilon) : miter := mfun_parallel_corner_miter_point(prev_src, next_src, prev_piece, next_piece) ; (abs (miter - join_start) <= limit) and (abs (miter - join_end) <= limit) else : false fi enddef ; % Return the internal inflection parameters of one cubic segment. The input piece % is a one-segment cubic path. The result is a pair of numeric parameters; a % missing root is encoded as -1, and two roots are returned in increasing order. vardef mfun_parallel_inflection_roots(expr piece) = save a, b, c, ab, ca, cb, disc, roots, r ; pair a, b, c, roots ; numeric ab, ca, cb, disc, r ; a := - firstpoint piece + 3 * firstpostcontrol piece - 3 * lastprecontrol piece + point 1 of piece ; b := 3 * firstpoint piece - 6 * firstpostcontrol piece + 3 * lastprecontrol piece ; c := -3 * firstpoint piece + 3 * firstpostcontrol piece ; ab := -6 * a crossprod b ; ca := 6 * c crossprod a ; cb := 2 * c crossprod b ; roots := (-1, -1) ; if abs(ab) > mfun_parallel_epsilon : disc := ca * ca - 4 * ab * cb ; if disc >= 0 : r := (-ca - sqrt(disc)) / (2 * ab) ; if (r > mfun_parallel_epsilon) and (r < 1 - mfun_parallel_epsilon) : roots := (r, ypart roots) ; fi r := (-ca + sqrt(disc)) / (2 * ab) ; if (r > mfun_parallel_epsilon) and (r < 1 - mfun_parallel_epsilon) : if xpart roots < 0 : roots := (r, ypart roots) ; elseif abs(r - xpart roots) > 64 * mfun_parallel_epsilon : roots := (xpart roots, r) ; fi fi fi elseif abs(ca) > mfun_parallel_epsilon : r := -cb / ca ; if (r > mfun_parallel_epsilon) and (r < 1 - mfun_parallel_epsilon) : roots := (r, -1) ; fi fi if (xpart roots > 0) and (ypart roots > 0) and (xpart roots > ypart roots) : (ypart roots, xpart roots) else : roots fi enddef ; % Split one cubic segment at its inflection parameters, if any. The input c is a % one-segment cubic path. The result is a path made from one, two, or three cubic % segments, joined without duplicate knots. vardef mfun_parallel_split_cubic_at_inflections(expr c) = save roots, rone, rtwo ; pair roots ; numeric rone, rtwo ; roots := mfun_parallel_inflection_roots(c) ; rone := xpart roots ; rtwo := ypart roots ; if (rone > 0) and (rtwo > 0) : subpath (0, rone) of c & subpath (rone,rtwo) of c & subpath (rtwo,1) of c elseif rone > 0 : subpath (0, rone) of c & subpath (rone,1) of c else : c fi enddef ; % Normalize a full path and split every nondegenerate segment at inflections. The % input may be open or cyclic and may contain straight segments. The output keeps % the input's cyclic status after normalization, so later stages can decide whether % to join the final piece back to the first one. vardef mfun_parallel_split_at_inflections(expr p) = save q ; path q ; q := normalized_path(p) ; for i within q : if i < pathlength if not cycle q : - 1 fi : mfun_parallel_split_cubic_at_inflections( pathpoint .. controls pathpostcontrol and (deltaprecontrol 1) .. (deltapoint 1) ) & fi endfor if cycle q : cycle else : nocycle fi enddef ; % Construct one cubic approximation to the offset of a single cubic segment. The % input c is one source segment and dist is the signed offset distance, with % positive distance using MetaPost's left normal. Endpoint positions and tangents % are matched using the curvature-scaled derivative of the true offset. vardef mfun_parallel_candidate(expr c, dist) = save deriva, derivb, normala, normalb, curva, curvb, scalea, scaleb, qa, qb, qc, qd ; pair deriva, derivb, normala, normalb, qa, qb, qc, qd; numeric curva, curvb, scalea, scaleb ; deriva := 3 * (firstpostcontrol c - firstpoint c) ; derivb := 3 * (lastpoint c - lastprecontrol c) ; normala := unitvector(deriva) rotated 90 ; normalb := unitvector(derivb) rotated 90 ; qa := firstpoint c + dist * normala ; qd := lastpoint c + dist * normalb ; curva := mfun_parallel_signed_curvature(c, 0) ; curvb := mfun_parallel_signed_curvature(c, 1) ; scalea := 1 - dist * curva ; scaleb := 1 - dist * curvb ; if abs(scalea) < mfun_parallel_min_speed_factor : scalea := if scalea < 0 : - fi mfun_parallel_min_speed_factor ; fi if abs(scaleb) < mfun_parallel_min_speed_factor : scaleb := if scaleb < 0 : - fi mfun_parallel_min_speed_factor ; fi qb := qa + (scalea / 3) * deriva ; qc := qd - (scaleb / 3) * derivb ; qa .. controls qb and qc .. qd enddef ; % Estimate how far an offset candidate is from sampled normal-offset targets. c is % the source segment, q is the candidate offset segment, and dist is the signed % offset distance. The returned number is the largest Euclidean sample error over % the configured interior sample points. vardef mfun_parallel_candidate_error(expr c, q, dist) = save worst, e, t, target, got ; numeric worst, e, t ; pair target, got ; worst := 0 ; for i = 1 step 1 until mfun_parallel_samples : t := i / (mfun_parallel_samples + 1) ; target := point t of c + dist * ((unitdirection t of c) rotated 90) ; got := point t of q ; e := abs(got - target) ; if e > worst : worst := e ; fi endfor ; worst enddef ; % Detect whether an offset segment is near a cusp of the true offset curve. The % input c is one source segment and dist is the signed offset distance. A cusp is % possible when 1 - dist * curvature is close to zero, so such pieces are forced to % subdivide further instead of being accepted too early. vardef mfun_parallel_cusp_risk(expr c, dist) = save risky ; boolean risky ; risky := false ; for i = 0 step 1 until 4 : if abs(1 - dist * mfun_parallel_signed_curvature(c, i/4)) < mfun_parallel_cusp_epsilon : risky := true ; fi endfor ; risky enddef ; % Measure the control polygon length of a one-segment cubic path. The input piece % is assumed to have one segment. This inexpensive size measure is used both to % reject degenerate pieces and to stop recursion on pieces that are already smaller % than the requested tolerance scale. vardef mfun_parallel_control_polygon_length(expr piece) = abs(firstpostcontrol piece - firstpoint piece) + abs(lastprecontrol piece - firstpostcontrol piece) + abs(lastpoint piece - lastprecontrol piece) enddef ; % Return true when a one-segment path is large enough to offset. The input piece is % measured by its control polygon length. Segments at or below epsilon are % skipped so zero-length cyclic tails do not create offset artifacts. vardef mfun_parallel_valid_segment(expr piece) = mfun_parallel_control_polygon_length(piece) > mfun_parallel_epsilon enddef ; % Recursively approximate the offset of one cubic segment. The input c is one % source segment, dist is the signed offset distance, and depth is the remaining % subdivision budget. The function accepts a cubic candidate when its sampled error % is small and cusp risk is low; otherwise it subdivides at t = .5 and appends the % two recursive offsets. vardef mfun_parallel_cubic_recursive(expr c, distance, depth) = save q, left_segment, right_segment, left_offset, right_offset, err, short_enough, risky ; path q, left_segment, right_segment, left_offset, right_offset ; numeric err ; boolean short_enough, risky ; q := mfun_parallel_candidate(c, distance) ; err := mfun_parallel_candidate_error(c, q, distance) ; short_enough := mfun_parallel_control_polygon_length(c) < 2 * mfun_parallel_tolerance ; risky := mfun_parallel_cusp_risk(c, distance) ; if (depth <= 0) or ((err <= mfun_parallel_tolerance) and (not risky)) or short_enough : q else : left_segment := subpath (0,.5) of c ; right_segment := subpath (.5,1) of c ; left_offset := mfun_parallel_cubic_recursive(left_segment, distance, depth - 1) ; right_offset := mfun_parallel_cubic_recursive(right_segment, distance, depth - 1) ; mfun_parallel_append_path_piece(left_offset, right_offset) fi enddef ; % Offset a complete MetaPost path, including multi-segment paths and corners. The % input p may be open or cyclic and may contain cubic or straight segments; distance % is the signed offset distance. The path is normalized, split at inflections, offset % piece by piece, and then reassembled with smooth joins, miter joins, or bevel % joins as appropriate. def mfun_parallel_append_path_piece_x(expr q) = if lastxy = firstpoint q : & q elseif abs(lastxy - firstpoint q) <= mfun_parallel_epsilon : & mfun_parallel_set_path_start(q, lastxy) else : -- firstpoint q & q fi enddef ; vardef mfun_parallel_adaptive_offset(expr p, dist) = save prepared, out, current, src, piece, join_kind, join_point ; save n, source_closed, keep_closing_knot ; path prepared, out, current, src[], piece[] ; pair join_point[] ; numeric join_kind[], n ; boolean source_closed, keep_closing_knot ; prepared := mfun_parallel_split_at_inflections(p) ; source_closed := cycle prepared ; keep_closing_knot := mfun_parallel_has_degenerate_cyclic_tail(p) ; n := 0 ; for i within prepared : if i < pathlength if not source_closed : - 1 fi : current := pathpoint .. controls pathpostcontrol and (deltaprecontrol 1) .. (deltapoint 1) ; if mfun_parallel_valid_segment(current) : src[n] := current ; piece[n] := mfun_parallel_cubic_recursive(src[n], dist, mfun_parallel_max_depth) ; n := n + 1 ; fi fi endfor for i = 0 step 1 until n - 2 : if mfun_parallel_join_is_smooth(src[i], src[i + 1]) : join_kind[i] := -1; % mitered ; % unset or mitered elseif mfun_parallel_corner_miter_usable(src[i], src[i + 1], piece[i], piece[i + 1], dist) : join_point[i] := mfun_parallel_corner_miter_point(src[i], src[i + 1], piece[i], piece[i + 1]) ; piece[i] := mfun_parallel_set_path_end(piece[i], join_point[i]) ; piece[i + 1] := mfun_parallel_set_path_start(piece[i + 1], join_point[i]) ; join_kind[i] := mitered ; % skipped else : join_kind[i] := beveled ; % checked fi endfor ; if source_closed : i := n - 1 ; if mfun_parallel_join_is_smooth(src[i], src[0]) : join_kind[i] := -1; % mitered ; % unset or mitered elseif mfun_parallel_corner_miter_usable(src[i], src[0], piece[i], piece[0], dist) : join_point[i] := mfun_parallel_corner_miter_point(src[i], src[0], piece[i], piece[0]) ; piece[i] := mfun_parallel_set_path_end(piece[i], join_point[i]) ; piece[0] := mfun_parallel_set_path_start(piece[0], join_point[i]) ; join_kind[i] := mitered ; % skipped else : join_kind[i] := beveled ; % checked fi fi % inefficient % out := piece[0] ; % for i = 1 step 1 until n - 1: % if join_kind[i - 1] = beveled : % connector := lastpoint out -- firstpoint piece[i] ; % out := mfun_parallel_append_path_piece(out, connector) ; % fi % out := mfun_parallel_append_path_piece(out, piece[i]) ; % endfor ; % out := piece[0] for i = 1 step 1 until n - 1: if join_kind[i - 1] = beveled : mfun_parallel_append_path_piece_x(lastxy -- firstpoint piece[i]) fi mfun_parallel_append_path_piece_x(piece[i]) endfor ; % if source_closed : if join_kind[n - 1] = beveled : connector := lastpoint out -- firstpoint piece[0] ; out := mfun_parallel_append_path_piece(out, connector) ; fi if abs(lastpoint out - firstpoint out) <= mfun_parallel_epsilon : out := mfun_parallel_set_path_end(out, firstpoint out) ; if keep_closing_knot : out := out && else : out := out & fi else : out := out -- fi cycle ; fi out enddef ; % Draw sampled points of the mathematical normal offset for comparison. The input p % is normalized before sampling, dist is the signed offset distance, n is the % number of sample intervals, and col is the dot color. Cyclic paths omit the % duplicate final sample so the closing point is not plotted twice. vardef mfun_parallel_sampled_points(expr p, distance, samples) = save q, t ; path q ; numeric t ; q := normalized_path(p) ; for i = 0 step 1 until samples if cycle q : - 1 fi : hide(t := (length q) * i / samples ;) point t of q + distance * ((unitdirection t of q) rotated 90) -- endfor nocycle enddef ; % A user interface: presetparameters "parallel" [ tolerance = .20, maxdepth = 13, samples = 5, minspeedfactor = .002, joinmode = mitered, % beveled miterlimit = 8, eps = 1/65536, cuspepsilon = .060, joinepsilon = .00001, smoothepsilon = .0005, path = origin, distance = 5, trace = false, ] ; def lmt_parallel = applyparameters "parallel" "lmt_do_parallel" enddef ; vardef lmt_do_parallel = pushparameters "parallel" ; % interim mfun_parallel_tolerance := getparameter "tolerance" ; interim mfun_parallel_max_depth := getparameter "maxdepth" ; interim mfun_parallel_samples := getparameter "samples" ; interim mfun_parallel_min_speed_factor := getparameter "minspeedfactor" ; interim mfun_parallel_join_mode := getparameter "joinmode" ; interim mfun_parallel_miter_limit := getparameter "miterlimit" ; % interim mfun_parallel_epsilon := getparameter "epsilon" ; interim mfun_parallel_cusp_epsilon := getparameter "cuspepsilon" ; interim mfun_parallel_join_epsilon := getparameter "joinepsilon" ; interim mfun_parallel_smooth_epsilon := getparameter "smoothepsilon" ; % save p ; path p ; p := getparameter "path" ; if getparameter "trace" : image ( draw p withcolor "darkred" ; drawdot mfun_parallel_sampled_points( p, getparameter "distance", getparameter "samples" ) withpen pencircle scaled 3 withcolor "middlegray" ; draw mfun_parallel_adaptive_offset(p,getparameter "distance") ; ) else : mfun_parallel_adaptive_offset(p,getparameter "distance") fi popparameters enddef ; % draw lmt_parallel [ % trace = true, % distance = -16, % samples = 10, % path = ((0,0){right} .. {left}(0,50) .. {right}(0,100) .. {left}(0,50) .. {right}(0,0) && cycle), % ] ; % draw lmt_parallel [ % path = ((0,0){right} .. {left}(0,50) .. {right}(0,100) .. {left}(0,50) .. {right}(0,0) && cycle), % ] withcolor red ; % draw lmt_parallel [ % comment = "near straight", % trace = true, % distance = 10, % samples = 5, % path = ((0, 0) .. controls (55, 3) and (115, -2) .. (170, 6)), % ] shifted (100,50) ; % draw lmt_parallel [ % comment = "tight", % trace = true, % distance = -12, % samples = 5, % path = ((0, 0) .. controls (100, 120) and (15, 165) .. (65, 20)), % ] shifted (0,-150) ; % draw lmt_parallel [ % comment = "polyline", % trace = true, % distance = 9, % samples = 5, % path = ((0, 0) -- (70, 0) -- (70, 55) -- (120, 55)), % ] shifted (100,-150) ; % draw lmt_parallel [ % comment = "square", % trace = true, % distance = -8, % samples = 5, % path = ((0, 0) -- (64, 0) -- (64, 64) -- (0, 64) -- cycle), % ] shifted (250,-150) ;