@ikijs/editor 0.9.0 → 0.10.1

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 CHANGED
@@ -49,60 +49,64 @@ leaves the document untouched.
49
49
  | Grid keyforms | `computeGridOffsets`, `interpolateGridOffsets`, `upsertGridKeyform` |
50
50
  | Factories | `createDefaultPart`, `createDefaultMatrixDeformer`, `createDefaultWarpDeformer`, `createGridMesh` |
51
51
  | Pixels | `detectAlphaBbox`, `ALPHA_BBOX_THRESHOLD`, `AlphaBbox`, `ALPHA_OPAQUE`, `HEAD_BAND`, `SPECK_CORE_FRACTION`, `denseCoreOf`, `isSpeckCore`, `foregroundSpan`, `headHalfOf` |
52
- | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles`, `createLayerSetMeasurer`, `TurnTargets`, `DEFAULT_TURN_TARGETS`, `TurnSolveReport` |
52
+ | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles`, `createLayerSetMeasurer`, `TurnTargets`, `DEFAULT_TURN_TARGETS`, `TurnSolveReport`, `RigStyle` |
53
53
  | Bindings | `captureBindingEndpoint` |
54
54
 
55
55
  ## Auto-rig
56
56
 
57
57
  `generateIkiFromLayerSet` turns role-named layers (`face`, `eye_L`, `eye_R`,
58
58
  `mouth`, plus optional `iris_*`, `brow_*`, `lash_*`, `hair_front`, `hair_back`,
59
- …) into a rigged model that blinks, gazes, opens its mouth, turns its head on a
60
- torso that follows and breathes, and emotes with its brows — including a
59
+ …) into a rigged model that blinks, gazes, opens its mouth, turns its head over
60
+ a torso that breathes, and emotes with its brows — including a
61
61
  hair-sway physics rig when a `hair_front` layer is present.
62
62
 
