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