Resources Numerical accuracy Calculation policy

Room outline, walls and openings

Builds the room a cabinet job sits in: its corners, the frame of each wall, inside tests, distances to walls, and the doors and windows along the walls.

How the precision is kept

Corners, wall frames and openings are 64-bit numbers rounded to 6 decimals of the job unit by r6 (core.js). A wall's unit direction is also rounded to 6 decimals. Arc walls are written out as chords that stay within 0.5 job units of the arc for inside tests, crossing tests and the room's overall size. A room taken from a host model is rounded to 0.1 mm, or to 1/16 inch in an inch job, before it is used.

Open item. Keep the wall direction vector unrounded, so points placed along a slanted wall are not shifted by the 6-decimal direction.

Calculations under this policy

19 calculations in 2 source files. 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/cabinetcraft/lib/room.js CadCraft 0.8.2 64-bit 15 calculations Reviewed

Tests None found by the review

Corners of a rectangle, L or U room

64-bit

The corner list of a room from its width, depth and notch.

U notch start a = (W − notch w) ÷ 2

Sums and differences of the room sizes with one halving; nothing is rounded here. The notch is clamped to at least 1 and to 1 or 2 less than the side, in job units.

  • addons/cabinetcraft/lib/room.js
  • lines 25 to 49
  • CadCraft 0.8.2
export function roomPts(room) {
  const W = room.w, D = room.d;
  const box = [[0, 0], [W, 0], [W, D], [0, D]];
  if (room.shape === 'free' && Array.isArray(room.pts)) return room.pts.map((p) => [p[0], p[1]]);
  const n = room.notch;
  if (room.shape === 'L' && n) {
    const nw = clamp(n.w, 1, W - 1), nd = clamp(n.d, 1, D - 1);
    switch (n.at) {
      case 'NW': return [[0, 0], [W, 0], [W, D], [nw, D], [nw, D - nd], [0, D - nd]];
      case 'SE': return [[0, 0], [W - nw, 0], [W - nw, nd], [W, nd], [W, D], [0, D]];
      case 'SW': return [[nw, 0], [W, 0], [W, D], [0, D], [0, nd], [nw, nd]];
      default: return [[0, 0], [W, 0], [W, D - nd], [W - nw, D - nd], [W - nw, D], [0, D]];   // NE
    }
  }
  if (room.shape === 'U' && n) {
    const horiz = n.at === 'N' || n.at === 'S';
    const nw = clamp(n.w, 1, (horiz ? W : D) - 2), nd = clamp(n.d, 1, (horiz ? D : W) - 1);
    if (n.at === 'S') { const a = (W - nw) / 2; return [[0, 0], [a, 0], [a, nd], [a + nw, nd], [a + nw, 0], [W, 0], [W, D], [0, D]]; }
    if (n.at === 'E') { const a = (D - nw) / 2; return [[0, 0], [W, 0], [W, a], [W - nd, a], [W - nd, a + nw], [W, a + nw], [W, D], [0, D]]; }
    if (n.at === 'W') { const a = (D - nw) / 2; return [[0, 0], [W, 0], [W, D], [0, D], [0, a + nw], [nd, a + nw], [nd, a], [0, a]]; }
    const a = (W - nw) / 2;   // N
    return [[0, 0], [W, 0], [W, D], [a + nw, D], [a + nw, D - nd], [a, D - nd], [a, D], [0, D]];
  }
  return box;
}

Right-angled outline made safe

64-bit

Checks a typed outline with right angles, drops repeated and collinear corners, makes it counter-clockwise and moves it to (0, 0).

2A = Σ (xᵢ yᵢ₊₁ − xᵢ₊₁ yᵢ); reject when |2A| < 1

Repeated, collinear and slanted edges are tested with an absolute 1e-6. The area test compares the doubled shoelace sum with 1. The corners are shifted by the smallest x and y and then rounded to 6 decimals.

  • addons/cabinetcraft/lib/room.js
  • lines 58 to 91
  • CadCraft 0.8.2
