@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.
@@ -0,0 +1,916 @@
1
+ import { IkiDeformer, IkiPart, IkiPhysics, IkiDeformerBinding, IkiMatrixDeformer, IkiBinding, IkiMesh, IkiModel, IkiWarpDeformer, IkiTexture, IkiUvRect, IkiTransform, IkiDeformerTransform, IkiTransformChannel, IkiGridKeyform, IkiPhysicsChain } from '@ikijs/format';
2
+
3
+ /**
4
+ * One invertible edit. The document is passed IN at apply/invert time — a
5
+ * command is constructed from `(partId, value)` alone, before any document
6
+ * exists — so the same command object can be applied, inverted, and re-applied
7
+ * by the undo/redo stack.
8
+ *
9
+ * Prior-value capture happens exactly ONCE, on the first {@link apply}. `redo`
10
+ * (a second `apply`) reuses that captured value rather than re-reading the
11
+ * current field, so undo always restores the original target.
12
+ */
13
+ interface EditCommand {
14
+ apply(doc: EditorDocument): void;
15
+ invert(doc: EditorDocument): void;
16
+ readonly label: string;
17
+ }
18
+ /** Channels of {@link IkiTransform} this editor can edit (object-field names,
19
+ * NOT the binding `IkiTransformChannel` vocabulary). */
20
+ type EditTransformChannel = "x" | "y" | "rotation" | "scaleX" | "scaleY" | "opacity";
21
+ /**
22
+ * Generic single-field command: reads/writes one field of the resolved part via
23
+ * a getter/setter closure, capturing the prior value on the first `apply` and
24
+ * restoring it on `invert`. `T` is the captured value's type; for cloned values
25
+ * (the color tuple) the getter/setter perform the copy.
26
+ */
27
+ declare class FieldCommand<T> implements EditCommand {
28
+ private readonly partId;
29
+ private readonly newValue;
30
+ private readonly get;
31
+ private readonly set;
32
+ readonly label: string;
33
+ private captured;
34
+ private prevValue;
35
+ constructor(partId: string, newValue: T, label: string, get: (part: IkiPart) => T, set: (part: IkiPart, value: T) => void);
36
+ apply(doc: EditorDocument): void;
37
+ invert(doc: EditorDocument): void;
38
+ }
39
+ /** Edit a part's RGBA fill. The 4-tuple is mutable, so the command clones on
40
+ * construction (caller's array), on capture (part's current color), and on
41
+ * assign (writing to the part) — it never retains the caller's or model's
42
+ * array by reference. */
43
+ declare class SetPartColor extends FieldCommand<[
44
+ number,
45
+ number,
46
+ number,
47
+ number
48
+ ]> {
49
+ constructor(partId: string, rgba: [number, number, number, number]);
50
+ }
51
+ /** Edit a part's width (model-space units). */
52
+ declare class SetPartWidth extends FieldCommand<number> {
53
+ constructor(partId: string, value: number);
54
+ }
55
+ /** Edit a part's height (model-space units). */
56
+ declare class SetPartHeight extends FieldCommand<number> {
57
+ constructor(partId: string, value: number);
58
+ }
59
+ /** Edit a part's paint order. */
60
+ declare class SetPartOrder extends FieldCommand<number> {
61
+ constructor(partId: string, value: number);
62
+ }
63
+ /**
64
+ * Edit one channel of a part's base transform. `x`/`y` are required; the rest
65
+ * are optional and may be absent on the part. For an optional channel the
66
+ * command captures the raw current value INCLUDING `undefined`, and restoring
67
+ * `undefined` DELETES the key so undo returns the part to its original
68
+ * (possibly-omitted) shape. Engine defaults (rotation 0 / scale 1 / opacity 1)
69
+ * are NOT substituted here.
70
+ */
71
+ declare class SetPartTransform extends FieldCommand<number | undefined> {
72
+ constructor(partId: string, channel: EditTransformChannel, value: number);
73
+ }
74
+ /**
75
+ * Capture the warp deformer's grid as one keyform at the driving parameter
76
+ * `value`, upserting `offsets` into `warps[0].keyforms`. The 4-tuple-style
77
+ * mutable `offsets` array is cloned on construction so a later caller mutation
78
+ * cannot corrupt apply/redo (mirrors {@link SetPartColor}).
79
+ *
80
+ * `apply` validates BEFORE mutating: the deformer must have a grid warp, the
81
+ * offsets length must equal `grid.points.length`, and `value` must lie within
82
+ * the driving parameter's declared `[min,max]` (fail fast, before
83
+ * `parseIkiModel` would reject it). Prior keyforms are deep-copied once on the
84
+ * first `apply` (capture-once like {@link FieldCommand}); `invert` restores
85
+ * that deep copy so re-apply after undo never aliases.
86
+ */
87
+ declare class CaptureGridKeyform implements EditCommand {
88
+ private readonly deformerId;
89
+ private readonly value;
90
+ readonly label = "Capture grid keyform";
91
+ private readonly offsets;
92
+ private captured;
93
+ private prevKeyforms;
94
+ constructor(deformerId: string, value: number, offsets: number[]);
95
+ apply(doc: EditorDocument): void;
96
+ invert(doc: EditorDocument): void;
97
+ }
98
+ /**
99
+ * Generic single-field command targeting a matrix deformer, mirroring
100
+ * {@link FieldCommand} but resolving via `doc.findMatrixDeformer` instead of
101
+ * `doc.findPart`. Capture-once on first `apply`; restore on `invert`.
102
+ */
103
+ declare class DeformerFieldCommand<T> implements EditCommand {
104
+ private readonly deformerId;
105
+ private readonly newValue;
106
+ private readonly get;
107
+ private readonly set;
108
+ readonly label: string;
109
+ private captured;
110
+ private prevValue;
111
+ constructor(deformerId: string, newValue: T, label: string, get: (deformer: IkiMatrixDeformer) => T, set: (deformer: IkiMatrixDeformer, value: T) => void);
112
+ apply(doc: EditorDocument): void;
113
+ invert(doc: EditorDocument): void;
114
+ }
115
+ /** Edit a matrix deformer's pivot x. */
116
+ declare class SetDeformerPivotX extends DeformerFieldCommand<number> {
117
+ constructor(deformerId: string, value: number);
118
+ }
119
+ /** Edit a matrix deformer's pivot y. */
120
+ declare class SetDeformerPivotY extends DeformerFieldCommand<number> {
121
+ constructor(deformerId: string, value: number);
122
+ }
123
+ /**
124
+ * Set a matrix deformer's pivot x and y atomically (one drag = one undo step).
125
+ * {@link SetDeformerPivotX} and {@link SetDeformerPivotY} remain for the
126
+ * Inspector's single-axis number inputs.
127
+ */
128
+ declare class SetDeformerPivot implements EditCommand {
129
+ private readonly deformerId;
130
+ readonly label = "Set pivot";
131
+ private captured;
132
+ private prevPivot;
133
+ private readonly pivot;
134
+ constructor(deformerId: string, pivot: {
135
+ x: number;
136
+ y: number;
137
+ });
138
+ apply(doc: EditorDocument): void;
139
+ invert(doc: EditorDocument): void;
140
+ }
141
+ /** Channels of {@link IkiDeformerTransform} this editor can edit. */
142
+ type DeformerTransformChannel = "x" | "y" | "rotation" | "scaleX" | "scaleY";
143
+ /**
144
+ * Edit one channel of a matrix deformer's base transform. Because
145
+ * {@link IkiDeformerTransform} REQUIRES finite `x` and `y`, this command
146
+ * captures and restores the WHOLE prior `transform` object (present or absent)
147
+ * rather than a single channel, so undo can delete the transform when it did
148
+ * not previously exist, and redo never produces a partial object missing `x`/`y`.
149
+ *
150
+ * When no `transform` is present on the deformer, `apply` creates one from
151
+ * the identity base `{ x: 0, y: 0 }` — the minimal valid shape the validator
152
+ * accepts — then writes the edited channel. For example, editing `rotation`
153
+ * on a transform-less deformer yields `{ x: 0, y: 0, rotation: <value> }`.
154
+ */
155
+ declare class SetDeformerTransform implements EditCommand {
156
+ private readonly deformerId;
157
+ private readonly channel;
158
+ private readonly value;
159
+ readonly label = "Set deformer transform";
160
+ private captured;
161
+ private prevTransform;
162
+ constructor(deformerId: string, channel: DeformerTransformChannel, value: number);
163
+ apply(doc: EditorDocument): void;
164
+ invert(doc: EditorDocument): void;
165
+ }
166
+ /**
167
+ * Replace a matrix deformer's `bindings` array wholesale. A single command
168
+ * covers add, edit, and remove (pass the desired final array; pass `[]` to
169
+ * remove all). Mirrors {@link CaptureGridKeyform}'s deep-copy discipline:
170
+ * clone-on-construction, capture-once, fresh deep copy on invert.
171
+ *
172
+ * Each {@link IkiDeformerBinding} is a flat object so a per-element spread
173
+ * `{ ...b }` is a sufficient deep copy.
174
+ */
175
+ declare class SetDeformerBindings implements EditCommand {
176
+ private readonly deformerId;
177
+ readonly label = "Set deformer bindings";
178
+ private readonly bindings;
179
+ private captured;
180
+ private prevBindings;
181
+ constructor(deformerId: string, bindings: IkiDeformerBinding[]);
182
+ apply(doc: EditorDocument): void;
183
+ invert(doc: EditorDocument): void;
184
+ }
185
+ /**
186
+ * Reparent a deformer (matrix or warp) under a new parent, or promote it to
187
+ * root (`newParentId === undefined`). Calls {@link validateDeformerReparent}
188
+ * FIRST so invalid reparents (cycles, warp parent, unknown id) throw before any
189
+ * capture or mutation — a throwing apply leaves the model and undo stack
190
+ * untouched.
191
+ *
192
+ * Absent-vs-present distinction: captures both the prior `parent` value AND
193
+ * whether the key was present on the object, so `invert` can delete the key
194
+ * (restore "absent") rather than blindly assigning `undefined`.
195
+ */
196
+ declare class SetDeformerParent implements EditCommand {
197
+ private readonly deformerId;
198
+ private readonly newParentId;
199
+ readonly label = "Set deformer parent";
200
+ private captured;
201
+ private prevParent;
202
+ private prevHadParent;
203
+ constructor(deformerId: string, newParentId: string | undefined);
204
+ apply(doc: EditorDocument): void;
205
+ invert(doc: EditorDocument): void;
206
+ }
207
+ /**
208
+ * Add a new part to the model. Validates the candidate model with
209
+ * {@link parseIkiModel} BEFORE mutating, so a structurally invalid part
210
+ * (bad color tuple, missing required fields, id collision with a deformer)
211
+ * throws an `IkiFormatError` and leaves the model untouched.
212
+ *
213
+ * Captures the prior base-UV entry for the part's id on the FIRST apply so
214
+ * invert can restore the exact pre-add state. This guards the id-reuse hazard:
215
+ * DeletePart(X) → AddPart(X′, different mesh) → undo(add) → undo(delete) —
216
+ * without restore, X's constructor-captured base would be gone, causing
217
+ * applyAtlas to fail when X is restored by the undo of the delete.
218
+ */
219
+ declare class AddPart implements EditCommand {
220
+ readonly label = "Add part";
221
+ private readonly part;
222
+ private captured;
223
+ private prevBaseMeshUvs;
224
+ constructor(part: IkiPart);
225
+ apply(doc: EditorDocument): void;
226
+ invert(doc: EditorDocument): void;
227
+ }
228
+ /**
229
+ * Add a new deformer (matrix or warp) to the model. Validates the candidate
230
+ * model with {@link parseIkiModel} BEFORE mutating — this enforces the warp
231
+ * rest-grid invariant, points length, pivot, parent, and bindings without
232
+ * hand-rolling partial checks. Mirrors {@link AddPart} but also tracks whether
233
+ * `model.deformers` was absent before the first apply, so `invert` can restore
234
+ * the exact key-absence state (mirrors {@link SetDeformerBindings}).
235
+ */
236
+ declare class AddDeformer implements EditCommand {
237
+ readonly label = "Add deformer";
238
+ private readonly deformer;
239
+ private captured;
240
+ private prevDeformersAbsent;
241
+ constructor(deformer: IkiDeformer);
242
+ apply(doc: EditorDocument): void;
243
+ invert(doc: EditorDocument): void;
244
+ }
245
+ /**
246
+ * Delete a part by id, preserving its original array slot so `invert` restores
247
+ * it at the same position. Slot position is cosmetic (the renderer uses the
248
+ * `order` field for paint ordering), but restoring the index keeps undo
249
+ * visually predictable.
250
+ *
251
+ * No `parseIkiModel` pre-check is needed: removing an element from an already-
252
+ * valid model cannot introduce a structural violation — EXCEPT for clip-mask
253
+ * references, the one part→part reference in the contract. `apply` refuses to
254
+ * delete a part still used as another part's `clip.masks` entry (guard below),
255
+ * so it can never leave a dangling mask ref that would fail `toIkiModel()`.
256
+ *
257
+ * Texture-reference safety (package invariant): `apply` refuses to delete a
258
+ * part that still carries `part.texture`. Texture/atlas state is non-undoable
259
+ * per the 5b boundary; clear the texture first via
260
+ * {@link EditorDocument.clearPartTextureRef} (model-committed) or
261
+ * {@link EditorDocument.applyAtlas} with no assignment (imported) — both are
262
+ * non-undoable. By the time a part is deletable it carries no texture, so
263
+ * `invert` can never restore a stale texture index that would render the wrong
264
+ * atlas region after a later atlas repack. No transactional atlas capture is
265
+ * needed in this command.
266
+ */
267
+ declare class DeletePart implements EditCommand {
268
+ private readonly partId;
269
+ readonly label = "Delete part";
270
+ private captured;
271
+ private removed;
272
+ private index;
273
+ constructor(partId: string);
274
+ apply(doc: EditorDocument): void;
275
+ invert(doc: EditorDocument): void;
276
+ }
277
+ /**
278
+ * Delete a deformer by id. Calls {@link validateDeformerDelete} FIRST so the
279
+ * delete is refused while anything still references it — a child deformer, an
280
+ * attached part, or a physics chain anchored to it — enforcing the same
281
+ * referential safety as {@link SetDeformerParent} and {@link SetPartDeformer}.
282
+ *
283
+ * `invert` re-inserts the deformer at its original index.
284
+ */
285
+ declare class DeleteDeformer implements EditCommand {
286
+ private readonly deformerId;
287
+ readonly label = "Delete deformer";
288
+ private captured;
289
+ private removed;
290
+ private index;
291
+ constructor(deformerId: string);
292
+ apply(doc: EditorDocument): void;
293
+ invert(doc: EditorDocument): void;
294
+ }
295
+ /**
296
+ * Replace a part's `bindings` array wholesale. A single command covers add,
297
+ * edit, and remove (pass the desired final array; pass `[]` to remove all).
298
+ * Mirrors {@link SetDeformerBindings}'s deep-copy and absent-vs-empty discipline:
299
+ * clone-on-construction, capture-once, fresh deep copy on every assign/invert.
300
+ *
301
+ * Each {@link IkiBinding} is a flat object, so a per-element spread `{ ...b }`
302
+ * is a sufficient deep copy.
303
+ *
304
+ * Validates the WRITTEN bindings against the declared parameters via a narrow
305
+ * synthetic {@link parseIkiModel} candidate (so unrelated in-flight invalid
306
+ * editor state — e.g. a NaN width on another part — cannot false-reject a
307
+ * binding edit). Validation runs BEFORE any mutation; on failure the model and
308
+ * undo stack are left untouched.
309
+ *
310
+ * Empty bindings → omit the `bindings` key on the candidate AND delete
311
+ * `part.bindings` on apply (keeps the model shape minimal; represents "no
312
+ * bindings" as an absent key rather than an empty array).
313
+ */
314
+ declare class SetPartBindings implements EditCommand {
315
+ private readonly partId;
316
+ readonly label = "Set part bindings";
317
+ private readonly bindings;
318
+ private captured;
319
+ private prevBindings;
320
+ constructor(partId: string, bindings: IkiBinding[]);
321
+ apply(doc: EditorDocument): void;
322
+ invert(doc: EditorDocument): void;
323
+ }
324
+ /**
325
+ * Attach a part to a deformer, or detach it (`newDeformerId === undefined`).
326
+ * Calls {@link validatePartAttach} FIRST so invalid attachments (warp without
327
+ * mesh, unknown ids) throw before any capture or mutation.
328
+ *
329
+ * Absent-vs-present distinction mirrors {@link SetDeformerParent}: captures
330
+ * both the prior `deformer` value and whether the key was present, so `invert`
331
+ * can delete the key rather than assigning `undefined`.
332
+ */
333
+ declare class SetPartDeformer implements EditCommand {
334
+ private readonly partId;
335
+ private readonly newDeformerId;
336
+ readonly label = "Set part deformer";
337
+ private captured;
338
+ private prevDeformer;
339
+ private prevHadDeformer;
340
+ constructor(partId: string, newDeformerId: string | undefined);
341
+ apply(doc: EditorDocument): void;
342
+ invert(doc: EditorDocument): void;
343
+ }
344
+ /**
345
+ * Add, regenerate, or remove the triangle mesh on a part.
346
+ *
347
+ * - `mesh !== undefined` → add or replace the mesh, registering the
348
+ * unit-square base UVs in the side-table so {@link EditorDocument.applyAtlas}
349
+ * can remap them later.
350
+ * - `mesh === undefined` → delete `part.mesh` and remove the side-table entry.
351
+ *
352
+ * Fails fast (BEFORE any mutation) on warp-topology violations:
353
+ * - REMOVE while `part.warps` is present (even empty) or the part is
354
+ * attached to a warp deformer — the format rejects any `warps` key once
355
+ * the mesh is gone.
356
+ * - ADD/REPLACE while `part.warps` has authored offsets (`length > 0`) —
357
+ * regenerating the mesh invalidates offset positions silently; the user
358
+ * must remove the warps first.
359
+ *
360
+ * The remove guard checks PRESENCE of `part.warps` (not length) while the
361
+ * add/replace guard checks LENGTH > 0. This asymmetry is intentional: the
362
+ * format allows `warps: []` only when a mesh exists, so any present key
363
+ * (even empty) would become invalid after mesh removal; but replacing a mesh
364
+ * under an empty `warps: []` is harmless because there are no authored offsets.
365
+ */
366
+ declare class SetPartMesh implements EditCommand {
367
+ private readonly partId;
368
+ readonly label = "Set part mesh";
369
+ private readonly mesh;
370
+ private captured;
371
+ private prevHadMesh;
372
+ private prevMesh;
373
+ private prevBaseMeshUvs;
374
+ constructor(partId: string, mesh: IkiMesh | undefined);
375
+ apply(doc: EditorDocument): void;
376
+ invert(doc: EditorDocument): void;
377
+ }
378
+ /**
379
+ * Add a physics rig to `model.physics`. Mirrors {@link AddDeformer}: clone on
380
+ * construction, cheap friendly duplicate-id pre-check, full synthetic-candidate
381
+ * validation before mutating, capture the absent-vs-present `physics` state once,
382
+ * and on invert delete the key when the array empties back to its prior absence.
383
+ */
384
+ declare class AddPhysicsRig implements EditCommand {
385
+ readonly label = "Add physics rig";
386
+ private readonly rig;
387
+ private captured;
388
+ private prevPhysicsAbsent;
389
+ constructor(rig: IkiPhysics);
390
+ apply(doc: EditorDocument): void;
391
+ invert(doc: EditorDocument): void;
392
+ }
393
+ /**
394
+ * Delete a physics rig by id. Mirrors {@link DeleteDeformer}: resolve/validate
395
+ * first, capture the removed rig + its index once, splice. Removing the LAST rig
396
+ * deletes the `physics` key to keep the exported shape minimal; invert re-inserts
397
+ * at the original index (recreating the array if it was deleted).
398
+ */
399
+ declare class DeletePhysicsRig implements EditCommand {
400
+ private readonly rigId;
401
+ readonly label = "Delete physics rig";
402
+ private captured;
403
+ private removed;
404
+ private index;
405
+ constructor(rigId: string);
406
+ apply(doc: EditorDocument): void;
407
+ invert(doc: EditorDocument): void;
408
+ }
409
+ /**
410
+ * Replace a physics rig in place (tuning). Mirrors {@link SetDeformerBindings}
411
+ * but DEEP-clones the nested `input`/`output` (a shallow spread would alias them).
412
+ * Forbids rename (`rig.id` must equal the target `rigId`) — this command tunes a
413
+ * rig, never re-keys it. Validates the whole candidate (the edited rig swapped in
414
+ * at its index) so cross-rig rules still run, then captures the prior rig once.
415
+ */
416
+ declare class SetPhysicsRig implements EditCommand {
417
+ private readonly rigId;
418
+ readonly label = "Set physics rig";
419
+ private readonly rig;
420
+ private captured;
421
+ private prevRig;
422
+ constructor(rigId: string, rig: IkiPhysics);
423
+ apply(doc: EditorDocument): void;
424
+ invert(doc: EditorDocument): void;
425
+ }
426
+
427
+ /**
428
+ * One part mapped to an imported atlas source. `index` is always 0 because the
429
+ * atlas is a single page; only the `uv` sub-rectangle varies per part.
430
+ */
431
+ interface AtlasAssignment {
432
+ partId: string;
433
+ uv: IkiUvRect;
434
+ }
435
+ /** Input to {@link EditorDocument.applyAtlas}: the new atlas table plus the
436
+ * per-part UV assignments into it. */
437
+ interface ApplyAtlasInput {
438
+ textures: IkiTexture[];
439
+ partTextureAssignments: AtlasAssignment[];
440
+ }
441
+ /**
442
+ * In-memory editing session over a single {@link IkiModel}. The model is held
443
+ * directly (no superset) and mutated in place by invertible {@link EditCommand}s
444
+ * pushed through {@link execute}; undo/redo invert/re-apply them.
445
+ *
446
+ * The constructor `structuredClone`s the input so the caller's model is never
447
+ * mutated. Parts are addressed by stable `id`, never by array index.
448
+ */
449
+ declare class EditorDocument {
450
+ private readonly model;
451
+ private readonly undoStack;
452
+ private readonly redoStack;
453
+ /** Editor-only session state, never serialized; keyed by stable part id. The
454
+ * unmodified BASE local uvs of every mesh part, captured once at construction
455
+ * so atlas remaps always derive from the original (idempotent). */
456
+ private readonly baseMeshUvs;
457
+ constructor(model: IkiModel);
458
+ /** Live reference to the working model — for READ access. Mutate it only
459
+ * through {@link execute}/{@link undo}/{@link redo}. */
460
+ getModel(): IkiModel;
461
+ /** Resolve a part by stable id. Throws a plain `Error` (NOT an
462
+ * `IkiFormatError`) with a path-qualified message if the id is unknown. */
463
+ findPart(id: string): IkiPart;
464
+ /** Resolve a warp deformer by stable id. Throws a path-qualified plain
465
+ * `Error` if no deformer matches the id or the match is not a warp deformer.
466
+ * READ/mutate-through accessor, consistent with {@link findPart}. */
467
+ findWarpDeformer(id: string): IkiWarpDeformer;
468
+ /** Resolve a matrix deformer by stable id. Throws a path-qualified plain
469
+ * `Error` if no deformer matches the id or the match is a warp deformer
470
+ * (`kind === "warp"`). A `kind` of `"matrix"` or `undefined` is a matrix
471
+ * deformer. READ/mutate-through accessor, consistent with {@link findPart}. */
472
+ findMatrixDeformer(id: string): IkiMatrixDeformer;
473
+ /** Resolve any deformer (matrix or warp) by stable id. Throws a
474
+ * path-qualified plain `Error` if no deformer matches the id.
475
+ * READ/mutate-through accessor, consistent with {@link findPart}. */
476
+ findDeformer(id: string): IkiDeformer;
477
+ /** Resolve a physics rig by stable id. Throws a path-qualified plain `Error`
478
+ * if no rig matches the id. The `physics` array is optional, so guard it.
479
+ * READ/mutate-through accessor, consistent with {@link findDeformer}. */
480
+ findPhysicsRig(id: string): IkiPhysics;
481
+ /**
482
+ * Record the base mesh UVs for a part inserted AFTER construction (e.g. by
483
+ * {@link AddPart}). Returns the PRIOR entry for that id (or `undefined` if
484
+ * none existed) so the caller can restore it on undo. No-op for meshless
485
+ * parts (returns `undefined`). Must be called by any command that pushes a
486
+ * mesh part into the model so the part joins the constructor-captured base-UV
487
+ * side state required by {@link applyAtlas}.
488
+ *
489
+ * The returned prior value is what {@link restoreBaseMeshUvs} expects on
490
+ * undo — pass it verbatim. Hazard guarded: DeletePart(X) → AddPart(X′) →
491
+ * undo(add) → undo(delete); without restore, X's constructor-captured entry
492
+ * would be permanently gone after undo of the add, causing applyAtlas to
493
+ * fail when X is restored.
494
+ */
495
+ captureBaseMeshUvs(partId: string): number[] | undefined;
496
+ /**
497
+ * Restore the base-UV side state to exactly what it was before a
498
+ * {@link captureBaseMeshUvs} call. Called from {@link AddPart.invert}:
499
+ * pass the value returned by captureBaseMeshUvs on first apply.
500
+ * - `prev` is `number[]` → sets the entry (restores a prior mesh's base).
501
+ * - `prev` is `undefined` → deletes the entry (no entry existed before the
502
+ * add, so a later different-mesh part reusing the id must not inherit this
503
+ * one's base).
504
+ * DeletePart does NOT call this — the entry persists across delete/undo so
505
+ * the restored part still has its base available for applyAtlas.
506
+ */
507
+ restoreBaseMeshUvs(partId: string, prev: number[] | undefined): void;
508
+ /** The construction-captured base UVs for a mesh part. Throws a path-qualified
509
+ * plain `Error` if absent. SINGLE accessor for both apply branches — never
510
+ * read `baseMeshUvs` with a bare `!` elsewhere. */
511
+ private requireBaseUvs;
512
+ /**
513
+ * Replace the atlas table and rewrite every part's texture reference in a
514
+ * single atomic step. For every part in `partTextureAssignments` set
515
+ * `texture = { index: 0, uv }`; CLEAR `texture` (delete the key) on every
516
+ * other part.
517
+ *
518
+ * Mesh parts are textured as a MATCHED PAIR: an assigned mesh part also has
519
+ * its per-vertex `mesh.uvs` remapped (from the construction-captured base)
520
+ * into the same `uv` rect; an unassigned mesh part has its `mesh.uvs` restored
521
+ * to that base. Quad parts carry `texture.uv` only and are untouched here.
522
+ *
523
+ * Deliberately NON-undoable: it does NOT push to or clear the undo/redo
524
+ * stacks (texture/atlas state is not undoable in 5b — the unified
525
+ * editor-state superset is deferred to 5d). `canUndo()`/`canRedo()` are
526
+ * unchanged after a call.
527
+ *
528
+ * Validate-all-then-apply: structural input validation, per-partId
529
+ * resolution, and a base-UV preflight over every mesh part all run BEFORE any
530
+ * mutation, so a bad input (wrong shape, duplicate partId, unknown partId, or
531
+ * a mesh part with no captured base) throws a plain `Error` and leaves the
532
+ * model exactly as it was — never a partial application.
533
+ */
534
+ applyAtlas(input: ApplyAtlasInput): void;
535
+ /**
536
+ * Clear a model-committed texture reference from a single part. Deliberately
537
+ * NON-undoable, matching {@link applyAtlas} — texture/atlas state is not in
538
+ * the undo model (unified editor-state is deferred). Does NOT push to or
539
+ * clear the undo/redo stacks. Does NOT touch `mesh.uvs` — atlas-space UVs on
540
+ * an untextured mesh are inert for color rendering; a later atlas import
541
+ * remaps from the base UVs anyway.
542
+ *
543
+ * This is the single consistent texture/atlas undo boundary: by the time a
544
+ * part is deletable (no texture), its DeletePart snapshot carries no texture
545
+ * reference, so no stale index can resurface on undo after a later atlas
546
+ * repack.
547
+ *
548
+ * Throws a path-qualified plain `Error` (NOT `IkiFormatError`) if the part
549
+ * id is unknown — same contract as {@link findPart}.
550
+ */
551
+ clearPartTextureRef(partId: string): void;
552
+ /**
553
+ * Validate the structural shape of an {@link ApplyAtlasInput} without
554
+ * touching the model, returning a known-good `{ texture?, assignmentsByPart }`
555
+ * for {@link applyAtlas} to resolve and apply.
556
+ */
557
+ private normalizeAtlasInput;
558
+ /**
559
+ * Overwrite a part's whole transform with a fresh copy of `transform`.
560
+ * Replacing the whole object (rather than individual channels) preserves any
561
+ * optional keys already absent from the incoming value.
562
+ *
563
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
564
+ * pose. Does NOT push to or clear undoStack/redoStack (sibling to
565
+ * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
566
+ * restoring the exact prior snapshot when the capture pose ends.
567
+ *
568
+ * `IkiTransform` is a flat number map, so a shallow spread is a sufficient
569
+ * deep copy — no aliasing of the caller's object remains.
570
+ */
571
+ setPartTransformEphemeral(partId: string, transform: IkiTransform): void;
572
+ /**
573
+ * Overwrite a matrix deformer's optional transform with a fresh copy, or
574
+ * delete it when `transform` is `undefined`.
575
+ * `undefined` deletes the key, restoring the deformer to the same state as
576
+ * one that never had a transform (absent-vs-present matters for downstream
577
+ * renderers).
578
+ *
579
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
580
+ * pose. Does NOT push to or clear undoStack/redoStack (sibling to
581
+ * {@link applyAtlas}'s non-undoable boundary). The caller is responsible for
582
+ * restoring the exact prior snapshot when the capture pose ends.
583
+ *
584
+ * `IkiDeformerTransform` is a flat number map, so a shallow spread is a
585
+ * sufficient deep copy — no aliasing of the caller's object remains.
586
+ */
587
+ setDeformerTransformEphemeral(deformerId: string, transform: IkiDeformerTransform | undefined): void;
588
+ /**
589
+ * Overwrite a part's whole bindings array with a fresh deep copy, or delete
590
+ * the key when `bindings` is empty.
591
+ *
592
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
593
+ * neutralization (zeroing the row being recaptured so the preview reflects
594
+ * base-only during posing). Does NOT push to or clear undoStack/redoStack.
595
+ * The caller is responsible for restoring the original bindings when the
596
+ * capture session ends.
597
+ */
598
+ setPartBindingsEphemeral(partId: string, bindings: IkiBinding[]): void;
599
+ /**
600
+ * Overwrite a matrix deformer's whole bindings array with a fresh deep copy,
601
+ * or delete the key when `bindings` is empty.
602
+ *
603
+ * Deliberately NON-undoable — used ONLY for an editor app's transient capture
604
+ * neutralization (zeroing the row being recaptured so the preview reflects
605
+ * base-only during posing). Does NOT push to or clear undoStack/redoStack.
606
+ * The caller is responsible for restoring the original bindings when the
607
+ * capture session ends.
608
+ */
609
+ setDeformerBindingsEphemeral(deformerId: string, bindings: IkiDeformerBinding[]): void;
610
+ /** Apply a command and record it as one undo step. Clears the redo stack. */
611
+ execute(cmd: EditCommand): void;
612
+ /** Invert the most recent command and move it onto the redo stack. */
613
+ undo(): void;
614
+ /** Re-apply the most recently undone command and move it back onto undo. */
615
+ redo(): void;
616
+ canUndo(): boolean;
617
+ canRedo(): boolean;
618
+ /**
619
+ * Validate and export the current working model by running it through
620
+ * {@link parseIkiModel}. Uses `structuredClone` so the validator's
621
+ * normalized output cannot alias the working model. Propagates
622
+ * `IkiFormatError` unchanged on failure — callers surface `.message`.
623
+ */
624
+ toIkiModel(): IkiModel;
625
+ /**
626
+ * Pretty-print the validated model as a `.iki` JSON string. Always
627
+ * validates first — invalid documents never reach a file.
628
+ */
629
+ serialize(): string;
630
+ }
631
+
632
+ /**
633
+ * Alpha bounding-box scan shared by every auto-rig ingestion path.
634
+ *
635
+ * The scan itself is environment-free — it only needs indexable RGBA bytes — so
636
+ * a browser editor (canvas `ImageData`) and the Node MCP server (a `sharp`
637
+ * raw buffer) run the SAME code instead of two copies that have to be kept
638
+ * byte-identical by hand. Only decoding differs between them.
639
+ */
640
+ /** Alpha at or above this counts as coverage; below it is treated as empty. */
641
+ declare const ALPHA_BBOX_THRESHOLD = 8;
642
+ /** Top-left origin, +y down — image space, not model space. */
643
+ interface AlphaBbox {
644
+ x: number;
645
+ y: number;
646
+ w: number;
647
+ h: number;
648
+ }
649
+ /**
650
+ * Tight bounding box of every pixel with alpha >= {@link ALPHA_BBOX_THRESHOLD},
651
+ * expanded 1px on each side (clamped to the image) for AA / extrude margin.
652
+ *
653
+ * Returns `null` when no pixel passes the threshold, leaving the "empty layer"
654
+ * error to the caller: each ingestion path reports it with its own error type
655
+ * and message.
656
+ */
657
+ declare function detectAlphaBbox(rgba: ArrayLike<number>, width: number, height: number): AlphaBbox | null;
658
+
659
+ /**
660
+ * Pure, binding-value logic for computing an endpoint (rest-to-posed delta or
661
+ * ratio) when capturing a transform channel binding. No DOM, no @ikijs/engine —
662
+ * the single home of the additive-vs-multiplicative rule, reused by both the
663
+ * part and deformer capture paths.
664
+ *
665
+ * Additive channels (translateX, translateY, rotate, scaleX, scaleY) return the
666
+ * delta: `posedValue - restValue`. The binding will multiply that delta across
667
+ * the driven range.
668
+ *
669
+ * Opacity is multiplicative: returns the ratio `posedValue / restValue`. The
670
+ * binding will multiply by that ratio across the driven range. When `restValue`
671
+ * is 0 (a degenerate case: base opacity cannot be represented multiplicatively
672
+ * since 0 * x ≡ 0), this returns 0 as a documented fallback — it does NOT
673
+ * recover `posedValue` and is NOT unit-tested as an identity. The store layer
674
+ * additionally skips an opacity capture when rest opacity is 0, surfacing an
675
+ * editError.
676
+ *
677
+ * Deformer channels never include opacity (they use `IkiMatrixChannel`), so the
678
+ * opacity branch is reached only for parts.
679
+ *
680
+ * Finiteness of the captured value is NOT validated here; the store's
681
+ * `captureEndpoint` finite-value guard is the appropriate layer for that check.
682
+ */
683
+ declare function captureBindingEndpoint(channel: IkiTransformChannel, restValue: number, posedValue: number): number;
684
+
685
+ /**
686
+ * Pure, grid-size-agnostic keyform/offset math for authoring a warp-deformer
687
+ * grid by dragging. No DOM, no `@ikijs/engine` — the load-bearing testable core.
688
+ * Constraints derive only from the input array lengths, never the sample grid.
689
+ */
690
+ /**
691
+ * Interpolate the grid offsets at `value`, mirroring the engine's
692
+ * `accumulateKeyformOffsets` clamp+lerp semantics: clamp to the first/last
693
+ * keyform (NO extrapolation) and linearly interpolate the bracketing pair.
694
+ * Returns a NEW array of length `keyforms[0].offsets.length`. Throws on empty.
695
+ */
696
+ declare function interpolateGridOffsets(keyforms: {
697
+ value: number;
698
+ offsets: number[];
699
+ }[], value: number): number[];
700
+ /**
701
+ * Per-control-point delta of the dragged grid from the rest grid: for each
702
+ * point `i`, `(draggedX_i - restX_i, draggedY_i - restY_i)`. The DOM layer
703
+ * assembles the full `restFrameDraggedPoints` (including untouched points), so
704
+ * this is a straight subtract — no prior-offset blending.
705
+ *
706
+ * Both arrays must have the SAME length and that length must be even (x,y
707
+ * pairs). Returns a NEW array of length `restPoints.length`.
708
+ */
709
+ declare function computeGridOffsets(restPoints: number[], restFrameDraggedPoints: number[]): number[];
710
+ /**
711
+ * Insert or replace the keyform at `value`, returning a NEW array. If a keyform
712
+ * already exists with an exact-match `value`, REPLACE its offsets (with a copy);
713
+ * otherwise INSERT `{ value, offsets: [...offsets] }` at the position that keeps
714
+ * the array strictly ascending by value. The input array and its keyform objects
715
+ * are never mutated, and `offsets` is copied so the result never aliases the
716
+ * caller's array.
717
+ *
718
+ * Deliberately RANGE-FREE — a generic, reusable array op. Value-range
719
+ * enforcement is the command's job, not this helper's.
720
+ */
721
+ declare function upsertGridKeyform(keyforms: IkiGridKeyform[], value: number, offsets: number[]): IkiGridKeyform[];
722
+
723
+ declare const ATLAS_PADDING = 2;
724
+ declare const UV_INSET_PX = 0.5;
725
+ /** Intrinsic pixel size of one decoded image; id is an editor-only stable key. */
726
+ interface AtlasSource {
727
+ id: string;
728
+ width: number;
729
+ height: number;
730
+ }
731
+ /** Sub-image pixel rect within the page, top-left origin, EXCLUDING the gutter. */
732
+ interface AtlasPlacement {
733
+ id: string;
734
+ x: number;
735
+ y: number;
736
+ width: number;
737
+ height: number;
738
+ }
739
+ interface AtlasLayout {
740
+ pageWidth: number;
741
+ pageHeight: number;
742
+ placements: AtlasPlacement[];
743
+ padding: number;
744
+ }
745
+ /**
746
+ * Deterministic shelf/row packer. Sources are sorted by id for stability so
747
+ * identical inputs always produce an identical layout.
748
+ *
749
+ * Padding is one-sided: each placement reserves `padding` px on its RIGHT and
750
+ * BOTTOM only. Page left/top edges need no gutter.
751
+ *
752
+ * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
753
+ * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
754
+ * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.
755
+ */
756
+ declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
757
+ /**
758
+ * Convert a pixel placement into a UV rect, inset by `insetPx` on all four
759
+ * edges and clamped to [0, 1] so the validator's bounds check always passes.
760
+ */
761
+ declare function uvRectFor(placement: AtlasPlacement, page: {
762
+ width: number;
763
+ height: number;
764
+ }, insetPx?: number): IkiUvRect;
765
+
766
+ /**
767
+ * Validate that reparenting `deformerId` under `newParentId` keeps the deformer
768
+ * hierarchy valid. Pass `newParentId === undefined` to move to root (always legal).
769
+ * Checks: existence, self-reference, undeclared parent, kind constraint (warp
770
+ * deformers cannot be parents), and cycle detection via the proposed edge.
771
+ */
772
+ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId: string, newParentId: string | undefined): void;
773
+ /**
774
+ * Validate that deleting `deformerId` is safe. Throws when the deformer does
775
+ * not exist, when another deformer is parented to it (must be reparented or
776
+ * detached first), when a part is attached to it (must be detached first), or
777
+ * when a physics chain anchors to it (must be re-anchored or deleted first).
778
+ *
779
+ * Every id that can reference a deformer must be covered here: the format
780
+ * validator rejects a dangling reference at export, so a delete this function
781
+ * lets through does not fail now — it strands the document in a state
782
+ * `toIkiModel()` refuses.
783
+ *
784
+ * Note: there is no validatePartDelete here, but NOT because parts are
785
+ * unreferenced — `clip.masks` names parts by id. That invariant is enforced
786
+ * closer to the edits that could break it: `DeletePart` refuses to remove a
787
+ * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off
788
+ * one (masks must be mesh parts). No command creates or edits `clip`.
789
+ */
790
+ declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
791
+ /**
792
+ * Validate that attaching part `partId` to deformer `newDeformerId` is valid.
793
+ * Pass `newDeformerId === undefined` to detach (always legal).
794
+ * Checks: part existence, undeclared deformer, and mesh-required-for-warp.
795
+ */
796
+ declare function validatePartAttach(deformers: IkiDeformer[], partId: string, parts: IkiPart[], newDeformerId: string | undefined): void;
797
+
798
+ /**
799
+ * Create a minimal valid part with a collision-free id and a paint order one
800
+ * above the current top-most part so it is immediately visible in the viewport.
801
+ * Uses a distinct non-white blue tint so it is distinguishable from the canvas
802
+ * background without requiring a texture.
803
+ *
804
+ * No optional keys (`texture`, `mesh`, `deformer`, `bindings`, `warps`) are set
805
+ * — the format treats all of them as absent by default.
806
+ */
807
+ declare function createDefaultPart(model: IkiModel): IkiPart;
808
+ /**
809
+ * Create a minimal valid matrix deformer rooted at the canvas origin.
810
+ * `kind` is omitted because the format treats its absence as "matrix" (the
811
+ * default), keeping the serialised model compact.
812
+ */
813
+ declare function createDefaultMatrixDeformer(model: IkiModel): IkiMatrixDeformer;
814
+ /**
815
+ * Create a regular grid mesh in part LOCAL space (±0.5 unit frame).
816
+ *
817
+ * Vertices span x ∈ [-0.5, 0.5] and y ∈ [-0.5, 0.5] (+y up, engine convention).
818
+ * Row 0 is the TOP of the grid (y = +0.5); row index increases downward.
819
+ * UVs are unit-square base coordinates: u = col/cols (0..1 left→right),
820
+ * v = row/rows (0..1 top→bottom). The top row maps to v=0 because v and y
821
+ * run in opposite directions — keeps textures upright without a post-flip.
822
+ * The UV-to-texture remap (atlas rect) is applied later in SetPartMesh, not here.
823
+ *
824
+ * Index winding per cell: [BL, BR, TL] then [TL, BR, TR], matching the engine's
825
+ * implicit-quad convention (see examples/editor/src/mesh-generator.ts).
826
+ *
827
+ * Bounds are validated BEFORE any array allocation because this factory runs
828
+ * before SetPartMesh's parseIkiModel — an unbounded count would freeze the
829
+ * editor before the format-level 65536 limit is ever reached.
830
+ */
831
+ declare function createGridMesh(cols: number, rows: number): IkiMesh;
832
+ /**
833
+ * Create a 4×4-cell warp deformer whose rest grid spans a quarter of the
834
+ * canvas in each direction (`±canvas.width/4` × `±canvas.height/4`). For the
835
+ * 1000-unit sample canvas this gives x,y ∈ [−250, 250], which is large enough
836
+ * to cover a typical face part without spilling to the edge.
837
+ *
838
+ * `warps` is omitted — the format treats its absence as "rest grid only", so
839
+ * the deformer is immediately usable without authored keyforms.
840
+ * `parent` is omitted — the deformer is placed at root; the caller may
841
+ * reparent it via `SetDeformerParent` after creation.
842
+ */
843
+ declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
844
+
845
+ /**
846
+ * Role table, role parsing, bbox→transform math, and model assembly for the
847
+ * AI auto-rig generator. All pure functions — no DOM, no canvas, no
848
+ * crypto.randomUUID.
849
+ *
850
+ * L/R = CHARACTER frame: *_L is the character's left = screen right.
851
+ */
852
+
853
+ /**
854
+ * Input contract from the host app to this package's auto-rig functions.
855
+ * Passed in after the host has decoded PNGs, computed alpha bboxes, and
856
+ * mapped filenames to canonical roles.
857
+ */
858
+ interface LayerInput {
859
+ /** Canonical role, e.g. "eye_L". */
860
+ role: string;
861
+ /** Original file name — used in error messages and as a stable id. */
862
+ fileName: string;
863
+ /** Shared canvas width (all layers have the same canvas size). */
864
+ canvasW: number;
865
+ /** Shared canvas height. */
866
+ canvasH: number;
867
+ /** Alpha-tight bounding box, top-left origin, +y down (image coords). */
868
+ bbox: {
869
+ x: number;
870
+ y: number;
871
+ w: number;
872
+ h: number;
873
+ };
874
+ /** Cropped image width = bbox.w. */
875
+ cropW: number;
876
+ /** Cropped image height = bbox.h. */
877
+ cropH: number;
878
+ }
879
+ /**
880
+ * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
881
+ *
882
+ * Steps:
883
+ * 1. Normalize each filename → role (normalizeRole).
884
+ * 2. Eagerly check each role against ROLE_TABLE — unknown roles throw early
885
+ * with the offending fileName included in the message.
886
+ * 3. Call assertRoleSet to check duplicates + required roles.
887
+ *
888
+ * Throws a path-qualified Error on any contract violation.
889
+ */
890
+ declare function parseLayerRoles(fileNames: string[]): {
891
+ role: string;
892
+ fileName: string;
893
+ }[];
894
+ /**
895
+ * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
896
+ * valid IkiModel ready for parseIkiModel.
897
+ *
898
+ * - Validate all inputs before deriving anything.
899
+ * - Place parts at source-derived positions (bboxToTransform, unshifted).
900
+ * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a
901
+ * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front
902
+ * layer is present.
903
+ * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings) and faceWarp
904
+ * (warp, 4×4, baked cylinder warp center-relative on faceCenterX).
905
+ * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh 4×4 + role bindings.
906
+ * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.
907
+ * - Part ids equal the role string (deterministic, no crypto.randomUUID).
908
+ * - Return parseIkiModel(structuredClone(model)) — every caller gets a
909
+ * validated model; bad assembly fails loudly.
910
+ */
911
+ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
912
+ width: number;
913
+ height: number;
914
+ }): IkiModel;
915
+
916
+ export { ALPHA_BBOX_THRESHOLD, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, UV_INSET_PX, captureBindingEndpoint, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, detectAlphaBbox, generateIkiFromLayerSet, interpolateGridOffsets, packAtlas, parseLayerRoles, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };