@ikijs/editor 0.6.0 → 0.8.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
@@ -72,12 +72,29 @@ slides, how much the far eye foreshortens, whether the silhouette holds),
72
72
  defaulting to `DEFAULT_TURN_TARGETS`. The features' slide is bounded by the art:
73
73
  every feature has to stay on the face plate, because past its contour the far
74
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.
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.
81
98
 
82
99
  `turnTargets.headHalfWidth` is what the shift targets are fractions of, when a
83
100
  caller has measured the actual head (the face plate stands in otherwise).
@@ -89,13 +106,29 @@ hair holds still, a face-plate or body edge slide) rather than assume the head
89
106
  is centred on the face plate or that whichever part drew furthest out at rest
90
107
  is still the furthest out after it turns.
91
108
 
109
+ `options.strandEdges` gives, per side, that side's iris and the `hair_front`
110
+ run it would slide under on the turn, as real pixel edges on the row holding
111
+ the iris's centre (`IrisStrand`: the iris's opaque span, and the run's outer
112
+ and face-side ends). One shape covers a strand outward of a clear iris, a run
113
+ over the iris centre that clears on the face side, and a fringe spanning the
114
+ face, whose `runFace` is `null` because it has no face-side end on that side.
115
+ `@ikijs/mcp` measures it off the pixels it decodes. It is always validated
116
+ against the layers, but like `headEdges` it is only read by the turn solve, so
117
+ it has no effect on the rig without a `nose`; absent or `{}`, the rig is exactly
118
+ the one built without it.
119
+
92
120
  It takes **already-decoded** layer geometry (`LayerInput`), never pixels, which
93
121
  is what keeps this package free of any image dependency: the editor app
94
122
  decodes with canvas, `@ikijs/mcp` decodes with `sharp`, and both feed the same
95
123
  pure function — including the optional `rowHalfWidths` (one entry per crop row,
96
124
  half that row's opaque span in canvas px), which `@ikijs/mcp` measures for the
97
125
  face so the plate turns on a radius that varies by row; absent, it turns on one
98
- constant radius.
126
+ constant radius. It also takes the optional `denseCore`, the tight box of a
127
+ layer's alpha ≥ 128 pixels in image coords, not grown, which `@ikijs/mcp`
128
+ measures for the nose: its centre and width are the nose's turn landmark, so a
129
+ shaded nose is fitted by its drawing rather than its soft feather, and its top
130
+ edge at its centre x is the bridge top the nose tilts 6° about on the turn, its
131
+ tip toward the far side; absent, the crop stands in for both.
99
132
 
100
133
  `*_L` / `*_R` are the **character's** sides — `eye_L` is the character's left
101
134
  eye, which appears on the viewer's right.
package/dist/index.d.mts CHANGED
@@ -888,6 +888,24 @@ interface LayerInput {
888
888
  * [0, cropW / 2], the most an inclusive span inside the crop can be.
889
889
  */
890
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
+ };
891
909
  }
892
910
  /**
893
911
  * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
@@ -904,6 +922,40 @@ declare function parseLayerRoles(fileNames: string[]): {
904
922
  role: string;
905
923
  fileName: string;
906
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
+ }
907
959
  /**
908
960
  * What a head does at full turn, as the cues `measure_turn_reference` reads off
909
961
  * a front/turned reference pair. The turn is fitted to these — the cylinder's
@@ -921,6 +973,14 @@ declare function parseLayerRoles(fileNames: string[]): {
921
973
  * layer set can do and the rig is built. The alternative is a generator that
922
974
  * refuses its own defaults, which would leave a perfectly good layer set with
923
975
  * no model at all. `solveTurnModel` reports which fields it clamped.
976
+ *
977
+ * One measurement is clamped too: an `eyeShift` past the room the art leaves
978
+ * the far eye — the face plate's edge, and the bangs' side strand when
979
+ * `strandEdges` is given — whoever passed it. That room is a fact about this
980
+ * layer set, not about the reference the shift was measured on, so the rig
981
+ * takes as much of the shift as the art allows and keeps fitting the other
982
+ * cues. An `eyeShift` below the slide the face's own turn already gives the
983
+ * eyes is still refused: no depth reaches it.
924
984
  */