/** A free outline made safe: right angles only, no repeated or collinear corners, no crossing edges, counter-clockwise, moved to (0, 0). null when it cannot be one. */
export function cleanOutline(raw) {
  if (!Array.isArray(raw)) return null;
  let pts = [];
  for (const p of raw.slice(0, MAX_PTS)) {
    if (!Array.isArray(p)) return null;
    const x = num(p[0], NaN), y = num(p[1], NaN);
    if (!Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1e5 || Math.abs(y) > 1e5) return null;
    pts.push([x, y]);
  }
  // drop repeats, then collinear corners
  pts = pts.filter((p, i) => { const q = pts[(i + 1) % pts.length]; return Math.abs(p[0] - q[0]) > EPS || Math.abs(p[1] - q[1]) > EPS; });
  for (let again = true; again && pts.length >= 4;) {
    again = false;
    for (let i = 0; i < pts.length; i++) {
      const a = pts[(i + pts.length - 1) % pts.length], b = pts[i], c = pts[(i + 1) % pts.length];
      if ((Math.abs(a[0] - b[0]) < EPS && Math.abs(b[0] - c[0]) < EPS) || (Math.abs(a[1] - b[1]) < EPS && Math.abs(b[1] - c[1]) < EPS)) { pts.splice(i, 1); again = true; break; }
    }
  }
  if (pts.length < 4) return null;
  for (let i = 0; i < pts.length; i++) {
    const a = pts[i], b = pts[(i + 1) % pts.length];
    if (Math.abs(a[0] - b[0]) > EPS && Math.abs(a[1] - b[1]) > EPS) return null;   // a slanted edge
  }
  for (let i = 0; i < pts.length; i++) for (let j = i + 2; j < pts.length; j++) {
    if (i === 0 && j === pts.length - 1) continue;
    if (segCross(pts[i], pts[(i + 1) % pts.length], pts[j], pts[(j + 1) % pts.length])) return null;
  }
  const A = area2(pts);
  if (Math.abs(A) < 1) return null;
  if (A < 0) pts.reverse();
  const x0 = Math.min(...pts.map((p) => p[0])), y0 = Math.min(...pts.map((p) => p[1]));
  return pts.map((p) => [r6(p[0] - x0), r6(p[1] - y0)]);
}

Do two edges cross

64-bit

Decides whether two outline edges properly cross.

o(p, q, r) = (q − p) × (r − p); cross when the signs differ on both segments

Ordinary 64-bit cross products, not an exact predicate. A value within ±1e-6 counts as touching, not crossing; the 1e-6 is an area in job units squared and does not scale with the edge lengths.

  • addons/cabinetcraft/lib/room.js
  • lines 52 to 56
  • CadCraft 0.8.2
