Resources Numerical accuracy Calculation policy

Trim

Cuts a line, arc, circle, polyline or construction line at the objects that cross it and keeps or removes the pieces.

How the precision is kept

Each crossing is found as a point in 64-bit numbers, then turned into a parameter along the object being cut. For lines, arcs, circles and polylines that parameter is rounded to 9 decimals, and the new end point is computed back from the rounded parameter, so the cut is held to about 1e-9 of the object's length and not to 15 digits. Construction lines keep the parameter unrounded but are cut along a stand-in segment 2 × 10⁹ units long.

Open item. Use the computed intersection point as the new end instead of a point recomputed from a parameter rounded to 9 decimals.

Calculations under this policy

4 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

geom.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-cadcraft.mjs

Cut parameters

64-bit

Where along an object each cutting edge crosses it.

t = segment index + parameter of the intersection; t = round(t × 10⁹) ÷ 10⁹

Each intersection point is converted to a parameter with primParam, and for every object except a construction line the parameter is rounded to 9 decimals with Math.round before it is used. The cut point is later recomputed from this rounded value, which moves it by up to 5 × 10⁻¹⁰ of the segment's length. For construction lines the parameter is kept as computed and values closer than 1e-15 are merged.

  • geom.js
  • lines 890 to 907
  • CadCraft 0.8.2
/** Parameter along the whole entity: poly → segment index + local t; circle/arc → fraction of sweep. */
function entityParams(e, cutters, blocks, edgeExtend = false) {
  const own = e.type === 'poly' ? polyPrims(e) : prims(e);
  const ts = [];
  own.forEach((p, i) => {
    // EDGEMODE 1: a cutting line or arc counts as the whole line or circle it lies on
    for (const c of cutters) for (const q of prims(c, blocks)) for (const x of intersect(p, q, false, edgeExtend && (c.type === 'line' || c.type === 'arc'))) {
      let t = primParam(p, x);
      if (p.rev) t = 1 - t;
      ts.push((e.type === 'poly' ? i : 0) + t);
    }
  });
  if (UNBOUNDED.has(e.type)) {   // a stand-in segment 2·FAR long: rounding t would move a cut by metres; keep it exact
    const u = ts.sort((a, b) => a - b).filter((t, i, l) => !i || t - l[i - 1] > 1e-15);
    return { own, ts: u };
  }
  return { own, ts: [...new Set(ts.map((t) => Math.round(t * 1e9) / 1e9))].sort((a, b) => a - b) };
}

Piece between two parameters

64-bit

The part of a line, arc, open polyline or construction line between two parameters, as a new object.

line: p₁ + (p₂ − p₁)·t; arc: a0 + sweep·t; polyline arc piece: bulge = tan(atan(b)·(t_b − t_a))

New ends are computed from the parameters, not taken from the intersection points. A construction line is cut along a stand-in segment that reaches 10⁹ units each way, so a cut point on it is a difference of numbers near 10⁹ and is good to about 1 × 10⁻⁷ drawing units. A piece shorter than 1e-9 in parameter (1e-15 on a construction line) is dropped.

  • geom.js
  • lines 952 to 990
  • CadCraft 0.8.2