925
985
  interface TurnTargets {
926
986
  /** How far the eye pair's centre slides toward the far side at full turn, as
@@ -986,9 +1046,43 @@ interface TurnSolveReport {
986
1046
  farEyeRatio: number;
987
1047
  silhouetteRatio: number;
988
1048
  };
989
- /** Defaulted fields the layer set could not reach, cut down to what it can.
990
- * Empty when every target was met. */
1049
+ /** The targets the rig fell short of, cut down to what it can reach: every
1050
+ * defaulted field it could not reach, and an `eyeShift` — the caller's own
1051
+ * too — past the room the art leaves the far eye (the face plate's edge,
1052
+ * and the bangs' strand when `strandEdges` was given). Empty when every
1053
+ * target was met. */
991
1054
  clamped: (keyof TurnTargets)[];
1055
+ /** Per side whose far iris the bangs' run covers at some far stop at the
1056
+ * fitted radius and depth (`options.strandEdges`): how much of it. Absent
1057
+ * when no side has an entry. Never a clamp: `clamped` still names an
1058
+ * `eyeShift` the strand bound cut short. */
1059
+ strandOverlap?: {
1060
+ left?: StrandOverlap;
1061
+ right?: StrandOverlap;
1062
+ };
1063
+ }
1064
+ /**
1065
+ * How much of one side's far iris the bangs' run covers: the length of the
1066
+ * landed iris span `[irisOuter, irisInner]` on its row that lies inside the
1067
+ * landed run `[runOuter, runFace]` — face-ward without end for a run with no
1068
+ * face-side edge (`runFace: null`) — at the far stop where that is largest.
1069
+ * Always read off real landed boundaries (`IrisStrand`).
1070
+ */
1071
+ interface StrandOverlap {
1072
+ /** The far stop the covered width is largest at, degrees. */
1073
+ deg: number;
1074
+ /** That covered width, px. */
1075
+ px: number;
1076
+ /** `px` over the head half-width the shift cues are fractions of. */
1077
+ hh: number;
1078
+ /** The covered width as painted, at rest, px. */
1079
+ restPx: number;
1080
+ /** Whether this side's strand bound held at the fitted depth — the iris
1081
+ * kept no deeper under the run than it is painted, at every far stop. When
1082
+ * false the fitted radius could not hold it (always so for `runFace:
1083
+ * null`), the eyes' turn depth is 0, and `px` is the coverage that is
1084
+ * left. */
1085
+ held: boolean;
992
1086
  }
993
1087
  /**
994
1088
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
@@ -1021,9 +1115,17 @@ interface TurnSolveReport {
1021
1115
  * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1022
1116
  * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1023
1117
  * 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.
1118
+ * defaults is clamped to what it can do, and so is a caller's `eyeShift` past
1119
+ * the room the art leaves the far eye — see TurnTargets for why, and pass
1120
+ * `options.onTurnSolved` to see which ones were clamped and what the turn
1121
+ * ended up reaching.
1122
+ *
1123
+ * `options.strandEdges` gives each side's iris and the bangs' run it would
1124
+ * slide under as measured pixel edges (`IrisStrand`), and with it the turn
1125
+ * keeps each far iris from sliding under that run any further than it is
1126
+ * painted, best effort: where no radius the fit can use holds that, the rig
1127
+ * is still built, with the eyes' depth at 0, and the report's
1128
+ * `strandOverlap` says how much of the iris the bangs cover.
1027
1129
  */
1028
1130
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1029
1131
  width: number;
@@ -1047,6 +1149,29 @@ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1047
1149
  x: number;
1048
1150
  }[];
1049
1151
  };
