Resources Numerical accuracy Calculation policy

Stage lighting plot: positions, fixtures and loads

Measures the pipes and trusses fixtures hang on, spreads fixtures evenly along them, finds which position a fixture belongs to and totals the wattage.

How the precision is kept

Lengths are sums of Math.hypot over a position's points in 64-bit numbers; a fixture's place along a position is a clamped projection. Measures are in millimeters, with k drawing units per millimeter. Nothing is rounded in the stored data; lengths shown in the properties panel and the command line are rounded to a whole millimeter.

Calculations under this policy

11 calculations in 3 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/lighting/lib/model.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests None found by the review

Length of a position

64-bit

The length of a pipe, truss or floor line along its points.

L = Σ √(Δx² + Δy²)

A running sum of Math.hypot over the segments in order, in drawing units.

  • addons/lighting/lib/model.js
  • line 81
  • CadCraft 0.8.2
export function lengthOf(pts) { let L = 0; for (let i = 0; i + 1 < pts.length; i++) L += Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y); return L; }

Where a point is on a position

64-bit

The distance along a position to the point nearest a fixture, and the fixture's distance from the line.

u = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); s = length before + u × |b − a|; off = |p − q|

The segment length is Math.sqrt of the squared length and the run before it is a sum of those. A zero-length segment is guarded. The first of equally near segments is kept.

  • addons/lighting/lib/model.js
  • lines 83 to 95
  • CadCraft 0.8.2
/** Where a point is on a polyline: { s (distance along from its first point), off (distance from the line), at (nearest point) }. */
export function project(pts, p) {
  if (pts.length === 1) return { s: 0, off: Math.hypot(p.x - pts[0].x, p.y - pts[0].y), at: pts[0] };
  let best = null, run = 0;
  for (let i = 0; i + 1 < pts.length; i++) {
    const a = pts[i], b = pts[i + 1], dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy, l = Math.sqrt(l2);
    const u = l2 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2)) : 0;
    const q = { x: a.x + u * dx, y: a.y + u * dy }, off = Math.hypot(p.x - q.x, p.y - q.y);
    if (!best || off < best.off) best = { s: run + u * l, off, at: q };
    run += l;
  }
  return best;
}

Position a fixture belongs to

64-bit

The nearest named position within reach of a point.

nearest with off ≤ tol

A plain comparison of distances; the tolerance is absolute, in drawing units.

  • addons/lighting/lib/model.js
  • lines 97 to 106
  • CadCraft 0.8.2
/** The position (by name) a point belongs to: the nearest within `tol` drawing units, or null. positions: [{ name, pts }] in drawing units. */
export function nearestPosition(positions, p, tol) {
  let best = null;
  for (const pos of positions) {
    if (!pos.name) continue;
    const r = project(pos.pts, p);
    if (r.off <= tol && (!best || r.off < best.off)) best = { name: pos.name, off: r.off, s: r.s };
  }
  return best;
}

Fixture data checked

64-bit

Checks a fixture's stored data: scale, wattage, DMX footprint and aim angle.

aim = ((aim mod 360) + 360) mod 360

The aim is reduced to 0 to 360 degrees with two remainders, which is exact for whole degrees. Wattage must be a positive number, otherwise the type's default applies.

  • addons/lighting/lib/model.js
  • lines 37 to 51
  • CadCraft 0.8.2
export function cleanFixture(data) {
  if (!data || typeof data !== 'object') return null;
  const type = BY_KEY.has(data.type) ? data.type : null;
  if (!type) return null;
  const u = data.u === 'in' ? 'in' : 'mm';
  const k = posNum(data.k) || 1;
  const fp = Number(data.footprint);
  return {
    u, k, type,
    pos: str(data.pos, 40), unit: str(data.unit, 16), chan: str(data.chan, 16), addr: str(data.addr, 16),
    purpose: str(data.purpose, 60), color: str(data.color, 40), note: str(data.note, 120),
    watts: posNum(data.watts), footprint: Number.isInteger(fp) && fp >= 1 && fp <= 512 ? fp : null,
    aim: Number.isFinite(Number(data.aim)) ? ((Number(data.aim) % 360) + 360) % 360 : 0,
  };
}

Wattage of a fixture

64-bit

The watts of a fixture: its own value, or its type's typical value.

A lookup with no arithmetic. The type values are whole watts from the table at the top of the file.

  • addons/lighting/lib/model.js
  • line 66
  • CadCraft 0.8.2
export const watts = (f) => f.watts ?? typeOf(f.type)?.watts ?? 0;
addons/lighting/main.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests None found by the review

Point at a distance along a position

64-bit

The drawing point a given distance along a position's line.

u = clamp((s − run) ÷ l, 0, 1); p = a + (b − a) × u

