rig-c 3.2.1 → 3.3.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/docs/AUTHORING.md +48 -0
- package/package.json +1 -1
- package/src/boundary.ts +129 -0
- package/src/check.ts +3 -0
- package/src/cli/core_commands.ts +23 -7
- package/src/cli/shared.ts +5 -1
- package/src/render.ts +6 -6
- package/src/render_core.ts +4 -3
- package/src/render_shared.ts +14 -6
- package/src/sheet.ts +6 -0
package/docs/AUTHORING.md
CHANGED
|
@@ -130,6 +130,7 @@ commands take it and what its default is.
|
|
|
130
130
|
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](../docs/AUTHORING.md)) <a id="cmd-split"></a> |
|
|
131
131
|
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong <a id="cmd-explain"></a> |
|
|
132
132
|
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings; ghost counts only visible contributions after draw order; `exposureA`/`exposureB` report each named slot’s whole-drawing background exposure even without a band ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--preroll <n>` walks each set `n` whole loops on a looping track before the loop it records, so a looping idle is drawn with its physics already warm, as a player's looping track carries it ([AUTHORING §8.13](../docs/AUTHORING.md#813-a-loop-with-its-physics-warm--render---preroll)). `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's <a id="cmd-render"></a> |
|
|
133
|
+
| `render … --physics off --boundary-from <dir>` | Suppress every physics constraint on both posers and read visible boundary displacement from a second render on the same viewport and frame grid. `frames.json` records off, `check` refuses it as a reference, and `boundary.json` carries per-frame distances or named SKIPs. See §8.14. |
|
|
133
134
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` <a id="cmd-preview"></a> |
|
|
134
135
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` <a id="cmd-vote"></a> |
|
|
135
136
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture <a id="cmd-pose"></a> |
|
|
@@ -9950,6 +9951,53 @@ still refused above 0, would reach the same looping walk; counted in steps rathe
|
|
|
9950
9951
|
than loops, it would move every compared frame off its `FrameRef` time unless the
|
|
9951
9952
|
steps were whole loops.
|
|
9952
9953
|
|
|
9954
|
+
### 8.14 Physics-off renders and boundary comparisons
|
|
9955
|
+
|
|
9956
|
+
`rigc render --candidate <dir> --physics off --out <off>` suppresses every physics
|
|
9957
|
+
constraint on both posers, after the animation timelines apply. Every other
|
|
9958
|
+
constraint and the animation clock keep their ordinary walk. `--physics on` is the
|
|
9959
|
+
default. Any other value is refused by name. An omitted mode retains the existing
|
|
9960
|
+
render bytes. Off records `physicsMode: "off"` in `frames.json`, geometry and sheet
|
|
9961
|
+
metadata, and adds a `physics=off` variant to sheet frame keys. A reference
|
|
9962
|
+
without the field means on. `check` refuses an off reference because its candidate
|
|
9963
|
+
is posed with physics on.
|
|
9964
|
+
|
|
9965
|
+
To compare two whole-figure renders, first render the reference, then render the
|
|
9966
|
+
second with `--viewport-from <reference-dir> --boundary-from <reference-dir>`.
|
|
9967
|
+
For example, render the public gallery physics recipe normally, then render it
|
|
9968
|
+
with those two flags and `--physics off`. Use the same animation selection, skin,
|
|
9969
|
+
fps and pre-roll on both runs. The shared viewport fixes the world box, scale and
|
|
9970
|
+
pixel dimensions. Check the viewport's recorded `pinned.outside` when interpreting
|
|
9971
|
+
an outline that reaches the canvas edge.
|
|
9972
|
+
|
|
9973
|
+
`boundary.json` (`rigc-boundary-difference/1`) records each matching frame's maximum
|
|
9974
|
+
symmetric nearest-boundary distance in **L-infinity pixels**, and the maximum over
|
|
9975
|
+
the frames read. A boundary pixel is visible foreground with at least one
|
|
9976
|
+
four-neighbour background pixel, or a foreground pixel on the canvas edge. Holes
|
|
9977
|
+
contribute boundaries too. Foreground means the rendered RGBA differs from the
|
|
9978
|
+
recorded background exactly. Background-coloured art has no observable coverage
|
|
9979
|
+
in this reading, and anything outside the viewport is absent. This is a pixel
|
|
9980
|
+
reading with no registration, threshold or pass verdict.
|
|
9981
|
+
|
|
9982
|
+
An identical on/on pair reads zero. An on/off pair measures the visible outline
|
|
9983
|
+
change on the chosen grid. Empty boundaries report `SKIP`, never zero. A frame
|
|
9984
|
+
schedule, viewport, background, skin, set roster or file mismatch also reports
|
|
9985
|
+
`SKIP` with a reason. Missing or unreadable frames remain unavailable. Mixed
|
|
9986
|
+
readable and empty frames report `PARTIAL` with the per-frame reasons. Both sides
|
|
9987
|
+
must record the same slot selection. `scope` records that selection explicitly. A backdrop is part of the visible outline when it is drawn, so a
|
|
9988
|
+
full-canvas opaque backdrop can make the distance zero even while the character
|
|
9989
|
+
moves. For `gallery/look`, use `--hide plate` on both renders to read the character
|
|
9990
|
+
without its backdrop. Every other drawn slot still contributes to that outline.
|
|
9991
|
+
`--extremes-only` takes no `--boundary-from`, and the reference directory must differ
|
|
9992
|
+
from `--out`. Reusing an output directory clears its previous `boundary.json`
|
|
9993
|
+
before writing the new render, including an `--extremes-only` render.
|
|
9994
|
+
|
|
9995
|
+
`--extremes` chooses a pair within one animation and reads corresponding authored
|
|
9996
|
+
vertices and local channels. Subtracting two extreme readings does not compare
|
|
9997
|
+
the same frames' visible outlines. `diff` compares skeleton data, and `check`
|
|
9998
|
+
reposes and registers a candidate against reference pictures. Neither reads two
|
|
9999
|
+
already-rendered whole-figure boundaries on an unchanged grid.
|
|
10000
|
+
|
|
9953
10001
|
## 9. Checking against the frames — `rigc check`
|
|
9954
10002
|
|
|
9955
10003
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/boundary.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Visible boundary displacement on the recorded pixel grid, without registration or thresholds. */
|
|
2
|
+
import { readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readPlate, type Plate, type RGBA } from '../tools/plate.ts';
|
|
5
|
+
import { FRAMES_SPEC, type FramesSidecar } from './render_shared.ts';
|
|
6
|
+
|
|
7
|
+
type DistanceReading = { status: 'READ'; maxPixels: number } | { status: 'SKIP'; reason: string };
|
|
8
|
+
|
|
9
|
+
/** Background-coloured pixels carry no observable foreground coverage. */
|
|
10
|
+
export function visibleBoundary(plate: Plate, background: RGBA): number[] {
|
|
11
|
+
const { width, height, data } = plate;
|
|
12
|
+
const foreground = new Uint8Array(width * height);
|
|
13
|
+
for (let pixel = 0; pixel < foreground.length; pixel++) {
|
|
14
|
+
foreground[pixel] = background.some((value, channel) => data[pixel * 4 + channel] !== value) ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
const boundary: number[] = [];
|
|
17
|
+
for (let y = 0; y < height; y++) {
|
|
18
|
+
for (let x = 0; x < width; x++) {
|
|
19
|
+
const pixel = y * width + x;
|
|
20
|
+
if (foreground[pixel] && (
|
|
21
|
+
x === 0 || y === 0 || x === width - 1 || y === height - 1 ||
|
|
22
|
+
!foreground[pixel - 1] || !foreground[pixel + 1] ||
|
|
23
|
+
!foreground[pixel - width] || !foreground[pixel + width]
|
|
24
|
+
)) boundary.push(pixel);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return boundary;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Exact eight-neighbour nearest-boundary distance in L-infinity pixels. Linear in canvas area. */
|
|
31
|
+
function distances(width: number, height: number, boundary: readonly number[]): Int32Array {
|
|
32
|
+
const distance = new Int32Array(width * height).fill(-1);
|
|
33
|
+
const queue = new Int32Array(distance.length);
|
|
34
|
+
let end = 0;
|
|
35
|
+
for (const pixel of boundary) {
|
|
36
|
+
distance[pixel] = 0;
|
|
37
|
+
queue[end++] = pixel;
|
|
38
|
+
}
|
|
39
|
+
for (let head = 0; head < end; head++) {
|
|
40
|
+
const pixel = queue[head];
|
|
41
|
+
const x = pixel % width;
|
|
42
|
+
const y = Math.floor(pixel / width);
|
|
43
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
44
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
45
|
+
const nextX = x + dx, nextY = y + dy;
|
|
46
|
+
if (nextX < 0 || nextY < 0 || nextX >= width || nextY >= height) continue;
|
|
47
|
+
const next = nextY * width + nextX;
|
|
48
|
+
if (distance[next] !== -1) continue;
|
|
49
|
+
distance[next] = distance[pixel] + 1;
|
|
50
|
+
queue[end++] = next;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return distance;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function boundaryDistance(a: Plate, b: Plate, background: RGBA): DistanceReading {
|
|
58
|
+
if (a.width !== b.width || a.height !== b.height) return { status: 'SKIP', reason: 'pixel dimensions differ' };
|
|
59
|
+
const left = visibleBoundary(a, background), right = visibleBoundary(b, background);
|
|
60
|
+
if (left.length === 0 || right.length === 0) {
|
|
61
|
+
return { status: 'SKIP', reason: 'one or both frames have no visible foreground boundary' };
|
|
62
|
+
}
|
|
63
|
+
const toLeft = distances(a.width, a.height, left), toRight = distances(a.width, a.height, right);
|
|
64
|
+
let maxPixels = 0;
|
|
65
|
+
for (const pixel of left) maxPixels = Math.max(maxPixels, toRight[pixel]);
|
|
66
|
+
for (const pixel of right) maxPixels = Math.max(maxPixels, toLeft[pixel]);
|
|
67
|
+
return { status: 'READ', maxPixels };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SCHEDULE_FIELDS = ['animation', 'fps', 'sampled', 'written', 'stride', 'duration'] as const;
|
|
71
|
+
|
|
72
|
+
/** A failed observation remains a named SKIP and never becomes a zero distance. */
|
|
73
|
+
export function boundaryComparison(reference: string, current: string, sidecar: FramesSidecar) {
|
|
74
|
+
const spec = 'rigc-boundary-difference/1';
|
|
75
|
+
const skip = (reason: string) => ({ spec, status: 'SKIP' as const, reason });
|
|
76
|
+
try {
|
|
77
|
+
if (realpathSync(reference) === realpathSync(current)) return skip('reference and output directories are the same');
|
|
78
|
+
const before = JSON.parse(readFileSync(join(reference, 'frames.json'), 'utf8')) as FramesSidecar;
|
|
79
|
+
if (before.spec !== FRAMES_SPEC || !Array.isArray(before.sets)) return skip('reference requires a render frames.json');
|
|
80
|
+
const coordinates = Object.keys(sidecar.viewport) as Array<keyof typeof sidecar.viewport>;
|
|
81
|
+
if (!before.viewport || coordinates.some((key) => !Number.isFinite(before.viewport[key]) || before.viewport[key] !== sidecar.viewport[key])) {
|
|
82
|
+
return skip('world viewport or pixel grid differs, use --viewport-from');
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(before.background) !== JSON.stringify(sidecar.background)) return skip('recorded backgrounds differ');
|
|
85
|
+
if (before.skin !== sidecar.skin || JSON.stringify(before.slots) !== JSON.stringify(sidecar.slots) || JSON.stringify(before.hidden) !== JSON.stringify(sidecar.hidden)) {
|
|
86
|
+
return skip(`skin or drawn slot selection differs: reference ${JSON.stringify({ skin: before.skin ?? null, slots: before.slots ?? null, hidden: before.hidden ?? null })}, current ${JSON.stringify({ skin: sidecar.skin ?? null, slots: sidecar.slots ?? null, hidden: sidecar.hidden ?? null })}. Required equal recorded selections`);
|
|
87
|
+
}
|
|
88
|
+
const frames: Array<{ set: string; file: string } & DistanceReading> = [];
|
|
89
|
+
for (const set of sidecar.sets) {
|
|
90
|
+
const match = before.sets.find((other) => other.dir === set.dir);
|
|
91
|
+
if (!match || SCHEDULE_FIELDS.some((key) => match[key] !== set[key])) {
|
|
92
|
+
return skip(`set ${JSON.stringify(set.dir)} has a different frame schedule`);
|
|
93
|
+
}
|
|
94
|
+
if (!set.dir || set.dir === '..' || set.dir === '.' || set.dir.includes('/') || set.dir.includes('\\')) {
|
|
95
|
+
return skip('set directory must be one relative directory name');
|
|
96
|
+
}
|
|
97
|
+
const names = (root: string): string[] => readdirSync(join(root, set.dir)).filter((name) => /^f\d+\.png$/.test(name)).sort();
|
|
98
|
+
const left = names(reference), right = names(current);
|
|
99
|
+
if (left.length === 0 || left.length !== set.written || JSON.stringify(left) !== JSON.stringify(right)) {
|
|
100
|
+
return skip(`set ${JSON.stringify(set.dir)} has missing or different frame files`);
|
|
101
|
+
}
|
|
102
|
+
for (const file of left) {
|
|
103
|
+
const a = readPlate(join(reference, set.dir, file)), b = readPlate(join(current, set.dir, file));
|
|
104
|
+
if ([a, b].some((plate) => plate.width !== sidecar.viewport.pixelWidth || plate.height !== sidecar.viewport.pixelHeight)) {
|
|
105
|
+
return skip(`set ${JSON.stringify(set.dir)} frame ${file} pixel dimensions disagree with frames.json`);
|
|
106
|
+
}
|
|
107
|
+
frames.push({ set: set.dir, file, ...boundaryDistance(a, b, sidecar.background) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (before.sets.length !== sidecar.sets.length || frames.length === 0) return skip('set roster differs or no frames were compared');
|
|
111
|
+
const readings = frames.filter((frame) => frame.status === 'READ');
|
|
112
|
+
let maxPixels = 0;
|
|
113
|
+
for (const frame of readings) if (frame.status === 'READ') maxPixels = Math.max(maxPixels, frame.maxPixels);
|
|
114
|
+
return {
|
|
115
|
+
spec,
|
|
116
|
+
status: readings.length === frames.length ? 'READ' : readings.length ? 'PARTIAL' : 'SKIP',
|
|
117
|
+
metric: 'symmetric Hausdorff L-infinity pixels',
|
|
118
|
+
coverage: 'visible pixels differing from the recorded background. Four-neighbour boundary including holes, clipped to viewport',
|
|
119
|
+
referencePhysics: before.physicsMode ?? 'on',
|
|
120
|
+
physics: sidecar.physicsMode ?? 'on',
|
|
121
|
+
scope: { slots: sidecar.slots ?? null, hidden: sidecar.hidden ?? null },
|
|
122
|
+
viewport: sidecar.viewport,
|
|
123
|
+
maxPixels: readings.length ? maxPixels : null,
|
|
124
|
+
frames,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return skip(`boundary unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/check.ts
CHANGED
|
@@ -1298,6 +1298,9 @@ export function checkAgainstFrames(options: CheckOptions): CheckReport {
|
|
|
1298
1298
|
'difference between two worlds. Render the reference frames without --environment.',
|
|
1299
1299
|
);
|
|
1300
1300
|
}
|
|
1301
|
+
if (located.sidecar?.physicsMode === 'off') {
|
|
1302
|
+
throw new CheckError('frames.json records physicsMode off, while check poses the candidate with physics on. Render the reference with --physics on.');
|
|
1303
|
+
}
|
|
1301
1304
|
// A frame set drawn after a pre-roll (`render --preroll`, issue #1684) shows the physics warm; check poses the
|
|
1302
1305
|
// candidate from rest, so the score would be a number about the difference between a warm loop and a cold one.
|
|
1303
1306
|
const referencePreroll = located.sidecar?.preroll;
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { boundaryComparison } from '../boundary.ts';
|
|
1
2
|
import { sampleParallax, parallaxPlate } from '../parallax_render.ts';
|
|
2
3
|
import { textureScaleReading, textureScaleLines, TEXTURE_SCALE_FPS } from '../texture_scale.ts';
|
|
3
4
|
import { extremesOf, extremesText, extremesPlate, extremesLine, extremesRefusal } from '../extremes.ts';
|
|
@@ -723,6 +724,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
723
724
|
// The pre-roll (issue #1684), read and refused as early, and refused beside a schedule: under a pre-roll a schedule's
|
|
724
725
|
// `t` could be read on the recording's clock or on the walk's, the two give different physics, and nothing settles it.
|
|
725
726
|
const preroll = readPrerollFlag(flags);
|
|
727
|
+
if (flags.physics !== undefined && flags.physics !== 'on' && flags.physics !== 'off') throw new UsageError(`--physics ${JSON.stringify(flags.physics)}: required on or off`);
|
|
728
|
+
const physicsMode = flags.physics === 'off' ? 'off' as const : undefined;
|
|
726
729
|
if (preroll !== undefined && environment !== undefined) {
|
|
727
730
|
throw new UsageError(
|
|
728
731
|
`--preroll takes no --environment: a schedule's t could be read from the first frame recorded or from the first loop walked, ` +
|
|
@@ -734,7 +737,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
734
737
|
const given = readGivenViewport(flags);
|
|
735
738
|
const extremesOnly = flags['extremes-only'] !== undefined;
|
|
736
739
|
if (extremesOnly) {
|
|
737
|
-
for (const flag of ['sheet', 'sheet-format', 'page-edges', 'geometry', 'folds', 'pass-through', 'seam', 'overlaps', 'angle-range']) {
|
|
740
|
+
for (const flag of ['sheet', 'sheet-format', 'page-edges', 'geometry', 'folds', 'pass-through', 'seam', 'overlaps', 'angle-range', 'boundary-from']) {
|
|
738
741
|
if (flags[flag] !== undefined) {
|
|
739
742
|
throw new UsageError(`--extremes-only takes no --${flag}: it writes only extremes.json and, when readable, extremes.png. Drop --${flag}, or use --extremes for ordinary frames and additional outputs`);
|
|
740
743
|
}
|
|
@@ -753,6 +756,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
753
756
|
const sheetFormat = wantsSheet ? readSheetFormat(flags) : undefined;
|
|
754
757
|
const sheetPageEdges = wantsSheet ? readPageEdges(flags) : undefined;
|
|
755
758
|
const outRoot = resolve(flags.out ?? 'render');
|
|
759
|
+
if (flags['boundary-from'] !== undefined && realpathAhead(flags['boundary-from']) === realpathAhead(outRoot)) throw new UsageError('--boundary-from must name an earlier render directory different from --out');
|
|
756
760
|
const geometry = flags.geometry !== undefined;
|
|
757
761
|
const extremes = extremesOnly || flags.extremes !== undefined;
|
|
758
762
|
// The fold reading (issue #1383) is its own flag rather than a part of --geometry, so a caller that asked for
|
|
@@ -850,7 +854,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
850
854
|
posersAsked(flags),
|
|
851
855
|
);
|
|
852
856
|
if (flags.parallax !== undefined) {
|
|
853
|
-
for (const flag of ['animation', 'fps', 'slot', 'hide', 'geometry', 'folds', 'extremes', 'extremes-only', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'viewport', 'viewport-from']) {
|
|
857
|
+
for (const flag of ['animation', 'fps', 'slot', 'hide', 'geometry', 'folds', 'extremes', 'extremes-only', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'physics', 'boundary-from', 'viewport', 'viewport-from']) {
|
|
854
858
|
if (flags[flag] !== undefined) throw new UsageError(`--parallax takes no --${flag}: it samples one named dial on its own 9-value grid; drop --${flag} or run ordinary render separately`);
|
|
855
859
|
}
|
|
856
860
|
if (flags.poser !== undefined && flags.poser !== 'core') throw new UsageError('--parallax requires --poser core: it reads only a rigc compiled model');
|
|
@@ -898,13 +902,14 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
898
902
|
// what the frames under it draw.
|
|
899
903
|
// The pre-roll rides on it for the environment's reason (issue #1684): it changes the pose, so the framing reads it.
|
|
900
904
|
const pose =
|
|
901
|
-
skin === undefined && subset === undefined && environment === undefined && preroll === undefined
|
|
905
|
+
skin === undefined && subset === undefined && environment === undefined && preroll === undefined && physicsMode === undefined
|
|
902
906
|
? undefined
|
|
903
907
|
: {
|
|
904
908
|
...(skin === undefined ? {} : { skin }),
|
|
905
909
|
...subsetFields(subset),
|
|
906
910
|
...(environment === undefined ? {} : { environment: environment.steps }),
|
|
907
911
|
...(preroll === undefined ? {} : { preroll }),
|
|
912
|
+
...(physicsMode === undefined ? {} : { physicsMode }),
|
|
908
913
|
};
|
|
909
914
|
if (skin !== undefined) console.log(` .. skin ${skin}`);
|
|
910
915
|
if (subset !== undefined) console.log(` .. ${subset.mode.padEnd(8)} ${subset.names.join(', ')}`);
|
|
@@ -1019,7 +1024,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1019
1024
|
if (geometry) {
|
|
1020
1025
|
for (const [name, frames] of sampled) {
|
|
1021
1026
|
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
1022
|
-
geometryTexts.set(name, geometryText(geometryFileOf(poser, animation, fps, frames, viewport, skin, environment?.steps, preroll)));
|
|
1027
|
+
geometryTexts.set(name, geometryText(geometryFileOf(poser, animation, fps, frames, viewport, skin, environment?.steps, preroll, physicsMode)));
|
|
1023
1028
|
}
|
|
1024
1029
|
}
|
|
1025
1030
|
// The fold reading, off the very attachments the frames were drawn from, against the rest table the geometry
|
|
@@ -1151,6 +1156,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1151
1156
|
subset,
|
|
1152
1157
|
environment: environment?.steps,
|
|
1153
1158
|
preroll,
|
|
1159
|
+
physicsMode,
|
|
1154
1160
|
pinned,
|
|
1155
1161
|
}),
|
|
1156
1162
|
);
|
|
@@ -1194,6 +1200,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1194
1200
|
}
|
|
1195
1201
|
|
|
1196
1202
|
mkdirSync(outRoot, { recursive: true });
|
|
1203
|
+
// A prior observation cannot describe the new frames, including an observation-only render.
|
|
1204
|
+
rmSync(join(outRoot, 'boundary.json'), { force: true });
|
|
1197
1205
|
if (extremesOnly) {
|
|
1198
1206
|
// Reuse the same posed grid and plates; selecting extremes needs no ordinary pixels.
|
|
1199
1207
|
// Clear only the selected sets, just as the ordinary render does. No frames.json
|
|
@@ -1289,7 +1297,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1289
1297
|
: ` + ${sheetPack.pages.map((p) => `${p.image} + ${p.table}`).join(' + ')} + ${SHEET_MANIFEST_FILE} (${sheetPack.pages.length} page(s), ${sheetPack.pageEdges})`;
|
|
1290
1298
|
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${sheetNote}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}${seamNote}${overlapNote}`;
|
|
1291
1299
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
1292
|
-
if (physics !== undefined) console.log(` .. ${physicsLine(physics.stepped, physicsDeclared, fps, frames.length)}`);
|
|
1300
|
+
if (physics !== undefined) console.log(` .. ${physicsLine(physics.stepped, physicsDeclared, fps, frames.length, physicsMode)}`);
|
|
1293
1301
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
1294
1302
|
const key = `${mesh.slot}\u0000${mesh.attachment}`;
|
|
1295
1303
|
const fault = faultsSaid.has(key) ? null : setupFaultLine(mesh);
|
|
@@ -1330,6 +1338,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1330
1338
|
...(environment === undefined ? {} : { environment: environment.steps }),
|
|
1331
1339
|
// Written only when a pre-roll was asked for (issue #1684), for the same reason.
|
|
1332
1340
|
...(preroll === undefined ? {} : { preroll }),
|
|
1341
|
+
...(physicsMode === undefined ? {} : { physicsMode }),
|
|
1333
1342
|
// Written only when the box was given (issue #1414): a fitted render says nothing and stays the bytes it was.
|
|
1334
1343
|
...(given === undefined ? {} : { pinned: pinnedRecord(given.by, outside) }),
|
|
1335
1344
|
background: BACKGROUND,
|
|
@@ -1340,6 +1349,13 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1340
1349
|
};
|
|
1341
1350
|
writeFileSync(join(outRoot, FRAMES_SIDECAR), `${JSON.stringify(sidecar, null, 2)}\n`);
|
|
1342
1351
|
console.log(`rigc: wrote ${join(outRoot, FRAMES_SIDECAR)}`);
|
|
1352
|
+
if (flags['boundary-from'] !== undefined) {
|
|
1353
|
+
const reading = boundaryComparison(resolve(flags['boundary-from']), outRoot, sidecar);
|
|
1354
|
+
writeFileSync(join(outRoot, 'boundary.json'), `${JSON.stringify(reading, null, 2)}\n`);
|
|
1355
|
+
const detail = 'reason' in reading ? reading.reason : `max ${String(reading.maxPixels)} L-infinity px over ${reading.frames.length} frame(s)`;
|
|
1356
|
+
console.log(` .. boundary ${reading.status}: ${detail}`);
|
|
1357
|
+
console.log(`rigc: wrote ${join(outRoot, 'boundary.json')}`);
|
|
1358
|
+
}
|
|
1343
1359
|
}
|
|
1344
1360
|
|
|
1345
1361
|
/**
|
|
@@ -1371,11 +1387,11 @@ export function physicsSteppedOf(frames: readonly Frame[], fps: number): { stepp
|
|
|
1371
1387
|
* `render`'s `physics:` line for one set (issue #1467): how many constraints were stepped at the set's rate, and — when
|
|
1372
1388
|
* fewer than the rig declares — the ones that took no step, by name, with why a constraint takes none.
|
|
1373
1389
|
*/
|
|
1374
|
-
export function physicsLine(stepped: readonly string[], declared: readonly string[], fps: number, frames: number): string {
|
|
1390
|
+
export function physicsLine(stepped: readonly string[], declared: readonly string[], fps: number, frames: number, physicsMode?: 'off'): string {
|
|
1375
1391
|
const head = `physics: ${stepped.length} constraint(s) stepped at ${fps} fps`;
|
|
1376
1392
|
const idle = declared.filter((name) => !stepped.includes(name));
|
|
1377
1393
|
if (idle.length === 0) return head;
|
|
1378
|
-
const why = frames === 1 ? 'a single pose takes no step' : 'inactive, or a mix of 0, on every frame';
|
|
1394
|
+
const why = physicsMode === 'off' ? '--physics off suppresses every physics constraint' : frames === 1 ? 'a single pose takes no step' : 'inactive, or a mix of 0, on every frame';
|
|
1379
1395
|
return `${head} — ${idle.length} of the ${declared.length} declared took none: ${idle.map((n) => JSON.stringify(n)).join(', ')} (${why})`;
|
|
1380
1396
|
}
|
|
1381
1397
|
|
package/src/cli/shared.ts
CHANGED
|
@@ -1108,6 +1108,8 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1108
1108
|
'runtime\'s, wind [1, 0] and gravity [0, 1]. frames.json and geometry.json record it, and the framing reads it. A ' +
|
|
1109
1109
|
'malformed file, a half no physics constraint answers, two steps on one frame and a step after every set\'s last frame ' +
|
|
1110
1110
|
'are refused by name. An inspection of what the rig offers, not a scene',
|
|
1111
|
+
physics: 'pose with physics on (default) or off. Off suppresses every physics constraint after timeline application on both posers. frames.json records physicsMode off and check refuses that reference',
|
|
1112
|
+
'boundary-from': 'read visible whole-figure pixel-boundary displacement against an earlier render on the identical viewport and frame grid. Writes boundary.json, no threshold or pass verdict',
|
|
1111
1113
|
preroll:
|
|
1112
1114
|
'walk each set this many whole loops on a looping track before the loop recorded, drawn by no frame, the physics carried ' +
|
|
1113
1115
|
'across every wrap as a player\'s looping track carries it — so a spring that converges is warm on the first frame. Frame i is the ' +
|
|
@@ -1307,6 +1309,8 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1307
1309
|
'texture-from': '<path>',
|
|
1308
1310
|
poser: 'core|spine',
|
|
1309
1311
|
environment: '<schedule.json>',
|
|
1312
|
+
physics: '<on|off>',
|
|
1313
|
+
'boundary-from': '<dir>',
|
|
1310
1314
|
preroll: '<n>',
|
|
1311
1315
|
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1312
1316
|
seam: '<slot>:<slot>[,<slot>:<slot>…]',
|
|
@@ -1785,7 +1789,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1785
1789
|
'rigc render … --viewport <x,y,w,h> | --viewport-from <frames.json> (a box given, not fitted: two renders on one pixel grid)',
|
|
1786
1790
|
'rigc render … --sheet [--sheet-format json-hash] [--page-edges pot|free] (deterministic sprite sheets + JSON hash table + sheet-manifest.json per set)',
|
|
1787
1791
|
],
|
|
1788
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'extremes', 'extremes-only', 'parallax', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'poser', 'out'],
|
|
1792
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'extremes', 'extremes-only', 'parallax', 'pass-through', 'seam', 'overlaps', 'angle-range', 'sheet', 'sheet-format', 'page-edges', 'environment', 'preroll', 'physics', 'boundary-from', 'poser', 'out'],
|
|
1789
1793
|
overrides: {
|
|
1790
1794
|
'page-edges': {
|
|
1791
1795
|
meaning:
|
package/src/render.ts
CHANGED
|
@@ -502,8 +502,8 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
502
502
|
bones: data.bones.map((bone) => ({ name: bone.name, parent: bone.parent?.name ?? null })),
|
|
503
503
|
slots: data.slots.map((slot) => ({ name: slot.name, bone: slot.boneData.name })),
|
|
504
504
|
subset: (opts, skin) => slotSubsetOf(data, opts, skin),
|
|
505
|
-
setup: (skin) => spinePosed(setupPosed(data, skin)),
|
|
506
|
-
animation: (name, skin, fps, count, visit, environment, _channels, times, loop, clockSteps) => {
|
|
505
|
+
setup: (skin, _channels, physicsMode) => spinePosed(setupPosed(data, skin, physicsMode)),
|
|
506
|
+
animation: (name, skin, fps, count, visit, environment, _channels, times, loop, clockSteps, physicsMode) => {
|
|
507
507
|
const skeleton = skeletonUnderSkin(data, skin);
|
|
508
508
|
const state = new AnimationState(new AnimationStateData(data));
|
|
509
509
|
// Not looping: the last frame sits at the animation's duration, and a looping
|
|
@@ -524,12 +524,12 @@ export function spinePoser(data: SkeletonData): Poser {
|
|
|
524
524
|
setSkeletonEnvironment(skeleton, environment?.(i));
|
|
525
525
|
// `clockSteps` (issue #1687, `render --preroll`): what the step moves the physics clock by, where that is not its own length.
|
|
526
526
|
skeleton.update(times === undefined ? clock : physicsClockStep(clockSteps?.[i - 1] ?? delta));
|
|
527
|
-
skeleton.updateWorldTransform(Physics.update);
|
|
527
|
+
skeleton.updateWorldTransform(physicsMode === 'off' ? Physics.none : Physics.update);
|
|
528
528
|
} else {
|
|
529
529
|
state.apply(skeleton);
|
|
530
530
|
setSkeletonEnvironment(skeleton, environment?.(i));
|
|
531
531
|
skeleton.update(0);
|
|
532
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
532
|
+
skeleton.updateWorldTransform(physicsMode === 'off' ? Physics.none : Physics.reset);
|
|
533
533
|
}
|
|
534
534
|
visit(i, posed);
|
|
535
535
|
}
|
|
@@ -592,11 +592,11 @@ function steppedPhysicsOf(skeleton: Skeleton): string[] {
|
|
|
592
592
|
* `sampleSetupPose` and the geometry export's `rest` table both pose from, so
|
|
593
593
|
* the two cannot come to describe different rest poses.
|
|
594
594
|
*/
|
|
595
|
-
function setupPosed(data: SkeletonData, skin: string | undefined): Skeleton {
|
|
595
|
+
function setupPosed(data: SkeletonData, skin: string | undefined, physicsMode?: 'off'): Skeleton {
|
|
596
596
|
const skeleton = skeletonUnderSkin(data, skin);
|
|
597
597
|
skeleton.setupPose();
|
|
598
598
|
skeleton.update(0);
|
|
599
|
-
skeleton.updateWorldTransform(Physics.reset);
|
|
599
|
+
skeleton.updateWorldTransform(physicsMode === 'off' ? Physics.none : Physics.reset);
|
|
600
600
|
return skeleton;
|
|
601
601
|
}
|
|
602
602
|
|
package/src/render_core.ts
CHANGED
|
@@ -804,14 +804,14 @@ export function corePoser(
|
|
|
804
804
|
const result = poseRawDial(input.doc, override, name, { through });
|
|
805
805
|
return { posed: corePosed(input, result.pose, through, []), read: result.read, applied: result.applied };
|
|
806
806
|
},
|
|
807
|
-
setup: (skin, channels) => {
|
|
807
|
+
setup: (skin, channels, physicsMode) => {
|
|
808
808
|
const input = inputOf(skin);
|
|
809
809
|
let values: ChannelValue[] | undefined;
|
|
810
810
|
const observe = channels ? { readLocals: (bones: readonly import('./model.ts').ModelBone[], sliders: readonly import('./core/constraints_slider.ts').SliderApplication[]) => { values = localChannels(bones, sliders); } } : {};
|
|
811
|
-
const pose = poseRawSetup(input.doc, { through, ...observe });
|
|
811
|
+
const pose = poseRawSetup(input.doc, { through, ...observe, ...(physicsMode === 'off' ? { physicsStep: () => false } : {}) });
|
|
812
812
|
return corePosed(input, pose, through, [], values);
|
|
813
813
|
},
|
|
814
|
-
animation: (name, skin, fps, count, visit, environment, channels, times, loop, clockSteps) => {
|
|
814
|
+
animation: (name, skin, fps, count, visit, environment, channels, times, loop, clockSteps, physicsMode) => {
|
|
815
815
|
let values: ChannelValue[] | undefined;
|
|
816
816
|
const observe = channels ? { readLocals: (bones: readonly import('./model.ts').ModelBone[], sliders: readonly import('./core/constraints_slider.ts').SliderApplication[]) => { values = localChannels(bones, sliders); } } : {};
|
|
817
817
|
const input = inputOf(skin);
|
|
@@ -827,6 +827,7 @@ export function corePoser(
|
|
|
827
827
|
const steppedNow = new Set<string>();
|
|
828
828
|
const inner = plants.physicsStep ?? stepPhysics;
|
|
829
829
|
const physicsStep: PhysicsStepper = (r, w, length, ctx) => {
|
|
830
|
+
if (physicsMode === 'off') return false;
|
|
830
831
|
const moved = inner(r, w, length, ctx);
|
|
831
832
|
if (ctx.time > 0 && ctx.states.get(r.name)?.lastTime === ctx.time) steppedNow.add(r.name);
|
|
832
833
|
return moved;
|
package/src/render_shared.ts
CHANGED
|
@@ -243,6 +243,8 @@ export interface FrameSet {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
export interface FramesSidecar {
|
|
246
|
+
/** Absent means on, including frame sets written before this field existed. */
|
|
247
|
+
physicsMode?: 'off';
|
|
246
248
|
spec: string;
|
|
247
249
|
example?: string;
|
|
248
250
|
rung?: string;
|
|
@@ -427,6 +429,8 @@ export interface UvWindow {
|
|
|
427
429
|
|
|
428
430
|
/** Options for `piecesOf` and the samplers that call it. */
|
|
429
431
|
export interface PoseOptions {
|
|
432
|
+
/** Suppress every physics constraint after animation application. */
|
|
433
|
+
physicsMode?: 'off';
|
|
430
434
|
/** Also read local bone/deform channels and mapped slider times, and retain original attachment source identity outside JSON, for --extremes. */
|
|
431
435
|
channels?: boolean;
|
|
432
436
|
/** Also record each piece's original-art UVs — see `PieceTexture`. */
|
|
@@ -1184,7 +1188,7 @@ export interface Poser {
|
|
|
1184
1188
|
/** `slotSubsetOf` against this skeleton posed under `skin` — refused by `SlotSubsetError`. */
|
|
1185
1189
|
subset(opts: Pick<PoseOptions, 'slots' | 'hidden'> | undefined, skin: string | undefined): SlotSubset | undefined;
|
|
1186
1190
|
/** The setup pose under `skin` (absent: no skin set at all). */
|
|
1187
|
-
setup(skin: string | undefined, channels?: boolean): Posed;
|
|
1191
|
+
setup(skin: string | undefined, channels?: boolean, physicsMode?: 'off'): Posed;
|
|
1188
1192
|
/**
|
|
1189
1193
|
* Animation `name` under `skin`, not looping: `visit(i, posed)` for every
|
|
1190
1194
|
* `i` from 0 to `count`, the pose at `i/fps`. The caller has checked `name`.
|
|
@@ -1205,7 +1209,7 @@ export interface Poser {
|
|
|
1205
1209
|
* follows `times` (`prerollTimes` says why the two part). Absent, every
|
|
1206
1210
|
* step moves the physics clock by its own length, as it always did.
|
|
1207
1211
|
*/
|
|
1208
|
-
animation(name: string, skin: string | undefined, fps: number, count: number, visit: (index: number, posed: Posed) => void, environment?: (index: number) => EnvironmentChange | undefined, channels?: boolean, times?: readonly number[], loop?: boolean, clockSteps?: readonly number[]): void;
|
|
1212
|
+
animation(name: string, skin: string | undefined, fps: number, count: number, visit: (index: number, posed: Posed) => void, environment?: (index: number) => EnvironmentChange | undefined, channels?: boolean, times?: readonly number[], loop?: boolean, clockSteps?: readonly number[], physicsMode?: 'off'): void;
|
|
1209
1213
|
/** The rest table for the (slot, attachment) pairs `shown` holds — see `AttachmentRest`. */
|
|
1210
1214
|
rest(skin: string | undefined, shown: readonly AttachmentPose[][]): AttachmentRest[];
|
|
1211
1215
|
}
|
|
@@ -1290,14 +1294,15 @@ export function sampleAnimation(source: PoseSource, name: string, fps: number, o
|
|
|
1290
1294
|
const recorded = (i: number, posed: Posed): void => {
|
|
1291
1295
|
if (i >= first) frames.push(frameOf(i - first, (i - first) * step, posed, draw(), opts));
|
|
1292
1296
|
};
|
|
1293
|
-
poser.animation(name, opts?.skin, fps, times.length - 1, recorded, undefined, opts?.channels === true ? true : undefined, times, true, clock);
|
|
1297
|
+
poser.animation(name, opts?.skin, fps, times.length - 1, recorded, undefined, opts?.channels === true ? true : undefined, times, true, clock, opts?.physicsMode);
|
|
1294
1298
|
return frames;
|
|
1295
1299
|
}
|
|
1296
1300
|
const visit = (i: number, posed: Posed): void => {
|
|
1297
1301
|
frames.push(frameOf(i, i * step, posed, draw(), opts));
|
|
1298
1302
|
};
|
|
1299
1303
|
// Without an environment the call is the one it always was, so a poser that takes none is asked nothing new.
|
|
1300
|
-
if (opts?.
|
|
1304
|
+
if (opts?.physicsMode === 'off') poser.animation(name, opts.skin, fps, count, visit, opts.environment === undefined ? undefined : environmentAt(opts.environment, fps), opts.channels, undefined, undefined, undefined, 'off');
|
|
1305
|
+
else if (opts?.channels) poser.animation(name, opts.skin, fps, count, visit, opts.environment === undefined ? undefined : environmentAt(opts.environment, fps), true);
|
|
1301
1306
|
else if (opts?.environment === undefined) poser.animation(name, opts?.skin, fps, count, visit);
|
|
1302
1307
|
else poser.animation(name, opts.skin, fps, count, visit, environmentAt(opts.environment, fps));
|
|
1303
1308
|
return frames;
|
|
@@ -1413,7 +1418,7 @@ export function prerollTimes(duration: number, fps: number, preroll: number): {
|
|
|
1413
1418
|
*/
|
|
1414
1419
|
export function sampleSetupPose(source: PoseSource, opts?: PoseOptions): Frame[] {
|
|
1415
1420
|
const poser = poserOf(source);
|
|
1416
|
-
const posed = opts?.channels ? poser.setup(opts.skin, true) : poser.setup(opts?.skin);
|
|
1421
|
+
const posed = opts?.physicsMode === 'off' ? poser.setup(opts.skin, opts.channels, 'off') : opts?.channels ? poser.setup(opts.skin, true) : poser.setup(opts?.skin);
|
|
1417
1422
|
return [frameOf(0, 0, posed, drawResolver(poser, opts)(), opts)];
|
|
1418
1423
|
}
|
|
1419
1424
|
|
|
@@ -1737,6 +1742,7 @@ export interface GeometryFile {
|
|
|
1737
1742
|
/** The setup pose, sampled the way `sampleSetupPose` samples it. */
|
|
1738
1743
|
setup: Omit<GeometryFrame, 'index' | 'time'>;
|
|
1739
1744
|
frames: GeometryFrame[];
|
|
1745
|
+
physicsMode?: 'off';
|
|
1740
1746
|
}
|
|
1741
1747
|
|
|
1742
1748
|
/**
|
|
@@ -1784,6 +1790,7 @@ export function geometryFileOf(
|
|
|
1784
1790
|
skin: string | undefined,
|
|
1785
1791
|
environment?: EnvironmentStep[],
|
|
1786
1792
|
preroll?: number,
|
|
1793
|
+
physicsMode?: 'off',
|
|
1787
1794
|
): GeometryFile {
|
|
1788
1795
|
const geometryFrame = (frame: Frame, where: string): Omit<GeometryFrame, 'index' | 'time'> => {
|
|
1789
1796
|
if (frame.bones === undefined || frame.attachments === undefined) {
|
|
@@ -1800,7 +1807,7 @@ export function geometryFileOf(
|
|
|
1800
1807
|
...geometryFrame(frame, `frame ${frame.index}`),
|
|
1801
1808
|
}));
|
|
1802
1809
|
const poser = poserOf(source);
|
|
1803
|
-
const setupFrame = sampleSetupPose(poser, { ...(skin === undefined ? {} : { skin }), bones: true, geometry: true })[0];
|
|
1810
|
+
const setupFrame = sampleSetupPose(poser, { ...(skin === undefined ? {} : { skin }), bones: true, geometry: true, ...(physicsMode === undefined ? {} : { physicsMode }) })[0];
|
|
1804
1811
|
const setup = geometryFrame(setupFrame, 'the setup pose');
|
|
1805
1812
|
const file: GeometryFile = {
|
|
1806
1813
|
spec: GEOMETRY_SPEC,
|
|
@@ -1809,6 +1816,7 @@ export function geometryFileOf(
|
|
|
1809
1816
|
...(skin === undefined ? {} : { skin }),
|
|
1810
1817
|
...(environment === undefined ? {} : { environment }),
|
|
1811
1818
|
...(preroll === undefined ? {} : { preroll }),
|
|
1819
|
+
...(physicsMode === undefined ? {} : { physicsMode }),
|
|
1812
1820
|
fps,
|
|
1813
1821
|
viewport: sidecarViewport(viewport),
|
|
1814
1822
|
bones: poser.bones.map(({ name, parent }) => ({ name, parent })),
|
package/src/sheet.ts
CHANGED
|
@@ -120,6 +120,7 @@ export interface SheetManifest {
|
|
|
120
120
|
environment?: EnvironmentStep[];
|
|
121
121
|
/** The whole loops walked before the loop these frames record (`render --preroll`, issue #1684); absent without it. */
|
|
122
122
|
preroll?: number;
|
|
123
|
+
physicsMode?: 'off';
|
|
123
124
|
pinned?: PinnedViewport;
|
|
124
125
|
fps: number;
|
|
125
126
|
duration: number;
|
|
@@ -185,6 +186,7 @@ export interface BuildSheetSetOptions {
|
|
|
185
186
|
subset?: SlotSubset;
|
|
186
187
|
environment?: EnvironmentStep[];
|
|
187
188
|
preroll?: number;
|
|
189
|
+
physicsMode?: 'off';
|
|
188
190
|
pinned?: PinnedViewport;
|
|
189
191
|
/** For negative-control testing in `selftest.ts` only: reverse the input order handed to `packAtlas`. */
|
|
190
192
|
reversePackInputs?: boolean;
|
|
@@ -276,6 +278,7 @@ export function sheetNamespaceOf(opts: {
|
|
|
276
278
|
subset?: SlotSubset;
|
|
277
279
|
environment?: EnvironmentStep[];
|
|
278
280
|
preroll?: number;
|
|
281
|
+
physicsMode?: 'off';
|
|
279
282
|
}): string {
|
|
280
283
|
const rig = sanitizeNamespaceSegment(opts.rigName);
|
|
281
284
|
const anim = sanitizeNamespaceSegment(opts.setName);
|
|
@@ -287,6 +290,7 @@ export function sheetNamespaceOf(opts: {
|
|
|
287
290
|
variants.push(`env=${digest}`);
|
|
288
291
|
}
|
|
289
292
|
if (opts.preroll !== undefined) variants.push(`preroll=${opts.preroll}`);
|
|
293
|
+
if (opts.physicsMode !== undefined) variants.push(`physics=${opts.physicsMode}`);
|
|
290
294
|
return variants.length === 0 ? `${rig}/${anim}` : `${rig}/${anim}/${variants.join('+')}`;
|
|
291
295
|
}
|
|
292
296
|
|
|
@@ -721,6 +725,7 @@ export function buildSheetSet(opts: BuildSheetSetOptions): SheetSetPack {
|
|
|
721
725
|
subset: opts.subset,
|
|
722
726
|
environment: opts.environment,
|
|
723
727
|
preroll: opts.preroll,
|
|
728
|
+
physicsMode: opts.physicsMode,
|
|
724
729
|
});
|
|
725
730
|
|
|
726
731
|
const padding = SHEET_PADDING;
|
|
@@ -875,6 +880,7 @@ export function buildSheetSet(opts: BuildSheetSetOptions): SheetSetPack {
|
|
|
875
880
|
: { hidden: [...opts.subset.names] }),
|
|
876
881
|
...(opts.environment === undefined ? {} : { environment: opts.environment }),
|
|
877
882
|
...(opts.preroll === undefined ? {} : { preroll: opts.preroll }),
|
|
883
|
+
...(opts.physicsMode === undefined ? {} : { physicsMode: opts.physicsMode }),
|
|
878
884
|
...(opts.pinned === undefined ? {} : { pinned: opts.pinned }),
|
|
879
885
|
fps: opts.fps,
|
|
880
886
|
duration,
|