jcode/skills/threejs-csg/loaders.md

3.5 KiB

Loaders, exporters and text/SVG shapes

All of these live under three/addons/, which the import map of template.html already provides. Import from the addon file, NOT from the THREE namespace (THREE.STLExporter is undefined):

import { STLExporter }   from 'three/addons/exporters/STLExporter.js';
import { STLLoader }     from 'three/addons/loaders/STLLoader.js';
import { SVGLoader }     from 'three/addons/loaders/SVGLoader.js';
import { FontLoader }    from 'three/addons/loaders/FontLoader.js';
import { TextGeometry }  from 'three/addons/geometries/TextGeometry.js';

Export STL

STLExporter.parse(object3d) traverses object3d and exports every Mesh in WORLD coordinates. Give it a throw-away Group of plain meshes - never the scene (grid, lights, helpers would be included) and never Brush objects with stale matrices:

function toSTL(objs, binary = false) {
  const g = new THREE.Group();
  for (const o of objs) {                       // o.mesh: Mesh or Brush (result of CSG)
    const m = new THREE.Mesh(o.mesh.geometry);  // plain Mesh, geometry is shared
    m.matrix.copy(o.mesh.matrixWorld); m.matrixAutoUpdate = false;
    g.add(m);
  }
  g.updateMatrixWorld(true);
  return new STLExporter().parse(g, { binary });   // string (ASCII) or DataView (binary)
}

Call obj.updateMatrixWorld(true) after changing position/rotation and before exporting or evaluating CSG. Holes must already be subtracted (export the group results, not the hole brushes).

Import STL

pickFile('.stl', false, buf => {                   // ArrayBuffer (see single-file-webapp skill)
  const geo = new STLLoader().parse(buf);          // handles ASCII and binary
  geo.computeVertexNormals(); geo.center();        // optional
  addObject({ type: 'stl', geometry: geo });
});

Extrude an SVG

pickFile('.svg', true, text => {
  const data = new SVGLoader().parse(text);
  const shapes = data.paths.flatMap(p => SVGLoader.createShapes(p));
  const geo = new THREE.ExtrudeGeometry(shapes, { depth: 2, bevelEnabled: false });
  geo.rotateX(Math.PI);  // SVG y points down
  geo.center();
  addObject({ type: 'svg', geometry: geo });
});

SVG units are px; scale with geo.scale(s, s, 1) (e.g. s = 0.1 for mm-ish sizes).

Solid text

const FONT_URL = 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/fonts/helvetiker_regular.typeface.json';
const font = await new FontLoader().loadAsync(FONT_URL);     // load once, cache
const geo = new TextGeometry('Hi', { font, size: 10, height: 2, curveSegments: 6 });
geo.center();

(height is the extrusion depth; in three >= r163 the option is called depth.)

Group with holes = CSG result

function buildGroup(members) {              // members: [{mesh, hole}]
  const ev = new Evaluator(); ev.useGroups = false;
  const solids = members.filter(m => !m.hole), holes = members.filter(m => m.hole);
  let result = null;
  for (const s of solids) { s.mesh.updateMatrixWorld(true);
    result = result ? ev.evaluate(result, s.mesh, ADDITION) : s.mesh; }
  for (const h of holes)  { h.mesh.updateMatrixWorld(true);
    result = ev.evaluate(result, h.mesh, SUBTRACTION); }
  result.material = solids[0].mesh.material;
  return result;                            // a Brush; keep `members` for ungroup
}

Operands must be Brush instances (create every object as a Brush from the start, so any object can take part in CSG later). Render holes with a transparent material (transparent: true, opacity: 0.35) until grouped.