Walks the segments with a running sum of lengths and interpolates in the segment that contains s. A zero-length segment is guarded.

  • addons/lighting/main.js
  • lines 148 to 156
  • CadCraft 0.8.2
function alongPts(pts, s) {
  let run = 0;
  for (let i = 0; i + 1 < pts.length; i++) {
    const a = pts[i], b = pts[i + 1], l = Math.hypot(b.x - a.x, b.y - a.y);
    if (s <= run + l || i + 2 === pts.length) { const u = l ? Math.max(0, Math.min(1, (s - run) / l)) : 0; return { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u }; }
    run += l;
  }
  return pts[0];
}

Fixtures spread along a position

64-bit

Places n fixtures evenly along a position.

sᵢ = L × (i + 0.5) ÷ n

Each distance is computed from the index, not by adding a step, so spacing error does not build up. The count is rounded to a whole number from 1 to 200.

  • addons/lighting/main.js
  • lines 129 to 132
  • CadCraft 0.8.2
const n = Math.round(Number(await c.number(t('ask.fix.count'), { default: 4 })));
if (!(n >= 1 && n <= 200)) continue;
const L = lengthOf(pos.pts);
for (let i = 0; i < n; i++) { const p = alongPts(pos.pts, (L * (i + 0.5)) / n); add(p.x, p.y, pos.name); }

A position moved, turned or scaled

64-bit

Applies a move, rotation or scale to a position's points and its scale.

k′ = k × √(m₀² + m₁²); p′ = M · p

The scale is the length of the matrix's first column, so a different scale on the other axis is not seen.

  • addons/lighting/main.js
  • line 50
  • CadCraft 0.8.2
return { ...d, k: d.k * Math.hypot(m[0], m[1]) || d.k, pts: d.pts.map((p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] })) };
addons/lighting/lib/draw.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests None found by the review

Fixture symbol turned to its aim

64-bit

Rotates a fixture's symbol by its aim angle.

x′ = x cos a − y sin a; y′ = x sin a + y cos a

The cosine and sine are computed once per symbol. At 90, 180 and 270 degrees they are not exactly 0 or ±1, so symbol points are off by about 1e-16 of their size.

  • addons/lighting/lib/draw.js
  • lines 43 to 53
  • CadCraft 0.8.2
/** The entities turned by `a` radians about 0,0. */
function turn(list, a) {
  const c = Math.cos(a), s = Math.sin(a), P = (x, y) => [x * c - y * s, x * s + y * c];
  return list.map((e) => {
    if (e.type === 'line') { const [x1, y1] = P(e.x1, e.y1), [x2, y2] = P(e.x2, e.y2); return { ...e, x1, y1, x2, y2 }; }
    if (e.type === 'circle') { const [cx, cy] = P(e.cx, e.cy); return { ...e, cx, cy }; }
    if (e.type === 'arc') { const [cx, cy] = P(e.cx, e.cy); return { ...e, cx, cy, a0: e.a0 + a, a1: e.a1 + a }; }
    if (e.type === 'poly') return { ...e, pts: e.pts.map((p) => { const [x, y] = P(p.x, p.y); return { ...p, x, y }; }) };
    return e;
  });
}

Position length in millimeters

64-bit

The length of a position for the schedule and the capacity warning.

mm = length in drawing units ÷ k

One division of the summed length by k; a missing k counts as 1.

  • addons/lighting/lib/draw.js
  • lines 148 to 149
  • CadCraft 0.8.2
/** Position length in millimetres (for the schedule's "length" and the pipe's capacity warning). */
export const positionLength = (pos) => lengthOf(pos.pts) / (pos.k || 1);

Legend totals

64-bit

The total number of fixtures and total watts at the foot of the legend.

total = Σ count; total watts = Σ watts

Plain sums over the legend rows, written into the drawing as text without rounding.

  • addons/lighting/lib/draw.js
  • lines 115 to 121
  • CadCraft 0.8.2
let totalN = 0, totalW = 0;
for (const r of rows) {
  const cy = yy - rowH / 2;
  const sym = SYMBOLS[r.type];
  if (sym) out.push(...moveEnts(sym(), colX[0], cy, 0.8 * k));
  out.push(text(colX[1] - 160 * k, cy, th, r.name, 'left'), text(colX[2] - 150 * k, cy, th, r.count), text(colX[3] - 150 * k, cy, th, r.watts));
  totalN += r.count; totalW += r.watts;

In detail

Method
Polyline length by summed segment lengths; nearest point on a polyline by clamped projection per segment; even spread at (i + 0.5) ÷ n of the length; symbol rotation by the aim angle.
Number types
64-bit numbers.
Tolerances
A fixture belongs to the nearest named position within tol drawing units (settings.tol, default 500 mm).
Limits
Wattage defaults are typical values per fixture type from a table in model.js that each fixture can override.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach