@ikijs/editor 0.7.0 → 0.8.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 +6 -1
- package/dist/index.d.mts +30 -6
- package/dist/index.d.ts +30 -6
- package/dist/index.js +144 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +144 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
@@ -949,12 +967,18 @@ interface IrisStrand {
|
|
|
949
967
|
*
|
|
950
968
|
* What happens when the layer set cannot reach a target depends on where the
|
|
951
969
|
* target came from. A number the CALLER passed is a measurement, so an
|
|
952
|
-
* unreachable one throws, naming the field and what was on offer
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
970
|
+
* unreachable one throws, naming the field and what was on offer — except a
|
|
971
|
+
* `silhouetteRatio` when no ratio in its own [0.5, 1.5] range renders on this
|
|
972
|
+
* layer set: that throw names the field and the whole range instead of an
|
|
973
|
+
* offer, since there is none narrower. Leaving the field out still rigs,
|
|
974
|
+
* because a defaulted one clamps.
|
|
975
|
+
*
|
|
976
|
+
* A number this module filled in — a default, or a nose/mouth derived from the
|
|
977
|
+
* eyes — is a style prior, not a promise about this character: it is CLAMPED
|
|
978
|
+
* to what the layer set can do and the rig is built. The alternative is a
|
|
979
|
+
* generator that refuses its own defaults, which would leave a perfectly good
|
|
980
|
+
* layer set with no model at all. `solveTurnModel` reports which fields it
|
|
981
|
+
* clamped.
|
|
958
982
|
*
|
|
959
983
|
* One measurement is clamped too: an `eyeShift` past the room the art leaves
|
|
960
984
|
* the far eye — the face plate's edge, and the bangs' side strand when
|
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.
|
|
@@ -949,12 +967,18 @@ interface IrisStrand {
|
|
|
949
967
|
*
|
|
950
968
|
* What happens when the layer set cannot reach a target depends on where the
|
|
951
969
|
* target came from. A number the CALLER passed is a measurement, so an
|
|
952
|
-
* unreachable one throws, naming the field and what was on offer
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
970
|
+
* unreachable one throws, naming the field and what was on offer — except a
|
|
971
|
+
* `silhouetteRatio` when no ratio in its own [0.5, 1.5] range renders on this
|
|
972
|
+
* layer set: that throw names the field and the whole range instead of an
|
|
973
|
+
* offer, since there is none narrower. Leaving the field out still rigs,
|
|
974
|
+
* because a defaulted one clamps.
|
|
975
|
+
*
|
|
976
|
+
* A number this module filled in — a default, or a nose/mouth derived from the
|
|
977
|
+
* eyes — is a style prior, not a promise about this character: it is CLAMPED
|
|
978
|
+
* to what the layer set can do and the rig is built. The alternative is a
|
|
979
|
+
* generator that refuses its own defaults, which would leave a perfectly good
|
|
980
|
+
* layer set with no model at all. `solveTurnModel` reports which fields it
|
|
981
|
+
* clamped.
|
|
958
982
|
*
|
|
959
983
|
* One measurement is clamped too: an `eyeShift` past the room the art leaves
|
|
960
984
|
* the far eye — the face plate's edge, and the bangs' side strand when
|
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) {
|
|
@@ -2013,6 +2052,7 @@ function meshCellsFor(w, h) {
|
|
|
2013
2052
|
}
|
|
2014
2053
|
var FACE_PLATE_CELLS = 10;
|
|
2015
2054
|
var FEATURE_GRID_CELLS = 4;
|
|
2055
|
+
var MOUTH_GRID_CELL_PX = 32;
|
|
2016
2056
|
var TURN_LATTICE_CELL_PX = 16;
|
|
2017
2057
|
var GRID_MARGIN = 0.12;
|
|
2018
2058
|
var HEAD_CYLINDER_RADIUS_FACTOR = 0.6 / 0.5;
|
|
@@ -2163,31 +2203,33 @@ function turnColumnMap(grid, faceCenterX, radiusX, angleX, travel) {
|
|
|
2163
2203
|
x + boundedCylinderBend(x - faceCenterX, radiusX, theta) + slide
|
|
2164
2204
|
);
|
|
2165
2205
|
}
|
|
2166
|
-
const cellFor = (
|
|
2206
|
+
const cellFor = (x) => {
|
|
2167
2207
|
let lo = 0;
|
|
2168
2208
|
let hi = grid.cols - 1;
|
|
2169
2209
|
while (lo < hi) {
|
|
2170
2210
|
const mid = lo + hi >> 1;
|
|
2171
|
-
if (
|
|
2211
|
+
if (x < restX[mid + 1]) hi = mid;
|
|
2172
2212
|
else lo = mid + 1;
|
|
2173
2213
|
}
|
|
2174
2214
|
return lo;
|
|
2175
2215
|
};
|
|
2176
|
-
const sample = (edges, values, v) => {
|
|
2177
|
-
const c = cellFor(v, edges);
|
|
2178
|
-
const s = Math.max(
|
|
2179
|
-
0,
|
|
2180
|
-
Math.min(1, (v - edges[c]) / (edges[c + 1] - edges[c]))
|
|
2181
|
-
);
|
|
2182
|
-
if (s === 0) return values[c];
|
|
2183
|
-
if (s === 1) return values[c + 1];
|
|
2184
|
-
return values[c] + (values[c + 1] - values[c]) * s;
|
|
2185
|
-
};
|
|
2186
2216
|
return {
|
|
2187
2217
|
restX,
|
|
2188
2218
|
warpedX,
|
|
2189
|
-
|
|
2190
|
-
|
|
2219
|
+
// Piecewise-linear read of `warpedX` over `restX`, the within-cell
|
|
2220
|
+
// fraction clamped to [0,1] as sampleWarpGrid clamps (s, t). A node reads
|
|
2221
|
+
// its own warped x: `warpedX[c] + Δ·1` is `warpedX[c + 1]` only to an
|
|
2222
|
+
// ulp, and the columns a bake reads are nodes.
|
|
2223
|
+
mapX: (x) => {
|
|
2224
|
+
const c = cellFor(x);
|
|
2225
|
+
const s = Math.max(
|
|
2226
|
+
0,
|
|
2227
|
+
Math.min(1, (x - restX[c]) / (restX[c + 1] - restX[c]))
|
|
2228
|
+
);
|
|
2229
|
+
if (s === 0) return warpedX[c];
|
|
2230
|
+
if (s === 1) return warpedX[c + 1];
|
|
2231
|
+
return warpedX[c] + (warpedX[c + 1] - warpedX[c]) * s;
|
|
2232
|
+
}
|
|
2191
2233
|
};
|
|
2192
2234
|
}
|
|
2193
2235
|
function renderedLandingX(part, vertexLanding, px, py) {
|
|
@@ -2293,7 +2335,7 @@ function plateLandingOn(plate, mapAt) {
|
|
|
2293
2335
|
y
|
|
2294
2336
|
);
|
|
2295
2337
|
}
|
|
2296
|
-
function plateGuardRowsFor(face, hairFront) {
|
|
2338
|
+
function plateGuardRowsFor(face, hairFront, widestY = -Infinity) {
|
|
2297
2339
|
const rowsOf = (part, y) => {
|
|
2298
2340
|
const { rows } = meshCellsFor(part.cropW, part.cropH);
|
|
2299
2341
|
return Array.from(
|
|
@@ -2308,7 +2350,7 @@ function plateGuardRowsFor(face, hairFront) {
|
|
|
2308
2350
|
return [
|
|
2309
2351
|
...faceRows,
|
|
2310
2352
|
...rowsOf(hairFront, hairFront.centerY).map(
|
|
2311
|
-
(y) => Math.max(bottom, Math.min(top, y))
|
|
2353
|
+
(y) => Math.max(bottom, Math.min(top, Math.max(y, widestY)))
|
|
2312
2354
|
)
|
|
2313
2355
|
];
|
|
2314
2356
|
}
|
|
@@ -2320,7 +2362,7 @@ function plateGuardsOf(carriers, faceHalfWidth, profile, hairFront) {
|
|
|
2320
2362
|
return {
|
|
2321
2363
|
plate,
|
|
2322
2364
|
edgeAt: profile === void 0 ? () => faceHalfWidth : profile.at,
|
|
2323
|
-
plateGuardRows: plateGuardRowsFor(plate.part, hairFront)
|
|
2365
|
+
plateGuardRows: plateGuardRowsFor(plate.part, hairFront, profile?.widestY)
|
|
2324
2366
|
};
|
|
2325
2367
|
}
|
|
2326
2368
|
var TurnTargetError = class extends Error {
|
|
@@ -2391,12 +2433,17 @@ function resolveTurnTargets(targets = {}) {
|
|
|
2391
2433
|
defaulted
|
|
2392
2434
|
};
|
|
2393
2435
|
}
|
|
2436
|
+
function noseDrawnBox(layer) {
|
|
2437
|
+
return layer.denseCore ?? layer.bbox;
|
|
2438
|
+
}
|
|
2394
2439
|
function turnLandmarks(layers, carriers) {
|
|
2395
2440
|
const markOf = (role) => {
|
|
2396
2441
|
const layer = layers.find((l) => l.role === role);
|
|
2397
2442
|
if (!layer) return void 0;
|
|
2398
|
-
const
|
|
2399
|
-
const
|
|
2443
|
+
const drawn = role === "nose" ? noseDrawnBox(layer) : layer.bbox;
|
|
2444
|
+
const t = bboxToTransform(drawn, layer.canvasW, layer.canvasH, role);
|
|
2445
|
+
const w = drawn === layer.bbox ? layer.cropW : drawn.w;
|
|
2446
|
+
const mark = { x: t.x, y: t.y, w };
|
|
2400
2447
|
const carrier = carriers?.get(role);
|
|
2401
2448
|
if (carrier !== void 0) mark.carrier = carrier;
|
|
2402
2449
|
return mark;
|
|
@@ -2493,14 +2540,15 @@ var TURN_SWEEP_SAMPLES = 32;
|
|
|
2493
2540
|
var HOLD_CLEARANCE = 1;
|
|
2494
2541
|
var TURN_HOLD_SAMPLES = 16;
|
|
2495
2542
|
var TURN_SETTLE_PASSES = 24;
|
|
2496
|
-
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge) {
|
|
2543
|
+
function hairFrontHoldTarget(x, y, deg, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdge, widestY = -Infinity) {
|
|
2497
2544
|
const local = x - faceCenterX;
|
|
2498
2545
|
const dist = Math.abs(local);
|
|
2499
2546
|
const side = Math.sign(local);
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2547
|
+
const row = Math.max(y, widestY);
|
|
2548
|
+
const painted = edgeAt(row);
|
|
2549
|
+
if (dist <= painted) return plateLandingAt(deg, x, row);
|
|
2502
2550
|
if (dist <= holdBase) {
|
|
2503
|
-
const inner = plateLandingAt(deg, faceCenterX + side * painted,
|
|
2551
|
+
const inner = plateLandingAt(deg, faceCenterX + side * painted, row);
|
|
2504
2552
|
const u = (dist - painted) / (holdBase - painted);
|
|
2505
2553
|
return inner + (faceCenterX + side * holdEdge - inner) * u;
|
|
2506
2554
|
}
|
|
@@ -2605,7 +2653,8 @@ function evaluateTurnCandidate(ctx, radius) {
|
|
|
2605
2653
|
plateLandingAt,
|
|
2606
2654
|
ctx.edgeAt,
|
|
2607
2655
|
hold.holdBase,
|
|
2608
|
-
hold.holdEdgeAt(deg)
|
|
2656
|
+
hold.holdEdgeAt(deg),
|
|
2657
|
+
ctx.profile?.widestY
|
|
2609
2658
|
),
|
|
2610
2659
|
leadShift
|
|
2611
2660
|
);
|
|
@@ -3347,6 +3396,7 @@ function solveTurnModel(targets, landmarks, lattice, faceCenterX, faceHalfWidth,
|
|
|
3347
3396
|
var EYE_STACK_PREFIXES = ["eye_", "iris_", "pupil_", "highlight_"];
|
|
3348
3397
|
var HAIR_FRONT_DEPTH = 0.1;
|
|
3349
3398
|
var MOUTH_TURN_TILT_DEG = 5;
|
|
3399
|
+
var NOSE_TURN_TILT_DEG = 6;
|
|
3350
3400
|
var NOD_TRAVEL = 30;
|
|
3351
3401
|
var NOD_BEND = 0.5;
|
|
3352
3402
|
var HAIR_BACK_NOD_DEPTH = -0.135;
|
|
@@ -3579,7 +3629,7 @@ function plateReachAt(plateLandingAt, faceCenterX, edgeAt, plateGuardRows, deg)
|
|
|
3579
3629
|
)
|
|
3580
3630
|
);
|
|
3581
3631
|
}
|
|
3582
|
-
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows) {
|
|
3632
|
+
function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandingAt, edgeAt, holdBase, holdEdgeAt, plateGuardRows, widestY = -Infinity) {
|
|
3583
3633
|
if (holdEdgeAt(0) !== holdBase) {
|
|
3584
3634
|
throw new Error(
|
|
3585
3635
|
`auto-rig: bakeHairFrontSilhouetteWarp: holdEdgeAt(0) is ${holdEdgeAt(0)}, not the hold edge's own rest distance ${holdBase}, so the bangs would move in the rest pose`
|
|
@@ -3617,7 +3667,8 @@ function bakeHairFrontSilhouetteWarp(mesh, partX, partY, faceCenterX, plateLandi
|
|
|
3617
3667
|
plateLandingAt,
|
|
3618
3668
|
edgeAt,
|
|
3619
3669
|
holdBase,
|
|
3620
|
-
holdEdge
|
|
3670
|
+
holdEdge,
|
|
3671
|
+
widestY
|
|
3621
3672
|
) - x,
|
|
3622
3673
|
0
|
|
3623
3674
|
);
|
|
@@ -3796,7 +3847,7 @@ function defaultPoseOf(bindings) {
|
|
|
3796
3847
|
return binding.from + (binding.to - binding.from) * t;
|
|
3797
3848
|
});
|
|
3798
3849
|
}
|
|
3799
|
-
function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
3850
|
+
function groupGridFor(members, cells, turnShiftPx, nodShiftPx, cellPx) {
|
|
3800
3851
|
let minX = Infinity;
|
|
3801
3852
|
let maxX = -Infinity;
|
|
3802
3853
|
let minY = Infinity;
|
|
@@ -3831,24 +3882,50 @@ function groupGridFor(members, cells, turnShiftPx, nodShiftPx) {
|
|
|
3831
3882
|
}
|
|
3832
3883
|
}
|
|
3833
3884
|
}
|
|
3885
|
+
const extentMinX = minX;
|
|
3886
|
+
const extentMaxX = maxX;
|
|
3834
3887
|
minX -= turnShiftPx;
|
|
3835
3888
|
maxX += turnShiftPx;
|
|
3836
3889
|
minY -= nodShiftPx;
|
|
3837
3890
|
maxY += nodShiftPx;
|
|
3838
3891
|
const marginX = (maxX - minX) * GRID_MARGIN + 1;
|
|
3839
3892
|
const marginY = (maxY - minY) * GRID_MARGIN + 1;
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3893
|
+
const points = generateGridPoints(
|
|
3894
|
+
cells,
|
|
3895
|
+
cells,
|
|
3896
|
+
minX - marginX,
|
|
3897
|
+
maxX + marginX,
|
|
3898
|
+
minY - marginY,
|
|
3899
|
+
maxY + marginY
|
|
3900
|
+
);
|
|
3901
|
+
if (cellPx === void 0) return { cols: cells, rows: cells, points };
|
|
3902
|
+
const stride = cells + 1;
|
|
3903
|
+
const uniformXs = Array.from({ length: stride }, (_, c) => points[c * 2]);
|
|
3904
|
+
const ys = Array.from(
|
|
3905
|
+
{ length: stride },
|
|
3906
|
+
(_, r) => points[r * stride * 2 + 1]
|
|
3907
|
+
);
|
|
3908
|
+
const xs = [uniformXs[0]];
|
|
3909
|
+
for (let c = 0; c < cells; c++) {
|
|
3910
|
+
const left = uniformXs[c];
|
|
3911
|
+
const right = uniformXs[c + 1];
|
|
3912
|
+
const from = Math.max(left, extentMinX);
|
|
3913
|
+
const to = Math.min(right, extentMaxX);
|
|
3914
|
+
if (right - left > cellPx && from < to) {
|
|
3915
|
+
const steps = Math.ceil((to - from) / cellPx);
|
|
3916
|
+
if (from > left) xs.push(from);
|
|
3917
|
+
for (let k = 1; k < steps; k++) {
|
|
3918
|
+
xs.push(from + k * (to - from) / steps);
|
|
3919
|
+
}
|
|
3920
|
+
if (to < right) xs.push(to);
|
|
3921
|
+
}
|
|
3922
|
+
xs.push(right);
|
|
3923
|
+
}
|
|
3924
|
+
const merged = [];
|
|
3925
|
+
for (const y of ys) {
|
|
3926
|
+
for (const x of xs) merged.push(x, y);
|
|
3927
|
+
}
|
|
3928
|
+
return { cols: xs.length - 1, rows: cells, points: merged };
|
|
3852
3929
|
}
|
|
3853
3930
|
function eyelidFoldFor(layers, layer, mesh, t) {
|
|
3854
3931
|
const side = ROLE_TABLE[layer.role].eyeSide;
|
|
@@ -3949,7 +4026,8 @@ function turnSetup(layers) {
|
|
|
3949
4026
|
own,
|
|
3950
4027
|
isPlate ? FACE_PLATE_CELLS : FEATURE_GRID_CELLS,
|
|
3951
4028
|
isPlate ? 0 : turnShift[turnFamily(role)],
|
|
3952
|
-
nodShiftPx
|
|
4029
|
+
nodShiftPx,
|
|
4030
|
+
group === "mouthWarp" ? MOUTH_GRID_CELL_PX : void 0
|
|
3953
4031
|
)
|
|
3954
4032
|
);
|
|
3955
4033
|
}
|
|
@@ -3959,11 +4037,31 @@ function turnSetup(layers) {
|
|
|
3959
4037
|
y: mouthAt.y,
|
|
3960
4038
|
tiltDeg: MOUTH_TURN_TILT_DEG
|
|
3961
4039
|
};
|
|
4040
|
+
const noseLayer = layers.find((l) => l.role === "nose");
|
|
4041
|
+
let noseAnchor;
|
|
4042
|
+
if (noseLayer) {
|
|
4043
|
+
const drawn = noseDrawnBox(noseLayer);
|
|
4044
|
+
const at = bboxToTransform(
|
|
4045
|
+
drawn,
|
|
4046
|
+
noseLayer.canvasW,
|
|
4047
|
+
noseLayer.canvasH,
|
|
4048
|
+
"nose"
|
|
4049
|
+
);
|
|
4050
|
+
noseAnchor = {
|
|
4051
|
+
x: at.x,
|
|
4052
|
+
y: at.y + drawn.h / 2,
|
|
4053
|
+
tiltDeg: NOSE_TURN_TILT_DEG
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
3962
4056
|
const carriers = new Map(
|
|
3963
|
-
members.map((m) =>
|
|
3964
|
-
m.
|
|
3965
|
-
m.group === "mouthWarp" ?
|
|
3966
|
-
|
|
4057
|
+
members.map((m) => {
|
|
4058
|
+
const grid = groupGrids.get(m.group);
|
|
4059
|
+
const anchor = m.group === "mouthWarp" ? mouthAnchor : m.group === "noseWarp" ? noseAnchor : void 0;
|
|
4060
|
+
return [
|
|
4061
|
+
m.role,
|
|
4062
|
+
anchor === void 0 ? { grid, part: m.part } : { grid, part: m.part, anchor }
|
|
4063
|
+
];
|
|
4064
|
+
})
|
|
3967
4065
|
);
|
|
3968
4066
|
const maxShift = Math.max(turnShift.eye, turnShift.nose, turnShift.mouth);
|
|
3969
4067
|
let reach = 0;
|
|
@@ -4269,7 +4367,8 @@ function generateIkiFromLayerSet(layers, canvas, options = {}) {
|
|
|
4269
4367
|
edgeAt,
|
|
4270
4368
|
holdBase,
|
|
4271
4369
|
turn?.holdEdgeAt ?? (() => holdBase),
|
|
4272
|
-
plateGuardRows
|
|
4370
|
+
plateGuardRows,
|
|
4371
|
+
profile?.widestY
|
|
4273
4372
|
)
|
|
4274
4373
|
);
|
|
4275
4374
|
ownWarps.push(
|