Resources Numerical accuracy Calculation policy

Markers: pieces laid on fabric

Keeps the pieces of a marker on the fabric, checks grain direction and overlaps, and works out the marker's length and fabric use (Apparel add-on).

How the precision is kept

A piece placed on a marker carries its own copy of the cut line, rounded to 2 decimals in millimeter drawings (4 in inch drawings) and thinned when it would not otherwise fit in one drawing object. Overlap checks, the marker length and the fabric efficiency are computed in 64-bit numbers from that copy.

Open item. Compute the placed piece's area from the full cut line before it is rounded and thinned.

Calculations under this policy

9 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.

addons/apparel/lib/marker.js CadCraft 0.8.2 64-bit 9 calculations Reviewed

Tests None found by the review

Thinning a cut line

64-bit

A closed cut line with the points removed that lie within a tolerance of the simplified line.

d = |(P − A) × (B − A)| ÷ |B − A|; kept when d > tol

The standard Douglas–Peucker split, run on both halves of the ring between point 0 and the point farthest from it. Kept points are not moved. A chord shorter than 1e-12 uses the plain distance to its start.

  • addons/apparel/lib/marker.js
  • lines 25 to 51
  • CadCraft 0.8.2
/** Douglas–Peucker on a closed ring of {x, y}: points within tol of the simplified line go (0.6 LC.17). */
export function thinRing(pts, tol) {
  const n = pts.length;
  if (n <= 4 || !(tol > 0)) return pts.slice();
  // split at point 0 and the point farthest from it
  let far = 0, fd = -1;
  for (let i = 1; i < n; i++) { const d = Math.hypot(pts[i].x - pts[0].x, pts[i].y - pts[0].y); if (d > fd) { fd = d; far = i; } }
  const keep = new Uint8Array(n);
  keep[0] = keep[far] = 1;
  const seg = (i0, i1) => {   // indices along the ring, i1 > i0 (mod n handled by the caller's unrolled range)
    const stack = [[i0, i1]];
    while (stack.length) {
      const [a, b] = stack.pop();
      if (b - a < 2) continue;
      const A = pts[a % n], B = pts[b % n], dx = B.x - A.x, dy = B.y - A.y, L = Math.hypot(dx, dy);
      let best = -1, bd = -1;
      for (let i = a + 1; i < b; i++) {
        const P = pts[i % n];
        const d = L > 1e-12 ? Math.abs(dy * (P.x - A.x) - dx * (P.y - A.y)) / L : Math.hypot(P.x - A.x, P.y - A.y);
        if (d > bd) { bd = d; best = i; }
      }
      if (bd > tol) { keep[best % n] = 1; stack.push([a, best], [best, b]); }
    }
  };
  seg(0, far); seg(far, n);
  return pts.filter((_, i) => keep[i]);
}

A piece's copy for the marker

64-bit

The cut line, grain angle and area stored with a piece placed on a marker.

x, y → toFixed(2) in mm, toFixed(4) in inches; area = |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)| ÷ 2 → toFixed(4); grain = atan2 of the grain line

The stored cut line is rounded to 0.01 mm or 0.0001 in. If the data is then too long for one object it is thinned, with a tolerance that doubles from 0.02 mm (0.001 in) until it fits. The area is computed from the stored points and then rounded to 4 decimals.

  • addons/apparel/lib/marker.js
  • lines 53 to 68
  • CadCraft 0.8.2
export function placedFrom(data, size, cut) {
  const inch = data.u === 'in';
  const dp = inch ? 4 : 2;
  // a long cut line is thinned to within a hair (0.02 mm, 0.001 in), more only if it still would not fit in the object
  const tol0 = inch ? 0.001 : 0.02;
  let out = null;
  for (let k = 0; k < 12; k++) {
    const pts = thinRing(cut, k ? tol0 * 2 ** (k - 1) : 0).map((q) => [+q.x.toFixed(dp), +q.y.toFixed(dp)]);
    let a = 0;
    for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; a += p[0] * q[1] - q[0] * p[1]; }
    const grain = data.grain ? Math.atan2(data.grain.b[1] - data.grain.a[1], data.grain.b[0] - data.grain.a[0]) : Math.PI / 2;
    out = { name: data.name || '', style: data.style || '', size: size || data.size || '', u: inch ? 'in' : 'mm', cut: pts, grain, area: +Math.abs(a / 2).toFixed(4), pair: false };
    if (JSON.stringify(out).length <= PLACED_LIMIT - 200) break;
  }
  return out;
}

Piece to drawing coordinates

64-bit

The position in the drawing of a point of a placed piece.

x′ = x + cos(rot)·sx·pₓ − sin(rot)·sy·pᵧ; y′ = y + sin(rot)·sx·pₓ + cos(rot)·sy·pᵧ

