@rig-c/parts 1.30.0 → 1.32.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 +49 -11
- package/docs/AUTHORING.md +9 -5
- package/docs/STABILITY.md +5 -4
- package/package.json +1 -1
- package/src/build.ts +301 -8
- package/src/check.ts +229 -10
- package/src/cloth.ts +60 -8
- package/src/config.ts +103 -16
- package/src/parts.ts +59 -12
- package/src/propose.ts +52 -27
- package/src/rig.ts +305 -3
package/src/rig.ts
CHANGED
|
@@ -164,13 +164,14 @@ 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
|
-
import { PAINTING_RUN, type PartsFile, readFrom, tagOf } from './parts.ts';
|
|
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 type { PosePoints } from './skeleton.ts';
|
|
174
175
|
import { applyFaceSurfaceDeform, 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
|
|
|
@@ -896,6 +897,20 @@ export interface AutoMeshReport {
|
|
|
896
897
|
motion_report?: unknown;
|
|
897
898
|
}
|
|
898
899
|
|
|
900
|
+
export const SETS_SPEC = 'rig-parts-sets/1';
|
|
901
|
+
export const SETS_FILE = 'sets.json';
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* `sets.json` (issue #454): the machine-readable map from each alternate
|
|
905
|
+
* painting set to its `<set>_<part>` attachment on each slot it covers and
|
|
906
|
+
* the slots it hides. Written beside `rig.json` only when `parts.json` carries
|
|
907
|
+
* `alternates`.
|
|
908
|
+
*/
|
|
909
|
+
export interface AttachmentSetsFile {
|
|
910
|
+
spec: typeof SETS_SPEC;
|
|
911
|
+
sets: Record<string, AlternateSetMap>;
|
|
912
|
+
}
|
|
913
|
+
|
|
899
914
|
export interface RigOutput {
|
|
900
915
|
rig: RigSpec;
|
|
901
916
|
motion: MotionSpec;
|
|
@@ -915,6 +930,95 @@ export interface RigOutput {
|
|
|
915
930
|
faceAxis: FaceAxis;
|
|
916
931
|
/** The turn control's parts (issue #251), in parts.json order; null when the config has no `turn`. */
|
|
917
932
|
turn: TurnRow[] | null;
|
|
933
|
+
/** `sets.json` (issue #454), present only when `parts.alternates` is non-empty. */
|
|
934
|
+
sets?: AttachmentSetsFile;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Issue #465: pose a rig's bones so each bone named in `drawn` reaches that
|
|
939
|
+
* world rotation (in Spine degrees, CCW, y up), adjusting its local `rotation`
|
|
940
|
+
* from its parent's posed transform. Bones are walked in parent-first order.
|
|
941
|
+
*/
|
|
942
|
+
export function poseRigBonesForDrawn(
|
|
943
|
+
bones: readonly RigBone[],
|
|
944
|
+
drawn: Readonly<Record<string, number>>,
|
|
945
|
+
stops?: ReadonlyArray<{ links: readonly string[] }>,
|
|
946
|
+
): RigBone[] {
|
|
947
|
+
const undrawn = new Map<string, string>();
|
|
948
|
+
for (const s of stops ?? []) for (const l of s.links) undrawn.set(`${l}_d`, l);
|
|
949
|
+
const realBone = (n: string): string => undrawn.get(n) ?? n;
|
|
950
|
+
const posed: RigBone[] = [];
|
|
951
|
+
const posedRead: RigBone[] = [];
|
|
952
|
+
for (const b of bones) {
|
|
953
|
+
const readB: RigBone = { ...b, ...(b.parent === undefined ? {} : { parent: realBone(b.parent) }) };
|
|
954
|
+
const target = drawn[b.name];
|
|
955
|
+
if (target === undefined) {
|
|
956
|
+
posed.push({ ...b });
|
|
957
|
+
posedRead.push(readB);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const m0 = computeExactFrameTransforms([...posedRead, { ...readB, rotation: 0 }]).get(b.name) as BoneTransform;
|
|
961
|
+
const rotation = places(normaliseDegrees(target - normaliseDegrees(m0.worldRotation)));
|
|
962
|
+
posed.push({ ...b, rotation });
|
|
963
|
+
posedRead.push({ ...readB, rotation });
|
|
964
|
+
}
|
|
965
|
+
return posed;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Issue #465: derive the alternate's `drawn` bone world rotations (in Spine
|
|
970
|
+
* degrees, CCW, y up) from its body-18 skeleton read (`pts`, in rig pixels, y
|
|
971
|
+
* down) against the base rig's setup bone origins (`B`) and setup world turns
|
|
972
|
+
* (`turn`).
|
|
973
|
+
*/
|
|
974
|
+
export function deriveAlternateDrawn(
|
|
975
|
+
pts: PosePoints,
|
|
976
|
+
B: ReadonlyMap<string, { x: number; y: number }>,
|
|
977
|
+
turn: ReadonlyMap<string, number>,
|
|
978
|
+
CX: number,
|
|
979
|
+
H: number,
|
|
980
|
+
): Record<string, number> {
|
|
981
|
+
const sx = (x: number): number => x - CX;
|
|
982
|
+
const sy = (y: number): number => cropToSpineY(y, H);
|
|
983
|
+
const deg = (a: readonly [number, number], b: readonly [number, number]): number =>
|
|
984
|
+
(Math.atan2(sy(b[1]) - sy(a[1]), sx(b[0]) - sx(a[0])) * 180) / Math.PI;
|
|
985
|
+
const mid = (a: readonly [number, number], b: readonly [number, number]): [number, number] => [
|
|
986
|
+
(a[0] + b[0]) / 2,
|
|
987
|
+
(a[1] + b[1]) / 2,
|
|
988
|
+
];
|
|
989
|
+
const out: Record<string, number> = {};
|
|
990
|
+
if (B.has('chest') && B.has('hip') && B.has('sleeve_r0') && B.has('sleeve_l0')) {
|
|
991
|
+
const hip = B.get('hip') as { x: number; y: number };
|
|
992
|
+
const sr0 = B.get('sleeve_r0') as { x: number; y: number };
|
|
993
|
+
const sl0 = B.get('sleeve_l0') as { x: number; y: number };
|
|
994
|
+
const setupTrunkAim = deg([hip.x, hip.y], mid([sr0.x, sr0.y], [sl0.x, sl0.y]));
|
|
995
|
+
const altTrunkAim = deg(mid(pts.r_hip, pts.l_hip), mid(pts.r_shoulder, pts.l_shoulder));
|
|
996
|
+
out.chest = places(normaliseDegrees((turn.get('chest') ?? 0) + (altTrunkAim - setupTrunkAim)));
|
|
997
|
+
}
|
|
998
|
+
if (B.has('head') && B.has('eye_r') && B.has('eye_l')) {
|
|
999
|
+
const er = B.get('eye_r') as { x: number; y: number };
|
|
1000
|
+
const el = B.get('eye_l') as { x: number; y: number };
|
|
1001
|
+
const setupEyeAim = deg([er.x, er.y], [el.x, el.y]);
|
|
1002
|
+
const altEyeAim = deg(pts.r_eye, pts.l_eye);
|
|
1003
|
+
out.head = places(normaliseDegrees((turn.get('head') ?? 0) + (altEyeAim - setupEyeAim)));
|
|
1004
|
+
}
|
|
1005
|
+
const limbPairs: ReadonlyArray<readonly [string, readonly [number, number], readonly [number, number]]> = [
|
|
1006
|
+
['sleeve_r0', pts.r_shoulder, pts.r_elbow],
|
|
1007
|
+
['sleeve_r1', pts.r_elbow, pts.r_wrist],
|
|
1008
|
+
['sleeve_l0', pts.l_shoulder, pts.l_elbow],
|
|
1009
|
+
['sleeve_l1', pts.l_elbow, pts.l_wrist],
|
|
1010
|
+
['leg_r0', pts.r_hip, pts.r_knee],
|
|
1011
|
+
['leg_r1', pts.r_knee, pts.r_ankle],
|
|
1012
|
+
['leg_l0', pts.l_hip, pts.l_knee],
|
|
1013
|
+
['leg_l1', pts.l_knee, pts.l_ankle],
|
|
1014
|
+
];
|
|
1015
|
+
for (const [bone, a, b] of limbPairs) {
|
|
1016
|
+
if (B.has(bone)) out[bone] = places(normaliseDegrees(deg(a, b)));
|
|
1017
|
+
}
|
|
1018
|
+
for (const foot of ['foot_r', 'foot_l'] as const) {
|
|
1019
|
+
if (B.has(foot)) out[foot] = places(normaliseDegrees(turn.get(foot) ?? 0));
|
|
1020
|
+
}
|
|
1021
|
+
return out;
|
|
918
1022
|
}
|
|
919
1023
|
|
|
920
1024
|
/**
|
|
@@ -1966,6 +2070,191 @@ export function buildRig(
|
|
|
1966
2070
|
});
|
|
1967
2071
|
}
|
|
1968
2072
|
refuseIfAny(problems);
|
|
2073
|
+
// ---- alternate painting sets (issues #454, #465) ------------------------
|
|
2074
|
+
// Only when `parts.alternates` is present: each alternate set's `<set>_<part>`
|
|
2075
|
+
// attachments are added to `skins.default[<part>]` on the existing bones and
|
|
2076
|
+
// slots before `applyStops` and `applyTurn`, so stopped links and turn twins
|
|
2077
|
+
// carry the alternate attachments alongside the base ones. When the alternate
|
|
2078
|
+
// carries a skeleton read (`alt.skeleton`) or explicit `drawn` angles, its
|
|
2079
|
+
// attachments are bound in the posed bone frames (`altWorld`) and `sets.json`
|
|
2080
|
+
// records `drawn` so a retarget does not pose the art a second time.
|
|
2081
|
+
let sets: AttachmentSetsFile | undefined;
|
|
2082
|
+
if (parts.alternates !== undefined && parts.alternates.length > 0) {
|
|
2083
|
+
const setEntries: Record<string, AlternateSetMap> = {};
|
|
2084
|
+
for (const [altIdx, alt] of parts.alternates.entries()) {
|
|
2085
|
+
const cfgAlt = cfg.alternates?.find((a) => a.name === alt.name);
|
|
2086
|
+
const explicitDrawn = alt.drawn ?? cfgAlt?.drawn;
|
|
2087
|
+
if (explicitDrawn !== undefined) {
|
|
2088
|
+
for (const boneName of Object.keys(explicitDrawn)) {
|
|
2089
|
+
if (!B.has(boneName)) {
|
|
2090
|
+
fail('RIG_NAME_RESOLVES', `config.alternates[${altIdx}].drawn.${boneName}`, `names the bone "${boneName}", which config.bones does not declare`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
refuseIfAny(problems);
|
|
2095
|
+
const derived = alt.skeleton === undefined ? undefined : deriveAlternateDrawn(alt.skeleton, B, turn, CX, H);
|
|
2096
|
+
const drawn: Record<string, number> | undefined =
|
|
2097
|
+
derived === undefined && explicitDrawn === undefined
|
|
2098
|
+
? undefined
|
|
2099
|
+
: {
|
|
2100
|
+
...(derived ?? {}),
|
|
2101
|
+
...Object.fromEntries(Object.entries(explicitDrawn ?? {}).map(([k, v]) => [k, places(normaliseDegrees(v))])),
|
|
2102
|
+
};
|
|
2103
|
+
const posedBones = drawn === undefined ? null : poseRigBonesForDrawn(bones, drawn);
|
|
2104
|
+
const altWorld = posedBones === null ? world : computeExactFrameTransforms(posedBones);
|
|
2105
|
+
const altTurn = new Map<string, number>();
|
|
2106
|
+
if (posedBones !== null) {
|
|
2107
|
+
for (const b of posedBones) {
|
|
2108
|
+
altTurn.set(b.name, places(normaliseDegrees((altWorld.get(b.name) as BoneTransform).worldRotation)));
|
|
2109
|
+
}
|
|
2110
|
+
} else {
|
|
2111
|
+
for (const [k, v] of turn) altTurn.set(k, v);
|
|
2112
|
+
}
|
|
2113
|
+
const altRegion = (image: string, bone: string, cx: number, cy: number): RegionAttachment => {
|
|
2114
|
+
const [x, y] = toBoneLocal(altWorld.get(bone) as BoneTransform, spineX(cx), spineY(cy));
|
|
2115
|
+
const deg = altTurn.get(bone) as number;
|
|
2116
|
+
return deg === 0 ? { image, x: places(x), y: places(y) } : { image, x: places(x), y: places(y), rotation: places(normaliseDegrees(-deg)) };
|
|
2117
|
+
};
|
|
2118
|
+
for (const p of alt.records) {
|
|
2119
|
+
const attName = `${alt.name}_${p.name}`;
|
|
2120
|
+
const rawImg = images.get(attName);
|
|
2121
|
+
if (rawImg === undefined) continue;
|
|
2122
|
+
const img = pad(rawImg, PAD, PAD, PAD, PAD, [0, 0, 0, 0]);
|
|
2123
|
+
const file = `${attName}.png`;
|
|
2124
|
+
outImages.push([file, img]);
|
|
2125
|
+
const mesh = cfg.meshes[p.name];
|
|
2126
|
+
if (mesh === undefined) {
|
|
2127
|
+
const bone = B.get(cfg.regions[p.name]) as Bone;
|
|
2128
|
+
skin[p.name][attName] = altRegion(file, bone.name, p.x + p.w / 2, p.y + p.h / 2);
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const fullSegs = meshSegments.get(p.name) as Segment[];
|
|
2132
|
+
const baseAtt = skin[p.name]?.[p.name];
|
|
2133
|
+
const baseBones = baseAtt !== undefined && 'type' in baseAtt ? new Set(baseAtt.weights.flatMap((v) => v.map((e) => e.bone))) : null;
|
|
2134
|
+
const filteredSegs = baseBones !== null && baseBones.size > 0 ? fullSegs.filter((s) => baseBones.has(s.bone)) : fullSegs;
|
|
2135
|
+
let segs = filteredSegs.length > 0 ? filteredSegs : fullSegs;
|
|
2136
|
+
if (drawn !== undefined) {
|
|
2137
|
+
const movePt = (bone: string, pt0: Point): Point => {
|
|
2138
|
+
const m0 = world.get(bone) as BoneTransform;
|
|
2139
|
+
const m1 = altWorld.get(bone) as BoneTransform;
|
|
2140
|
+
const [lx, ly] = toBoneLocal(m0, spineX(pt0[0]), spineY(pt0[1]));
|
|
2141
|
+
const [wx, wy] = toWorld(m1, lx, ly);
|
|
2142
|
+
return [wx + CX, cropToSpineY(wy, H)];
|
|
2143
|
+
};
|
|
2144
|
+
const pt = (q: readonly [number, number]): Point => [q[0], q[1]];
|
|
2145
|
+
const skSegs: Record<string, [Point, Point]> | null =
|
|
2146
|
+
alt.skeleton === undefined
|
|
2147
|
+
? null
|
|
2148
|
+
: {
|
|
2149
|
+
sleeve_r0: [pt(alt.skeleton.r_shoulder), pt(alt.skeleton.r_elbow)],
|
|
2150
|
+
sleeve_r1: [pt(alt.skeleton.r_elbow), pt(alt.skeleton.r_wrist)],
|
|
2151
|
+
sleeve_l0: [pt(alt.skeleton.l_shoulder), pt(alt.skeleton.l_elbow)],
|
|
2152
|
+
sleeve_l1: [pt(alt.skeleton.l_elbow), pt(alt.skeleton.l_wrist)],
|
|
2153
|
+
leg_r0: [pt(alt.skeleton.r_hip), pt(alt.skeleton.r_knee)],
|
|
2154
|
+
leg_r1: [pt(alt.skeleton.r_knee), pt(alt.skeleton.r_ankle)],
|
|
2155
|
+
leg_l0: [pt(alt.skeleton.l_hip), pt(alt.skeleton.l_knee)],
|
|
2156
|
+
leg_l1: [pt(alt.skeleton.l_knee), pt(alt.skeleton.l_ankle)],
|
|
2157
|
+
};
|
|
2158
|
+
segs = segs.map((s) => {
|
|
2159
|
+
const skPair = skSegs?.[s.bone];
|
|
2160
|
+
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) };
|
|
2161
|
+
});
|
|
2162
|
+
const nonSkirt = segs.filter((s) => !hipChainLinks.has(s.bone));
|
|
2163
|
+
if (nonSkirt.length > 0) segs = nonSkirt;
|
|
2164
|
+
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);
|
|
2165
|
+
segs = yOverlapping.length > 0 ? yOverlapping : [segs[0]];
|
|
2166
|
+
}
|
|
2167
|
+
const allArt = alphaAbove(img, ART_ALPHA);
|
|
2168
|
+
const strayVal = 'auto' in mesh ? mesh.auto.source.stray : ('contour' in mesh ? mesh.contour.stray : mesh.stray);
|
|
2169
|
+
const stray = strayVal === undefined ? null : latticeStray(allArt, strayVal);
|
|
2170
|
+
const art = stray === null ? allArt : stray.art;
|
|
2171
|
+
const grid = 'grid' in mesh ? mesh.grid : 28;
|
|
2172
|
+
const r = 'r' in mesh ? mesh.r : 80;
|
|
2173
|
+
const lm = latticeMesh(attName, art, grid, maxLoopPasses);
|
|
2174
|
+
if ('code' in lm) {
|
|
2175
|
+
skin[p.name][attName] = altRegion(file, segs[0].bone, p.x + p.w / 2, p.y + p.h / 2);
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
const ox = p.x - PAD;
|
|
2179
|
+
const oy = p.y - PAD;
|
|
2180
|
+
const w = img.width;
|
|
2181
|
+
const h = img.height;
|
|
2182
|
+
const limits: InfluenceLimits = meshLimits('influences' in mesh ? mesh.influences : undefined);
|
|
2183
|
+
const segsAt = dressSegsAt(p, segs);
|
|
2184
|
+
const exponent = mesh.exponent ?? DISTANCE_EXPONENT;
|
|
2185
|
+
let touchesArt: boolean[] | null = null;
|
|
2186
|
+
if (drawn !== undefined && limits.maxInfluences === 1) {
|
|
2187
|
+
const lc = latticeCells(art, grid);
|
|
2188
|
+
touchesArt = lm.vertices.map(([vx0, vy0]) => {
|
|
2189
|
+
const ii = Math.round(vx0 / grid);
|
|
2190
|
+
const jj = Math.round(vy0 / grid);
|
|
2191
|
+
for (const di of [-1, 0]) {
|
|
2192
|
+
for (const dj of [-1, 0]) {
|
|
2193
|
+
const ci = ii + di;
|
|
2194
|
+
const cj = jj + dj;
|
|
2195
|
+
if (ci >= 0 && ci < lc.nx && cj >= 0 && cj < lc.ny && lc.cells.data[cj * lc.nx + ci] === 1) return true;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return false;
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
const rawShares: Influence[][] = [];
|
|
2202
|
+
for (const [vx0, vy0] of lm.vertices) {
|
|
2203
|
+
const wx = vx0 + ox;
|
|
2204
|
+
const wy = vy0 + oy;
|
|
2205
|
+
const vSegs = segsAt(wy);
|
|
2206
|
+
let shares = influences([wx, wy], vSegs, r, limits, exponent);
|
|
2207
|
+
if (shares.length === 0) {
|
|
2208
|
+
shares = influences([wx, wy], vSegs, Infinity, { maxInfluences: 1, minWeight: 0 }, exponent);
|
|
2209
|
+
}
|
|
2210
|
+
rawShares.push(limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares));
|
|
2211
|
+
}
|
|
2212
|
+
if (touchesArt !== null) {
|
|
2213
|
+
let firstArtBone: string | null = null;
|
|
2214
|
+
for (let vi = 0; vi < lm.vertices.length; vi++) {
|
|
2215
|
+
if (touchesArt[vi] && rawShares[vi].length > 0) {
|
|
2216
|
+
firstArtBone = rawShares[vi][0].bone;
|
|
2217
|
+
break;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (firstArtBone !== null) {
|
|
2221
|
+
for (let vi = 0; vi < lm.vertices.length; vi++) {
|
|
2222
|
+
if (!touchesArt[vi]) rawShares[vi] = [{ bone: firstArtBone, weight: 1 }];
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const weights: WeightEntry[][] = [];
|
|
2227
|
+
for (const [vi, [vx0, vy0]] of lm.vertices.entries()) {
|
|
2228
|
+
const wx = vx0 + ox;
|
|
2229
|
+
const wy = vy0 + oy;
|
|
2230
|
+
const ent: WeightEntry[] = rawShares[vi].map(({ bone, weight }) => {
|
|
2231
|
+
const [x, y] = toBoneLocal(altWorld.get(bone) as BoneTransform, spineX(wx), spineY(wy));
|
|
2232
|
+
return { bone, x: places(x), y: places(y), weight };
|
|
2233
|
+
});
|
|
2234
|
+
weights.push(ent);
|
|
2235
|
+
}
|
|
2236
|
+
const uvs: number[] = [];
|
|
2237
|
+
for (const [x, y] of lm.vertices) uvs.push(pyRound(x / w, 6), pyRound(y / h, 6));
|
|
2238
|
+
skin[p.name][attName] = { type: 'mesh', image: file, width: w, height: h, uvs, triangles: lm.triangles, hull: lm.hull, weights };
|
|
2239
|
+
}
|
|
2240
|
+
const slotMap: Record<string, string> = {};
|
|
2241
|
+
for (const [slotName, att] of Object.entries(alt.slots)) slotMap[slotName] = att;
|
|
2242
|
+
const hideList: string[] = [];
|
|
2243
|
+
for (const h of alt.hides ?? []) {
|
|
2244
|
+
if (!hideList.includes(h)) hideList.push(h);
|
|
2245
|
+
if (stills[h] !== undefined && !hideList.includes(`${h}${STILL_SUFFIX}`)) hideList.push(`${h}${STILL_SUFFIX}`);
|
|
2246
|
+
}
|
|
2247
|
+
for (const slotName of Object.keys(alt.slots)) {
|
|
2248
|
+
if (stills[slotName] !== undefined && !hideList.includes(`${slotName}${STILL_SUFFIX}`)) hideList.push(`${slotName}${STILL_SUFFIX}`);
|
|
2249
|
+
}
|
|
2250
|
+
setEntries[alt.name] = {
|
|
2251
|
+
slots: slotMap,
|
|
2252
|
+
...(hideList.length > 0 ? { hides: hideList } : {}),
|
|
2253
|
+
...(drawn === undefined ? {} : { drawn }),
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
sets = { spec: SETS_SPEC, sets: setEntries };
|
|
2257
|
+
}
|
|
1969
2258
|
// ---- leg stops (issue #404) --------------------------------------------
|
|
1970
2259
|
// Only when the config has `stops`: each link after the first (and any bone hanging from a stopped link) hangs
|
|
1971
2260
|
// from the drawn link `<link>_d` above it, and slots and weights on a stopped link bind `<link>_d`.
|
|
@@ -2007,7 +2296,20 @@ export function buildRig(
|
|
|
2007
2296
|
overrides: cfg.turn?.deform,
|
|
2008
2297
|
});
|
|
2009
2298
|
});
|
|
2010
|
-
return {
|
|
2299
|
+
return {
|
|
2300
|
+
rig,
|
|
2301
|
+
motion,
|
|
2302
|
+
meshReport,
|
|
2303
|
+
images: outImages,
|
|
2304
|
+
controls,
|
|
2305
|
+
meshKeyed,
|
|
2306
|
+
idleKeys,
|
|
2307
|
+
loopPasses,
|
|
2308
|
+
autoMotion,
|
|
2309
|
+
faceAxis,
|
|
2310
|
+
turn: turned?.rows ?? null,
|
|
2311
|
+
...(sets === undefined ? {} : { sets }),
|
|
2312
|
+
};
|
|
2011
2313
|
}
|
|
2012
2314
|
|
|
2013
2315
|
/**
|