@ikijs/editor 0.7.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
@@ -123,7 +123,12 @@ decodes with canvas, `@ikijs/mcp` decodes with `sharp`, and both feed the same
123
123
  pure function — including the optional `rowHalfWidths` (one entry per crop row,
124
124
  half that row's opaque span in canvas px), which `@ikijs/mcp` measures for the
125
125
  face so the plate turns on a radius that varies by row; absent, it turns on one
126
- 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.
127
132
 
128
133
  `*_L` / `*_R` are the **character's** sides — `eye_L` is the character's left
129
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.
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.
package/dist/index.js CHANGED
@@ -1837,6 +1837,45 @@ function validateLayerInputs(layers, canvas) {
1837
1837
  }
1838
1838
  }
1839
1839
  }
1840
+ if (layer.denseCore !== void 0) {
1841
+ const where = `auto-rig: validateLayerInputs: role "${role}" denseCore`;
1842
+ const core = layer.denseCore;
1843
+ if (typeof core !== "object" || core === null || Array.isArray(core)) {
1844
+ throw new Error(`${where} must be a plain object { x, y, w, h }`);
1845
+ }
1846
+ for (const field of ["x", "y", "w", "h"]) {
1847
+ const v = core[field];
1848
+ const size = field === "w" || field === "h";
1849
+ if (typeof v !== "number" || !Number.isFinite(v) || size && v <= 0) {
1850
+ throw new Error(
1851
+ `${where}.${field} is ${String(v)}, not a finite number${size ? " > 0" : ""}`
1852
+ );
1853
+ }
1854
+ }
1855
+ const { x, y, w, h } = layer.denseCore;
1856
+ const right = bbox.x + bbox.w;
1857
+ const bottom = bbox.y + bbox.h;
1858
+ if (x < bbox.x) {
1859
+ throw new Error(
1860
+ `${where}.x (${x}) lies left of the bbox's left edge (${bbox.x})`
1861
+ );
1862
+ }
1863
+ if (y < bbox.y) {
1864
+ throw new Error(
1865
+ `${where}.y (${y}) lies above the bbox's top edge (${bbox.y})`
1866
+ );
1867
+ }
1868
+ if (x + w > right) {
1869
+ throw new Error(
1870
+ `${where}.w (${w}) puts its right edge at ${x + w}, past the bbox's right edge (${right})`
1871
+ );
1872
+ }
1873
+ if (y + h > bottom) {
1874
+ throw new Error(
1875
+ `${where}.h (${h}) puts its bottom edge at ${y + h}, past the bbox's bottom edge (${bottom})`
1876
+ );
1877
+ }
1878
+ }
1840
1879
  }
1841
1880
  }
1842
1881
  function validateStrandEdges(layers, strandEdges) {
@@ -2391,12 +2430,17 @@ function resolveTurnTargets(targets = {}) {
2391
2430
  defaulted
2392
2431
  };
2393
2432
  }
2433
+ function noseDrawnBox(layer) {
2434
+ return layer.denseCore ?? layer.bbox;
2435
+ }
2394
2436
  function turnLandmarks(layers, carriers) {
2395
2437
  const markOf = (role) => {
2396
2438
  const layer = layers.find((l) => l.role === role);
2397
2439
  if (!layer) return void 0;
2398
- const t = bboxToTransform(layer.bbox, layer.canvasW, layer.canvasH, role);
2399
- const mark = { x: t.x, y: t.y, w: layer.cropW };
2440
+ const drawn = role === "nose" ? noseDrawnBox(layer) : layer.bbox;
2441
+ const t = bboxToTransform(drawn, layer.canvasW, layer.canvasH, role);
2442
+ const w = drawn === layer.bbox ? layer.cropW : drawn.w;
2443
+ const mark = { x: t.x, y: t.y, w };
2400
2444
  const carrier = carriers?.get(role);
2401
2445
  if (carrier !== void 0) mark.carrier = carrier;
2402
2446
  return mark;
@@ -3347,6 +3391,7 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
3347
3391
  var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
3348
3392
  var HAIR_FRONT_DEPTH = 0.1;
3349
3393
  var MOUTH_TURN_TILT_DEG = 5;
3394
+ var NOSE_TURN_TILT_DEG = 6;
3350
3395
  var NOD_TRAVEL = 30;
3351
3396
  var NOD_BEND = 0.5;
3352
3397
  var HAIR_BACK_NOD_DEPTH = -0.135;
@@ -3959,11 +4004,31 @@ function turnSetup(layers) {
3959
4004
  y: mouthAt.y,
3960
4005
  tiltDeg: MOUTH_TURN_TILT_DEG
3961
4006
  };
4007
+ const noseLayer = layers.find((l) => l.role === "nose");
4008
+ let noseAnchor;
4009
+ if (noseLayer) {
4010
+ const drawn = noseDrawnBox(noseLayer);
4011
+ const at = bboxToTransform(
4012
+ drawn,
4013
+ noseLayer.canvasW,
4014
+ noseLayer.canvasH,
4015
+ "nose"
4016
+ );
4017
+ noseAnchor = {
4018
+ x: at.x,
4019
+ y: at.y + drawn.h / 2,
4020
+ tiltDeg: NOSE_TURN_TILT_DEG
4021
+ };
4022
+ }
3962
4023
  const carriers = new Map(
3963
- members.map((m) => [
3964
- m.role,
3965
- m.group === "mouthWarp" ? { grid: groupGrids.get(m.group), part: m.part, anchor: mouthAnchor } : { grid: groupGrids.get(m.group), part: m.part }
3966
- ])
4024
+ members.map((m) => {
4025
+ const grid = groupGrids.get(m.group);
4026
+ const anchor = m.group === "mouthWarp" ? mouthAnchor : m.group === "noseWarp" ? noseAnchor : void 0;
4027
+ return [
4028
+ m.role,
4029
+ anchor === void 0 ? { grid, part: m.part } : { grid, part: m.part, anchor }
4030
+ ];
4031
+ })
3967
4032
  );
3968
4033
  const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
3969
4034
  let reach = 0;