@es3d/es3d 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +86 -3
- package/docs/REFERENCE.md +581 -0
- package/docs/SPEC.md +417 -0
- package/package.json +50 -4
- package/scripts/es3d-cli.mjs +377 -0
- package/src/cache/geometry-store.js +392 -0
- package/src/cache/mesh-bundle.js +123 -0
- package/src/cache/sha256.js +73 -0
- package/src/format/ast.js +238 -0
- package/src/format/colors.js +93 -0
- package/src/format/compact-parser.js +171 -0
- package/src/format/compact-tokenizer.js +165 -0
- package/src/format/errors.js +247 -0
- package/src/format/json.js +55 -0
- package/src/format/limits.js +14 -0
- package/src/format/parse.js +92 -0
- package/src/format/schema.js +617 -0
- package/src/format/serializer.js +333 -0
- package/src/format/validator.js +330 -0
- package/src/format/value-error.js +12 -0
- package/src/format/value-format.js +314 -0
- package/src/format/values.js +459 -0
- package/src/format/xml-parser.js +136 -0
- package/src/format/xml-tokenizer.js +237 -0
- package/src/geometry/contour.js +86 -0
- package/src/geometry/extrude.js +175 -0
- package/src/geometry/gear.js +85 -0
- package/src/geometry/geometry-cache.js +159 -0
- package/src/geometry/lod-error.js +178 -0
- package/src/geometry/mesh.js +458 -0
- package/src/geometry/primitives.js +299 -0
- package/src/geometry/registry.js +645 -0
- package/src/geometry/revolve.js +136 -0
- package/src/geometry/sweep.js +441 -0
- package/src/index.js +47 -0
- package/src/io/compression.js +67 -0
- package/src/io/convert.js +236 -0
- package/src/io/download.js +13 -0
- package/src/io/external-scene.js +127 -0
- package/src/io/file-loader.js +28 -0
- package/src/io/gltf.js +630 -0
- package/src/io/mesh-import.js +104 -0
- package/src/io/obj.js +352 -0
- package/src/io/step-brep.js +439 -0
- package/src/io/step-exact.js +1149 -0
- package/src/io/step.js +1418 -0
- package/src/io/stl.js +193 -0
- package/src/io/svg-import.js +787 -0
- package/src/material/materials.js +154 -0
- package/src/math/mat4.js +475 -0
- package/src/math/random.js +30 -0
- package/src/math/trig.js +65 -0
- package/src/math/vec3.js +81 -0
- package/src/optimize/optimizer.js +579 -0
- package/src/path/path-flatten.js +322 -0
- package/src/path/path-parser.js +332 -0
- package/src/path/shapes.js +129 -0
- package/src/path/triangulate.js +517 -0
- package/src/pipeline.js +74 -0
- package/src/render/camera.js +168 -0
- package/src/render/frame-builder.js +362 -0
- package/src/render/geometry-pool.js +149 -0
- package/src/render/lighting.js +149 -0
- package/src/render/orbit-controls.js +237 -0
- package/src/render/renderer.js +249 -0
- package/src/render/scroll-timeline.js +104 -0
- package/src/render/shaders/glsl.js +285 -0
- package/src/render/shaders/wgsl.js +252 -0
- package/src/render/stage.js +352 -0
- package/src/render/texture-cache.js +40 -0
- package/src/render/textures.js +223 -0
- package/src/render/webgl2/webgl2-backend.js +532 -0
- package/src/render/webgpu/webgpu-backend.js +480 -0
- package/src/scene/animation.js +171 -0
- package/src/scene/camera-constraints.js +117 -0
- package/src/scene/flatten.js +754 -0
- package/src/scene/picking.js +161 -0
- package/src/scene/scene-graph.js +320 -0
- package/src/transform/transform.js +211 -0
- package/src/wasm/kernels-binary.js +3 -0
- package/src/wasm/kernels.js +385 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// Geometry registry: turns shape elements into geometry descriptors.
|
|
2
|
+
//
|
|
3
|
+
// A descriptor captures everything needed to (lazily) produce triangles:
|
|
4
|
+
// key cache key built only from shape-defining parameters; elements
|
|
5
|
+
// that differ only in size share the same mesh (see `unit`)
|
|
6
|
+
// unit matrix from the canonical mesh space into element space
|
|
7
|
+
// lods number of tessellation levels (1 = no level of detail)
|
|
8
|
+
// kind 'triangles' | 'lines'
|
|
9
|
+
// doubleSided render back faces (flat or open surfaces)
|
|
10
|
+
// bounds conservative canonical bounds, known without tessellating
|
|
11
|
+
// generate(lod, quality, report) -> Mesh
|
|
12
|
+
// identity (set here, built-in shapes only) the key with the full text of
|
|
13
|
+
// every input it hashes: what the persistent geometry cache
|
|
14
|
+
// names its records by, so that two shapes whose short keys
|
|
15
|
+
// collide can never share a cached mesh
|
|
16
|
+
//
|
|
17
|
+
// New element types can be added with registerGeometry(); this is the
|
|
18
|
+
// extension point that keeps the core format small.
|
|
19
|
+
|
|
20
|
+
import * as mat4 from '../math/mat4.js';
|
|
21
|
+
import { valueOf } from '../format/schema.js';
|
|
22
|
+
import { pathControlBounds } from '../path/path-parser.js';
|
|
23
|
+
import { flattenPath, circleContour } from '../path/path-flatten.js';
|
|
24
|
+
import { buildRegions } from '../path/shapes.js';
|
|
25
|
+
import { unitBox, roundedBox, unitSphere, unitCylinder, unitCapsule, unitPlane, unitTorus, lineMesh, lodSegments } from './primitives.js';
|
|
26
|
+
import {
|
|
27
|
+
sphereDeviation, cylinderDeviation, capsuleDeviation, torusDeviation, roundedBoxDeviation,
|
|
28
|
+
gearDeviation, pathFlattenDeviation, bevelDeviation, circleSagitta,
|
|
29
|
+
} from './lod-error.js';
|
|
30
|
+
import { flatMesh, extrudeMesh } from './extrude.js';
|
|
31
|
+
import { revolveMesh } from './revolve.js';
|
|
32
|
+
import { sweepMesh, spinePolylines, catmullRom } from './sweep.js';
|
|
33
|
+
import { gearOutline } from './gear.js';
|
|
34
|
+
import { Mesh, computeNormals } from './mesh.js';
|
|
35
|
+
|
|
36
|
+
export const LOD_COUNT = 4;
|
|
37
|
+
|
|
38
|
+
function lodList(fn) {
|
|
39
|
+
const a = new Array(LOD_COUNT);
|
|
40
|
+
for (let lod = 0; lod < LOD_COUNT; lod++) a[lod] = fn(lod);
|
|
41
|
+
return a;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function segsAt(base, min, fixed) {
|
|
45
|
+
return fixed ? [fixed] : lodList((lod) => lodSegments(base, lod, 1, min));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const registry = new Map();
|
|
49
|
+
// shapes defined in this module; filled at the end of it. A replacement
|
|
50
|
+
// registered later loses the identity (its key may hash anything)
|
|
51
|
+
let builtins = null;
|
|
52
|
+
|
|
53
|
+
export function registerGeometry(name, describe) {
|
|
54
|
+
registry.set(name, describe);
|
|
55
|
+
if (builtins) builtins.delete(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasGeometry(name) {
|
|
59
|
+
return registry.has(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// raw inputs hashed into the key of the descriptor being built, or null
|
|
63
|
+
// when it cannot have an identity (keyed by a hash of mesh data)
|
|
64
|
+
let inputs = null;
|
|
65
|
+
|
|
66
|
+
/** @returns {object|null} descriptor */
|
|
67
|
+
export function describeShape(el, ctx) {
|
|
68
|
+
const fn = registry.get(el.name);
|
|
69
|
+
if (!fn) return null;
|
|
70
|
+
inputs = builtins && builtins.has(el.name) ? [] : null;
|
|
71
|
+
try {
|
|
72
|
+
const desc = fn(el, ctx);
|
|
73
|
+
if (desc && inputs) desc.identity = [desc.key, ...inputs].map((t) => `${t.length}:${t}`).join('');
|
|
74
|
+
return desc;
|
|
75
|
+
} finally {
|
|
76
|
+
inputs = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const box3 = (minX, minY, minZ, maxX, maxY, maxZ) => ({ min: [minX, minY, minZ], max: [maxX, maxY, maxZ] });
|
|
81
|
+
const S = (x, y, z) => mat4.fromScaling(mat4.createF64(), x, y, z);
|
|
82
|
+
|
|
83
|
+
function hashString(s) {
|
|
84
|
+
if (inputs) inputs.push(s);
|
|
85
|
+
// FNV-1a, two lanes for a 64-bit-ish key
|
|
86
|
+
let h1 = 0x811c9dc5, h2 = 0x01000193;
|
|
87
|
+
for (let i = 0; i < s.length; i++) {
|
|
88
|
+
const c = s.charCodeAt(i);
|
|
89
|
+
h1 = Math.imul(h1 ^ c, 0x01000193);
|
|
90
|
+
h2 = Math.imul(h2 ^ c, 0x5bd1e995);
|
|
91
|
+
}
|
|
92
|
+
return (h1 >>> 0).toString(36) + (h2 >>> 0).toString(36) + ':' + s.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Hash of a typed array's contents (two 32-bit lanes + length). */
|
|
96
|
+
function hashTyped(a) {
|
|
97
|
+
// mesh data: hashing all of it for a persistent name would cost more than
|
|
98
|
+
// the mesh does to build, so such shapes have no identity
|
|
99
|
+
inputs = null;
|
|
100
|
+
if (!a) return '';
|
|
101
|
+
const w = new Uint32Array(a.buffer, a.byteOffset, a.byteLength >> 2);
|
|
102
|
+
let h1 = 0x9747b28c, h2 = 0x01000193;
|
|
103
|
+
for (let i = 0; i < w.length; i++) {
|
|
104
|
+
let k = Math.imul(w[i], 0xcc9e2d51);
|
|
105
|
+
k = Math.imul((k << 15) | (k >>> 17), 0x1b873593);
|
|
106
|
+
h1 ^= k;
|
|
107
|
+
h1 = (Math.imul((h1 << 13) | (h1 >>> 19), 5) + 0xe6546b64) | 0;
|
|
108
|
+
h2 = Math.imul(h2 ^ w[i], 0x85ebca6b);
|
|
109
|
+
h2 ^= h2 >>> 13;
|
|
110
|
+
}
|
|
111
|
+
return (h1 >>> 0).toString(36) + (h2 >>> 0).toString(36) + ':' + a.length;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rawOf(el, name) {
|
|
115
|
+
const a = el.attrIndex && el.attrIndex[name];
|
|
116
|
+
return a ? a.value.trim() : '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Path data of a path-consuming element (inline d or referenced <path>). */
|
|
120
|
+
function pathSource(el) {
|
|
121
|
+
if (el.values.d) return { path: el.values.d, raw: rawOf(el, 'd'), fillRule: valueOf(el, 'fillRule') };
|
|
122
|
+
const t = el.pathTarget;
|
|
123
|
+
if (t && t.values && t.values.d) {
|
|
124
|
+
const rule = el.values.fillRule !== undefined ? el.values.fillRule : valueOf(t, 'fillRule');
|
|
125
|
+
return { path: t.values.d, raw: rawOf(t, 'd'), fillRule: rule };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pathDiag(segments) {
|
|
131
|
+
const b = pathControlBounds(segments);
|
|
132
|
+
return Math.hypot(b.maxX - b.minX, b.maxY - b.minY) || 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pathTolerance(segments, lod, quality) {
|
|
136
|
+
return pathFlattenDeviation(pathDiag(segments), lod) / quality;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function subpathsOpen(segments) {
|
|
140
|
+
let open = false;
|
|
141
|
+
let inSub = false;
|
|
142
|
+
let closed = false;
|
|
143
|
+
for (const s of segments) {
|
|
144
|
+
if (s.c === 'M') {
|
|
145
|
+
if (inSub && !closed) open = true;
|
|
146
|
+
inSub = true;
|
|
147
|
+
closed = false;
|
|
148
|
+
} else if (s.c === 'Z') closed = true;
|
|
149
|
+
}
|
|
150
|
+
if (inSub && !closed) open = true;
|
|
151
|
+
return open;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function regionsFor(src, lod, quality, report) {
|
|
155
|
+
const contours = flattenPath(src.path.segments, { tolerance: pathTolerance(src.path.segments, lod, quality) });
|
|
156
|
+
for (const c of contours) {
|
|
157
|
+
if (c.degenerate && report) report('warning', 'W_DEGENERATE_CONTOUR', 'A subpath has fewer than three distinct points and encloses no area.');
|
|
158
|
+
}
|
|
159
|
+
const shape = buildRegions(contours, src.fillRule);
|
|
160
|
+
if (!shape.regions.length && report) report('warning', 'W_EMPTY_FILL', 'The path encloses no area; nothing is filled.');
|
|
161
|
+
return shape;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- primitives --------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
registerGeometry('box', (el) => {
|
|
167
|
+
const size = valueOf(el, 'size');
|
|
168
|
+
const r = valueOf(el, 'radius');
|
|
169
|
+
if (r > 0) {
|
|
170
|
+
const fixed = el.values.segments;
|
|
171
|
+
return {
|
|
172
|
+
key: `rbox:${size.join(',')}:${r}:${fixed || ''}`,
|
|
173
|
+
unit: null,
|
|
174
|
+
lods: fixed ? 1 : LOD_COUNT,
|
|
175
|
+
kind: 'triangles',
|
|
176
|
+
doubleSided: false,
|
|
177
|
+
bounds: box3(-size[0] / 2, -size[1] / 2, -size[2] / 2, size[0] / 2, size[1] / 2, size[2] / 2),
|
|
178
|
+
lodDeviation: segsAt(4, 1, fixed).map((n) => roundedBoxDeviation(r, n)),
|
|
179
|
+
generate: (lod, q) => roundedBox(size[0], size[1], size[2], r, fixed || lodSegments(4, lod, q, 1)),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
key: 'box',
|
|
184
|
+
unit: S(size[0], size[1], size[2]),
|
|
185
|
+
lods: 1,
|
|
186
|
+
kind: 'triangles',
|
|
187
|
+
doubleSided: false,
|
|
188
|
+
bounds: box3(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5),
|
|
189
|
+
lodDeviation: [0],
|
|
190
|
+
generate: () => unitBox(),
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
registerGeometry('sphere', (el) => {
|
|
195
|
+
const r = valueOf(el, 'radius');
|
|
196
|
+
const fixed = el.values.segments;
|
|
197
|
+
return {
|
|
198
|
+
key: `sphere:${fixed || ''}`,
|
|
199
|
+
unit: S(r, r, r),
|
|
200
|
+
lods: fixed ? 1 : LOD_COUNT,
|
|
201
|
+
kind: 'triangles',
|
|
202
|
+
doubleSided: false,
|
|
203
|
+
bounds: box3(-1, -1, -1, 1, 1, 1),
|
|
204
|
+
lodDeviation: segsAt(48, 8, fixed).map(sphereDeviation),
|
|
205
|
+
generate: (lod, q) => unitSphere(fixed || lodSegments(48, lod, q, 8)),
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
function cylinderLike(el, top) {
|
|
210
|
+
const r = valueOf(el, 'radius');
|
|
211
|
+
const h = valueOf(el, 'height');
|
|
212
|
+
const caps = valueOf(el, 'caps');
|
|
213
|
+
const fixed = el.values.segments;
|
|
214
|
+
const t = top / r;
|
|
215
|
+
const m = Math.max(1, t);
|
|
216
|
+
return {
|
|
217
|
+
key: `cyl:${t}:${caps}:${fixed || ''}`,
|
|
218
|
+
unit: S(r, h, r),
|
|
219
|
+
lods: fixed ? 1 : LOD_COUNT,
|
|
220
|
+
kind: 'triangles',
|
|
221
|
+
doubleSided: !caps,
|
|
222
|
+
bounds: box3(-m, -0.5, -m, m, 0.5, m),
|
|
223
|
+
lodDeviation: segsAt(48, 8, fixed).map((n) => cylinderDeviation(t, n)),
|
|
224
|
+
generate: (lod, q) => unitCylinder(t, fixed || lodSegments(48, lod, q, 8), caps),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
registerGeometry('cylinder', (el) => {
|
|
229
|
+
const r = valueOf(el, 'radius');
|
|
230
|
+
const top = el.values.radiusTop !== undefined ? el.values.radiusTop : r;
|
|
231
|
+
return cylinderLike(el, top);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
registerGeometry('cone', (el) => cylinderLike(el, 0));
|
|
235
|
+
|
|
236
|
+
registerGeometry('capsule', (el) => {
|
|
237
|
+
const r = valueOf(el, 'radius');
|
|
238
|
+
const h = valueOf(el, 'height');
|
|
239
|
+
const L = Math.max(0, h / r - 2);
|
|
240
|
+
const fixed = el.values.segments;
|
|
241
|
+
return {
|
|
242
|
+
key: `capsule:${L}:${fixed || ''}`,
|
|
243
|
+
unit: S(r, r, r),
|
|
244
|
+
lods: fixed ? 1 : LOD_COUNT,
|
|
245
|
+
kind: 'triangles',
|
|
246
|
+
doubleSided: false,
|
|
247
|
+
bounds: box3(-1, -(L / 2 + 1), -1, 1, L / 2 + 1, 1),
|
|
248
|
+
lodDeviation: segsAt(32, 8, fixed).map(capsuleDeviation),
|
|
249
|
+
generate: (lod, q) => unitCapsule(L, fixed || lodSegments(32, lod, q, 8)),
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
registerGeometry('plane', (el) => {
|
|
254
|
+
const size = valueOf(el, 'size');
|
|
255
|
+
return {
|
|
256
|
+
key: 'plane',
|
|
257
|
+
unit: S(size[0], 1, size[1]),
|
|
258
|
+
lods: 1,
|
|
259
|
+
kind: 'triangles',
|
|
260
|
+
doubleSided: true,
|
|
261
|
+
bounds: box3(-0.5, 0, -0.5, 0.5, 0, 0.5),
|
|
262
|
+
lodDeviation: [0],
|
|
263
|
+
generate: () => unitPlane(),
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
registerGeometry('torus', (el) => {
|
|
268
|
+
const R = valueOf(el, 'radius');
|
|
269
|
+
const tube = valueOf(el, 'tube');
|
|
270
|
+
const arc = valueOf(el, 'arc');
|
|
271
|
+
const t = tube / R;
|
|
272
|
+
const fs = el.values.segments;
|
|
273
|
+
const ft = el.values.tubeSegments;
|
|
274
|
+
return {
|
|
275
|
+
key: `torus:${t}:${arc}:${fs || ''}:${ft || ''}`,
|
|
276
|
+
unit: S(R, R, R),
|
|
277
|
+
lods: fs && ft ? 1 : LOD_COUNT,
|
|
278
|
+
kind: 'triangles',
|
|
279
|
+
doubleSided: false,
|
|
280
|
+
bounds: box3(-(1 + t), -t, -(1 + t), 1 + t, t, 1 + t),
|
|
281
|
+
lodDeviation: (fs && ft ? [0] : lodList((lod) => 0)).map((_, lod) => torusDeviation(t, arc, fs || lodSegments(64, lod, 1, 8), ft || lodSegments(24, lod, 1, 6))),
|
|
282
|
+
generate: (lod, q) => unitTorus(t, arc, fs || lodSegments(64, lod, q, 8), ft || lodSegments(24, lod, q, 6)),
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --- curves --------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function pointsBounds(pts, dim, pad = 0) {
|
|
289
|
+
const b = box3(Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity);
|
|
290
|
+
for (let i = 0; i < pts.length; i += dim) {
|
|
291
|
+
for (let k = 0; k < 3; k++) {
|
|
292
|
+
const v = k < dim ? pts[i + k] : 0;
|
|
293
|
+
if (v < b.min[k]) b.min[k] = v;
|
|
294
|
+
if (v > b.max[k]) b.max[k] = v;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (let k = 0; k < 3; k++) {
|
|
298
|
+
b.min[k] -= pad;
|
|
299
|
+
b.max[k] += pad;
|
|
300
|
+
}
|
|
301
|
+
return b;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
registerGeometry('line', (el) => {
|
|
305
|
+
const a = el.values.from, b = el.values.to;
|
|
306
|
+
const r = valueOf(el, 'radius');
|
|
307
|
+
if (r > 0) {
|
|
308
|
+
// a tube is the shared unit cylinder, oriented and scaled along the segment
|
|
309
|
+
const d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
310
|
+
const len = Math.hypot(d[0], d[1], d[2]);
|
|
311
|
+
const y = [d[0] / len, d[1] / len, d[2] / len];
|
|
312
|
+
const ref = Math.abs(y[1]) < 0.99 ? [0, 1, 0] : [1, 0, 0];
|
|
313
|
+
let x = [ref[1] * y[2] - ref[2] * y[1], ref[2] * y[0] - ref[0] * y[2], ref[0] * y[1] - ref[1] * y[0]];
|
|
314
|
+
const xl = Math.hypot(...x);
|
|
315
|
+
x = x.map((v) => v / xl);
|
|
316
|
+
const z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]];
|
|
317
|
+
// columns: x*r, dir*len, z*r (right handed since z = x cross y), translation = midpoint
|
|
318
|
+
const m = mat4.createF64();
|
|
319
|
+
m.set([x[0] * r, x[1] * r, x[2] * r, 0, y[0] * len, y[1] * len, y[2] * len, 0, z[0] * r, z[1] * r, z[2] * r, 0, (a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, 1]);
|
|
320
|
+
const fixed = el.values.segments;
|
|
321
|
+
return {
|
|
322
|
+
key: `cyl:1:true:${fixed || ''}`,
|
|
323
|
+
unit: m,
|
|
324
|
+
lods: fixed ? 1 : LOD_COUNT,
|
|
325
|
+
kind: 'triangles',
|
|
326
|
+
doubleSided: false,
|
|
327
|
+
bounds: box3(-1, -0.5, -1, 1, 0.5, 1),
|
|
328
|
+
lodDeviation: segsAt(48, 8, fixed).map((n) => cylinderDeviation(1, n)),
|
|
329
|
+
generate: (lod, q) => unitCylinder(1, fixed || lodSegments(48, lod, q, 8), true),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
key: `line:${a.join(',')}:${b.join(',')}`,
|
|
334
|
+
unit: null,
|
|
335
|
+
lods: 1,
|
|
336
|
+
kind: 'lines',
|
|
337
|
+
doubleSided: false,
|
|
338
|
+
bounds: pointsBounds([...a, ...b], 3),
|
|
339
|
+
lodDeviation: [0],
|
|
340
|
+
generate: () => lineMesh([...a, ...b], false),
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
registerGeometry('polyline', (el) => {
|
|
345
|
+
const r = valueOf(el, 'radius');
|
|
346
|
+
const closed = valueOf(el, 'closed');
|
|
347
|
+
const smooth = valueOf(el, 'smooth');
|
|
348
|
+
const fixed = el.values.segments;
|
|
349
|
+
const pts = el.values.points;
|
|
350
|
+
const d = el.values.d;
|
|
351
|
+
const keyBase = pts ? `pts:${hashString(rawOf(el, 'points'))}` : `d:${hashString(rawOf(el, 'd'))}`;
|
|
352
|
+
const polylines = (lod, q) => {
|
|
353
|
+
if (pts) {
|
|
354
|
+
let p = pts;
|
|
355
|
+
if (smooth) p = catmullRom(pts, closed, lodSegments(12, lod, q, 2));
|
|
356
|
+
return [{ points: p, closed }];
|
|
357
|
+
}
|
|
358
|
+
const tol = pathTolerance(d.segments, lod, q);
|
|
359
|
+
return flattenPath(d.segments, { tolerance: tol }).map((c) => {
|
|
360
|
+
const n = c.points.length / 2;
|
|
361
|
+
const p3 = new Float64Array(n * 3);
|
|
362
|
+
for (let i = 0; i < n; i++) {
|
|
363
|
+
p3[i * 3] = c.points[i * 2];
|
|
364
|
+
p3[i * 3 + 1] = c.points[i * 2 + 1];
|
|
365
|
+
}
|
|
366
|
+
return { points: p3, closed: c.closed || closed };
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
let bounds;
|
|
370
|
+
if (pts) {
|
|
371
|
+
const raw = pointsBounds(pts, 3);
|
|
372
|
+
const diag = Math.hypot(raw.max[0] - raw.min[0], raw.max[1] - raw.min[1], raw.max[2] - raw.min[2]);
|
|
373
|
+
// splines may overshoot the control points slightly
|
|
374
|
+
bounds = pointsBounds(pts, 3, r + (smooth ? 0.1 * diag : 0));
|
|
375
|
+
} else {
|
|
376
|
+
const cb = pathControlBounds(d.segments);
|
|
377
|
+
bounds = box3(cb.minX - r, cb.minY - r, -r, cb.maxX + r, cb.maxY + r, r);
|
|
378
|
+
}
|
|
379
|
+
if (r > 0) {
|
|
380
|
+
return {
|
|
381
|
+
key: `tube:${keyBase}:${r}:${closed}:${smooth}:${fixed || ''}`,
|
|
382
|
+
unit: null,
|
|
383
|
+
lods: LOD_COUNT,
|
|
384
|
+
kind: 'triangles',
|
|
385
|
+
doubleSided: false,
|
|
386
|
+
bounds,
|
|
387
|
+
lodDeviation: lodList((lod) => {
|
|
388
|
+
const spine = pts
|
|
389
|
+
? Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]) * (smooth ? 0.05 / lodSegments(12, lod, 1, 2) : 0)
|
|
390
|
+
: pathFlattenDeviation(pathDiag(d.segments), lod);
|
|
391
|
+
return spine + r * circleSagitta(fixed || lodSegments(24, lod, 1, 6));
|
|
392
|
+
}),
|
|
393
|
+
generate: (lod, q, report) => sweepMesh({ kind: 'circle', radius: r, segments: fixed || lodSegments(24, lod, q, 6) }, polylines(lod, q), { caps: true }, report).mesh,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
key: `polyline:${keyBase}:${closed}:${smooth}`,
|
|
398
|
+
unit: null,
|
|
399
|
+
lods: smooth || d ? LOD_COUNT : 1,
|
|
400
|
+
kind: 'lines',
|
|
401
|
+
doubleSided: false,
|
|
402
|
+
bounds,
|
|
403
|
+
lodDeviation: smooth || d
|
|
404
|
+
? lodList((lod) => (pts
|
|
405
|
+
? Math.hypot(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]) * (smooth ? 0.05 / lodSegments(12, lod, 1, 2) : 0)
|
|
406
|
+
: pathFlattenDeviation(pathDiag(d.segments), lod)))
|
|
407
|
+
: [0],
|
|
408
|
+
generate: (lod, q) => {
|
|
409
|
+
const list = polylines(lod, q);
|
|
410
|
+
if (list.length === 1) return lineMesh(list[0].points, list[0].closed);
|
|
411
|
+
// several subpaths: concatenate
|
|
412
|
+
const meshes = list.map((l) => lineMesh(l.points, l.closed));
|
|
413
|
+
let vc = 0, ic = 0;
|
|
414
|
+
for (const m of meshes) {
|
|
415
|
+
vc += m.vertexCount;
|
|
416
|
+
ic += m.indices.length;
|
|
417
|
+
}
|
|
418
|
+
const positions = new Float32Array(vc * 3);
|
|
419
|
+
const indices = new Uint32Array(ic);
|
|
420
|
+
let vo = 0, io = 0;
|
|
421
|
+
for (const m of meshes) {
|
|
422
|
+
positions.set(m.positions, vo * 3);
|
|
423
|
+
for (let i = 0; i < m.indices.length; i++) indices[io + i] = m.indices[i] + vo;
|
|
424
|
+
vo += m.vertexCount;
|
|
425
|
+
io += m.indices.length;
|
|
426
|
+
}
|
|
427
|
+
return new Mesh({ positions, indices, kind: 'lines' });
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// --- flat shapes -------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
registerGeometry('polygon', (el) => {
|
|
435
|
+
const pts = el.values.points;
|
|
436
|
+
const contour = { points: Float64Array.from(pts), smooth: new Uint8Array(pts.length / 2), closed: true };
|
|
437
|
+
return {
|
|
438
|
+
key: `polygon:${hashString(rawOf(el, 'points'))}`,
|
|
439
|
+
unit: null,
|
|
440
|
+
lods: 1,
|
|
441
|
+
kind: 'triangles',
|
|
442
|
+
doubleSided: true,
|
|
443
|
+
bounds: pointsBounds(pts, 2),
|
|
444
|
+
lodDeviation: [0],
|
|
445
|
+
generate: (lod, q, report) => flatMesh(buildRegions([contour], 'nonzero').regions, report),
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
registerGeometry('path', (el) => {
|
|
450
|
+
const src = pathSource(el);
|
|
451
|
+
if (!src) return null;
|
|
452
|
+
const cb = pathControlBounds(src.path.segments);
|
|
453
|
+
return {
|
|
454
|
+
key: `path:${hashString(src.raw)}:${src.fillRule}`,
|
|
455
|
+
unit: null,
|
|
456
|
+
lods: LOD_COUNT,
|
|
457
|
+
kind: 'triangles',
|
|
458
|
+
doubleSided: true,
|
|
459
|
+
bounds: box3(cb.minX, cb.minY, 0, cb.maxX, cb.maxY, 0),
|
|
460
|
+
lodDeviation: lodList((lod) => pathFlattenDeviation(pathDiag(src.path.segments), lod)),
|
|
461
|
+
generate: (lod, q, report) => flatMesh(regionsFor(src, lod, q, report).regions, report),
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
registerGeometry('extrude', (el) => {
|
|
466
|
+
const src = pathSource(el);
|
|
467
|
+
if (!src) return null;
|
|
468
|
+
const depth = valueOf(el, 'depth');
|
|
469
|
+
const bevel = valueOf(el, 'bevel');
|
|
470
|
+
const bevelSegments = valueOf(el, 'bevelSegments');
|
|
471
|
+
const center = valueOf(el, 'center');
|
|
472
|
+
const cb = pathControlBounds(src.path.segments);
|
|
473
|
+
const z0 = center ? -depth / 2 : 0;
|
|
474
|
+
return {
|
|
475
|
+
key: `extrude:${hashString(src.raw)}:${src.fillRule}:${depth}:${bevel}:${bevelSegments}:${center}`,
|
|
476
|
+
unit: null,
|
|
477
|
+
lods: LOD_COUNT,
|
|
478
|
+
kind: 'triangles',
|
|
479
|
+
doubleSided: false,
|
|
480
|
+
bounds: box3(cb.minX, cb.minY, z0, cb.maxX, cb.maxY, z0 + depth),
|
|
481
|
+
lodDeviation: lodList((lod) => pathFlattenDeviation(pathDiag(src.path.segments), lod) + bevelDeviation(bevel, Math.max(1, Math.round(bevelSegments / (lod >= 2 ? 2 : 1))))),
|
|
482
|
+
generate: (lod, q, report) =>
|
|
483
|
+
extrudeMesh(regionsFor(src, lod, q, report), { depth, bevel, bevelSegments: Math.max(1, Math.round(bevelSegments / (lod >= 2 ? 2 : 1))), center }, report),
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
registerGeometry('revolve', (el) => {
|
|
488
|
+
const src = pathSource(el);
|
|
489
|
+
if (!src) return null;
|
|
490
|
+
const angle = valueOf(el, 'angle');
|
|
491
|
+
const fixed = el.values.segments;
|
|
492
|
+
const cb = pathControlBounds(src.path.segments);
|
|
493
|
+
const R = Math.max(Math.abs(cb.maxX), Math.abs(cb.minX));
|
|
494
|
+
return {
|
|
495
|
+
key: `revolve:${hashString(src.raw)}:${angle}:${fixed || ''}`,
|
|
496
|
+
unit: null,
|
|
497
|
+
lods: LOD_COUNT,
|
|
498
|
+
kind: 'triangles',
|
|
499
|
+
doubleSided: subpathsOpen(src.path.segments),
|
|
500
|
+
bounds: box3(-R, cb.minY, -R, R, cb.maxY, R),
|
|
501
|
+
lodDeviation: lodList((lod) => {
|
|
502
|
+
const segs = fixed || Math.max(3, Math.round(lodSegments(48, lod, 1, 8) * Math.min(1, angle / 360) + (angle < 360 ? 1 : 0)));
|
|
503
|
+
return pathFlattenDeviation(pathDiag(src.path.segments), lod) + R * circleSagitta(segs);
|
|
504
|
+
}),
|
|
505
|
+
generate: (lod, q, report) => {
|
|
506
|
+
const contours = flattenPath(src.path.segments, { tolerance: pathTolerance(src.path.segments, lod, q) });
|
|
507
|
+
const segs = fixed || Math.max(3, Math.round(lodSegments(48, lod, q, 8) * Math.min(1, angle / 360) + (angle < 360 ? 1 : 0)));
|
|
508
|
+
return revolveMesh(contours, { angle, segments: segs }, report).mesh;
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
function spineBounds(spine) {
|
|
514
|
+
if (spine.kind === 'helix') return box3(-spine.radius, Math.min(0, spine.pitch * spine.turns), -spine.radius, spine.radius, Math.max(0, spine.pitch * spine.turns), spine.radius);
|
|
515
|
+
if (spine.kind === 'points') {
|
|
516
|
+
const b = pointsBounds(spine.points, 3);
|
|
517
|
+
const pad = 0.15 * Math.hypot(b.max[0] - b.min[0], b.max[1] - b.min[1], b.max[2] - b.min[2]);
|
|
518
|
+
return pointsBounds(spine.points, 3, pad);
|
|
519
|
+
}
|
|
520
|
+
const b = box3(Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity);
|
|
521
|
+
for (const s of spine.segments) {
|
|
522
|
+
if (s.c === 'Z') continue;
|
|
523
|
+
const pts = [[s.x, s.y, s.z]];
|
|
524
|
+
if (s.c === 'C') pts.push([s.x1, s.y1, s.z1], [s.x2, s.y2, s.z2]);
|
|
525
|
+
if (s.c === 'Q') pts.push([s.x1, s.y1, s.z1]);
|
|
526
|
+
for (const p of pts) for (let k = 0; k < 3; k++) {
|
|
527
|
+
b.min[k] = Math.min(b.min[k], p[k] || 0);
|
|
528
|
+
b.max[k] = Math.max(b.max[k], p[k] || 0);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return b;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
registerGeometry('sweep', (el) => {
|
|
535
|
+
const spine = el.values.along;
|
|
536
|
+
const radius = el.values.radius;
|
|
537
|
+
const src = radius === undefined ? pathSource(el) : null;
|
|
538
|
+
if (radius === undefined && !src) return null;
|
|
539
|
+
const smooth = valueOf(el, 'smooth');
|
|
540
|
+
const twist = valueOf(el, 'twist');
|
|
541
|
+
const taper = valueOf(el, 'taper');
|
|
542
|
+
const caps = valueOf(el, 'caps');
|
|
543
|
+
const fixed = el.values.segments;
|
|
544
|
+
let profileR = radius;
|
|
545
|
+
if (src) {
|
|
546
|
+
const cb = pathControlBounds(src.path.segments);
|
|
547
|
+
profileR = Math.max(Math.abs(cb.minX), Math.abs(cb.maxX), Math.abs(cb.minY), Math.abs(cb.maxY));
|
|
548
|
+
}
|
|
549
|
+
const sb = spineBounds(spine);
|
|
550
|
+
const pad = profileR * Math.max(1, taper) * 1.5;
|
|
551
|
+
const spineOpen = spine.kind === 'helix' || (spine.kind === 'path' ? subpathsOpen(spine.segments) : !(spine.points.length > 6 && spine.points[0] === spine.points[spine.points.length - 3] && spine.points[1] === spine.points[spine.points.length - 2] && spine.points[2] === spine.points[spine.points.length - 1]));
|
|
552
|
+
const spineSize = Math.hypot(sb.max[0] - sb.min[0], sb.max[1] - sb.min[1], sb.max[2] - sb.min[2]) || 1;
|
|
553
|
+
return {
|
|
554
|
+
key: `sweep:${src ? hashString(src.raw) + ':' + src.fillRule : 'r' + radius}:${hashString(rawOf(el, 'along'))}:${smooth}:${twist}:${taper}:${caps}:${fixed || ''}`,
|
|
555
|
+
unit: null,
|
|
556
|
+
lods: LOD_COUNT,
|
|
557
|
+
kind: 'triangles',
|
|
558
|
+
doubleSided: !caps && spineOpen,
|
|
559
|
+
bounds: box3(sb.min[0] - pad, sb.min[1] - pad, sb.min[2] - pad, sb.max[0] + pad, sb.max[1] + pad, sb.max[2] + pad),
|
|
560
|
+
lodDeviation: lodList((lod) => {
|
|
561
|
+
const profileErr = src
|
|
562
|
+
? pathFlattenDeviation(pathDiag(src.path.segments), lod)
|
|
563
|
+
: (radius || 0) * circleSagitta(fixed || lodSegments(24, lod, 1, 6));
|
|
564
|
+
return pathFlattenDeviation(spineSize, lod) + profileErr;
|
|
565
|
+
}),
|
|
566
|
+
generate: (lod, q, report) => {
|
|
567
|
+
const spines = spinePolylines(spine, {
|
|
568
|
+
tolerance: pathFlattenDeviation(spineSize, lod) / q,
|
|
569
|
+
segmentsPerTurn: lodSegments(48, lod, q, 8),
|
|
570
|
+
smooth,
|
|
571
|
+
subdivisions: lodSegments(12, lod, q, 2),
|
|
572
|
+
});
|
|
573
|
+
const profile = src
|
|
574
|
+
? { kind: 'shape', ...regionsFor(src, lod, q, report) }
|
|
575
|
+
: { kind: 'circle', radius, segments: fixed || lodSegments(24, lod, q, 6) };
|
|
576
|
+
return sweepMesh(profile, spines, { twist, taper, caps }, report).mesh;
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
registerGeometry('gear', (el) => {
|
|
582
|
+
const teeth = valueOf(el, 'teeth');
|
|
583
|
+
const m = valueOf(el, 'module');
|
|
584
|
+
const pa = valueOf(el, 'pressureAngle');
|
|
585
|
+
const depth = valueOf(el, 'depth') / m;
|
|
586
|
+
const bore = valueOf(el, 'bore') / m;
|
|
587
|
+
const bevel = valueOf(el, 'bevel') / m;
|
|
588
|
+
const center = valueOf(el, 'center');
|
|
589
|
+
const ra = teeth / 2 + 1;
|
|
590
|
+
const z0 = center ? -depth / 2 : 0;
|
|
591
|
+
return {
|
|
592
|
+
key: `gear:${teeth}:${pa}:${depth}:${bore}:${bevel}:${center}`,
|
|
593
|
+
unit: S(m, m, m),
|
|
594
|
+
lods: LOD_COUNT,
|
|
595
|
+
kind: 'triangles',
|
|
596
|
+
doubleSided: false,
|
|
597
|
+
bounds: box3(-ra, -ra, z0, ra, ra, z0 + depth),
|
|
598
|
+
lodDeviation: lodList((lod) => gearDeviation(teeth, lodSegments(8, lod, 1, 2)) + bevelDeviation(bevel, lod >= 2 ? 1 : 2)),
|
|
599
|
+
generate: (lod, q, report) => {
|
|
600
|
+
const flank = lodSegments(8, lod, q, 2);
|
|
601
|
+
const arc = lodSegments(4, lod, q, 2);
|
|
602
|
+
const g = gearOutline(teeth, pa, flank, arc);
|
|
603
|
+
const outer = { ...g.outline, isWall: true };
|
|
604
|
+
const holes = [];
|
|
605
|
+
if (bore > 0) {
|
|
606
|
+
const c = circleContour(bore, lodSegments(48, lod, q, 8));
|
|
607
|
+
// holes are clockwise
|
|
608
|
+
const n = c.points.length / 2;
|
|
609
|
+
const rev = new Float64Array(c.points.length);
|
|
610
|
+
for (let i = 0; i < n; i++) {
|
|
611
|
+
rev[i * 2] = c.points[(n - 1 - i) * 2];
|
|
612
|
+
rev[i * 2 + 1] = c.points[(n - 1 - i) * 2 + 1];
|
|
613
|
+
}
|
|
614
|
+
holes.push({ points: rev, smooth: c.smooth, closed: true, isWall: true });
|
|
615
|
+
}
|
|
616
|
+
const shape = { regions: [{ outer, holes }], walls: [outer, ...holes] };
|
|
617
|
+
return extrudeMesh(shape, { depth, bevel, bevelSegments: lod >= 2 ? 1 : 2, center }, report);
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
registerGeometry('mesh', (el) => {
|
|
623
|
+
const positions = el.values.positions;
|
|
624
|
+
const indices = el.values.indices || Uint32Array.from({ length: positions.length / 3 }, (_, i) => i);
|
|
625
|
+
const normals = el.values.normals;
|
|
626
|
+
const uvs = el.values.uvs;
|
|
627
|
+
const smooth = valueOf(el, 'smooth');
|
|
628
|
+
// keyed by value, not spelling: "1.0" and "1" describe the same mesh
|
|
629
|
+
const key = `mesh:${hashTyped(positions)}:${hashTyped(el.values.indices)}:${hashTyped(normals)}:${hashTyped(uvs)}:${smooth}`;
|
|
630
|
+
return {
|
|
631
|
+
key,
|
|
632
|
+
unit: null,
|
|
633
|
+
lods: 1,
|
|
634
|
+
kind: 'triangles',
|
|
635
|
+
doubleSided: false,
|
|
636
|
+
bounds: pointsBounds(positions, 3),
|
|
637
|
+
lodDeviation: [0],
|
|
638
|
+
generate: () => {
|
|
639
|
+
if (normals) return new Mesh({ positions: Float32Array.from(positions), normals: Float32Array.from(normals), uvs: uvs ? Float32Array.from(uvs) : null, indices: Uint32Array.from(indices) });
|
|
640
|
+
return computeNormals(positions, indices, smooth, uvs);
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
builtins = new Set(registry.keys());
|