Builds the 20 mm starter shapes (box, cylinder, cone, sphere, roof) and moves placements so their boxes line up or stand on the ground.
How the precision is kept
Every shape is built from the constant SIZE = 0.02 m and R = SIZE ÷ 2 in 64-bit numbers. Round shapes are polygons with a fixed number of sides (32 round the cylinder and cone, 24 by 12 on the sphere). Align and drop to ground read each placement's bounding box and add one difference to the translation part of its matrix; moves smaller than 1e-9 model units are skipped.
Calculations under this policy
6 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.
A 20 mm cube and a 20 mm cylinder, standing on z = 0 and centered on the origin.
S = 0.02; R = S ÷ 2; box: S × S square pulled up by S; cylinder: 32-sided polygon of radius R pulled up by S
S = 0.02 is the nearest 64-bit number to 0.02 and R = S ÷ 2 is exact from it. The cylinder is a 32-sided prism, so its footprint is a polygon and not a circle.
A cone with a 32-sided base of radius R and its apex at height S.
The base corners come from ngonPts and the apex is the exact point [0, 0, S]; each side is one triangle between two neighboring base corners and the apex. No tolerance is used here.
Each corner is one 64-bit expression. The bottom pole is exactly [0, 0, 0]; at the top pole sin θ evaluates to about 1.2 × 10⁻¹⁶ instead of 0, so the points computed for it differ by about 10⁻¹⁸ m, and the seam column j = 24 is computed with φ = 2π rather than reused from j = 0 and differs from it by up to about 2.4 × 10⁻¹⁸ m. Both are passed to meshDef as computed.
beginner.js
lines 46 to 57
ModelCraft 0.8.1
sphere: (m, name) => {
const seg = 24, rings = 12, polys = [];
const p = (i, j) => { const th = Math.PI * i / rings, ph = 2 * Math.PI * j / seg; return [R * Math.sin(th) * Math.cos(ph), R * Math.sin(th) * Math.sin(ph), R - R * Math.cos(th)]; };
for (let i = 0; i < rings; i++) for (let j = 0; j < seg; j++) {
const a = p(i, j), b = p(i + 1, j), c = p(i + 1, j + 1), dd = p(i, j + 1);
// wound outward (counter-clockwise seen from outside); the poles' quads lose their zero-length side
if (i === 0) polys.push({ loops: [[a, c, b]] });
else if (i === rings - 1) polys.push({ loops: [[a, dd, b]] });
else polys.push({ loops: [[a, dd, c, b]] });
}
return meshDef(m, polys, name, 'group');
},
Roof
64-bit
A triangular prism: a triangle across x pulled along y from −R to +R.
The triangle is drawn at y = −R and pulled by S. If the lowest y afterwards is more than 1e-9 below −R the pull went the other way and every point is shifted by +S; the 1e-9 is absolute, in model units.
beginner.js
lines 58 to 67
ModelCraft 0.8.1
roof: (m, name) => {
// a triangle across red (x), pulled along green (y) from −R to +R
const d = m.newDef('group', name);
addPolygon(d, [[-R, -R, 0], [R, -R, 0], [0, -R, S]]);
pushPull(d, [...d.faces.keys()][0], S);
let lo = Infinity;
for (const p of d.verts.values()) lo = Math.min(lo, p[1]);
if (lo < -R - 1e-9) for (const [id, p] of d.verts) d.verts.set(id, [p[0], p[1] + S, p[2]]); // it went the other way
return d;
},
Align
64-bit
Moves each selected placement so the minimum, middle or maximum of its box on one axis meets that of the whole selection.
target = lo, hi or (lo + hi) ÷ 2; move = target − own value; m[12 + axis] += move
The target and each placement's own value are read from bounding boxes and subtracted once; the difference is added to the translation entry of the matrix. A move with |d| < 1e-9 (absolute, model units) is skipped, so placements already within that distance are left as they are.
beginner.js
lines 115 to 131
ModelCraft 0.8.1
export function align(m, ctxDef, iids, axis, where) {
const boxes = iids.map((iid) => [iid, placementBox(m, ctxDef, iid)]).filter(([, b]) => b);
if (boxes.length < 2) return 0;
let lo = Infinity, hi = -Infinity;
for (const [, b] of boxes) { lo = Math.min(lo, b.min[axis]); hi = Math.max(hi, b.max[axis]); }
const target = where === 'min' ? lo : where === 'max' ? hi : (lo + hi) / 2;
let n = 0;
for (const [iid, b] of boxes) {
const at = where === 'min' ? b.min[axis] : where === 'max' ? b.max[axis] : (b.min[axis] + b.max[axis]) / 2;
const d = target - at;
if (Math.abs(d) < 1e-9) continue;
const v = [0, 0, 0]; v[axis] = d;
moveBy(ctxDef.insts.get(iid), v[0], v[1], v[2]);
n++;
}
return n;
}
Drop to ground
64-bit
Moves each selected placement up or down so the bottom of its box is at z = 0.
m[14] += −box.min.z
One subtraction per placement, from the box's lowest z. A placement whose lowest z is within 1e-9 of zero (absolute) is not moved.
beginner.js
lines 133 to 142
ModelCraft 0.8.1
export function dropToGround(m, ctxDef, iids) {
let n = 0;
for (const iid of iids) {
const b = placementBox(m, ctxDef, iid);
if (!b || Math.abs(b.min[2]) < 1e-9) continue;
moveBy(ctxDef.insts.get(iid), 0, 0, -b.min[2]);
n++;
}
return n;
}
In detail
Method
Corner coordinates from sine and cosine of evenly spaced angles; align moves each box's minimum, middle or maximum to that of the whole selection.
Number types
64-bit numbers throughout; matrices are plain arrays of 16 numbers.
Tolerances
1e-9 model units, absolute: a move smaller than this is skipped in align and dropToGround, and the roof is shifted back only if it went more than 1e-9 past −R.
Limits
The round shapes are faceted, so their volume and surface are those of the polygons, not of a true cylinder, cone or sphere.
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.