@ikijs/editor 0.4.0 → 0.6.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 CHANGED
@@ -49,7 +49,7 @@ 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` |
52
- | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles` |
52
+ | Auto-rig | `generateIkiFromLayerSet`, `parseLayerRoles`, `TurnTargets`, `DEFAULT_TURN_TARGETS`, `TurnSolveReport` |
53
53
  | Bindings | `captureBindingEndpoint` |
54
54
 
55
55
  ## Auto-rig
@@ -60,10 +60,42 @@ leaves the document untouched.
60
60
  torso that follows and 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. A target you passed that this layer set cannot
76
+ reach **throws**, naming the field and the range it could have had; a target
77
+ that came from the defaults is a style prior, not a measurement of this
78
+ character, so it is **clamped** to what the layer set can do and the rig is
79
+ built. Pass `options.onTurnSolved` to see what the turn settled on and which
80
+ targets were clamped.
81
+
82
+ `turnTargets.headHalfWidth` is what the shift targets are fractions of, when a
83
+ caller has measured the actual head (the face plate stands in otherwise).
84
+ Alongside it, `options.headEdges` names, per side, every layer with an opaque
85
+ pixel at the eye row and its own rest x there — a companion to a measured
86
+ `headHalfWidth`, absent otherwise — so the solver can land each candidate
87
+ through its OWN part's deformation (the bangs move with their hold, the back
88
+ hair holds still, a face-plate or body edge slide) rather than assume the head
89
+ is centred on the face plate or that whichever part drew furthest out at rest
90
+ is still the furthest out after it turns.
91
+
63
92
  It takes **already-decoded** layer geometry (`LayerInput`), never pixels, which
64
93
  is what keeps this package free of any image dependency: the editor app
65
94
  decodes with canvas, `@ikijs/mcp` decodes with `sharp`, and both feed the same
