@es3d/es3d 1.0.0 → 1.0.1

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.
Files changed (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +92 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. package/src/wasm/kernels.js +385 -385
@@ -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
+ }
@@ -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
+ }