function segCross(a, b, c, d) {
  const o = (p, q, r) => (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
  const d1 = o(a, b, c), d2 = o(a, b, d), d3 = o(c, d, a), d4 = o(c, d, b);
  return ((d1 > EPS && d2 < -EPS) || (d1 < -EPS && d2 > EPS)) && ((d3 > EPS && d4 < -EPS) || (d3 < -EPS && d4 > EPS));
}

Arc walls as chords

64-bit

Writes each arc wall out as short chords so the room can be tested as a polygon.

step = 2 × acos(max(0, 1 − 0.5 ÷ r)); n = ⌈|sweep| ÷ step⌉, between 4 and 400

The chord count keeps the polygon within 0.5 job units of the arc, unless the cap of 400 chords is reached. The 0.5 is not scaled to the unit, so it is 0.5 mm or 0.5 inch. The acos argument is kept at 0 or above.

  • addons/cabinetcraft/lib/room.js
  • lines 108 to 123
  • CadCraft 0.8.2
/** The outline as a polygon with the arcs written out as short chords (for inside tests, areas and drawing). */
export function outlineOf(pts, arcs = []) {
  const out = [];
  const sw = new Map((arcs || []).map((a) => [a.i, a.sweep]));
  for (let i = 0; i < pts.length; i++) {
    const a = pts[i], b = pts[(i + 1) % pts.length];
    if (sw.has(i)) {
      // enough chords that the polygon is within 0.5 of the true arc (a unit-free guess of 1 mm = 1 unit is fine: a smaller job just gets more chords)
      const g = arcGeom(a, b, sw.get(i));
      const step = g ? 2 * Math.acos(Math.max(0, 1 - 0.5 / g.r)) : 0.2;
      const n = Math.max(4, Math.min(400, Math.ceil(Math.abs(sw.get(i)) * Math.PI / 180 / Math.max(step, 1e-3))));
      out.push(...arcPoints(a, b, sw.get(i), n).slice(0, -1));
    } else out.push(a);
  }
  return out;
}

Free outline with slanted and arc walls

64-bit

Checks an outline of any angle with arc edges and returns its corners, arcs and overall width and depth.

A corner is dropped as collinear when |cross| < 1e-9 × max(1, product of the two edge lengths), which scales with size. Walls shorter than 1 and a doubled area under 1 reject the outline. The width and depth are taken from the chord polygon, so on an outward arc they can be up to 0.5 short of the arc's true extent; corners are rounded to 6 decimals.

  • addons/cabinetcraft/lib/room.js
  • lines 127 to 177
  • CadCraft 0.8.2
/**
 * A free outline of ANY angle, with arc edges: { pts, arcs } counter-clockwise and moved so its bounding box starts at (0, 0), or null when it cannot be one
 * (fewer than three corners, repeated corners, edges that cross, a wall shorter than 1). Collinear corners are dropped unless an arc starts or ends there.
 */
export function cleanShape(raw, rawArcs) {
  if (!Array.isArray(raw)) return null;
  let pts = [];
  for (const p of raw.slice(0, MAX_PTS)) {
    if (!Array.isArray(p)) return null;
    const x = num(p[0], NaN), y = num(p[1], NaN);
    if (!Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1e5 || Math.abs(y) > 1e5) return null;
    pts.push([x, y]);
  }
  let arcs = cleanArcs(rawArcs, pts.length);
  const keep = pts.map(() => true);
  // a repeated corner (same as the next one) is dropped; an arc on it goes too
  for (let i = 0; i < pts.length; i++) { const q = pts[(i + 1) % pts.length]; if (Math.abs(pts[i][0] - q[0]) <= EPS && Math.abs(pts[i][1] - q[1]) <= EPS) keep[i] = false; }
  if (arcs.some((a) => !keep[a.i])) arcs = arcs.filter((a) => keep[a.i]);
  const arcAt = new Set();
  arcs.forEach((a) => { arcAt.add(a.i); arcAt.add((a.i + 1) % pts.length); });
  let list = pts.map((p, i) => ({ p, i, arc: arcs.find((a) => a.i === i) || null, isEnd: arcAt.has(i) })).filter((_, i) => keep[i]);
  for (let again = true; again && list.length >= 4;) {
    again = false;
    for (let i = 0; i < list.length; i++) {
      const e = list[i];
      if (e.isEnd) continue;
      const a = list[(i + list.length - 1) % list.length].p, b = e.p, c = list[(i + 1) % list.length].p;
      const cr = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
      if (Math.abs(cr) < 1e-9 * Math.max(1, Math.hypot(b[0] - a[0], b[1] - a[1]) * Math.hypot(c[0] - b[0], c[1] - b[1])) && ((b[0] - a[0]) * (c[0] - b[0]) + (b[1] - a[1]) * (c[1] - b[1])) > 0) { list.splice(i, 1); again = true; break; }
    }
  }
  if (list.length < 3) return null;
  let P = list.map((e) => e.p);
  let A = list.map((e, k) => (e.arc ? { i: k, sweep: e.arc.sweep } : null)).filter(Boolean);
  for (let i = 0; i < P.length; i++) { const q = P[(i + 1) % P.length]; if (Math.hypot(P[i][0] - q[0], P[i][1] - q[1]) < 1) return null; }
  // orientation from the corners (the arcs' signs are physical: + away from the room, so they do not change when the order is reversed)
  if (polyArea2(P) < 0) {
    const n = P.length;
    P = P.slice().reverse();
    A = A.map((a) => ({ i: (n - 2 - a.i + n) % n, sweep: a.sweep })).sort((x, y) => x.i - y.i);
  }
  const poly0 = outlineOf(P, A);
  for (let i = 0; i < poly0.length; i++) for (let j = i + 2; j < poly0.length; j++) {
    if (i === 0 && j === poly0.length - 1) continue;
    if (segCross(poly0[i], poly0[(i + 1) % poly0.length], poly0[j], poly0[(j + 1) % poly0.length])) return null;
  }
  if (Math.abs(polyArea2(poly0)) < 1) return null;
  const poly = outlineOf(P, A);
  const x0 = Math.min(...poly.map((p) => p[0])), y0 = Math.min(...poly.map((p) => p[1]));
  return { pts: P.map((p) => [r6(p[0] - x0), r6(p[1] - y0)]), arcs: A, w: r6(Math.max(...poly.map((p) => p[0])) - x0), d: r6(Math.max(...poly.map((p) => p[1])) - y0) };
}

Wall frames, lengths and corner turns

64-bit

For each wall: its length, direction, the frame cabinets are placed in, its angle and the turn at each corner.

chord = √(Δx² + Δy²); u = Δ ÷ chord; n = (−u_y, uₓ); angle = atan2(u_y, uₓ) × 180 ÷ π

The unit direction u is rounded to 6 decimals, so on a slanted wall each component can be off by up to 0.0000005; a point 10 000 units along the wall can move by up to 0.005 units per axis. The chord, angle and turns are rounded to 6 decimals too. The division by the chord has no guard here; cleanShape rejects walls shorter than 1.

  • addons/cabinetcraft/lib/room.js
  • lines 204 to 235
  • CadCraft 0.8.2
/**
 * The walls, counter-clockwise: each has the frame the layout uses (origin = the wall's start corner, u along it, n into the room). A slanted wall's frame
 * is rotated; an ARC wall's frame is its CHORD's (u from corner to corner) and it also has `arc` (angled.js arcGeom), `chord` and `len` = the arc length.
 * `turnStart` / `turnEnd` = how far the boundary turns at its two corners, degrees, + = left (a room corner), − = right (an inside corner).
 */
export function walls(room) {
  const pts = roomPts(room);
  const rect = room.shape === 'rect' || !room.shape;
  const ids = rect ? ['S', 'E', 'N', 'W'] : pts.map((_, i) => 'P' + (i + 1));
  const n = pts.length;
  const sw = new Map((room.shape === 'free' && room.arcs ? room.arcs : []).map((a) => [a.i, a.sweep]));
  const out = pts.map((a, i) => {
    const b = pts[(i + 1) % n];
    const chord = Math.hypot(b[0] - a[0], b[1] - a[1]);
    const u = { x: r6((b[0] - a[0]) / chord), y: r6((b[1] - a[1]) / chord) };
    const arc = sw.has(i) ? arcGeom(a, b, sw.get(i)) : null;
    const w = { id: ids[i], i, a: { x: a[0], y: a[1] }, b: { x: b[0], y: b[1] }, len: arc ? arc.arcLen : r6(chord), chord: r6(chord), arc,
      frame: { o: { x: a[0], y: a[1] }, u, n: { x: r6(-u.y), y: r6(u.x) }, ang: Math.atan2(u.y, u.x) } };
    w.angle = r6((Math.atan2(u.y, u.x) * 180) / Math.PI);
    w.t0 = arc ? arc.t0 : u; w.t1 = arc ? arc.t1 : u;
    return w;
  });
  out.forEach((w, i) => {
    const p = out[(i + n - 1) % n], q = out[(i + 1) % n];
    w.turnStart = r6(turnBetween(p.t1, w.t0));
    w.turnEnd = r6(turnBetween(w.t1, q.t0));
    w.convexStart = w.turnStart > 1e-6;   // a left turn into this wall: the room's corner
    w.convexEnd = w.turnEnd > 1e-6;
    w.nextId = q.id; w.prevId = p.id;
  });
  return out;
}

Point inside the room

64-bit

Tests whether a point is inside the outline; the boundary counts as inside.

on edge when |cross| < 1e-4 × max(1, edge length); else even-odd count of edges crossed by a ray

The on-edge test allows about 1e-4 job units off the line and a 1e-4 box at the ends. The ray test divides by yⱼ − yᵢ only when the two ends are on opposite sides of the ray, so the divisor is never zero.

  • addons/cabinetcraft/lib/room.js
  • lines 241 to 249
  • CadCraft 0.8.2
function pointInPoly(pts, x, y) {
  for (let i = 0; i < pts.length; i++) if (onSeg(pts[i], pts[(i + 1) % pts.length], x, y)) return true;   // on an edge
  let inside = false;
  for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
    const [xi, yi] = pts[i], [xj, yj] = pts[j];
    if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
  }
  return inside;
}