66
- pure function.
95
+ pure function — including the optional `rowHalfWidths` (one entry per crop row,
96
+ half that row's opaque span in canvas px), which `@ikijs/mcp` measures for the
97
+ face so the plate turns on a radius that varies by row; absent, it turns on one
98
+ constant radius.
67
99
 
68
100
  `*_L` / `*_R` are the **character's** sides — `eye_L` is the character's left
69
101
  eye, which appears on the viewer's right.
package/dist/index.d.mts CHANGED
@@ -875,6 +875,19 @@ interface LayerInput {
875
875
  cropW: number;
876
876
  /** Cropped image height = bbox.h. */
877
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[];
878
891
  }
879
892
  /**
880
893
  * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
@@ -891,29 +904,149 @@ declare function parseLayerRoles(fileNames: string[]): {
891
904
  role: string;
892
905
  fileName: string;
893
906
  }[];
907
+ /**
908
+ * What a head does at full turn, as the cues `measure_turn_reference` reads off
909
+ * a front/turned reference pair. The turn is fitted to these — the cylinder's
910
+ * radius and the features' depths are solved from them, not tuned.
911
+ *
912
+ * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
913
+ * reference measures, and the nose and the mouth derive from the eyes when they
914
+ * are not given.
915
+ *
916
+ * What happens when the layer set cannot reach a target depends on where the
917
+ * target came from. A number the CALLER passed is a measurement, so an
918
+ * unreachable one throws, naming the field and what was on offer. A number this
919
+ * module filled in — a default, or a nose/mouth derived from the eyes — is a
920
+ * style prior, not a promise about this character: it is CLAMPED to what the
921
+ * layer set can do and the rig is built. The alternative is a generator that
922
+ * refuses its own defaults, which would leave a perfectly good layer set with
923
+ * no model at all. `solveTurnModel` reports which fields it clamped.
924
+ */
925
+ interface TurnTargets {
926
+ /** How far the eye pair's centre slides toward the far side at full turn, as
927
+ * a fraction of the head's half-width. The sign is ignored: which way is
928
+ * the turn's own business. */
929
+ eyeShift?: number;
930
+ /** The far eye's width over the near eye's at full turn, divided by the same
931
+ * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
932
+ farEyeRatio?: number;
933
+ /** The head's half-width at full turn over its half-width at rest, as a
934
+ * RENDER measures it — the outermost opaque edge on each side, whichever
935
+ * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
936
+ * produces it is fitted to it, so a measured one is met or refused, never
937
+ * quietly rendered as something else. */
938
+ silhouetteRatio?: number;
939
+ /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
940
+ noseShift?: number;
941
+ /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
942
+ mouthShift?: number;
943
+ /** The head's own half-width at the eye row, in canvas px: what the shift
944
+ * fractions are fractions OF, and the rest distance the silhouette hold
945
+ * pivots on. Without it both fall back to the face plate's half-width, which
946
+ * is narrower than the head the hair draws, so the shifts land short — an
947
+ * approximation for callers that have no pixels, not a default to prefer. */
948
+ headHalfWidth?: number;
949
+ }
950
+ /**
951
+ * A `turnTargets` field this generator will not build a rig from: not a number,
952
+ * outside its range, unreachable on this layer set, or a measured head narrower
953
+ * than the face plate it is drawn around. Every one of them carries a number
954
+ * the CALLER passed, so a host can tell them apart from the invariant breaks the
955
+ * rest of the generator throws and report them as bad input instead of failing.
956
+ */
957
+ declare class TurnTargetError extends Error {
958
+ constructor(message: string);
959
+ }
960
+ /**
961
+ * The reference the turn is fitted to when the caller measured none of its own:
962
+ * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
963
+ * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
964
+ * normalised by the same ratio at rest, which is what the field means).
965
+ * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
966
+ */
967
+ declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
968
+ /** The turn depth of each feature family: the share of the pinned bulk slide
969
+ * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
970
+ interface TurnDepths {
971
+ eye: number;
972
+ nose: number;
973
+ mouth: number;
974
+ }
975
+ /** What the turn solve settled on, for a caller that wants to see it — the
976
+ * cues it reached and the targets it had to cut down to reach them. */
977
+ interface TurnSolveReport {
978
+ /** Cylinder radius the face-warp bake and the parallax unit were built on. */
979
+ radius: number;
980
+ /** Rest distance from the face centre the silhouette hold pivots on. */
981
+ holdBase: number;
982
+ depths: TurnDepths;
983
+ /** What the rig actually reaches, in the targets' own units. */
984
+ achieved: {
985
+ eyeShift: number;
986
+ farEyeRatio: number;
987
+ silhouetteRatio: number;
988
+ };
989
+ /** Defaulted fields the layer set could not reach, cut down to what it can.
990
+ * Empty when every target was met. */
991
+ clamped: (keyof TurnTargets)[];
992
+ }
894
993
  /**
895
994
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
896
995
  * valid IkiModel ready for parseIkiModel.
897
996
  *
898
997
  * - Validate all inputs before deriving anything.
899
998
  * - Place parts at source-derived positions (bboxToTransform, unshifted).
900
- * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a
901
- * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front
902
- * layer is present.
903
- * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings),
999
+ * - Emit the standard parameters (STANDARD_PARAMETERS), plus a conditional
1000
+ * HairSwayX/Z pair + hair-sway physics rigs when a hair_front layer is
1001
+ * present.
1002
+ * - Size every turn group's grid to its own members and the virtual lattice
1003
+ * to those grids, then solve the turn on them (turnSetup, solveTurnModel):
1004
+ * one surface — radius, travel, the head-level nod radius — that every
1005
+ * bake below samples.
1006
+ * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
1007
+ * turn translates nothing rigidly; its travel is in the group grids), one
1008
+ * warp deformer per turn group present — `faceWarp` for the plate,
1009
+ * `eyeWarp_L/R`, `mouthWarp`, `noseWarp`, `browWarp_L/R`, `blushWarp_L/R`
1010
+ * — each a warp2d over its own grid baked from that surface with the
1011
+ * family's solved depth as keyform geometry (bakeTurnGroupWarp2D), and
904
1012
  * bodyDeformer when a body layer is present (matrix, torso-base pivot,
905
- * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,
906
- * baked cylinder warp center-relative on faceCenterX).
1013
+ * AngleX follow + Breath follow).
907
1014
  * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by
908
- * meshCellsFor + role bindings.
1015
+ * meshCellsFor + role bindings, each face-family part on its group's warp.
909
1016
  * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.
910
1017
  * - Part ids equal the role string (deterministic, no crypto.randomUUID).
911
1018
  * - Return parseIkiModel(structuredClone(model)) — every caller gets a
912
1019
  * validated model; bad assembly fails loudly.
1020
+ *
1021
+ * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1022
+ * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1023
+ * passed that this layer set cannot reach throws; one that came from the
1024
+ * defaults is clamped to what it can do — see TurnTargets for why the two
1025
+ * differ, and pass `options.onTurnSolved` to see which ones were clamped and
1026
+ * what the turn ended up reaching.
913
1027
  */
914
1028
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
915
1029
  width: number;
916
1030
  height: number;
1031
+ }, options?: {
1032
+ turnTargets?: TurnTargets;
1033
+ /** Called once, after a turn is solved, with what it settled on. Not called
1034
+ * for a layer set without a nose, which solves no turn at all. */
1035
+ onTurnSolved?: (report: TurnSolveReport) => void;
1036
+ /** Every role with an opaque pixel in the eye-row band, per side, when
1037
+ * `turnTargets.headHalfWidth` was measured off that same union — a
1038
+ * companion to it, not a turn target itself (the caller-facing contract
1039
+ * stays the three cues). See `TurnSolveContext.headEdges`. */
1040
+ headEdges?: {
1041
+ left: {
1042
+ role: string;
1043
+ x: number;
1044
+ }[];
1045
+ right: {
1046
+ role: string;
1047
+ x: number;
1048
+ }[];
1049
+ };
917
1050
  }): IkiModel;
