okgeometry-api 1.19.0 → 1.20.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/dist/Mesh.d.ts +20 -0
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +29 -0
- package/dist/Mesh.js.map +1 -1
- package/dist/SculptSession.d.ts +10 -0
- package/dist/SculptSession.d.ts.map +1 -1
- package/dist/SculptSession.js +14 -0
- package/dist/SculptSession.js.map +1 -1
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.d.ts.map +1 -1
- package/dist/wasm-base64.js +1 -1
- package/dist/wasm-base64.js.map +1 -1
- package/dist/wasm-bindings.d.ts +30 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +44 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +59 -59
- package/src/Mesh.ts +3852 -3810
- package/src/SculptSession.ts +377 -362
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +21 -0
- package/src/wasm-bindings.js +46 -0
package/src/SculptSession.ts
CHANGED
|
@@ -1,362 +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
|
-
*
|
|
340
|
-
*
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return wasm.
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
}
|