/** The piece of a line / arc / open poly between parameters t0 < t1. */
export function subEntity(e, t0, t1) {
  if (t1 - t0 < (UNBOUNDED.has(e.type) ? 1e-15 : 1e-9)) return null;
  if (UNBOUNDED.has(e.type)) {
    // along prims' stand-in segment: an end at 0 (xline) or 1 is still infinite; a cut end is a real point
    const p = prims(e)[0], at = (t) => primPoint(p, t);
    const open0 = e.type === 'xline' && t0 <= 1e-12, open1 = t1 >= 1 - 1e-12;
    if (open0 && open1) return { ...e };
    if (open1) { const a = at(t0); return { ...e, type: 'ray', x: a.x, y: a.y }; }
    if (open0) { const b = at(t1); return { ...e, type: 'ray', x: b.x, y: b.y, dx: -e.dx, dy: -e.dy }; }
    const a = at(t0), b = at(t1), out = { ...e, type: 'line', x1: a.x, y1: a.y, x2: b.x, y2: b.y };
    delete out.x; delete out.y; delete out.dx; delete out.dy;
    return out;
  }
  if (e.type === 'line') {
    const p = { x: e.x1 + (e.x2 - e.x1) * t0, y: e.y1 + (e.y2 - e.y1) * t0 }, q = { x: e.x1 + (e.x2 - e.x1) * t1, y: e.y1 + (e.y2 - e.y1) * t1 };
    return { ...e, x1: p.x, y1: p.y, x2: q.x, y2: q.y };
  }
  if (e.type === 'arc') {
    const [a0, a1] = arcRange(e.a0, e.a1), s = a1 - a0;
    return { ...e, a0: normAngle(a0 + s * t0), a1: normAngle(a0 + s * t1) };
  }
  // open poly
  const i0 = Math.floor(t0), i1 = Math.min(Math.ceil(t1) - 1, e.pts.length - 2);
  const pts = [];
  const at = (i, t) => {
    const p = e.pts[i], q = e.pts[i + 1], b = p.b || 0;
    if (Math.abs(b) < 1e-12) return { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t };
    const a = bulgeArc(p, q, b), u = a.rev ? 1 - t : t;
    return primPoint(a, u);
  };
  const partBulge = (i, ta, tb) => { const b = e.pts[i].b || 0; return Math.abs(b) < 1e-12 ? 0 : Math.tan(Math.atan(b) * (tb - ta)); };
  for (let i = i0; i <= i1; i++) {
    const ta = i === i0 ? t0 - i : 0, tb = i === i1 ? t1 - i : 1;
    pts.push({ ...at(i, ta), b: partBulge(i, ta, tb) });
    if (i === i1) pts.push({ ...at(i, tb), b: 0 });
  }
  return { ...e, closed: false, pts };
}

Trim at a pick

64-bit

The pieces that stay when the part of an object round the picked point is cut away.

circle: remaining arc from angle t_hi × 2π to t_lo × 2π

Uses the rounded parameters from entityParams. A circle becomes an arc whose two angles are the neighboring cut parameters times 2π. Cuts within 1e-9 in parameter of an open object's ends are ignored.

  • geom.js
  • lines 915 to 939
  • CadCraft 0.8.2
/** Trim e at the cutting edges around the picked point: returns the pieces that stay ([] = all gone, null = nothing to cut). */
export function trimEntity(e, cutters, pick, blocks) {
  if (!['line', 'arc', 'circle', 'poly', 'xline', 'ray'].includes(e.type)) return null;
  const { own, ts } = entityParams(e, cutters.filter((c) => c !== e), blocks);
  const tp = pickParam(e, own, pick);
  if (e.type === 'circle') {
    if (ts.length < 2) return null;
    const angs = ts.map((t) => t * TAU), ap = tp * TAU;
    let lo = angs[angs.length - 1] - TAU, hi = angs[0];
    for (let i = 0; i + 1 < angs.length; i++) if (ap >= angs[i] && ap <= angs[i + 1]) { lo = angs[i]; hi = angs[i + 1]; }
    if (ap > angs[angs.length - 1]) { lo = angs[angs.length - 1]; hi = angs[0] + TAU; }
    return [{ ...e, type: 'arc', a0: normAngle(hi), a1: normAngle(lo) }];
  }
  const maxT = e.type === 'poly' ? (e.closed ? e.pts.length : e.pts.length - 1) : 1;
  const tiny = UNBOUNDED.has(e.type) ? 1e-15 : 1e-9;   // t along a construction line's 2·FAR stand-in
  const inner = ts.filter((t) => t > tiny && t < maxT - tiny);
  if (e.type === 'poly' && e.closed) return trimClosedPoly(e, ts, tp);
  if (!inner.length) return null;
  let lo = 0, hi = maxT;
  for (const t of inner) { if (t < tp) lo = t; else if (t > tp) { hi = t; break; } }
  const keep = [];
  if (lo > tiny) keep.push(subEntity(e, 0, lo));
  if (hi < maxT - tiny) keep.push(subEntity(e, hi, maxT));
  return keep.filter(Boolean);
}