Cabinet footprint inside the room

64-bit

Decides whether a polygon lies inside the room.

A corner may be outside by up to tol = 0.5 job units and a room corner may poke in by the same amount. The 0.5 is absolute: 0.5 mm in a millimeter job, 0.5 inch in an inch job. The center tested is the mean of the corners.

  • addons/cabinetcraft/lib/room.js
  • lines 253 to 262
  • CadCraft 0.8.2
/** Does a polygon (any angle) lie inside the room? Its corners and middle are in, no room corner is inside it, and no room edge crosses its edges. */
export function polyInRoom(room, poly, tol = 0.5) {
  const out = roomOutline(room);
  for (const [x, y] of poly) if (!pointInPoly(out, x, y) && distPointPoly(out, x, y) > tol) return false;
  const cx = poly.reduce((s, p) => s + p[0], 0) / poly.length, cy = poly.reduce((s, p) => s + p[1], 0) / poly.length;
  if (!pointInPoly(out, cx, cy)) return false;
  // a room corner poking into the polygon (further than tol from its edges)
  for (const [px, py] of out) if (pointInPoly(poly, px, py) && distPointPoly(poly, px, py) > tol) return false;
  return true;
}

Distance from a point to a wall segment

64-bit

The shortest distance from a point to a segment.

t = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); d = |p − (a + t (b − a))|

