spine-rigc 2.13.0 → 2.13.1
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/package.json +1 -1
- package/src/assertions/model/stepped_poses.ts +11 -5
- package/src/core/constraints.ts +102 -11
- package/src/core/raw.ts +15 -3
- package/src/core/walk.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,9 +42,11 @@ const modeOf = (bone: ModelBone): string | null => (foldInheritMode(bone.inherit
|
|
|
42
42
|
/** A channel the walk computed; a slot row holds `null` only where a rounding wrote one, which the walk does not. */
|
|
43
43
|
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/** Each bone's posed mode as the facts spell it (`modeOf`), by name. */
|
|
46
|
+
const modesOf = (posed: readonly ModelBone[]): ReadonlyMap<string, string | null> => new Map(posed.map((b) => [b.name, modeOf(b)]));
|
|
47
|
+
|
|
48
|
+
/** One walk pose as A10 reads it, each bone's mode taken from `modes` (`modesOf` the bones the timelines posed at the pose's time). */
|
|
49
|
+
function frameOf(pose: WalkPose, modes: ReadonlyMap<string, string | null>): SteppedFrame {
|
|
48
50
|
return {
|
|
49
51
|
bones: pose.bones.map((b) => ({ name: b.name, a: b.a, b: b.b, c: b.c, d: b.d, worldX: b.worldX, worldY: b.worldY, inherit: modes.get(b.name) ?? null })),
|
|
50
52
|
drawn: pose.drawn.map((d) => ({ slot: d.slot, attachment: d.attachment, vertices: d.vertices })),
|
|
@@ -80,11 +82,15 @@ export function modelSteppedPoses(read: ReadDocument): SteppedPoseFacts {
|
|
|
80
82
|
},
|
|
81
83
|
// The document's stated mode — asked only of a bone posing none, which the reader leaves this side no document to hold.
|
|
82
84
|
statedInherit: (name) => `${JSON.stringify(read.doc.bones.find((b) => b.name === name)?.inheritMode)}`,
|
|
83
|
-
setup: () => frameOf(poseWalkSetup(viewOf()), viewOf().bones),
|
|
85
|
+
setup: () => frameOf(poseWalkSetup(viewOf()), modesOf(viewOf().bones)),
|
|
84
86
|
walk: (animation, step, frames) => {
|
|
85
87
|
const anim = animationOf(animation);
|
|
86
88
|
const poses = poseLoopingWalk(viewOf(), animation, new Array<number>(frames).fill(step));
|
|
87
|
-
|
|
89
|
+
// A bone's posed mode moves only by an `inherit` timeline: every other one `posedBones` reads leaves the setup's mode on its copy. So an animation
|
|
90
|
+
// keying none poses the setup's modes at every time, and they are read once rather than posed again at each of its frames (issue #1179).
|
|
91
|
+
const keysInherit = anim.timelines.bones.some((target) => target.timelines.some((tl) => tl.kind === 'inherit'));
|
|
92
|
+
const setupModes = keysInherit ? null : modesOf(viewOf().bones);
|
|
93
|
+
return poses.slice(1).map((pose) => frameOf(pose, setupModes ?? modesOf(posedBones(viewOf(), anim.timelines, pose.animationTime))));
|
|
88
94
|
},
|
|
89
95
|
};
|
|
90
96
|
}
|
package/src/core/constraints.ts
CHANGED
|
@@ -1529,9 +1529,9 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
|
|
|
1529
1529
|
if (inactiveWhy(state, c) !== null) skipped.add(i);
|
|
1530
1530
|
});
|
|
1531
1531
|
// A path constraint's offset reads its slot bone's world as the runtime last brought it up to date (`./constraints_path.ts`, *Which slot bone*).
|
|
1532
|
-
const plan = records.some((c) => c.kind === 'path') ? slotBonePlan(bones, active, records, skipped) : new Map<number, SlotBoneEvent | null>();
|
|
1532
|
+
const plan = records.some((c) => c.kind === 'path') ? keptPlan(slotBonePlans, state, records, skipped, () => slotBonePlan(bones, active, records, skipped)) : new Map<number, SlotBoneEvent | null>();
|
|
1533
1533
|
// An ik over an inactive bone reads the frame above it as the pass left it (issue #979).
|
|
1534
|
-
const updated = records.some((c) => c.kind === 'ik' && c.bones.some((b) => !active.has(b))) ? frameUpdatedBefore(bones, active, records, skipped) : new Map<number, boolean>();
|
|
1534
|
+
const updated = records.some((c) => c.kind === 'ik' && c.bones.some((b) => !active.has(b))) ? keptPlan(frameUpdates, state, records, skipped, () => frameUpdatedBefore(bones, active, records, skipped)) : new Map<number, boolean>();
|
|
1535
1535
|
const snapshots = new Map<number, CoreWorld>();
|
|
1536
1536
|
// Issue #1049: under the step a slider's physics timelines write the pass's physics records, which a later physics constraint steps with.
|
|
1537
1537
|
const physicsPose = physics === undefined ? undefined : sliderPhysicsTarget(records, active, physics, rules);
|
|
@@ -1578,24 +1578,111 @@ export function applyConstraints(bones: readonly ModelBone[], world: ReadonlyMap
|
|
|
1578
1578
|
return state.world;
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
|
-
/**
|
|
1582
|
-
|
|
1581
|
+
/**
|
|
1582
|
+
* What `slotBonePlan` (`./constraints_path.ts`) and `frameUpdatedBefore` read
|
|
1583
|
+
* of a record: its kind and the names it orders — a name by value, a list by
|
|
1584
|
+
* identity. Every pass of a walk poses its records afresh (`posedRecords`,
|
|
1585
|
+
* `pathDeformed`, `stepPhysicsRecords` spread each one), and a spread carries
|
|
1586
|
+
* these lists over as the same arrays.
|
|
1587
|
+
*/
|
|
1588
|
+
function planShape(c: CoreConstraintRecord): readonly unknown[] {
|
|
1589
|
+
switch (c.kind) {
|
|
1590
|
+
case 'slider':
|
|
1591
|
+
return [c.kind, c.timelines.bones];
|
|
1592
|
+
case 'ik':
|
|
1593
|
+
return [c.kind, c.target, c.bones];
|
|
1594
|
+
case 'physics':
|
|
1595
|
+
return [c.kind, c.bone];
|
|
1596
|
+
case 'transform':
|
|
1597
|
+
return [c.kind, c.source, c.bones];
|
|
1598
|
+
case 'path':
|
|
1599
|
+
return [c.kind, c.bones, c.slotDeps, c.slotBone];
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** A plan kept per solver index, with the record shapes (`planShape`) and the skipped records it was built over. */
|
|
1604
|
+
interface KeptPlan<T> {
|
|
1605
|
+
shapes: ReadonlyArray<readonly unknown[]>;
|
|
1606
|
+
skipped: readonly number[];
|
|
1607
|
+
plan: T;
|
|
1608
|
+
}
|
|
1609
|
+
const slotBonePlans = new WeakMap<ReadonlyMap<string, number>, KeptPlan<Map<number, SlotBoneEvent | null>>>();
|
|
1610
|
+
const frameUpdates = new WeakMap<ReadonlyMap<string, number>, KeptPlan<Map<number, boolean>>>();
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* A pass's ordering plan — `slotBonePlan` or `frameUpdatedBefore` — reused
|
|
1614
|
+
* from the pass before when nothing it reads has changed (issue #1179): the
|
|
1615
|
+
* bones' names and parents and the active set (one solver index stands for
|
|
1616
|
+
* those, `boneIndex`), each record's shape, and which records are skipped.
|
|
1617
|
+
* Otherwise built again. Both read names and nothing else, so a reused plan is
|
|
1618
|
+
* the plan the pass would have built; neither is written once built.
|
|
1619
|
+
*/
|
|
1620
|
+
function keptPlan<T>(kept: WeakMap<ReadonlyMap<string, number>, KeptPlan<T>>, state: SolverState, records: readonly CoreConstraintRecord[], skipped: ReadonlySet<number>, build: () => T): T {
|
|
1621
|
+
const shapes = records.map(planShape);
|
|
1622
|
+
const skips = [...skipped];
|
|
1623
|
+
const last = kept.get(state.index);
|
|
1624
|
+
if (
|
|
1625
|
+
last !== undefined &&
|
|
1626
|
+
last.shapes.length === shapes.length &&
|
|
1627
|
+
shapes.every((sh, i) => sh.length === last.shapes[i].length && sh.every((v, j) => v === last.shapes[i][j])) &&
|
|
1628
|
+
last.skipped.length === skips.length &&
|
|
1629
|
+
skips.every((k, i) => k === last.skipped[i])
|
|
1630
|
+
) {
|
|
1631
|
+
return last.plan;
|
|
1632
|
+
}
|
|
1633
|
+
const plan = build();
|
|
1634
|
+
kept.set(state.index, { shapes, skipped: skips, plan });
|
|
1635
|
+
return plan;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** The last bone index each view's active set was asked for, with the names and parents it was built over (`boneIndex`). */
|
|
1639
|
+
const indexOfView = new WeakMap<ReadonlySet<string>, { names: string[]; parents: Array<string | undefined>; index: Map<string, number> }>();
|
|
1583
1640
|
|
|
1584
1641
|
/**
|
|
1585
1642
|
* Each bone's position in `bones`, by name — the solver's index. Every pose of
|
|
1586
1643
|
* a view lists the same bones in the same order (issue #1134: a walk built it
|
|
1587
1644
|
* anew on each of its passes), so the index last built under the view's
|
|
1588
1645
|
* active set (`activeBones`, one object per view) is reused when the names
|
|
1589
|
-
* agree position by position, and built again otherwise
|
|
1646
|
+
* and the parents agree position by position, and built again otherwise —
|
|
1647
|
+
* the parents because `repose` keeps each bone's subtree beside the index
|
|
1648
|
+
* (`subtreesOf`), and a subtree is a reading of them. Read, never written.
|
|
1590
1649
|
*/
|
|
1591
1650
|
function boneIndex(bones: readonly ModelBone[], active: ReadonlySet<string>): Map<string, number> {
|
|
1592
1651
|
const kept = indexOfView.get(active);
|
|
1593
|
-
if (kept !== undefined && kept.names.length === bones.length && bones.every((b, i) => b.name === kept.names[i])) return kept.index;
|
|
1652
|
+
if (kept !== undefined && kept.names.length === bones.length && bones.every((b, i) => b.name === kept.names[i] && b.parent === kept.parents[i])) return kept.index;
|
|
1594
1653
|
const index = new Map(bones.map((b, i) => [b.name, i]));
|
|
1595
|
-
indexOfView.set(active, { names: bones.map((b) => b.name), index });
|
|
1654
|
+
indexOfView.set(active, { names: bones.map((b) => b.name), parents: bones.map((b) => b.parent), index });
|
|
1596
1655
|
return index;
|
|
1597
1656
|
}
|
|
1598
1657
|
|
|
1658
|
+
/** Each bone's subtree as `subtreesOf` built it, kept per solver index (one object per view's bones, `boneIndex`). */
|
|
1659
|
+
const subtreesOfIndex = new WeakMap<ReadonlyMap<string, number>, ReadonlyArray<readonly number[]>>();
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* For the bone at each position of `state.bones`, the positions of the bone
|
|
1663
|
+
* and of every bone below it, ascending (issue #1179). Bones are parents
|
|
1664
|
+
* first, so each position is appended to its own list and its ancestors' in
|
|
1665
|
+
* ascending order. Built from the bones' parents once per solver index —
|
|
1666
|
+
* `boneIndex` hands back one index only while the names and the parents are
|
|
1667
|
+
* the ones it was built over, and the other two solver states (`previousPassSlotBones`,
|
|
1668
|
+
* `historyTaint`) build an index of their own.
|
|
1669
|
+
*/
|
|
1670
|
+
function subtreesOf(state: SolverState): ReadonlyArray<readonly number[]> {
|
|
1671
|
+
const kept = subtreesOfIndex.get(state.index);
|
|
1672
|
+
if (kept !== undefined) return kept;
|
|
1673
|
+
const lists: number[][] = state.bones.map(() => []);
|
|
1674
|
+
for (let k = 0; k < state.bones.length; k++) {
|
|
1675
|
+
let at: number | undefined = k;
|
|
1676
|
+
while (at !== undefined) {
|
|
1677
|
+
lists[at].push(k);
|
|
1678
|
+
const parent: string | undefined = state.bones[at].parent;
|
|
1679
|
+
at = parent === undefined ? undefined : state.index.get(parent);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
subtreesOfIndex.set(state.index, lists);
|
|
1683
|
+
return lists;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1599
1686
|
/** After a constraint: the bones it moved in world space read back into local values, and every bone below one it moved posed again (the header's update order). */
|
|
1600
1687
|
function repose(state: SolverState, changed: readonly string[], inWorld: readonly string[]): void {
|
|
1601
1688
|
for (const name of inWorld) {
|
|
@@ -1606,13 +1693,17 @@ function repose(state: SolverState, changed: readonly string[], inWorld: readonl
|
|
|
1606
1693
|
const moved = changed;
|
|
1607
1694
|
const below = new Set(changed);
|
|
1608
1695
|
const keep = inWorld;
|
|
1609
|
-
//
|
|
1610
|
-
|
|
1696
|
+
// A bone joins `below` only through its parent, so the bones that can are the moved bones' subtrees: the walk visits those, in bone order (issue
|
|
1697
|
+
// #1179), where it visited every bone from the first one moved (issue #1134) — the bones it no longer visits are the ones whose parent `below`
|
|
1698
|
+
// could never hold, so each bone it does visit reads the same `below`, and is posed again in the same order.
|
|
1699
|
+
const subtrees = subtreesOf(state);
|
|
1700
|
+
const lists: Array<readonly number[]> = [];
|
|
1611
1701
|
for (const name of changed) {
|
|
1612
1702
|
const at = state.index.get(name);
|
|
1613
|
-
if (at !== undefined
|
|
1703
|
+
if (at !== undefined) lists.push(subtrees[at]);
|
|
1614
1704
|
}
|
|
1615
|
-
|
|
1705
|
+
const visit = lists.length === 1 ? lists[0] : [...new Set(lists.flat())].sort((x, y) => x - y);
|
|
1706
|
+
for (const k of visit) {
|
|
1616
1707
|
const b = state.bones[k];
|
|
1617
1708
|
// Not through an inactive bone the constraint did not move itself (issue #979): the bones below it keep what the constraints wrote into them.
|
|
1618
1709
|
if (b.parent !== undefined && below.has(b.parent) && (!state.rules.inactiveHoldsItsWorld || state.active.has(b.parent) || moved.includes(b.parent))) below.add(b.name);
|
package/src/core/raw.ts
CHANGED
|
@@ -120,7 +120,7 @@ import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.
|
|
|
120
120
|
import { attachmentStates } from './deform.ts';
|
|
121
121
|
import { drawOrderAt } from './draw_order.ts';
|
|
122
122
|
import { eventsFired } from './events.ts';
|
|
123
|
-
import { REGION_TRIANGLES, REGION_UVS } from './clipping.ts';
|
|
123
|
+
import { REGION_TRIANGLES, REGION_UVS, type ShapeClipper } from './clipping.ts';
|
|
124
124
|
import { poseGeometry, type ShownGeometry } from './vertices.ts';
|
|
125
125
|
import { RUNTIME_DEG, type CoreWorld } from './world.ts';
|
|
126
126
|
import type { SliderApplication } from './constraints_slider.ts';
|
|
@@ -240,6 +240,16 @@ function numberOf(v: number | null, what: string, mode: WalkMode): number {
|
|
|
240
240
|
return mode.keep ? (v === null ? Number.NaN : v) : finite(v, what);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* What the looping walk cuts a drawn attachment through under a clip: nothing
|
|
245
|
+
* (issue #1179). Its one reader, A10, reads no clipped row — `./walk.ts`
|
|
246
|
+
* narrows them away with the events — so the walk does not cut them; what it
|
|
247
|
+
* still does is plan every clip it starts (`poseClipped`'s `clipShapeOf`), so
|
|
248
|
+
* a clip the core does not draw refuses the walk by name as before. Every
|
|
249
|
+
* number a looping pose carries is computed as it was.
|
|
250
|
+
*/
|
|
251
|
+
const NO_CLIPPED_ROWS: ShapeClipper = () => null;
|
|
252
|
+
|
|
243
253
|
/** The bones of a pose from the world transforms (the header's `bones`). */
|
|
244
254
|
function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>): RawBone[] {
|
|
245
255
|
const active = activeBones(doc);
|
|
@@ -266,7 +276,8 @@ function rawBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>):
|
|
|
266
276
|
|
|
267
277
|
/** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
|
|
268
278
|
function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
|
|
269
|
-
const
|
|
279
|
+
const draw = drawWalkOf(doc, order, plant);
|
|
280
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
270
281
|
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
271
282
|
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
272
283
|
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
@@ -399,7 +410,8 @@ export function walkIn(doc: CompiledDocument, animation: string, steps: readonly
|
|
|
399
410
|
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
400
411
|
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
401
412
|
const drawn = rawDrawn(doc, shown, posed.world, drawOrder, plant, mode);
|
|
402
|
-
// A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none.
|
|
413
|
+
// A looping walk fires no event here: what fires across the wrap was not measured, and the walk's one reader (A10) reads none. Nor does it cut its
|
|
414
|
+
// drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its `clipped` rows are empty.
|
|
403
415
|
const events = mode.loop ? [] : (plant.events ?? eventsFired)(anim.timelines.events, last, t, rawNumber).map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
|
|
404
416
|
last = t;
|
|
405
417
|
poses.push({ trackTime, animationTime: t, bones: rawBones(doc, posed.world), slots: slots.rows, drawOrder, ...drawn, events, shown });
|
package/src/core/walk.ts
CHANGED
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
* NaN and Infinity stood where spine-core's did.
|
|
59
59
|
*
|
|
60
60
|
* Events are not carried: A10 reads none, and what a looping track fires
|
|
61
|
-
* across the wrap was not measured.
|
|
61
|
+
* across the wrap was not measured. Nor are the clipped rows, and the walk
|
|
62
|
+
* does not cut them (issue #1179): every clip it starts is still planned, so
|
|
63
|
+
* a clip the core does not draw refuses the walk by name, as below.
|
|
62
64
|
*
|
|
63
65
|
* ⛔ **What is still refused** is the raw entry's other refusal: a construct
|
|
64
66
|
* the core would leave out (a block the oracle's core dump names absent) is a
|