@ikijs/editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2233 @@
1
+ // src/document.ts
2
+ import { parseIkiModel } from "@ikijs/format";
3
+
4
+ // src/mesh-uv.ts
5
+ function remapMeshUvsToRect(baseUvs, rect) {
6
+ if (baseUvs.length % 2 !== 0) {
7
+ throw new Error(
8
+ `remapMeshUvsToRect: baseUvs must have an even length (u,v pairs), got ${baseUvs.length}`
9
+ );
10
+ }
11
+ const out = new Array(baseUvs.length);
12
+ for (let i = 0; i < baseUvs.length; i += 2) {
13
+ out[i] = rect.x + baseUvs[i] * rect.width;
14
+ out[i + 1] = rect.y + baseUvs[i + 1] * rect.height;
15
+ }
16
+ return out;
17
+ }
18
+
19
+ // src/document.ts
20
+ var EditorDocument = class {
21
+ model;
22
+ undoStack = [];
23
+ redoStack = [];
24
+ /** Editor-only session state, never serialized; keyed by stable part id. The
25
+ * unmodified BASE local uvs of every mesh part, captured once at construction
26
+ * so atlas remaps always derive from the original (idempotent). */
27
+ baseMeshUvs = /* @__PURE__ */ new Map();
28
+ constructor(model) {
29
+ this.model = structuredClone(model);
30
+ for (const part of this.model.parts) {
31
+ if (part.mesh) {
32
+ this.baseMeshUvs.set(part.id, part.mesh.uvs.slice());
33
+ }
34
+ }
35
+ }
36
+ /** Live reference to the working model — for READ access. Mutate it only
37
+ * through {@link execute}/{@link undo}/{@link redo}. */
38
+ getModel() {
39
+ return this.model;
40
+ }
41
+ /** Resolve a part by stable id. Throws a plain `Error` (NOT an
42
+ * `IkiFormatError`) with a path-qualified message if the id is unknown. */
43
+ findPart(id) {
44
+ const part = this.model.parts.find((p) => p.id === id);
45
+ if (!part) {
46
+ throw new Error(`parts: no part with id "${id}"`);
47
+ }
48
+ return part;
49
+ }
50
+ /** Resolve a warp deformer by stable id. Throws a path-qualified plain
51
+ * `Error` if no deformer matches the id or the match is not a warp deformer.
52
+ * READ/mutate-through accessor, consistent with {@link findPart}. */
53
+ findWarpDeformer(id) {
54
+ const deformer = this.model.deformers?.find((d) => d.id === id);
55
+ if (!deformer || deformer.kind !== "warp") {
56
+ throw new Error(`deformers: no warp deformer with id "${id}"`);
57
+ }
58
+ return deformer;
59
+ }
60
+ /** Resolve a matrix deformer by stable id. Throws a path-qualified plain
61
+ * `Error` if no deformer matches the id or the match is a warp deformer
62
+ * (`kind === "warp"`). A `kind` of `"matrix"` or `undefined` is a matrix
63
+ * deformer. READ/mutate-through accessor, consistent with {@link findPart}. */
64
+ findMatrixDeformer(id) {
65
+ const deformer = this.model.deformers?.find((d) => d.id === id);
66
+ if (!deformer || deformer.kind === "warp") {
67
+ throw new Error(`deformers: no matrix deformer with id "${id}"`);
68
+ }
69
+ return deformer;
70
+ }
71
+ /** Resolve any deformer (matrix or warp) by stable id. Throws a
72
+ * path-qualified plain `Error` if no deformer matches the id.
73
+ * READ/mutate-through accessor, consistent with {@link findPart}. */
74
+ findDeformer(id) {
75
+ const deformer = this.model.deformers?.find((d) => d.id === id);
76
+ if (!deformer) {
77
+ throw new Error(`deformers: no deformer with id "${id}"`);
78
+ }
79
+ return deformer;
80
+ }
81
+ /** Resolve a physics rig by stable id. Throws a path-qualified plain `Error`
82
+ * if no rig matches the id. The `physics` array is optional, so guard it.
83
+ * READ/mutate-through accessor, consistent with {@link findDeformer}. */
84
+ findPhysicsRig(id) {
85
+ const rig = this.model.physics?.find((r) => r.id === id);
86
+ if (!rig) {
87
+ throw new Error(`physics: no physics rig with id "${id}"`);
88
+ }
89
+ return rig;
90
+ }
91
+ /**
92
+ * Record the base mesh UVs for a part inserted AFTER construction (e.g. by
93
+ * {@link AddPart}). Returns the PRIOR entry for that id (or `undefined` if
94
+ * none existed) so the caller can restore it on undo. No-op for meshless
95
+ * parts (returns `undefined`). Must be called by any command that pushes a
96
+ * mesh part into the model so the part joins the constructor-captured base-UV
97
+ * side state required by {@link applyAtlas}.
98
+ *
99
+ * The returned prior value is what {@link restoreBaseMeshUvs} expects on
100
+ * undo — pass it verbatim. Hazard guarded: DeletePart(X) → AddPart(X′) →
101
+ * undo(add) → undo(delete); without restore, X's constructor-captured entry
102
+ * would be permanently gone after undo of the add, causing applyAtlas to
103
+ * fail when X is restored.
104
+ */
105
+ captureBaseMeshUvs(partId) {
106
+ const prev = this.baseMeshUvs.get(partId);
107
+ const part = this.model.parts.find((p) => p.id === partId);
108
+ if (part?.mesh) {
109
+ this.baseMeshUvs.set(partId, part.mesh.uvs.slice());
110
+ }
111
+ return prev;
112
+ }
113
+ /**
114
+ * Restore the base-UV side state to exactly what it was before a
115
+ * {@link captureBaseMeshUvs} call. Called from {@link AddPart.invert}:
116
+ * pass the value returned by captureBaseMeshUvs on first apply.
117
+ * - `prev` is `number[]` → sets the entry (restores a prior mesh's base).
118
+ * - `prev` is `undefined` → deletes the entry (no entry existed before the
119
+ * add, so a later different-mesh part reusing the id must not inherit this
120
+ * one's base).
121
+ * DeletePart does NOT call this — the entry persists across delete/undo so
122
+ * the restored part still has its base available for applyAtlas.
123
+ */
124
+ restoreBaseMeshUvs(partId, prev) {
125
+ if (prev !== void 0) {
126
+ this.baseMeshUvs.set(partId, prev);
127
+ } else {
128
+ this.baseMeshUvs.delete(partId);
129
+ }
130
+ }
131
+ /** The construction-captured base UVs for a mesh part. Throws a path-qualified
132
+ * plain `Error` if absent. SINGLE accessor for both apply branches — never
133
+ * read `baseMeshUvs` with a bare `!` elsewhere. */
134
+ requireBaseUvs(partId) {
135
+ const base = this.baseMeshUvs.get(partId);
136
+ if (!base) {
137
+ throw new Error(`parts: no base mesh uvs captured for part "${partId}"`);
138
+ }
139
+ return base;
140
+ }
141
+ /**
142
+ * Replace the atlas table and rewrite every part's texture reference in a
143
+ * single atomic step. For every part in `partTextureAssignments` set
144
+ * `texture = { index: 0, uv }`; CLEAR `texture` (delete the key) on every
145
+ * other part.
146
+ *
147
+ * Mesh parts are textured as a MATCHED PAIR: an assigned mesh part also has
148
+ * its per-vertex `mesh.uvs` remapped (from the construction-captured base)
149
+ * into the same `uv` rect; an unassigned mesh part has its `mesh.uvs` restored
150
+ * to that base. Quad parts carry `texture.uv` only and are untouched here.
151
+ *
152
+ * Deliberately NON-undoable: it does NOT push to or clear the undo/redo
153
+ * stacks (texture/atlas state is not undoable in 5b — the unified
154
+ * editor-state superset is deferred to 5d). `canUndo()`/`canRedo()` are
155
+ * unchanged after a call.
156
+ *
157
+ * Validate-all-then-apply: structural input validation, per-partId
158
+ * resolution, and a base-UV preflight over every mesh part all run BEFORE any
159
+ * mutation, so a bad input (wrong shape, duplicate partId, unknown partId, or
160
+ * a mesh part with no captured base) throws a plain `Error` and leaves the
161
+ * model exactly as it was — never a partial application.
162
+ */
163
+ applyAtlas(input) {
164
+ const { texture, assignmentsByPart } = this.normalizeAtlasInput(input);
165
+ const resolved = /* @__PURE__ */ new Map();
166
+ for (const [partId, uv] of assignmentsByPart) {
167
+ resolved.set(this.findPart(partId), uv);
168
+ }
169
+ for (const part of this.model.parts) {
170
+ if (part.mesh) {
171
+ this.requireBaseUvs(part.id);
172
+ }
173
+ }
174
+ this.model.textures = texture === void 0 ? void 0 : [{ source: texture.source }];
175
+ for (const part of this.model.parts) {
176
+ const uv = resolved.get(part);
177
+ if (uv) {
178
+ part.texture = {
179
+ index: 0,
180
+ uv: { x: uv.x, y: uv.y, width: uv.width, height: uv.height }
181
+ };
182
+ if (part.mesh) {
183
+ part.mesh = {
184
+ ...part.mesh,
185
+ uvs: remapMeshUvsToRect(this.requireBaseUvs(part.id), uv)
186
+ };
187
+ }
188
+ } else {
189
+ delete part.texture;
190
+ if (part.mesh) {
191
+ part.mesh = {
192
+ ...part.mesh,
193
+ uvs: this.requireBaseUvs(part.id).slice()
194
+ };
195
+ }
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Clear a model-committed texture reference from a single part. Deliberately
201
+ * NON-undoable, matching {@link applyAtlas} — texture/atlas state is not in
202
+ * the undo model (unified editor-state is deferred). Does NOT push to or
203
+ * clear the undo/redo stacks. Does NOT touch `mesh.uvs` — atlas-space UVs on
204
+ * an untextured mesh are inert for color rendering; a later atlas import
205
+ * remaps from the base UVs anyway.
206
+ *
207
+ * This is the single consistent texture/atlas undo boundary: by the time a
208
+ * part is deletable (no texture), its DeletePart snapshot carries no texture
209
+ * reference, so no stale index can resurface on undo after a later atlas
210
+ * repack.
211
+ *
212
+ * Throws a path-qualified plain `Error` (NOT `IkiFormatError`) if the part
213
+ * id is unknown — same contract as {@link findPart}.
214
+ */
215
+ clearPartTextureRef(partId) {
216
+ const part = this.findPart(partId);
217
+ delete part.texture;
218
+ }
219
+ /**
220
+ * Validate the structural shape of an {@link ApplyAtlasInput} without
221
+ * touching the model, returning a known-good `{ texture?, assignmentsByPart }`
222
+ * for {@link applyAtlas} to resolve and apply.
223
+ */
224
+ normalizeAtlasInput(input) {
225
+ if (input.textures.length > 1) {
226
+ throw new Error(
227
+ `applyAtlas: textures must be a single atlas page (got ${input.textures.length})`
228
+ );
229
+ }
230
+ if (input.partTextureAssignments.length > 0 && input.textures.length !== 1) {
231
+ throw new Error(
232
+ "applyAtlas: partTextureAssignments require exactly one texture"
233
+ );
234
+ }
235
+ const assignmentsByPart = /* @__PURE__ */ new Map();
236
+ for (const { partId, uv } of input.partTextureAssignments) {
237
+ if (assignmentsByPart.has(partId)) {
238
+ throw new Error(
239
+ `applyAtlas: duplicate partId "${partId}" in partTextureAssignments`
240
+ );
241
+ }
242
+ assignmentsByPart.set(partId, uv);
243
+ }
244
+ return { texture: input.textures[0], assignmentsByPart };
245
+ }
246
+ /**
247
+ * Overwrite a part's whole transform with a fresh copy of `transform`.
248
+ * Replacing the whole object (rather than individual channels) preserves any
249
+ * optional keys already absent from the incoming value.
250
+ *
251
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
252
+ * pose. Does NOT push to or clear undoStack/redoStack (sibling to
253
+ * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
254
+ * restoring the exact prior snapshot when the capture pose ends.
255
+ *
256
+ * `IkiTransform` is a flat number map, so a shallow spread is a sufficient
257
+ * deep copy — no aliasing of the caller's object remains.
258
+ */
259
+ setPartTransformEphemeral(partId, transform) {
260
+ const part = this.findPart(partId);
261
+ part.transform = { ...transform };
262
+ }
263
+ /**
264
+ * Overwrite a matrix deformer's optional transform with a fresh copy, or
265
+ * delete it when `transform` is `undefined`.
266
+ * `undefined` deletes the key, restoring the deformer to the same state as
267
+ * one that never had a transform (absent-vs-present matters for downstream
268
+ * renderers).
269
+ *
270
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
271
+ * pose. Does NOT push to or clear undoStack/redoStack (sibling to
272
+ * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
273
+ * restoring the exact prior snapshot when the capture pose ends.
274
+ *
275
+ * `IkiDeformerTransform` is a flat number map, so a shallow spread is a
276
+ * sufficient deep copy — no aliasing of the caller's object remains.
277
+ */
278
+ setDeformerTransformEphemeral(deformerId, transform) {
279
+ const deformer = this.findMatrixDeformer(deformerId);
280
+ if (transform === void 0) {
281
+ delete deformer.transform;
282
+ } else {
283
+ deformer.transform = { ...transform };
284
+ }
285
+ }
286
+ /**
287
+ * Overwrite a part's whole bindings array with a fresh deep copy, or delete
288
+ * the key when `bindings` is empty.
289
+ *
290
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
291
+ * neutralization (zeroing the row being recaptured so the preview reflects
292
+ * base-only during posing). Does NOT push to or clear undoStack/redoStack.
293
+ * The caller is responsible for restoring the original bindings when the
294
+ * capture session ends.
295
+ */
296
+ setPartBindingsEphemeral(partId, bindings) {
297
+ const part = this.findPart(partId);
298
+ if (bindings.length > 0) {
299
+ part.bindings = bindings.map((b) => ({ ...b }));
300
+ } else {
301
+ delete part.bindings;
302
+ }
303
+ }
304
+ /**
305
+ * Overwrite a matrix deformer's whole bindings array with a fresh deep copy,
306
+ * or delete the key when `bindings` is empty.
307
+ *
308
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
309
+ * neutralization (zeroing the row being recaptured so the preview reflects
310
+ * base-only during posing). Does NOT push to or clear undoStack/redoStack.
311
+ * The caller is responsible for restoring the original bindings when the
312
+ * capture session ends.
313
+ */
314
+ setDeformerBindingsEphemeral(deformerId, bindings) {
315
+ const deformer = this.findMatrixDeformer(deformerId);
316
+ if (bindings.length > 0) {
317
+ deformer.bindings = bindings.map((b) => ({ ...b }));
318
+ } else {
319
+ delete deformer.bindings;
320
+ }
321
+ }
322
+ /** Apply a command and record it as one undo step. Clears the redo stack. */
323
+ execute(cmd) {
324
+ cmd.apply(this);
325
+ this.undoStack.push(cmd);
326
+ this.redoStack.length = 0;
327
+ }
328
+ /** Invert the most recent command and move it onto the redo stack. */
329
+ undo() {
330
+ const cmd = this.undoStack.pop();
331
+ if (!cmd) return;
332
+ cmd.invert(this);
333
+ this.redoStack.push(cmd);
334
+ }
335
+ /** Re-apply the most recently undone command and move it back onto undo. */
336
+ redo() {
337
+ const cmd = this.redoStack.pop();
338
+ if (!cmd) return;
339
+ cmd.apply(this);
340
+ this.undoStack.push(cmd);
341
+ }
342
+ canUndo() {
343
+ return this.undoStack.length > 0;
344
+ }
345
+ canRedo() {
346
+ return this.redoStack.length > 0;
347
+ }
348
+ /**
349
+ * Validate and export the current working model by running it through
350
+ * {@link parseIkiModel}. Uses `structuredClone` so the validator's
351
+ * normalized output cannot alias the working model. Propagates
352
+ * `IkiFormatError` unchanged on failure — callers surface `.message`.
353
+ */
354
+ toIkiModel() {
355
+ return parseIkiModel(structuredClone(this.model));
356
+ }
357
+ /**
358
+ * Pretty-print the validated model as a `.iki` JSON string. Always
359
+ * validates first — invalid documents never reach a file.
360
+ */
361
+ serialize() {
362
+ return JSON.stringify(this.toIkiModel(), null, 2);
363
+ }
364
+ };
365
+
366
+ // src/commands.ts
367
+ import {
368
+ IKI_FORMAT_VERSION,
369
+ IkiFormatError,
370
+ parseIkiModel as parseIkiModel2
371
+ } from "@ikijs/format";
372
+
373
+ // src/grid-keyform.ts
374
+ function interpolateGridOffsets(keyforms, value) {
375
+ if (keyforms.length === 0) {
376
+ throw new Error("interpolateGridOffsets: keyforms must be non-empty");
377
+ }
378
+ if (value <= keyforms[0].value) {
379
+ return [...keyforms[0].offsets];
380
+ }
381
+ const last = keyforms[keyforms.length - 1];
382
+ if (value >= last.value) {
383
+ return [...last.offsets];
384
+ }
385
+ let lo = keyforms[0];
386
+ let hi = keyforms[1];
387
+ for (let k = 1; k < keyforms.length - 1; k++) {
388
+ if (keyforms[k].value <= value) {
389
+ lo = keyforms[k];
390
+ hi = keyforms[k + 1];
391
+ }
392
+ }
393
+ const t = (value - lo.value) / (hi.value - lo.value);
394
+ return lo.offsets.map((loOff, i) => loOff + (hi.offsets[i] - loOff) * t);
395
+ }
396
+ function computeGridOffsets(restPoints, restFrameDraggedPoints) {
397
+ if (restPoints.length !== restFrameDraggedPoints.length) {
398
+ throw new Error(
399
+ `computeGridOffsets: restFrameDraggedPoints length ${restFrameDraggedPoints.length} must equal restPoints length ${restPoints.length}`
400
+ );
401
+ }
402
+ if (restPoints.length % 2 !== 0) {
403
+ throw new Error(
404
+ `computeGridOffsets: restPoints length ${restPoints.length} must be even (x,y pairs)`
405
+ );
406
+ }
407
+ return restPoints.map((rest, i) => restFrameDraggedPoints[i] - rest);
408
+ }
409
+ function upsertGridKeyform(keyforms, value, offsets) {
410
+ const result = keyforms.map((kf) => ({
411
+ value: kf.value,
412
+ offsets: [...kf.offsets]
413
+ }));
414
+ const existing = result.findIndex((kf) => kf.value === value);
415
+ if (existing !== -1) {
416
+ result[existing] = { value, offsets: [...offsets] };
417
+ return result;
418
+ }
419
+ const insertAt = result.findIndex((kf) => kf.value > value);
420
+ const entry = { value, offsets: [...offsets] };
421
+ if (insertAt === -1) {
422
+ result.push(entry);
423
+ } else {
424
+ result.splice(insertAt, 0, entry);
425
+ }
426
+ return result;
427
+ }
428
+
429
+ // src/reparent.ts
430
+ function kindOf(d) {
431
+ return d.kind === "warp" ? "warp" : "matrix";
432
+ }
433
+ function validateDeformerReparent(deformers, deformerId, newParentId) {
434
+ const target = deformers.find((d) => d.id === deformerId);
435
+ if (target === void 0) {
436
+ throw new Error(`deformers: no deformer with id "${deformerId}"`);
437
+ }
438
+ if (newParentId === void 0) return;
439
+ if (newParentId === deformerId) {
440
+ throw new Error(
441
+ `deformers."${deformerId}".parent "${newParentId}" is a self-reference`
442
+ );
443
+ }
444
+ const parent = deformers.find((d) => d.id === newParentId);
445
+ if (parent === void 0) {
446
+ throw new Error(
447
+ `deformers."${deformerId}".parent "${newParentId}" is not a declared deformer`
448
+ );
449
+ }
450
+ if (kindOf(parent) === "warp") {
451
+ throw new Error(
452
+ `deformers."${deformerId}".parent "${newParentId}" must be a matrix deformer (warp deformers cannot be parents)`
453
+ );
454
+ }
455
+ const parentOf = /* @__PURE__ */ new Map();
456
+ for (const d of deformers) {
457
+ if (d.parent !== void 0) parentOf.set(d.id, d.parent);
458
+ }
459
+ parentOf.set(deformerId, newParentId);
460
+ const visited = /* @__PURE__ */ new Set();
461
+ let cur = deformerId;
462
+ while (cur !== void 0) {
463
+ if (visited.has(cur)) {
464
+ throw new Error(
465
+ `deformers: reparenting "${deformerId}" under "${newParentId}" would create a cycle`
466
+ );
467
+ }
468
+ visited.add(cur);
469
+ cur = parentOf.get(cur);
470
+ }
471
+ }
472
+ function validateDeformerDelete(deformers, parts, physicsChains, deformerId) {
473
+ const target = deformers.find((d) => d.id === deformerId);
474
+ if (target === void 0) {
475
+ throw new Error(`deformers: no deformer with id "${deformerId}"`);
476
+ }
477
+ const childDeformer = deformers.find((d) => d.parent === deformerId);
478
+ if (childDeformer !== void 0) {
479
+ throw new Error(
480
+ `deformers."${deformerId}": cannot delete \u2014 deformer "${childDeformer.id}" is parented to it; reparent or detach it first`
481
+ );
482
+ }
483
+ const attachedPart = parts.find((p) => p.deformer === deformerId);
484
+ if (attachedPart !== void 0) {
485
+ throw new Error(
486
+ `deformers."${deformerId}": cannot delete \u2014 part "${attachedPart.id}" is attached to it; detach it first`
487
+ );
488
+ }
489
+ const anchoredChain = physicsChains.find(
490
+ (c) => c.anchorDeformer === deformerId
491
+ );
492
+ if (anchoredChain !== void 0) {
493
+ throw new Error(
494
+ `deformers."${deformerId}": cannot delete \u2014 physics chain "${anchoredChain.id}" anchors to it; re-anchor or delete the chain first`
495
+ );
496
+ }
497
+ }
498
+ function validatePartAttach(deformers, partId, parts, newDeformerId) {
499
+ const part = parts.find((p) => p.id === partId);
500
+ if (part === void 0) {
501
+ throw new Error(`parts: no part with id "${partId}"`);
502
+ }
503
+ if (newDeformerId === void 0) return;
504
+ const deformer = deformers.find((d) => d.id === newDeformerId);
505
+ if (deformer === void 0) {
506
+ throw new Error(
507
+ `parts."${partId}".deformer "${newDeformerId}" is not a declared deformer`
508
+ );
509
+ }
510
+ if (kindOf(deformer) === "warp" && part.mesh === void 0) {
511
+ throw new Error(
512
+ `parts."${partId}".deformer "${newDeformerId}" is a warp deformer and requires a mesh`
513
+ );
514
+ }
515
+ }
516
+
517
+ // src/commands.ts
518
+ function findIdCollision(model, id) {
519
+ for (const p of model.parts) {
520
+ if (p.id === id) return "part";
521
+ }
522
+ for (const d of model.deformers ?? []) {
523
+ if (d.id === id) return "deformer";
524
+ }
525
+ return void 0;
526
+ }
527
+ var FieldCommand = class {
528
+ constructor(partId, newValue, label, get, set) {
529
+ this.partId = partId;
530
+ this.newValue = newValue;
531
+ this.get = get;
532
+ this.set = set;
533
+ this.label = label;
534
+ }
535
+ partId;
536
+ newValue;
537
+ get;
538
+ set;
539
+ label;
540
+ captured = false;
541
+ prevValue;
542
+ apply(doc) {
543
+ const part = doc.findPart(this.partId);
544
+ if (!this.captured) {
545
+ this.prevValue = this.get(part);
546
+ this.captured = true;
547
+ }
548
+ this.set(part, this.newValue);
549
+ }
550
+ invert(doc) {
551
+ const part = doc.findPart(this.partId);
552
+ this.set(part, this.prevValue);
553
+ }
554
+ };
555
+ var SetPartColor = class extends FieldCommand {
556
+ constructor(partId, rgba) {
557
+ super(
558
+ partId,
559
+ [...rgba],
560
+ "Set color",
561
+ (part) => [...part.color],
562
+ (part, value) => {
563
+ part.color = [...value];
564
+ }
565
+ );
566
+ }
567
+ };
568
+ var SetPartWidth = class extends FieldCommand {
569
+ constructor(partId, value) {
570
+ super(
571
+ partId,
572
+ value,
573
+ "Set width",
574
+ (part) => part.width,
575
+ (part, v) => {
576
+ part.width = v;
577
+ }
578
+ );
579
+ }
580
+ };
581
+ var SetPartHeight = class extends FieldCommand {
582
+ constructor(partId, value) {
583
+ super(
584
+ partId,
585
+ value,
586
+ "Set height",
587
+ (part) => part.height,
588
+ (part, v) => {
589
+ part.height = v;
590
+ }
591
+ );
592
+ }
593
+ };
594
+ var SetPartOrder = class extends FieldCommand {
595
+ constructor(partId, value) {
596
+ super(
597
+ partId,
598
+ value,
599
+ "Set order",
600
+ (part) => part.order,
601
+ (part, v) => {
602
+ part.order = v;
603
+ }
604
+ );
605
+ }
606
+ };
607
+ var SetPartTransform = class extends FieldCommand {
608
+ constructor(partId, channel, value) {
609
+ super(
610
+ partId,
611
+ value,
612
+ "Set transform",
613
+ (part) => part.transform[channel],
614
+ (part, v) => {
615
+ if (v === void 0) {
616
+ delete part.transform[channel];
617
+ } else {
618
+ part.transform[channel] = v;
619
+ }
620
+ }
621
+ );
622
+ }
623
+ };
624
+ var CaptureGridKeyform = class {
625
+ constructor(deformerId, value, offsets) {
626
+ this.deformerId = deformerId;
627
+ this.value = value;
628
+ this.offsets = [...offsets];
629
+ }
630
+ deformerId;
631
+ value;
632
+ label = "Capture grid keyform";
633
+ offsets;
634
+ captured = false;
635
+ prevKeyforms;
636
+ apply(doc) {
637
+ const deformer = doc.findWarpDeformer(this.deformerId);
638
+ const warp = deformer.warps?.[0];
639
+ if (!warp) {
640
+ throw new Error(
641
+ `deformers."${this.deformerId}".warps: no grid warp to capture into`
642
+ );
643
+ }
644
+ if (this.offsets.length !== deformer.grid.points.length) {
645
+ throw new Error(
646
+ `deformers."${this.deformerId}".warps[0].keyforms.offsets length ${this.offsets.length} must equal grid.points length ${deformer.grid.points.length}`
647
+ );
648
+ }
649
+ const param = doc.getModel().parameters.find((p) => p.id === warp.parameter);
650
+ if (!param) {
651
+ throw new Error(
652
+ `deformers."${this.deformerId}".warps[0].parameter "${warp.parameter}" is not a declared parameter`
653
+ );
654
+ }
655
+ if (this.value < param.min || this.value > param.max) {
656
+ throw new Error(
657
+ `deformers."${this.deformerId}".warps[0].keyforms.value ${this.value} is outside parameter "${warp.parameter}" range [${param.min},${param.max}]`
658
+ );
659
+ }
660
+ if (!this.captured) {
661
+ this.prevKeyforms = structuredClone(warp.keyforms);
662
+ this.captured = true;
663
+ }
664
+ warp.keyforms = upsertGridKeyform(warp.keyforms, this.value, [
665
+ ...this.offsets
666
+ ]);
667
+ }
668
+ invert(doc) {
669
+ const warp = doc.findWarpDeformer(this.deformerId).warps?.[0];
670
+ if (!warp) {
671
+ throw new Error(
672
+ `deformers."${this.deformerId}".warps: no grid warp to restore into`
673
+ );
674
+ }
675
+ warp.keyforms = structuredClone(this.prevKeyforms);
676
+ }
677
+ };
678
+ var DeformerFieldCommand = class {
679
+ constructor(deformerId, newValue, label, get, set) {
680
+ this.deformerId = deformerId;
681
+ this.newValue = newValue;
682
+ this.get = get;
683
+ this.set = set;
684
+ this.label = label;
685
+ }
686
+ deformerId;
687
+ newValue;
688
+ get;
689
+ set;
690
+ label;
691
+ captured = false;
692
+ prevValue;
693
+ apply(doc) {
694
+ const deformer = doc.findMatrixDeformer(this.deformerId);
695
+ if (!this.captured) {
696
+ this.prevValue = this.get(deformer);
697
+ this.captured = true;
698
+ }
699
+ this.set(deformer, this.newValue);
700
+ }
701
+ invert(doc) {
702
+ const deformer = doc.findMatrixDeformer(this.deformerId);
703
+ this.set(deformer, this.prevValue);
704
+ }
705
+ };
706
+ var SetDeformerPivotX = class extends DeformerFieldCommand {
707
+ constructor(deformerId, value) {
708
+ super(
709
+ deformerId,
710
+ value,
711
+ "Set pivot x",
712
+ (d) => d.pivot.x,
713
+ (d, v) => {
714
+ d.pivot.x = v;
715
+ }
716
+ );
717
+ }
718
+ };
719
+ var SetDeformerPivotY = class extends DeformerFieldCommand {
720
+ constructor(deformerId, value) {
721
+ super(
722
+ deformerId,
723
+ value,
724
+ "Set pivot y",
725
+ (d) => d.pivot.y,
726
+ (d, v) => {
727
+ d.pivot.y = v;
728
+ }
729
+ );
730
+ }
731
+ };
732
+ var SetDeformerPivot = class {
733
+ constructor(deformerId, pivot) {
734
+ this.deformerId = deformerId;
735
+ this.pivot = { x: pivot.x, y: pivot.y };
736
+ }
737
+ deformerId;
738
+ label = "Set pivot";
739
+ captured = false;
740
+ prevPivot;
741
+ pivot;
742
+ apply(doc) {
743
+ const deformer = doc.findMatrixDeformer(this.deformerId);
744
+ if (!this.captured) {
745
+ this.prevPivot = { x: deformer.pivot.x, y: deformer.pivot.y };
746
+ this.captured = true;
747
+ }
748
+ deformer.pivot = { x: this.pivot.x, y: this.pivot.y };
749
+ }
750
+ invert(doc) {
751
+ const deformer = doc.findMatrixDeformer(this.deformerId);
752
+ deformer.pivot = { x: this.prevPivot.x, y: this.prevPivot.y };
753
+ }
754
+ };
755
+ var SetDeformerTransform = class {
756
+ constructor(deformerId, channel, value) {
757
+ this.deformerId = deformerId;
758
+ this.channel = channel;
759
+ this.value = value;
760
+ }
761
+ deformerId;
762
+ channel;
763
+ value;
764
+ label = "Set deformer transform";
765
+ captured = false;
766
+ prevTransform;
767
+ apply(doc) {
768
+ const deformer = doc.findMatrixDeformer(this.deformerId);
769
+ if (!this.captured) {
770
+ this.prevTransform = deformer.transform === void 0 ? void 0 : { ...deformer.transform };
771
+ this.captured = true;
772
+ }
773
+ const next = {
774
+ ...deformer.transform ?? { x: 0, y: 0 }
775
+ };
776
+ next[this.channel] = this.value;
777
+ deformer.transform = next;
778
+ }
779
+ invert(doc) {
780
+ const deformer = doc.findMatrixDeformer(this.deformerId);
781
+ if (this.prevTransform === void 0) {
782
+ delete deformer.transform;
783
+ } else {
784
+ deformer.transform = { ...this.prevTransform };
785
+ }
786
+ }
787
+ };
788
+ var SetDeformerBindings = class {
789
+ constructor(deformerId, bindings) {
790
+ this.deformerId = deformerId;
791
+ this.bindings = bindings.map((b) => ({ ...b }));
792
+ }
793
+ deformerId;
794
+ label = "Set deformer bindings";
795
+ bindings;
796
+ captured = false;
797
+ prevBindings;
798
+ apply(doc) {
799
+ const deformer = doc.findMatrixDeformer(this.deformerId);
800
+ const candidateDeformer = {
801
+ kind: "matrix",
802
+ id: "_d",
803
+ pivot: { x: 0, y: 0 }
804
+ };
805
+ if (this.bindings.length > 0) {
806
+ candidateDeformer.bindings = this.bindings.map((b) => ({ ...b }));
807
+ }
808
+ const candidate = {
809
+ version: IKI_FORMAT_VERSION,
810
+ name: "_",
811
+ canvas: { width: 1, height: 1 },
812
+ parameters: doc.getModel().parameters,
813
+ parts: [
814
+ {
815
+ id: "_",
816
+ color: [0, 0, 0, 1],
817
+ width: 1,
818
+ height: 1,
819
+ transform: { x: 0, y: 0 },
820
+ order: 0
821
+ }
822
+ ],
823
+ deformers: [candidateDeformer]
824
+ };
825
+ try {
826
+ parseIkiModel2(candidate);
827
+ } catch (e) {
828
+ if (e instanceof IkiFormatError) {
829
+ throw new IkiFormatError(
830
+ e.message.replace(
831
+ /^deformers\[0\]/,
832
+ `deformers."${this.deformerId}"`
833
+ )
834
+ );
835
+ }
836
+ throw e;
837
+ }
838
+ if (!this.captured) {
839
+ this.prevBindings = deformer.bindings === void 0 ? void 0 : deformer.bindings.map((b) => ({ ...b }));
840
+ this.captured = true;
841
+ }
842
+ if (this.bindings.length > 0) {
843
+ deformer.bindings = this.bindings.map((b) => ({ ...b }));
844
+ } else {
845
+ delete deformer.bindings;
846
+ }
847
+ }
848
+ invert(doc) {
849
+ const deformer = doc.findMatrixDeformer(this.deformerId);
850
+ if (this.prevBindings === void 0) {
851
+ delete deformer.bindings;
852
+ } else {
853
+ deformer.bindings = this.prevBindings.map((b) => ({ ...b }));
854
+ }
855
+ }
856
+ };
857
+ var SetDeformerParent = class {
858
+ constructor(deformerId, newParentId) {
859
+ this.deformerId = deformerId;
860
+ this.newParentId = newParentId;
861
+ }
862
+ deformerId;
863
+ newParentId;
864
+ label = "Set deformer parent";
865
+ captured = false;
866
+ prevParent = void 0;
867
+ prevHadParent = false;
868
+ apply(doc) {
869
+ validateDeformerReparent(
870
+ doc.getModel().deformers ?? [],
871
+ this.deformerId,
872
+ this.newParentId
873
+ );
874
+ const deformer = doc.findDeformer(this.deformerId);
875
+ if (!this.captured) {
876
+ this.prevHadParent = Object.prototype.hasOwnProperty.call(
877
+ deformer,
878
+ "parent"
879
+ );
880
+ this.prevParent = deformer.parent;
881
+ this.captured = true;
882
+ }
883
+ if (this.newParentId !== void 0) {
884
+ deformer.parent = this.newParentId;
885
+ } else {
886
+ delete deformer.parent;
887
+ }
888
+ }
889
+ invert(doc) {
890
+ const deformer = doc.findDeformer(this.deformerId);
891
+ if (this.prevHadParent) {
892
+ deformer.parent = this.prevParent;
893
+ } else {
894
+ delete deformer.parent;
895
+ }
896
+ }
897
+ };
898
+ var AddPart = class {
899
+ label = "Add part";
900
+ part;
901
+ captured = false;
902
+ prevBaseMeshUvs = void 0;
903
+ constructor(part) {
904
+ this.part = structuredClone(part);
905
+ }
906
+ apply(doc) {
907
+ const hit = findIdCollision(doc.getModel(), this.part.id);
908
+ if (hit === "part") {
909
+ throw new Error(
910
+ `parts: id "${this.part.id}" collides with an existing part id`
911
+ );
912
+ }
913
+ if (hit === "deformer") {
914
+ throw new Error(
915
+ `parts: id "${this.part.id}" collides with an existing deformer id`
916
+ );
917
+ }
918
+ const candidate = structuredClone(doc.getModel());
919
+ candidate.parts.push(structuredClone(this.part));
920
+ parseIkiModel2(candidate);
921
+ doc.getModel().parts.push(structuredClone(this.part));
922
+ const prev = doc.captureBaseMeshUvs(this.part.id);
923
+ if (!this.captured) {
924
+ this.prevBaseMeshUvs = prev;
925
+ this.captured = true;
926
+ }
927
+ }
928
+ invert(doc) {
929
+ const parts = doc.getModel().parts;
930
+ const i = parts.findIndex((p) => p.id === this.part.id);
931
+ if (i !== -1) parts.splice(i, 1);
932
+ doc.restoreBaseMeshUvs(this.part.id, this.prevBaseMeshUvs);
933
+ }
934
+ };
935
+ var AddDeformer = class {
936
+ label = "Add deformer";
937
+ deformer;
938
+ captured = false;
939
+ prevDeformersAbsent = false;
940
+ constructor(deformer) {
941
+ this.deformer = structuredClone(deformer);
942
+ }
943
+ apply(doc) {
944
+ const model = doc.getModel();
945
+ const hit = findIdCollision(model, this.deformer.id);
946
+ if (hit === "deformer") {
947
+ throw new Error(
948
+ `deformers: id "${this.deformer.id}" collides with an existing deformer id`
949
+ );
950
+ }
951
+ if (hit === "part") {
952
+ throw new Error(
953
+ `deformers: id "${this.deformer.id}" collides with an existing part id`
954
+ );
955
+ }
956
+ const candidate = structuredClone(model);
957
+ candidate.deformers = [
958
+ ...candidate.deformers ?? [],
959
+ structuredClone(this.deformer)
960
+ ];
961
+ parseIkiModel2(candidate);
962
+ if (!this.captured) {
963
+ this.prevDeformersAbsent = model.deformers === void 0;
964
+ this.captured = true;
965
+ }
966
+ if (model.deformers === void 0) {
967
+ model.deformers = [];
968
+ }
969
+ model.deformers.push(structuredClone(this.deformer));
970
+ }
971
+ invert(doc) {
972
+ const model = doc.getModel();
973
+ const arr = model.deformers;
974
+ if (!arr) return;
975
+ const i = arr.findIndex((d) => d.id === this.deformer.id);
976
+ if (i !== -1) arr.splice(i, 1);
977
+ if (this.prevDeformersAbsent && arr.length === 0) {
978
+ delete model.deformers;
979
+ }
980
+ }
981
+ };
982
+ var DeletePart = class {
983
+ constructor(partId) {
984
+ this.partId = partId;
985
+ }
986
+ partId;
987
+ label = "Delete part";
988
+ captured = false;
989
+ removed;
990
+ index;
991
+ apply(doc) {
992
+ const part = doc.findPart(this.partId);
993
+ if (part.texture !== void 0) {
994
+ throw new Error(
995
+ `parts."${this.partId}": cannot delete \u2014 part has a texture reference; clear its texture first`
996
+ );
997
+ }
998
+ const parts = doc.getModel().parts;
999
+ const masker = parts.find(
1000
+ (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId)
1001
+ );
1002
+ if (masker) {
1003
+ throw new Error(
1004
+ `parts."${this.partId}": cannot delete \u2014 used as a clip mask by part "${masker.id}"; remove its clip first`
1005
+ );
1006
+ }
1007
+ const i = parts.indexOf(part);
1008
+ if (!this.captured) {
1009
+ this.removed = structuredClone(part);
1010
+ this.index = i;
1011
+ this.captured = true;
1012
+ }
1013
+ parts.splice(i, 1);
1014
+ }
1015
+ invert(doc) {
1016
+ doc.getModel().parts.splice(this.index, 0, structuredClone(this.removed));
1017
+ }
1018
+ };
1019
+ var DeleteDeformer = class {
1020
+ constructor(deformerId) {
1021
+ this.deformerId = deformerId;
1022
+ }
1023
+ deformerId;
1024
+ label = "Delete deformer";
1025
+ captured = false;
1026
+ removed;
1027
+ index;
1028
+ apply(doc) {
1029
+ const model = doc.getModel();
1030
+ validateDeformerDelete(
1031
+ model.deformers ?? [],
1032
+ model.parts,
1033
+ model.physicsChains ?? [],
1034
+ this.deformerId
1035
+ );
1036
+ const arr = model.deformers;
1037
+ const i = arr.findIndex((d) => d.id === this.deformerId);
1038
+ if (!this.captured) {
1039
+ this.removed = structuredClone(arr[i]);
1040
+ this.index = i;
1041
+ this.captured = true;
1042
+ }
1043
+ arr.splice(i, 1);
1044
+ }
1045
+ invert(doc) {
1046
+ doc.getModel().deformers.splice(this.index, 0, structuredClone(this.removed));
1047
+ }
1048
+ };
1049
+ var SetPartBindings = class {
1050
+ constructor(partId, bindings) {
1051
+ this.partId = partId;
1052
+ this.bindings = bindings.map((b) => ({ ...b }));
1053
+ }
1054
+ partId;
1055
+ label = "Set part bindings";
1056
+ bindings;
1057
+ captured = false;
1058
+ prevBindings;
1059
+ apply(doc) {
1060
+ const candidatePart = {
1061
+ id: "_",
1062
+ color: [0, 0, 0, 1],
1063
+ width: 1,
1064
+ height: 1,
1065
+ transform: { x: 0, y: 0 },
1066
+ order: 0
1067
+ };
1068
+ if (this.bindings.length > 0) {
1069
+ candidatePart.bindings = this.bindings.map((b) => ({ ...b }));
1070
+ }
1071
+ const candidate = {
1072
+ version: IKI_FORMAT_VERSION,
1073
+ name: "_",
1074
+ canvas: { width: 1, height: 1 },
1075
+ parameters: doc.getModel().parameters,
1076
+ parts: [candidatePart]
1077
+ };
1078
+ try {
1079
+ parseIkiModel2(candidate);
1080
+ } catch (e) {
1081
+ if (e instanceof IkiFormatError) {
1082
+ throw new IkiFormatError(
1083
+ e.message.replace(/^parts\[0\]/, `parts."${this.partId}"`)
1084
+ );
1085
+ }
1086
+ throw e;
1087
+ }
1088
+ const part = doc.findPart(this.partId);
1089
+ if (!this.captured) {
1090
+ this.prevBindings = part.bindings === void 0 ? void 0 : part.bindings.map((b) => ({ ...b }));
1091
+ this.captured = true;
1092
+ }
1093
+ if (this.bindings.length > 0) {
1094
+ part.bindings = this.bindings.map((b) => ({ ...b }));
1095
+ } else {
1096
+ delete part.bindings;
1097
+ }
1098
+ }
1099
+ invert(doc) {
1100
+ const part = doc.findPart(this.partId);
1101
+ if (this.prevBindings === void 0) {
1102
+ delete part.bindings;
1103
+ } else {
1104
+ part.bindings = this.prevBindings.map((b) => ({ ...b }));
1105
+ }
1106
+ }
1107
+ };
1108
+ var SetPartDeformer = class {
1109
+ constructor(partId, newDeformerId) {
1110
+ this.partId = partId;
1111
+ this.newDeformerId = newDeformerId;
1112
+ }
1113
+ partId;
1114
+ newDeformerId;
1115
+ label = "Set part deformer";
1116
+ captured = false;
1117
+ prevDeformer = void 0;
1118
+ prevHadDeformer = false;
1119
+ apply(doc) {
1120
+ validatePartAttach(
1121
+ doc.getModel().deformers ?? [],
1122
+ this.partId,
1123
+ doc.getModel().parts,
1124
+ this.newDeformerId
1125
+ );
1126
+ const part = doc.findPart(this.partId);
1127
+ if (!this.captured) {
1128
+ this.prevHadDeformer = Object.prototype.hasOwnProperty.call(
1129
+ part,
1130
+ "deformer"
1131
+ );
1132
+ this.prevDeformer = part.deformer;
1133
+ this.captured = true;
1134
+ }
1135
+ if (this.newDeformerId !== void 0) {
1136
+ part.deformer = this.newDeformerId;
1137
+ } else {
1138
+ delete part.deformer;
1139
+ }
1140
+ }
1141
+ invert(doc) {
1142
+ const part = doc.findPart(this.partId);
1143
+ if (this.prevHadDeformer) {
1144
+ part.deformer = this.prevDeformer;
1145
+ } else {
1146
+ delete part.deformer;
1147
+ }
1148
+ }
1149
+ };
1150
+ function isWarpDeformer(model, deformerId) {
1151
+ const d = (model.deformers ?? []).find((x) => x.id === deformerId);
1152
+ return d?.kind === "warp";
1153
+ }
1154
+ var SetPartMesh = class {
1155
+ constructor(partId, mesh) {
1156
+ this.partId = partId;
1157
+ this.mesh = mesh === void 0 ? void 0 : structuredClone(mesh);
1158
+ }
1159
+ partId;
1160
+ label = "Set part mesh";
1161
+ mesh;
1162
+ captured = false;
1163
+ prevHadMesh = false;
1164
+ prevMesh;
1165
+ prevBaseMeshUvs;
1166
+ apply(doc) {
1167
+ const part = doc.findPart(this.partId);
1168
+ if (this.mesh !== void 0) {
1169
+ const candidatePart = {
1170
+ id: "_",
1171
+ color: [0, 0, 0, 1],
1172
+ width: 1,
1173
+ height: 1,
1174
+ transform: { x: 0, y: 0 },
1175
+ order: 0,
1176
+ mesh: structuredClone(this.mesh)
1177
+ };
1178
+ const candidate = {
1179
+ version: IKI_FORMAT_VERSION,
1180
+ name: "_",
1181
+ canvas: { width: 1, height: 1 },
1182
+ parameters: doc.getModel().parameters,
1183
+ parts: [candidatePart]
1184
+ };
1185
+ try {
1186
+ parseIkiModel2(candidate);
1187
+ } catch (e) {
1188
+ if (e instanceof IkiFormatError) {
1189
+ throw new IkiFormatError(
1190
+ e.message.replace(/^parts\[0\]/, `parts."${this.partId}"`)
1191
+ );
1192
+ }
1193
+ throw e;
1194
+ }
1195
+ }
1196
+ const attachedToWarp = part.deformer !== void 0 && isWarpDeformer(doc.getModel(), part.deformer);
1197
+ if (this.mesh === void 0) {
1198
+ if (part.warps !== void 0 || attachedToWarp) {
1199
+ throw new IkiFormatError(
1200
+ `parts."${this.partId}": cannot remove mesh \u2014 part has warps or is attached to a warp deformer; remove its warps / detach from the warp deformer first`
1201
+ );
1202
+ }
1203
+ const masker = doc.getModel().parts.find(
1204
+ (p) => p.id !== this.partId && p.clip?.masks.includes(this.partId)
1205
+ );
1206
+ if (masker) {
1207
+ throw new IkiFormatError(
1208
+ `parts."${this.partId}": cannot remove mesh \u2014 used as a clip mask by part "${masker.id}" (masks require a mesh); remove its clip first`
1209
+ );
1210
+ }
1211
+ } else {
1212
+ if ((part.warps?.length ?? 0) > 0) {
1213
+ throw new IkiFormatError(
1214
+ `parts."${this.partId}": cannot regenerate mesh \u2014 part has warps whose offsets are bound to the current rest mesh; remove its warps first`
1215
+ );
1216
+ }
1217
+ }
1218
+ if (!this.captured) {
1219
+ this.prevHadMesh = part.mesh !== void 0;
1220
+ this.prevMesh = part.mesh ? structuredClone(part.mesh) : void 0;
1221
+ this.prevBaseMeshUvs = doc.captureBaseMeshUvs(this.partId);
1222
+ this.captured = true;
1223
+ }
1224
+ if (this.mesh !== void 0) {
1225
+ const storedUvs = part.texture !== void 0 ? remapMeshUvsToRect(this.mesh.uvs, part.texture.uv) : this.mesh.uvs.slice();
1226
+ part.mesh = {
1227
+ vertices: this.mesh.vertices.slice(),
1228
+ uvs: storedUvs,
1229
+ indices: this.mesh.indices.slice()
1230
+ };
1231
+ } else {
1232
+ delete part.mesh;
1233
+ }
1234
+ if (this.mesh !== void 0) {
1235
+ doc.restoreBaseMeshUvs(this.partId, this.mesh.uvs.slice());
1236
+ } else {
1237
+ doc.restoreBaseMeshUvs(this.partId, void 0);
1238
+ }
1239
+ }
1240
+ invert(doc) {
1241
+ const part = doc.findPart(this.partId);
1242
+ if (this.prevHadMesh) {
1243
+ if (this.prevBaseMeshUvs === void 0) {
1244
+ throw new Error(
1245
+ `parts."${this.partId}": cannot invert SetPartMesh \u2014 no base mesh uvs captured for a mesh part (broken side-table invariant)`
1246
+ );
1247
+ }
1248
+ const restored = structuredClone(this.prevMesh);
1249
+ restored.uvs = part.texture !== void 0 ? remapMeshUvsToRect(this.prevBaseMeshUvs, part.texture.uv) : this.prevBaseMeshUvs.slice();
1250
+ part.mesh = restored;
1251
+ } else {
1252
+ delete part.mesh;
1253
+ }
1254
+ doc.restoreBaseMeshUvs(this.partId, this.prevBaseMeshUvs);
1255
+ }
1256
+ };
1257
+ function validatePhysicsCandidate(doc, candidatePhysics) {
1258
+ const candidatePart = {
1259
+ id: "_",
1260
+ color: [0, 0, 0, 1],
1261
+ width: 1,
1262
+ height: 1,
1263
+ transform: { x: 0, y: 0 },
1264
+ order: 0
1265
+ };
1266
+ const candidate = {
1267
+ version: IKI_FORMAT_VERSION,
1268
+ name: "_",
1269
+ canvas: { width: 1, height: 1 },
1270
+ parameters: doc.getModel().parameters,
1271
+ parts: [candidatePart],
1272
+ deformers: doc.getModel().deformers,
1273
+ physics: candidatePhysics,
1274
+ physicsChains: doc.getModel().physicsChains
1275
+ };
1276
+ try {
1277
+ parseIkiModel2(candidate);
1278
+ } catch (e) {
1279
+ if (e instanceof IkiFormatError) {
1280
+ const m = e.message.match(/^physics\[(\d+)\]/);
1281
+ const rigId = m ? candidatePhysics[Number(m[1])]?.id : void 0;
1282
+ if (rigId !== void 0) {
1283
+ throw new IkiFormatError(
1284
+ e.message.replace(/^physics\[\d+\]/, `physics."${rigId}"`)
1285
+ );
1286
+ }
1287
+ }
1288
+ throw e;
1289
+ }
1290
+ }
1291
+ var AddPhysicsRig = class {
1292
+ label = "Add physics rig";
1293
+ rig;
1294
+ captured = false;
1295
+ prevPhysicsAbsent = false;
1296
+ constructor(rig) {
1297
+ this.rig = structuredClone(rig);
1298
+ }
1299
+ apply(doc) {
1300
+ const model = doc.getModel();
1301
+ if ((model.physics ?? []).some((r) => r.id === this.rig.id)) {
1302
+ throw new Error(
1303
+ `physics: id "${this.rig.id}" collides with an existing physics rig id`
1304
+ );
1305
+ }
1306
+ validatePhysicsCandidate(doc, [
1307
+ ...model.physics ?? [],
1308
+ structuredClone(this.rig)
1309
+ ]);
1310
+ if (!this.captured) {
1311
+ this.prevPhysicsAbsent = model.physics === void 0;
1312
+ this.captured = true;
1313
+ }
1314
+ if (model.physics === void 0) {
1315
+ model.physics = [];
1316
+ }
1317
+ model.physics.push(structuredClone(this.rig));
1318
+ }
1319
+ invert(doc) {
1320
+ const model = doc.getModel();
1321
+ const arr = model.physics;
1322
+ if (!arr) return;
1323
+ const i = arr.findIndex((r) => r.id === this.rig.id);
1324
+ if (i !== -1) arr.splice(i, 1);
1325
+ if (this.prevPhysicsAbsent && arr.length === 0) {
1326
+ delete model.physics;
1327
+ }
1328
+ }
1329
+ };
1330
+ var DeletePhysicsRig = class {
1331
+ constructor(rigId) {
1332
+ this.rigId = rigId;
1333
+ }
1334
+ rigId;
1335
+ label = "Delete physics rig";
1336
+ captured = false;
1337
+ removed;
1338
+ index;
1339
+ apply(doc) {
1340
+ const rig = doc.findPhysicsRig(this.rigId);
1341
+ const arr = doc.getModel().physics;
1342
+ const i = arr.indexOf(rig);
1343
+ if (!this.captured) {
1344
+ this.removed = structuredClone(rig);
1345
+ this.index = i;
1346
+ this.captured = true;
1347
+ }
1348
+ arr.splice(i, 1);
1349
+ if (arr.length === 0) {
1350
+ delete doc.getModel().physics;
1351
+ }
1352
+ }
1353
+ invert(doc) {
1354
+ const model = doc.getModel();
1355
+ (model.physics ??= []).splice(this.index, 0, structuredClone(this.removed));
1356
+ }
1357
+ };
1358
+ var SetPhysicsRig = class {
1359
+ constructor(rigId, rig) {
1360
+ this.rigId = rigId;
1361
+ this.rig = structuredClone(rig);
1362
+ }
1363
+ rigId;
1364
+ label = "Set physics rig";
1365
+ rig;
1366
+ captured = false;
1367
+ prevRig;
1368
+ apply(doc) {
1369
+ if (this.rig.id !== this.rigId) {
1370
+ throw new Error(
1371
+ `physics."${this.rigId}": cannot change rig id to "${this.rig.id}" (rename unsupported)`
1372
+ );
1373
+ }
1374
+ const model = doc.getModel();
1375
+ const i = (model.physics ?? []).findIndex((r) => r.id === this.rigId);
1376
+ if (i === -1) {
1377
+ throw new Error(`physics: no physics rig with id "${this.rigId}"`);
1378
+ }
1379
+ const candidate = model.physics.map(
1380
+ (r, idx) => idx === i ? structuredClone(this.rig) : r
1381
+ );
1382
+ validatePhysicsCandidate(doc, candidate);
1383
+ if (!this.captured) {
1384
+ this.prevRig = structuredClone(model.physics[i]);
1385
+ this.captured = true;
1386
+ }
1387
+ model.physics[i] = structuredClone(this.rig);
1388
+ }
1389
+ invert(doc) {
1390
+ const arr = doc.getModel().physics;
1391
+ if (!arr) return;
1392
+ const i = arr.findIndex((r) => r.id === this.rigId);
1393
+ if (i !== -1) arr[i] = structuredClone(this.prevRig);
1394
+ }
1395
+ };
1396
+
1397
+ // src/alpha-bbox.ts
1398
+ var ALPHA_BBOX_THRESHOLD = 8;
1399
+ function detectAlphaBbox(rgba, width, height) {
1400
+ let minX = width;
1401
+ let maxX = -1;
1402
+ let minY = height;
1403
+ let maxY = -1;
1404
+ for (let y3 = 0; y3 < height; y3++) {
1405
+ for (let x3 = 0; x3 < width; x3++) {
1406
+ const alpha = rgba[(y3 * width + x3) * 4 + 3];
1407
+ if (alpha >= ALPHA_BBOX_THRESHOLD) {
1408
+ if (x3 < minX) minX = x3;
1409
+ if (x3 > maxX) maxX = x3;
1410
+ if (y3 < minY) minY = y3;
1411
+ if (y3 > maxY) maxY = y3;
1412
+ }
1413
+ }
1414
+ }
1415
+ if (maxX === -1) return null;
1416
+ const x = Math.max(0, minX - 1);
1417
+ const y = Math.max(0, minY - 1);
1418
+ const x2 = Math.min(width - 1, maxX + 1);
1419
+ const y2 = Math.min(height - 1, maxY + 1);
1420
+ return { x, y, w: x2 - x + 1, h: y2 - y + 1 };
1421
+ }
1422
+
1423
+ // src/binding-capture.ts
1424
+ function captureBindingEndpoint(channel, restValue, posedValue) {
1425
+ if (channel === "opacity") {
1426
+ return restValue === 0 ? 0 : posedValue / restValue;
1427
+ }
1428
+ return posedValue - restValue;
1429
+ }
1430
+
1431
+ // src/atlas.ts
1432
+ var ATLAS_PADDING = 2;
1433
+ var UV_INSET_PX = 0.5;
1434
+ function packAtlas(sources, padding = ATLAS_PADDING) {
1435
+ for (const src of sources) {
1436
+ if (!isFinite(src.width) || src.width <= 0) {
1437
+ throw new Error(
1438
+ `packAtlas: source "${src.id}" has invalid width ${src.width}`
1439
+ );
1440
+ }
1441
+ if (!isFinite(src.height) || src.height <= 0) {
1442
+ throw new Error(
1443
+ `packAtlas: source "${src.id}" has invalid height ${src.height}`
1444
+ );
1445
+ }
1446
+ }
1447
+ if (sources.length === 0) {
1448
+ return { pageWidth: 0, pageHeight: 0, placements: [], padding };
1449
+ }
1450
+ const sorted = sources.slice().sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1451
+ const totalArea = sorted.reduce(
1452
+ (sum, s) => sum + (s.width + padding) * (s.height + padding),
1453
+ 0
1454
+ );
1455
+ const targetWidth = Math.max(
1456
+ Math.ceil(Math.sqrt(totalArea)),
1457
+ // Ensure at least the widest single source fits.
1458
+ Math.max(...sorted.map((s) => s.width + padding))
1459
+ );
1460
+ const placements = [];
1461
+ let shelfX = 0;
1462
+ let shelfY = 0;
1463
+ let shelfHeight = 0;
1464
+ for (const src of sorted) {
1465
+ const paddedW = src.width + padding;
1466
+ const paddedH = src.height + padding;
1467
+ if (shelfX > 0 && shelfX + paddedW > targetWidth) {
1468
+ shelfY += shelfHeight;
1469
+ shelfX = 0;
1470
+ shelfHeight = 0;
1471
+ }
1472
+ placements.push({
1473
+ id: src.id,
1474
+ x: shelfX,
1475
+ y: shelfY,
1476
+ width: src.width,
1477
+ height: src.height
1478
+ });
1479
+ shelfX += paddedW;
1480
+ if (paddedH > shelfHeight) shelfHeight = paddedH;
1481
+ }
1482
+ let pageWidth = 0;
1483
+ let pageHeight = 0;
1484
+ for (const p of placements) {
1485
+ const right = p.x + p.width + padding;
1486
+ const bottom = p.y + p.height + padding;
1487
+ if (right > pageWidth) pageWidth = right;
1488
+ if (bottom > pageHeight) pageHeight = bottom;
1489
+ }
1490
+ return { pageWidth, pageHeight, placements, padding };
1491
+ }
1492
+ function uvRectFor(placement, page, insetPx = UV_INSET_PX) {
1493
+ const px = placement.x + insetPx;
1494
+ const py = placement.y + insetPx;
1495
+ const pw = placement.width - insetPx * 2;
1496
+ const ph = placement.height - insetPx * 2;
1497
+ const x = Math.max(0, px / page.width);
1498
+ const y = Math.max(0, py / page.height);
1499
+ const width = Math.min(1 - x, Math.max(0, pw / page.width));
1500
+ const height = Math.min(1 - y, Math.max(0, ph / page.height));
1501
+ return { x, y, width, height };
1502
+ }
1503
+
1504
+ // src/factories.ts
1505
+ function generateUniqueId(model, base) {
1506
+ const used = /* @__PURE__ */ new Set();
1507
+ for (const p of model.parts) {
1508
+ used.add(p.id);
1509
+ }
1510
+ for (const d of model.deformers ?? []) {
1511
+ used.add(d.id);
1512
+ }
1513
+ if (!used.has(base)) return base;
1514
+ let n = 2;
1515
+ while (true) {
1516
+ const candidate = `${base}_${n}`;
1517
+ if (!used.has(candidate)) return candidate;
1518
+ n++;
1519
+ }
1520
+ }
1521
+ function generateRegularGridPoints(cols, rows, minX, maxX, minY, maxY) {
1522
+ const pts = [];
1523
+ for (let row = 0; row <= rows; row++) {
1524
+ const t = row / rows;
1525
+ const y = maxY - t * (maxY - minY);
1526
+ for (let col = 0; col <= cols; col++) {
1527
+ const s = col / cols;
1528
+ const x = minX + s * (maxX - minX);
1529
+ pts.push(x, y);
1530
+ }
1531
+ }
1532
+ return pts;
1533
+ }
1534
+ function createDefaultPart(model) {
1535
+ const order = model.parts.length ? Math.max(...model.parts.map((p) => p.order)) + 1 : 0;
1536
+ return {
1537
+ id: generateUniqueId(model, "part"),
1538
+ color: [0.45, 0.6, 0.85, 1],
1539
+ width: 150,
1540
+ height: 150,
1541
+ transform: { x: 0, y: 0 },
1542
+ order
1543
+ };
1544
+ }
1545
+ function createDefaultMatrixDeformer(model) {
1546
+ return {
1547
+ id: generateUniqueId(model, "deformer"),
1548
+ pivot: { x: 0, y: 0 }
1549
+ };
1550
+ }
1551
+ function createGridMesh(cols, rows) {
1552
+ if (!Number.isInteger(cols) || cols < 1 || !Number.isInteger(rows) || rows < 1 || (cols + 1) * (rows + 1) > 65536) {
1553
+ throw new Error(
1554
+ "createGridMesh: cols and rows must be integers >= 1 with (cols+1)*(rows+1) <= 65536"
1555
+ );
1556
+ }
1557
+ const colVerts = cols + 1;
1558
+ const rowVerts = rows + 1;
1559
+ const vertices = [];
1560
+ const uvs = [];
1561
+ for (let row = 0; row < rowVerts; row++) {
1562
+ const t = row / rows;
1563
+ const y = 0.5 - t;
1564
+ const v = t;
1565
+ for (let col = 0; col < colVerts; col++) {
1566
+ const s = col / cols;
1567
+ const x = -0.5 + s;
1568
+ const u = s;
1569
+ vertices.push(x, y);
1570
+ uvs.push(u, v);
1571
+ }
1572
+ }
1573
+ const indices = [];
1574
+ for (let row = 0; row < rows; row++) {
1575
+ for (let col = 0; col < cols; col++) {
1576
+ const tl = row * colVerts + col;
1577
+ const tr = row * colVerts + col + 1;
1578
+ const bl = (row + 1) * colVerts + col;
1579
+ const br = (row + 1) * colVerts + col + 1;
1580
+ indices.push(bl, br, tl);
1581
+ indices.push(tl, br, tr);
1582
+ }
1583
+ }
1584
+ return { vertices, uvs, indices };
1585
+ }
1586
+ function createDefaultWarpDeformer(model) {
1587
+ const hx = model.canvas.width / 4;
1588
+ const hy = model.canvas.height / 4;
1589
+ return {
1590
+ kind: "warp",
1591
+ id: generateUniqueId(model, "warp"),
1592
+ grid: {
1593
+ cols: 4,
1594
+ rows: 4,
1595
+ points: generateRegularGridPoints(4, 4, -hx, hx, -hy, hy)
1596
+ }
1597
+ };
1598
+ }
1599
+
1600
+ // src/auto-rig.ts
1601
+ import {
1602
+ IKI_FORMAT_VERSION as IKI_FORMAT_VERSION2,
1603
+ StandardParameter,
1604
+ parseIkiModel as parseIkiModel3
1605
+ } from "@ikijs/format";
1606
+ var ROLE_TABLE = {
1607
+ // hair_back stays rigid (silhouette behind the face); per-layer front/back
1608
+ // depth parallax is a later slice.
1609
+ hair_back: { deformer: "headDeformer", order: 0, mesh: false },
1610
+ face: { deformer: "faceWarp", order: 10, mesh: true },
1611
+ nose: { deformer: "faceWarp", order: 15, mesh: true },
1612
+ blush_L: { deformer: "faceWarp", order: 20, mesh: true },
1613
+ blush_R: { deformer: "faceWarp", order: 20, mesh: true },
1614
+ mouth: { deformer: "faceWarp", order: 25, mesh: true },
1615
+ eye_L: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "L" },
1616
+ eye_R: { deformer: "faceWarp", order: 30, mesh: true, eyeSide: "R" },
1617
+ iris_L: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "L" },
1618
+ iris_R: { deformer: "faceWarp", order: 31, mesh: true, eyeSide: "R" },
1619
+ pupil_L: { deformer: "faceWarp", order: 32, mesh: true, eyeSide: "L" },
1620
+ pupil_R: { deformer: "faceWarp", order: 32, mesh: true, eyeSide: "R" },
1621
+ highlight_L: { deformer: "faceWarp", order: 33, mesh: true, eyeSide: "L" },
1622
+ highlight_R: { deformer: "faceWarp", order: 33, mesh: true, eyeSide: "R" },
1623
+ // Upper lashes: an OPTIONAL separate layer ABOVE the iris that folds down to
1624
+ // the closed-eye seam (the same crease the white folds to), covering the cut
1625
+ // eyeball cleanly. When absent, the white's own fold is the only closed line.
1626
+ lash_L: { deformer: "faceWarp", order: 34, mesh: true, eyeSide: "L" },
1627
+ lash_R: { deformer: "faceWarp", order: 34, mesh: true, eyeSide: "R" },
1628
+ brow_L: { deformer: "faceWarp", order: 40, mesh: true },
1629
+ brow_R: { deformer: "faceWarp", order: 40, mesh: true },
1630
+ // Front hair rides faceWarp (mesh) so it follows the head-turn curvature with
1631
+ // the face instead of detaching as a rigid blob; its bbox joins the faceWarp
1632
+ // grid union so the grid covers it.
1633
+ hair_front: { deformer: "faceWarp", order: 50, mesh: true }
1634
+ };
1635
+ var REQUIRED_ROLES = ["face", "eye_L", "eye_R", "mouth"];
1636
+ var ALIAS_MAP = {
1637
+ eyebrow_L: "brow_L",
1638
+ eyebrow_R: "brow_R",
1639
+ eye_white_L: "eye_L",
1640
+ eye_white_R: "eye_R"
1641
+ };
1642
+ function normalizeRole(raw) {
1643
+ const noExt = raw.replace(/\.[^.]+$/, "");
1644
+ const collapsed = noExt.toLowerCase().replace(/[-\s]+/g, "_");
1645
+ const sided = collapsed.replace(
1646
+ /_([lr])$/,
1647
+ (_, s) => `_${s.toUpperCase()}`
1648
+ );
1649
+ return ALIAS_MAP[sided] ?? sided;
1650
+ }
1651
+ function assertRoleSet(roles) {
1652
+ const seen = /* @__PURE__ */ new Set();
1653
+ for (const role of roles) {
1654
+ if (!(role in ROLE_TABLE)) {
1655
+ throw new Error(`auto-rig: unknown role "${role}"`);
1656
+ }
1657
+ if (seen.has(role)) {
1658
+ throw new Error(`auto-rig: duplicate role "${role}"`);
1659
+ }
1660
+ seen.add(role);
1661
+ }
1662
+ for (const required of REQUIRED_ROLES) {
1663
+ if (!seen.has(required)) {
1664
+ throw new Error(`auto-rig: missing required role "${required}"`);
1665
+ }
1666
+ }
1667
+ }
1668
+ function parseLayerRoles(fileNames) {
1669
+ const pairs = fileNames.map((fileName) => {
1670
+ const role = normalizeRole(fileName);
1671
+ if (!(role in ROLE_TABLE)) {
1672
+ throw new Error(
1673
+ `auto-rig: unknown role "${role}" from file "${fileName}"`
1674
+ );
1675
+ }
1676
+ return { role, fileName };
1677
+ });
1678
+ assertRoleSet(pairs.map((p) => p.role));
1679
+ return pairs;
1680
+ }
1681
+ function bboxToTransform(bbox, canvasW, canvasH, partLabel) {
1682
+ if (bbox.w <= 0 || bbox.h <= 0) {
1683
+ throw new Error(`auto-rig: empty bbox for ${partLabel ?? "layer"}`);
1684
+ }
1685
+ const x = bbox.x + bbox.w / 2 - canvasW / 2;
1686
+ const y = canvasH / 2 - (bbox.y + bbox.h / 2);
1687
+ return { x, y };
1688
+ }
1689
+ function validateLayerInputs(layers, canvas) {
1690
+ if (layers.length === 0) {
1691
+ throw new Error("auto-rig: validateLayerInputs: layers must not be empty");
1692
+ }
1693
+ assertRoleSet(layers.map((l) => l.role));
1694
+ for (const layer of layers) {
1695
+ const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
1696
+ if (bbox.w <= 0) {
1697
+ throw new Error(
1698
+ `auto-rig: validateLayerInputs: role "${role}" has non-positive bbox.w (${bbox.w})`
1699
+ );
1700
+ }
1701
+ if (bbox.h <= 0) {
1702
+ throw new Error(
1703
+ `auto-rig: validateLayerInputs: role "${role}" has non-positive bbox.h (${bbox.h})`
1704
+ );
1705
+ }
1706
+ if (cropW <= 0) {
1707
+ throw new Error(
1708
+ `auto-rig: validateLayerInputs: role "${role}" has non-positive cropW (${cropW})`
1709
+ );
1710
+ }
1711
+ if (cropH <= 0) {
1712
+ throw new Error(
1713
+ `auto-rig: validateLayerInputs: role "${role}" has non-positive cropH (${cropH})`
1714
+ );
1715
+ }
1716
+ if (canvasW !== canvas.width || canvasH !== canvas.height) {
1717
+ throw new Error(
1718
+ `auto-rig: validateLayerInputs: role "${role}" canvas size (${canvasW}\xD7${canvasH}) does not match canvas arg (${canvas.width}\xD7${canvas.height})`
1719
+ );
1720
+ }
1721
+ }
1722
+ }
1723
+ function generateGridPoints(cols, rows, minX, maxX, minY, maxY) {
1724
+ const pts = [];
1725
+ for (let row = 0; row <= rows; row++) {
1726
+ const t = row / rows;
1727
+ const y = maxY - t * (maxY - minY);
1728
+ for (let col = 0; col <= cols; col++) {
1729
+ const s = col / cols;
1730
+ const x = minX + s * (maxX - minX);
1731
+ pts.push(x, y);
1732
+ }
1733
+ }
1734
+ return pts;
1735
+ }
1736
+ function createPixelGridMesh(cols, rows, w, h) {
1737
+ const colVerts = cols + 1;
1738
+ const rowVerts = rows + 1;
1739
+ const vertices = [];
1740
+ const uvs = [];
1741
+ for (let row = 0; row < rowVerts; row++) {
1742
+ const t = row / rows;
1743
+ const y = h / 2 - t * h;
1744
+ const v = t;
1745
+ for (let col = 0; col < colVerts; col++) {
1746
+ const s = col / cols;
1747
+ const x = -w / 2 + s * w;
1748
+ const u = s;
1749
+ vertices.push(x, y);
1750
+ uvs.push(u, v);
1751
+ }
1752
+ }
1753
+ const indices = [];
1754
+ for (let row = 0; row < rows; row++) {
1755
+ for (let col = 0; col < cols; col++) {
1756
+ const tl = row * colVerts + col;
1757
+ const tr = row * colVerts + col + 1;
1758
+ const bl = (row + 1) * colVerts + col;
1759
+ const br = (row + 1) * colVerts + col + 1;
1760
+ indices.push(bl, br, tl);
1761
+ indices.push(tl, br, tr);
1762
+ }
1763
+ }
1764
+ return { vertices, uvs, indices };
1765
+ }
1766
+ function bakeHeadTurnGridWarpCentered(grid, parameter, centerX) {
1767
+ const ANGLES = [-30, 0, 30];
1768
+ const halfWidth = (grid.points[grid.cols * 2] - grid.points[0]) / 2;
1769
+ const RADIUS = halfWidth * (0.6 / 0.5);
1770
+ const pointCount = grid.points.length / 2;
1771
+ const DEG_TO_RAD = Math.PI / 180;
1772
+ const keyforms = ANGLES.map((angleDeg) => {
1773
+ const theta = angleDeg * DEG_TO_RAD;
1774
+ const offsets = [];
1775
+ for (let i = 0; i < pointCount; i++) {
1776
+ const x = grid.points[i * 2];
1777
+ const localX = x - centerX;
1778
+ const alpha = Math.asin(Math.max(-1, Math.min(1, localX / RADIUS)));
1779
+ const xPrime = centerX + RADIUS * Math.sin(alpha + theta);
1780
+ const dx = xPrime - x;
1781
+ offsets.push(dx, 0);
1782
+ }
1783
+ return { value: angleDeg, offsets };
1784
+ });
1785
+ return { parameter, keyforms };
1786
+ }
1787
+ var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
1788
+ function bindingsForRole(spec, role, cropW, cropH) {
1789
+ const isEyeStack = EYE_STACK_PREFIXES.some((p) => role.startsWith(p));
1790
+ if (isEyeStack && spec.eyeSide !== void 0) {
1791
+ const isGazeRole = role.startsWith("iris_") || role.startsWith("pupil_") || role.startsWith("highlight_");
1792
+ if (!isGazeRole) return [];
1793
+ const gx = Math.min(cropW * 0.18, 22);
1794
+ const gy = Math.min(cropH * 0.18, 16);
1795
+ return [
1796
+ {
1797
+ parameter: StandardParameter.EyeballX,
1798
+ channel: "translateX",
1799
+ from: -gx,
1800
+ to: gx
1801
+ },
1802
+ {
1803
+ parameter: StandardParameter.EyeballY,
1804
+ channel: "translateY",
1805
+ from: -gy,
1806
+ to: gy
1807
+ }
1808
+ ];
1809
+ }
1810
+ if (role === "mouth") {
1811
+ return [
1812
+ // Mouth open: scaleY from 0 (closed, param=0) to 3 (wide open, param=1).
1813
+ {
1814
+ parameter: StandardParameter.MouthOpen,
1815
+ channel: "scaleY",
1816
+ from: 0,
1817
+ to: 3
1818
+ },
1819
+ // Mouth form: scaleX from -0.2 (pursed, param=-1) to 0.4 (wide, param=1).
1820
+ {
1821
+ parameter: StandardParameter.MouthForm,
1822
+ channel: "scaleX",
1823
+ from: -0.2,
1824
+ to: 0.4
1825
+ }
1826
+ ];
1827
+ }
1828
+ if (role === "brow_L" || role === "brow_R") {
1829
+ const ty = Math.min(cropH * 0.8, 18);
1830
+ const deg = 12;
1831
+ if (role === "brow_L") {
1832
+ return [
1833
+ {
1834
+ parameter: StandardParameter.BrowLeftY,
1835
+ channel: "translateY",
1836
+ from: -ty,
1837
+ to: ty
1838
+ },
1839
+ {
1840
+ parameter: StandardParameter.BrowLeftAngle,
1841
+ channel: "rotate",
1842
+ from: -deg,
1843
+ to: deg
1844
+ }
1845
+ ];
1846
+ } else {
1847
+ return [
1848
+ {
1849
+ parameter: StandardParameter.BrowRightY,
1850
+ channel: "translateY",
1851
+ from: -ty,
1852
+ to: ty
1853
+ },
1854
+ {
1855
+ parameter: StandardParameter.BrowRightAngle,
1856
+ channel: "rotate",
1857
+ from: -deg,
1858
+ to: deg
1859
+ }
1860
+ ];
1861
+ }
1862
+ }
1863
+ if (role === "hair_front") {
1864
+ return [
1865
+ {
1866
+ parameter: StandardParameter.HairSwayX,
1867
+ channel: "rotate",
1868
+ from: -8,
1869
+ to: 8
1870
+ },
1871
+ {
1872
+ parameter: StandardParameter.HairSwayX,
1873
+ channel: "translateX",
1874
+ from: -10,
1875
+ to: 10
1876
+ }
1877
+ ];
1878
+ }
1879
+ return [];
1880
+ }
1881
+ var EYELID_FOLD_CREASE = 0.15;
1882
+ var EYELID_FOLD_K = 0.04;
1883
+ var LASH_FOLD_K = 0.2;
1884
+ function bakeEyelidFoldWarp(mesh, parameter, creaseOffsetY, k) {
1885
+ const closed = [];
1886
+ const zeros = [];
1887
+ for (let i = 0; i < mesh.vertices.length; i += 2) {
1888
+ const vy = mesh.vertices[i + 1];
1889
+ closed.push(0, creaseOffsetY - (1 - k) * vy);
1890
+ zeros.push(0, 0);
1891
+ }
1892
+ return {
1893
+ parameter,
1894
+ keyforms: [
1895
+ { value: 0, offsets: closed },
1896
+ { value: 1, offsets: zeros }
1897
+ ]
1898
+ };
1899
+ }
1900
+ function generateIkiFromLayerSet(layers, canvas) {
1901
+ validateLayerInputs(layers, canvas);
1902
+ const hasHair = layers.some((l) => l.role === "hair_front");
1903
+ const parameters = [
1904
+ {
1905
+ id: StandardParameter.MouthOpen,
1906
+ name: "Mouth Open",
1907
+ min: 0,
1908
+ max: 1,
1909
+ default: 0
1910
+ },
1911
+ {
1912
+ id: StandardParameter.MouthForm,
1913
+ name: "Mouth Form",
1914
+ min: -1,
1915
+ max: 1,
1916
+ default: 0
1917
+ },
1918
+ {
1919
+ id: StandardParameter.EyeOpenLeft,
1920
+ name: "Eye L",
1921
+ min: 0,
1922
+ max: 1,
1923
+ default: 1
1924
+ },
1925
+ {
1926
+ id: StandardParameter.EyeOpenRight,
1927
+ name: "Eye R",
1928
+ min: 0,
1929
+ max: 1,
1930
+ default: 1
1931
+ },
1932
+ {
1933
+ id: StandardParameter.EyeballX,
1934
+ name: "Gaze X",
1935
+ min: -1,
1936
+ max: 1,
1937
+ default: 0
1938
+ },
1939
+ {
1940
+ id: StandardParameter.EyeballY,
1941
+ name: "Gaze Y",
1942
+ min: -1,
1943
+ max: 1,
1944
+ default: 0
1945
+ },
1946
+ {
1947
+ id: StandardParameter.AngleX,
1948
+ name: "Head Angle",
1949
+ min: -30,
1950
+ max: 30,
1951
+ default: 0
1952
+ },
1953
+ {
1954
+ id: StandardParameter.Breath,
1955
+ name: "Breath",
1956
+ min: 0,
1957
+ max: 1,
1958
+ default: 0
1959
+ },
1960
+ {
1961
+ id: StandardParameter.BrowLeftY,
1962
+ name: "Brow L Y",
1963
+ min: -1,
1964
+ max: 1,
1965
+ default: 0
1966
+ },
1967
+ {
1968
+ id: StandardParameter.BrowRightY,
1969
+ name: "Brow R Y",
1970
+ min: -1,
1971
+ max: 1,
1972
+ default: 0
1973
+ },
1974
+ {
1975
+ id: StandardParameter.BrowLeftAngle,
1976
+ name: "Brow L Angle",
1977
+ min: -1,
1978
+ max: 1,
1979
+ default: 0
1980
+ },
1981
+ {
1982
+ id: StandardParameter.BrowRightAngle,
1983
+ name: "Brow R Angle",
1984
+ min: -1,
1985
+ max: 1,
1986
+ default: 0
1987
+ }
1988
+ ];
1989
+ if (hasHair) {
1990
+ parameters.push({
1991
+ id: StandardParameter.HairSwayX,
1992
+ name: "Hair Sway X",
1993
+ min: -20,
1994
+ max: 20,
1995
+ default: 0
1996
+ });
1997
+ }
1998
+ const faceLayers = layers.filter((l) => l.role === "face");
1999
+ const faceLayer = faceLayers[0];
2000
+ const faceTransform = bboxToTransform(
2001
+ faceLayer.bbox,
2002
+ faceLayer.canvasW,
2003
+ faceLayer.canvasH,
2004
+ "face"
2005
+ );
2006
+ const faceCenterX = faceTransform.x;
2007
+ const faceCropH = faceLayer.cropH;
2008
+ const faceWarpLayers = layers.filter(
2009
+ (l) => ROLE_TABLE[l.role].deformer === "faceWarp"
2010
+ );
2011
+ let unionMinX = -canvas.width / 2;
2012
+ let unionMaxX = canvas.width / 2;
2013
+ let unionMinY = -canvas.height / 2;
2014
+ let unionMaxY = canvas.height / 2;
2015
+ if (faceWarpLayers.length > 0) {
2016
+ const transforms = faceWarpLayers.map(
2017
+ (l) => bboxToTransform(l.bbox, l.canvasW, l.canvasH, l.role)
2018
+ );
2019
+ unionMinX = Math.min(
2020
+ ...transforms.map((t, i) => t.x - faceWarpLayers[i].cropW / 2)
2021
+ );
2022
+ unionMaxX = Math.max(
2023
+ ...transforms.map((t, i) => t.x + faceWarpLayers[i].cropW / 2)
2024
+ );
2025
+ unionMinY = Math.min(
2026
+ ...transforms.map((t, i) => t.y - faceWarpLayers[i].cropH / 2)
2027
+ );
2028
+ unionMaxY = Math.max(
2029
+ ...transforms.map((t, i) => t.y + faceWarpLayers[i].cropH / 2)
2030
+ );
2031
+ const spanX = unionMaxX - unionMinX;
2032
+ const spanY = unionMaxY - unionMinY;
2033
+ const MARGIN = 0.12;
2034
+ unionMinX -= spanX * MARGIN;
2035
+ unionMaxX += spanX * MARGIN;
2036
+ unionMinY -= spanY * MARGIN;
2037
+ unionMaxY += spanY * MARGIN;
2038
+ }
2039
+ const halfW = Math.max(faceCenterX - unionMinX, unionMaxX - faceCenterX);
2040
+ const faceGridMinX = faceCenterX - halfW;
2041
+ const faceGridMaxX = faceCenterX + halfW;
2042
+ const faceGrid = {
2043
+ cols: 4,
2044
+ rows: 4,
2045
+ points: generateGridPoints(
2046
+ 4,
2047
+ 4,
2048
+ faceGridMinX,
2049
+ faceGridMaxX,
2050
+ unionMinY,
2051
+ unionMaxY
2052
+ )
2053
+ };
2054
+ const faceBottom = faceTransform.y - faceCropH / 2;
2055
+ const neckPivot = {
2056
+ x: faceCenterX,
2057
+ y: faceBottom - faceCropH * 0.15
2058
+ // 15% below face bottom = neck
2059
+ };
2060
+ const faceWarpBake = bakeHeadTurnGridWarpCentered(
2061
+ faceGrid,
2062
+ StandardParameter.AngleX,
2063
+ faceCenterX
2064
+ );
2065
+ const deformers = [
2066
+ // headDeformer: rigid matrix rotating/translating the whole head about the
2067
+ // neck pivot; bindings mirror sample-model.ts exactly.
2068
+ {
2069
+ id: "headDeformer",
2070
+ pivot: neckPivot,
2071
+ bindings: [
2072
+ {
2073
+ parameter: StandardParameter.AngleX,
2074
+ channel: "rotate",
2075
+ from: 6,
2076
+ to: -6
2077
+ },
2078
+ {
2079
+ parameter: StandardParameter.AngleX,
2080
+ channel: "translateX",
2081
+ from: -50,
2082
+ to: 50
2083
+ },
2084
+ {
2085
+ parameter: StandardParameter.Breath,
2086
+ channel: "translateY",
2087
+ from: 0,
2088
+ to: -12
2089
+ }
2090
+ ]
2091
+ },
2092
+ // faceWarp: cylinder-bend warp parented to headDeformer; grid is symmetric
2093
+ // about faceCenterX so the bake's cylinder axis aligns with the face center.
2094
+ {
2095
+ kind: "warp",
2096
+ id: "faceWarp",
2097
+ parent: "headDeformer",
2098
+ grid: faceGrid,
2099
+ warps: [faceWarpBake]
2100
+ }
2101
+ ];
2102
+ const eyeCreaseBySide = {};
2103
+ for (const layer of layers) {
2104
+ const side = ROLE_TABLE[layer.role].eyeSide;
2105
+ if ((layer.role === "eye_L" || layer.role === "eye_R") && side) {
2106
+ const ey = bboxToTransform(
2107
+ layer.bbox,
2108
+ layer.canvasW,
2109
+ layer.canvasH,
2110
+ layer.role
2111
+ ).y;
2112
+ eyeCreaseBySide[side] = ey - EYELID_FOLD_CREASE * layer.cropH;
2113
+ }
2114
+ }
2115
+ const parts = layers.map((layer) => {
2116
+ const { role, bbox, cropW, cropH, canvasW, canvasH } = layer;
2117
+ const spec = ROLE_TABLE[role];
2118
+ const t = bboxToTransform(bbox, canvasW, canvasH, role);
2119
+ const roleBindings = bindingsForRole(spec, role, cropW, cropH);
2120
+ if (spec.mesh) {
2121
+ const mesh = createPixelGridMesh(4, 4, cropW, cropH);
2122
+ const part = {
2123
+ id: role,
2124
+ color: [1, 1, 1, 1],
2125
+ width: 1,
2126
+ height: 1,
2127
+ order: spec.order,
2128
+ transform: t,
2129
+ deformer: spec.deformer,
2130
+ mesh
2131
+ };
2132
+ if (roleBindings.length > 0) {
2133
+ part.bindings = roleBindings;
2134
+ }
2135
+ if (spec.eyeSide !== void 0) {
2136
+ const isLash = role.startsWith("lash_");
2137
+ if (role.startsWith("eye_") || isLash) {
2138
+ const openParam = spec.eyeSide === "L" ? StandardParameter.EyeOpenLeft : StandardParameter.EyeOpenRight;
2139
+ const creaseWorldY = eyeCreaseBySide[spec.eyeSide] ?? t.y - EYELID_FOLD_CREASE * cropH;
2140
+ part.warps = [
2141
+ bakeEyelidFoldWarp(
2142
+ mesh,
2143
+ openParam,
2144
+ creaseWorldY - t.y,
2145
+ isLash ? LASH_FOLD_K : EYELID_FOLD_K
2146
+ )
2147
+ ];
2148
+ } else {
2149
+ part.clip = { masks: [`eye_${spec.eyeSide}`] };
2150
+ }
2151
+ }
2152
+ return part;
2153
+ } else {
2154
+ return {
2155
+ id: role,
2156
+ color: [1, 1, 1, 1],
2157
+ width: cropW,
2158
+ height: cropH,
2159
+ order: spec.order,
2160
+ transform: t,
2161
+ deformer: spec.deformer
2162
+ };
2163
+ }
2164
+ });
2165
+ const model = {
2166
+ version: IKI_FORMAT_VERSION2,
2167
+ name: "Auto-Rigged Model",
2168
+ canvas: { width: canvas.width, height: canvas.height },
2169
+ textures: [],
2170
+ parameters,
2171
+ deformers,
2172
+ parts,
2173
+ // Secondary motion: a spring lags AngleX onto HairSwayX so front hair sways
2174
+ // behind the head turn (same constants as the hand-authored sample). Omitted
2175
+ // when there is no front hair to drive.
2176
+ physics: hasHair ? [
2177
+ {
2178
+ id: "hairSway",
2179
+ input: { parameter: StandardParameter.AngleX, weight: 1 },
2180
+ output: { parameter: StandardParameter.HairSwayX, scale: -10 },
2181
+ mass: 1,
2182
+ stiffness: 80,
2183
+ damping: 10
2184
+ }
2185
+ ] : void 0
2186
+ };
2187
+ return parseIkiModel3(structuredClone(model));
2188
+ }
2189
+ export {
2190
+ ALPHA_BBOX_THRESHOLD,
2191
+ ATLAS_PADDING,
2192
+ AddDeformer,
2193
+ AddPart,
2194
+ AddPhysicsRig,
2195
+ CaptureGridKeyform,
2196
+ DeleteDeformer,
2197
+ DeletePart,
2198
+ DeletePhysicsRig,
2199
+ EditorDocument,
2200
+ SetDeformerBindings,
2201
+ SetDeformerParent,
2202
+ SetDeformerPivot,
2203
+ SetDeformerPivotX,
2204
+ SetDeformerPivotY,
2205
+ SetDeformerTransform,
2206
+ SetPartBindings,
2207
+ SetPartColor,
2208
+ SetPartDeformer,
2209
+ SetPartHeight,
2210
+ SetPartMesh,
2211
+ SetPartOrder,
2212
+ SetPartTransform,
2213
+ SetPartWidth,
2214
+ SetPhysicsRig,
2215
+ UV_INSET_PX,
2216
+ captureBindingEndpoint,
2217
+ computeGridOffsets,
2218
+ createDefaultMatrixDeformer,
2219
+ createDefaultPart,
2220
+ createDefaultWarpDeformer,
2221
+ createGridMesh,
2222
+ detectAlphaBbox,
2223
+ generateIkiFromLayerSet,
2224
+ interpolateGridOffsets,
2225
+ packAtlas,
2226
+ parseLayerRoles,
2227
+ upsertGridKeyform,
2228
+ uvRectFor,
2229
+ validateDeformerDelete,
2230
+ validateDeformerReparent,
2231
+ validatePartAttach
2232
+ };
2233
+ //# sourceMappingURL=index.mjs.map