One projection and one Math.hypot. A zero-length segment is guarded and gives the distance to its point.

  • addons/cabinetcraft/lib/room.js
  • lines 263 to 267
  • CadCraft 0.8.2
function distPointSeg(px, py, a, b) {
  const dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy;
  const t = L2 ? Math.max(0, Math.min(1, ((px - a[0]) * dx + (py - a[1]) * dy) / L2)) : 0;
  return Math.hypot(px - (a[0] + t * dx), py - (a[1] + t * dy));
}

Clearance from a footprint to the nearest wall

64-bit

The smallest distance between a footprint and any wall, 0 when they touch or cross.

The minimum over every wall edge and footprint edge of three point-to-segment distances; the footprint's far end point of each edge is covered as the start of the next. Crossing uses cross products beyond ±1e-9, an absolute value.

  • addons/cabinetcraft/lib/room.js
  • lines 278 to 292
  • CadCraft 0.8.2
/** Distance from a convex polygon (or a rectangle [x0, y0, x1, y1]) to the nearest wall (0 when touching or crossing). */
export function distToWalls(room, shape) {
  const poly = shape.length === 4 && typeof shape[0] === 'number' ? [[shape[0], shape[1]], [shape[2], shape[1]], [shape[2], shape[3]], [shape[0], shape[3]]] : shape;
  const out = roomOutline(room);
  let best = Infinity;
  for (let i = 0; i < out.length; i++) {
    const a = out[i], b = out[(i + 1) % out.length];
    for (let j = 0; j < poly.length; j++) {
      const c = poly[j], d = poly[(j + 1) % poly.length];
      if (segsCross(a, b, c, d)) return 0;
      best = Math.min(best, distPointSeg(c[0], c[1], a, b), distPointSeg(a[0], a[1], c, d), distPointSeg(b[0], b[1], c, d));
    }
  }
  return best === Infinity ? 0 : best;
}

Door and window positions along a wall

64-bit

Clamps each opening's width, offset, height and sill to its wall and the room height.

inch default = mm default × (1 ÷ 25.4); at ≤ chord − w