63
- On the turn the head's outline holds still — the bangs that draw it pin their
64
- own silhouette — while everything inside it slides: the face plate, its
65
- features, and the bangs' own inner strands. That slide is the plate's own ask, a
66
- quarter of its half-width, capped to what the held outline has room for when
67
- `turnTargets.headHalfWidth` was measured and uncut when it was not. That turn is
68
- **fitted, not tuned**: on a layer set with a `nose`, the cylinder's radius and
69
- each feature's own depth on top of that slide are solved from
70
- `options.turnTargets` — the cues a 30° reference measures (how far the eye pair
71
- slides, how much the far eye foreshortens, whether the silhouette holds),
72
- defaulting to `DEFAULT_TURN_TARGETS`. The features' slide is bounded by the art:
73
- every feature has to stay on the face plate, because past its contour the far
74
- eye is drawn over the side hair, which still bends with the plate inside that
75
- held outline and swallows it. Given `options.strandEdges`, the eyes are also
76
- kept from sliding the far iris under the bangs' side strand any further than it
77
- is painted — best effort, never a refusal: the fit prefers radii where some eye
78
- depth holds that, and where none does (a fringe spanning the face included) the
79
- rig is still built with the eyes' turn depth at 0. The report's `strandOverlap`
80
- gives, per side whose far iris the bangs' run still covers at some far stop,
81
- the covered width: how much of the iris's painted row the run covers at the
82
- stop where that is largest, in px and head half-widths, next to how much it
83
- covers at rest, and whether the bound held — `true` for an iris painted under
84
- its run that the turn took no deeper, `false` where the fitted radius could not
85
- hold it and the eyes' turn depth is 0.
86
-
87
- A target that came from the defaults is a style prior, not a measurement of
88
- this character, so it is **clamped** to what the layer set can do and the rig is
89
- built. So is an `eyeShift` you passed that runs past the room the art leaves the
90
- far eye — the face plate's edge, or the bangs' strand — since that room is a
91
- fact about these layers, not about the reference. Every other target you passed
92
- that this layer set cannot reach **throws**, naming the field and the range it
93
- could have had: a `farEyeRatio` or `silhouetteRatio` no radius renders, a
94
- `noseShift` / `mouthShift` outside what that feature can reach, or an
95
- `eyeShift` smaller than the slide the face's own turn already gives the eyes.
96
- Pass `options.onTurnSolved` to see what the turn settled on and which targets
97
- were clamped.
63
+ The rig reproduces a Live2D default rig, measured on the Cubism sample models
64
+ (the numbers are in `auto-rig/profile.ts`): the turn reads through parallax,
65
+ not a reshaped face. The face plate translates, changing its width by a few
66
+ percent, the chin leading it; the features lead the plate by their own amounts
67
+ (the nose most, then the brows, eyes and mouth), the eyes foreshortening mildly
68
+ about their own centres; the front hair rides the face where it lies over it and holds the head's
69
+ outline where it draws it, and the back hair stays behind; the neck and the
70
+ ears the face layer paints are cut off into islands of the plate's own mesh,
71
+ drawn behind the head — the neck stays (only the chin's shade slides across
72
+ it), the ears lag the face. AngleZ rolls the head 10° about the chin; the torso only
73
+ breathes.
74
+
75
+ On a layer set with a `nose`, `options.turnTargets` — the cues a 30° reference
76
+ measures (how far the eye pair slides, how much the far eye foreshortens,
77
+ whether the silhouette holds) — are **fitted** each by the one knob it reads:
78
+ the turn's amount, the eyes' foreshortening, the hair's width, a feature's own
79
+ shift. With none given the profile itself renders (`DEFAULT_TURN_TARGETS`
80
+ expresses it in the cues' units). `options.style` tunes a character from the
81
+ profile where the Live2D samples themselves disagree: the whole turn, the
82
+ features' lead, the front hair's follow and its outline's, the blink and the
83
+ hair sway. The turn
84
+ is bounded by the art: the far eye stays inside the plate's outline and, given
85
+ `options.strandEdges`, the far iris may not slide under the bangs' side strand
86
+ any further than it is painted — best effort, never a refusal. The report's
87
+ `strandOverlap` gives, per side whose far iris the bangs' run still covers at
88
+ some far stop, the covered width: how much of the iris's painted row the run
89
+ covers at the stop where that is largest, in px and head half-widths, next to
90
+ how much it covers at rest, and whether the bound held.
91
+ [`AUTO-RIG.md`](./AUTO-RIG.md) describes the model, the fit and the limits.
92
+
93
+ An `eyeShift` you passed that runs past the room the art leaves the far eye —
94
+ the face plate's edge, or the bangs' strand — is **clamped** and the rig is
95
+ built, since that room is a fact about these layers, not about the reference;
96
+ so is the profile's own turn where the art has less room than it needs. Every
97
+ other target you passed that this layer set cannot reach **throws**, naming the
98
+ field and the range it could have had: a `farEyeRatio` or `silhouetteRatio`
99
+ out of the rig's reach, or a `noseShift` / `mouthShift` outside what that
100
+ feature can reach. Pass `options.onTurnSolved` to see what the turn settled on
101
+ and which targets were clamped.
98
102
 
99
103
  `turnTargets.headHalfWidth` is what the shift targets are fractions of, when a
100
104
  caller has measured the actual head (the face plate stands in otherwise).
101
105
  Alongside it, `options.headEdges` names, per side, every layer with an opaque
102
106
  pixel at the eye row and its own rest x there — a companion to a measured
103
107
  `headHalfWidth`, absent otherwise — so the solver can land each candidate
104
- through its OWN part's deformation (the bangs move with their hold, the back
105
- hair holds still, a face-plate or body edge slide) rather than assume the head
108
+ through its OWN part's deformation (the bangs ride the face, the back hair
109
+ stays behind it, a face-plate edge slides) rather than assume the head
106
110
  is centred on the face plate or that whichever part drew furthest out at rest
107
111
  is still the furthest out after it turns.
108
112
 