One 64-bit expression per coordinate, with the sine and cosine of the block reference's rotation.

  • addons/apparel/lib/marker.js
  • lines 100 to 101
  • CadCraft 0.8.2
/** The insert's transform: x' = x + c·sx·px − s·sy·py … */
const tx = (ins) => { const c = Math.cos(ins.rot || 0), s = Math.sin(ins.rot || 0), sx = ins.sx ?? 1, sy = ins.sy ?? 1; return (p) => ({ x: (ins.x || 0) + c * sx * p[0] - s * sy * p[1], y: (ins.y || 0) + s * sx * p[0] + c * sy * p[1] }); };

Grain direction check

64-bit

Whether a placed piece's grain line runs along the fabric within the marker's tolerance.

tol = degrees × π ÷ 180 + 1e-6; ok when the angle between grain and fabric length ≤ tol (or to the opposite direction, on fabric that may be turned)

The angle difference is folded to 0…π with a remainder. A fixed 1e-6 rad is added to the tolerance, so a piece turned by exactly the allowed angle passes.

  • addons/apparel/lib/marker.js
  • lines 114 to 131
  • CadCraft 0.8.2
/**
 * Does the piece keep the fabric's rules? The fabric's length runs along the marker's x; a piece's grain line must lie along
 * it within `tol` degrees — either way on fabric that may be turned ('any'), only the way it was cut (grain pointing +x) on
 * one-way fabric (nap, a print with a direction), where a mirrored copy is refused too.
 */
export function grainProblem(p, ins, m, mIns = { rot: 0 }) {
  const along = mIns.rot || 0;
  const g = grainAngle(p, ins);
  const tol = Math.max(0, m.tol || 0) * Math.PI / 180 + 1e-6;
  const mirrored = (ins.sx ?? 1) * (ins.sy ?? 1) < 0;
  if (m.nap === 'oneway') {
    if (mirrored && !p.pair) return 'mirrored';
    if (angDiff(g, along) <= tol) return null;
    if (angDiff(g, along + Math.PI) <= tol) return 'turned';
    return 'grain';
  }
  return angDiff(g, along) <= tol || angDiff(g, along + Math.PI) <= tol ? null : 'grain';
}

Is the piece on the fabric

64-bit

Whether a piece lies on the fabric, off it, or across an edge.

bounding box in the marker's frame against 0 … width and 0, with eps = 0.01 mm or 0.0005 in

The piece's points are turned into the marker's frame and only their bounding box is compared. There is no upper limit along the length: the marker grows with the pieces.

  • addons/apparel/lib/marker.js
  • lines 133 to 142
  • CadCraft 0.8.2
/** Where a polygon (drawing coordinates) is against a marker: off it, on it, or partly off (across an edge). */
export function inMarker(poly, mIns, m) {
  const c = Math.cos(-(mIns.rot || 0)), s = Math.sin(-(mIns.rot || 0));
  const loc = poly.map((p) => ({ x: c * (p.x - mIns.x) - s * (p.y - mIns.y), y: s * (p.x - mIns.x) + c * (p.y - mIns.y) }));
  const b = bboxOf(loc);
  const eps = (m.u === 'in' ? 0.0005 : 0.01);
  if (b.y1 < -eps || b.y0 > m.w + eps || b.x1 < -eps) return 'out';
  if (b.y0 < -eps || b.y1 > m.w + eps || b.x0 < -eps) return 'edge';
  return 'in';
}

Do two edges cross

64-bit

Whether two cut line segments cross strictly inside both.

den = r × q; u = ((c − a) × q) ÷ den; v = ((c − a) × r) ÷ den; crossing when 1e-9 < u, v < 1 − 1e-9

Parallel when |den| < 1e-12, absolute on a product of two lengths. Crossings at or within 1e-9 of an end point are not counted, which is what lets pieces touch.

  • addons/apparel/lib/marker.js
  • lines 144 to 151
  • CadCraft 0.8.2
const segHit = (a, b, c, d) => {
  const r = { x: b.x - a.x, y: b.y - a.y }, q = { x: d.x - c.x, y: d.y - c.y };
  const den = r.x * q.y - r.y * q.x;
  if (Math.abs(den) < 1e-12) return false;
  const u = ((c.x - a.x) * q.y - (c.y - a.y) * q.x) / den, v = ((c.x - a.x) * r.y - (c.y - a.y) * r.x) / den;
  const e = 1e-9;
  return u > e && u < 1 - e && v > e && v < 1 - e;
};

Do two pieces overlap

64-bit

Whether two placed pieces overlap.

Bounding boxes that only touch are let through. Otherwise every edge pair is tested with segHit; with no crossing, one piece is inside the other when the middle of its first edge is inside the other outline (even-odd test).

  • addons/apparel/lib/marker.js
  • lines 160 to 173
  • CadCraft 0.8.2