Defaults of 900, 1200, 2100 and 1000 mm are converted with 1 ÷ 25.4, which is exact as a definition but not exactly representable in binary. Values are clamped against the wall's chord, which was already rounded to 6 decimals, and are rounded to 6 decimals themselves.

  • addons/cabinetcraft/lib/room.js
  • lines 294 to 314
  • CadCraft 0.8.2
export function cleanOpenings(raw, room, unit = 'mm') {
  const W = walls(room);
  const k = unit === 'in' ? 1 / 25.4 : 1;
  const out = [];
  const seen = new Set();
  for (const o of (Array.isArray(raw) ? raw.slice(0, 40) : [])) {
    if (!o || typeof o !== 'object') continue;
    const wall = W.find((w) => w.id === o.wall);
    if (!wall) continue;
    const kind = o.kind === 'window' ? 'window' : 'door';
    const w = clamp(num(o.w, (kind === 'door' ? 900 : 1200) * k), 1, wall.chord);
    const at = clamp(num(o.at, 0), 0, Math.max(0, wall.chord - w));
    let id = txt(o.id, 12).replace(/[^A-Za-z0-9-]/g, '') || (kind === 'door' ? 'D' : 'W') + (out.length + 1);
    while (seen.has(id)) id += 'x';
    seen.add(id);
    const h = clamp(num(o.h, (kind === 'door' ? 2100 : 1200) * k), 1, room.h);
    const sill = kind === 'door' ? 0 : clamp(num(o.sill, 1000 * k), 0, Math.max(0, room.h - 1));
    out.push({ id, kind, wall: wall.id, at: r6(at), w: r6(w), h: r6(h), sill: r6(sill), swing: o.swing === 'out' ? 'out' : 'in', name: txt(o.name, 40) });
  }
  return out;
}

Floor strip in front of an opening

64-bit

The four corners of the strip of floor in front of a door or window, in room coordinates.

P(s, o) = origin + u × s + n × o

Uses the wall frame whose u and n were rounded to 6 decimals, and rounds each corner to 6 decimals again.

  • addons/cabinetcraft/lib/room.js
  • lines 316 to 323
  • CadCraft 0.8.2
/** The strip of floor in front of an opening, in world plan coordinates: depth into the room, the opening's width along the wall. */
export function openingPoly(room, op, depth) {
  const wall = wallById(room, op.wall);
  if (!wall) return null;
  const f = wall.frame;
  const P = (s, o) => [r6(f.o.x + f.u.x * s + f.n.x * o), r6(f.o.y + f.u.y * s + f.n.y * o)];
  return [P(op.at, 0), P(op.at + op.w, 0), P(op.at + op.w, depth), P(op.at, depth)];
}

Typed corner coordinates

64-bit

Reads x,y pairs typed by a person.

Each number must start with a digit: the pattern is an optional minus, digits, then an optional point and digits. Typed .5 is read as 5 and -.5 as 5, because the match starts at the first digit. Exponents are not read.

  • addons/cabinetcraft/lib/room.js
  • lines 333 to 338
  • CadCraft 0.8.2
/** Parse "x,y" lines or "x,y x,y …" typed by a person into corner pairs. */
export function parsePoints(text) {
  const out = [];
  for (const m of String(text || '').matchAll(/(-?\d+(?:\.\d+)?)\s*[,;\s]\s*(-?\d+(?:\.\d+)?)/g)) out.push([Number(m[1]), Number(m[2])]);
  return out;
}

Outline from edge lengths and turns

64-bit

Corners of a right-angled outline from the length of each edge and a left or right turn at each corner.

pᵢ₊₁ = pᵢ + dir × lengthᵢ; dir turns by ±90°

The direction is always one of (±1, 0) or (0, ±1), so each step is one exact multiplication and one addition. Corners are running sums, so a rounding in one edge carries to the corners after it.

  • addons/cabinetcraft/lib/room.js
  • lines 340 to 351
  • CadCraft 0.8.2
