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