@rig-c/parts 1.30.0 → 1.31.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/src/rig.ts CHANGED
@@ -166,7 +166,7 @@ import { type ContourChainRibs, type ContourReport, contourMesh, type ContourReg
166
166
  import { type LocalInfluence, localInfluences } from './localweights.ts';
167
167
  import { artCoverage, ART_ALPHA, 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';
@@ -896,6 +896,20 @@ export interface AutoMeshReport {
896
896
  motion_report?: unknown;
897
897
  }
898
898
 
899
+ export const SETS_SPEC = 'rig-parts-sets/1';
900
+ export const SETS_FILE = 'sets.json';
901
+
902
+ /**
903
+ * `sets.json` (issue #454): the machine-readable map from each alternate
904
+ * painting set to its `<set>_<part>` attachment on each slot it covers and
905
+ * the slots it hides. Written beside `rig.json` only when `parts.json` carries
906
+ * `alternates`.
907
+ */
908
+ export interface AttachmentSetsFile {
909
+ spec: typeof SETS_SPEC;
910
+ sets: Record<string, AlternateSetMap>;
911
+ }
912
+
899
913
  export interface RigOutput {
900
914
  rig: RigSpec;
901
915
  motion: MotionSpec;
@@ -915,6 +929,8 @@ export interface RigOutput {
915
929
  faceAxis: FaceAxis;
916
930
  /** The turn control's parts (issue #251), in parts.json order; null when the config has no `turn`. */
917
931
  turn: TurnRow[] | null;
932
+ /** `sets.json` (issue #454), present only when `parts.alternates` is non-empty. */
933
+ sets?: AttachmentSetsFile;
918
934
  }
919
935
 
920
936
  /**
@@ -1966,6 +1982,93 @@ export function buildRig(
1966
1982
  });
1967
1983
  }
1968
1984
  refuseIfAny(problems);
1985
+ // ---- alternate painting sets (issue #454) -------------------------------
1986
+ // Only when `parts.alternates` is present: each alternate set's `<set>_<part>`
1987
+ // attachments are added to `skins.default[<part>]` on the existing bones and
1988
+ // slots before `applyStops` and `applyTurn`, so stopped links and turn twins
1989
+ // carry the alternate attachments alongside the base ones.
1990
+ let sets: AttachmentSetsFile | undefined;
1991
+ if (parts.alternates !== undefined && parts.alternates.length > 0) {
1992
+ const setEntries: Record<string, AlternateSetMap> = {};
1993
+ for (const alt of parts.alternates) {
1994
+ for (const p of alt.records) {
1995
+ const attName = `${alt.name}_${p.name}`;
1996
+ const rawImg = images.get(attName);
1997
+ if (rawImg === undefined) continue;
1998
+ const img = pad(rawImg, PAD, PAD, PAD, PAD, [0, 0, 0, 0]);
1999
+ const file = `${attName}.png`;
2000
+ outImages.push([file, img]);
2001
+ const mesh = cfg.meshes[p.name];
2002
+ if (mesh === undefined) {
2003
+ const bone = B.get(cfg.regions[p.name]) as Bone;
2004
+ skin[p.name][attName] = region(file, bone.name, p.x + p.w / 2, p.y + p.h / 2);
2005
+ continue;
2006
+ }
2007
+ const fullSegs = meshSegments.get(p.name) as Segment[];
2008
+ const baseAtt = skin[p.name]?.[p.name];
2009
+ const baseBones = baseAtt !== undefined && 'type' in baseAtt ? new Set(baseAtt.weights.flatMap((v) => v.map((e) => e.bone))) : null;
2010
+ const filteredSegs = baseBones !== null && baseBones.size > 0 ? fullSegs.filter((s) => baseBones.has(s.bone)) : fullSegs;
2011
+ const segs = filteredSegs.length > 0 ? filteredSegs : fullSegs;
2012
+ const allArt = alphaAbove(img, ART_ALPHA);
2013
+ const strayVal = 'auto' in mesh ? mesh.auto.source.stray : ('contour' in mesh ? mesh.contour.stray : mesh.stray);
2014
+ const stray = strayVal === undefined ? null : latticeStray(allArt, strayVal);
2015
+ const art = stray === null ? allArt : stray.art;
2016
+ const grid = 'grid' in mesh ? mesh.grid : 28;
2017
+ const r = 'r' in mesh ? mesh.r : 80;
2018
+ const lm = latticeMesh(attName, art, grid, maxLoopPasses);
2019
+ if ('code' in lm) {
2020
+ skin[p.name][attName] = region(file, segs[0].bone, p.x + p.w / 2, p.y + p.h / 2);
2021
+ continue;
2022
+ }
2023
+ const ox = p.x - PAD;
2024
+ const oy = p.y - PAD;
2025
+ const w = img.width;
2026
+ const h = img.height;
2027
+ const limits: InfluenceLimits = meshLimits('influences' in mesh ? mesh.influences : undefined);
2028
+ const segsAt = dressSegsAt(p, segs);
2029
+ const exponent = mesh.exponent ?? DISTANCE_EXPONENT;
2030
+ const rawShares: Influence[][] = [];
2031
+ for (const [vx0, vy0] of lm.vertices) {
2032
+ const wx = vx0 + ox;
2033
+ const wy = vy0 + oy;
2034
+ const vSegs = segsAt(wy);
2035
+ let shares = influences([wx, wy], vSegs, r, limits, exponent);
2036
+ if (shares.length === 0) {
2037
+ shares = influences([wx, wy], vSegs, Infinity, { maxInfluences: 1, minWeight: 0 }, exponent);
2038
+ }
2039
+ rawShares.push(limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares));
2040
+ }
2041
+ const weights: WeightEntry[][] = [];
2042
+ for (const [vi, [vx0, vy0]] of lm.vertices.entries()) {
2043
+ const wx = vx0 + ox;
2044
+ const wy = vy0 + oy;
2045
+ const ent: WeightEntry[] = rawShares[vi].map(({ bone, weight }) => {
2046
+ const [x, y] = toBoneLocal(world.get(bone) as BoneTransform, spineX(wx), spineY(wy));
2047
+ return { bone, x: places(x), y: places(y), weight };
2048
+ });
2049
+ weights.push(ent);
2050
+ }
2051
+ const uvs: number[] = [];
2052
+ for (const [x, y] of lm.vertices) uvs.push(pyRound(x / w, 6), pyRound(y / h, 6));
2053
+ skin[p.name][attName] = { type: 'mesh', image: file, width: w, height: h, uvs, triangles: lm.triangles, hull: lm.hull, weights };
2054
+ }
2055
+ const slotMap: Record<string, string> = {};
2056
+ for (const [slotName, att] of Object.entries(alt.slots)) slotMap[slotName] = att;
2057
+ const hideList: string[] = [];
2058
+ for (const h of alt.hides ?? []) {
2059
+ if (!hideList.includes(h)) hideList.push(h);
2060
+ if (stills[h] !== undefined && !hideList.includes(`${h}${STILL_SUFFIX}`)) hideList.push(`${h}${STILL_SUFFIX}`);
2061
+ }
2062
+ for (const slotName of Object.keys(alt.slots)) {
2063
+ if (stills[slotName] !== undefined && !hideList.includes(`${slotName}${STILL_SUFFIX}`)) hideList.push(`${slotName}${STILL_SUFFIX}`);
2064
+ }
2065
+ setEntries[alt.name] = {
2066
+ slots: slotMap,
2067
+ ...(hideList.length > 0 ? { hides: hideList } : {}),
2068
+ };
2069
+ }
2070
+ sets = { spec: SETS_SPEC, sets: setEntries };
2071
+ }
1969
2072
  // ---- leg stops (issue #404) --------------------------------------------
1970
2073
  // Only when the config has `stops`: each link after the first (and any bone hanging from a stopped link) hangs
1971
2074
  // from the drawn link `<link>_d` above it, and slots and weights on a stopped link bind `<link>_d`.
@@ -2007,7 +2110,20 @@ export function buildRig(
2007
2110
  overrides: cfg.turn?.deform,
2008
2111
  });
2009
2112
  });
2010
- return { rig, motion, meshReport, images: outImages, controls, meshKeyed, idleKeys, loopPasses, autoMotion, faceAxis, turn: turned?.rows ?? null };
2113
+ return {
2114
+ rig,
2115
+ motion,
2116
+ meshReport,
2117
+ images: outImages,
2118
+ controls,
2119
+ meshKeyed,
2120
+ idleKeys,
2121
+ loopPasses,
2122
+ autoMotion,
2123
+ faceAxis,
2124
+ turn: turned?.rows ?? null,
2125
+ ...(sets === undefined ? {} : { sets }),
2126
+ };
2011
2127
  }
2012
2128
 
2013
2129
  /**