@ikijs/editor 0.8.1 → 0.10.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,6 +656,408 @@ interface AlphaBbox {
656
656
  */
657
657
  declare function detectAlphaBbox(rgba: ArrayLike<number>, width: number, height: number): AlphaBbox | null;
658
658
 
659
+ /**
660
+ * Per-character tuning, starting from the profile: only the quantities the
661
+ * Live2D samples themselves disagree on by more than their own median
662
+ * (style, not construction). `turn`, `featureLead` and `sway` multiply the
663
+ * profile (1 = as measured); `hairFollow`, `outlineFollow` and `blink`
664
+ * replace its value.
665
+ */
666
+ interface RigStyle {
667
+ /** The whole turn — plate, features and hair (the samples' face slides
668
+ * 0.05–0.25 hh). Ignored when a `turnTargets.eyeShift` is given, which
669
+ * fits it instead. */
670
+ turn?: number;
671
+ /** How far the features lead the plate (the nose leads it 2.2–4.4×). */
672
+ featureLead?: number;
673
+ /** The front hair's share of the face's turn (the samples' 1.0–1.37;
674
+ * profile 1.1). */
675
+ hairFollow?: number;
676
+ /** Its outer edge's share, where the front hair draws the head's outline
677
+ * (profile 0: the outline holds; `hairFollow` rides it whole). */
678
+ outlineFollow?: number;
679
+ /** How far the upper lid comes down, over the eye's height (0.30–0.67). */
680
+ blink?: number;
681
+ /** Hair sway amplitude (0.02–0.36 hh at the extremes). */
682
+ sway?: number;
683
+ }
684
+
685
+ /**
686
+ * The public shapes of the auto-rig: what a host hands `generateIkiFromLayerSet`
687
+ * and what the turn solve reports back. Pixel measurement lives in
688
+ * `layer-measure.ts`; everything here is already-decoded geometry.
689
+ */
690
+
691
+ /** One role layer, as a host measured it off its decoded pixels. */
692
+ interface LayerInput {
693
+ /** Canonical role (see `parseLayerRoles`); becomes the part id. */
694
+ role: string;
695
+ fileName: string;
696
+ canvasW: number;
697
+ canvasH: number;
698
+ /** Alpha bounding box in canvas px (y down) — the crop the atlas holds. */
699
+ bbox: {
700
+ x: number;
701
+ y: number;
702
+ w: number;
703
+ h: number;
704
+ };
705
+ cropW: number;
706
+ cropH: number;
707
+ /** Face only: half the opaque span of each crop row, canvas px (0 = none). */
708
+ rowHalfWidths?: number[];
709
+ /** Nose only: the tight box of its alpha ≥ 128 pixels, canvas px. */
710
+ denseCore?: {
711
+ x: number;
712
+ y: number;
713
+ w: number;
714
+ h: number;
715
+ };
716
+ /** Face only: per crop column, the canvas row of the last pixel of the
717
+ * first dark stroke met going down from the plate's widest row (the jaw's
718
+ * outline, across a neck), or −1 where the column leaves the paint first.
719
+ * Where the head ends and the neck the body keeps begins. */
720
+ jawRows?: number[];
721
+ }
722
+ /**
723
+ * One side's iris against the `hair_front` run it would slide under on the
724
+ * turn, on the row holding the iris centre. Model coordinates: x is canvas x
725
+ * minus half the canvas width, y is half the canvas height minus canvas y.
726
+ */
727
+ interface IrisStrand {
728
+ y: number;
729
+ irisOuter: number;
730
+ irisInner: number;
731
+ runOuter: number;
732
+ /** The run's face-side end; `null` for a fringe spanning the face. */
733
+ runFace: number | null;
734
+ }
735
+ /**
736
+ * Head-turn cues at ±30°, as `measure_turn_reference` reads them off a front
737
+ * and a turned image. Shifts are fractions of the head half-width, taken as
738
+ * magnitudes: that tool signs them by the reference's turn direction.
739
+ */
740
+ interface TurnTargets {
741
+ eyeShift?: number;
742
+ farEyeRatio?: number;
743
+ silhouetteRatio?: number;
744
+ noseShift?: number;
745
+ mouthShift?: number;
746
+ /** The head half-width the shifts are fractions of, canvas px. */
747
+ headHalfWidth?: number;
748
+ }
749
+ /** A turn target this layer set cannot reach, or turn input that is malformed. */
750
+ declare class TurnTargetError extends Error {
751
+ constructor(message: string);
752
+ }
753
+ /**
754
+ * The Live2D profile's own turn (`profile.ts`), in the cues' units — what a
755
+ * rig with no `turnTargets` renders on a head whose silhouette at the eye row
756
+ * is back hair about 1 hh (eye row → chin) wide. eyeShift: the eye pair moves
757
+ * 0.211 hh at ±30 and that silhouette's centre −0.009 hh, over a 1 hh head
758
+ * half-width, 0.22. farEyeRatio: the far eye narrows to 0.85 and the near one
759
+ * widens to 1.085, and the cue is their ratio, 0.78. silhouetteRatio: the
760
+ * silhouette holds its width.
761
+ *
762
+ * With no target given the rig renders the profile itself, whatever the cues
763
+ * then read on its art (a head whose side locks draw its silhouette, and ride
764
+ * the face, reads a smaller eyeShift); a given target is fitted.
765
+ */
766
+ declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
767
+ /** How far each feature family stands in front of the plate, as the depth a
768
+ * rotation would need to give its shift at ±30 (shift / sin 30°), in head
769
+ * half-widths — the nose leads the mouth and the mouth the eyes. */
770
+ interface TurnDepths {
771
+ eye: number;
772
+ nose: number;
773
+ mouth: number;
774
+ }
775
+ /** How much of a far iris's painted row the bangs' run covers. */
776
+ interface StrandOverlap {
777
+ /** The turn stop (AngleX, degrees) where the coverage is largest. */
778
+ deg: number;
779
+ /** Covered width there, canvas px. */
780
+ px: number;
781
+ /** The same in head half-widths. */
782
+ hh: number;
783
+ /** Covered width at rest. */
784
+ restPx: number;
785
+ /** Whether the iris went no deeper under the run than it is painted.
786
+ * Always `false` for a fringe spanning the face (`runFace: null`), which
787
+ * has no side to keep the iris clear of. */
788
+ held: boolean;
789
+ }
790
+ interface TurnSolveReport {
791
+ /** The curvature the plate would need to put the eyes as far in front of
792
+ * its edge as their lead over it does, as a radius, canvas px. */
793
+ radius: number;
794
+ /** The measured head half-width, canvas px, which the shift cues are
795
+ * fractions of — on a set with none measured (hairless) the face crop's
796
+ * half-width, while the cues are then fractions of the plate's painted
797
+ * half-width at the eye row. */
798
+ holdBase: number;
799
+ depths: TurnDepths;
800
+ /** What the rig renders at ±30° (mean of both directions). */
801
+ achieved: {
802
+ eyeShift: number;
803
+ farEyeRatio: number;
804
+ silhouetteRatio: number;
805
+ };
806
+ /** Targets cut down to what this layer set can do. */
807
+ clamped: (keyof TurnTargets)[];
808
+ strandOverlap?: {
809
+ left?: StrandOverlap;
810
+ right?: StrandOverlap;
811
+ };
812
+ }
813
+ interface HeadEdges {
814
+ left: {
815
+ role: string;
816
+ x: number;
817
+ }[];
818
+ right: {
819
+ role: string;
820
+ x: number;
821
+ }[];
822
+ }
823
+ interface GenerateOptions {
824
+ turnTargets?: TurnTargets;
825
+ /** Per-character tuning from the profile's defaults (`profile.ts`). */
826
+ style?: RigStyle;
827
+ onTurnSolved?: (report: TurnSolveReport) => void;
828
+ headEdges?: HeadEdges;
829
+ strandEdges?: {
830
+ left?: IrisStrand;
831
+ right?: IrisStrand;
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Map file names to roles. Throws on an unknown role, a role named twice, or
837
+ * a required role (`face`, `eye_L`, `eye_R`, `mouth`) missing.
838
+ */
839
+ declare function parseLayerRoles(fileNames: string[]): {
840
+ role: string;
841
+ fileName: string;
842
+ }[];
843
+
844
+ /**
845
+ * `generateIkiFromLayerSet`: role layers in, a rigged `.iki` out.
846
+ *
847
+ * The rig's shape follows a Live2D default rig (`profile.ts`): a torso that
848
+ * only breathes; a head deformer that rolls about the chin and breathes; one
849
+ * small warp grid per feature (each eye, each brow, the nose, the mouth),
850
+ * translating and foreshortening it by its own lead over the plate; and the
851
+ * plate, the hair and the blush moved by per-vertex keyforms of their own —
852
+ * the plate as up to four islands of one drawing: a head that slides, a neck
853
+ * that stays and two ears that lag. Blink, gaze, brows, mouth, breath and
854
+ * hair sway are part warps and bindings under those.
855
+ */
856
+
857
+ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
858
+ width: number;
859
+ height: number;
860
+ }, options?: GenerateOptions): IkiModel;
861
+
862
+ /**
863
+ * The alpha rules a layer set is measured by, and the pre-rig measurement of
864
+ * that set, in one DOM-free copy — the way `detectAlphaBbox` is one.
865
+ *
866
+ * Both auto-rig hosts — the editor app (canvas `getImageData`) and
867
+ * `@ikijs/mcp`'s `auto_rig_from_layers` (a `sharp` raw buffer) — hand
868
+ * `createLayerSetMeasurer` the RGBA they decoded and rig what it measures, and
869
+ * `@ikijs/mcp`'s other tools (`measure_turn_reference`, `measure_layers`, the
870
+ * composer) read the same rules. Two copies would have to be kept identical by
871
+ * hand, or the head a render measures back, or the nose core the composer
872
+ * placed, would part from the one the rig was fitted to. Only decoding differs
873
+ * between the hosts.
874
+ */
875
+
876
+ /** Half-height of the row band the head span is taken over, in px. Fixed at
877
+ * ±10 rows rather than scaled to the image, and that is the convention: any
878
+ * other measurement of head width meant to be compared with the one
879
+ * `@ikijs/mcp`'s `measure_turn_reference` takes off a render — one taken off
880
+ * layer alpha, a hand script — has to span the same band. Shared by that tool
881
+ * and `createLayerSetMeasurer`, so the head it spans off the layers' alpha
882
+ * union spans this band too. */
883
+ declare const HEAD_BAND = 10;
884
+ /** Alpha at or above this is opaque. Shared by `@ikijs/mcp`'s
885
+ * `measure_turn_reference`, whose "alpha" foreground rule reads an engine
886
+ * render's silhouette by it, and by the layer-alpha silhouette
887
+ * `createLayerSetMeasurer` measures the head on, which has to follow that
888
+ * rule to be comparable with a render. */
889
+ declare const ALPHA_OPAQUE = 128;
890
+ /**
891
+ * A layer's dense core: the tight box of its pixels at alpha ≥ ALPHA_OPAQUE,
892
+ * not grown. A soft-alpha part (a nose drawn as a shaded bump) is mostly
893
+ * feather, and the core is the drawing a viewer reads inside it — `@ikijs/mcp`'s
894
+ * composer sizes and places the nose by it, and `createLayerSetMeasurer` reads
895
+ * the same core as the nose's `LayerInput.denseCore`, its turn landmark. It is
896
+ * not the crop: that stays the alpha ≥ 8 box grown by 1 px (`detectAlphaBbox`).
897
+ * `null` when no pixel reaches the threshold, a part painted wholly
898
+ * translucent. A core that is a speck of its part (`isSpeckCore`) is not the
899
+ * drawing either, and both readers fall back to the whole part, as they do for
900
+ * `null`.
901
+ */
902
+ declare function denseCoreOf(rgba: ArrayLike<number>, width: number, height: number): {
903
+ x: number;
904
+ y: number;
905
+ w: number;
906
+ h: number;
907
+ } | null;
908
+ /**
909
+ * The fraction of its part's width, or of its height, under which a dense core
910
+ * is a speck (`isSpeckCore`). Measured core/part ratios, width / height: the
911
+ * hero's composed nose 0.72 / 0.76 (core 39×63 in a 54×83 crop), three
912
+ * generated shaded-bump noses (the plugin's 0.11 nose prompt) 0.75–0.85 /
913
+ * 0.79–0.91, `@ikijs/mcp`'s `writeSoftNose` test fixture 0.56 / 0.63 and its
914
+ * `writeSoftNoseLayers` 0.40 / 0.40, a nostril mark ≤ 0.1. So the line sits at
915
+ * least 1.6× under every real nose and fixture, and 2.5× over a speck. At the
916
+ * line, sizing by the core would scale the whole part to 4× its layout width,
917
+ * the largest blow-up `@ikijs/mcp`'s composer still accepts.
918
+ */
919
+ declare const SPECK_CORE_FRACTION = 0.25;
920
+ /**
921
+ * Whether a dense core is a speck of its part: narrower than
922
+ * SPECK_CORE_FRACTION of the part's width, or shorter than that fraction of
923
+ * its height (strictly `<`). Either dimension counts, because the core does
924
+ * more than size a nose: its bottom row is the tip the composer places, and
925
+ * its top-centre the rig's tilt pivot, so a sliver misplaces the nose as
926
+ * surely as a speck mis-sizes it. A speck is a lone nostril mark or highlight,
927
+ * not the drawing, and every reader falls back to the whole part: `@ikijs/mcp`'s
928
+ * compose sizes and places its trimmed source part whole, and
929
+ * `createLayerSetMeasurer` hands the rig no core, so the crop stands in.
930
+ *
931
+ * The two apply it to different pixels — compose to the source part before
932
+ * resampling, the rig and `measure_layers` to the composed layer against its
933
+ * crop — so their verdicts can part, but only for a core within about a
934
+ * resampled pixel of the line, or one resampling pushes across ALPHA_OPAQUE.
935
+ * Compose's report carries its source verdict when that was a speck, and
936
+ * otherwise what the composed layer shows, so it warns of a parting either
937
+ * way; `measure_layers` reports the layer alone.
938
+ */
939
+ declare function isSpeckCore(core: {
940
+ w: number;
941
+ h: number;
942
+ }, part: {
943
+ w: number;
944
+ h: number;
945
+ }): boolean;
946
+ /**
947
+ * Outermost set pixels of `mask` across the row band `rowLo..rowHi` (clamped
948
+ * to the image) — the silhouette span at that height. An empty band returns
949
+ * `left > right`, which every caller has to check for.
950
+ *
951
+ * Shared by `@ikijs/mcp`'s `measure_turn_reference`, which spans a render's
952
+ * head with it, and `createLayerSetMeasurer`, which spans the layers' alpha
953
+ * union: the two heads are compared, so they have to be spanned the same way.
954
+ */
955
+ declare function foregroundSpan(mask: Uint8Array, width: number, height: number, rowLo: number, rowHi: number): {
956
+ left: number;
957
+ right: number;
958
+ };
959
+ /**
960
+ * Half of a silhouette span, in px. The span is INCLUSIVE — `left` and `right`
961
+ * are both foreground columns — so the head is `right - left + 1` px wide.
962
+ * Shared by `@ikijs/mcp`'s `measure_turn_reference` and
963
+ * `createLayerSetMeasurer`: the two head half-widths are compared against each
964
+ * other, so they cannot be halved differently.
965
+ */
966
+ declare function headHalfOf(span: {
967
+ left: number;
968
+ right: number;
969
+ }): number;
970
+ /** One decoded layer, as a host hands it to `LayerSetMeasurer.add`. */
971
+ interface RgbaLayer {
972
+ /** Canonical role, e.g. "eye_L" — the host has run `parseLayerRoles` on
973
+ * the set already. */
974
+ role: string;
975
+ /** Original file name: the `LayerInput`'s own, and named in errors. */
976
+ fileName: string;
977
+ /** Straight-alpha RGBA, stride 4, `canvas.width × canvas.height` pixels —
978
+ * canvas `ImageData.data` or a `sharp` raw buffer alike. */
979
+ rgba: ArrayLike<number>;
980
+ }
981
+ /**
982
+ * Measures a layer set one decoded layer at a time, so a host holds one
983
+ * full-canvas RGBA buffer at once, however many layers the set has. Call
984
+ * `finish` once, after the last `add`: it hands back its own `layers` array,
985
+ * which a later `add` would change.
986
+ */
987
+ interface LayerSetMeasurer {
988
+ /**
989
+ * Measure one layer while its `rgba` is in memory, keeping no reference to
990
+ * it: only the set's opaque union, this layer's per-row opaque extent and,
991
+ * for `hair_front`, its opaque mask outlive the call, so the host may drop
992
+ * `rgba` as soon as it returns. Returns the layer's `LayerInput` — its crop
993
+ * box, the face's `rowHalfWidths`, the nose's `denseCore` unless that is a
994
+ * speck of its crop (`isSpeckCore`) — or `null` for an empty layer, one with
995
+ * no pixel at or above `ALPHA_BBOX_THRESHOLD`, which it records nothing for:
996
+ * the host reports that with its own error. Throws, naming the file, when
997
+ * `rgba` is not `width × height × 4` long; checking the decoded dimensions
998
+ * is the host's job.
999
+ */
1000
+ add(layer: RgbaLayer): LayerInput | null;
1001
+ /** The set's measurement, over every layer `add` recorded. The set must be
1002
+ * one `parseLayerRoles` accepted; otherwise this may throw, or measure a
1003
+ * partial set the generator then refuses. */
1004
+ finish(): LayerSetMeasurement;
1005
+ }
1006
+ interface LayerSetMeasurement {
1007
+ /** Every recorded layer's `LayerInput`, in `add` order. */
1008
+ layers: LayerInput[];
1009
+ /** The head's half-width at the eye row, canvas px: half the opaque
1010
+ * (alpha ≥ ALPHA_OPAQUE) union's span over ±HEAD_BAND rows about the row
1011
+ * of the iris centres, or of the eye centres without irises. Absent when
1012
+ * the union has no span there — a layer set painted translucent below that
1013
+ * threshold. */
1014
+ headHalfWidth?: number;
1015
+ /** Whether `headHalfWidth` is wider than the face plate, and so applied;
1016
+ * the face plate stands in for it otherwise, as the generator's own
1017
+ * fallback. */
1018
+ headHalfWidthApplied: boolean;
1019
+ /**
1020
+ * Ready to go into `generateIkiFromLayerSet`'s options, each key present
1021
+ * only when measured: `turnTargets.headHalfWidth` when it is applied (merge
1022
+ * the caller's own targets under it), `headEdges` — every layer's own
1023
+ * opaque extent in the same eye-row band, per side, in model x — with it,
1024
+ * and `strandEdges` whenever the set has `hair_front`, `iris_L` and `iris_R`
1025
+ * and a side measures one (`IrisStrand`).
1026
+ */
1027
+ turnOptions: {
1028
+ turnTargets?: {
1029
+ headHalfWidth: number;
1030
+ };
1031
+ headEdges?: {
1032
+ left: {
1033
+ role: string;
1034
+ x: number;
1035
+ }[];
1036
+ right: {
1037
+ role: string;
1038
+ x: number;
1039
+ }[];
1040
+ };
1041
+ strandEdges?: {
1042
+ left?: IrisStrand;
1043
+ right?: IrisStrand;
1044
+ };
1045
+ };
1046
+ }
1047
+ /**
1048
+ * Start measuring a layer set on a `canvas`-sized canvas: the pass a host runs
1049
+ * between decoding its layers and `generateIkiFromLayerSet`. `add` each
1050
+ * decoded layer, then `finish`.
1051
+ *
1052
+ * Throws before allocating anything when `canvas.width` or `canvas.height` is
1053
+ * not a positive safe integer, or `width × height × 4` is not a safe integer.
1054
+ * How large a canvas to accept at all is each host's own limit.
1055
+ */
1056
+ declare function createLayerSetMeasurer(canvas: {
1057
+ width: number;
1058
+ height: number;
1059
+ }): LayerSetMeasurer;
1060
+
659
1061
  /**
660
1062
  * Pure, binding-value logic for computing an endpoint (rest-to-posed delta or
661
1063
  * ratio) when capturing a transform channel binding. No DOM, no @ikijs/engine —
@@ -842,342 +1244,4 @@ declare function createGridMesh(cols: number, rows: number): IkiMesh;
842
1244
  */
843
1245
  declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
844
1246
 
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
- * The layer's painted half-width on every CROP row, top → bottom, one entry
880
- * per row (`cropH` of them): half the row's opaque span in canvas px under
881
- * 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.
883
- * Optional, and only the FACE's is read: it gives the face plate a
884
- * row-dependent turn radius and a chin swing (`faceRowProfile`,
885
- * `turnSurface`); absent — as the editor's own import leaves it — the plate
886
- * turns on one radius on every row. Validated before anything reads it: an
887
- * array of length `cropH` whose every entry is a finite number in
888
- * [0, cropW / 2], the most an inclusive span inside the crop can be.
889
- */
890
- rowHalfWidths?: number[];
891
- /**
892
- * The tight box of the layer's alpha ≥ 128 pixels, in image coords like
893
- * `bbox` and NOT grown: the drawing a viewer reads inside a soft feather,
894
- * which the crop's alpha ≥ 8 rule takes in whole. Optional, and only the
895
- * NOSE's is read: its centre and width are the nose's turn landmark
896
- * (`turnLandmarks`), so a shaded nose is fitted by the drawing rather than
897
- * 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
900
- * before anything reads it: a plain object whose x, y, w and h are finite,
901
- * w and h positive, and the box inside `bbox`.
902
- */
903
- denseCore?: {
904
- x: number;
905
- y: number;
906
- w: number;
907
- h: number;
908
- };
909
- }
910
- /**
911
- * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
912
- *
913
- * Steps:
914
- * 1. Normalize each filename → role (normalizeRole).
915
- * 2. Eagerly check each role against ROLE_TABLE — unknown roles throw early
916
- * with the offending fileName included in the message.
917
- * 3. Call assertRoleSet to check duplicates + required roles.
918
- *
919
- * Throws a path-qualified Error on any contract violation.
920
- */
921
- declare function parseLayerRoles(fileNames: string[]): {
922
- role: string;
923
- fileName: string;
924
- }[];
925
- /**
926
- * One side's iris and the bangs' run it would slide under on the turn, as
927
- * real pixel boundaries on ONE image row — the row holding that iris's centre
928
- * — in model coordinates: x is canvas x minus half the canvas width, like
929
- * `bboxToTransform`'s crop edges, and `y` is that row's centre. Every x is the
930
- * painted edge facing its clear neighbour, read under one opacity rule, so the
931
- * gap between an iris edge and a run edge is a real distance with no margin to
932
- * add. "Outer" is toward the head's edge on that side. The run is the one a
933
- * 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.
936
- *
937
- * One shape covers three cases:
938
- * - an ordinary side strand outward of a clear iris (`runFace` outward of
939
- * `irisOuter`);
940
- * - a run over the iris centre that clears on the face side (`runFace`
941
- * inward of that centre);
942
- * - a fringe spanning the face: the run still covers the OTHER iris's
943
- * centre, so it has no face-side end on this side and `runFace` is
944
- * `null` — an absent edge, not an invented one.
945
- */
946
- interface IrisStrand {
947
- /** The iris row's centre, model y. */
948
- y: number;
949
- /** The outer end of that iris layer's opaque span on the row. */
950
- irisOuter: number;
951
- /** Its inner end, toward the face. */
952
- irisInner: number;
953
- /** The outer end of the `hair_front` run the iris would slide under. */
954
- runOuter: number;
955
- /** The run's face-side end, or `null` when it reaches the other iris's
956
- * centre. */
957
- runFace: number | null;
958
- }
959
- /**
960
- * What a head does at full turn, as the cues `measure_turn_reference` reads off
961
- * a front/turned reference pair. The turn is fitted to these — the cylinder's
962
- * radius and the features' depths are solved from them, not tuned.
963
- *
964
- * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
965
- * reference measures, and the nose and the mouth derive from the eyes when they
966
- * are not given.
967
- *
968
- * What happens when the layer set cannot reach a target depends on where the
969
- * 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 — except a
971
- * `silhouetteRatio` when no ratio in its own [0.5, 1.5] range renders on this
972
- * layer set: that throw names the field and the whole range instead of an
973
- * offer, since there is none narrower. Leaving the field out still rigs,
974
- * because a defaulted one clamps.
975
- *
976
- * A number this module filled in — a default, or a nose/mouth derived from the
977
- * eyes — is a style prior, not a promise about this character: it is CLAMPED
978
- * to what the layer set can do and the rig is built. The alternative is a
979
- * generator that refuses its own defaults, which would leave a perfectly good
980
- * layer set with no model at all. `solveTurnModel` reports which fields it
981
- * clamped.
982
- *
983
- * One measurement is clamped too: an `eyeShift` past the room the art leaves
984
- * the far eye — the face plate's edge, and the bangs' side strand when
985
- * `strandEdges` is given — whoever passed it. That room is a fact about this
986
- * layer set, not about the reference the shift was measured on, so the rig
987
- * takes as much of the shift as the art allows and keeps fitting the other
988
- * cues. An `eyeShift` below the slide the face's own turn already gives the
989
- * eyes is still refused: no depth reaches it.
990
- */
991
- interface TurnTargets {
992
- /** How far the eye pair's centre slides toward the far side at full turn, as
993
- * a fraction of the head's half-width. The sign is ignored: which way is
994
- * the turn's own business. */
995
- eyeShift?: number;
996
- /** The far eye's width over the near eye's at full turn, divided by the same
997
- * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
998
- farEyeRatio?: number;
999
- /** The head's half-width at full turn over its half-width at rest, as a
1000
- * RENDER measures it — the outermost opaque edge on each side, whichever
1001
- * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
1002
- * produces it is fitted to it, so a measured one is met or refused, never
1003
- * quietly rendered as something else. */
1004
- silhouetteRatio?: number;
1005
- /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
1006
- noseShift?: number;
1007
- /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
1008
- mouthShift?: number;
1009
- /** The head's own half-width at the eye row, in canvas px: what the shift
1010
- * fractions are fractions OF, and the rest distance the silhouette hold
1011
- * pivots on. Without it both fall back to the face plate's half-width, which
1012
- * is narrower than the head the hair draws, so the shifts land short — an
1013
- * approximation for callers that have no pixels, not a default to prefer. */
1014
- headHalfWidth?: number;
1015
- }
1016
- /**
1017
- * A `turnTargets` field this generator will not build a rig from: not a number,
1018
- * outside its range, unreachable on this layer set, or a measured head narrower
1019
- * than the face plate it is drawn around. Every one of them carries a number
1020
- * the CALLER passed, so a host can tell them apart from the invariant breaks the
1021
- * rest of the generator throws and report them as bad input instead of failing.
1022
- */
1023
- declare class TurnTargetError extends Error {
1024
- constructor(message: string);
1025
- }
1026
- /**
1027
- * The reference the turn is fitted to when the caller measured none of its own:
1028
- * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
1029
- * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
1030
- * normalised by the same ratio at rest, which is what the field means).
1031
- * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
1032
- */
1033
- declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
1034
- /** The turn depth of each feature family: the share of the pinned bulk slide
1035
- * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
1036
- interface TurnDepths {
1037
- eye: number;
1038
- nose: number;
1039
- mouth: number;
1040
- }
1041
- /** What the turn solve settled on, for a caller that wants to see it — the
1042
- * cues it reached and the targets it had to cut down to reach them. */
1043
- interface TurnSolveReport {
1044
- /** Cylinder radius the face-warp bake and the parallax unit were built on. */
1045
- radius: number;
1046
- /** Rest distance from the face centre the silhouette hold pivots on. */
1047
- holdBase: number;
1048
- depths: TurnDepths;
1049
- /** What the rig actually reaches, in the targets' own units. */
1050
- achieved: {
1051
- eyeShift: number;
1052
- farEyeRatio: number;
1053
- silhouetteRatio: number;
1054
- };
1055
- /** The targets the rig fell short of, cut down to what it can reach: every
1056
- * defaulted field it could not reach, and an `eyeShift` — the caller's own
1057
- * too — past the room the art leaves the far eye (the face plate's edge,
1058
- * and the bangs' strand when `strandEdges` was given). Empty when every
1059
- * target was met. */
1060
- clamped: (keyof TurnTargets)[];
1061
- /** Per side whose far iris the bangs' run covers at some far stop at the
1062
- * fitted radius and depth (`options.strandEdges`): how much of it. Absent
1063
- * when no side has an entry. Never a clamp: `clamped` still names an
1064
- * `eyeShift` the strand bound cut short. */
1065
- strandOverlap?: {
1066
- left?: StrandOverlap;
1067
- right?: StrandOverlap;
1068
- };
1069
- }
1070
- /**
1071
- * How much of one side's far iris the bangs' run covers: the length of the
1072
- * landed iris span `[irisOuter, irisInner]` on its row that lies inside the
1073
- * landed run `[runOuter, runFace]` — face-ward without end for a run with no
1074
- * face-side edge (`runFace: null`) — at the far stop where that is largest.
1075
- * Always read off real landed boundaries (`IrisStrand`).
1076
- */
1077
- interface StrandOverlap {
1078
- /** The far stop the covered width is largest at, degrees. */
1079
- deg: number;
1080
- /** That covered width, px. */
1081
- px: number;
1082
- /** `px` over the head half-width the shift cues are fractions of. */
1083
- hh: number;
1084
- /** The covered width as painted, at rest, px. */
1085
- restPx: number;
1086
- /** Whether this side's strand bound held at the fitted depth — the iris
1087
- * kept no deeper under the run than it is painted, at every far stop. When
1088
- * false the fitted radius could not hold it (always so for `runFace:
1089
- * null`), the eyes' turn depth is 0, and `px` is the coverage that is
1090
- * left. */
1091
- held: boolean;
1092
- }
1093
- /**
1094
- * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
1095
- * valid IkiModel ready for parseIkiModel.
1096
- *
1097
- * - Validate all inputs before deriving anything.
1098
- * - Place parts at source-derived positions (bboxToTransform, unshifted).
1099
- * - Emit the standard parameters (STANDARD_PARAMETERS), plus a conditional
1100
- * HairSwayX/Z pair + hair-sway physics rigs when a hair_front layer is
1101
- * present.
1102
- * - Size every turn group's grid to its own members and the virtual lattice
1103
- * to those grids, then solve the turn on them (turnSetup, solveTurnModel):
1104
- * one surface — radius, travel, the head-level nod radius — that every
1105
- * bake below samples.
1106
- * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
1107
- * turn translates nothing rigidly; its travel is in the group grids), one
1108
- * warp deformer per turn group present — `faceWarp` for the plate,
1109
- * `eyeWarp_L/R`, `mouthWarp`, `noseWarp`, `browWarp_L/R`, `blushWarp_L/R`
1110
- * — each a warp2d over its own grid baked from that surface with the
1111
- * family's solved depth as keyform geometry (bakeTurnGroupWarp2D), and
1112
- * bodyDeformer when a body layer is present (matrix, torso-base pivot,
1113
- * AngleX follow + Breath follow).
1114
- * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by
1115
- * meshCellsFor + role bindings, each face-family part on its group's warp.
1116
- * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.
1117
- * - Part ids equal the role string (deterministic, no crypto.randomUUID).
1118
- * - Return parseIkiModel(structuredClone(model)) — every caller gets a
1119
- * validated model; bad assembly fails loudly.
1120
- *
1121
- * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1122
- * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1123
- * passed that this layer set cannot reach throws; one that came from the
1124
- * defaults is clamped to what it can do, and so is a caller's `eyeShift` past
1125
- * the room the art leaves the far eye — see TurnTargets for why, and pass
1126
- * `options.onTurnSolved` to see which ones were clamped and what the turn
1127
- * ended up reaching.
1128
- *
1129
- * `options.strandEdges` gives each side's iris and the bangs' run it would
1130
- * slide under as measured pixel edges (`IrisStrand`), and with it the turn
1131
- * keeps each far iris from sliding under that run any further than it is
1132
- * painted, best effort: where no radius the fit can use holds that, the rig
1133
- * is still built, with the eyes' depth at 0, and the report's
1134
- * `strandOverlap` says how much of the iris the bangs cover.
1135
- */
1136
- declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1137
- width: number;
1138
- height: number;
1139
- }, options?: {
1140
- turnTargets?: TurnTargets;
1141
- /** Called once, after a turn is solved, with what it settled on. Not called
1142
- * for a layer set without a nose, which solves no turn at all. */
1143
- onTurnSolved?: (report: TurnSolveReport) => void;
1144
- /** Every role with an opaque pixel in the eye-row band, per side, when
1145
- * `turnTargets.headHalfWidth` was measured off that same union — a
1146
- * companion to it, not a turn target itself (the caller-facing contract
1147
- * stays the three cues). See `TurnSolveContext.headEdges`. */
1148
- headEdges?: {
1149
- left: {
1150
- role: string;
1151
- x: number;
1152
- }[];
1153
- right: {
1154
- role: string;
1155
- x: number;
1156
- }[];
1157
- };
1158
- /** Per side (`left` the −x side, `right` the +x side, like `headEdges`),
1159
- * that side's iris and the bangs' run it would slide under on the turn,
1160
- * as real pixel edges on the row holding the iris's centre — see
1161
- * `IrisStrand` for the contract and its three cases (a strand outward of
1162
- * a clear iris, a run over the iris centre that clears on the face side,
1163
- * a fringe spanning the face). A side's iris is whichever of `iris_L` /
1164
- * `iris_R` sits on that side; the sides are independent, so either may
1165
- * be absent. With it the turn keeps each far iris from sliding under its
1166
- * run any further than it is painted, best effort: never a refusal. The
1167
- * report's `strandOverlap` gives, per side whose far iris the run still
1168
- * covers at some far stop, the covered width at the stop where it is
1169
- * largest, next to the painted one, and whether the bound held — `true`
1170
- * for an iris painted under its run that the turn took no deeper,
1171
- * `false` where the fitted radius could not hold it and the eyes' turn
1172
- * depth is 0. It is always
1173
- * validated against the layers first (`validateStrandEdges`), but only
1174
- * the turn solve reads it, so like `headEdges` it has no effect on the
1175
- * rig without a nose; absent or `{}`, the rig is the one built without
1176
- * it. */
1177
- strandEdges?: {
1178
- left?: IrisStrand;
1179
- right?: IrisStrand;
1180
- };
1181
- }): IkiModel;
1182
-
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 };
1247
+ 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, type RigStyle, 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 };