918
1051
 
919
- export { ALPHA_BBOX_THRESHOLD, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, UV_INSET_PX, captureBindingEndpoint, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, detectAlphaBbox, generateIkiFromLayerSet, interpolateGridOffsets, packAtlas, parseLayerRoles, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
1052
+ 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 LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, 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 };
package/dist/index.d.ts CHANGED
@@ -875,6 +875,19 @@ interface LayerInput {
875
875
  cropW: number;
876
876
  /** Cropped image height = bbox.h. */
877
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[];
878
891
  }
879
892
  /**
880
893
  * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
@@ -891,29 +904,149 @@ declare function parseLayerRoles(fileNames: string[]): {
891
904
  role: string;
892
905
  fileName: string;
893
906
  }[];
907
+ /**
908
+ * What a head does at full turn, as the cues `measure_turn_reference` reads off
909
+ * a front/turned reference pair. The turn is fitted to these — the cylinder's
910
+ * radius and the features' depths are solved from them, not tuned.
911
+ *
912
+ * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
913
+ * reference measures, and the nose and the mouth derive from the eyes when they
914
+ * are not given.
915
+ *
916
+ * What happens when the layer set cannot reach a target depends on where the
917
+ * target came from. A number the CALLER passed is a measurement, so an
918
+ * unreachable one throws, naming the field and what was on offer. A number this
919
+ * module filled in — a default, or a nose/mouth derived from the eyes — is a
920
+ * style prior, not a promise about this character: it is CLAMPED to what the
921
+ * layer set can do and the rig is built. The alternative is a generator that
922
+ * refuses its own defaults, which would leave a perfectly good layer set with
923
+ * no model at all. `solveTurnModel` reports which fields it clamped.
924
+ */
925
+ interface TurnTargets {
926
+ /** How far the eye pair's centre slides toward the far side at full turn, as
927
+ * a fraction of the head's half-width. The sign is ignored: which way is
928
+ * the turn's own business. */
929
+ eyeShift?: number;
930
+ /** The far eye's width over the near eye's at full turn, divided by the same
931
+ * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
932
+ farEyeRatio?: number;
933
+ /** The head's half-width at full turn over its half-width at rest, as a
934
+ * RENDER measures it — the outermost opaque edge on each side, whichever
935
+ * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
936
+ * produces it is fitted to it, so a measured one is met or refused, never
937
+ * quietly rendered as something else. */
938
+ silhouetteRatio?: number;
939
+ /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
940
+ noseShift?: number;
941
+ /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
942
+ mouthShift?: number;
943
+ /** The head's own half-width at the eye row, in canvas px: what the shift
944
+ * fractions are fractions OF, and the rest distance the silhouette hold
945
+ * pivots on. Without it both fall back to the face plate's half-width, which
946
+ * is narrower than the head the hair draws, so the shifts land short — an
947
+ * approximation for callers that have no pixels, not a default to prefer. */
948
+ headHalfWidth?: number;
949
+ }
950
+ /**
951
+ * A `turnTargets` field this generator will not build a rig from: not a number,
952
+ * outside its range, unreachable on this layer set, or a measured head narrower
953
+ * than the face plate it is drawn around. Every one of them carries a number
954
+ * the CALLER passed, so a host can tell them apart from the invariant breaks the
955
+ * rest of the generator throws and report them as bad input instead of failing.
956
+ */
957
+ declare class TurnTargetError extends Error {
958
+ constructor(message: string);
959
+ }
960
+ /**
961
+ * The reference the turn is fitted to when the caller measured none of its own:
962
+ * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
963
+ * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
964
+ * normalised by the same ratio at rest, which is what the field means).
965
+ * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
966
+ */
967
+ declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
968
+ /** The turn depth of each feature family: the share of the pinned bulk slide
969
+ * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
970
+ interface TurnDepths {
971
+ eye: number;
972
+ nose: number;
973
+ mouth: number;
974
+ }
975
+ /** What the turn solve settled on, for a caller that wants to see it — the
976
+ * cues it reached and the targets it had to cut down to reach them. */
977
+ interface TurnSolveReport {
978
+ /** Cylinder radius the face-warp bake and the parallax unit were built on. */
979
+ radius: number;
980
+ /** Rest distance from the face centre the silhouette hold pivots on. */
981
+ holdBase: number;
982
+ depths: TurnDepths;
983
+ /** What the rig actually reaches, in the targets' own units. */
984
+ achieved: {
985
+ eyeShift: number;
986
+ farEyeRatio: number;
987
+ silhouetteRatio: number;
988
+ };
989
+ /** Defaulted fields the layer set could not reach, cut down to what it can.
990
+ * Empty when every target was met. */
991
+ clamped: (keyof TurnTargets)[];
992
+ }
894
993
  /**
895
994
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
896
995
  * valid IkiModel ready for parseIkiModel.
897
996
  *
898
997
  * - Validate all inputs before deriving anything.
899
998
  * - Place parts at source-derived positions (bboxToTransform, unshifted).
900
- * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a
901
- * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front
902
- * layer is present.
903
- * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings),
999
+ * - Emit the standard parameters (STANDARD_PARAMETERS), plus a conditional
1000
+ * HairSwayX/Z pair + hair-sway physics rigs when a hair_front layer is
1001
+ * present.
1002
+ * - Size every turn group's grid to its own members and the virtual lattice
1003
+ * to those grids, then solve the turn on them (turnSetup, solveTurnModel):
1004
+ * one surface — radius, travel, the head-level nod radius — that every
1005
+ * bake below samples.
1006
+ * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
1007
+ * turn translates nothing rigidly; its travel is in the group grids), one
1008
+ * warp deformer per turn group present — `faceWarp` for the plate,
1009
+ * `eyeWarp_L/R`, `mouthWarp`, `noseWarp`, `browWarp_L/R`, `blushWarp_L/R`
1010
+ * — each a warp2d over its own grid baked from that surface with the
1011
+ * family's solved depth as keyform geometry (bakeTurnGroupWarp2D), and
904
1012
  * bodyDeformer when a body layer is present (matrix, torso-base pivot,
905
- * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,
906
- * baked cylinder warp center-relative on faceCenterX).
1013
+ * AngleX follow + Breath follow).
907
1014
  * - Mesh parts (spec.mesh===true) → width:1, height:1, pixel grid mesh sized by
908
- * meshCellsFor + role bindings.
1015
+ * meshCellsFor + role bindings, each face-family part on its group's warp.
909
1016
  * - Static parts (spec.mesh===false) → width:cropW, height:cropH, no mesh.
910
1017
  * - Part ids equal the role string (deterministic, no crypto.randomUUID).
911
1018
  * - Return parseIkiModel(structuredClone(model)) — every caller gets a
912
1019
  * validated model; bad assembly fails loudly.
1020
+ *
1021
+ * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1022
+ * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1023
+ * passed that this layer set cannot reach throws; one that came from the
1024
+ * defaults is clamped to what it can do — see TurnTargets for why the two
1025
+ * differ, and pass `options.onTurnSolved` to see which ones were clamped and
1026
+ * what the turn ended up reaching.
913
1027
  */
