okgeometry-api 1.26.1 → 1.27.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.
Files changed (99) hide show
  1. package/dist/Arc.js +1 -1
  2. package/dist/Arc.js.map +1 -1
  3. package/dist/Brep.js +1 -1
  4. package/dist/Brep.js.map +1 -1
  5. package/dist/Circle.js +1 -1
  6. package/dist/Circle.js.map +1 -1
  7. package/dist/Line.js +1 -1
  8. package/dist/Line.js.map +1 -1
  9. package/dist/Mesh.d.ts +41 -0
  10. package/dist/Mesh.d.ts.map +1 -1
  11. package/dist/Mesh.js +235 -21
  12. package/dist/Mesh.js.map +1 -1
  13. package/dist/MeshSurface.js +1 -1
  14. package/dist/MeshSurface.js.map +1 -1
  15. package/dist/NurbsCurve.js +1 -1
  16. package/dist/NurbsCurve.js.map +1 -1
  17. package/dist/NurbsSurface.js +1 -1
  18. package/dist/NurbsSurface.js.map +1 -1
  19. package/dist/PolyCurve.js +1 -1
  20. package/dist/PolyCurve.js.map +1 -1
  21. package/dist/Polyline.js +1 -1
  22. package/dist/Polyline.js.map +1 -1
  23. package/dist/Ray.js +1 -1
  24. package/dist/Ray.js.map +1 -1
  25. package/dist/SculptSession.js +1 -1
  26. package/dist/SculptSession.js.map +1 -1
  27. package/dist/engine-threaded.d.ts +2 -0
  28. package/dist/engine-threaded.d.ts.map +1 -0
  29. package/dist/engine-threaded.js +33 -0
  30. package/dist/engine-threaded.js.map +1 -0
  31. package/dist/engine.d.ts +21 -0
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +42 -0
  34. package/dist/engine.js.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/mesh-boolean.pool.d.ts +23 -1
  40. package/dist/mesh-boolean.pool.d.ts.map +1 -1
  41. package/dist/mesh-boolean.pool.js +97 -12
  42. package/dist/mesh-boolean.pool.js.map +1 -1
  43. package/dist/mesh-boolean.protocol.d.ts +21 -1
  44. package/dist/mesh-boolean.protocol.d.ts.map +1 -1
  45. package/dist/mesh-boolean.protocol.js.map +1 -1
  46. package/dist/mesh-boolean.worker.d.ts.map +1 -1
  47. package/dist/mesh-boolean.worker.js +94 -1
  48. package/dist/mesh-boolean.worker.js.map +1 -1
  49. package/dist/wasm-active.d.ts +20 -0
  50. package/dist/wasm-active.d.ts.map +1 -0
  51. package/dist/wasm-active.js +26 -0
  52. package/dist/wasm-active.js.map +1 -0
  53. package/dist/wasm-base64-threaded.d.ts +2 -0
  54. package/dist/wasm-base64-threaded.d.ts.map +1 -0
  55. package/dist/wasm-base64-threaded.js +3 -0
  56. package/dist/wasm-base64-threaded.js.map +1 -0
  57. package/dist/wasm-base64.d.ts +1 -1
  58. package/dist/wasm-base64.d.ts.map +1 -1
  59. package/dist/wasm-base64.js +1 -1
  60. package/dist/wasm-base64.js.map +1 -1
  61. package/dist/wasm-bindings-threaded.d.ts +2664 -0
  62. package/dist/wasm-bindings-threaded.d.ts.map +1 -0
  63. package/dist/wasm-bindings-threaded.js +5008 -0
  64. package/dist/wasm-bindings-threaded.js.map +1 -0
  65. package/dist/wasm-bindings.d.ts +130 -0
  66. package/dist/wasm-bindings.d.ts.map +1 -1
  67. package/dist/wasm-bindings.js +208 -0
  68. package/dist/wasm-bindings.js.map +1 -1
  69. package/dist/workerHelpers-threaded.d.ts +2 -0
  70. package/dist/workerHelpers-threaded.d.ts.map +1 -0
  71. package/dist/workerHelpers-threaded.js +100 -0
  72. package/dist/workerHelpers-threaded.js.map +1 -0
  73. package/package.json +8 -3
  74. package/src/Arc.ts +117 -117
  75. package/src/Brep.ts +1 -1
  76. package/src/Circle.ts +166 -166
  77. package/src/Line.ts +171 -171
  78. package/src/Mesh.ts +300 -45
  79. package/src/MeshSurface.ts +1 -1
  80. package/src/NurbsCurve.ts +655 -655
  81. package/src/NurbsSurface.ts +721 -721
  82. package/src/PolyCurve.ts +203 -203
  83. package/src/Polyline.ts +316 -316
  84. package/src/Ray.ts +90 -90
  85. package/src/SculptSession.ts +377 -377
  86. package/src/engine-threaded.ts +39 -0
  87. package/src/engine.ts +46 -0
  88. package/src/index.ts +1 -1
  89. package/src/mesh-boolean.pool.ts +208 -86
  90. package/src/mesh-boolean.protocol.ts +44 -1
  91. package/src/mesh-boolean.worker.ts +170 -56
  92. package/src/wasm-active.ts +31 -0
  93. package/src/wasm-base64-threaded.ts +2 -0
  94. package/src/wasm-base64.ts +1 -1
  95. package/src/wasm-bindings-threaded.d.ts +1989 -0
  96. package/src/wasm-bindings-threaded.js +5235 -0
  97. package/src/wasm-bindings.d.ts +104 -0
  98. package/src/wasm-bindings.js +225 -0
  99. package/src/workerHelpers-threaded.js +107 -0
