@rig-c/parts 1.31.0 → 1.33.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/cli.ts +12 -7
- package/docs/AUTHORING.md +24 -3
- package/docs/STABILITY.md +5 -4
- package/package.json +2 -2
- package/src/build.ts +98 -10
- package/src/check.ts +223 -6
- package/src/config.ts +148 -8
- package/src/parts.ts +7 -0
- package/src/propose.ts +1 -1
- package/src/rig.ts +213 -10
- package/src/turn.ts +302 -45
package/src/rig.ts
CHANGED
|
@@ -164,14 +164,15 @@ import {
|
|
|
164
164
|
} from './automesh.ts';
|
|
165
165
|
import { type ContourChainRibs, type ContourReport, contourMesh, type ContourRegion, type ContourRib } from './contour.ts';
|
|
166
166
|
import { type LocalInfluence, localInfluences } from './localweights.ts';
|
|
167
|
-
import { artCoverage, ART_ALPHA, latticeMesh, latticeStray, ONE_LOOP_PASSES } from './mesh.ts';
|
|
167
|
+
import { artCoverage, ART_ALPHA, latticeCells, latticeMesh, latticeStray, ONE_LOOP_PASSES } from './mesh.ts';
|
|
168
168
|
import { bindFaceSurfaceDeform, BLINK, blinkHoldMisses, blinkSpan, CONTROL_SUFFIX, controlledBones, IDLE_FPS, idleMotion, type MotionSpec, moveKeysToControls } from './motion.ts';
|
|
169
169
|
import { type AlternateSetMap, PAINTING_RUN, type PartsFile, readFrom, tagOf } from './parts.ts';
|
|
170
170
|
import { EYE_GROUP_TAGS } from './propose.ts';
|
|
171
171
|
import { alphaAbove, crop, pad, type Raster } from './raster/index.ts';
|
|
172
172
|
import { pyRound } from './round.ts';
|
|
173
173
|
import { boneHeat, type HeatDust, heatInfluences, HEAT_LIMITS, heatRow, type HeatRow, latticeSilhouette, tracedSilhouette } from './heat.ts';
|
|
174
|
-
import
|
|
174
|
+
import type { PosePoints } from './skeleton.ts';
|
|
175
|
+
import { applyFaceSurfaceDeform, FACE_RATED_BAND, TURN_BONE, type TurnSpec, turnConstraintName, turnFractions, turnTwin } from './turn.ts';
|
|
175
176
|
import { DISTANCE_EXPONENT, type FloorForm, type Influence, influences, type InfluenceLimits, type MeshInfluences, meshLimits, type Segment } from './weights.ts';
|
|
176
177
|
|
|
177
178
|
/** Transparent pixels added round every part image — the reference's `PAD`. */
|
|
@@ -907,6 +908,7 @@ export const SETS_FILE = 'sets.json';
|
|
|
907
908
|
*/
|
|
908
909
|
export interface AttachmentSetsFile {
|
|
909
910
|
spec: typeof SETS_SPEC;
|
|
911
|
+
threshold?: { yaw: number; pitch: number };
|
|
910
912
|
sets: Record<string, AlternateSetMap>;
|
|
911
913
|
}
|
|
912
914
|
|
|
@@ -933,6 +935,93 @@ export interface RigOutput {
|
|
|
933
935
|
sets?: AttachmentSetsFile;
|
|
934
936
|
}
|
|
935
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Issue #465: pose a rig's bones so each bone named in `drawn` reaches that
|
|
940
|
+
* world rotation (in Spine degrees, CCW, y up), adjusting its local `rotation`
|
|
941
|
+
* from its parent's posed transform. Bones are walked in parent-first order.
|
|
942
|
+
*/
|
|
943
|
+
export function poseRigBonesForDrawn(
|
|
944
|
+
bones: readonly RigBone[],
|
|
945
|
+
drawn: Readonly<Record<string, number>>,
|
|
946
|
+
stops?: ReadonlyArray<{ links: readonly string[] }>,
|
|
947
|
+
): RigBone[] {
|
|
948
|
+
const undrawn = new Map<string, string>();
|
|
949
|
+
for (const s of stops ?? []) for (const l of s.links) undrawn.set(`${l}_d`, l);
|
|
950
|
+
const realBone = (n: string): string => undrawn.get(n) ?? n;
|
|
951
|
+
const posed: RigBone[] = [];
|
|
952
|
+
const posedRead: RigBone[] = [];
|
|
953
|
+
for (const b of bones) {
|
|
954
|
+
const readB: RigBone = { ...b, ...(b.parent === undefined ? {} : { parent: realBone(b.parent) }) };
|
|
955
|
+
const target = drawn[b.name];
|
|
956
|
+
if (target === undefined) {
|
|
957
|
+
posed.push({ ...b });
|
|
958
|
+
posedRead.push(readB);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const m0 = computeExactFrameTransforms([...posedRead, { ...readB, rotation: 0 }]).get(b.name) as BoneTransform;
|
|
962
|
+
const rotation = places(normaliseDegrees(target - normaliseDegrees(m0.worldRotation)));
|
|
963
|
+
posed.push({ ...b, rotation });
|
|
964
|
+
posedRead.push({ ...readB, rotation });
|
|
965
|
+
}
|
|
966
|
+
return posed;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Issue #465: derive the alternate's `drawn` bone world rotations (in Spine
|
|
971
|
+
* degrees, CCW, y up) from its body-18 skeleton read (`pts`, in rig pixels, y
|
|
972
|
+
* down) against the base rig's setup bone origins (`B`) and setup world turns
|
|
973
|
+
* (`turn`).
|
|
974
|
+
*/
|
|
975
|
+
export function deriveAlternateDrawn(
|
|
976
|
+
pts: PosePoints,
|
|
977
|
+
B: ReadonlyMap<string, { x: number; y: number }>,
|
|
978
|
+
turn: ReadonlyMap<string, number>,
|
|
979
|
+
CX: number,
|
|
980
|
+
H: number,
|
|
981
|
+
): Record<string, number> {
|
|
982
|
+
const sx = (x: number): number => x - CX;
|
|
983
|
+
const sy = (y: number): number => cropToSpineY(y, H);
|
|
984
|
+
const deg = (a: readonly [number, number], b: readonly [number, number]): number =>
|
|
985
|
+
(Math.atan2(sy(b[1]) - sy(a[1]), sx(b[0]) - sx(a[0])) * 180) / Math.PI;
|
|
986
|
+
const mid = (a: readonly [number, number], b: readonly [number, number]): [number, number] => [
|
|
987
|
+
(a[0] + b[0]) / 2,
|
|
988
|
+
(a[1] + b[1]) / 2,
|
|
989
|
+
];
|
|
990
|
+
const out: Record<string, number> = {};
|
|
991
|
+
if (B.has('chest') && B.has('hip') && B.has('sleeve_r0') && B.has('sleeve_l0')) {
|
|
992
|
+
const hip = B.get('hip') as { x: number; y: number };
|
|
993
|
+
const sr0 = B.get('sleeve_r0') as { x: number; y: number };
|
|
994
|
+
const sl0 = B.get('sleeve_l0') as { x: number; y: number };
|
|
995
|
+
const setupTrunkAim = deg([hip.x, hip.y], mid([sr0.x, sr0.y], [sl0.x, sl0.y]));
|
|
996
|
+
const altTrunkAim = deg(mid(pts.r_hip, pts.l_hip), mid(pts.r_shoulder, pts.l_shoulder));
|
|
997
|
+
out.chest = places(normaliseDegrees((turn.get('chest') ?? 0) + (altTrunkAim - setupTrunkAim)));
|
|
998
|
+
}
|
|
999
|
+
if (B.has('head') && B.has('eye_r') && B.has('eye_l')) {
|
|
1000
|
+
const er = B.get('eye_r') as { x: number; y: number };
|
|
1001
|
+
const el = B.get('eye_l') as { x: number; y: number };
|
|
1002
|
+
const setupEyeAim = deg([er.x, er.y], [el.x, el.y]);
|
|
1003
|
+
const altEyeAim = deg(pts.r_eye, pts.l_eye);
|
|
1004
|
+
out.head = places(normaliseDegrees((turn.get('head') ?? 0) + (altEyeAim - setupEyeAim)));
|
|
1005
|
+
}
|
|
1006
|
+
const limbPairs: ReadonlyArray<readonly [string, readonly [number, number], readonly [number, number]]> = [
|
|
1007
|
+
['sleeve_r0', pts.r_shoulder, pts.r_elbow],
|
|
1008
|
+
['sleeve_r1', pts.r_elbow, pts.r_wrist],
|
|
1009
|
+
['sleeve_l0', pts.l_shoulder, pts.l_elbow],
|
|
1010
|
+
['sleeve_l1', pts.l_elbow, pts.l_wrist],
|
|
1011
|
+
['leg_r0', pts.r_hip, pts.r_knee],
|
|
1012
|
+
['leg_r1', pts.r_knee, pts.r_ankle],
|
|
1013
|
+
['leg_l0', pts.l_hip, pts.l_knee],
|
|
1014
|
+
['leg_l1', pts.l_knee, pts.l_ankle],
|
|
1015
|
+
];
|
|
1016
|
+
for (const [bone, a, b] of limbPairs) {
|
|
1017
|
+
if (B.has(bone)) out[bone] = places(normaliseDegrees(deg(a, b)));
|
|
1018
|
+
}
|
|
1019
|
+
for (const foot of ['foot_r', 'foot_l'] as const) {
|
|
1020
|
+
if (B.has(foot)) out[foot] = places(normaliseDegrees(turn.get(foot) ?? 0));
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
936
1025
|
/**
|
|
937
1026
|
* `RIG_BLINK_HOLD_SPANS_A_FRAME` (issue #32): the eyes' hold must put an
|
|
938
1027
|
* idle frame inside the closed window for every `blink.t`, or the idle
|
|
@@ -1982,15 +2071,51 @@ export function buildRig(
|
|
|
1982
2071
|
});
|
|
1983
2072
|
}
|
|
1984
2073
|
refuseIfAny(problems);
|
|
1985
|
-
// ---- alternate painting sets (
|
|
2074
|
+
// ---- alternate painting sets (issues #454, #465) ------------------------
|
|
1986
2075
|
// Only when `parts.alternates` is present: each alternate set's `<set>_<part>`
|
|
1987
2076
|
// attachments are added to `skins.default[<part>]` on the existing bones and
|
|
1988
2077
|
// slots before `applyStops` and `applyTurn`, so stopped links and turn twins
|
|
1989
|
-
// carry the alternate attachments alongside the base ones.
|
|
2078
|
+
// carry the alternate attachments alongside the base ones. When the alternate
|
|
2079
|
+
// carries a skeleton read (`alt.skeleton`) or explicit `drawn` angles, its
|
|
2080
|
+
// attachments are bound in the posed bone frames (`altWorld`) and `sets.json`
|
|
2081
|
+
// records `drawn` so a retarget does not pose the art a second time.
|
|
1990
2082
|
let sets: AttachmentSetsFile | undefined;
|
|
1991
2083
|
if (parts.alternates !== undefined && parts.alternates.length > 0) {
|
|
1992
2084
|
const setEntries: Record<string, AlternateSetMap> = {};
|
|
1993
|
-
for (const alt of parts.alternates) {
|
|
2085
|
+
for (const [altIdx, alt] of parts.alternates.entries()) {
|
|
2086
|
+
const cfgAlt = cfg.alternates?.find((a) => a.name === alt.name);
|
|
2087
|
+
const explicitDrawn = alt.drawn ?? cfgAlt?.drawn;
|
|
2088
|
+
if (explicitDrawn !== undefined) {
|
|
2089
|
+
for (const boneName of Object.keys(explicitDrawn)) {
|
|
2090
|
+
if (!B.has(boneName)) {
|
|
2091
|
+
fail('RIG_NAME_RESOLVES', `config.alternates[${altIdx}].drawn.${boneName}`, `names the bone "${boneName}", which config.bones does not declare`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
refuseIfAny(problems);
|
|
2096
|
+
const derived = alt.skeleton === undefined ? undefined : deriveAlternateDrawn(alt.skeleton, B, turn, CX, H);
|
|
2097
|
+
const drawn: Record<string, number> | undefined =
|
|
2098
|
+
derived === undefined && explicitDrawn === undefined
|
|
2099
|
+
? undefined
|
|
2100
|
+
: {
|
|
2101
|
+
...(derived ?? {}),
|
|
2102
|
+
...Object.fromEntries(Object.entries(explicitDrawn ?? {}).map(([k, v]) => [k, places(normaliseDegrees(v))])),
|
|
2103
|
+
};
|
|
2104
|
+
const posedBones = drawn === undefined ? null : poseRigBonesForDrawn(bones, drawn);
|
|
2105
|
+
const altWorld = posedBones === null ? world : computeExactFrameTransforms(posedBones);
|
|
2106
|
+
const altTurn = new Map<string, number>();
|
|
2107
|
+
if (posedBones !== null) {
|
|
2108
|
+
for (const b of posedBones) {
|
|
2109
|
+
altTurn.set(b.name, places(normaliseDegrees((altWorld.get(b.name) as BoneTransform).worldRotation)));
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
for (const [k, v] of turn) altTurn.set(k, v);
|
|
2113
|
+
}
|
|
2114
|
+
const altRegion = (image: string, bone: string, cx: number, cy: number): RegionAttachment => {
|
|
2115
|
+
const [x, y] = toBoneLocal(altWorld.get(bone) as BoneTransform, spineX(cx), spineY(cy));
|
|
2116
|
+
const deg = altTurn.get(bone) as number;
|
|
2117
|
+
return deg === 0 ? { image, x: places(x), y: places(y) } : { image, x: places(x), y: places(y), rotation: places(normaliseDegrees(-deg)) };
|
|
2118
|
+
};
|
|
1994
2119
|
for (const p of alt.records) {
|
|
1995
2120
|
const attName = `${alt.name}_${p.name}`;
|
|
1996
2121
|
const rawImg = images.get(attName);
|
|
@@ -2001,14 +2126,45 @@ export function buildRig(
|
|
|
2001
2126
|
const mesh = cfg.meshes[p.name];
|
|
2002
2127
|
if (mesh === undefined) {
|
|
2003
2128
|
const bone = B.get(cfg.regions[p.name]) as Bone;
|
|
2004
|
-
skin[p.name][attName] =
|
|
2129
|
+
skin[p.name][attName] = altRegion(file, bone.name, p.x + p.w / 2, p.y + p.h / 2);
|
|
2005
2130
|
continue;
|
|
2006
2131
|
}
|
|
2007
2132
|
const fullSegs = meshSegments.get(p.name) as Segment[];
|
|
2008
2133
|
const baseAtt = skin[p.name]?.[p.name];
|
|
2009
2134
|
const baseBones = baseAtt !== undefined && 'type' in baseAtt ? new Set(baseAtt.weights.flatMap((v) => v.map((e) => e.bone))) : null;
|
|
2010
2135
|
const filteredSegs = baseBones !== null && baseBones.size > 0 ? fullSegs.filter((s) => baseBones.has(s.bone)) : fullSegs;
|
|
2011
|
-
|
|
2136
|
+
let segs = filteredSegs.length > 0 ? filteredSegs : fullSegs;
|
|
2137
|
+
if (drawn !== undefined) {
|
|
2138
|
+
const movePt = (bone: string, pt0: Point): Point => {
|
|
2139
|
+
const m0 = world.get(bone) as BoneTransform;
|
|
2140
|
+
const m1 = altWorld.get(bone) as BoneTransform;
|
|
2141
|
+
const [lx, ly] = toBoneLocal(m0, spineX(pt0[0]), spineY(pt0[1]));
|
|
2142
|
+
const [wx, wy] = toWorld(m1, lx, ly);
|
|
2143
|
+
return [wx + CX, cropToSpineY(wy, H)];
|
|
2144
|
+
};
|
|
2145
|
+
const pt = (q: readonly [number, number]): Point => [q[0], q[1]];
|
|
2146
|
+
const skSegs: Record<string, [Point, Point]> | null =
|
|
2147
|
+
alt.skeleton === undefined
|
|
2148
|
+
? null
|
|
2149
|
+
: {
|
|
2150
|
+
sleeve_r0: [pt(alt.skeleton.r_shoulder), pt(alt.skeleton.r_elbow)],
|
|
2151
|
+
sleeve_r1: [pt(alt.skeleton.r_elbow), pt(alt.skeleton.r_wrist)],
|
|
2152
|
+
sleeve_l0: [pt(alt.skeleton.l_shoulder), pt(alt.skeleton.l_elbow)],
|
|
2153
|
+
sleeve_l1: [pt(alt.skeleton.l_elbow), pt(alt.skeleton.l_wrist)],
|
|
2154
|
+
leg_r0: [pt(alt.skeleton.r_hip), pt(alt.skeleton.r_knee)],
|
|
2155
|
+
leg_r1: [pt(alt.skeleton.r_knee), pt(alt.skeleton.r_ankle)],
|
|
2156
|
+
leg_l0: [pt(alt.skeleton.l_hip), pt(alt.skeleton.l_knee)],
|
|
2157
|
+
leg_l1: [pt(alt.skeleton.l_knee), pt(alt.skeleton.l_ankle)],
|
|
2158
|
+
};
|
|
2159
|
+
segs = segs.map((s) => {
|
|
2160
|
+
const skPair = skSegs?.[s.bone];
|
|
2161
|
+
return skPair !== undefined ? { bone: s.bone, a: skPair[0], b: skPair[1] } : { bone: s.bone, a: movePt(s.bone, s.a), b: movePt(s.bone, s.b) };
|
|
2162
|
+
});
|
|
2163
|
+
const nonSkirt = segs.filter((s) => !hipChainLinks.has(s.bone));
|
|
2164
|
+
if (nonSkirt.length > 0) segs = nonSkirt;
|
|
2165
|
+
const yOverlapping = segs.filter((s) => Math.max(s.a[1], s.b[1]) >= p.y && Math.min(s.a[1], s.b[1]) <= p.y + p.h);
|
|
2166
|
+
segs = yOverlapping.length > 0 ? yOverlapping : [segs[0]];
|
|
2167
|
+
}
|
|
2012
2168
|
const allArt = alphaAbove(img, ART_ALPHA);
|
|
2013
2169
|
const strayVal = 'auto' in mesh ? mesh.auto.source.stray : ('contour' in mesh ? mesh.contour.stray : mesh.stray);
|
|
2014
2170
|
const stray = strayVal === undefined ? null : latticeStray(allArt, strayVal);
|
|
@@ -2017,7 +2173,7 @@ export function buildRig(
|
|
|
2017
2173
|
const r = 'r' in mesh ? mesh.r : 80;
|
|
2018
2174
|
const lm = latticeMesh(attName, art, grid, maxLoopPasses);
|
|
2019
2175
|
if ('code' in lm) {
|
|
2020
|
-
skin[p.name][attName] =
|
|
2176
|
+
skin[p.name][attName] = altRegion(file, segs[0].bone, p.x + p.w / 2, p.y + p.h / 2);
|
|
2021
2177
|
continue;
|
|
2022
2178
|
}
|
|
2023
2179
|
const ox = p.x - PAD;
|
|
@@ -2027,6 +2183,22 @@ export function buildRig(
|
|
|
2027
2183
|
const limits: InfluenceLimits = meshLimits('influences' in mesh ? mesh.influences : undefined);
|
|
2028
2184
|
const segsAt = dressSegsAt(p, segs);
|
|
2029
2185
|
const exponent = mesh.exponent ?? DISTANCE_EXPONENT;
|
|
2186
|
+
let touchesArt: boolean[] | null = null;
|
|
2187
|
+
if (drawn !== undefined && limits.maxInfluences === 1) {
|
|
2188
|
+
const lc = latticeCells(art, grid);
|
|
2189
|
+
touchesArt = lm.vertices.map(([vx0, vy0]) => {
|
|
2190
|
+
const ii = Math.round(vx0 / grid);
|
|
2191
|
+
const jj = Math.round(vy0 / grid);
|
|
2192
|
+
for (const di of [-1, 0]) {
|
|
2193
|
+
for (const dj of [-1, 0]) {
|
|
2194
|
+
const ci = ii + di;
|
|
2195
|
+
const cj = jj + dj;
|
|
2196
|
+
if (ci >= 0 && ci < lc.nx && cj >= 0 && cj < lc.ny && lc.cells.data[cj * lc.nx + ci] === 1) return true;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return false;
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2030
2202
|
const rawShares: Influence[][] = [];
|
|
2031
2203
|
for (const [vx0, vy0] of lm.vertices) {
|
|
2032
2204
|
const wx = vx0 + ox;
|
|
@@ -2038,12 +2210,26 @@ export function buildRig(
|
|
|
2038
2210
|
}
|
|
2039
2211
|
rawShares.push(limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares));
|
|
2040
2212
|
}
|
|
2213
|
+
if (touchesArt !== null) {
|
|
2214
|
+
let firstArtBone: string | null = null;
|
|
2215
|
+
for (let vi = 0; vi < lm.vertices.length; vi++) {
|
|
2216
|
+
if (touchesArt[vi] && rawShares[vi].length > 0) {
|
|
2217
|
+
firstArtBone = rawShares[vi][0].bone;
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (firstArtBone !== null) {
|
|
2222
|
+
for (let vi = 0; vi < lm.vertices.length; vi++) {
|
|
2223
|
+
if (!touchesArt[vi]) rawShares[vi] = [{ bone: firstArtBone, weight: 1 }];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2041
2227
|
const weights: WeightEntry[][] = [];
|
|
2042
2228
|
for (const [vi, [vx0, vy0]] of lm.vertices.entries()) {
|
|
2043
2229
|
const wx = vx0 + ox;
|
|
2044
2230
|
const wy = vy0 + oy;
|
|
2045
2231
|
const ent: WeightEntry[] = rawShares[vi].map(({ bone, weight }) => {
|
|
2046
|
-
const [x, y] = toBoneLocal(
|
|
2232
|
+
const [x, y] = toBoneLocal(altWorld.get(bone) as BoneTransform, spineX(wx), spineY(wy));
|
|
2047
2233
|
return { bone, x: places(x), y: places(y), weight };
|
|
2048
2234
|
});
|
|
2049
2235
|
weights.push(ent);
|
|
@@ -2062,13 +2248,22 @@ export function buildRig(
|
|
|
2062
2248
|
for (const slotName of Object.keys(alt.slots)) {
|
|
2063
2249
|
if (stills[slotName] !== undefined && !hideList.includes(`${slotName}${STILL_SUFFIX}`)) hideList.push(`${slotName}${STILL_SUFFIX}`);
|
|
2064
2250
|
}
|
|
2251
|
+
const coversFace = Object.hasOwn(slotMap, 'face');
|
|
2065
2252
|
setEntries[alt.name] = {
|
|
2066
2253
|
slots: slotMap,
|
|
2067
2254
|
...(hideList.length > 0 ? { hides: hideList } : {}),
|
|
2255
|
+
...(drawn === undefined ? {} : { drawn }),
|
|
2256
|
+
...(coversFace && cfg.turn !== undefined ? { threshold: { ...FACE_RATED_BAND } } : {}),
|
|
2068
2257
|
};
|
|
2069
2258
|
}
|
|
2070
|
-
|
|
2259
|
+
const anyCoversFace = Object.values(setEntries).some((e) => Object.hasOwn(e.slots, 'face'));
|
|
2260
|
+
sets = {
|
|
2261
|
+
spec: SETS_SPEC,
|
|
2262
|
+
...(anyCoversFace ? { threshold: { ...FACE_RATED_BAND } } : {}),
|
|
2263
|
+
sets: setEntries,
|
|
2264
|
+
};
|
|
2071
2265
|
}
|
|
2266
|
+
const hasAlternateFaceSet = sets !== undefined && Object.values(sets.sets).some((e) => Object.hasOwn(e.slots, 'face'));
|
|
2072
2267
|
// ---- leg stops (issue #404) --------------------------------------------
|
|
2073
2268
|
// Only when the config has `stops`: each link after the first (and any bone hanging from a stopped link) hangs
|
|
2074
2269
|
// from the drawn link `<link>_d` above it, and slots and weights on a stopped link bind `<link>_d`.
|
|
@@ -2108,7 +2303,15 @@ export function buildRig(
|
|
|
2108
2303
|
opaqueWidth: facePart?.w,
|
|
2109
2304
|
opaqueHeight: facePart?.h,
|
|
2110
2305
|
overrides: cfg.turn?.deform,
|
|
2306
|
+
hasAlternateFaceSet,
|
|
2111
2307
|
});
|
|
2308
|
+
if (sets !== undefined && (merged.animations.turn !== undefined || merged.animations.tilt !== undefined)) {
|
|
2309
|
+
for (const entry of Object.values(sets.sets)) {
|
|
2310
|
+
if (Object.hasOwn(entry.slots, 'face')) {
|
|
2311
|
+
entry.threshold = { ...FACE_RATED_BAND };
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2112
2315
|
});
|
|
2113
2316
|
return {
|
|
2114
2317
|
rig,
|