Resources Numerical accuracy Calculation policy

Freeform cage edits from the workspace

Turns gizmo drags, clicks on the ground plane and edge extrusions in the Form workspace into new positions of the control cage that defines a freeform shape.

How the precision is kept

The cage's vertices are plain arrays of 64-bit numbers and the edits here are short 64-bit expressions. A point clicked on the ground plane is rounded to 0.001 before it is stored. The transform itself is applied by form.js; this file builds the matrix from the gizmo with three.js Matrix4. Drawing buffers and analysis colors in this file are 32-bit and only feed the screen.

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.

ws-form.js PartCraft 0.8.1 64-bit 4 calculations Reviewed

Tests None found by the review

Cage move from the gizmo

64-bit

The transform applied to the selected cage vertices while the gizmo is dragged.

M = W_now × W_start⁻¹

The matrix is the pivot's current world matrix times the inverse saved when the drag began, both computed by three.js Matrix4, and is always applied to the cage as it was at the start of the drag, so repeated updates during one drag do not accumulate. The application to the vertices is in form.js transform.

  • ws-form.js
  • lines 191 to 196
  • PartCraft 0.8.1
tc.addEventListener('objectChange', () => {
  if (!S.drag) return;
  S.pivot.updateMatrixWorld(true);
  const M = S.pivot.matrixWorld.clone().multiply(S.drag.inv);
  S.drag.last = FM.transform(S.drag.cage, S.sel, M.elements, { soft: S.soft });
  previewCage(host, S.drag.last);

Point for a new face

64-bit

The position stored when a face is drawn by clicks.

existing vertex within 9 px, else ray ∩ plane y = 0 rounded to 0.001

A click near an existing cage vertex copies that vertex exactly. Otherwise the ray's hit on the ground plane is rounded with Math.round(v × 1000) ÷ 1000 before it is kept, so the stored coordinates are multiples of 0.001.

  • ws-form.js
  • lines 249 to 264
  • PartCraft 0.8.1
function faceClick(host, x, y) {
  const { THREE, view } = host;
  const cage = cageOf(host) || FM.emptyForm();
  let p = null;
  cage.verts.forEach((q) => { const s = project(host, q); if (!p && Math.hypot(s[0] - x, s[1] - y) < 9) p = [...q]; });
  if (!p) {
    const ray = view.ray(x, y).ray;
    const hit = ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), new THREE.Vector3());
    if (!hit) { host.status(t('form.face.click_ground')); return; }
    p = hit.toArray().map((v) => Math.round(v * 1000) / 1000);
  }
  S.faceDots.push(p);
  drawDots(host);
  if (S.faceDots.length === 4) finishFace(host);
  else host.status(t('form.face.point', { n: S.faceDots.length }), true);
}

Direction of an edge extrusion

64-bit

The direction and distance open edges are extruded by.

d = Σ (edge middle − face center); offset = d ÷ |d| × distance

Face centers and edge middles are 64-bit means of cage vertices. The sum is normalized with Math.hypot, with 1 used when its length is zero, in which case the offset is zero.

  • ws-form.js
  • lines 296 to 304
  • PartCraft 0.8.1
let d = [0, 0, 0];
for (const k of S.sel.edges) {
  const e = topo.E.get(k); if (!e || e.f.length !== 1) throw new Error(t('form.err.extrude_open'));
  const f = c.faces[e.f[0]]; const cen = f.reduce((s, v) => s.map((x, i) => x + c.verts[v][i] / f.length), [0, 0, 0]);
  const mid = [0, 1, 2].map((i) => (c.verts[e.a][i] + c.verts[e.b][i]) / 2);
  d = d.map((x, i) => x + mid[i] - cen[i]);
}
const l = Math.hypot(...d) || 1;
return FM.extrudeEdges(c, S.sel.edges, d.map((x) => x / l * S.dist));

Tolerance for joining a new face

64-bit

The distance within which a new face's points join existing cage vertices.

tol = 10⁻⁶ × max(1, size setting)

The tolerance is scaled to the workspace's primitive size setting, not to the cage's actual extent, with a floor of 1e-6. The merge itself is done by addFace in form.js.

  • ws-form.js
  • lines 274 to 281
  • PartCraft 0.8.1
function finishFace(host) {
  const pts = S.faceDots;
  S.faceDots = [];
  drawDots(host);
  const cage = cageOf(host) || FM.emptyForm();
  try { const next = FM.addFace(cage, pts, { tol: 1e-6 * Math.max(1, S.size) }); save(host, next, t('form.undo.add_face')); host.status(t('form.face.added'), true); }
  catch (e) { host.status(t('form.face.error', { msg: e.message })); }
}

In detail

Method
Gizmo move, rotate and scale as M = pivot world matrix × inverse of the matrix at the start of the drag. Clicked face points from a ray against the plane y = 0. Edge extrusion along the normalized sum of (edge middle − face center).
Number types
64-bit numbers for cage vertices and directions; Float32Array only for display buffers and vertex colors.
Tolerances
A clicked point reuses a cage vertex within 9 pixels. A new ground point is rounded to 0.001. New faces are merged with a tolerance of 1e-6 × max(1, the primitive size setting).
Limits
Only points clicked on the ground plane are rounded to 0.001; positions dragged with the gizmo are not rounded in this file.

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.

Every policy that takes this approach