Decides the plan point a click becomes when it follows an angle step, a length step or a wall, and offsets a path drawn on a wall face to the wall's center line.
How the precision is kept
The pointer position arrives in plan millimeters and the snapped point is computed in 64-bit numbers. Angles are rounded to the step in degrees and turned back into a direction with cosine and sine; lengths can be rounded to a step. The resulting point is what the wall tool stores.
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.
The point at an orthogonal or stepped angle from the last point, stopping at a wall it crosses or at a rounded length.
θ = round(θ ÷ step) × step; l = len × cos(θ₀ − θ); p = from + l × (cos θ, sin θ)
The angle is rounded in degrees (to 90° with ortho, else to the step when within 3°) and the direction is cos and sin of θ × π ÷ 180, so at 90° the x component is a tiny non-zero number rather than exactly 0. The length is the pointer's projection on that direction, optionally rounded to a length step with a minimum of one step. A crossing with a wall line is solved by cross products and skipped when |cross| < 1e-9.
model-tools.js
lines 63 to 86
BimCraft 0.8.1
let ang = Math.atan2(dy, dx) / D2R, kind = null;
if (ortho) { ang = Math.round(ang / 90) * 90; kind = 'ortho'; }
else if (step > 0) { const s = Math.round(ang / step) * step; if (Math.abs(s - ang) <= 3) { ang = s; kind = 'angle'; } }
if (kind) {
let l = len * Math.cos((Math.atan2(dy, dx) / D2R - ang) * D2R);
// where this direction crosses a wall (its centre line or a face) near the pointer, stop there
const dir = [Math.cos(ang * D2R), Math.sin(ang * D2R)];
let hit = null;
for (const w of [...walls, ...lines]) {
for (const off of w.grid ? [0] : [0, w.t / 2, -w.t / 2]) {
const L = dist(w.a, w.b); if (!L) continue;
const u = [(w.b[0] - w.a[0]) / L, (w.b[1] - w.a[1]) / L], n = [-u[1], u[0]];
const a = [w.a[0] + n[0] * off, w.a[1] + n[1] * off];
const c = dir[0] * u[1] - dir[1] * u[0];
if (Math.abs(c) < 1e-9) continue;
const s = ((a[0] - from[0]) * u[1] - (a[1] - from[1]) * u[0]) / c, tt = ((a[0] - from[0]) * dir[1] - (a[1] - from[1]) * dir[0]) / c;
if (s > 1 && tt >= -1 && tt <= L + 1 && Math.abs(s - l) <= tol && (!hit || Math.abs(s - l) < Math.abs(hit.s - l))) hit = { s, w };
}
}
if (hit) return { p: [from[0] + dir[0] * hit.s, from[1] + dir[1] * hit.s], kind: 'on', angle: ang, wall: hit.w.grid ? undefined : hit.w };
const aligned = align && alignment(p, dir);
if (aligned) return { ...aligned, angle: ang };
if (round > 0) l = Math.max(round, Math.round(l / round) * round);
return { p: [from[0] + dir[0] * l, from[1] + dir[1] * l], kind, angle: ang };
Nearest point on a wall line or face
64-bit
The point on a wall's center line or face nearest the pointer.
s = clamp((p − a)·u, 0, L); q = a + s·u
The projection is a dot product with the unit direction, clamped to the wall's length. Faces are the center line moved by half the thickness along the normal.
model-tools.js
lines 91 to 102
BimCraft 0.8.1
let on = null;
for (const w of [...walls, ...lines]) {
const L = dist(w.a, w.b); if (!L) continue;
const u = [(w.b[0] - w.a[0]) / L, (w.b[1] - w.a[1]) / L], n = [-u[1], u[0]];
for (const off of w.grid ? [0] : [0, w.t / 2, -w.t / 2]) {
const a = [w.a[0] + n[0] * off, w.a[1] + n[1] * off];
const s = Math.max(0, Math.min(L, (p[0] - a[0]) * u[0] + (p[1] - a[1]) * u[1]));
const q = [a[0] + u[0] * s, a[1] + u[1] * s], d = dist(p, q);
if (d <= tol * 0.6 && (!on || d < on.d)) on = { p: q, d, w };
}
}
if (on) return { p: on.p, kind: 'on', wall: on.w.grid ? undefined : on.w };
Signed area of a drawn loop
64-bit
The signed area of a closed chain of points, used to tell which side is outside.
A = ½ Σ (xᵢ·yᵢ₊₁ − yᵢ·xᵢ₊₁)
The shoelace sum in point order. Only its sign is used by loopSide, and a result of exactly 0 counts as clockwise.
model-tools.js
lines 114 to 115
BimCraft 0.8.1
/** Signed area of a closed loop of points (anticlockwise > 0). */
export function loopArea(points) { let s = 0; for (let i = 0; i < points.length; i++) { const a = points[i], b = points[(i + 1) % points.length]; s += a[0] * b[1] - a[1] * b[0]; } return s / 2; }
Center lines for a path drawn on a wall face
64-bit
Offsets a drawn path by half the wall thickness and mitres the corners.
offset = ± t ÷ 2 along the left normal; corner = intersection of neighboring offset lines
Segments shorter than 1 mm are dropped. Neighboring offset lines are intersected by cross products of the unscaled segment vectors; the parallel test |cross| < 1e-9 is in mm² and is not scaled to the segment lengths. A corner more than 4 thicknesses from either segment end is not applied.
model-tools.js
lines 118 to 142
BimCraft 0.8.1
/** Centre lines for a path drawn on a wall face. Outside is explicitly left/right of travel. */
export function wallPath(points, thickness, reference = 'center', side = 'left', axisOffset = undefined) {
// Optional authoritative location-line offset; retain the established face-chain/miter algorithm.
const off = axisOffset ?? (reference === 'center' ? 0 : thickness / 2 * (reference === 'outside' ? -1 : 1) * (side === 'left' ? 1 : -1));
const segs = [];
for (let i = 1; i < points.length; i++) {
const a = points[i - 1], b = points[i], L = dist(a, b);
if (L < 1) continue;
const n = [-(b[1] - a[1]) / L * off, (b[0] - a[0]) / L * off];
segs.push({ a: [a[0] + n[0], a[1] + n[1]], b: [b[0] + n[0], b[1] + n[1]], t: thickness });
}
const join = (a, b) => {
const u = [a.b[0] - a.a[0], a.b[1] - a.a[1]], v = [b.b[0] - b.a[0], b.b[1] - b.a[1]];
const cross = u[0] * v[1] - u[1] * v[0];
if (Math.abs(cross) < 1e-9) return;
const d = [b.a[0] - a.a[0], b.a[1] - a.a[1]], k = (d[0] * v[1] - d[1] * v[0]) / cross;
const q = [a.a[0] + k * u[0], a.a[1] + k * u[1]];
// Avoid unbounded mitres at nearly reversing corners.
if (dist(q, a.b) > 4 * thickness || dist(q, b.a) > 4 * thickness) return;
a.b = q; b.a = q;
};
for (let i = 1; i < segs.length; i++) join(segs[i - 1], segs[i]);
if (segs.length >= 3 && dist(points[0], points[points.length - 1]) < 1) join(segs[segs.length - 1], segs[0]);
return segs;
}
In detail
Method
Polar point from a rounded angle and a projected length; line and line intersection by cross products; nearest point on a segment by clamped projection; mitred offset of a chain.
Number types
64-bit numbers in plan millimeters.
Tolerances
Angle step held when within 3° of the pointer. Parallel test |cross| < 1e-9 on unit vectors (snap) and on unscaled segment vectors (wallPath). Segments shorter than 1 mm are skipped. A mitre more than 4 thicknesses from a segment end is not applied. The snap distance tol is passed in by the page.
Limits
A direction at 90° or 270° is cos and sin of degrees × π ÷ 180, so the component that should be 0 is a tiny non-zero number.
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.