914
1028
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
915
1029
  width: number;
916
1030
  height: number;
1031
+ }, options?: {
1032
+ turnTargets?: TurnTargets;
1033
+ /** Called once, after a turn is solved, with what it settled on. Not called
1034
+ * for a layer set without a nose, which solves no turn at all. */
1035
+ onTurnSolved?: (report: TurnSolveReport) => void;
1036
+ /** Every role with an opaque pixel in the eye-row band, per side, when
1037
+ * `turnTargets.headHalfWidth` was measured off that same union — a
1038
+ * companion to it, not a turn target itself (the caller-facing contract
1039
+ * stays the three cues). See `TurnSolveContext.headEdges`. */
1040
+ headEdges?: {
1041
+ left: {
1042
+ role: string;
1043
+ x: number;
1044
+ }[];
1045
+ right: {
1046
+ role: string;
1047
+ x: number;
1048
+ }[];
1049
+ };
917
1050
  }): IkiModel;
918
1051
 
919
- export { ALPHA_BBOX_THRESHOLD, ATLAS_PADDING, AddDeformer, AddPart, AddPhysicsRig, type AlphaBbox, type ApplyAtlasInput, type AtlasAssignment, type AtlasLayout, type AtlasPlacement, type AtlasSource, CaptureGridKeyform, type DeformerTransformChannel, DeleteDeformer, DeletePart, DeletePhysicsRig, type EditCommand, type EditTransformChannel, EditorDocument, type LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, UV_INSET_PX, captureBindingEndpoint, computeGridOffsets, createDefaultMatrixDeformer, createDefaultPart, createDefaultWarpDeformer, createGridMesh, detectAlphaBbox, generateIkiFromLayerSet, interpolateGridOffsets, packAtlas, parseLayerRoles, upsertGridKeyform, uvRectFor, validateDeformerDelete, validateDeformerReparent, validatePartAttach };
1052
+ 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 LayerInput, SetDeformerBindings, SetDeformerParent, SetDeformerPivot, SetDeformerPivotX, SetDeformerPivotY, SetDeformerTransform, SetPartBindings, SetPartColor, SetPartDeformer, SetPartHeight, SetPartMesh, SetPartOrder, SetPartTransform, SetPartWidth, SetPhysicsRig, 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 };