/** The corners of a right-angled outline from the lengths of its edges: start at (0, 0) going east, then turn left (L) or right (R) at each corner. "4000 L 2500 L 1500 R …". */
export function outlineFromTurns(lengths, turns) {
  const pts = [[0, 0]];
  let dir = [1, 0];
  for (let i = 0; i < lengths.length; i++) {
    const p = pts[pts.length - 1];
    pts.push([p[0] + dir[0] * lengths[i], p[1] + dir[1] * lengths[i]]);
    dir = turns[i] === 'R' ? [dir[1], -dir[0]] : [-dir[1], dir[0]];
  }
  pts.pop();
  return pts;
}

Outline from typed walls

64-bit

Corners and arcs of an outline from typed wall lengths, directions and arc radii.

next = p + len × (cos θ, sin θ); arc chord = 2 r sin(|sweep| ÷ 2)

Each corner comes from edgeEnd or arcEnd in angled.js, which round to 6 decimals, and the next corner starts from that rounded value. The outline is taken as closed when the last point is within 1 job unit of the start; that point is then dropped and the gap goes into the last wall.

  • addons/cabinetcraft/lib/room.js
  • lines 371 to 396
  • CadCraft 0.8.2
export function outlineFromSegments(segs) {
  if (!Array.isArray(segs) || segs.length < 2) return null;
  const pts = [[0, 0]], arcs = [];
  let heading = 0;
  for (let k = 0; k < segs.length; k++) {
    const sg = segs[k], p = pts[pts.length - 1];
    let next;
    if (sg.kind === 'arc') {
      const rr = num(sg.radius, NaN), sw = num(sg.sweep, NaN);
      if (!(rr > 0) || !(Math.abs(sw) >= 0.5 && Math.abs(sw) < 180)) return null;
      heading = sg.heading === null || sg.heading === undefined ? heading : num(sg.heading, heading);
      next = arcEnd(p, heading, rr, sw);
      arcs.push({ i: pts.length - 1, sweep: sw });
    } else {
      const len = num(sg.len, NaN);
      if (!(len > 0)) return null;
      heading = num(sg.angle, 0);
      next = edgeEnd(p, len, heading);
    }
    pts.push(next);
  }
  // the last point is the start again when the outline closes by itself
  const last = pts[pts.length - 1];
  if (Math.hypot(last[0], last[1]) < 1) pts.pop();
  return { pts, arcs: arcs.filter((a) => a.i < pts.length) };
}
addons/cabinetcraft/lib/fromroom.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests None found by the review

Room outline from a host model, cleaned

64-bit

Drops repeated and in-line points from a room outline given by the host app and makes it counter-clockwise.

drop b when |ab| < 1 or |(b − a) × (c − b)| < 1e-3 × max(1, |ab| × |bc|)

The collinear test is relative: a corner that turns by less than about 0.057° (sine under 1e-3) is removed. Points closer than 1 mm are merged and a loop with area under 1 mm² is refused.

  • addons/cabinetcraft/lib/fromroom.js
  • lines 22 to 37
  • CadCraft 0.8.2
export function cleanLoop(pts) {
  let p = (Array.isArray(pts) ? pts : []).map((q) => [Number(q[0]), Number(q[1])]).filter((q) => Number.isFinite(q[0]) && Number.isFinite(q[1]));
  if (p.length > 1 && Math.hypot(p[0][0] - p[p.length - 1][0], p[0][1] - p[p.length - 1][1]) < 1) p = p.slice(0, -1);
  // drop repeated points and points in a straight line
  let again = true;
  while (again && p.length > 3) {
    again = false;
    for (let i = 0; i < p.length; i++) {
      const a = p[(i + p.length - 1) % p.length], b = p[i], c = p[(i + 1) % p.length];
      const cross = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
      if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 1 || Math.abs(cross) < 1e-3 * Math.max(1, Math.hypot(b[0] - a[0], b[1] - a[1]) * Math.hypot(c[0] - b[0], c[1] - b[1]))) { p.splice(i, 1); again = true; break; }
    }
  }
  if (p.length < 3 || Math.abs(area(p)) < 1) return null;
  return area(p) < 0 ? p.reverse() : p;
}

Host room moved to (0, 0) and rounded

64-bit

Shifts the host's outline so its lower-left corner is the origin and converts it to the job unit.

mm job: round(v × 10) ÷ 10; inch job: round(v ÷ 25.4 × 16) ÷ 16

