spine-rigc 2.54.0 → 2.55.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.
@@ -32,6 +32,7 @@ import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
32
32
  import { parseMotionSpec } from '../motion.ts';
33
33
  import { type InspectCompare, type InspectFrame, InspectError, inspectCandidate, INSPECT_DEFAULT_FPS, legendText, readBonePatterns, readShow, readTimes, readZoom } from '../inspect.ts';
34
34
  import { FOLDS_FILE, foldLine, foldsFileOf, foldsOf, foldsText, setupFaultLine } from '../folds.ts';
35
+ import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
35
36
  import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
36
37
  import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
37
38
  import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser } from '../render_shared.ts';
@@ -539,6 +540,22 @@ function environmentRefusal(given: string, steps: readonly EnvironmentStep[], sk
539
540
  return null;
540
541
  }
541
542
 
543
+ /**
544
+ * `--pass-through <cloth>:<leg>[,…]` read against the skeleton's slots (issue
545
+ * #1413, `../passthrough.ts`), or refused as a usage error before anything is
546
+ * posed: a malformed list, an empty name, a slot the skeleton does not have, a
547
+ * pair named twice — each by its entry. `undefined` without the flag.
548
+ */
549
+ function readPassThroughFlag(raw: string | undefined, facts: SkeletonFacts): PassPair[] | undefined {
550
+ if (raw === undefined) return undefined;
551
+ try {
552
+ return readPassPairs(raw, facts.slots.map((slot) => slot.name));
553
+ } catch (err) {
554
+ if (err instanceof PassThroughError) throw new UsageError(err.message);
555
+ throw err;
556
+ }
557
+ }
558
+
542
559
  /**
543
560
  * render — the frame series, drawn by the same rasteriser `check` measures with.
544
561
  *
@@ -580,6 +597,8 @@ export function cmdRender(flags: Record<string, string>): void {
580
597
  }
581
598
  }
582
599
  }
600
+ // The pass-through reading (issue #1413) is its own flag for --folds' reason: without it, every file is the bytes it was.
601
+ const passThrough = flags['pass-through'];
583
602
  // The same refusal for the same reason: the reading is of every mesh the pose shows, whatever a picture leaves out.
584
603
  if (folds) {
585
604
  for (const flag of ['slot', 'hide'] as const) {
@@ -593,6 +612,19 @@ export function cmdRender(flags: Record<string, string>): void {
593
612
  }
594
613
  }
595
614
 
615
+ // And for the pass-through reading: it reads the two slots as the frame draws them, and a subset changes which draw.
616
+ if (passThrough !== undefined) {
617
+ for (const flag of ['slot', 'hide'] as const) {
618
+ if (flags[flag] !== undefined) {
619
+ throw new UsageError(
620
+ `--pass-through takes no --${flag}: it reads the cloth and the leg as the whole rig draws them, which a subset of the ` +
621
+ `drawn slots would change. Run \`rigc render --pass-through\` on the whole rig for the reading, and --${flag} ${flags[flag]} ` +
622
+ 'without --pass-through for the pictures — both land on the same grid',
623
+ );
624
+ }
625
+ }
626
+ }
627
+
596
628
  console.log('rigc render');
597
629
  console.log(` .. skeleton ${skeletonPath}`);
598
630
  console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
@@ -610,6 +642,7 @@ export function cmdRender(flags: Record<string, string>): void {
610
642
  const only = readAnimationFlag(flags, [...facts.animations]);
611
643
  const skin = readSkinFlag(flags, [...facts.skins]);
612
644
  const subset = readSlotSubsetFlags(flags, facts, skin);
645
+ const passPairs = readPassThroughFlag(passThrough, facts);
613
646
  // One object, so the framing and the frames cannot be posed under two
614
647
  // different skins — which would frame one shot with another shot's box.
615
648
  // Not annotated `PoseOptions`: that name is `src/pose.ts`'s in this file, and
@@ -669,7 +702,7 @@ export function cmdRender(flags: Record<string, string>): void {
669
702
  // `--geometry` rides on the SAME call (issue #864): the bones and the whole
670
703
  // attachments are read off the skeleton at the step that drew each frame, so
671
704
  // the export's grid is this frame set's by construction.
672
- const sampling = geometry ? { ...pose, bones: true, geometry: true } : folds ? { ...pose, geometry: true } : pose;
705
+ const sampling = geometry ? { ...pose, bones: true, geometry: true } : folds || passPairs !== undefined ? { ...pose, geometry: true } : pose;
673
706
  const sampled: Map<string, Frame[]> =
674
707
  only === undefined
675
708
  ? sampleAll(poser, fps, sampling)
@@ -694,9 +727,37 @@ export function cmdRender(flags: Record<string, string>): void {
694
727
  foldReadings.set(name, foldsFileOf(animation, fps, skin, foldsOf(poser.rest(skin, shown), read)));
695
728
  }
696
729
  }
697
- return { viewport, sampled, geometryTexts, foldReadings };
730
+ // The pass-through reading, off the very pieces the frames are drawn from, in their draw order — one per set, every
731
+ // pair the flag names. A pair whose slot draws on no frame of any set drawn is refused rather than read as 0.
732
+ const passReadings = new Map<string, ReturnType<typeof passThroughFileOf>>();
733
+ if (passPairs !== undefined) {
734
+ const draws = new Set<string>();
735
+ for (const frames of sampled.values()) for (const frame of frames) for (const piece of frame.pieces) draws.add(piece.slot);
736
+ for (const pair of passPairs) {
737
+ for (const slot of [pair.cloth, pair.leg]) {
738
+ if (!draws.has(slot)) {
739
+ throw new UsageError(
740
+ `--pass-through: slot ${JSON.stringify(slot)} draws on no frame of [${[...sampled.keys()].join(', ')}]${skin === undefined ? '' : ` under skin ${JSON.stringify(skin)}`} ` +
741
+ '— it shows no region or mesh there, so it has no drawn outline to read',
742
+ );
743
+ }
744
+ }
745
+ }
746
+ for (const [name, frames] of sampled) {
747
+ const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
748
+ const shown = frames.map((frame) => frame.attachments ?? []);
749
+ const read = frames.map((frame) => ({ index: frame.index, time: frame.time, pieces: frame.pieces, attachments: frame.attachments ?? [] }));
750
+ try {
751
+ passReadings.set(name, passThroughFileOf(animation, fps, skin, passThroughOf(passPairs, poser.rest(skin, shown), read, pages)));
752
+ } catch (err) {
753
+ if (err instanceof PassThroughError) throw new UsageError(`--pass-through, ${JSON.stringify(name)}: ${err.message}`);
754
+ throw err;
755
+ }
756
+ }
757
+ }
758
+ return { viewport, sampled, geometryTexts, foldReadings, passReadings };
698
759
  });
699
- const { viewport, sampled, geometryTexts, foldReadings } = posed.value;
760
+ const { viewport, sampled, geometryTexts, foldReadings, passReadings } = posed.value;
700
761
  console.log(` .. poser ${posed.note}`);
701
762
  if (environment !== undefined) {
702
763
  const response = environmentResponse(skeletonText);
@@ -733,6 +794,8 @@ export function cmdRender(flags: Record<string, string>): void {
733
794
  if (geometryOut !== undefined) writeFileSync(join(dir, GEOMETRY_FILE), geometryOut);
734
795
  const foldReading = foldReadings.get(name);
735
796
  if (foldReading !== undefined) writeFileSync(join(dir, FOLDS_FILE), foldsText(foldReading));
797
+ const passReading = passReadings.get(name);
798
+ if (passReading !== undefined) writeFileSync(join(dir, PASS_THROUGH_FILE), passThroughText(passReading));
736
799
  const duration = frames[frames.length - 1].time;
737
800
  sets.push({
738
801
  dir: dirName,
@@ -746,7 +809,9 @@ export function cmdRender(flags: Record<string, string>): void {
746
809
  const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
747
810
  const folding = foldReading?.meshes.filter((m) => m.worst !== null).length ?? 0;
748
811
  const foldNote = foldReading === undefined ? '' : ` + ${FOLDS_FILE} (${folding} of ${foldReading.meshes.length} mesh(es) fold)`;
749
- const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}`;
812
+ const passing = passReading?.pairs.filter((p) => p.frames.length > 0).length ?? 0;
813
+ const passNote = passReading === undefined ? '' : ` + ${PASS_THROUGH_FILE} (${passing} of ${passReading.pairs.length} pair(s) pass through)`;
814
+ const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}`;
750
815
  console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
751
816
  for (const mesh of foldReading?.meshes ?? []) {
752
817
  const key = `${mesh.slot}\u0000${mesh.attachment}`;
@@ -756,6 +821,10 @@ export function cmdRender(flags: Record<string, string>): void {
756
821
  const line = foldLine(foldReading?.animation ?? null, fps, mesh);
757
822
  if (line !== null) console.log(` .. folds ${line}`);
758
823
  }
824
+ for (const pair of passReading?.pairs ?? []) {
825
+ console.log(` .. pass ${passSummaryLine(passReading?.animation ?? null, fps, pair)}`);
826
+ for (const frame of pair.frames) console.log(` .. pass ${passFrameLine(passReading?.animation ?? null, pair, frame)}`);
827
+ }
759
828
  }
760
829
 
761
830
  // The sidecar is what makes this a frame SET rather than a pile of pictures:
package/src/cli/shared.ts CHANGED
@@ -38,6 +38,7 @@ import { checkAgainstFrames, type CheckOptions, CheckPlates, type CheckReport }
38
38
  import { compile, CompileError, droppedStateReason, headerBoundsOf, type CompileOptions } from '../compile.ts';
39
39
  import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
40
40
  import { FOLDS_FILE } from '../folds.ts';
41
+ import { PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
41
42
  import { parseJsonWithPosition } from '../json-position.ts';
42
43
  import { RUNG_IDS } from '../ladder.ts';
43
44
  import { MODEL_DOCUMENT_FILE, MODEL_DOCUMENT_SPEC, modelDocument, spineFileSha256 } from '../model.ts';
@@ -1013,6 +1014,13 @@ const FLAG_MEANINGS: Record<string, string> = {
1013
1014
  'with where they are (the world box of their posed vertices) and how big (their setup-pose area, and its share of the mesh\'s). ' +
1014
1015
  'One line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. ' +
1015
1016
  'A reading, not a gate. Not with --slot/--hide: it reads every mesh the pose shows',
1017
+ 'pass-through':
1018
+ `also read where a cloth slot passes through a leg slot, for each <cloth>:<leg> pair, and write ${PASS_THROUGH_FILE} into ` +
1019
+ 'each frame directory: the frames where the cloth draws over the leg and its drawn side lies inside the leg\'s drawn ' +
1020
+ `outline by ${PASS_THROUGH_RUN} steps or more — rows read in the cloth's setup pose from its hem's corner up, the run stepped in ` +
1021
+ 'world units, one texel of the coarser drawing to a step — each with the depth and the world box. One line per pair, and one per frame that passes through. ' +
1022
+ 'The order is the declaration: the first name must draw over the second for a frame to be read. A reading, not a gate. ' +
1023
+ 'An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name. Not with --slot/--hide',
1016
1024
  environment:
1017
1025
  'the world the rig\'s physics answers, in time: a file { "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, ' +
1018
1026
  '"wind"?: [x, y], "gravity"?: [x, y] }, …] }, x right and y down, each step holding from its t until the next and set ' +
@@ -1202,6 +1210,7 @@ const FLAG_VALUES: Record<string, string> = {
1202
1210
  'texture-from': '<path>',
1203
1211
  poser: 'core|spine',
1204
1212
  environment: '<schedule.json>',
1213
+ 'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
1205
1214
  reference: '<dir|skeleton.json>',
1206
1215
  'reference-atlas': '<path>',
1207
1216
  bones: `<correspondence.json|${IDENTITY_CORRESPONDENCE}>`,
@@ -1608,8 +1617,9 @@ export const COMMANDS: CommandDoc[] = [
1608
1617
  'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--geometry] [--folds] [--poser core|spine] [--out render/]',
1609
1618
  'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
1610
1619
  'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
1620
+ 'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
1611
1621
  ],
1612
- flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'folds', 'environment', 'poser', 'out'],
1622
+ flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'folds', 'pass-through', 'environment', 'poser', 'out'],
1613
1623
  overrides: {
1614
1624
  out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
1615
1625
  fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },