@es3d/es3d 1.0.0 → 1.0.2
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 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
package/src/geometry/extrude.js
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
// Flat fills and extrusions of 2D regions.
|
|
2
|
-
//
|
|
3
|
-
// extrude: caps are the triangulated regions (front at z = depth facing +Z,
|
|
4
|
-
// back at z = 0 facing -Z); side walls follow the contours where the fill
|
|
5
|
-
// state changes. An optional quarter-circle bevel insets the caps by `bevel`
|
|
6
|
-
// so the silhouette of the 2D path is kept at the widest point.
|
|
7
|
-
|
|
8
|
-
import { Mesh, MeshBuilder } from './mesh.js';
|
|
9
|
-
import { triangulateRegion } from '../path/shapes.js';
|
|
10
|
-
import { contourFrames, bounds2 } from './contour.js';
|
|
11
|
-
import { extrudeWasm } from '../wasm/kernels.js';
|
|
12
|
-
|
|
13
|
-
function regionBounds(regions) {
|
|
14
|
-
const lists = [];
|
|
15
|
-
for (const r of regions) {
|
|
16
|
-
lists.push(r.outer.points);
|
|
17
|
-
for (const h of r.holes) lists.push(h.points);
|
|
18
|
-
}
|
|
19
|
-
return bounds2(lists);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Warn about a triangulation that does not cover its region (self-intersecting input). */
|
|
23
|
-
export function reportTriangulation(error, report) {
|
|
24
|
-
if (error > 0.01 && report) {
|
|
25
|
-
report('warning', 'W_SELF_INTERSECTION', `A filled contour appears to be self-intersecting; the triangulation covers ${(100 * (1 - error)).toFixed(1)}% of the expected area.`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Triangulate regions once; reports malformed (self-intersecting) input. */
|
|
30
|
-
export function triangulateRegions(regions, report) {
|
|
31
|
-
const out = [];
|
|
32
|
-
for (const r of regions) {
|
|
33
|
-
const t = triangulateRegion(r);
|
|
34
|
-
reportTriangulation(t.error, report);
|
|
35
|
-
out.push({ region: r, ...t });
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Flat double-sided fill in the XY plane (normal +Z). */
|
|
41
|
-
export function flatMesh(regions, report) {
|
|
42
|
-
const tris = triangulateRegions(regions, report);
|
|
43
|
-
let vertices = 0, indices = 0;
|
|
44
|
-
for (const t of tris) {
|
|
45
|
-
vertices += t.positions.length / 2;
|
|
46
|
-
indices += t.indices.length;
|
|
47
|
-
}
|
|
48
|
-
const b = new MeshBuilder('triangles', vertices, indices);
|
|
49
|
-
const bb = regionBounds(regions);
|
|
50
|
-
for (const t of tris) {
|
|
51
|
-
const base = b.vertexCount;
|
|
52
|
-
const p = t.positions;
|
|
53
|
-
for (let i = 0; i < p.length; i += 2) {
|
|
54
|
-
b.vertex(p[i], p[i + 1], 0, 0, 0, 1, (p[i] - bb.minX) / bb.size, (p[i + 1] - bb.minY) / bb.size);
|
|
55
|
-
}
|
|
56
|
-
const idx = t.indices;
|
|
57
|
-
for (let i = 0; i < idx.length; i += 3) b.tri(base + idx[i], base + idx[i + 1], base + idx[i + 2]);
|
|
58
|
-
}
|
|
59
|
-
return b.build();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Inset positions of a contour: p - a * miter (a = 0 for contours that are not walls).
|
|
64
|
-
*/
|
|
65
|
-
function insetPoints(contour, amount) {
|
|
66
|
-
const pts = contour.points;
|
|
67
|
-
if (!amount || contour.isWall === false) return pts;
|
|
68
|
-
const f = contourFrames(pts, contour.smooth, true);
|
|
69
|
-
const out = new Float64Array(pts.length);
|
|
70
|
-
for (let i = 0; i < pts.length; i += 2) {
|
|
71
|
-
out[i] = pts[i] - f.miter[i] * amount;
|
|
72
|
-
out[i + 1] = pts[i + 1] - f.miter[i + 1] * amount;
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Profile rings (back to front) for a straight or bevelled wall. */
|
|
78
|
-
function wallProfile(depth, bevel, segs) {
|
|
79
|
-
const rings = [];
|
|
80
|
-
if (!(bevel > 0)) {
|
|
81
|
-
rings.push({ inset: 0, z: 0, nr: 1, nz: 0 }, { inset: 0, z: depth, nr: 1, nz: 0 });
|
|
82
|
-
return rings;
|
|
83
|
-
}
|
|
84
|
-
for (let k = 0; k <= segs; k++) {
|
|
85
|
-
const phi = (k / segs) * (Math.PI / 2);
|
|
86
|
-
rings.push({ inset: bevel * (1 - Math.sin(phi)), z: bevel * (1 - Math.cos(phi)), nr: Math.sin(phi), nz: -Math.cos(phi) });
|
|
87
|
-
}
|
|
88
|
-
for (let k = segs; k >= 0; k--) {
|
|
89
|
-
const phi = (k / segs) * (Math.PI / 2);
|
|
90
|
-
const ring = { inset: bevel * (1 - Math.sin(phi)), z: depth - bevel * (1 - Math.cos(phi)), nr: Math.sin(phi), nz: Math.cos(phi) };
|
|
91
|
-
// the straight section starts where the back bevel ends
|
|
92
|
-
if (k === segs && Math.abs(ring.z - rings[rings.length - 1].z) < 1e-12) continue;
|
|
93
|
-
rings.push(ring);
|
|
94
|
-
}
|
|
95
|
-
return rings;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* @param {{regions: object[], walls: object[]}} shape from buildRegions (contours carry `smooth` and `isWall`)
|
|
100
|
-
* @param {{depth:number, bevel?:number, bevelSegments?:number, center?:boolean}} o
|
|
101
|
-
*/
|
|
102
|
-
export function extrudeMesh(shape, o, report) {
|
|
103
|
-
const depth = o.depth;
|
|
104
|
-
const bevel = Math.min(o.bevel || 0, depth / 2);
|
|
105
|
-
const segs = o.bevelSegments || 3;
|
|
106
|
-
const z0 = o.center ? -depth / 2 : 0;
|
|
107
|
-
const bb = regionBounds(shape.regions);
|
|
108
|
-
const S = bb.size;
|
|
109
|
-
const rings = wallProfile(depth, bevel, segs);
|
|
110
|
-
const w = extrudeWasm(shape, bevel, rings, z0, z0 + depth, bb.minX, bb.minY, S);
|
|
111
|
-
if (w) {
|
|
112
|
-
for (const e of w.errors) reportTriangulation(e, report);
|
|
113
|
-
return new Mesh({ positions: w.positions, normals: w.normals, uvs: w.uvs, indices: w.indices, kind: 'triangles' });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// caps: the inset contours are triangulated directly (re-using the original
|
|
117
|
-
// triangulation with moved vertices can flip long thin triangles)
|
|
118
|
-
const capRegions = shape.regions.map((r) => ({
|
|
119
|
-
outer: { ...r.outer, points: insetPoints(r.outer, bevel) },
|
|
120
|
-
holes: r.holes.map((h) => ({ ...h, points: insetPoints(h, bevel) })),
|
|
121
|
-
}));
|
|
122
|
-
const caps = triangulateRegions(capRegions, report);
|
|
123
|
-
const walls = shape.walls.map((c) => ({ pts: c.points, f: contourFrames(c.points, c.smooth, true) }));
|
|
124
|
-
// count first, so the mesh is allocated once at its final size
|
|
125
|
-
let vertices = 0, indices = 0;
|
|
126
|
-
for (const t of caps) {
|
|
127
|
-
vertices += t.positions.length;
|
|
128
|
-
indices += 2 * t.indices.length;
|
|
129
|
-
}
|
|
130
|
-
for (const { f } of walls) {
|
|
131
|
-
vertices += f.n * rings.length * 2;
|
|
132
|
-
indices += f.n * (rings.length - 1) * 6;
|
|
133
|
-
}
|
|
134
|
-
const b = new MeshBuilder('triangles', vertices, indices);
|
|
135
|
-
for (const t of caps) {
|
|
136
|
-
const flat = t.positions;
|
|
137
|
-
const idx = t.indices;
|
|
138
|
-
for (const [z, nz] of [[z0 + depth, 1], [z0, -1]]) {
|
|
139
|
-
const base = b.vertexCount;
|
|
140
|
-
for (let i = 0; i < flat.length; i += 2) {
|
|
141
|
-
b.vertex(flat[i], flat[i + 1], z, 0, 0, nz, (flat[i] - bb.minX) / S, (flat[i + 1] - bb.minY) / S);
|
|
142
|
-
}
|
|
143
|
-
for (let i = 0; i < idx.length; i += 3) {
|
|
144
|
-
if (nz > 0) b.tri(base + idx[i], base + idx[i + 1], base + idx[i + 2]);
|
|
145
|
-
else b.tri(base + idx[i], base + idx[i + 2], base + idx[i + 1]);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// walls
|
|
151
|
-
for (const { pts, f } of walls) {
|
|
152
|
-
const n = f.n;
|
|
153
|
-
for (let e = 0; e < n; e++) {
|
|
154
|
-
const i0 = e, i1 = (e + 1) % n;
|
|
155
|
-
// normal at each end of the edge: the edge's own at corners, else the vertex's
|
|
156
|
-
const n0x = f.corner[i0] ? f.edgeN[e * 2] : f.vertN[i0 * 2], n0y = f.corner[i0] ? f.edgeN[e * 2 + 1] : f.vertN[i0 * 2 + 1];
|
|
157
|
-
const n1x = f.corner[i1] ? f.edgeN[e * 2] : f.vertN[i1 * 2], n1y = f.corner[i1] ? f.edgeN[e * 2 + 1] : f.vertN[i1 * 2 + 1];
|
|
158
|
-
const u0 = f.arc[e] / S, u1 = f.arc[e + 1] / S;
|
|
159
|
-
let prevA = -1, prevB = -1;
|
|
160
|
-
for (const ring of rings) {
|
|
161
|
-
const ax = pts[i0 * 2] - f.miter[i0 * 2] * ring.inset;
|
|
162
|
-
const ay = pts[i0 * 2 + 1] - f.miter[i0 * 2 + 1] * ring.inset;
|
|
163
|
-
const bx = pts[i1 * 2] - f.miter[i1 * 2] * ring.inset;
|
|
164
|
-
const by = pts[i1 * 2 + 1] - f.miter[i1 * 2 + 1] * ring.inset;
|
|
165
|
-
const z = z0 + ring.z;
|
|
166
|
-
const a = b.vertex(ax, ay, z, n0x * ring.nr, n0y * ring.nr, ring.nz, u0, ring.z / S);
|
|
167
|
-
const c = b.vertex(bx, by, z, n1x * ring.nr, n1y * ring.nr, ring.nz, u1, ring.z / S);
|
|
168
|
-
if (prevA >= 0) b.quad(prevA, prevB, c, a);
|
|
169
|
-
prevA = a;
|
|
170
|
-
prevB = c;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return b.build();
|
|
175
|
-
}
|
|
1
|
+
// Flat fills and extrusions of 2D regions.
|
|
2
|
+
//
|
|
3
|
+
// extrude: caps are the triangulated regions (front at z = depth facing +Z,
|
|
4
|
+
// back at z = 0 facing -Z); side walls follow the contours where the fill
|
|
5
|
+
// state changes. An optional quarter-circle bevel insets the caps by `bevel`
|
|
6
|
+
// so the silhouette of the 2D path is kept at the widest point.
|
|
7
|
+
|
|
8
|
+
import { Mesh, MeshBuilder } from './mesh.js';
|
|
9
|
+
import { triangulateRegion } from '../path/shapes.js';
|
|
10
|
+
import { contourFrames, bounds2 } from './contour.js';
|
|
11
|
+
import { extrudeWasm } from '../wasm/kernels.js';
|
|
12
|
+
|
|
13
|
+
function regionBounds(regions) {
|
|
14
|
+
const lists = [];
|
|
15
|
+
for (const r of regions) {
|
|
16
|
+
lists.push(r.outer.points);
|
|
17
|
+
for (const h of r.holes) lists.push(h.points);
|
|
18
|
+
}
|
|
19
|
+
return bounds2(lists);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Warn about a triangulation that does not cover its region (self-intersecting input). */
|
|
23
|
+
export function reportTriangulation(error, report) {
|
|
24
|
+
if (error > 0.01 && report) {
|
|
25
|
+
report('warning', 'W_SELF_INTERSECTION', `A filled contour appears to be self-intersecting; the triangulation covers ${(100 * (1 - error)).toFixed(1)}% of the expected area.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Triangulate regions once; reports malformed (self-intersecting) input. */
|
|
30
|
+
export function triangulateRegions(regions, report) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const r of regions) {
|
|
33
|
+
const t = triangulateRegion(r);
|
|
34
|
+
reportTriangulation(t.error, report);
|
|
35
|
+
out.push({ region: r, ...t });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Flat double-sided fill in the XY plane (normal +Z). */
|
|
41
|
+
export function flatMesh(regions, report) {
|
|
42
|
+
const tris = triangulateRegions(regions, report);
|
|
43
|
+
let vertices = 0, indices = 0;
|
|
44
|
+
for (const t of tris) {
|
|
45
|
+
vertices += t.positions.length / 2;
|
|
46
|
+
indices += t.indices.length;
|
|
47
|
+
}
|
|
48
|
+
const b = new MeshBuilder('triangles', vertices, indices);
|
|
49
|
+
const bb = regionBounds(regions);
|
|
50
|
+
for (const t of tris) {
|
|
51
|
+
const base = b.vertexCount;
|
|
52
|
+
const p = t.positions;
|
|
53
|
+
for (let i = 0; i < p.length; i += 2) {
|
|
54
|
+
b.vertex(p[i], p[i + 1], 0, 0, 0, 1, (p[i] - bb.minX) / bb.size, (p[i + 1] - bb.minY) / bb.size);
|
|
55
|
+
}
|
|
56
|
+
const idx = t.indices;
|
|
57
|
+
for (let i = 0; i < idx.length; i += 3) b.tri(base + idx[i], base + idx[i + 1], base + idx[i + 2]);
|
|
58
|
+
}
|
|
59
|
+
return b.build();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Inset positions of a contour: p - a * miter (a = 0 for contours that are not walls).
|
|
64
|
+
*/
|
|
65
|
+
function insetPoints(contour, amount) {
|
|
66
|
+
const pts = contour.points;
|
|
67
|
+
if (!amount || contour.isWall === false) return pts;
|
|
68
|
+
const f = contourFrames(pts, contour.smooth, true);
|
|
69
|
+
const out = new Float64Array(pts.length);
|
|
70
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
71
|
+
out[i] = pts[i] - f.miter[i] * amount;
|
|
72
|
+
out[i + 1] = pts[i + 1] - f.miter[i + 1] * amount;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Profile rings (back to front) for a straight or bevelled wall. */
|
|
78
|
+
function wallProfile(depth, bevel, segs) {
|
|
79
|
+
const rings = [];
|
|
80
|
+
if (!(bevel > 0)) {
|
|
81
|
+
rings.push({ inset: 0, z: 0, nr: 1, nz: 0 }, { inset: 0, z: depth, nr: 1, nz: 0 });
|
|
82
|
+
return rings;
|
|
83
|
+
}
|
|
84
|
+
for (let k = 0; k <= segs; k++) {
|
|
85
|
+
const phi = (k / segs) * (Math.PI / 2);
|
|
86
|
+
rings.push({ inset: bevel * (1 - Math.sin(phi)), z: bevel * (1 - Math.cos(phi)), nr: Math.sin(phi), nz: -Math.cos(phi) });
|
|
87
|
+
}
|
|
88
|
+
for (let k = segs; k >= 0; k--) {
|
|
89
|
+
const phi = (k / segs) * (Math.PI / 2);
|
|
90
|
+
const ring = { inset: bevel * (1 - Math.sin(phi)), z: depth - bevel * (1 - Math.cos(phi)), nr: Math.sin(phi), nz: Math.cos(phi) };
|
|
91
|
+
// the straight section starts where the back bevel ends
|
|
92
|
+
if (k === segs && Math.abs(ring.z - rings[rings.length - 1].z) < 1e-12) continue;
|
|
93
|
+
rings.push(ring);
|
|
94
|
+
}
|
|
95
|
+
return rings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {{regions: object[], walls: object[]}} shape from buildRegions (contours carry `smooth` and `isWall`)
|
|
100
|
+
* @param {{depth:number, bevel?:number, bevelSegments?:number, center?:boolean}} o
|
|
101
|
+
*/
|
|
102
|
+
export function extrudeMesh(shape, o, report) {
|
|
103
|
+
const depth = o.depth;
|
|
104
|
+
const bevel = Math.min(o.bevel || 0, depth / 2);
|
|
105
|
+
const segs = o.bevelSegments || 3;
|
|
106
|
+
const z0 = o.center ? -depth / 2 : 0;
|
|
107
|
+
const bb = regionBounds(shape.regions);
|
|
108
|
+
const S = bb.size;
|
|
109
|
+
const rings = wallProfile(depth, bevel, segs);
|
|
110
|
+
const w = extrudeWasm(shape, bevel, rings, z0, z0 + depth, bb.minX, bb.minY, S);
|
|
111
|
+
if (w) {
|
|
112
|
+
for (const e of w.errors) reportTriangulation(e, report);
|
|
113
|
+
return new Mesh({ positions: w.positions, normals: w.normals, uvs: w.uvs, indices: w.indices, kind: 'triangles' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// caps: the inset contours are triangulated directly (re-using the original
|
|
117
|
+
// triangulation with moved vertices can flip long thin triangles)
|
|
118
|
+
const capRegions = shape.regions.map((r) => ({
|
|
119
|
+
outer: { ...r.outer, points: insetPoints(r.outer, bevel) },
|
|
120
|
+
holes: r.holes.map((h) => ({ ...h, points: insetPoints(h, bevel) })),
|
|
121
|
+
}));
|
|
122
|
+
const caps = triangulateRegions(capRegions, report);
|
|
123
|
+
const walls = shape.walls.map((c) => ({ pts: c.points, f: contourFrames(c.points, c.smooth, true) }));
|
|
124
|
+
// count first, so the mesh is allocated once at its final size
|
|
125
|
+
let vertices = 0, indices = 0;
|
|
126
|
+
for (const t of caps) {
|
|
127
|
+
vertices += t.positions.length;
|
|
128
|
+
indices += 2 * t.indices.length;
|
|
129
|
+
}
|
|
130
|
+
for (const { f } of walls) {
|
|
131
|
+
vertices += f.n * rings.length * 2;
|
|
132
|
+
indices += f.n * (rings.length - 1) * 6;
|
|
133
|
+
}
|
|
134
|
+
const b = new MeshBuilder('triangles', vertices, indices);
|
|
135
|
+
for (const t of caps) {
|
|
136
|
+
const flat = t.positions;
|
|
137
|
+
const idx = t.indices;
|
|
138
|
+
for (const [z, nz] of [[z0 + depth, 1], [z0, -1]]) {
|
|
139
|
+
const base = b.vertexCount;
|
|
140
|
+
for (let i = 0; i < flat.length; i += 2) {
|
|
141
|
+
b.vertex(flat[i], flat[i + 1], z, 0, 0, nz, (flat[i] - bb.minX) / S, (flat[i + 1] - bb.minY) / S);
|
|
142
|
+
}
|
|
143
|
+
for (let i = 0; i < idx.length; i += 3) {
|
|
144
|
+
if (nz > 0) b.tri(base + idx[i], base + idx[i + 1], base + idx[i + 2]);
|
|
145
|
+
else b.tri(base + idx[i], base + idx[i + 2], base + idx[i + 1]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// walls
|
|
151
|
+
for (const { pts, f } of walls) {
|
|
152
|
+
const n = f.n;
|
|
153
|
+
for (let e = 0; e < n; e++) {
|
|
154
|
+
const i0 = e, i1 = (e + 1) % n;
|
|
155
|
+
// normal at each end of the edge: the edge's own at corners, else the vertex's
|
|
156
|
+
const n0x = f.corner[i0] ? f.edgeN[e * 2] : f.vertN[i0 * 2], n0y = f.corner[i0] ? f.edgeN[e * 2 + 1] : f.vertN[i0 * 2 + 1];
|
|
157
|
+
const n1x = f.corner[i1] ? f.edgeN[e * 2] : f.vertN[i1 * 2], n1y = f.corner[i1] ? f.edgeN[e * 2 + 1] : f.vertN[i1 * 2 + 1];
|
|
158
|
+
const u0 = f.arc[e] / S, u1 = f.arc[e + 1] / S;
|
|
159
|
+
let prevA = -1, prevB = -1;
|
|
160
|
+
for (const ring of rings) {
|
|
161
|
+
const ax = pts[i0 * 2] - f.miter[i0 * 2] * ring.inset;
|
|
162
|
+
const ay = pts[i0 * 2 + 1] - f.miter[i0 * 2 + 1] * ring.inset;
|
|
163
|
+
const bx = pts[i1 * 2] - f.miter[i1 * 2] * ring.inset;
|
|
164
|
+
const by = pts[i1 * 2 + 1] - f.miter[i1 * 2 + 1] * ring.inset;
|
|
165
|
+
const z = z0 + ring.z;
|
|
166
|
+
const a = b.vertex(ax, ay, z, n0x * ring.nr, n0y * ring.nr, ring.nz, u0, ring.z / S);
|
|
167
|
+
const c = b.vertex(bx, by, z, n1x * ring.nr, n1y * ring.nr, ring.nz, u1, ring.z / S);
|
|
168
|
+
if (prevA >= 0) b.quad(prevA, prevB, c, a);
|
|
169
|
+
prevA = a;
|
|
170
|
+
prevB = c;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return b.build();
|
|
175
|
+
}
|
package/src/geometry/gear.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
// Involute spur gear outline (module 1); the descriptor scales it by the module.
|
|
2
|
-
//
|
|
3
|
-
// A gear is described by a handful of parameters instead of thousands of
|
|
4
|
-
// vertices: the outline is generated from the involute of the base circle,
|
|
5
|
-
// then handed to the regular extrusion, bevel included if you asked for one.
|
|
6
|
-
// A 2D outline goes in. The generic extrude operator does the rest.
|
|
7
|
-
|
|
8
|
-
import { gearOutlineWasm } from '../wasm/kernels.js';
|
|
9
|
-
|
|
10
|
-
const TAU = Math.PI * 2;
|
|
11
|
-
|
|
12
|
-
function inv(a) {
|
|
13
|
-
return Math.tan(a) - a;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @returns {{outline: {points: Float64Array, smooth: Uint8Array, closed: true}, outerRadius: number, rootRadius: number}}
|
|
18
|
-
*/
|
|
19
|
-
export function gearOutline(teeth, pressureAngleDeg, flankSamples = 6, arcSamples = 3) {
|
|
20
|
-
const fast = gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples);
|
|
21
|
-
if (fast) return fast;
|
|
22
|
-
const N = teeth;
|
|
23
|
-
const alpha = (pressureAngleDeg * Math.PI) / 180;
|
|
24
|
-
const rp = N / 2;
|
|
25
|
-
const rb = rp * Math.cos(alpha);
|
|
26
|
-
let ra = rp + 1;
|
|
27
|
-
const rf = rp - 1.25;
|
|
28
|
-
const psi = Math.PI / (2 * N);
|
|
29
|
-
const invA = inv(alpha);
|
|
30
|
-
const half = (r) => {
|
|
31
|
-
// half tooth angle at radius r (r >= rb)
|
|
32
|
-
const ar = Math.acos(Math.min(1, rb / r));
|
|
33
|
-
return psi + invA - inv(ar);
|
|
34
|
-
};
|
|
35
|
-
// pointed teeth: shrink the tip radius until the tip keeps some width
|
|
36
|
-
if (half(ra) < psi * 0.15) {
|
|
37
|
-
let lo = Math.max(rb, rf), hi = ra;
|
|
38
|
-
for (let k = 0; k < 40; k++) {
|
|
39
|
-
const mid = (lo + hi) / 2;
|
|
40
|
-
if (half(mid) > psi * 0.15) lo = mid;
|
|
41
|
-
else hi = mid;
|
|
42
|
-
}
|
|
43
|
-
ra = lo;
|
|
44
|
-
}
|
|
45
|
-
const rStart = Math.max(rb, rf);
|
|
46
|
-
const pts = [];
|
|
47
|
-
const smooth = [];
|
|
48
|
-
const add = (r, a, s) => {
|
|
49
|
-
pts.push(r * Math.cos(a), r * Math.sin(a));
|
|
50
|
-
smooth.push(s ? 1 : 0);
|
|
51
|
-
};
|
|
52
|
-
const pitchAngle = TAU / N;
|
|
53
|
-
for (let k = 0; k < N; k++) {
|
|
54
|
-
const c = k * pitchAngle;
|
|
55
|
-
const leftRoot = c - half(rStart);
|
|
56
|
-
const rightRoot = c + half(rStart);
|
|
57
|
-
const prevRightRoot = c - pitchAngle + half(rStart);
|
|
58
|
-
// root arc from the previous tooth to this one
|
|
59
|
-
for (let i = 1; i < arcSamples; i++) {
|
|
60
|
-
add(rf, prevRightRoot + ((leftRoot - prevRightRoot) * i) / arcSamples, true);
|
|
61
|
-
}
|
|
62
|
-
// left flank: radial segment (when the root is inside the base circle) then involute
|
|
63
|
-
add(rf, leftRoot, false);
|
|
64
|
-
if (rf < rStart - 1e-9) add(rStart, leftRoot, false);
|
|
65
|
-
for (let i = 1; i <= flankSamples; i++) {
|
|
66
|
-
const r = rStart + ((ra - rStart) * i) / flankSamples;
|
|
67
|
-
add(r, c - half(r), i < flankSamples);
|
|
68
|
-
}
|
|
69
|
-
// tip arc
|
|
70
|
-
const tipL = c - half(ra), tipR = c + half(ra);
|
|
71
|
-
for (let i = 1; i < arcSamples; i++) add(ra, tipL + ((tipR - tipL) * i) / arcSamples, true);
|
|
72
|
-
// right flank going down
|
|
73
|
-
for (let i = flankSamples; i >= 1; i--) {
|
|
74
|
-
const r = rStart + ((ra - rStart) * i) / flankSamples;
|
|
75
|
-
add(r, c + half(r), i < flankSamples);
|
|
76
|
-
}
|
|
77
|
-
if (rf < rStart - 1e-9) add(rStart, rightRoot, false);
|
|
78
|
-
add(rf, rightRoot, false);
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
outline: { points: Float64Array.from(pts), smooth: Uint8Array.from(smooth), closed: true },
|
|
82
|
-
outerRadius: ra,
|
|
83
|
-
rootRadius: rf,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
1
|
+
// Involute spur gear outline (module 1); the descriptor scales it by the module.
|
|
2
|
+
//
|
|
3
|
+
// A gear is described by a handful of parameters instead of thousands of
|
|
4
|
+
// vertices: the outline is generated from the involute of the base circle,
|
|
5
|
+
// then handed to the regular extrusion, bevel included if you asked for one.
|
|
6
|
+
// A 2D outline goes in. The generic extrude operator does the rest.
|
|
7
|
+
|
|
8
|
+
import { gearOutlineWasm } from '../wasm/kernels.js';
|
|
9
|
+
|
|
10
|
+
const TAU = Math.PI * 2;
|
|
11
|
+
|
|
12
|
+
function inv(a) {
|
|
13
|
+
return Math.tan(a) - a;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {{outline: {points: Float64Array, smooth: Uint8Array, closed: true}, outerRadius: number, rootRadius: number}}
|
|
18
|
+
*/
|
|
19
|
+
export function gearOutline(teeth, pressureAngleDeg, flankSamples = 6, arcSamples = 3) {
|
|
20
|
+
const fast = gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples);
|
|
21
|
+
if (fast) return fast;
|
|
22
|
+
const N = teeth;
|
|
23
|
+
const alpha = (pressureAngleDeg * Math.PI) / 180;
|
|
24
|
+
const rp = N / 2;
|
|
25
|
+
const rb = rp * Math.cos(alpha);
|
|
26
|
+
let ra = rp + 1;
|
|
27
|
+
const rf = rp - 1.25;
|
|
28
|
+
const psi = Math.PI / (2 * N);
|
|
29
|
+
const invA = inv(alpha);
|
|
30
|
+
const half = (r) => {
|
|
31
|
+
// half tooth angle at radius r (r >= rb)
|
|
32
|
+
const ar = Math.acos(Math.min(1, rb / r));
|
|
33
|
+
return psi + invA - inv(ar);
|
|
34
|
+
};
|
|
35
|
+
// pointed teeth: shrink the tip radius until the tip keeps some width
|
|
36
|
+
if (half(ra) < psi * 0.15) {
|
|
37
|
+
let lo = Math.max(rb, rf), hi = ra;
|
|
38
|
+
for (let k = 0; k < 40; k++) {
|
|
39
|
+
const mid = (lo + hi) / 2;
|
|
40
|
+
if (half(mid) > psi * 0.15) lo = mid;
|
|
41
|
+
else hi = mid;
|
|
42
|
+
}
|
|
43
|
+
ra = lo;
|
|
44
|
+
}
|
|
45
|
+
const rStart = Math.max(rb, rf);
|
|
46
|
+
const pts = [];
|
|
47
|
+
const smooth = [];
|
|
48
|
+
const add = (r, a, s) => {
|
|
49
|
+
pts.push(r * Math.cos(a), r * Math.sin(a));
|
|
50
|
+
smooth.push(s ? 1 : 0);
|
|
51
|
+
};
|
|
52
|
+
const pitchAngle = TAU / N;
|
|
53
|
+
for (let k = 0; k < N; k++) {
|
|
54
|
+
const c = k * pitchAngle;
|
|
55
|
+
const leftRoot = c - half(rStart);
|
|
56
|
+
const rightRoot = c + half(rStart);
|
|
57
|
+
const prevRightRoot = c - pitchAngle + half(rStart);
|
|
58
|
+
// root arc from the previous tooth to this one
|
|
59
|
+
for (let i = 1; i < arcSamples; i++) {
|
|
60
|
+
add(rf, prevRightRoot + ((leftRoot - prevRightRoot) * i) / arcSamples, true);
|
|
61
|
+
}
|
|
62
|
+
// left flank: radial segment (when the root is inside the base circle) then involute
|
|
63
|
+
add(rf, leftRoot, false);
|
|
64
|
+
if (rf < rStart - 1e-9) add(rStart, leftRoot, false);
|
|
65
|
+
for (let i = 1; i <= flankSamples; i++) {
|
|
66
|
+
const r = rStart + ((ra - rStart) * i) / flankSamples;
|
|
67
|
+
add(r, c - half(r), i < flankSamples);
|
|
68
|
+
}
|
|
69
|
+
// tip arc
|
|
70
|
+
const tipL = c - half(ra), tipR = c + half(ra);
|
|
71
|
+
for (let i = 1; i < arcSamples; i++) add(ra, tipL + ((tipR - tipL) * i) / arcSamples, true);
|
|
72
|
+
// right flank going down
|
|
73
|
+
for (let i = flankSamples; i >= 1; i--) {
|
|
74
|
+
const r = rStart + ((ra - rStart) * i) / flankSamples;
|
|
75
|
+
add(r, c + half(r), i < flankSamples);
|
|
76
|
+
}
|
|
77
|
+
if (rf < rStart - 1e-9) add(rStart, rightRoot, false);
|
|
78
|
+
add(rf, rightRoot, false);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
outline: { points: Float64Array.from(pts), smooth: Uint8Array.from(smooth), closed: true },
|
|
82
|
+
outerRadius: ra,
|
|
83
|
+
rootRadius: rf,
|
|
84
|
+
};
|
|
85
|
+
}
|