package/dist/index.d.mts CHANGED
@@ -657,343 +657,207 @@ interface AlphaBbox {
657
657
  declare function detectAlphaBbox(rgba: ArrayLike<number>, width: number, height: number): AlphaBbox | null;
658
658
 
659
659
  /**
660
- * Role table, role parsing, bbox→transform math, and model assembly for the
661
- * AI auto-rig generator. All pure functions — no DOM, no canvas, no
662
- * crypto.randomUUID.
663
- *
664
- * L/R = CHARACTER frame: *_L is the character's left = screen right.
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
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
+ }
666
684
 
667
685
  /**
668
- * Input contract from the host app to this package's auto-rig functions.
669
- * Passed in after the host has decoded PNGs, computed alpha bboxes, and
670
- * mapped filenames to canonical roles.
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.
671
689
  */
690
+
691
+ /** One role layer, as a host measured it off its decoded pixels. */
672
692
  interface LayerInput {
673
- /** Canonical role, e.g. "eye_L". */
693
+ /** Canonical role (see `parseLayerRoles`); becomes the part id. */
674
694
  role: string;
675
- /** Original file name — used in error messages and as a stable id. */
676
695
  fileName: string;
677
- /** Shared canvas width (all layers have the same canvas size). */
678
696
  canvasW: number;
679
- /** Shared canvas height. */
680
697
  canvasH: number;
681
- /** Alpha-tight bounding box, top-left origin, +y down (image coords). */
698
+ /** Alpha bounding box in canvas px (y down) — the crop the atlas holds. */
682
699
  bbox: {
683
700
  x: number;
684
701
  y: number;
685
702
  w: number;
686
703
  h: number;
687
704
  };
688
- /** Cropped image width = bbox.w. */
689
705
  cropW: number;
690
- /** Cropped image height = bbox.h. */
691
706
  cropH: number;
692
- /**
693
- * The layer's painted half-width on every CROP row, top → bottom, one entry
694
- * per row (`cropH` of them): half the row's opaque span in canvas px under
695
- * the same alpha ≥ 128 rule the head's `headEdges` are measured with
696
- * (`createLayerSetMeasurer`), 0 for a row with no opaque pixel.
697
- * Optional, and only the FACE's is read: it gives the face plate a
698
- * row-dependent turn radius and a chin swing (`faceRowProfile`,
699
- * `turnSurface`); absent — from a host that does not measure it — the plate
700
- * turns on one radius on every row. Validated before anything reads it: an
701
- * array of length `cropH` whose every entry is a finite number in
702
- * [0, cropW / 2], the most an inclusive span inside the crop can be.
703
- */
707
+ /** Face only: half the opaque span of each crop row, canvas px (0 = none). */
704
708
  rowHalfWidths?: number[];
705
- /**
706
- * The tight box of the layer's alpha ≥ 128 pixels, in image coords like
707
- * `bbox` and NOT grown: the drawing a viewer reads inside a soft feather,
708
- * which the crop's alpha ≥ 8 rule takes in whole. Optional, and only the
709
- * NOSE's is read: its centre and width are the nose's turn landmark
710
- * (`turnLandmarks`), so a shaded nose is fitted by the drawing rather than
711
- * by its feather, and its top edge at its centre x is the bridge top the
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
714
- * before anything reads it: a plain object whose x, y, w and h are finite,
715
- * w and h positive, and the box inside `bbox`.
716
- */
709
+ /** Nose only: the tight box of its alpha ≥ 128 pixels, canvas px. */
717
710
  denseCore?: {
718
711
  x: number;
719
712
  y: number;
720
713
  w: number;
721
714
  h: number;
722
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[];
723
721
  }
724
722
  /**
725
- * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
726
- *
727
- * Steps:
728
- * 1. Normalize each filename → role (normalizeRole).
729
- * 2. Eagerly check each role against ROLE_TABLE — unknown roles throw early
730
- * with the offending fileName included in the message.
731
- * 3. Call assertRoleSet to check duplicates + required roles.
732
- *
733
- * Throws a path-qualified Error on any contract violation.
734
- */
735
- declare function parseLayerRoles(fileNames: string[]): {
736
- role: string;
737
- fileName: string;
738
- }[];
739
- /**
740
- * One side's iris and the bangs' run it would slide under on the turn, as
741
- * real pixel boundaries on ONE image row — the row holding that iris's centre
742
- * — in model coordinates: x is canvas x minus half the canvas width, like
743
- * `bboxToTransform`'s crop edges, and `y` is that row's centre. Every x is the
744
- * painted edge facing its clear neighbour, read under one opacity rule, so the
745
- * gap between an iris edge and a run edge is a real distance with no margin to
746
- * add. "Outer" is toward the head's edge on that side. The run is the one a
747
- * producer judges a strand: it may skip runs it judges hair detail
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.
751
- *
752
- * One shape covers three cases:
753
- * - an ordinary side strand outward of a clear iris (`runFace` outward of
754
- * `irisOuter`);
755
- * - a run over the iris centre that clears on the face side (`runFace`
756
- * inward of that centre);
757
- * - a fringe spanning the face: the run still covers the OTHER iris's
758
- * centre, so it has no face-side end on this side and `runFace` is
759
- * `null` — an absent edge, not an invented one.
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.
760
726
  */
761
727
  interface IrisStrand {
762
- /** The iris row's centre, model y. */
763
728
  y: number;
764
- /** The outer end of that iris layer's opaque span on the row. */
765
729
  irisOuter: number;
766
- /** Its inner end, toward the face. */
767
730
  irisInner: number;
768
- /** The outer end of the `hair_front` run the iris would slide under. */
769
731
  runOuter: number;
770
- /** The run's face-side end, or `null` when it reaches the other iris's
771
- * centre. */
732
+ /** The run's face-side end; `null` for a fringe spanning the face. */
772
733
  runFace: number | null;
773
734
  }
774
735
  /**
775
- * What a head does at full turn, as the cues `measure_turn_reference` reads off
776
- * a front/turned reference pair. The turn is fitted to these — the cylinder's
777
- * radius and the features' depths are solved from them, not tuned.
778
- *
779
- * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
780
- * reference measures, and the nose and the mouth derive from the eyes when they
781
- * are not given.
782
- *
783
- * What happens when the layer set cannot reach a target depends on where the
784
- * target came from. A number the CALLER passed is a measurement, so an
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.
797
- *
798
- * One measurement is clamped too: an `eyeShift` past the room the art leaves
799
- * the far eye — the face plate's edge, and the bangs' side strand when
800
- * `strandEdges` is given — whoever passed it. That room is a fact about this
801
- * layer set, not about the reference the shift was measured on, so the rig
802
- * takes as much of the shift as the art allows and keeps fitting the other
803
- * cues. An `eyeShift` below the slide the face's own turn already gives the
804
- * eyes is still refused: no depth reaches it.
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.
805
739
  */
806
740
  interface TurnTargets {
807
- /** How far the eye pair's centre slides toward the far side at full turn, as
808
- * a fraction of the head's half-width. The sign is ignored: which way is
809
- * the turn's own business. */
810
741
  eyeShift?: number;
811
- /** The far eye's width over the near eye's at full turn, divided by the same
812
- * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
813
742
  farEyeRatio?: number;
814
- /** The head's half-width at full turn over its half-width at rest, as a
815
- * RENDER measures it — the outermost opaque edge on each side, whichever
816
- * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
817
- * produces it is fitted to it, so a measured one is met or refused, never
818
- * quietly rendered as something else. */
819
743
  silhouetteRatio?: number;
820
- /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
821
744
  noseShift?: number;
822
- /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
823
745
  mouthShift?: number;
824
- /** The head's own half-width at the eye row, in canvas px: what the shift
825
- * fractions are fractions OF, and the rest distance the silhouette hold
826
- * pivots on. Without it both fall back to the face plate's half-width, which
827
- * is narrower than the head the hair draws, so the shifts land short — an
828
- * approximation for callers that have no pixels, not a default to prefer. */
746
+ /** The head half-width the shifts are fractions of, canvas px. */
829
747
  headHalfWidth?: number;
830
748
  }
831
- /**
832
- * A `turnTargets` field this generator will not build a rig from: not a number,
833
- * outside its range, unreachable on this layer set, or a measured head narrower
834
- * than the face plate it is drawn around. Every one of them carries a number
835
- * the CALLER passed, so a host can tell them apart from the invariant breaks the
836
- * rest of the generator throws and report them as bad input instead of failing.
837
- */
749
+ /** A turn target this layer set cannot reach, or turn input that is malformed. */
838
750
  declare class TurnTargetError extends Error {
839
751
  constructor(message: string);
840
752
  }
841
753
  /**
842
- * The reference the turn is fitted to when the caller measured none of its own:
843
- * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
844
- * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
845
- * normalised by the same ratio at rest, which is what the field means).
846
- * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
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.
847
765
  */
848
766
  declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
849
- /** The turn depth of each feature family: the share of the pinned bulk slide
850
- * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
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. */
851
770
  interface TurnDepths {
852
771
  eye: number;
853
772
  nose: number;
854
773
  mouth: number;
855
774
  }
856
- /** What the turn solve settled on, for a caller that wants to see it — the
857
- * cues it reached and the targets it had to cut down to reach them. */
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
+ }
858
790
  interface TurnSolveReport {
859
- /** Cylinder radius the face-warp bake and the parallax unit were built on. */
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. */
860
793
  radius: number;
861
- /** Rest distance from the face centre the silhouette hold pivots on. */
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. */
862
798
  holdBase: number;
863
799
  depths: TurnDepths;
864
- /** What the rig actually reaches, in the targets' own units. */
800
+ /** What the rig renders at ±30° (mean of both directions). */
865
801
  achieved: {
866
802
  eyeShift: number;
867
803
  farEyeRatio: number;
868
804
  silhouetteRatio: number;
869
805
  };
870
- /** The targets the rig fell short of, cut down to what it can reach: every
871
- * defaulted field it could not reach, and an `eyeShift` — the caller's own
872
- * too — past the room the art leaves the far eye (the face plate's edge,
873
- * and the bangs' strand when `strandEdges` was given). Empty when every
874
- * target was met. */
806
+ /** Targets cut down to what this layer set can do. */
875
807
  clamped: (keyof TurnTargets)[];
876
- /** Per side whose far iris the bangs' run covers at some far stop at the
877
- * fitted radius and depth (`options.strandEdges`): how much of it. Absent
878
- * when no side has an entry. Never a clamp: `clamped` still names an
879
- * `eyeShift` the strand bound cut short. */
880
808
  strandOverlap?: {
881
809
  left?: StrandOverlap;
882
810
  right?: StrandOverlap;
883
811
  };
884
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
+
885
835
  /**
886
- * How much of one side's far iris the bangs' run covers: the length of the
887
- * landed iris span `[irisOuter, irisInner]` on its row that lies inside the
888
- * landed run `[runOuter, runFace]` — face-ward without end for a run with no
889
- * face-side edge (`runFace: null`) — at the far stop where that is largest.
890
- * Always read off real landed boundaries (`IrisStrand`).
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.
891
838
  */
892
- interface StrandOverlap {
893
- /** The far stop the covered width is largest at, degrees. */
894
- deg: number;
895
- /** That covered width, px. */
896
- px: number;
897
- /** `px` over the head half-width the shift cues are fractions of. */
898
- hh: number;
899
- /** The covered width as painted, at rest, px. */
900
- restPx: number;
901
- /** Whether this side's strand bound held at the fitted depth — the iris
902
- * kept no deeper under the run than it is painted, at every far stop. When
903
- * false the fitted radius could not hold it (always so for `runFace:
904
- * null`), the eyes' turn depth is 0, and `px` is the coverage that is
905
- * left. */
906
- held: boolean;
907
- }
839
+ declare function parseLayerRoles(fileNames: string[]): {
840
+ role: string;
841
+ fileName: string;
842
+ }[];
843
+
908
844
  /**
909
- * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
910
- * valid IkiModel ready for parseIkiModel.
911
- *
912
- * - Validate all inputs before deriving anything.
913
- * - Place parts at source-derived positions (bboxToTransform, unshifted).
914
- * - Emit the standard parameters (STANDARD_PARAMETERS), plus a conditional
915
- * HairSwayX/Z pair + hair-sway physics rigs when a hair_front layer is
916
- * present.
917
- * - Size every turn group's grid to its own members and the virtual lattice
918
- * to those grids, then solve the turn on them (turnSetup, solveTurnModel):
919
- * one surface — radius, travel, the head-level nod radius — that every
920
- * bake below samples.
921
- * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
922
- * turn translates nothing rigidly; its travel is in the group grids), one
923
- * warp deformer per turn group present — `faceWarp` for the plate,
924
- * `eyeWarp_L/R`, `mouthWarp`, `noseWarp`, `browWarp_L/R`, `blushWarp_L/R`
925
- * — each a warp2d over its own grid baked from that surface with the
926
- * family's solved depth as keyform geometry (bakeTurnGroupWarp2D), and
927
- * bodyDeformer when a body layer is present (matrix, torso-base pivot,
928
- * AngleX follow + Breath follow).
929
- * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by
930
- * meshCellsFor + role bindings, each face-family part on its group's warp.
931
- * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.
932
- * - Part ids equal the role string (deterministic, no crypto.randomUUID).
933
- * - Return parseIkiModel(structuredClone(model)) — every caller gets a
934
- * validated model; bad assembly fails loudly.
845
+ * `generateIkiFromLayerSet`: role layers in, a rigged `.iki` out.
935
846
  *
936
- * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
937
- * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
938
- * passed that this layer set cannot reach throws; one that came from the
939
- * defaults is clamped to what it can do, and so is a caller's `eyeShift` past
940
- * the room the art leaves the far eye — see TurnTargets for why, and pass
941
- * `options.onTurnSolved` to see which ones were clamped and what the turn
942
- * ended up reaching.
943
- *
944
- * `options.strandEdges` gives each side's iris and the bangs' run it would
945
- * slide under as measured pixel edges (`IrisStrand`), and with it the turn
946
- * keeps each far iris from sliding under that run any further than it is
947
- * painted, best effort: where no radius the fit can use holds that, the rig
948
- * is still built, with the eyes' depth at 0, and the report's
949
- * `strandOverlap` says how much of the iris the bangs cover.
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.
950
855
  */
856
+
951
857
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
952
858
  width: number;
953
859
  height: number;
954
- }, options?: {
955
- turnTargets?: TurnTargets;
956
- /** Called once, after a turn is solved, with what it settled on. Not called
957
- * for a layer set without a nose, which solves no turn at all. */
958
- onTurnSolved?: (report: TurnSolveReport) => void;
959
- /** Every role with an opaque pixel in the eye-row band, per side, when
960
- * `turnTargets.headHalfWidth` was measured off that same union — a
961
- * companion to it, not a turn target itself (the caller-facing contract
962
- * stays the three cues). See `TurnSolveContext.headEdges`. */
963
- headEdges?: {
964
- left: {
965
- role: string;
966
- x: number;
967
- }[];
968
- right: {
969
- role: string;
970
- x: number;
971
- }[];
972
- };
973
- /** Per side (`left` the −x side, `right` the +x side, like `headEdges`),
974
- * that side's iris and the bangs' run it would slide under on the turn,
975
- * as real pixel edges on the row holding the iris's centre — see
976
- * `IrisStrand` for the contract and its three cases (a strand outward of
977
- * a clear iris, a run over the iris centre that clears on the face side,
978
- * a fringe spanning the face). A side's iris is whichever of `iris_L` /
979
- * `iris_R` sits on that side; the sides are independent, so either may
980
- * be absent. With it the turn keeps each far iris from sliding under its
981
- * run any further than it is painted, best effort: never a refusal. The
982
- * report's `strandOverlap` gives, per side whose far iris the run still
983
- * covers at some far stop, the covered width at the stop where it is
984
- * largest, next to the painted one, and whether the bound held — `true`
985
- * for an iris painted under its run that the turn took no deeper,
986
- * `false` where the fitted radius could not hold it and the eyes' turn
987
- * depth is 0. It is always
988
- * validated against the layers first (`validateStrandEdges`), but only
989
- * the turn solve reads it, so like `headEdges` it has no effect on the
990
- * rig without a nose; absent or `{}`, the rig is the one built without
991
- * it. */
992
- strandEdges?: {
993
- left?: IrisStrand;
994
- right?: IrisStrand;
995
- };
996
- }): IkiModel;
860
+ }, options?: GenerateOptions): IkiModel;
997
861
 
998
862
  /**
999
863
  * The alpha rules a layer set is measured by, and the pre-rig measurement of
@@ -1380,4 +1244,4 @@ declare function createGridMesh(cols: number, rows: number): IkiMesh;
1380
1244
  */
1381
1245
  declare function createDefaultWarpDeformer(model: IkiModel): IkiWarpDeformer;
1382
1246
 
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 };
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 };