@ikijs/editor 0.8.0 → 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/README.md +37 -28
- package/dist/index.d.mts +405 -199
- package/dist/index.d.ts +405 -199
- package/dist/index.js +325 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +317 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
* (
|
|
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 —
|
|
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 —
|
|
899
|
-
*
|
|
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
|
-
* (
|
|
935
|
-
* the row as clear), so an iris under only such a wisp counts
|
|
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
|
|
@@ -967,12 +782,18 @@ interface IrisStrand {
|
|
|
967
782
|
*
|
|
968
783
|
* What happens when the layer set cannot reach a target depends on where the
|
|
969
784
|
* target came from. A number the CALLER passed is a measurement, so an
|
|
970
|
-
* unreachable one throws, naming the field and what was on offer
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
785
|
+
* unreachable one throws, naming the field and what was on offer — except a
|
|
786
|
+
* `silhouetteRatio` when no ratio in its own [0.5, 1.5] range renders on this
|
|
787
|
+
* layer set: that throw names the field and the whole range instead of an
|
|
788
|
+
* offer, since there is none narrower. Leaving the field out still rigs,
|
|
789
|
+
* because a defaulted one clamps.
|
|
790
|
+
*
|
|
791
|
+
* A number this module filled in — a default, or a nose/mouth derived from the
|
|
792
|
+
* eyes — is a style prior, not a promise about this character: it is CLAMPED
|
|
793
|
+
* to what the layer set can do and the rig is built. The alternative is a
|
|
794
|
+
* generator that refuses its own defaults, which would leave a perfectly good
|
|
795
|
+
* layer set with no model at all. `solveTurnModel` reports which fields it
|
|
796
|
+
* clamped.
|
|
976
797
|
*
|
|
977
798
|
* One measurement is clamped too: an `eyeShift` past the room the art leaves
|
|
978
799
|
* the far eye — the face plate's edge, and the bangs' side strand when
|
|
@@ -1174,4 +995,389 @@ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
|
|
|
1174
995
|
};
|
|
1175
996
|
}): IkiModel;
|
|
1176
997
|
|
|
1177
|
-
|
|
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 };
|