Resources Numerical accuracy Calculation policy

Illustrate: projection, hidden lines and vector pictures

Projects a manual view onto the picture, removes hidden lines and writes the result as line art and PDF pages.

How the precision is kept

These files draw pictures: the view on screen and the SVG and PDF output. The math is 64-bit on the manual's 32-bit meshes, with tolerances in picture units. PDF coordinates are written with 3 decimals of a millimeter.

Calculations under this policy

4 calculations in 4 source files. 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.

illustrate/camera.js PartCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests tools/partcraft-test/test-illustrate.mjs

Projection onto the picture

32-bit, display only

A model point's place in the picture and its depth.

orthographic: u = aspect ÷ 2 + q·right ÷ 2h; perspective: u = aspect ÷ 2 + q·right ÷ (2 tan(fov ÷ 2) × depth)

The view axes are unit vectors from two cross products. In perspective the depth is floored at 1e-9 before dividing. The result places lines in a picture.

  • illustrate/camera.js
  • lines 15 to 48
  • PartCraft 0.8.1
export function projector(cam, aspect = 1) {
  const dir = norm(sub(cam.target, cam.pos));
  let right = norm(cross(dir, cam.up));
  if (len(right) < 1e-9) right = norm(cross(dir, Math.abs(dir[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0]));
  const up = cross(right, dir);
  const dist = len(sub(cam.target, cam.pos));
  if (cam.type === 'persp') {
    const tanH = Math.tan((cam.fov || 35) * Math.PI / 360);   // half the vertical field
    return {
      type: 'persp', aspect, right, up, dir, mmPerUnit: 2 * dist * tanH,
      project(p) {
        const q = sub(p, cam.pos), d = dot(q, dir);
        const k = 1 / (2 * tanH * Math.max(d, 1e-9));
        return [aspect / 2 + dot(q, right) * k, 0.5 + dot(q, up) * k, d];
      },
      ray(u, v) {
        const x = (u - aspect / 2) * 2 * tanH, y = (v - 0.5) * 2 * tanH;
        return { o: [...cam.pos], d: norm(add(dir, add(mul(right, x), mul(up, y)))) };
      },
    };
  }
  const h = cam.height || 100;
  return {
    type: 'ortho', aspect, right, up, dir, mmPerUnit: 2 * h,
    project(p) {
      const q = sub(p, cam.target);
      return [aspect / 2 + dot(q, right) / (2 * h), 0.5 + dot(q, up) / (2 * h), dot(sub(p, cam.pos), dir)];
    },
    ray(u, v) {
      const o = add(cam.target, add(mul(right, (u - aspect / 2) * 2 * h), mul(up, (v - 0.5) * 2 * h)));
      return { o: sub(o, mul(dir, dist + 1e6)), d: dir };
    },
  };
}
illustrate/hlr.js PartCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests tools/partcraft-test/test-illustrate.mjs

Is a picture point hidden

32-bit, display only

Decides whether a point of an edge is behind a triangle of another surface.

barycentric l₁, l₂, l₃ in the picture; hidden when the triangle's depth < point depth − 10 eps

Triangles whose picture determinant is below 1e-18 are skipped, and a point within 1e-7 of a triangle's edge does not count as covered. Depth is interpolated, perspective-correct for a perspective camera.

  • illustrate/hlr.js
  • lines 115 to 127
  • PartCraft 0.8.1
function occluded(u, v, d, self = -1) {
  const i = Math.min(G - 1, Math.max(0, Math.floor((u - u0) / cw))), j = Math.min(G - 1, Math.max(0, Math.floor((v - v0) / ch)));
  for (const k of grid[j * G + i]) {
    const p = parts[occ[k * 2]], tt = occ[k * 2 + 1], I = p.mesh.indices, Q = p.Q;
    const a = I[tt * 3] * 3, b = I[tt * 3 + 1] * 3, c = I[tt * 3 + 2] * 3;
    const x1 = Q[a], y1 = Q[a + 1], x2 = Q[b], y2 = Q[b + 1], x3 = Q[c], y3 = Q[c + 1];
    const den = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
    if (Math.abs(den) < 1e-18) continue;
    const l1 = ((y2 - y3) * (u - x3) + (x3 - x2) * (v - y3)) / den, l2 = ((y3 - y1) * (u - x3) + (x1 - x3) * (v - y3)) / den, l3 = 1 - l1 - l2;
    if (l1 < margin || l2 < margin || l3 < margin) continue;
    let w1 = l1, w2 = l2, w3 = l3, depth;
    if (persp) { w1 = l1 / Q[a + 2]; w2 = l2 / Q[b + 2]; w3 = l3 / Q[c + 2]; const s = w1 + w2 + w3; depth = 1 / s; w1 /= s; w2 /= s; w3 /= s; } else depth = l1 * Q[a + 2] + l2 * Q[b + 2] + l3 * Q[c + 2];
    if (depth >= d - eps * 10) continue;
illustrate/edges.js PartCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Feature edges

32-bit, display only

The mesh edges drawn as lines: open edges and edges where two faces meet at more than an angle.

drawn when open, or cos(face angle) < cos(angle)

The limit is Math.cos(angle × π ÷ 180), 20° by default. The segment list is returned as a Float32Array for drawing.

  • illustrate/edges.js
  • lines 53 to 62
  • PartCraft 0.8.1
export function featureSegments(mesh, { angle = 20, topo = null } = {}) {
  const { edges } = topo || topology(mesh);
  const P = mesh.positions, lim = Math.cos(angle * Math.PI / 180);
  const out = [];
  for (const e of edges) {
    if (e.t2 >= 0 && e.cos >= lim) continue;
    out.push(P[e.a * 3], P[e.a * 3 + 1], P[e.a * 3 + 2], P[e.b * 3], P[e.b * 3 + 1], P[e.b * 3 + 2]);
  }
  return Float32Array.from(out);
}
illustrate/pdf.js PartCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests tools/partcraft-test/test-illustrate.mjs

Numbers in the PDF

32-bit, display only

How a coordinate is written into a PDF page.

toFixed(3), with magnitudes below 1e-9 written as 0. The page unit is the millimeter, so drawn positions are placed to 0.001 mm on the page.

  • illustrate/pdf.js
  • line 15
  • PartCraft 0.8.1
const num = (v) => { const r = Math.abs(v) < 1e-9 ? 0 : v; return String(Number(r.toFixed(3))); };

In detail

Method
Orthographic or perspective projection onto a picture one unit high. Hidden lines by cutting each edge into short pieces and testing each piece's middle against the triangles in front of it, found through a grid over the picture (the file calls it the mesh method).
Number types
64-bit numbers; Float32Array meshes and feature segments.
Tolerances
Depth test eps = 1e-6 × max(size, 1) + 1e-9, used times 10; barycentric margin 1e-7; perspective depth floored at 1e-9.

Approach: Display only

Computed to draw the screen, often in 32-bit on the graphics processor. No number a person reads, saves or exports comes from it.

Every policy that takes this approach