@es3d/es3d 1.0.1 → 1.1.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/README.md +27 -7
- package/browser/es3d-stage.min.js +10349 -0
- package/docs/SPEC.md +9 -0
- package/package.json +4 -2
- package/scripts/es3d-cli.mjs +56 -20
- package/src/format/parse.js +43 -3
- package/src/index.js +12 -5
- package/src/io/convert.js +53 -5
- package/src/io/export-model.js +679 -0
- package/src/io/file-loader.js +3 -2
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/openscad-import.js +1295 -0
- package/src/io/openscad.js +252 -0
- package/src/io/text-encode.js +87 -0
- package/src/io/threejs.js +397 -0
- package/src/io/usd.js +669 -0
- package/src/io/x3d.js +1183 -0
- package/src/optimize/optimizer.js +34 -9
- package/src/render/stage.js +87 -11
- package/src/scene/flatten.js +208 -4
- package/src/scene/scene-graph.js +13 -2
- package/src/stage.js +8 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// three.js r186 module. The script is `import * as THREE from 'three'` and
|
|
2
|
+
// `export function createScene()`, which returns a Group. three.js is Y-up,
|
|
3
|
+
// the same as ES3D, and Matrix4.fromArray is column-major.
|
|
4
|
+
//
|
|
5
|
+
// Geometry (r186):
|
|
6
|
+
// BoxGeometry(width, height, depth) is centred.
|
|
7
|
+
// SphereGeometry(radius, widthSegments, heightSegments).
|
|
8
|
+
// CylinderGeometry(radiusTop, radiusBottom, height, radialSegments, 1, openEnded).
|
|
9
|
+
// radiusTop is the +Y end, which is the ES3D top.
|
|
10
|
+
// CapsuleGeometry(radius, length, capSegments, radialSegments). length is the
|
|
11
|
+
// cylindrical middle, not the total height, so length = h − 2r. A capsule
|
|
12
|
+
// with no middle is a SphereGeometry.
|
|
13
|
+
// PlaneGeometry(width, height) lies in XY facing +Z. A rotation of −90°
|
|
14
|
+
// about X, matrix [1, 0, 0, 0, 0, 0, −1, 0, 0, 1, 0, 0, 0, 0, 0, 1] in
|
|
15
|
+
// column-major order, lays it in XZ facing +Y. (x, y, 0) becomes (x, 0, −y);
|
|
16
|
+
// a centred plane is unchanged by the sign.
|
|
17
|
+
// TorusGeometry lies in XY around Z. The same −90° rotation about X puts it around Y.
|
|
18
|
+
// ExtrudeGeometry extrudes a Shape along +Z, which matches an ES3D extrude with no bevel.
|
|
19
|
+
// LatheGeometry revolves Vector2(radius, height) around Y. Its zero angle lies on +Z,
|
|
20
|
+
// so a partial revolve starts at π/2, which is +X, and turns toward −Z.
|
|
21
|
+
// MeshStandardMaterial.color is sRGB. The property is metalness, and roughness
|
|
22
|
+
// defaults to 1, so 0.5 is always written. metalness defaults to 0.
|
|
23
|
+
//
|
|
24
|
+
// A repeated single mesh is an InstancedMesh. A repeated group is a loop that
|
|
25
|
+
// clones meshes whose geometry was created once. Scatter with a random offset
|
|
26
|
+
// inlines mulberry32 and es3dSin, the same functions the flattener uses, so
|
|
27
|
+
// the positions match. The script does not import ES3D and it is not minified.
|
|
28
|
+
|
|
29
|
+
import * as mat4 from '../math/mat4.js';
|
|
30
|
+
import { n, trsOf } from './text-encode.js';
|
|
31
|
+
import { paint } from './export-model.js';
|
|
32
|
+
|
|
33
|
+
const RX90 = new Float64Array([1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]);
|
|
34
|
+
|
|
35
|
+
function nums(values) {
|
|
36
|
+
return Array.from(values, (v) => n(v)).join(', ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @returns {{text: string, notes: string[]}}
|
|
41
|
+
*/
|
|
42
|
+
export function exportThree(model) {
|
|
43
|
+
const notes = model.notes.slice();
|
|
44
|
+
const tally = new Map();
|
|
45
|
+
const add = (key, k = 1) => tally.set(key, (tally.get(key) || 0) + k);
|
|
46
|
+
const lines = [];
|
|
47
|
+
const geos = new Map();
|
|
48
|
+
const mats = new Map();
|
|
49
|
+
let seq = 0;
|
|
50
|
+
const fresh = (p) => `${p}${seq++}`;
|
|
51
|
+
let needsRandom = false;
|
|
52
|
+
|
|
53
|
+
const material = (index, forceDouble = false) => {
|
|
54
|
+
const key = `${index}:${forceDouble ? 1 : 0}`;
|
|
55
|
+
if (mats.has(key)) return mats.get(key);
|
|
56
|
+
const id = fresh('m');
|
|
57
|
+
mats.set(key, id);
|
|
58
|
+
const m = model.materials[index];
|
|
59
|
+
const c = m.baseColorSrgb;
|
|
60
|
+
const side = (m.doubleSided || forceDouble) ? ', side: THREE.DoubleSide' : '';
|
|
61
|
+
if (m.unlit) {
|
|
62
|
+
lines.push(` const ${id} = new THREE.MeshBasicMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
lines.push(` const ${id} = new THREE.MeshStandardMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), metalness: ${n(m.metallic)}, roughness: ${n(m.roughness)}, transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
|
|
66
|
+
return id;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const geometry = (solid) => {
|
|
70
|
+
const key = solid.key;
|
|
71
|
+
if (geos.has(key)) return geos.get(key);
|
|
72
|
+
const id = fresh('g');
|
|
73
|
+
geos.set(key, id);
|
|
74
|
+
lines.push(` const ${id} = ${geometryExpr(solid)};`);
|
|
75
|
+
return id;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const geometryExpr = (solid) => {
|
|
79
|
+
if (solid.kind === 'box') {
|
|
80
|
+
const s = solid.size;
|
|
81
|
+
return `new THREE.BoxGeometry(${n(s[0])}, ${n(s[1])}, ${n(s[2])})`;
|
|
82
|
+
}
|
|
83
|
+
if (solid.kind === 'sphere') return `new THREE.SphereGeometry(${n(solid.radius)}, ${solid.segments}, ${Math.max(3, Math.round(solid.segments / 2))})`;
|
|
84
|
+
if (solid.kind === 'cylinder' || solid.kind === 'cone' || solid.kind === 'frustum') {
|
|
85
|
+
const top = solid.kind === 'cone' ? 0 : (solid.radiusTop !== undefined ? solid.radiusTop : solid.radius);
|
|
86
|
+
const bottom = solid.radius;
|
|
87
|
+
return `new THREE.CylinderGeometry(${n(top)}, ${n(bottom)}, ${n(solid.height)}, ${solid.segments}, 1, ${solid.caps === false})`;
|
|
88
|
+
}
|
|
89
|
+
if (solid.kind === 'capsule') {
|
|
90
|
+
const mid = solid.height - 2 * solid.radius;
|
|
91
|
+
if (mid <= 1e-6) return `new THREE.SphereGeometry(${n(solid.radius)}, ${solid.segments}, ${Math.max(3, Math.round(solid.segments / 2))})`;
|
|
92
|
+
const caps = Math.max(1, Math.round(solid.segments / 4));
|
|
93
|
+
return `new THREE.CapsuleGeometry(${n(solid.radius)}, ${n(mid)}, ${caps}, ${solid.segments})`;
|
|
94
|
+
}
|
|
95
|
+
if (solid.kind === 'plane') return `new THREE.PlaneGeometry(${n(solid.width)}, ${n(solid.depth)})`;
|
|
96
|
+
if (solid.kind === 'torus') return `new THREE.TorusGeometry(${n(solid.radius)}, ${n(solid.tube)}, ${solid.tubeSegments}, ${solid.segments})`;
|
|
97
|
+
if (solid.kind === 'extrude') return extrudeExpr(solid);
|
|
98
|
+
if (solid.kind === 'revolve') return latheExpr(solid);
|
|
99
|
+
if (solid.kind === 'surface' && solid.regions.length === 1) return shapeExpr(solid.regions[0], 'ShapeGeometry');
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const meshOf = (node) => {
|
|
104
|
+
if (node.solid.kind === 'mesh' || node.solid.kind === 'lines' || geometryExpr(node.solid) === null) {
|
|
105
|
+
return baked(node);
|
|
106
|
+
}
|
|
107
|
+
const g = geometry(node.solid);
|
|
108
|
+
const m = material(node.material, node.doubleSided);
|
|
109
|
+
if (node.solid.kind === 'plane') add('plane');
|
|
110
|
+
const id = fresh('o');
|
|
111
|
+
lines.push(` const ${id} = new THREE.Mesh(${g}, ${m});`);
|
|
112
|
+
apply(id, placed(node));
|
|
113
|
+
return id;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const baked = (node) => {
|
|
117
|
+
const mesh = node.solid.kind === 'mesh' || node.solid.kind === 'lines' ? node.solid : model.bakedMesh(node.entry);
|
|
118
|
+
add(node.solid.kind === 'mesh' || node.solid.kind === 'lines' ? 'mesh' : node.solid.kind);
|
|
119
|
+
const g = fresh('g');
|
|
120
|
+
const positions = `new Float32Array([${nums(mesh.positions)}])`;
|
|
121
|
+
lines.push(` const ${g} = new THREE.BufferGeometry();`);
|
|
122
|
+
lines.push(` ${g}.setAttribute('position', new THREE.BufferAttribute(${positions}, 3));`);
|
|
123
|
+
if (mesh.normals) lines.push(` ${g}.setAttribute('normal', new THREE.BufferAttribute(new Float32Array([${nums(mesh.normals)}]), 3));`);
|
|
124
|
+
lines.push(` ${g}.setIndex([${Array.from(mesh.indices).join(', ')}]);`);
|
|
125
|
+
const m = material(node.material, node.doubleSided);
|
|
126
|
+
const id = fresh('o');
|
|
127
|
+
const kind = mesh.kind === 'lines' ? 'LineSegments' : 'Mesh';
|
|
128
|
+
lines.push(` const ${id} = new THREE.${kind}(${g}, ${m});`);
|
|
129
|
+
const matrix = node.solid.kind === 'mesh' || node.solid.kind === 'lines' ? node.matrix : node.meshMatrix;
|
|
130
|
+
apply(id, matrix);
|
|
131
|
+
return id;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const apply = (id, matrix) => {
|
|
135
|
+
if (!matrix) return;
|
|
136
|
+
const t = trsOf(matrix);
|
|
137
|
+
if (t && t.plain) {
|
|
138
|
+
lines.push(` ${id}.position.set(${n(t.translation[0])}, ${n(t.translation[1])}, ${n(t.translation[2])});`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
lines.push(` ${id}.matrix.fromArray([${nums(matrix)}]);`);
|
|
142
|
+
lines.push(` ${id}.matrixAutoUpdate = false;`);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const emit = (node, parent) => {
|
|
146
|
+
if (!node) return;
|
|
147
|
+
if (node.kind === 'shape') {
|
|
148
|
+
const id = meshOf(node);
|
|
149
|
+
lines.push(` ${parent}.add(${id});`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (node.kind === 'repeat' && node.children.length === 1 && node.children[0].kind === 'shape' && geometryExpr(node.children[0].solid)) {
|
|
153
|
+
instanced(node, parent);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (node.kind === 'repeat' || node.kind === 'scatter') {
|
|
157
|
+
looped(node, parent);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const id = fresh('o');
|
|
161
|
+
lines.push(` const ${id} = new THREE.Group();`);
|
|
162
|
+
apply(id, node.matrix);
|
|
163
|
+
lines.push(` ${parent}.add(${id});`);
|
|
164
|
+
for (const c of node.children) emit(c, id);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const instanced = (node, parent) => {
|
|
168
|
+
add('instanced');
|
|
169
|
+
const child = node.children[0];
|
|
170
|
+
if (child.solid.kind === 'plane') add('plane', node.count);
|
|
171
|
+
const g = geometry(child.solid);
|
|
172
|
+
const m = material(child.material, child.doubleSided);
|
|
173
|
+
const id = fresh('o');
|
|
174
|
+
lines.push(` const ${id} = new THREE.InstancedMesh(${g}, ${m}, ${node.count});`);
|
|
175
|
+
const step = node.step;
|
|
176
|
+
lines.push(` const step_${id} = new THREE.Matrix4().fromArray([${nums(step)}]);`);
|
|
177
|
+
lines.push(` const acc_${id} = new THREE.Matrix4();`);
|
|
178
|
+
lines.push(` const local_${id} = new THREE.Matrix4().fromArray([${nums(placed(child) || mat4.createF64())}]);`);
|
|
179
|
+
lines.push(` const tmp_${id} = new THREE.Matrix4();`);
|
|
180
|
+
lines.push(` for (let i = 0; i < ${node.count}; i++) {`);
|
|
181
|
+
lines.push(` tmp_${id}.multiplyMatrices(acc_${id}, local_${id});`);
|
|
182
|
+
lines.push(` ${id}.setMatrixAt(i, tmp_${id});`);
|
|
183
|
+
lines.push(` acc_${id}.multiply(step_${id});`);
|
|
184
|
+
lines.push(` }`);
|
|
185
|
+
apply(id, node.matrix);
|
|
186
|
+
lines.push(` ${parent}.add(${id});`);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const looped = (node, parent) => {
|
|
190
|
+
if (node.kind === 'scatter') {
|
|
191
|
+
needsRandom = !!(node.params.radius || node.params.randomRotation);
|
|
192
|
+
add('scatter', node.params.count);
|
|
193
|
+
} else add('repeat', node.count);
|
|
194
|
+
const holder = fresh('o');
|
|
195
|
+
lines.push(` const ${holder} = new THREE.Group();`);
|
|
196
|
+
apply(holder, node.matrix);
|
|
197
|
+
lines.push(` ${parent}.add(${holder});`);
|
|
198
|
+
const materialIds = node.kind === 'scatter' && node.params.randomMaterial && node.params.randomMaterial.length
|
|
199
|
+
? node.params.randomMaterial
|
|
200
|
+
: [null];
|
|
201
|
+
const factories = materialIds.map((id) => {
|
|
202
|
+
const kids = id ? node.children.map((c) => paint(c, id, null, model.resolve)) : node.children;
|
|
203
|
+
const name = fresh('make');
|
|
204
|
+
lines.push(` const ${name} = () => {`);
|
|
205
|
+
lines.push(` const copy = new THREE.Group();`);
|
|
206
|
+
for (const c of kids) emit(c, 'copy');
|
|
207
|
+
lines.push(` return copy;`);
|
|
208
|
+
lines.push(` };`);
|
|
209
|
+
return name;
|
|
210
|
+
});
|
|
211
|
+
const callCopy = factories.length === 1
|
|
212
|
+
? `const copy = ${factories[0]}();`
|
|
213
|
+
: `const choices = [${factories.join(', ')}]; const mi = Math.min(choices.length - 1, Math.floor(u[8] * choices.length)); const copy = choices[mi]();`;
|
|
214
|
+
if (node.kind === 'repeat') {
|
|
215
|
+
lines.push(` {`);
|
|
216
|
+
lines.push(` const step = new THREE.Matrix4().fromArray([${nums(node.step)}]);`);
|
|
217
|
+
lines.push(` const acc = new THREE.Matrix4();`);
|
|
218
|
+
lines.push(` for (let i = 0; i < ${node.count}; i++) {`);
|
|
219
|
+
lines.push(` ${callCopy}`);
|
|
220
|
+
lines.push(` copy.matrix.multiplyMatrices(acc, copy.matrix);`);
|
|
221
|
+
lines.push(` copy.matrixAutoUpdate = false;`);
|
|
222
|
+
lines.push(` ${holder}.add(copy);`);
|
|
223
|
+
lines.push(` acc.multiply(step);`);
|
|
224
|
+
lines.push(` }`);
|
|
225
|
+
lines.push(` }`);
|
|
226
|
+
} else {
|
|
227
|
+
needsRandom = true;
|
|
228
|
+
const p = node.params;
|
|
229
|
+
lines.push(` {`);
|
|
230
|
+
lines.push(` const rand = mulberry32(${p.seed >>> 0});`);
|
|
231
|
+
lines.push(` const lerp = (r, t) => r[0] + (r[1] - r[0]) * t;`);
|
|
232
|
+
lines.push(` for (let i = 0; i < ${p.count}; i++) {`);
|
|
233
|
+
lines.push(` const u = [rand(), rand(), rand(), rand(), rand(), rand(), rand(), rand(), rand()];`);
|
|
234
|
+
lines.push(` const pos = [0, 0, 0], rot = [0, 0, 0], scl = [1, 1, 1];`);
|
|
235
|
+
if (p.grid) {
|
|
236
|
+
lines.push(` const ix = i % ${p.grid[0]}, iz = Math.floor(i / ${p.grid[0]});`);
|
|
237
|
+
lines.push(` pos[0] = (ix - ${n((p.grid[0] - 1) / 2)}) * ${n(p.spacing[0])} + (u[0] - 0.5) * ${n(p.jitter)} * ${n(p.spacing[0])};`);
|
|
238
|
+
lines.push(` pos[2] = (iz - ${n((p.grid[1] - 1) / 2)}) * ${n(p.spacing[1])} + (u[1] - 0.5) * ${n(p.jitter)} * ${n(p.spacing[1])};`);
|
|
239
|
+
} else if (p.radius) {
|
|
240
|
+
lines.push(` const r = ${n(p.radius)} * Math.sqrt(u[0]);`);
|
|
241
|
+
lines.push(` const a = u[1] * Math.PI * 2;`);
|
|
242
|
+
lines.push(` pos[0] = r * es3dCos(a);`);
|
|
243
|
+
lines.push(` pos[2] = r * es3dSin(a);`);
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(` pos[0] = (u[0] - 0.5) * ${n(p.area[0])};`);
|
|
246
|
+
lines.push(` pos[2] = (u[1] - 0.5) * ${n(p.area[1])};`);
|
|
247
|
+
}
|
|
248
|
+
if (p.randomRotation) {
|
|
249
|
+
const rr = p.randomRotation.ranges;
|
|
250
|
+
lines.push(` rot[0] = lerp([${n(rr[0][0])}, ${n(rr[0][1])}], u[2]);`);
|
|
251
|
+
lines.push(` rot[1] = lerp([${n(rr[1][0])}, ${n(rr[1][1])}], u[3]);`);
|
|
252
|
+
lines.push(` rot[2] = lerp([${n(rr[2][0])}, ${n(rr[2][1])}], u[4]);`);
|
|
253
|
+
}
|
|
254
|
+
if (p.randomScale) {
|
|
255
|
+
const rs = p.randomScale.ranges;
|
|
256
|
+
if (p.randomScale.uniform) lines.push(` scl[0] = scl[1] = scl[2] = lerp([${n(rs[0][0])}, ${n(rs[0][1])}], u[5]);`);
|
|
257
|
+
else {
|
|
258
|
+
lines.push(` scl[0] = lerp([${n(rs[0][0])}, ${n(rs[0][1])}], u[5]);`);
|
|
259
|
+
lines.push(` scl[1] = lerp([${n(rs[1][0])}, ${n(rs[1][1])}], u[6]);`);
|
|
260
|
+
lines.push(` scl[2] = lerp([${n(rs[2][0])}, ${n(rs[2][1])}], u[7]);`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
lines.push(` const x = rot[0] * Math.PI / 180, y = rot[1] * Math.PI / 180, z = rot[2] * Math.PI / 180;`);
|
|
264
|
+
lines.push(` const cx = es3dCos(x), sx = es3dSin(x), cy = es3dCos(y), sy = es3dSin(y), cz = es3dCos(z), sz = es3dSin(z);`);
|
|
265
|
+
lines.push(` const e = [cz * cy, sz * cy, -sy, 0, cz * sy * sx - sz * cx, sz * sy * sx + cz * cx, cy * sx, 0, cz * sy * cx + sz * sx, sz * sy * cx - cz * sx, cy * cx, 0, pos[0], pos[1], pos[2], 1];`);
|
|
266
|
+
lines.push(` e[0] *= scl[0]; e[1] *= scl[0]; e[2] *= scl[0]; e[4] *= scl[1]; e[5] *= scl[1]; e[6] *= scl[1]; e[8] *= scl[2]; e[9] *= scl[2]; e[10] *= scl[2];`);
|
|
267
|
+
lines.push(` const placed = new THREE.Matrix4().fromArray(e);`);
|
|
268
|
+
lines.push(` ${callCopy}`);
|
|
269
|
+
lines.push(` copy.matrix.multiplyMatrices(placed, copy.matrix);`);
|
|
270
|
+
lines.push(` copy.matrixAutoUpdate = false;`);
|
|
271
|
+
lines.push(` ${holder}.add(copy);`);
|
|
272
|
+
lines.push(` }`);
|
|
273
|
+
lines.push(` }`);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
emit(model.root, 'root');
|
|
278
|
+
for (const light of model.lights) lines.push(lightLine(light, add));
|
|
279
|
+
for (const camera of model.cameras) lines.push(cameraLine(camera));
|
|
280
|
+
|
|
281
|
+
if (tally.get('plane')) notes.push(`${tally.get('plane').toLocaleString('en-US')} plane${tally.get('plane') === 1 ? '' : 's'} rotated from XY into XZ.`);
|
|
282
|
+
if (tally.get('instanced')) notes.push(`${tally.get('instanced').toLocaleString('en-US')} repeat${tally.get('instanced') === 1 ? '' : 's'} written as an InstancedMesh.`);
|
|
283
|
+
if (tally.get('scatter')) notes.push(`${tally.get('scatter').toLocaleString('en-US')} scattered copies placed with the same mulberry32 sequence as ES3D.`);
|
|
284
|
+
const dropped = ['extrude', 'revolve', 'surface', 'torus', 'frustum'].filter((k) => tally.get(k));
|
|
285
|
+
if (dropped.length) notes.push(`Some shapes were written as triangle buffers (${dropped.join(', ')}).`);
|
|
286
|
+
|
|
287
|
+
const helper = needsRandom ? RANDOM_HELPER : '';
|
|
288
|
+
const text = `import * as THREE from 'three';
|
|
289
|
+
${helper}
|
|
290
|
+
export function createScene() {
|
|
291
|
+
const root = new THREE.Group();
|
|
292
|
+
${lines.join('\n')}
|
|
293
|
+
return root;
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
return { text, notes };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function lightLine(light) {
|
|
300
|
+
const c = light.color;
|
|
301
|
+
const color = `new THREE.Color().setRGB(${n(c[0])}, ${n(c[1])}, ${n(c[2])}, THREE.LinearSRGBColorSpace)`;
|
|
302
|
+
if (light.type === 'ambient') return ` root.add(new THREE.AmbientLight(${color}));`;
|
|
303
|
+
if (light.type === 'hemisphere') return ` root.add(new THREE.HemisphereLight(${color}, new THREE.Color().setRGB(${n(light.ground[0])}, ${n(light.ground[1])}, ${n(light.ground[2])}, THREE.LinearSRGBColorSpace)));`;
|
|
304
|
+
const p = light.position, d = light.direction;
|
|
305
|
+
if (light.type === 'directional') return ` { const L = new THREE.DirectionalLight(${color}); L.position.set(${n(p[0] - d[0])}, ${n(p[1] - d[1])}, ${n(p[2] - d[2])}); L.target.position.set(${n(p[0])}, ${n(p[1])}, ${n(p[2])}); root.add(L); root.add(L.target); }`;
|
|
306
|
+
if (light.type === 'spot') {
|
|
307
|
+
const angle = Math.acos(Math.min(1, Math.max(-1, light.outer)));
|
|
308
|
+
return ` { const L = new THREE.SpotLight(${color}); L.position.set(${n(p[0])}, ${n(p[1])}, ${n(p[2])}); L.target.position.set(${n(p[0] + d[0])}, ${n(p[1] + d[1])}, ${n(p[2] + d[2])}); L.angle = ${n(angle)}; root.add(L); root.add(L.target); }`;
|
|
309
|
+
}
|
|
310
|
+
return ` { const L = new THREE.PointLight(${color}); L.position.set(${n(p[0])}, ${n(p[1])}, ${n(p[2])}); root.add(L); }`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function cameraLine(camera) {
|
|
314
|
+
const p = camera.position, t = camera.target;
|
|
315
|
+
if (camera.type === 'orthographic') {
|
|
316
|
+
const hh = camera.height / 2, hw = hh * (camera.aspect || 1);
|
|
317
|
+
return ` { const C = new THREE.OrthographicCamera(${n(-hw)}, ${n(hw)}, ${n(hh)}, ${n(-hh)}); C.position.set(${n(p[0])}, ${n(p[1])}, ${n(p[2])}); C.lookAt(${n(t[0])}, ${n(t[1])}, ${n(t[2])}); root.add(C); }`;
|
|
318
|
+
}
|
|
319
|
+
return ` { const C = new THREE.PerspectiveCamera(${n(camera.fov || 45)}, ${n(camera.aspect || 1)}); C.position.set(${n(p[0])}, ${n(p[1])}, ${n(p[2])}); C.lookAt(${n(t[0])}, ${n(t[1])}, ${n(t[2])}); root.add(C); }`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pathCalls(pts) {
|
|
323
|
+
let text = '';
|
|
324
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
325
|
+
const call = i === 0 ? 'moveTo' : 'lineTo';
|
|
326
|
+
text += `.${call}(${n(pts[i])}, ${n(pts[i + 1])})`;
|
|
327
|
+
}
|
|
328
|
+
return text + '.closePath()';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function shapeExpr(region, ctor) {
|
|
332
|
+
const holes = (region.holes || []).map((h, i) => ` const h${i} = new THREE.Path()${pathCalls(h)}; s.holes.push(h${i});`).join('');
|
|
333
|
+
return `(() => { const s = new THREE.Shape()${pathCalls(region.outer)};${holes} return new THREE.${ctor}(s); })()`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function extrudeExpr(solid) {
|
|
337
|
+
const bodies = solid.regions.map((region, i) => {
|
|
338
|
+
const holes = (region.holes || []).map((h, k) => ` const h${i}${k} = new THREE.Path()${pathCalls(h)}; s${i}.holes.push(h${i}${k});`).join('');
|
|
339
|
+
return `const s${i} = new THREE.Shape()${pathCalls(region.outer)};${holes}`;
|
|
340
|
+
}).join(' ');
|
|
341
|
+
const list = solid.regions.map((_, i) => `s${i}`).join(', ');
|
|
342
|
+
const center = solid.center ? `.translate(0, 0, ${n(-solid.depth / 2)})` : '';
|
|
343
|
+
return `(() => { ${bodies} return new THREE.ExtrudeGeometry([${list}], { depth: ${n(solid.depth)}, bevelEnabled: false })${center}; })()`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function latheExpr(solid) {
|
|
347
|
+
const pts = [];
|
|
348
|
+
for (let i = 0; i < solid.points.length; i += 2) pts.push(`new THREE.Vector2(${n(solid.points[i])}, ${n(solid.points[i + 1])})`);
|
|
349
|
+
const full = !(solid.angle < 360 - 1e-6);
|
|
350
|
+
// phi 0 sits on +Z. ES3D starts on +X and turns toward −Z, which is phi = π/2.
|
|
351
|
+
const sweep = full ? '' : `, ${n(Math.PI / 2)}, ${n(solid.angle * Math.PI / 180)}`;
|
|
352
|
+
return `new THREE.LatheGeometry([${pts.join(', ')}], ${solid.segments}${sweep})`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function placed(node) {
|
|
356
|
+
if (!node.solid || (node.solid.kind !== 'plane' && node.solid.kind !== 'torus')) return node.matrix;
|
|
357
|
+
if (!node.matrix) return RX90;
|
|
358
|
+
const out = mat4.createF64();
|
|
359
|
+
mat4.multiply(out, node.matrix, RX90);
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const RANDOM_HELPER = `function mulberry32(seed) {
|
|
364
|
+
let a = seed >>> 0;
|
|
365
|
+
return function next() {
|
|
366
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
367
|
+
let t = a;
|
|
368
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
369
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
370
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const PI = Math.PI, TWO_PI = PI * 2, HALF_PI = PI * 0.5, QUARTER_PI = PI * 0.25;
|
|
374
|
+
const S1 = -1.66666666666666324348e-1, S2 = 8.33333333332248946124e-3, S3 = -1.98412698298579493134e-4;
|
|
375
|
+
const S4 = 2.75573137070700676789e-6, S5 = -2.50507602534068634195e-8, S6 = 1.58969099521155010221e-10;
|
|
376
|
+
const C1 = 4.16666666666666019037e-2, C2 = -1.38888888888741095749e-3, C3 = 2.48015872894767294178e-5;
|
|
377
|
+
const C4 = -2.75573143513906633035e-7, C5 = 2.0875723212981748279e-9, C6 = -1.13596475577881948265e-11;
|
|
378
|
+
function kernelSin(x) {
|
|
379
|
+
const z = x * x, v = z * x, r = S2 + z * (S3 + z * (S4 + z * (S5 + z * S6)));
|
|
380
|
+
return x + v * (S1 + z * r);
|
|
381
|
+
}
|
|
382
|
+
function kernelCos(x) {
|
|
383
|
+
const z = x * x, r = z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6)))));
|
|
384
|
+
return 1 - 0.5 * z + z * r;
|
|
385
|
+
}
|
|
386
|
+
function es3dSin(x) {
|
|
387
|
+
if (!Number.isFinite(x)) return x;
|
|
388
|
+
let y = x - TWO_PI * Math.round(x / TWO_PI);
|
|
389
|
+
let sign = 1;
|
|
390
|
+
if (y < 0) { sign = -1; y = -y; }
|
|
391
|
+
if (y > HALF_PI) y = PI - y;
|
|
392
|
+
if (y < 0) y = 0;
|
|
393
|
+
const s = y > QUARTER_PI ? kernelCos(HALF_PI - y) : kernelSin(y);
|
|
394
|
+
return sign < 0 ? -s : s;
|
|
395
|
+
}
|
|
396
|
+
function es3dCos(x) { return es3dSin(x + HALF_PI); }
|
|
397
|
+
`;
|