Turns clicks and typed values into the arcs, rectangles, offsets, moves, rotations, scales, guides and dimensions that are stored in the 3D model.
How the precision is kept
Each tool works in 64-bit world coordinates in meters and converts to the open group's own coordinates with the group's inverse matrix just before the kernel is called. Typed values replace the pointer-derived value without passing through the readout. Angles near a multiple of 15 degrees are replaced by that multiple on purpose; the snap windows are fixed angles.
Calculations under this policy
18 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 radius, center and sweep of an arc through two points that stands off the chord by a given bulge.
R = (h² + (c ÷ 2)²) ÷ (2|h|); center = midpoint + side × sign(h) × (|h| − R); sweep = 4 × atan(2|h| ÷ c)
A bulge of 1e-6 m or less gives no arc. For a shallow bulge R is much larger than the chord, the center lies about R away, and the start direction is the normalized difference of two points that far apart.
m3d/tools.js
lines 600 to 610
CadCraft 0.8.2
const c = dist(this.p0, this.p1), m = mul(add(this.p0, this.p1), 0.5);
if (Math.abs(h) <= EPS) return null;
const u = norm(sub(this.p1, this.p0)), w = this.side;
const R = (h * h + (c / 2) ** 2) / (2 * Math.abs(h));
const ctr = add(m, mul(w, Math.sign(h) * (Math.abs(h) - R)));
const sweep = 4 * Math.atan(2 * Math.abs(h) / c);
const u0 = norm(sub(this.p0, ctr));
const n = norm(cross(u0, norm(sub(add(m, mul(w, h)), ctr))));
void u;
return arcPts(ctr, len(n) ? n : this.plane.n, u0, R, sweep, this.segs);
}
Sweep of an arc drawn from its center
64-bit
The angle from the arc's start direction to the pointer, counter-clockwise about the plane normal.
a = atan2((u × w)·n, u·w), plus 2π when a ≤ 0; snapped to k × π ÷ 12 when within 0.03 rad
atan2 of a cross and a dot product keeps its digits at every angle. An angle within 0.03 rad of a multiple of 15 degrees is replaced by that multiple, so the stored sweep is then exactly k × π ÷ 12 in 64-bit.
m3d/tools.js
lines 685 to 689
CadCraft 0.8.2
let a = Math.atan2(dot(cross(this.u, w), this.n), dot(this.u, w));
if (a <= 0) a += 2 * Math.PI;
const step = Math.PI / 12;
if (Math.abs(a - Math.round(a / step) * step) < 0.03) a = Math.round(a / step) * step; // 15° steps snap
this.sweep = a;
Rectangle width and height
64-bit
The two sides of a rectangle from its first corner to the pointer, along the plane's two axes.
w = (q − p₀)·u; h = (q − p₀)·v
Two dot products of the same difference vector with the plane's two axes. The values are passed on unrounded; the value box formats copies of them.
One division, guarded for a zero-length side, then Math.hypot. The result becomes the offset distance when the pointer sets it.
m3d/tools.js
lines 905 to 906
CadCraft 0.8.2
/** Distance from 2D point p to segment ab. */
const segD2 = (p, a, b) => { const dx = b[0] - a[0], dy = b[1] - a[1], L = dx * dx + dy * dy; const t = L ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L)) : 0; return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t); };
Offset distance and side
64-bit
The signed distance an outline is offset by: the least distance to any side, positive into a face.
d = ± min over sides of distance(p, side); inside by an even-odd crossing count
The minimum is taken over all sides in order, and the sign comes from the inside test (for a face) or from the side of the nearest segment (for a chain of edges). The value is not rounded before it is used.
m3d/tools.js
lines 971 to 982
CadCraft 0.8.2
let m = Infinity;
for (const L of this.loops2) for (let i = 0; i < L.length; i++) m = Math.min(m, segD2(p, L[i], L[(i + 1) % L.length]));
const inside = inLoop2(p, this.loops2[0]) && !this.loops2.slice(1).some((L) => inLoop2(p, L));
d = inside ? m : -m;
} else {
const P = this.base.pts.map((q) => to2(this.fr, q));
let m = Infinity, side = 1;
for (let i = 0; i + 1 < P.length + (this.base.closed ? 1 : 0); i++) {
const a = P[i], b = P[(i + 1) % P.length], dd = segD2(p, a, b);
if (dd < m) { m = dd; side = Math.sign((b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])) || 1; }
}
d = side * m;
Point inside an outline
64-bit
Whether the pointer lies inside a face's outline, which decides if an offset goes inward or outward.
count the sides whose ends straddle the point's height and cross to its right; inside when the count is odd
One division per straddling side and no tolerance, so a point exactly on the outline can fall either way. The division is by the side's height difference, which the straddle test has already shown to be non-zero.
m3d/tools.js
line 907
CadCraft 0.8.2
const inLoop2 = (p, L) => { let c = false; for (let i = 0, j = L.length - 1; i < L.length; j = i++) { const a = L[i], b = L[j]; if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]) c = !c; } return c; };
Push/pull distance
64-bit
The distance a face is pushed, in the open group's own scale, and the distance reported back.
kernel distance = d × k; reported d = returned distance ÷ k; k = |normal taken into the group's axes|
The world distance is multiplied by the group's scale along the face normal and the kernel's answer is divided by the same number. With collision on, the distance is replaced by the length of the move that is allowed.
m3d/tools.js
lines 1115 to 1121
CadCraft 0.8.2
if (this.coll && Math.abs(d) > EPS) {
const r = this.coll.clamp(mul(this.n, d));
if (r.hit) { d = Math.sign(d) * len(r.delta); this.stopped = true; }
}
this.d = d;
const r = this.h.model.preview((m) => pushPull(m.context(this.h.path).def, this.fid, d * k), this.scope());
if (r && r.ok) this.d = r.distance / k;
Move and copy
64-bit
The translation applied to the selection, in the open group's own coordinates.
The transform of each copy when a copy is repeated (3x) or divided (/3).
copy k: T(k × delta) or R(k × angle); divided: k ÷ n of the delta or angle
Every copy is made from the original with its own multiple of the move or angle. Nothing is accumulated from one copy to the next, so the last copy carries one rounding, not n.
m3d/tools.js
lines 1341 to 1349
CadCraft 0.8.2
const step = (f) => (rot ? rotation(T.localDir(rot.n), rot.a * f, T.local(rot.c)) : translation(T.localDir(mul(delta, f))));
if (n < 1 || n > 500) return false;
this.h.model.undo();
const made = this.h.model.change(v.kind === 'array' ? 'Copy ×' + n : 'Copy /' + n, (m) => {
const def = m.context(this.h.path).def;
const out = [];
for (let k = 1; k <= n; k++) {
const s = copySel(def, src);
transformSel(def, s, step(v.kind === 'array' ? k : k / n));
Rotation angle
64-bit
The angle the selection is turned by, from the reference direction to the pointer.
a = atan2((ref × v)·n, ref·v); snapped to k × π ÷ 12 when within 2.5°
The pointer angle comes from atan2 of a cross and a dot product, which keeps its digits at every angle. An angle within 2.5 degrees of a multiple of 15 degrees is replaced by that multiple, and the unrounded angle is what is applied.
m3d/tools.js
lines 1401 to 1404
CadCraft 0.8.2
let a = Math.atan2(dot(cross(this.ref, v), this.n), dot(this.ref, v));
const step = Math.PI / 12, snapped = Math.round(a / step) * step;
if (Math.abs(a - snapped) < 2.5 * Math.PI / 180) a = snapped;
this.apply(a);
Uniform scale factor
64-bit
The factor the selection is scaled by about a fixed point, from a reference point to the pointer.
f = max(0.001, ((p − base)·u) ÷ (u·u)), u = ref − base
One projection and one division by the squared length of the reference vector. The factor is clamped at 0.001 from below and applied unrounded.
m3d/tools.js
lines 1598 to 1600
CadCraft 0.8.2
const u = sub(this.ref, this.base);
const f = Math.max(0.001, dot(sub(r.p, this.base), u) / dot(u, u));
this.apply(f);
Stretch by a grip
64-bit
The matrix that scales the selection by separate factors along the group's three axes about an anchor.
M = T(a) × diag(fx, fy, fz) × T(−a)
Two 4×4 products. The translation column comes out as a − f × a, which loses digits when the anchor is far from the group's origin and the factor is close to 1. The value box rounds to 4 decimals; the matrix does not.
m3d/tools.js
lines 1559 to 1569
CadCraft 0.8.2
/** Factors per axis (the open group's red, green, blue) about the anchor, previewed. */
applyAxes(fs) {
const sel = this.h.sel.filter((s) => s.t !== 'g' && s.t !== 'd' && s.t !== 's');
const a = this.gripAnchor;
const S = [fs[0], 0, 0, 0, 0, fs[1], 0, 0, 0, 0, fs[2], 0, 0, 0, 0, 1];
const m4 = mmul(translation(a), mmul(S, translation(mul(a, -1))));
this.fs = fs;
this.h.model.preview((m) => transformSel(m.context(this.h.path).def, sel, m4), this.scope());
this.h.changed();
this.h.vcb(t('m3d.vcb.scale'), fs.map((x) => String(+x.toFixed(4))).join(', '));
}
Guide line offset from an edge
64-bit
The perpendicular offset of a new guide line from the edge or guide it was measured from.
w = w₀ − dir × (w₀·dir), w₀ = pointer − start
The part of the pointer vector along the direction is subtracted. The formula assumes the direction has length 1.
Where the dimension line sits: the pointer's offset square to the measured line.
off = q − ab × (q·ab), q = pointer on the plane − first point, ab = unit direction of the measured line
The pointer is put on a plane through the first point, and the component along the measured line is subtracted from its vector. The offset is stored unrounded.
m3d/tools.js
lines 2079 to 2080
CadCraft 0.8.2
const ray = this.h.cam.ray(ev.x, ev.y), g = rayPlane(ray.o, ray.d, planeFrom(n, this.pa));
if (g) { const q = sub(g.p, this.pa); this.off = sub(q, mul(ab, dot(q, ab))); }
Terrain brush weights
64-bit
How much each terrain point under the round brush is raised or lowered.
w = 0.5 × (1 + cos(π × d ÷ R)) for d < R; new z = z + w × Δz
The distance is horizontal (Math.hypot of x and y) in the group's own units; the radius is scaled by the group's scale along its x axis. Only points that belong to triangles alone are moved.
m3d/tools.js
lines 2190 to 2204
CadCraft 0.8.2
/** The vertices the brush moves around local point p, with their weights and heights now. */
brush(p) {
const def = this.T.ctx().def, R = this.radius * (len(this.T.localDir([1, 0, 0])) || 1); // the brush in the group's own units
const tri = new Set();
for (const f of def.faces.values()) if (f.loops.length === 1 && f.loops[0].length === 3) for (const v of f.loops[0]) tri.add(v);
const notTri = new Set();
for (const f of def.faces.values()) if (!(f.loops.length === 1 && f.loops[0].length === 3)) for (const l of f.loops) for (const v of l) notTri.add(v);
const out = [];
for (const [v, q] of def.verts) {
const d = Math.hypot(q[0] - p[0], q[1] - p[1]);
if (d >= R || notTri.has(v) || !tri.has(v)) continue;
out.push({ v, w: 0.5 * (1 + Math.cos((Math.PI * d) / R)), z: q[2] });
}
return out;
}
Selection bounding box
64-bit
The box around the selection in world coordinates; its middle is the center for turning and scaling from the keyboard.
min and max per axis over the transformed points; center = (min + max) × 0.5
Each point is transformed by the group's matrix and compared; nothing is accumulated. A placed group contributes only the two corners of its own transformed box.
m3d/tools.js
lines 172 to 191
CadCraft 0.8.2
/** The selection's bounding box {min, max} in world coordinates, or null. */
selBox() {
const m = this.host.model, c = this.ctx(), def = c.def;
let bb = null;
const put = (p) => {
if (!bb) bb = { min: p.slice(), max: p.slice() };
for (let i = 0; i < 3; i++) { bb.min[i] = Math.min(bb.min[i], p[i]); bb.max[i] = Math.max(bb.max[i], p[i]); }
};
for (const s of this.host.sel) {
if (s.t === 'e') { const e = def.edges.get(s.id); if (e) { put(apply(c.m, def.verts.get(e.a))); put(apply(c.m, def.verts.get(e.b))); } }
else if (s.t === 'f') { const f = def.faces.get(s.id); if (f) for (const l of f.loops) for (const v of l) put(apply(c.m, def.verts.get(v))); }
else if (s.t === 'i') {
const inst = def.insts.get(s.id), d = inst && m.def(inst.def);
const ib = d && m.bbox(d, mmul(c.m, inst.m));
if (ib) { put(ib.min); put(ib.max); }
}
}
for (const s of this.host.sel) if (s.t === 'g') { const g = m.guides.get(s.id); if (g) put(g.p); }
return bb;
}
Frame of a placed component
64-bit
The three axis directions a component gets when it is laid on a face.
z = face normal toward the eye; x = red axis less its part along z (or blue × z); y = z × x
x is normalized and y is a cross product of two unit vectors. The fallback to blue × z is taken when the projected red axis is shorter than 1e-6 or the face is more than about 26 degrees from level (|z·blue| < 0.9).
m3d/tools.js
lines 2379 to 2389
CadCraft 0.8.2
/** The world frame the copy gets at snap r: [x, y, z] directions. */
frameAt(r) {
const A = this.T.axes();
if (!this.align || !r.face?.n) return [A.x, A.y, A.z];
let z = norm(r.face.n);
if (dot(z, sub(this.h.cam.eye, r.p)) < 0) z = mul(z, -1);
let x = sub(A.x, mul(z, dot(A.x, z)));
if (len(x) < 1e-6 || Math.abs(dot(z, A.z)) < 0.9) { x = cross(A.z, z); if (len(x) < 1e-6) x = A.x; }
x = norm(x);
return [x, cross(z, x), z];
}
In detail
Method
Closed forms: arc radius and center from chord and bulge, angles from atan2 of a cross and a dot product, scale factors from a projection, offsets from the pointer's distance to the outline.
Number types
64-bit numbers. The pointer position is in CSS pixels and enters only through the camera ray.
Tolerances
EPS = 1e-6 m is the smallest length, bulge, offset or move a tool accepts. Angle snaps: 0.03 rad for an arc from its center, 2.5 degrees for rotate, 0.035 rad for the protractor. A rotation below 1e-9 rad and a scale within 1e-9 of 1 are dropped. A scale factor is not allowed below 0.001.
Limits
A shallow two-point arc is built from a distant center, so its points carry the cancellation of center plus radius. Values shown in the value box are rounded; the stored values are not.
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.