Every corner is rounded to 0.1 mm, or to 1/16 inch in an inch job, before it becomes the room. The shift ox, oy is kept unrounded so the room can be put back in the host model.

  • addons/cabinetcraft/lib/fromroom.js
  • lines 43 to 48
  • CadCraft 0.8.2
const k = u === 'in' ? 1 / 25.4 : 1;
const ox = Math.min(...loop.map((q) => q[0])), oy = Math.min(...loop.map((q) => q[1]));
const rel = loop.map((q) => [q[0] - ox, q[1] - oy]);
const rnd = (v) => (u === 'in' ? Math.round(v * k * 16) / 16 : Math.round(v * 10) / 10);
const pts = rel.map((q) => [rnd(q[0]), rnd(q[1])]);
const W = Math.max(...pts.map((q) => q[0])), D = Math.max(...pts.map((q) => q[1]));

Host doors and windows onto the nearest wall

64-bit

Puts each door or window of the host model on the room wall nearest its middle, with its offset along that wall.

s = (p − a)·u; off = |(p − a) × u|; at = clamp(s − w ÷ 2, 0, len − w)

The projection uses the wall direction rounded to 6 decimals. An opening further than 600 mm (600 ÷ 25.4 in an inch job) from every straight wall is skipped; arc walls are not considered. The offset is not rounded here; cleanProject rounds it afterwards.

  • addons/cabinetcraft/lib/fromroom.js
  • lines 64 to 76
  • CadCraft 0.8.2
const x = (Number(o.x) - ox) * k, y = (Number(o.y) - oy) * k, w = Number(o.w) * k;
if (!Number.isFinite(x) || !Number.isFinite(y) || !(w > 0)) { skipped.push(o); continue; }
let best = null;
for (const wall of wl) {
  if (wall.arc) continue;
  const dx = x - wall.a.x, dy = y - wall.a.y, s = dx * wall.frame.u.x + dy * wall.frame.u.y, off = Math.abs(-dx * wall.frame.u.y + dy * wall.frame.u.x);
  if (s < -w / 2 || s > wall.len + w / 2) continue;
  if (!best || off < best.off) best = { wall, s, off };
}
if (!best || best.off > MAX_OFF * k) { skipped.push(o); continue; }
const win = o.kind === 'window';
const id = (win ? 'W' : 'D') + (win ? ++nw : ++nd);
const at = Math.max(0, Math.min(best.wall.len - w, best.s - w / 2));

Room point back in the host's axes

64-bit

Converts a point in the room's own axes to millimeters in the host model.

X = ox + x × k, k = 25.4 for an inch job, else 1

One multiplication by the exact factor 25.4 and one addition of the stored shift.

  • addons/cabinetcraft/lib/fromroom.js
  • lines 84 to 88
  • CadCraft 0.8.2
export function toModel(project, x, y) {
  const s = project.site || { ox: 0, oy: 0 };
  const k = project.unit === 'in' ? 25.4 : 1;
  return [s.ox + x * k, s.oy + y * k];
}

In detail

Method
Shoelace sum for orientation and area; even-odd ray test for point in polygon; point-to-segment distance by clamped projection; segment crossing by the signs of four cross products; arcs from chord and sweep angle (angled.js).
Number types
64-bit numbers; corner lists are arrays of [x, y] pairs.
Tolerances
Repeated or collinear corners: 1e-6 absolute (right-angled outline), or |cross| < 1e-9 × max(1, product of the two edge lengths) (free outline). Segment crossing: cross products beyond ±1e-6 (cleaning) or ±1e-9 (distance to walls), absolute. On-edge test: 1e-4 × max(1, edge length) and a 1e-4 box. Inside-room slack: 0.5 job units. Minimum wall length 1, minimum doubled area 1. Imported loops: corners with |cross| < 1e-3 × max(1, product of edge lengths) are dropped; openings further than 600 mm from every wall are skipped.
Limits
The 0.5 chord tolerance and the 1-unit minimums are the same number in a millimeter job and an inch job. Typed numbers with no digit before the decimal point are not read as typed (see parsePoints).

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