/** Two cut lines overlap: their edges cross, or one lies inside the other. Touching is allowed. */
export function overlaps(A, B) {
  const a = bboxOf(A), b = bboxOf(B);
  if (a.x1 <= b.x0 || b.x1 <= a.x0 || a.y1 <= b.y0 || b.y1 <= a.y0) return false;
  for (let i = 0; i < A.length; i++) {
    const p = A[i], q = A[(i + 1) % A.length];
    const lo = { x: Math.min(p.x, q.x), y: Math.min(p.y, q.y) }, hi = { x: Math.max(p.x, q.x), y: Math.max(p.y, q.y) };
    if (hi.x < b.x0 || lo.x > b.x1 || hi.y < b.y0 || lo.y > b.y1) continue;
    for (let j = 0; j < B.length; j++) if (segHit(p, q, B[j], B[(j + 1) % B.length])) return true;
  }
  // no crossing: one inside the other (test a middle point of an edge, so shared corners do not count)
  const mid = (P) => ({ x: (P[0].x + P[1].x) / 2, y: (P[0].y + P[1].y) / 2 });
  return pointIn(mid(A), B) || pointIn(mid(B), A);
}

Marker length and efficiency

64-bit

The fabric length used, the area of the pieces and the share of the fabric they cover.

len = max x of the pieces + end allowance; used = Σ area; eff = used ÷ (len × width)

The length and used area are rounded to 4 decimals and the efficiency to 6 before they are stored with the marker. The areas added are the stored, already rounded areas of the placed pieces.

  • addons/apparel/lib/marker.js
  • lines 175 to 182
  • CadCraft 0.8.2
/** The marker's numbers from the pieces on it (polys in the marker's own frame). */
export function stats(m, items) {
  let len = 0, used = 0;
  for (const it of items) { for (const p of it.poly) if (p.x > len) len = p.x; used += it.area; }
  if (items.length) len += m.end || 0;
  const total = len * m.w;
  return { len: +len.toFixed(4), used: +used.toFixed(4), eff: total > 0 ? +(used / total).toFixed(6) : 0, placed: items.length };
}

Marker label

64-bit

The width, length and efficiency written under the marker.

inches: 2 decimals; mm: 1 decimal, or meters with 3 decimals from 1000 mm; efficiency: percent with 1 decimal

Rounding for the label text only. The drawn fabric rectangle uses the unrounded width and the stored length, and is at least a quarter of the width long.

  • addons/apparel/lib/marker.js
  • lines 88 to 98
  • CadCraft 0.8.2
export function drawMarker(m, t = (k) => k) {
  const len = Math.max(m.len || 0, m.w * 0.25);
  const h = m.u === 'in' ? 0.5 : 12;
  const fmt = (v) => (m.u === 'in' ? (+v.toFixed(2)) + ' in' : (v >= 1000 ? (+(v / 1000).toFixed(3)) + ' m' : (+v.toFixed(1)) + ' mm'));
  return [
    { type: 'poly', closed: true, pts: [{ x: 0, y: 0, b: 0 }, { x: len, y: 0, b: 0 }, { x: len, y: m.w, b: 0 }, { x: 0, y: m.w, b: 0 }] },
    { type: 'line', x1: 0, y1: m.w + h * 0.4, x2: len, y2: m.w + h * 0.4, lt: 'DASHED', lts: m.u === 'in' ? 0.5 : 10 },   // the selvedge
    { type: 'text', x: 0, y: -h * 1.6, h, rot: 0, s: t('marker.label', { name: m.name || t('marker.name'), w: fmt(m.w), len: fmt(m.len || 0), eff: ((m.eff || 0) * 100).toFixed(1), placed: m.placed || 0, total: m.total || 0 }), ha: 'left', va: 'baseline' },
    { type: 'text', x: 0, y: -h * 3.2, h: h * 0.8, rot: 0, s: t(m.nap === 'oneway' ? 'marker.nap.oneway' : 'marker.nap.any', { tol: m.tol || 0 }), ha: 'left', va: 'baseline' },
  ];
}

In detail

Method
Douglas–Peucker thinning; shoelace area; segment crossing by cross products; even-odd point in polygon; efficiency = area of the pieces ÷ (length × width).
Number types
64-bit numbers; cut lines are arrays [x, y].
Tolerances
Thinning is used only when the unthinned copy is over 10,800 characters; it starts at 0.02 mm (0.001 in) and doubles until the copy fits. Grain tolerance = the marker's angle in degrees + 1e-6 rad. On-fabric test allows 0.01 mm (0.0005 in). Segments are parallel below a cross product of 1e-12; a crossing counts only strictly inside both segments (1e-9 from the ends).
Limits
Area, overlap and efficiency are those of the rounded (and, for long outlines, thinned) copy of the cut line, not of the piece's full cut line. Touching pieces do not count as overlapping.

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