@ikijs/editor 0.8.1 → 0.9.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.d.ts CHANGED
@@ -656,192 +656,6 @@ interface AlphaBbox {
656
656
  */
657
657
  declare function detectAlphaBbox(rgba: ArrayLike<number>, width: number, height: number): AlphaBbox | null;
658
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
659
  /**
846
660
  * Role table, role parsing, bbox→transform math, and model assembly for the
847
661
  * AI auto-rig generator. All pure functions — no DOM, no canvas, no
@@ -879,10 +693,10 @@ interface LayerInput {
879
693
  * The layer's painted half-width on every CROP row, top → bottom, one entry
880
694
  * per row (`cropH` of them): half the row's opaque span in canvas px under
881
695
  * the same alpha ≥ 128 rule the head's `headEdges` are measured with
882
- * (`@ikijs/mcp`'s `measure-turn`), 0 for a row with no opaque pixel.
696
+ * (`createLayerSetMeasurer`), 0 for a row with no opaque pixel.
883
697
  * Optional, and only the FACE's is read: it gives the face plate a
884
698
  * row-dependent turn radius and a chin swing (`faceRowProfile`,
885
- * `turnSurface`); absent — as the editor's own import leaves it — the plate
699
+ * `turnSurface`); absent — from a host that does not measure it — the plate
886
700
  * turns on one radius on every row. Validated before anything reads it: an
887
701
  * array of length `cropH` whose every entry is a finite number in
888
702
  * [0, cropW / 2], the most an inclusive span inside the crop can be.
@@ -895,8 +709,8 @@ interface LayerInput {
895
709
  * NOSE's is read: its centre and width are the nose's turn landmark
896
710
  * (`turnLandmarks`), so a shaded nose is fitted by the drawing rather than
897
711
  * by its feather, and its top edge at its centre x is the bridge top the
898
- * nose tilts about on the turn (`NOSE_TURN_TILT_DEG`). Absent — as the
899
- * editor's own import leaves it — the crop stands in for both. Validated
712
+ * nose tilts about on the turn (`NOSE_TURN_TILT_DEG`). Absent — from a
713
+ * host that does not measure it — the crop stands in for both. Validated
900
714
  * before anything reads it: a plain object whose x, y, w and h are finite,
901
715
  * w and h positive, and the box inside `bbox`.
902
716
  */
@@ -931,8 +745,9 @@ declare function parseLayerRoles(fileNames: string[]): {
931
745
  * gap between an iris edge and a run edge is a real distance with no margin to
932
746
  * add. "Outer" is toward the head's edge on that side. The run is the one a
933
747
  * producer judges a strand: it may skip runs it judges hair detail
934
- * (`@ikijs/mcp` treats those narrower than half the iris's painted width on
935
- * the row as clear), so an iris under only such a wisp counts as clear.
748
+ * (`createLayerSetMeasurer` treats those narrower than half the iris's
749
+ * painted width on the row as clear), so an iris under only such a wisp counts
750
+ * as clear.
936
751
  *
937
752
  * One shape covers three cases:
938
753
  * - an ordinary side strand outward of a clear iris (`runFace` outward of
@@ -1180,4 +995,389 @@ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1180
995
  };
1181
996
  }): IkiModel;
1182
997
 
