@ikijs/editor 0.4.0 → 0.5.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,6 +60,35 @@ 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
package/dist/index.d.mts CHANGED
@@ -891,6 +891,92 @@ declare function parseLayerRoles(fileNames: string[]): {
891
891
  role: string;
892
892
  fileName: string;
893
893
  }[];
894
+ /**
895
+ * What a head does at full turn, as the cues `measure_turn_reference` reads off
896
+ * a front/turned reference pair. The turn is fitted to these — the cylinder's
897
+ * radius and the features' depths are solved from them, not tuned.
898
+ *
899
+ * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
900
+ * reference measures, and the nose and the mouth derive from the eyes when they
901
+ * are not given.
902
+ *
903
+ * What happens when the layer set cannot reach a target depends on where the
904
+ * target came from. A number the CALLER passed is a measurement, so an
905
+ * unreachable one throws, naming the field and what was on offer. A number this
906
+ * module filled in — a default, or a nose/mouth derived from the eyes — is a
907
+ * style prior, not a promise about this character: it is CLAMPED to what the
908
+ * layer set can do and the rig is built. The alternative is a generator that
909
+ * refuses its own defaults, which would leave a perfectly good layer set with
910
+ * no model at all. `solveTurnModel` reports which fields it clamped.
911
+ */
912
+ interface TurnTargets {
913
+ /** How far the eye pair's centre slides toward the far side at full turn, as
914
+ * a fraction of the head's half-width. The sign is ignored: which way is
915
+ * the turn's own business. */
916
+ eyeShift?: number;
917
+ /** The far eye's width over the near eye's at full turn, divided by the same
918
+ * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
919
+ farEyeRatio?: number;
920
+ /** The head's half-width at full turn over its half-width at rest, as a
921
+ * RENDER measures it — the outermost opaque edge on each side, whichever
922
+ * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
923
+ * produces it is fitted to it, so a measured one is met or refused, never
924
+ * quietly rendered as something else. */
925
+ silhouetteRatio?: number;
926
+ /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
927
+ noseShift?: number;
928
+ /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
929
+ mouthShift?: number;
930
+ /** The head's own half-width at the eye row, in canvas px: what the shift
931
+ * fractions are fractions OF, and the rest distance the silhouette hold
932
+ * pivots on. Without it both fall back to the face plate's half-width, which
933
+ * is narrower than the head the hair draws, so the shifts land short — an
934
+ * approximation for callers that have no pixels, not a default to prefer. */
935
+ headHalfWidth?: number;
936
+ }
937
+ /**
938
+ * A `turnTargets` field this generator will not build a rig from: not a number,
939
+ * outside its range, unreachable on this layer set, or a measured head narrower
940
+ * than the face plate it is drawn around. Every one of them carries a number
941
+ * the CALLER passed, so a host can tell them apart from the invariant breaks the
942
+ * rest of the generator throws and report them as bad input instead of failing.
943
+ */
944
+ declare class TurnTargetError extends Error {
945
+ constructor(message: string);
946
+ }
947
+ /**
948
+ * The reference the turn is fitted to when the caller measured none of its own:
949
+ * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
950
+ * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
951
+ * normalised by the same ratio at rest, which is what the field means).
952
+ * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
953
+ */
954
+ declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
955
+ /** The turn depth of each feature family: the share of the pinned bulk slide
956
+ * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
957
+ interface TurnDepths {
958
+ eye: number;
959
+ nose: number;
960
+ mouth: number;
961
+ }
962
+ /** What the turn solve settled on, for a caller that wants to see it — the
963
+ * cues it reached and the targets it had to cut down to reach them. */
964
+ interface TurnSolveReport {
965
+ /** Cylinder radius the face-warp bake and the parallax unit were built on. */
966
+ radius: number;
967
+ /** Rest distance from the face centre the silhouette hold pivots on. */
968
+ holdBase: number;
969
+ depths: TurnDepths;
970
+ /** What the rig actually reaches, in the targets' own units. */
971
+ achieved: {
972
+ eyeShift: number;
973
+ farEyeRatio: number;
974
+ silhouetteRatio: number;
975
+ };
976
+ /** Defaulted fields the layer set could not reach, cut down to what it can.
977
+ * Empty when every target was met. */
978
+ clamped: (keyof TurnTargets)[];
979
+ }
894
980
  /**
895
981
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
896
982
  * valid IkiModel ready for parseIkiModel.
@@ -900,7 +986,8 @@ declare function parseLayerRoles(fileNames: string[]): {
900
986
  * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a
901
987
  * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front
902
988
  * layer is present.
903
- * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings),
989
+ * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
990
+ * turn translates nothing rigidly; its travel is in the face warp),
904
991
  * bodyDeformer when a body layer is present (matrix, torso-base pivot,
905
992
  * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,
906
993
  * baked cylinder warp center-relative on faceCenterX).
@@ -910,10 +997,36 @@ declare function parseLayerRoles(fileNames: string[]): {
910
997
  * - Part ids equal the role string (deterministic, no crypto.randomUUID).
911
998
  * - Return parseIkiModel(structuredClone(model)) — every caller gets a
912
999
  * validated model; bad assembly fails loudly.
1000
+ *
1001
+ * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1002
+ * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1003
+ * passed that this layer set cannot reach throws; one that came from the
1004
+ * defaults is clamped to what it can do — see TurnTargets for why the two
1005
+ * differ, and pass `options.onTurnSolved` to see which ones were clamped and
1006
+ * what the turn ended up reaching.
913
1007
  */