1152
+ /** Per side (`left` the −x side, `right` the +x side, like `headEdges`),
1153
+ * that side's iris and the bangs' run it would slide under on the turn,
1154
+ * as real pixel edges on the row holding the iris's centre — see
1155
+ * `IrisStrand` for the contract and its three cases (a strand outward of
1156
+ * a clear iris, a run over the iris centre that clears on the face side,
1157
+ * a fringe spanning the face). A side's iris is whichever of `iris_L` /
1158
+ * `iris_R` sits on that side; the sides are independent, so either may
1159
+ * be absent. With it the turn keeps each far iris from sliding under its
1160
+ * run any further than it is painted, best effort: never a refusal. The
1161
+ * report's `strandOverlap` gives, per side whose far iris the run still
1162
+ * covers at some far stop, the covered width at the stop where it is
1163
+ * largest, next to the painted one, and whether the bound held — `true`
1164
+ * for an iris painted under its run that the turn took no deeper,
1165
+ * `false` where the fitted radius could not hold it and the eyes' turn
1166
+ * depth is 0. It is always
1167
+ * validated against the layers first (`validateStrandEdges`), but only
1168
+ * the turn solve reads it, so like `headEdges` it has no effect on the
1169
+ * rig without a nose; absent or `{}`, the rig is the one built without
1170
+ * it. */
1171
+ strandEdges?: {
1172
+ left?: IrisStrand;
1173
+ right?: IrisStrand;
1174
+ };
1050
1175
  }): IkiModel;
1051
1176
 
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 };
1177
+ 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 };
package/dist/index.d.ts CHANGED
@@ -888,6 +888,24 @@ interface LayerInput {
888
888
  * [0, cropW / 2], the most an inclusive span inside the crop can be.
889
889
  */
890
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
+ };
891
909
  }
892
910
  /**
893
911
  * Map an array of raw filenames to canonical `{ role, fileName }` pairs.
@@ -904,6 +922,40 @@ declare function parseLayerRoles(fileNames: string[]): {
904
922
  role: string;
905
923
  fileName: string;
906
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
+ }
907
959
  /**
908
960
  * What a head does at full turn, as the cues `measure_turn_reference` reads off
909
961
  * a front/turned reference pair. The turn is fitted to these — the cylinder's
@@ -921,6 +973,14 @@ declare function parseLayerRoles(fileNames: string[]): {
921
973
  * layer set can do and the rig is built. The alternative is a generator that
922
974
  * refuses its own defaults, which would leave a perfectly good layer set with
923
975
  * no model at all. `solveTurnModel` reports which fields it clamped.
976
+ *
977
+ * One measurement is clamped too: an `eyeShift` past the room the art leaves
978
+ * the far eye — the face plate's edge, and the bangs' side strand when
979
+ * `strandEdges` is given — whoever passed it. That room is a fact about this
980
+ * layer set, not about the reference the shift was measured on, so the rig
981
+ * takes as much of the shift as the art allows and keeps fitting the other
982
+ * cues. An `eyeShift` below the slide the face's own turn already gives the
983
+ * eyes is still refused: no depth reaches it.
924
984
  */
925
985
  interface TurnTargets {
926
986
  /** How far the eye pair's centre slides toward the far side at full turn, as
@@ -986,9 +1046,43 @@ interface TurnSolveReport {
986
1046
  farEyeRatio: number;
987
1047
  silhouetteRatio: number;
988
1048
  };
989
- /** Defaulted fields the layer set could not reach, cut down to what it can.
990
- * Empty when every target was met. */
1049
+ /** The targets the rig fell short of, cut down to what it can reach: every
1050
+ * defaulted field it could not reach, and an `eyeShift` — the caller's own
1051
+ * too — past the room the art leaves the far eye (the face plate's edge,
1052
+ * and the bangs' strand when `strandEdges` was given). Empty when every
1053
+ * target was met. */
991
1054
  clamped: (keyof TurnTargets)[];
1055
+ /** Per side whose far iris the bangs' run covers at some far stop at the
1056
+ * fitted radius and depth (`options.strandEdges`): how much of it. Absent
1057
+ * when no side has an entry. Never a clamp: `clamped` still names an
1058
+ * `eyeShift` the strand bound cut short. */
1059
+ strandOverlap?: {
1060
+ left?: StrandOverlap;
1061
+ right?: StrandOverlap;
1062
+ };
1063
+ }
1064
+ /**
1065
+ * How much of one side's far iris the bangs' run covers: the length of the
1066
+ * landed iris span `[irisOuter, irisInner]` on its row that lies inside the
1067
+ * landed run `[runOuter, runFace]` — face-ward without end for a run with no
1068
+ * face-side edge (`runFace: null`) — at the far stop where that is largest.
1069
+ * Always read off real landed boundaries (`IrisStrand`).
1070
+ */
1071
+ interface StrandOverlap {
1072
+ /** The far stop the covered width is largest at, degrees. */
1073
+ deg: number;
1074
+ /** That covered width, px. */
1075
+ px: number;
1076
+ /** `px` over the head half-width the shift cues are fractions of. */
1077
+ hh: number;
1078
+ /** The covered width as painted, at rest, px. */
1079
+ restPx: number;
1080
+ /** Whether this side's strand bound held at the fitted depth — the iris
1081
+ * kept no deeper under the run than it is painted, at every far stop. When
1082
+ * false the fitted radius could not hold it (always so for `runFace:
1083
+ * null`), the eyes' turn depth is 0, and `px` is the coverage that is
1084
+ * left. */
1085
+ held: boolean;
992
1086
  }
993
1087
  /**
994
1088
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
@@ -1021,9 +1115,17 @@ interface TurnSolveReport {
1021
1115
  * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1022
1116
  * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1023
1117
  * 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.
1118
+ * defaults is clamped to what it can do, and so is a caller's `eyeShift` past
1119
+ * the room the art leaves the far eye — see TurnTargets for why, and pass
1120
+ * `options.onTurnSolved` to see which ones were clamped and what the turn
1121
+ * ended up reaching.
1122
+ *
1123
+ * `options.strandEdges` gives each side's iris and the bangs' run it would
1124
+ * slide under as measured pixel edges (`IrisStrand`), and with it the turn
1125
+ * keeps each far iris from sliding under that run any further than it is
1126
+ * painted, best effort: where no radius the fit can use holds that, the rig
1127
+ * is still built, with the eyes' depth at 0, and the report's
1128
+ * `strandOverlap` says how much of the iris the bangs cover.
1027
1129
  */
1028
1130
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1029
1131
  width: number;
@@ -1047,6 +1149,29 @@ declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
1047
1149
  x: number;
1048
1150
  }[];
1049
1151
  };
1152
+ /** Per side (`left` the −x side, `right` the +x side, like `headEdges`),
1153
+ * that side's iris and the bangs' run it would slide under on the turn,
1154
+ * as real pixel edges on the row holding the iris's centre — see
1155
+ * `IrisStrand` for the contract and its three cases (a strand outward of
1156
+ * a clear iris, a run over the iris centre that clears on the face side,
1157
+ * a fringe spanning the face). A side's iris is whichever of `iris_L` /
1158
+ * `iris_R` sits on that side; the sides are independent, so either may
1159
+ * be absent. With it the turn keeps each far iris from sliding under its
1160
+ * run any further than it is painted, best effort: never a refusal. The
1161
+ * report's `strandOverlap` gives, per side whose far iris the run still
1162
+ * covers at some far stop, the covered width at the stop where it is
1163
+ * largest, next to the painted one, and whether the bound held — `true`
1164
+ * for an iris painted under its run that the turn took no deeper,
1165
+ * `false` where the fitted radius could not hold it and the eyes' turn
1166
+ * depth is 0. It is always
1167
+ * validated against the layers first (`validateStrandEdges`), but only
1168
+ * the turn solve reads it, so like `headEdges` it has no effect on the
1169
+ * rig without a nose; absent or `{}`, the rig is the one built without
1170
+ * it. */
1171
+ strandEdges?: {
1172
+ left?: IrisStrand;
1173
+ right?: IrisStrand;
1174
+ };
1050
1175
  }): IkiModel;
1051
1176
 
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 };
1177
+ 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 };