1183
- export { ALPHA_BBOX_THRESHOLD, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type IrisStrand, type LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, captureBindingEndpoint, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, detectAlphaBbox, generateIkiFromLayerSet, interpolateGridOffsets, packAtlas, parseLayerRoles, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
998
+ /**
999
+ * The alpha rules a layer set is measured by, and the pre-rig measurement of
1000
+ * that set, in one DOM-free copy — the way `detectAlphaBbox` is one.
1001
+ *
1002
+ * Both auto-rig hosts — the editor app (canvas `getImageData`) and
1003
+ * `@ikijs/mcp`'s `auto_rig_from_layers` (a `sharp` raw buffer) — hand
1004
+ * `createLayerSetMeasurer` the RGBA they decoded and rig what it measures, and
1005
+ * `@ikijs/mcp`'s other tools (`measure_turn_reference`, `measure_layers`, the
1006
+ * composer) read the same rules. Two copies would have to be kept identical by
1007
+ * hand, or the head a render measures back, or the nose core the composer
1008
+ * placed, would part from the one the rig was fitted to. Only decoding differs
1009
+ * between the hosts.
1010
+ */
1011
+
1012
+ /** Half-height of the row band the head span is taken over, in px. Fixed at
1013
+ * ±10 rows rather than scaled to the image, and that is the convention: any
1014
+ * other measurement of head width meant to be compared with the one
1015
+ * `@ikijs/mcp`'s `measure_turn_reference` takes off a render — one taken off
1016
+ * layer alpha, a hand script — has to span the same band. Shared by that tool
1017
+ * and `createLayerSetMeasurer`, so the head it spans off the layers' alpha
1018
+ * union spans this band too. */
1019
+ declare const HEAD_BAND = 10;
1020
+ /** Alpha at or above this is opaque. Shared by `@ikijs/mcp`'s
1021
+ * `measure_turn_reference`, whose "alpha" foreground rule reads an engine
1022
+ * render's silhouette by it, and by the layer-alpha silhouette
1023
+ * `createLayerSetMeasurer` measures the head on, which has to follow that
1024
+ * rule to be comparable with a render. */
1025
+ declare const ALPHA_OPAQUE = 128;
1026
+ /**
1027
+ * A layer's dense core: the tight box of its pixels at alpha ≥ ALPHA_OPAQUE,
1028
+ * not grown. A soft-alpha part (a nose drawn as a shaded bump) is mostly
1029
+ * feather, and the core is the drawing a viewer reads inside it — `@ikijs/mcp`'s
1030
+ * composer sizes and places the nose by it, and `createLayerSetMeasurer` reads
1031
+ * the same core as the nose's `LayerInput.denseCore`, its turn landmark. It is
1032
+ * not the crop: that stays the alpha ≥ 8 box grown by 1 px (`detectAlphaBbox`).
1033
+ * `null` when no pixel reaches the threshold, a part painted wholly
1034
+ * translucent. A core that is a speck of its part (`isSpeckCore`) is not the
1035
+ * drawing either, and both readers fall back to the whole part, as they do for
1036
+ * `null`.
1037
+ */
1038
+ declare function denseCoreOf(rgba: ArrayLike<number>, width: number, height: number): {
1039
+ x: number;
1040
+ y: number;
1041
+ w: number;
1042
+ h: number;
1043
+ } | null;
1044
+ /**
1045
+ * The fraction of its part's width, or of its height, under which a dense core
1046
+ * is a speck (`isSpeckCore`). Measured core/part ratios, width / height: the
1047
+ * hero's composed nose 0.72 / 0.76 (core 39×63 in a 54×83 crop), three
1048
+ * generated shaded-bump noses (the plugin's 0.11 nose prompt) 0.75–0.85 /
1049
+ * 0.79–0.91, `@ikijs/mcp`'s `writeSoftNose` test fixture 0.56 / 0.63 and its
1050
+ * `writeSoftNoseLayers` 0.40 / 0.40, a nostril mark ≤ 0.1. So the line sits at
1051
+ * least 1.6× under every real nose and fixture, and 2.5× over a speck. At the
1052
+ * line, sizing by the core would scale the whole part to 4× its layout width,
1053
+ * the largest blow-up `@ikijs/mcp`'s composer still accepts.
1054
+ */
1055
+ declare const SPECK_CORE_FRACTION = 0.25;
1056
+ /**
1057
+ * Whether a dense core is a speck of its part: narrower than
1058
+ * SPECK_CORE_FRACTION of the part's width, or shorter than that fraction of
1059
+ * its height (strictly `<`). Either dimension counts, because the core does
1060
+ * more than size a nose: its bottom row is the tip the composer places, and
1061
+ * its top-centre the rig's tilt pivot, so a sliver misplaces the nose as
1062
+ * surely as a speck mis-sizes it. A speck is a lone nostril mark or highlight,
1063
+ * not the drawing, and every reader falls back to the whole part: `@ikijs/mcp`'s
1064
+ * compose sizes and places its trimmed source part whole, and
1065
+ * `createLayerSetMeasurer` hands the rig no core, so the crop stands in.
1066
+ *
1067
+ * The two apply it to different pixels — compose to the source part before
1068
+ * resampling, the rig and `measure_layers` to the composed layer against its
1069
+ * crop — so their verdicts can part, but only for a core within about a
1070
+ * resampled pixel of the line, or one resampling pushes across ALPHA_OPAQUE.
1071
+ * Compose's report carries its source verdict when that was a speck, and
1072
+ * otherwise what the composed layer shows, so it warns of a parting either
1073
+ * way; `measure_layers` reports the layer alone.
1074
+ */
1075
+ declare function isSpeckCore(core: {
1076
+ w: number;
1077
+ h: number;
1078
+ }, part: {
1079
+ w: number;
1080
+ h: number;
1081
+ }): boolean;
1082
+ /**
1083
+ * Outermost set pixels of `mask` across the row band `rowLo..rowHi` (clamped
1084
+ * to the image) — the silhouette span at that height. An empty band returns
1085
+ * `left > right`, which every caller has to check for.
1086
+ *
1087
+ * Shared by `@ikijs/mcp`'s `measure_turn_reference`, which spans a render's
1088
+ * head with it, and `createLayerSetMeasurer`, which spans the layers' alpha
1089
+ * union: the two heads are compared, so they have to be spanned the same way.
1090
+ */
1091
+ declare function foregroundSpan(mask: Uint8Array, width: number, height: number, rowLo: number, rowHi: number): {
1092
+ left: number;
1093
+ right: number;
1094
+ };
1095
+ /**
1096
+ * Half of a silhouette span, in px. The span is INCLUSIVE — `left` and `right`
1097
+ * are both foreground columns — so the head is `right - left + 1` px wide.
1098
+ * Shared by `@ikijs/mcp`'s `measure_turn_reference` and
1099
+ * `createLayerSetMeasurer`: the two head half-widths are compared against each
1100
+ * other, so they cannot be halved differently.
1101
+ */
1102
+ declare function headHalfOf(span: {
1103
+ left: number;
1104
+ right: number;
1105
+ }): number;
1106
+ /** One decoded layer, as a host hands it to `LayerSetMeasurer.add`. */
1107
+ interface RgbaLayer {
1108
+ /** Canonical role, e.g. "eye_L" — the host has run `parseLayerRoles` on
1109
+ * the set already. */
1110
+ role: string;
1111
+ /** Original file name: the `LayerInput`'s own, and named in errors. */
1112
+ fileName: string;
1113
+ /** Straight-alpha RGBA, stride 4, `canvas.width × canvas.height` pixels —
1114
+ * canvas `ImageData.data` or a `sharp` raw buffer alike. */
1115
+ rgba: ArrayLike<number>;
1116
+ }
1117
+ /**
1118
+ * Measures a layer set one decoded layer at a time, so a host holds one
1119
+ * full-canvas RGBA buffer at once, however many layers the set has. Call
1120
+ * `finish` once, after the last `add`: it hands back its own `layers` array,
1121
+ * which a later `add` would change.
1122
+ */
1123
+ interface LayerSetMeasurer {
1124
+ /**
1125
+ * Measure one layer while its `rgba` is in memory, keeping no reference to
1126
+ * it: only the set's opaque union, this layer's per-row opaque extent and,
1127
+ * for `hair_front`, its opaque mask outlive the call, so the host may drop
1128
+ * `rgba` as soon as it returns. Returns the layer's `LayerInput` — its crop
1129
+ * box, the face's `rowHalfWidths`, the nose's `denseCore` unless that is a
1130
+ * speck of its crop (`isSpeckCore`) — or `null` for an empty layer, one with
1131
+ * no pixel at or above `ALPHA_BBOX_THRESHOLD`, which it records nothing for:
1132
+ * the host reports that with its own error. Throws, naming the file, when
1133
+ * `rgba` is not `width × height × 4` long; checking the decoded dimensions
1134
+ * is the host's job.
1135
+ */
1136
+ add(layer: RgbaLayer): LayerInput | null;
1137
+ /** The set's measurement, over every layer `add` recorded. The set must be
1138
+ * one `parseLayerRoles` accepted; otherwise this may throw, or measure a
1139
+ * partial set the generator then refuses. */
1140
+ finish(): LayerSetMeasurement;
1141
+ }
1142
+ interface LayerSetMeasurement {
1143
+ /** Every recorded layer's `LayerInput`, in `add` order. */
1144
+ layers: LayerInput[];
1145
+ /** The head's half-width at the eye row, canvas px: half the opaque
1146
+ * (alpha ≥ ALPHA_OPAQUE) union's span over ±HEAD_BAND rows about the row
1147
+ * of the iris centres, or of the eye centres without irises. Absent when
1148
+ * the union has no span there — a layer set painted translucent below that
1149
+ * threshold. */
1150
+ headHalfWidth?: number;
1151
+ /** Whether `headHalfWidth` is wider than the face plate, and so applied;
1152
+ * the face plate stands in for it otherwise, as the generator's own
1153
+ * fallback. */
1154
+ headHalfWidthApplied: boolean;
1155
+ /**
1156
+ * Ready to go into `generateIkiFromLayerSet`'s options, each key present
1157
+ * only when measured: `turnTargets.headHalfWidth` when it is applied (merge
1158
+ * the caller's own targets under it), `headEdges` — every layer's own
1159
+ * opaque extent in the same eye-row band, per side, in model x — with it,
1160
+ * and `strandEdges` whenever the set has `hair_front`, `iris_L` and `iris_R`
1161
+ * and a side measures one (`IrisStrand`).
1162
+ */
1163
+ turnOptions: {
1164
+ turnTargets?: {
1165
+ headHalfWidth: number;
1166
+ };
1167
+ headEdges?: {
1168
+ left: {
1169
+ role: string;
1170
+ x: number;
1171
+ }[];
1172
+ right: {
1173
+ role: string;
1174
+ x: number;
1175
+ }[];
1176
+ };
1177
+ strandEdges?: {
1178
+ left?: IrisStrand;
1179
+ right?: IrisStrand;
1180
+ };
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Start measuring a layer set on a `canvas`-sized canvas: the pass a host runs
1185
+ * between decoding its layers and `generateIkiFromLayerSet`. `add` each
1186
+ * decoded layer, then `finish`.
1187
+ *
1188
+ * Throws before allocating anything when `canvas.width` or `canvas.height` is
1189
+ * not a positive safe integer, or `width × height × 4` is not a safe integer.
1190
+ * How large a canvas to accept at all is each host's own limit.
1191
+ */
1192
+ declare function createLayerSetMeasurer(canvas: {
1193
+ width: number;
1194
+ height: number;
1195
+ }): LayerSetMeasurer;
1196
+
1197
+ /**
1198
+ * Pure, binding-value logic for computing an endpoint (rest-to-posed delta or
1199
+ * ratio) when capturing a transform channel binding. No DOM, no @ikijs/engine —
1200
+ * the single home of the additive-vs-multiplicative rule, reused by both the
1201
+ * part and deformer capture paths.
1202
+ *
1203
+ * Additive channels (translateX, translateY, rotate, scaleX, scaleY) return the
1204
+ * delta: `posedValue - restValue`. The binding will multiply that delta across
1205
+ * the driven range.
1206
+ *
1207
+ * Opacity is multiplicative: returns the ratio `posedValue / restValue`. The
1208
+ * binding will multiply by that ratio across the driven range. When `restValue`
1209
+ * is 0 (a degenerate case: base opacity cannot be represented multiplicatively
1210
+ * since 0 * x ≡ 0), this returns 0 as a documented fallback — it does NOT
1211
+ * recover `posedValue` and is NOT unit-tested as an identity. The store layer
1212
+ * additionally skips an opacity capture when rest opacity is 0, surfacing an
1213
+ * editError.
1214
+ *
1215
+ * Deformer channels never include opacity (they use `IkiMatrixChannel`), so the
1216
+ * opacity branch is reached only for parts.
1217
+ *
1218
+ * Finiteness of the captured value is NOT validated here; the store's
1219
+ * `captureEndpoint` finite-value guard is the appropriate layer for that check.
1220
+ */
1221
+ declare function captureBindingEndpoint(channel: IkiTransformChannel, restValue: number, posedValue: number): number;
1222
+
1223
+ /**
1224
+ * Pure, grid-size-agnostic keyform/offset math for authoring a warp-deformer
1225
+ * grid by dragging. No DOM, no `@ikijs/engine` — the load-bearing testable core.
1226
+ * Constraints derive only from the input array lengths, never the sample grid.
1227
+ */
1228
+ /**
1229
+ * Interpolate the grid offsets at `value`, mirroring the engine's
1230
+ * `accumulateKeyformOffsets` clamp+lerp semantics: clamp to the first/last
1231
+ * keyform (NO extrapolation) and linearly interpolate the bracketing pair.
1232
+ * Returns a NEW array of length `keyforms[0].offsets.length`. Throws on empty.
1233
+ */
1234
+ declare function interpolateGridOffsets(keyforms: {
1235
+ value: number;
1236
+ offsets: number[];
1237
+ }[], value: number): number[];
1238
+ /**
1239
+ * Per-control-point delta of the dragged grid from the rest grid: for each
1240
+ * point `i`, `(draggedX_i - restX_i, draggedY_i - restY_i)`. The DOM layer
1241
+ * assembles the full `restFrameDraggedPoints` (including untouched points), so
1242
+ * this is a straight subtract — no prior-offset blending.
1243
+ *
1244
+ * Both arrays must have the SAME length and that length must be even (x,y
1245
+ * pairs). Returns a NEW array of length `restPoints.length`.
1246
+ */
1247
+ declare function computeGridOffsets(restPoints: number[], restFrameDraggedPoints: number[]): number[];
1248
+ /**
1249
+ * Insert or replace the keyform at `value`, returning a NEW array. If a keyform
1250
+ * already exists with an exact-match `value`, REPLACE its offsets (with a copy);
1251
+ * otherwise INSERT `{ value, offsets: [...offsets] }` at the position that keeps
1252
+ * the array strictly ascending by value. The input array and its keyform objects
1253
+ * are never mutated, and `offsets` is copied so the result never aliases the
1254
+ * caller's array.
1255
+ *
1256
+ * Deliberately RANGE-FREE — a generic, reusable array op. Value-range
1257
+ * enforcement is the command's job, not this helper's.
1258
+ */
1259
+ declare function upsertGridKeyform(keyforms: IkiGridKeyform[], value: number, offsets: number[]): IkiGridKeyform[];
1260
+
1261
+ declare const ATLAS_PADDING = 2;
1262
+ declare const UV_INSET_PX = 0.5;
1263
+ /** Intrinsic pixel size of one decoded image; id is an editor-only stable key. */
1264
+ interface AtlasSource {
1265
+ id: string;
1266
+ width: number;
1267
+ height: number;
1268
+ }
1269
+ /** Sub-image pixel rect within the page, top-left origin, EXCLUDING the gutter. */
1270
+ interface AtlasPlacement {
1271
+ id: string;
1272
+ x: number;
1273
+ y: number;
1274
+ width: number;
1275
+ height: number;
1276
+ }
1277
+ interface AtlasLayout {
1278
+ pageWidth: number;
1279
+ pageHeight: number;
1280
+ placements: AtlasPlacement[];
1281
+ padding: number;
1282
+ }
1283
+ /**
1284
+ * Deterministic shelf/row packer. Sources are sorted by id for stability so
1285
+ * identical inputs always produce an identical layout.
1286
+ *
1287
+ * Padding is one-sided: each placement reserves `padding` px on its RIGHT and
1288
+ * BOTTOM only. Page left/top edges need no gutter.
1289
+ *
1290
+ * pageWidth/pageHeight = tight bound (max x+width+padding, max y+height+padding).
1291
+ * Empty sources → { pageWidth: 0, pageHeight: 0, placements: [], padding }.
1292
+ * Throws a plain Error naming the offending source id on a non-finite or <= 0 dimension.
1293
+ */
1294
+ declare function packAtlas(sources: AtlasSource[], padding?: number): AtlasLayout;
1295
+ /**
1296
+ * Convert a pixel placement into a UV rect, inset by `insetPx` on all four
1297
+ * edges and clamped to [0, 1] so the validator's bounds check always passes.
1298
+ */
1299
+ declare function uvRectFor(placement: AtlasPlacement, page: {
1300
+ width: number;
1301
+ height: number;
1302
+ }, insetPx?: number): IkiUvRect;
1303
+
1304
+ /**
1305
+ * Validate that reparenting `deformerId` under `newParentId` keeps the deformer
1306
+ * hierarchy valid. Pass `newParentId === undefined` to move to root (always legal).
1307
+ * Checks: existence, self-reference, undeclared parent, kind constraint (warp
1308
+ * deformers cannot be parents), and cycle detection via the proposed edge.
1309
+ */
1310
+ declare function validateDeformerReparent(deformers: IkiDeformer[], deformerId: string, newParentId: string | undefined): void;
1311
+ /**
1312
+ * Validate that deleting `deformerId` is safe. Throws when the deformer does
1313
+ * not exist, when another deformer is parented to it (must be reparented or
1314
+ * detached first), when a part is attached to it (must be detached first), or
1315
+ * when a physics chain anchors to it (must be re-anchored or deleted first).
1316
+ *
1317
+ * Every id that can reference a deformer must be covered here: the format
1318
+ * validator rejects a dangling reference at export, so a delete this function
1319
+ * lets through does not fail now — it strands the document in a state
1320
+ * `toIkiModel()` refuses.
1321
+ *
1322
+ * Note: there is no validatePartDelete here, but NOT because parts are
1323
+ * unreferenced — `clip.masks` names parts by id. That invariant is enforced
1324
+ * closer to the edits that could break it: `DeletePart` refuses to remove a
1325
+ * part still used as a mask, and `SetPartMesh` refuses to strip the mesh off
1326
+ * one (masks must be mesh parts). No command creates or edits `clip`.
1327
+ */
1328
+ declare function validateDeformerDelete(deformers: IkiDeformer[], parts: IkiPart[], physicsChains: IkiPhysicsChain[], deformerId: string): void;
1329
+ /**
1330
+ * Validate that attaching part `partId` to deformer `newDeformerId` is valid.
1331
+ * Pass `newDeformerId === undefined` to detach (always legal).
1332
+ * Checks: part existence, undeclared deformer, and mesh-required-for-warp.
1333
+ */
1334
+ declare function validatePartAttach(deformers: IkiDeformer[], partId: string, parts: IkiPart[], newDeformerId: string | undefined): void;
1335
+
1336
+ /**
1337
+ * Create a minimal valid part with a collision-free id and a paint order one
1338
+ * above the current top-most part so it is immediately visible in the viewport.
1339
+ * Uses a distinct non-white blue tint so it is distinguishable from the canvas
1340
+ * background without requiring a texture.
1341
+ *
1342
+ * No optional keys (`texture`, `mesh`, `deformer`, `bindings`, `warps`) are set
1343
+ * — the format treats all of them as absent by default.
1344
+ */
1345
+ declare function createDefaultPart(model: IkiModel): IkiPart;
1346
+ /**
1347
+ * Create a minimal valid matrix deformer rooted at the canvas origin.
1348
+ * `kind` is omitted because the format treats its absence as "matrix" (the
1349
+ * default), keeping the serialised model compact.
1350
+ */
1351
+ declare function createDefaultMatrixDeformer(model: IkiModel): IkiMatrixDeformer;
1352
+ /**
1353
+ * Create a regular grid mesh in part LOCAL space (±0.5 unit frame).
1354
+ *
1355
+ * Vertices span x ∈ [-0.5, 0.5] and y ∈ [-0.5, 0.5] (+y up, engine convention).
1356
+ * Row 0 is the TOP of the grid (y = +0.5); row index increases downward.
1357
+ * UVs are unit-square base coordinates: u = col/cols (0..1 left→right),
1358
+ * v = row/rows (0..1 top→bottom). The top row maps to v=0 because v and y
1359
+ * run in opposite directions — keeps textures upright without a post-flip.
1360
+ * The UV-to-texture remap (atlas rect) is applied later in SetPartMesh, not here.
1361
+ *
1362
+ * Index winding per cell: [BL, BR, TL] then [TL, BR, TR], matching the engine's
1363
+ * implicit-quad convention (see examples/editor/src/mesh-generator.ts).
1364
+ *
1365
+ * Bounds are validated BEFORE any array allocation because this factory runs
1366
+ * before SetPartMesh's parseIkiModel — an unbounded count would freeze the
1367
+ * editor before the format-level 65536 limit is ever reached.
1368
+ */
1369
+ declare function createGridMesh(cols: number, rows: number): IkiMesh;
1370
+ /**
1371
+ * Create a 4×4-cell warp deformer whose rest grid spans a quarter of the
1372
+ * canvas in each direction (`±canvas.width/4` × `±canvas.height/4`). For the
1373
+ * 1000-unit sample canvas this gives x,y ∈ [−250, 250], which is large enough
1374
+ * to cover a typical face part without spilling to the edge.
1375
+ *
1376
+ * `warps` is omitted — the format treats its absence as "rest grid only", so
1377
+ * the deformer is immediately usable without authored keyforms.
1378
+ * `parent` is omitted — the deformer is placed at root; the caller may
1379
+ * reparent it via `SetDeformerParent` after creation.
1380
+ */
1381
+ declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
1382
+
1383
+ export { ALPHA_BBOX_THRESHOLD, ALPHA_OPAQUE, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, DEFAULT_TURN_TARGETS, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, HEAD_BAND, type IrisStrand, type LayerInput, type LayerSetMeasurement, type LayerSetMeasurer, type RgbaLayer, SPECK_CORE_FRACTION, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, type StrandOverlap, type TurnDepths, type TurnSolveReport, TurnTargetError, type TurnTargets, UV_INSET_PX, captureBindingEndpoint, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, createLayerSetMeasurer, denseCoreOf, detectAlphaBbox, foregroundSpan, generateIkiFromLayerSet, headHalfOf, interpolateGridOffsets, isSpeckCore, packAtlas, parseLayerRoles, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };