Turns a member's start point, end point and rotation into its own axes, and moves points between the model and a member's own coordinates.
How the precision is kept
Plain 64-bit numbers in arrays of three, millimeters, z up. The member direction is the difference of the two end points scaled by one over its length from Math.hypot; the section axes come from one projection and one cross product. A member counts as vertical when the z part of its unit direction is above 0.999999 (within about 0.08 degrees of vertical) and then takes fixed axes.
Calculations under this policy
5 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 origin, the unit direction, the two section axes and the length of a member line.
x = (e − s) ÷ |e − s|; v = unit(Z − x·x_z); u = v × x; u′ = cos a·u + sin a·v, v′ = −sin a·u + cos a·v
The direction is the difference multiplied by 1 ÷ L rather than divided by L, which can differ from a true division in the last bit. For a member just under the 0.999999 threshold, Z − x·x_z has a z part 1 − x_z² that subtracts nearly equal numbers before it is normalized. The rotation is degrees × Math.PI ÷ 180.
geom.js
lines 25 to 40
DetailCraft 0.8.1
/** Frame of a line from s to e with rotation (deg): { o, x, u, v, len }. */
export function frame(s, e, rotation = 0) {
const d = sub(e, s), L = len(d);
const x = L > 0 ? mul(d, 1 / L) : [1, 0, 0];
let v;
if (Math.abs(x[2]) > 0.999999) v = [1, 0, 0];
else v = norm(sub([0, 0, 1], mul(x, x[2])));
let u = cross(v, x);
if (rotation) {
const a = rotation * Math.PI / 180, c = Math.cos(a), sn = Math.sin(a);
// rotate about +x: u' = c·u + s·v, v' = −s·u + c·v
const u2 = add(mul(u, c), mul(v, sn)), v2 = add(mul(u, -sn), mul(v, c));
u = u2; v = v2;
}
return { o: s, x, u, v, len: L };
}
Local point to model point
64-bit
The model coordinates of a point given along the member and across its section.
p = o + x·x̂ + u·û + (v + voff)·v̂
Three scaled axes added to the origin in a fixed order, all 64-bit. No tolerance and no rounding.
geom.js
lines 47 to 50
DetailCraft 0.8.1
/** World point of local (x along, u, v) in a frame with a v offset (section coordinates centered). */
export function toWorld(f, x, u, v, voff = 0) {
return add(add(add(f.o, mul(f.x, x)), mul(f.u, u)), mul(f.v, v + voff));
}
Model point to local point
64-bit
Where a model point lies along a member and across its section.
d = p − o; x = d·x̂, u = d·û, v = d·v̂ − voff
The frame origin is subtracted first, then three dot products are taken, so the products work on distances from the member and not on the model's absolute coordinates.
geom.js
lines 51 to 55
DetailCraft 0.8.1
/** Local {x, u, v} (section-centered) of a world point. */
export function toLocal(f, p, voff = 0) {
const d = sub(p, f.o);
return { x: dot(d, f.x), u: dot(d, f.u), v: dot(d, f.v) - voff };
}
End face position on a plane-cut end
64-bit
How far along the part its end face is at a given point of the section, for square ends and mitre cuts.
x = x0 + ku·u + kv·v
One 64-bit expression. A square end returns 0 or the stored length unchanged.
geom.js
lines 57 to 65
DetailCraft 0.8.1
/**
* Where a part's end face is, along the part (x from its cut start), at section point (u, v) (v from the section's center):
* a square end is at 0 / length; a plane cut (a mitre, 0.7.1 71.20) is `part.endCuts[end]` = { x0, ku, kv }, x = x0 + ku·u + kv·v.
*/
export function endX(part, end, u = 0, v = 0) {
const c = part.endCuts && part.endCuts[end];
if (!c) return end === 'start' ? 0 : part.length;
return c.x0 + c.ku * u + c.kv * v;
}
Section outline clipped at a height
64-bit
The part of a section outline below or above a line, used for coped ends.
t = (limit − a_v) ÷ (b_v − a_v); new point = (a_u + (b_u − a_u)·t, limit)
Sutherland–Hodgman on one line. The side test allows 1e-9 mm, absolute. The division only runs when the two points are on different sides, so their v values differ; the new point's v is set to the limit itself, not computed.
geom.js
lines 75 to 89
DetailCraft 0.8.1
/** Clips a section loop to v ≤ vmax (or v ≥ vmin with below = false). Sutherland–Hodgman on one line. */
export function clipLoopV(loop, limit, keepBelow = true) {
const inside = (p) => keepBelow ? p[1] <= limit + 1e-9 : p[1] >= limit - 1e-9;
const out = [];
for (let i = 0; i < loop.length; i++) {
const a = loop[i], b = loop[(i + 1) % loop.length];
const ia = inside(a), ib = inside(b);
if (ia) out.push(a);
if (ia !== ib) {
const t = (limit - a[1]) / (b[1] - a[1]);
out.push([a[0] + (b[0] - a[0]) * t, limit]);
}
}
return out.length >= 3 ? out : [];
}
In detail
Method
x = (end − start) ÷ length; v = global Z made square to x (global X for a vertical member); u = v × x; a rotation in degrees then turns u and v about x.
Number types
64-bit numbers in plain arrays.
Tolerances
Vertical test: |x_z| > 0.999999 on a unit vector. Loop clipping: 1e-9 mm, absolute, on the side test.
Limits
A line of zero length gets the direction [1, 0, 0]. Either side of the vertical threshold the rule for the section axes changes, so a member within about 0.08 degrees of vertical is oriented by the vertical rule.
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.