914
1008
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
915
1009
  width: number;
916
1010
  height: number;
1011
+ }, options?: {
1012
+ turnTargets?: TurnTargets;
1013
+ /** Called once, after a turn is solved, with what it settled on. Not called
1014
+ * for a layer set without a nose, which solves no turn at all. */
1015
+ onTurnSolved?: (report: TurnSolveReport) => void;
1016
+ /** Every role with an opaque pixel in the eye-row band, per side, when
1017
+ * `turnTargets.headHalfWidth` was measured off that same union — a
1018
+ * companion to it, not a turn target itself (the caller-facing contract
1019
+ * stays the three cues). See `TurnSolveContext.headEdges`. */
1020
+ headEdges?: {
1021
+ left: {
1022
+ role: string;
1023
+ x: number;
1024
+ }[];
1025
+ right: {
1026
+ role: string;
1027
+ x: number;
1028
+ }[];
1029
+ };
917
1030
  }): IkiModel;
918
1031
 
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 };
1032
+ 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
@@ -891,6 +891,92 @@ declare function parseLayerRoles(fileNames: string[]): {
891
891
  role: string;
892
892
  fileName: string;
893
893
  }[];
894
+ /**
895
+ * What a head does at full turn, as the cues `measure_turn_reference` reads off
896
+ * a front/turned reference pair. The turn is fitted to these — the cylinder's
897
+ * radius and the features' depths are solved from them, not tuned.
898
+ *
899
+ * Every field is optional: `DEFAULT_TURN_TARGETS` supplies the three a
900
+ * reference measures, and the nose and the mouth derive from the eyes when they
901
+ * are not given.
902
+ *
903
+ * What happens when the layer set cannot reach a target depends on where the
904
+ * target came from. A number the CALLER passed is a measurement, so an
905
+ * unreachable one throws, naming the field and what was on offer. A number this
906
+ * module filled in — a default, or a nose/mouth derived from the eyes — is a
907
+ * style prior, not a promise about this character: it is CLAMPED to what the
908
+ * layer set can do and the rig is built. The alternative is a generator that
909
+ * refuses its own defaults, which would leave a perfectly good layer set with
910
+ * no model at all. `solveTurnModel` reports which fields it clamped.
911
+ */
912
+ interface TurnTargets {
913
+ /** How far the eye pair's centre slides toward the far side at full turn, as
914
+ * a fraction of the head's half-width. The sign is ignored: which way is
915
+ * the turn's own business. */
916
+ eyeShift?: number;
917
+ /** The far eye's width over the near eye's at full turn, divided by the same
918
+ * ratio at rest. 1 is no foreshortening; 0 is an edge-on far eye. */
919
+ farEyeRatio?: number;
920
+ /** The head's half-width at full turn over its half-width at rest, as a
921
+ * RENDER measures it — the outermost opaque edge on each side, whichever
922
+ * part owns it. 1 holds the silhouette; below 1 narrows it. The hold that
923
+ * produces it is fitted to it, so a measured one is met or refused, never
924
+ * quietly rendered as something else. */
925
+ silhouetteRatio?: number;
926
+ /** `eyeShift` for the nose. Derived from `eyeShift` when absent. */
927
+ noseShift?: number;
928
+ /** `eyeShift` for the mouth. Derived from `eyeShift` when absent. */
929
+ mouthShift?: number;
930
+ /** The head's own half-width at the eye row, in canvas px: what the shift
931
+ * fractions are fractions OF, and the rest distance the silhouette hold
932
+ * pivots on. Without it both fall back to the face plate's half-width, which
933
+ * is narrower than the head the hair draws, so the shifts land short — an
934
+ * approximation for callers that have no pixels, not a default to prefer. */
935
+ headHalfWidth?: number;
936
+ }
937
+ /**
938
+ * A `turnTargets` field this generator will not build a rig from: not a number,
939
+ * outside its range, unreachable on this layer set, or a measured head narrower
940
+ * than the face plate it is drawn around. Every one of them carries a number
941
+ * the CALLER passed, so a host can tell them apart from the invariant breaks the
942
+ * rest of the generator throws and report them as bad input instead of failing.
943
+ */
944
+ declare class TurnTargetError extends Error {
945
+ constructor(message: string);
946
+ }
947
+ /**
948
+ * The reference the turn is fitted to when the caller measured none of its own:
949
+ * a 3/4 portrait, which `measure_turn_reference` reads as 0.671 / −0.224 /
950
+ * 1.012 (its raw turned far/near iris ratio is 0.71 — 0.67 is that ratio
951
+ * normalised by the same ratio at rest, which is what the field means).
952
+ * The silhouette is held exactly: a 1 % change is inside that tool's own noise.
953
+ */
954
+ declare const DEFAULT_TURN_TARGETS: Readonly<Required<Pick<TurnTargets, "eyeShift" | "farEyeRatio" | "silhouetteRatio">>>;
955
+ /** The turn depth of each feature family: the share of the pinned bulk slide
956
+ * (`headTurnParallaxUnit`) it gets back at full turn. Solved, never tuned. */
957
+ interface TurnDepths {
958
+ eye: number;
959
+ nose: number;
960
+ mouth: number;
961
+ }
962
+ /** What the turn solve settled on, for a caller that wants to see it — the
963
+ * cues it reached and the targets it had to cut down to reach them. */
964
+ interface TurnSolveReport {
965
+ /** Cylinder radius the face-warp bake and the parallax unit were built on. */
966
+ radius: number;
967
+ /** Rest distance from the face centre the silhouette hold pivots on. */
968
+ holdBase: number;
969
+ depths: TurnDepths;
970
+ /** What the rig actually reaches, in the targets' own units. */
971
+ achieved: {
972
+ eyeShift: number;
973
+ farEyeRatio: number;
974
+ silhouetteRatio: number;
975
+ };
976
+ /** Defaulted fields the layer set could not reach, cut down to what it can.
977
+ * Empty when every target was met. */
978
+ clamped: (keyof TurnTargets)[];
979
+ }
894
980
  /**
895
981
  * Auto-rig: given decoded layer inputs and the shared canvas size, produce a
896
982
  * valid IkiModel ready for parseIkiModel.
@@ -900,7 +986,8 @@ declare function parseLayerRoles(fileNames: string[]): {
900
986
  * - Emit the standard parameters (same ids/ranges as sample-model.ts), plus a
901
987
  * conditional HairSwayX descriptor + hair-sway physics rig when a hair_front
902
988
  * layer is present.
903
- * - Build headDeformer (matrix, neck pivot, AngleX+Breath bindings),
989
+ * - Build headDeformer (matrix, neck pivot, nod/tilt/breath bindings — the
990
+ * turn translates nothing rigidly; its travel is in the face warp),
904
991
  * bodyDeformer when a body layer is present (matrix, torso-base pivot,
905
992
  * AngleX follow + Breath follow), and faceWarp (warp, FACE_GRID_CELLS²,
906
993
  * baked cylinder warp center-relative on faceCenterX).
@@ -910,10 +997,36 @@ declare function parseLayerRoles(fileNames: string[]): {
910
997
  * - Part ids equal the role string (deterministic, no crypto.randomUUID).
911
998
  * - Return parseIkiModel(structuredClone(model)) — every caller gets a
912
999
  * validated model; bad assembly fails loudly.
1000
+ *
1001
+ * `options.turnTargets` is what the head turn is fitted to: the cues a 30°
1002
+ * reference measures, defaulting to DEFAULT_TURN_TARGETS. A target the CALLER
1003
+ * passed that this layer set cannot reach throws; one that came from the
1004
+ * defaults is clamped to what it can do — see TurnTargets for why the two
1005
+ * differ, and pass `options.onTurnSolved` to see which ones were clamped and
1006
+ * what the turn ended up reaching.
913
1007
  */
914
1008
  declare function generateIkiFromLayerSet(layers: LayerInput[], canvas: {
915
1009
  width: number;
916
1010
  height: number;
1011
+ }, options?: {
1012
+ turnTargets?: TurnTargets;
1013
+ /** Called once, after a turn is solved, with what it settled on. Not called
1014
+ * for a layer set without a nose, which solves no turn at all. */
1015
+ onTurnSolved?: (report: TurnSolveReport) => void;
1016
+ /** Every role with an opaque pixel in the eye-row band, per side, when
1017
+ * `turnTargets.headHalfWidth` was measured off that same union — a
1018
+ * companion to it, not a turn target itself (the caller-facing contract
1019
+ * stays the three cues). See `TurnSolveContext.headEdges`. */
1020
+ headEdges?: {
1021
+ left: {
1022
+ role: string;
1023
+ x: number;
1024
+ }[];
1025
+ right: {
1026
+ role: string;
1027
+ x: number;
1028
+ }[];
1029
+ };
917
1030
  }): IkiModel;
918
1031
 
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 };
1032
+ 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 };