@@ -1,377 +1,377 @@
1
- import { ensureInit } from "./engine.js";
2
- import { Mesh } from "./Mesh.js";
3
- import * as wasm from "./wasm-bindings.js";
4
-
5
- /**
6
- * Sculpting brushes. Codes are a stable WASM ABI — never renumber.
7
- */
8
- export enum SculptBrush {
9
- Draw = 0,
10
- DrawSharp = 1,
11
- Clay = 2,
12
- Smooth = 3,
13
- Inflate = 4,
14
- Grab = 5,
15
- SnakeHook = 6,
16
- Flatten = 7,
17
- Scrape = 8,
18
- Fill = 9,
19
- Pinch = 10,
20
- Crease = 11,
21
- Layer = 12,
22
- Nudge = 13,
23
- /** Paints the sculpt mask (protection) instead of moving geometry. */
24
- Mask = 14,
25
- }
26
-
27
- /** Brush falloff profiles over normalized distance. Stable WASM ABI codes. */
28
- export enum SculptFalloff {
29
- Smooth = 0,
30
- Sphere = 1,
31
- Linear = 2,
32
- Sharp = 3,
33
- Root = 4,
34
- Constant = 5,
35
- }
36
-
37
- export interface SculptBrushSettings {
38
- brush: SculptBrush;
39
- /** Brush radius in mesh (local) units. */
40
- radius: number;
41
- /** Strength, typically 0..1. */
42
- strength: number;
43
- falloff?: SculptFalloff;
44
- /** Invert the brush direction (carve instead of add, etc.). */
45
- invert?: boolean;
46
- /** Only affect vertices facing the viewer (default true). */
47
- frontFacesOnly?: boolean;
48
- /** Clay plane offset as a fraction of the radius (default 0.05). */
49
- planeOffset?: number;
50
- /** Mirror across local planes: combine SYMMETRY_X | SYMMETRY_Y | SYMMETRY_Z. */
51
- symmetry?: number;
52
- }
53
-
54
- export const SYMMETRY_X = 1;
55
- export const SYMMETRY_Y = 2;
56
- export const SYMMETRY_Z = 4;
57
-
58
- export interface SculptRayHit {
59
- point: [number, number, number];
60
- normal: [number, number, number];
61
- distance: number;
62
- triangleIndex: number;
63
- }
64
-
65
- export interface SculptDabInput {
66
- /** Brush center on the surface (usually the raycast hit point). */
67
- center: [number, number, number];
68
- /** Normalized view ray direction, used for front-face masking. */
69
- viewDir: [number, number, number];
70
- /**
71
- * World-space drag vector. Grab: total drag since the stroke start.
72
- * Snake hook / nudge: drag since the previous dab. Others: ignored.
73
- */
74
- drag?: [number, number, number];
75
- /** Tablet pressure 0..1 (default 1). */
76
- pressure?: number;
77
- }
78
-
79
- /**
80
- * Render patch after a dab/undo/redo/mask op: for each changed vertex,
81
- * 8 floats `[index, x, y, z, nx, ny, nz, mask]`.
82
- */
83
- export type SculptPatch = Float32Array;
84
-
85
- export interface SculptRenderData {
86
- vertexCount: number;
87
- triangleCount: number;
88
- positions: Float32Array;
89
- normals: Float32Array;
90
- /** Per-vertex sculpt mask in [0, 1] (1 = protected). */
91
- mask: Float32Array;
92
- indices: Uint32Array;
93
- }
94
-
95
- /**
96
- * Interactive sculpting session over a triangle mesh.
97
- *
98
- * The mesh, its spatial index, adjacency and normals live WASM-side for the
99
- * lifetime of the session, so brush dabs are incremental: each dab returns
100
- * only the changed vertices as a compact patch. Call `dispose()` when done —
101
- * sessions hold WASM memory.
102
- *
103
- * ```ts
104
- * const session = SculptSession.fromMesh(mesh);
105
- * session.beginStroke({ brush: SculptBrush.Draw, radius: 0.5, strength: 0.8 });
106
- * const hit = session.raycast(origin, dir);
107
- * if (hit) session.dab({ center: hit.point, viewDir: dir });
108
- * session.endStroke();
109
- * const result = session.toMesh(); // commit
110
- * session.dispose();
111
- * ```
112
- */
113
- export class SculptSession {
114
- private id: number;
115
- private disposed = false;
116
-
117
- private constructor(id: number) {
118
- this.id = id;
119
- }
120
-
121
- /** Create a session from a Mesh. Throws on invalid/empty geometry. */
122
- static fromMesh(mesh: Mesh): SculptSession {
123
- ensureInit();
124
- const buffer = mesh.rawBuffer;
125
- const id = wasm.sculpt_session_create(mesh.vertexCount, buffer);
126
- if (!id) {
127
- throw new Error("SculptSession: invalid mesh (empty or malformed buffer)");
128
- }
129
- return new SculptSession(id);
130
- }
131
-
132
- /** Create a session from a raw mesh buffer `[vertexCount, xyz..., idx...]`. */
133
- static fromBuffer(vertexCount: number, buffer: Float64Array): SculptSession {
134
- ensureInit();
135
- const id = wasm.sculpt_session_create(vertexCount, buffer);
136
- if (!id) {
137
- throw new Error("SculptSession: invalid mesh buffer");
138
- }
139
- return new SculptSession(id);
140
- }
141
-
142
- /** Free the WASM-side session. Safe to call more than once. */
143
- dispose(): void {
144
- if (!this.disposed) {
145
- wasm.sculpt_session_release(this.id);
146
- this.disposed = true;
147
- }
148
- }
149
-
150
- get isDisposed(): boolean {
151
- return this.disposed;
152
- }
153
-
154
- private ensureAlive(): void {
155
- if (this.disposed) {
156
- throw new Error("SculptSession: session already disposed");
157
- }
158
- }
159
-
160
- get vertexCount(): number {
161
- this.ensureAlive();
162
- const info = wasm.sculpt_session_info(this.id);
163
- return info.length ? info[0] : 0;
164
- }
165
-
166
- get triangleCount(): number {
167
- this.ensureAlive();
168
- const info = wasm.sculpt_session_info(this.id);
169
- return info.length ? info[1] : 0;
170
- }
171
-
172
- /** Average edge length — a good baseline for default brush radius. */
173
- get averageEdgeLength(): number {
174
- this.ensureAlive();
175
- const info = wasm.sculpt_session_info(this.id);
176
- return info.length ? info[2] : 0;
177
- }
178
-
179
- get undoDepth(): number {
180
- this.ensureAlive();
181
- const info = wasm.sculpt_session_info(this.id);
182
- return info.length ? info[3] : 0;
183
- }
184
-
185
- get redoDepth(): number {
186
- this.ensureAlive();
187
- const info = wasm.sculpt_session_info(this.id);
188
- return info.length ? info[4] : 0;
189
- }
190
-
191
- /**
192
- * Full render snapshot (f32 positions/normals + u32 indices) for building
193
- * an indexed, smooth-shaded display mesh. Fetch once at session start and
194
- * after `subdivide()`; use dab patches for incremental updates.
195
- */
196
- getRenderData(): SculptRenderData {
197
- this.ensureAlive();
198
- const raw = wasm.sculpt_session_render_data(this.id);
199
- if (raw.length < 2) {
200
- throw new Error("SculptSession: no render data (session lost)");
201
- }
202
- const vertexCount = raw[0];
203
- const triangleCount = raw[1];
204
- const pStart = 2;
205
- const nStart = pStart + vertexCount * 3;
206
- const mStart = nStart + vertexCount * 3;
207
- const iStart = mStart + vertexCount;
208
- const positions = raw.slice(pStart, nStart);
209
- const normals = raw.slice(nStart, mStart);
210
- const mask = raw.slice(mStart, iStart);
211
- const indices = new Uint32Array(triangleCount * 3);
212
- for (let i = 0; i < indices.length; i++) {
213
- indices[i] = raw[iStart + i];
214
- }
215
- return { vertexCount, triangleCount, positions, normals, mask, indices };
216
- }
217
-
218
- /** Raycast the current (deforming) surface. */
219
- raycast(
220
- origin: [number, number, number],
221
- direction: [number, number, number],
222
- ): SculptRayHit | null {
223
- this.ensureAlive();
224
- const hit = wasm.sculpt_session_raycast(
225
- this.id,
226
- origin[0],
227
- origin[1],
228
- origin[2],
229
- direction[0],
230
- direction[1],
231
- direction[2],
232
- );
233
- if (hit.length < 8) return null;
234
- return {
235
- point: [hit[0], hit[1], hit[2]],
236
- normal: [hit[3], hit[4], hit[5]],
237
- distance: hit[6],
238
- triangleIndex: hit[7],
239
- };
240
- }
241
-
242
- /** Begin a brush stroke. Ends any stroke still in flight. */
243
- beginStroke(settings: SculptBrushSettings): void {
244
- this.ensureAlive();
245
- const flags =
246
- (settings.invert ? 1 : 0) | ((settings.frontFacesOnly ?? true) ? 2 : 0);
247
- const ok = wasm.sculpt_session_begin_stroke(
248
- this.id,
249
- settings.brush,
250
- settings.radius,
251
- settings.strength,
252
- settings.falloff ?? SculptFalloff.Smooth,
253
- flags,
254
- settings.planeOffset ?? 0.05,
255
- settings.symmetry ?? 0,
256
- );
257
- if (!ok) {
258
- throw new Error("SculptSession: beginStroke failed (bad settings or session lost)");
259
- }
260
- }
261
-
262
- /**
263
- * Apply one brush dab. Returns the changed-vertex render patch
264
- * (`[index, x,y,z, nx,ny,nz]` per vertex); empty when nothing changed.
265
- */
266
- dab(input: SculptDabInput): SculptPatch {
267
- this.ensureAlive();
268
- const drag = input.drag ?? [0, 0, 0];
269
- return wasm.sculpt_session_dab(
270
- this.id,
271
- input.center[0],
272
- input.center[1],
273
- input.center[2],
274
- input.viewDir[0],
275
- input.viewDir[1],
276
- input.viewDir[2],
277
- drag[0],
278
- drag[1],
279
- drag[2],
280
- input.pressure ?? 1,
281
- );
282
- }
283
-
284
- /** Finish the active stroke; the stroke becomes one undo step. */
285
- endStroke(): void {
286
- this.ensureAlive();
287
- wasm.sculpt_session_end_stroke(this.id);
288
- }
289
-
290
- /** Undo the last stroke. Returns the restoring patch (empty = nothing to undo). */
291
- undo(): SculptPatch {
292
- this.ensureAlive();
293
- return wasm.sculpt_session_undo(this.id);
294
- }
295
-
296
- /** Redo the last undone stroke. */
297
- redo(): SculptPatch {
298
- this.ensureAlive();
299
- return wasm.sculpt_session_redo(this.id);
300
- }
301
-
302
- /**
303
- * Uniform 1:4 shape-preserving (linear midpoint) subdivision for more
304
- * sculptable detail. Clears session undo history. Returns false when
305
- * refused by the safety cap (~8M triangles). Refetch `getRenderData()`
306
- * afterwards.
307
- */
308
- subdivide(): boolean {
309
- this.ensureAlive();
310
- return wasm.sculpt_session_subdivide(this.id) === 1;
311
- }
312
-
313
- /**
314
- * Smooth face group around a seed triangle: edge-connected triangles whose
315
- * dihedral angles stay below `maxAngleDeg` (whole planar face, whole
316
- * cylinder wall — the same grouping used for face hovers on meshes).
317
- * Empty when the seed is out of range.
318
- */
319
- faceGroup(seedTriangle: number, maxAngleDeg = 30): Uint32Array {
320
- this.ensureAlive();
321
- return wasm.sculpt_session_face_group(this.id, seedTriangle, maxAngleDeg);
322
- }
323
-
324
- /**
325
- * Locally refine the given triangles 1:4 for finer sculpting control.
326
- * Neighboring triangles get crack-free conforming splits (red-green
327
- * refinement) so the mesh stays seamless/watertight. Shape preserving
328
- * (linear midpoints); the mask interpolates. Clears session undo history.
329
- * Returns false when refused (empty/out-of-range input or the ~8M
330
- * triangle safety cap). Refetch `getRenderData()` afterwards.
331
- */
332
- subdivideFaces(triangles: Uint32Array | number[]): boolean {
333
- this.ensureAlive();
334
- const arr = triangles instanceof Uint32Array ? triangles : Uint32Array.from(triangles);
335
- return wasm.sculpt_session_subdivide_faces(this.id, arr) === 1;
336
- }
337
-
338
- /**
339
- * Locally REDUCE point density in the given triangle group by collapsing
340
- * interior edges (shortest first) until the group has at most
341
- * `targetPoints` unique vertices or no safe collapse remains. Only
342
- * vertices strictly interior to the group are removed — the group
343
- * boundary and everything outside it are untouched, so a closed mesh
344
- * stays watertight. Clears session undo history. Returns false when no
345
- * collapse was possible. Refetch `getRenderData()` afterwards.
346
- */
347
- decimateFaces(triangles: Uint32Array | number[], targetPoints: number): boolean {
348
- this.ensureAlive();
349
- const arr = triangles instanceof Uint32Array ? triangles : Uint32Array.from(triangles);
350
- return wasm.sculpt_session_decimate_faces(this.id, arr, Math.max(0, Math.floor(targetPoints))) === 1;
351
- }
352
-
353
- /**
354
- * Fill the whole sculpt mask (0 clears, 1 protects everything).
355
- * Undoable; returns the render patch of changed vertices.
356
- */
357
- maskFill(value: number): SculptPatch {
358
- this.ensureAlive();
359
- return wasm.sculpt_session_mask_fill(this.id, value);
360
- }
361
-
362
- /** Invert the sculpt mask. Undoable; returns the render patch. */
363
- maskInvert(): SculptPatch {
364
- this.ensureAlive();
365
- return wasm.sculpt_session_mask_invert(this.id);
366
- }
367
-
368
- /** Current mesh as a new Mesh instance (for committing the sculpt). */
369
- toMesh(): Mesh {
370
- this.ensureAlive();
371
- const buffer = wasm.sculpt_session_mesh_buffer(this.id);
372
- if (!buffer.length) {
373
- throw new Error("SculptSession: mesh buffer unavailable (session lost)");
374
- }
375
- return Mesh.fromBuffer(buffer);
376
- }
377
- }
1
+ import { ensureInit } from "./engine.js";
2
+ import { Mesh } from "./Mesh.js";
3
+ import { wasm } from "./wasm-active.js";
4
+
5
+ /**
6
+ * Sculpting brushes. Codes are a stable WASM ABI — never renumber.
7
+ */
8
+ export enum SculptBrush {
9
+ Draw = 0,
10
+ DrawSharp = 1,
11
+ Clay = 2,
12
+ Smooth = 3,
13
+ Inflate = 4,
14
+ Grab = 5,
15
+ SnakeHook = 6,
16
+ Flatten = 7,
17
+ Scrape = 8,
18
+ Fill = 9,
19
+ Pinch = 10,
20
+ Crease = 11,
21
+ Layer = 12,
22
+ Nudge = 13,
23
+ /** Paints the sculpt mask (protection) instead of moving geometry. */
24
+ Mask = 14,
25
+ }
26
+
27
+ /** Brush falloff profiles over normalized distance. Stable WASM ABI codes. */
28
+ export enum SculptFalloff {
29
+ Smooth = 0,
30
+ Sphere = 1,
31
+ Linear = 2,
32
+ Sharp = 3,
33
+ Root = 4,
34
+ Constant = 5,
35
+ }
36
+
37
+ export interface SculptBrushSettings {
38
+ brush: SculptBrush;
39
+ /** Brush radius in mesh (local) units. */
40
+ radius: number;
41
+ /** Strength, typically 0..1. */
42
+ strength: number;
43
+ falloff?: SculptFalloff;
44
+ /** Invert the brush direction (carve instead of add, etc.). */
45
+ invert?: boolean;
46
+ /** Only affect vertices facing the viewer (default true). */
47
+ frontFacesOnly?: boolean;
48
+ /** Clay plane offset as a fraction of the radius (default 0.05). */
49
+ planeOffset?: number;
50
+ /** Mirror across local planes: combine SYMMETRY_X | SYMMETRY_Y | SYMMETRY_Z. */
51
+ symmetry?: number;
52
+ }
53
+
54
+ export const SYMMETRY_X = 1;
55
+ export const SYMMETRY_Y = 2;
56
+ export const SYMMETRY_Z = 4;
57
+
58
+ export interface SculptRayHit {
59
+ point: [number, number, number];
60
+ normal: [number, number, number];
61
+ distance: number;
62
+ triangleIndex: number;
63
+ }
64
+
65
+ export interface SculptDabInput {
66
+ /** Brush center on the surface (usually the raycast hit point). */
67
+ center: [number, number, number];
68
+ /** Normalized view ray direction, used for front-face masking. */
69
+ viewDir: [number, number, number];
70
+ /**
71
+ * World-space drag vector. Grab: total drag since the stroke start.
72
+ * Snake hook / nudge: drag since the previous dab. Others: ignored.
73
+ */
74
+ drag?: [number, number, number];
75
+ /** Tablet pressure 0..1 (default 1). */
76
+ pressure?: number;
77
+ }
78
+
79
+ /**
80
+ * Render patch after a dab/undo/redo/mask op: for each changed vertex,
81
+ * 8 floats `[index, x, y, z, nx, ny, nz, mask]`.
82
+ */
83
+ export type SculptPatch = Float32Array;
84
+
85
+ export interface SculptRenderData {
86
+ vertexCount: number;
87
+ triangleCount: number;
88
+ positions: Float32Array;
89
+ normals: Float32Array;
90
+ /** Per-vertex sculpt mask in [0, 1] (1 = protected). */
91
+ mask: Float32Array;
92
+ indices: Uint32Array;
93
+ }
94
+
95
+ /**
96
+ * Interactive sculpting session over a triangle mesh.
97
+ *
98
+ * The mesh, its spatial index, adjacency and normals live WASM-side for the
99
+ * lifetime of the session, so brush dabs are incremental: each dab returns
100
+ * only the changed vertices as a compact patch. Call `dispose()` when done —
101
+ * sessions hold WASM memory.
102
+ *
103
+ * ```ts
104
+ * const session = SculptSession.fromMesh(mesh);
105
+ * session.beginStroke({ brush: SculptBrush.Draw, radius: 0.5, strength: 0.8 });
106
+ * const hit = session.raycast(origin, dir);
107
+ * if (hit) session.dab({ center: hit.point, viewDir: dir });
108
+ * session.endStroke();
109
+ * const result = session.toMesh(); // commit
110
+ * session.dispose();
111
+ * ```
112
+ */
113
+ export class SculptSession {
114
+ private id: number;
115
+ private disposed = false;
116
+
117
+ private constructor(id: number) {
118
+ this.id = id;
119
+ }
120
+
121
+ /** Create a session from a Mesh. Throws on invalid/empty geometry. */
122
+ static fromMesh(mesh: Mesh): SculptSession {
123
+ ensureInit();
124
+ const buffer = mesh.rawBuffer;
125
+ const id = wasm.sculpt_session_create(mesh.vertexCount, buffer);
126
+ if (!id) {
127
+ throw new Error("SculptSession: invalid mesh (empty or malformed buffer)");
128
+ }
129
+ return new SculptSession(id);
130
+ }
131
+
132
+ /** Create a session from a raw mesh buffer `[vertexCount, xyz..., idx...]`. */
133
+ static fromBuffer(vertexCount: number, buffer: Float64Array): SculptSession {
134
+ ensureInit();
135
+ const id = wasm.sculpt_session_create(vertexCount, buffer);
136
+ if (!id) {
137
+ throw new Error("SculptSession: invalid mesh buffer");
138
+ }
139
+ return new SculptSession(id);
140
+ }
141
+
142
+ /** Free the WASM-side session. Safe to call more than once. */
143
+ dispose(): void {
144
+ if (!this.disposed) {
145
+ wasm.sculpt_session_release(this.id);
146
+ this.disposed = true;
147
+ }
148
+ }
149
+
150
+ get isDisposed(): boolean {
151
+ return this.disposed;
152
+ }
153
+
154
+ private ensureAlive(): void {
155
+ if (this.disposed) {
156
+ throw new Error("SculptSession: session already disposed");
157
+ }
158
+ }
159
+
160
+ get vertexCount(): number {
161
+ this.ensureAlive();
162
+ const info = wasm.sculpt_session_info(this.id);
163
+ return info.length ? info[0] : 0;
164
+ }
165
+
166
+ get triangleCount(): number {
167
+ this.ensureAlive();
168
+ const info = wasm.sculpt_session_info(this.id);
169
+ return info.length ? info[1] : 0;
170
+ }
171
+
172
+ /** Average edge length — a good baseline for default brush radius. */
173
+ get averageEdgeLength(): number {
174
+ this.ensureAlive();
175
+ const info = wasm.sculpt_session_info(this.id);
176
+ return info.length ? info[2] : 0;
177
+ }
178
+
179
+ get undoDepth(): number {
180
+ this.ensureAlive();
181
+ const info = wasm.sculpt_session_info(this.id);
182
+ return info.length ? info[3] : 0;
183
+ }
184
+
185
+ get redoDepth(): number {
186
+ this.ensureAlive();
187
+ const info = wasm.sculpt_session_info(this.id);
188
+ return info.length ? info[4] : 0;
189
+ }
190
+
191
+ /**
192
+ * Full render snapshot (f32 positions/normals + u32 indices) for building
193
+ * an indexed, smooth-shaded display mesh. Fetch once at session start and
194
+ * after `subdivide()`; use dab patches for incremental updates.
195
+ */
196
+ getRenderData(): SculptRenderData {
197
+ this.ensureAlive();
198
+ const raw = wasm.sculpt_session_render_data(this.id);
199
+ if (raw.length < 2) {
200
+ throw new Error("SculptSession: no render data (session lost)");
201
+ }
202
+ const vertexCount = raw[0];
203
+ const triangleCount = raw[1];
204
+ const pStart = 2;
205
+ const nStart = pStart + vertexCount * 3;
206
+ const mStart = nStart + vertexCount * 3;
207
+ const iStart = mStart + vertexCount;
208
+ const positions = raw.slice(pStart, nStart);
209
+ const normals = raw.slice(nStart, mStart);
210
+ const mask = raw.slice(mStart, iStart);
211
+ const indices = new Uint32Array(triangleCount * 3);
212
+ for (let i = 0; i < indices.length; i++) {
213
+ indices[i] = raw[iStart + i];
214
+ }
215
+ return { vertexCount, triangleCount, positions, normals, mask, indices };
216
+ }
217
+
218
+ /** Raycast the current (deforming) surface. */
219
+ raycast(
220
+ origin: [number, number, number],
221
+ direction: [number, number, number],
222
+ ): SculptRayHit | null {
223
+ this.ensureAlive();
224
+ const hit = wasm.sculpt_session_raycast(
225
+ this.id,
226
+ origin[0],
227
+ origin[1],
228
+ origin[2],
229
+ direction[0],
230
+ direction[1],
231
+ direction[2],
232
+ );
233
+ if (hit.length < 8) return null;
234
+ return {
235
+ point: [hit[0], hit[1], hit[2]],
236
+ normal: [hit[3], hit[4], hit[5]],
237
+ distance: hit[6],
238
+ triangleIndex: hit[7],
239
+ };
240
+ }
241
+
242
+ /** Begin a brush stroke. Ends any stroke still in flight. */
243
+ beginStroke(settings: SculptBrushSettings): void {
244
+ this.ensureAlive();
245
+ const flags =
246
+ (settings.invert ? 1 : 0) | ((settings.frontFacesOnly ?? true) ? 2 : 0);
247
+ const ok = wasm.sculpt_session_begin_stroke(
248
+ this.id,
249
+ settings.brush,
250
+ settings.radius,
251
+ settings.strength,
252
+ settings.falloff ?? SculptFalloff.Smooth,
253
+ flags,
254
+ settings.planeOffset ?? 0.05,
255
+ settings.symmetry ?? 0,
256
+ );
257
+ if (!ok) {
258
+ throw new Error("SculptSession: beginStroke failed (bad settings or session lost)");
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Apply one brush dab. Returns the changed-vertex render patch
264
+ * (`[index, x,y,z, nx,ny,nz]` per vertex); empty when nothing changed.
265
+ */
266
+ dab(input: SculptDabInput): SculptPatch {
267
+ this.ensureAlive();
268
+ const drag = input.drag ?? [0, 0, 0];
269
+ return wasm.sculpt_session_dab(
270
+ this.id,
271
+ input.center[0],
272
+ input.center[1],
273
+ input.center[2],
274
+ input.viewDir[0],
275
+ input.viewDir[1],
276
+ input.viewDir[2],
277
+ drag[0],
278
+ drag[1],
279
+ drag[2],
280
+ input.pressure ?? 1,
281
+ );
282
+ }
283
+
284
+ /** Finish the active stroke; the stroke becomes one undo step. */
285
+ endStroke(): void {
286
+ this.ensureAlive();
287
+ wasm.sculpt_session_end_stroke(this.id);
288
+ }
289
+
290
+ /** Undo the last stroke. Returns the restoring patch (empty = nothing to undo). */
291
+ undo(): SculptPatch {
292
+ this.ensureAlive();
293
+ return wasm.sculpt_session_undo(this.id);
294
+ }
295
+
296
+ /** Redo the last undone stroke. */
297
+ redo(): SculptPatch {
298
+ this.ensureAlive();
299
+ return wasm.sculpt_session_redo(this.id);
300
+ }
301
+
302
+ /**
303
+ * Uniform 1:4 shape-preserving (linear midpoint) subdivision for more
304
+ * sculptable detail. Clears session undo history. Returns false when
305
+ * refused by the safety cap (~8M triangles). Refetch `getRenderData()`
306
+ * afterwards.
307
+ */
308
+ subdivide(): boolean {
309
+ this.ensureAlive();
310
+ return wasm.sculpt_session_subdivide(this.id) === 1;
311
+ }
312
+
313
+ /**
314
+ * Smooth face group around a seed triangle: edge-connected triangles whose
315
+ * dihedral angles stay below `maxAngleDeg` (whole planar face, whole
316
+ * cylinder wall — the same grouping used for face hovers on meshes).
317
+ * Empty when the seed is out of range.
318
+ */
319
+ faceGroup(seedTriangle: number, maxAngleDeg = 30): Uint32Array {
320
+ this.ensureAlive();
321
+ return wasm.sculpt_session_face_group(this.id, seedTriangle, maxAngleDeg);
322
+ }
323
+
324
+ /**
325
+ * Locally refine the given triangles 1:4 for finer sculpting control.
326
+ * Neighboring triangles get crack-free conforming splits (red-green
327
+ * refinement) so the mesh stays seamless/watertight. Shape preserving
328
+ * (linear midpoints); the mask interpolates. Clears session undo history.
329
+ * Returns false when refused (empty/out-of-range input or the ~8M
330
+ * triangle safety cap). Refetch `getRenderData()` afterwards.
331
+ */
332
+ subdivideFaces(triangles: Uint32Array | number[]): boolean {
333
+ this.ensureAlive();
334
+ const arr = triangles instanceof Uint32Array ? triangles : Uint32Array.from(triangles);
335
+ return wasm.sculpt_session_subdivide_faces(this.id, arr) === 1;
336
+ }
337
+
338
+ /**
339
+ * Locally REDUCE point density in the given triangle group by collapsing
340
+ * interior edges (shortest first) until the group has at most
341
+ * `targetPoints` unique vertices or no safe collapse remains. Only
342
+ * vertices strictly interior to the group are removed — the group
343
+ * boundary and everything outside it are untouched, so a closed mesh
344
+ * stays watertight. Clears session undo history. Returns false when no
345
+ * collapse was possible. Refetch `getRenderData()` afterwards.
346
+ */
347
+ decimateFaces(triangles: Uint32Array | number[], targetPoints: number): boolean {
348
+ this.ensureAlive();
349
+ const arr = triangles instanceof Uint32Array ? triangles : Uint32Array.from(triangles);
350
+ return wasm.sculpt_session_decimate_faces(this.id, arr, Math.max(0, Math.floor(targetPoints))) === 1;
351
+ }
352
+
353
+ /**
354
+ * Fill the whole sculpt mask (0 clears, 1 protects everything).
355
+ * Undoable; returns the render patch of changed vertices.
356
+ */
357
+ maskFill(value: number): SculptPatch {
358
+ this.ensureAlive();
359
+ return wasm.sculpt_session_mask_fill(this.id, value);
360
+ }
361
+
362
+ /** Invert the sculpt mask. Undoable; returns the render patch. */
363
+ maskInvert(): SculptPatch {
364
+ this.ensureAlive();
365
+ return wasm.sculpt_session_mask_invert(this.id);
366
+ }
367
+
368
+ /** Current mesh as a new Mesh instance (for committing the sculpt). */
369
+ toMesh(): Mesh {
370
+ this.ensureAlive();
371
+ const buffer = wasm.sculpt_session_mesh_buffer(this.id);
372
+ if (!buffer.length) {
373
+ throw new Error("SculptSession: mesh buffer unavailable (session lost)");
374
+ }
375
+ return Mesh.fromBuffer(buffer);
376
+ }
377
+ }