Splitting into pieces

64-bit

An object divided at all its cutting edges into pieces that can be removed one by one.

Pieces are pairs of the rounded parameters in order. On a closed object a cut within 1e-9 of the full parameter range is moved to 0, and neighbors closer than 1e-9 make no piece.

  • geom.js
  • lines 992 to 1020
  • CadCraft 0.8.2
/**
 * TRIM in pieces (0.4 T.1–T.3): e split at its cutting edges, so a pick, a freehand path, a fence or a crossing window can
 * name which pieces go. → { e, own, pieces: [{ t0, t1 }], maxT, closed } in entityParams' parameter (poly: segment index +
 * local t; circle: fraction of a turn, counter-clockwise from 0°), or null for objects TRIM does not cut. Pieces cover e
 * in order; a closed object's last piece runs past maxT round to the first cut. No cut = one piece, the whole object
 * (AutoCAD's Quick mode then erases it). `edgeExtend`: cutting lines and arcs extended (EDGEMODE 1).
 */
export function trimSplit(e, cutters, blocks, { edgeExtend = false } = {}) {
  if (!e || !['line', 'arc', 'circle', 'poly', 'xline', 'ray'].includes(e.type)) return null;
  if (e.type === 'poly' && e.pts.length < 2) return null;
  const { own, ts } = entityParams(e, cutters.filter((c) => c !== e && (c.id === undefined || c.id !== e.id)), blocks, edgeExtend);
  const closed = e.type === 'circle' || (e.type === 'poly' && !!e.closed);
  const maxT = e.type === 'poly' ? (e.closed ? e.pts.length : e.pts.length - 1) : 1;
  const pieces = [];
  if (closed) {
    const cuts = [...new Set(ts.map((t) => { const u = ((t % maxT) + maxT) % maxT; return Math.abs(u - maxT) < 1e-9 ? 0 : u; }))].sort((a, b) => a - b);
    if (cuts.length < 2) pieces.push({ t0: 0, t1: maxT, whole: true });
    else {
      for (let i = 0; i + 1 < cuts.length; i++) if (cuts[i + 1] - cuts[i] > 1e-9) pieces.push({ t0: cuts[i], t1: cuts[i + 1] });
      pieces.push({ t0: cuts[cuts.length - 1], t1: cuts[0] + maxT });
    }
  } else {
    const tiny = UNBOUNDED.has(e.type) ? 1e-15 : 1e-9;   // t along a construction line's 2·FAR stand-in
    const inner = ts.filter((t) => t > tiny && t < maxT - tiny);
    const at = [0, ...inner, maxT];
    for (let i = 0; i + 1 < at.length; i++) if (at[i + 1] - at[i] > tiny) pieces.push({ t0: at[i], t1: at[i + 1] });
  }
  return { e, own, pieces, maxT, closed };
}

In detail

Method
Parameter along the object: a line or arc runs 0 to 1, a circle 0 to 1 counter-clockwise from 0°, a polyline uses segment index plus the fraction along that segment.
Number types
64-bit numbers.
Tolerances
Cut parameters are rounded to 1e-9 and equal values merged (1e-15 for construction lines, unrounded). Pieces shorter than 1e-9 in parameter are dropped (1e-15 for construction lines).
Limits
On a line of length L a cut end can be up to 5 × 10⁻¹⁰ × L from the computed intersection: a line from 0 to 1000 cut at x = 333.3333333333333 ends at 333.333333. A construction line through the origin cut at the same place starts at 333.33333337306976.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach