spine-rigc 2.54.0 → 2.56.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/README.md +1 -1
- package/docs/AUTHORING.md +590 -30
- package/package.json +1 -1
- package/src/anglerange.ts +190 -0
- package/src/assertions/bodies/a23.ts +80 -5
- package/src/cli/core_commands.ts +236 -14
- package/src/cli/shared.ts +53 -8
- package/src/compile.ts +45 -6
- package/src/core/entry.ts +6 -1
- package/src/deformgen.ts +333 -2
- package/src/motion.ts +20 -8
- package/src/passthrough.ts +575 -0
- package/src/render_shared.ts +30 -0
- package/src/rig.ts +95 -3
- package/src/stop.ts +472 -0
- package/src/timelines.ts +75 -29
- package/src/transform.ts +20 -0
- package/src/types.ts +4 -3
- package/src/viewportpin.ts +168 -0
- package/src/whirl.ts +580 -0
package/src/rig.ts
CHANGED
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
* - `invariants` — the structural facts skeleton JSON cannot state about itself,
|
|
33
33
|
* which the validator's archetype assertions read. Nothing in the file says
|
|
34
34
|
* "this bone carries the cut's axis" or "this parentage is forbidden".
|
|
35
|
+
* - `stops` — a swinging chain held off a leg, one declaration for the network
|
|
36
|
+
* of bones and constraints it stands for (`RigStop`), expanded into plain
|
|
37
|
+
* bones and constraints before anything else reads the rig.
|
|
35
38
|
*
|
|
36
39
|
* ## What a field's PRESENCE means
|
|
37
40
|
*
|
|
@@ -61,6 +64,7 @@
|
|
|
61
64
|
*/
|
|
62
65
|
import { ikShapeFault } from './assertions/bodies/a47.ts';
|
|
63
66
|
import { CompileError, NotImplementedError } from './errors.ts';
|
|
67
|
+
import { expandStops } from './stop.ts';
|
|
64
68
|
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
65
69
|
import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
|
|
66
70
|
|
|
@@ -1887,6 +1891,69 @@ export interface RigDeformFoldExemption {
|
|
|
1887
1891
|
why: string;
|
|
1888
1892
|
}
|
|
1889
1893
|
|
|
1894
|
+
// ---------------------------------------------------------------------------
|
|
1895
|
+
// stops — a swinging chain held off a leg (issue #1413)
|
|
1896
|
+
// ---------------------------------------------------------------------------
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* One chain held off a leg: the network [`src/stop.ts`](stop.ts) expands it to,
|
|
1900
|
+
* written once instead of by hand. Spine has no collider and no angle limit, so
|
|
1901
|
+
* a physics-driven skirt chain swings its hem through the thigh it hangs in
|
|
1902
|
+
* front of; this holds the chain's DRAWN links — the bones its mesh is
|
|
1903
|
+
* weighted to — so they may swing away from the leg and not toward it past
|
|
1904
|
+
* their rest angle.
|
|
1905
|
+
*
|
|
1906
|
+
* It expands, before anything else reads the rig, to exactly:
|
|
1907
|
+
*
|
|
1908
|
+
* - `<chain>_touch` — a point bone, child of `against`, at `at`;
|
|
1909
|
+
* - `<chain>_stop` — a frame hung where the first link hangs (its parent,
|
|
1910
|
+
* `x`, `y` and `length`), aimed at the point by the ik constraint
|
|
1911
|
+
* `<chain>_stop_aim`;
|
|
1912
|
+
* - `<link>_rel` — a child of the frame that the transform constraint
|
|
1913
|
+
* `<link>_rel_copy` gives the first link's world rotation;
|
|
1914
|
+
* - `<link>_d` — the first link's drawn link, a child of the frame, which the
|
|
1915
|
+
* transform constraint `<link>_clamp` gives `_rel`'s turn in the frame,
|
|
1916
|
+
* clamped to the range;
|
|
1917
|
+
* - for each link below the first, `<link>_d` — its drawn link, a sibling
|
|
1918
|
+
* with its stated transform — and `<link>_clamp`, which gives it the link's
|
|
1919
|
+
* own turn, clamped to `lower`.
|
|
1920
|
+
*
|
|
1921
|
+
* So a chain's next link hangs from the drawn link above it: author the chain
|
|
1922
|
+
* with each link's parent below `<previous link>_d`, and weight the mesh to the
|
|
1923
|
+
* `_d` bones. What the expansion derives, it derives from the setup pose this
|
|
1924
|
+
* file states (`src/stop.ts` lists it); nothing is measured off the art.
|
|
1925
|
+
*/
|
|
1926
|
+
export interface RigStop {
|
|
1927
|
+
/** The name the point, the frame and the aim are called after. */
|
|
1928
|
+
chain: string;
|
|
1929
|
+
/**
|
|
1930
|
+
* The chain's swinging links, root first: each one written by a constraint
|
|
1931
|
+
* in `constraints` (its swing), each below the first hanging from the drawn
|
|
1932
|
+
* link of the one above. Resolved by name.
|
|
1933
|
+
*/
|
|
1934
|
+
links: string[];
|
|
1935
|
+
/** The bone the point rides on — the leg. Resolved by name. */
|
|
1936
|
+
against: string;
|
|
1937
|
+
/** The point, `[x, y]` in `against`'s local space. Required: the compiler places no point. */
|
|
1938
|
+
at: [number, number];
|
|
1939
|
+
/**
|
|
1940
|
+
* The first drawn link's range, as its two ends: degrees from its rest angle
|
|
1941
|
+
* to the aim line, counted positive AWAY from the point. `inward: 0,
|
|
1942
|
+
* outward: 60` — never past rest toward the leg, up to 60° away from it.
|
|
1943
|
+
* Both required, `inward` not above `outward`.
|
|
1944
|
+
*/
|
|
1945
|
+
inward: number;
|
|
1946
|
+
outward: number;
|
|
1947
|
+
/** Each lower drawn link's range off its own rest bend, the same way round. Required when `links` has more than one. */
|
|
1948
|
+
lower?: RigStopLower;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/** A lower link's range — `RigStop.lower`. */
|
|
1952
|
+
export interface RigStopLower {
|
|
1953
|
+
inward: number;
|
|
1954
|
+
outward: number;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1890
1957
|
// ---------------------------------------------------------------------------
|
|
1891
1958
|
// the file
|
|
1892
1959
|
// ---------------------------------------------------------------------------
|
|
@@ -1932,6 +1999,12 @@ export interface RigSpec {
|
|
|
1932
1999
|
*/
|
|
1933
2000
|
events?: Record<string, RigEvent>;
|
|
1934
2001
|
invariants?: RigInvariants;
|
|
2002
|
+
/**
|
|
2003
|
+
* Chains held off a leg — see `RigStop`. Expanded by `parseRigSpec` into
|
|
2004
|
+
* bones and constraints before anything else reads them; kept as written,
|
|
2005
|
+
* and emitted nowhere.
|
|
2006
|
+
*/
|
|
2007
|
+
stops?: RigStop[];
|
|
1935
2008
|
}
|
|
1936
2009
|
|
|
1937
2010
|
// ---------------------------------------------------------------------------
|
|
@@ -1960,7 +2033,9 @@ function isObj(v: unknown): v is Record<string, unknown> {
|
|
|
1960
2033
|
* fault.
|
|
1961
2034
|
*/
|
|
1962
2035
|
export const RIG_KEYS = {
|
|
1963
|
-
RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants'],
|
|
2036
|
+
RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants', 'stops'],
|
|
2037
|
+
RigStop: ['chain', 'links', 'against', 'at', 'inward', 'outward', 'lower'],
|
|
2038
|
+
RigStopLower: ['inward', 'outward'],
|
|
1964
2039
|
RigSkeletonHeader: ['x', 'y', 'width', 'height', 'fps', 'referenceScale', 'images', 'audio', 'stageBox'],
|
|
1965
2040
|
RigStageBox: ['slot', 'attachment'],
|
|
1966
2041
|
RigBone: ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inherit', 'skin', 'color', 'icon', 'from'],
|
|
@@ -2060,8 +2135,10 @@ type RigTypeTable = {
|
|
|
2060
2135
|
export const RIG_TYPES = {
|
|
2061
2136
|
RigSpec: {
|
|
2062
2137
|
spec: 'enum', name: 'string', note: 'string', skeleton: 'object', images: 'string', bones: 'object[]', slots: 'object[]',
|
|
2063
|
-
skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object',
|
|
2138
|
+
skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object', stops: 'object[]',
|
|
2064
2139
|
},
|
|
2140
|
+
RigStop: { chain: 'string', links: 'string[]', against: 'string', at: 'number[]', inward: 'number', outward: 'number', lower: 'object' },
|
|
2141
|
+
RigStopLower: { inward: 'number', outward: 'number' },
|
|
2065
2142
|
RigSkeletonHeader: {
|
|
2066
2143
|
x: 'number', y: 'number', width: 'number | null', height: 'number | null', fps: 'number', referenceScale: 'number',
|
|
2067
2144
|
images: 'string', audio: 'string | null', stageBox: 'object',
|
|
@@ -2405,6 +2482,12 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2405
2482
|
at(event, 'RigEvent', `event "${name}"`, ['events', name]);
|
|
2406
2483
|
}
|
|
2407
2484
|
|
|
2485
|
+
for (const [i, stop] of (Array.isArray(raw.stops) ? raw.stops : []).entries()) {
|
|
2486
|
+
const who = isObj(stop) && typeof stop.chain === 'string' ? `stop "${stop.chain}"` : `stops[${i}]`;
|
|
2487
|
+
at(stop, 'RigStop', who, ['stops', i]);
|
|
2488
|
+
if (isObj(stop)) at(stop.lower, 'RigStopLower', `${who}'s "lower"`, ['stops', i, 'lower']);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2408
2491
|
if (isObj(raw.invariants)) {
|
|
2409
2492
|
at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
|
|
2410
2493
|
for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
|
|
@@ -2528,6 +2611,11 @@ function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | n
|
|
|
2528
2611
|
if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
|
|
2529
2612
|
if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
|
|
2530
2613
|
if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
|
|
2614
|
+
if (head === 'stops') {
|
|
2615
|
+
const node = Array.isArray(raw.stops) ? raw.stops[second] : undefined;
|
|
2616
|
+
const who = isObj(node) && typeof node.chain === 'string' ? `stop ${JSON.stringify(node.chain)}` : `stops[${second}]`;
|
|
2617
|
+
return `${who}${rest(path.slice(2))}`;
|
|
2618
|
+
}
|
|
2531
2619
|
}
|
|
2532
2620
|
if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
|
|
2533
2621
|
if (head === 'skins' && typeof second === 'string') {
|
|
@@ -2609,7 +2697,11 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2609
2697
|
throw new CompileError(`${where}: a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)`);
|
|
2610
2698
|
}
|
|
2611
2699
|
|
|
2612
|
-
|
|
2700
|
+
// `stops` first, ahead of every name this function resolves: a chain's next
|
|
2701
|
+
// link hangs from the drawn link a stop generates, so the bones below are
|
|
2702
|
+
// read as the expansion lays them out — and every reader after this one sees
|
|
2703
|
+
// ordinary bones and constraints (`src/stop.ts`).
|
|
2704
|
+
const spec = expandStops(raw as unknown as RigSpec, where);
|
|
2613
2705
|
|
|
2614
2706
|
// The stage, stated or stated absent. Half a statement is refused here rather
|
|
2615
2707
|
// than resolved in `compile`, because which half was meant is not derivable
|
package/src/stop.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stops` — a swinging chain held off a leg (issue #1413).
|
|
3
|
+
*
|
|
4
|
+
* Spine has no collider and no angle limit — a physics constraint's `limit` is
|
|
5
|
+
* a velocity cap — so a skirt chain driven by physics swings its hem straight
|
|
6
|
+
* through the thigh it hangs in front of. The working stand-in, tuned by hand on
|
|
7
|
+
* a dancer's pleated skirt, is a small network per chain: a point on the leg, a
|
|
8
|
+
* frame that aims at it from the chain's root, the swinging link's rotation
|
|
9
|
+
* copied onto that frame, and the DRAWN link — the one the mesh is weighted to —
|
|
10
|
+
* held by a transform clamp, so it may swing away from the leg and not past its
|
|
11
|
+
* rest angle toward it. One declaration here expands to exactly that network,
|
|
12
|
+
* and to nothing else: what it writes is what an author would write by hand,
|
|
13
|
+
* bone for bone and constraint for constraint, which `LG01` holds byte for byte.
|
|
14
|
+
*
|
|
15
|
+
* ⭐ **The expansion happens before anything else reads the rig.** `parseRigSpec`
|
|
16
|
+
* calls `expandStops` ahead of its own name resolution, so every reader after it
|
|
17
|
+
* — the parse's own checks, `compile`, the gate — sees ordinary bones and
|
|
18
|
+
* constraints and nothing that knows a stop was there. A stop therefore cannot
|
|
19
|
+
* buy an exemption, a default or a code path of its own: it is spelling.
|
|
20
|
+
*
|
|
21
|
+
* Every position and angle is the declaration's, or derived from it and the
|
|
22
|
+
* setup pose the rig spec states — no value is measured off the art and none is
|
|
23
|
+
* defaulted. What is derived, and from what:
|
|
24
|
+
*
|
|
25
|
+
* - the point bone's `x`/`y` are `at`, in `against`'s local space, as written;
|
|
26
|
+
* - the frame hangs where the first link hangs — its parent, `x`, `y` and
|
|
27
|
+
* `length` are the link's — and its `rotation` points it at the point;
|
|
28
|
+
* - the drawn link's rest rotation in the frame is the angle from the aim line
|
|
29
|
+
* to the link, so at rest the drawn link lies on the link;
|
|
30
|
+
* - which way is **inward** is the way from the link toward the aim line: the
|
|
31
|
+
* point is on the leg, so turning toward it is turning into the leg;
|
|
32
|
+
* - the clamp's two bounds are that rest angle moved by `inward` and `outward`.
|
|
33
|
+
*
|
|
34
|
+
* The angles are read in the exact frame (`computeExactFrameTransforms`): they
|
|
35
|
+
* are statements about the rig as authored, the rule `src/transform.ts` states
|
|
36
|
+
* for what is measured rather than bound.
|
|
37
|
+
*/
|
|
38
|
+
import { CompileError } from './errors.ts';
|
|
39
|
+
import type { RigBone, RigConstraint, RigIkConstraint, RigSpec, RigStop, RigTransformConstraint } from './rig.ts';
|
|
40
|
+
import {
|
|
41
|
+
computeExactFrameTransforms,
|
|
42
|
+
directionDegrees,
|
|
43
|
+
localDirectionDegrees,
|
|
44
|
+
toWorld,
|
|
45
|
+
type BoneTransform,
|
|
46
|
+
} from './transform.ts';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A planted wrong expansion, for the selftest's mutants (`LG` controls) — each
|
|
50
|
+
* a mistake a hand-built network could carry and still build green.
|
|
51
|
+
*
|
|
52
|
+
* - `swapRange` — inward taken the other way round, so the clamp holds the
|
|
53
|
+
* link off the side it should swing to and lets it into the leg;
|
|
54
|
+
* - `aimAtAgainst` — the frame aimed at the `against` bone's origin rather
|
|
55
|
+
* than at the point on it;
|
|
56
|
+
* - `appendConstraints` — every generated constraint at the end of the
|
|
57
|
+
* array, after the physics of the links below, which then swing from a
|
|
58
|
+
* drawn link not yet clamped.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export interface StopPlant {
|
|
63
|
+
swapRange?: boolean;
|
|
64
|
+
aimAtAgainst?: boolean;
|
|
65
|
+
appendConstraints?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The names one stop generates, all derived from its `chain` and its links' names. */
|
|
69
|
+
export interface StopNames {
|
|
70
|
+
/** `<chain>_touch` — the point, a child of `against`. */
|
|
71
|
+
point: string;
|
|
72
|
+
/** `<chain>_stop` — the frame, aimed at the point. */
|
|
73
|
+
frame: string;
|
|
74
|
+
/** `<chain>_stop_aim` — the ik constraint that aims it. */
|
|
75
|
+
aim: string;
|
|
76
|
+
/** `<first link>_rel` — the first link's rotation, copied onto the frame. */
|
|
77
|
+
rel: string;
|
|
78
|
+
/** `<first link>_rel_copy` — the transform constraint that copies it. */
|
|
79
|
+
copy: string;
|
|
80
|
+
/** `<link>_d` per link — the drawn link. */
|
|
81
|
+
drawn: string[];
|
|
82
|
+
/** `<link>_clamp` per link — the transform constraint that holds it. */
|
|
83
|
+
clamp: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The generated names of a stop, in one place so the refusals and the expansion cannot spell them apart. */
|
|
87
|
+
export function stopNames(chain: string, links: readonly string[]): StopNames {
|
|
88
|
+
return {
|
|
89
|
+
point: `${chain}_touch`,
|
|
90
|
+
frame: `${chain}_stop`,
|
|
91
|
+
aim: `${chain}_stop_aim`,
|
|
92
|
+
rel: `${links[0]}_rel`,
|
|
93
|
+
copy: `${links[0]}_rel_copy`,
|
|
94
|
+
drawn: links.map((l) => `${l}_d`),
|
|
95
|
+
clamp: links.map((l) => `${l}_clamp`),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
100
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One stop, read and checked: every field present, every name resolved. */
|
|
104
|
+
interface ReadStop {
|
|
105
|
+
who: string;
|
|
106
|
+
chain: string;
|
|
107
|
+
links: string[];
|
|
108
|
+
against: string;
|
|
109
|
+
at: [number, number];
|
|
110
|
+
inward: number;
|
|
111
|
+
outward: number;
|
|
112
|
+
lower: { inward: number; outward: number } | null;
|
|
113
|
+
names: StopNames;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The transform fields a drawn link copies from its link (the frame copies `x`, `y` and `length` only). */
|
|
117
|
+
const TWIN_FIELDS = ['length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inherit'] as const;
|
|
118
|
+
|
|
119
|
+
/** A degree bound a clamp can reach: a local rotation read off a world copy is in (-180, 180]. */
|
|
120
|
+
const BOUND = 180;
|
|
121
|
+
|
|
122
|
+
/** An angle in (-540, 540) brought into (-180, 180] by at most one whole turn. */
|
|
123
|
+
function halfTurn(deg: number): number {
|
|
124
|
+
if (deg > 180) return deg - 360;
|
|
125
|
+
if (deg <= -180) return deg + 360;
|
|
126
|
+
return deg;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The rig spec with every `stops` entry expanded into the bones and constraints
|
|
131
|
+
* it stands for, or the spec itself when it declares no `stops`.
|
|
132
|
+
*
|
|
133
|
+
* Bones: the frame immediately before the first link; the first link's `_rel`
|
|
134
|
+
* and `_d` immediately after it; each lower link's `_d` immediately after that
|
|
135
|
+
* link; the point after every other bone. Constraints: the aim immediately
|
|
136
|
+
* before the last constraint that writes the first link, the copy and the first
|
|
137
|
+
* clamp immediately after it, and each lower clamp immediately after the last
|
|
138
|
+
* constraint that writes its link — so the frame is aimed before the link
|
|
139
|
+
* swings, and every swing is copied and clamped before anything below it reads
|
|
140
|
+
* the drawn link.
|
|
141
|
+
*
|
|
142
|
+
* The returned spec keeps `stops` as written, so `compile` can refuse a link a
|
|
143
|
+
* motion-spec physics table also drives (that table is emitted after every rig
|
|
144
|
+
* constraint, past the clamps); nothing emits it.
|
|
145
|
+
*/
|
|
146
|
+
export function expandStops(spec: RigSpec, where: string, plant: StopPlant = {}): RigSpec {
|
|
147
|
+
const declared: unknown = spec.stops;
|
|
148
|
+
if (declared === undefined) return spec;
|
|
149
|
+
if (!Array.isArray(declared)) {
|
|
150
|
+
throw new CompileError(`${where}: "stops" is ${JSON.stringify(declared)}; it is an array of { "chain", "links", "against", "at", "inward", "outward", "lower"? }`);
|
|
151
|
+
}
|
|
152
|
+
const authored = new Map<string, RigBone>();
|
|
153
|
+
for (const bone of spec.bones) if (isObj(bone) && typeof bone.name === 'string') authored.set(bone.name, bone);
|
|
154
|
+
const constraints = spec.constraints ?? [];
|
|
155
|
+
|
|
156
|
+
// -- 1. every field, every name ----------------------------------------------
|
|
157
|
+
const stops: ReadStop[] = [];
|
|
158
|
+
const stoppedBy = new Map<string, string>();
|
|
159
|
+
for (const [i, entry] of declared.entries()) {
|
|
160
|
+
if (!isObj(entry)) throw new CompileError(`${where}: stops[${i}] is ${JSON.stringify(entry)}; a stop is an object`);
|
|
161
|
+
const chain = entry.chain;
|
|
162
|
+
if (typeof chain !== 'string' || chain.length === 0) {
|
|
163
|
+
throw new CompileError(`${where}: stops[${i}] needs a "chain" — the name its point, frame and aim are called after ("<chain>_touch", "<chain>_stop", "<chain>_stop_aim")`);
|
|
164
|
+
}
|
|
165
|
+
const who = `${where}: stop "${chain}"`;
|
|
166
|
+
const links: unknown = entry.links;
|
|
167
|
+
if (!Array.isArray(links) || links.length === 0 || !links.every((l): l is string => typeof l === 'string')) {
|
|
168
|
+
throw new CompileError(`${who} states "links" ${JSON.stringify(links) ?? 'absent'}; it is the chain's swinging links, root first — a non-empty array of bone names`);
|
|
169
|
+
}
|
|
170
|
+
for (const [k, link] of links.entries()) {
|
|
171
|
+
const bone = authored.get(link);
|
|
172
|
+
if (bone === undefined) throw new CompileError(`${who}: links[${k}] names bone "${link}", which this rig does not declare`);
|
|
173
|
+
const other = stoppedBy.get(link);
|
|
174
|
+
if (other !== undefined) {
|
|
175
|
+
throw new CompileError(
|
|
176
|
+
`${who}: links[${k}] "${link}" is already stopped by stop "${other}" — one drawn link "${link}_d" and one clamp "${link}_clamp" per link, so a link is stopped once`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
stoppedBy.set(link, chain);
|
|
180
|
+
if (bone.from !== undefined) {
|
|
181
|
+
throw new CompileError(`${who}: links[${k}] "${link}" takes its place from the manifest ("from"), and a stop copies the link's stated x, y and rotation onto its drawn link — state them`);
|
|
182
|
+
}
|
|
183
|
+
if (bone.parent === undefined) {
|
|
184
|
+
throw new CompileError(`${who}: links[${k}] "${link}" is the skeleton's root, and a stop hangs its frame and its drawn link where the link hangs — a link has a parent`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const against = entry.against;
|
|
188
|
+
if (typeof against !== 'string' || against.length === 0) {
|
|
189
|
+
throw new CompileError(`${who} needs "against" — the bone the point rides on, the leg the chain is held off`);
|
|
190
|
+
}
|
|
191
|
+
if (!authored.has(against)) throw new CompileError(`${who}: "against" names bone "${against}", which this rig does not declare`);
|
|
192
|
+
const at: unknown = entry.at;
|
|
193
|
+
if (at === undefined) {
|
|
194
|
+
throw new CompileError(`${who} states no "at" — the point on "${against}" the frame aims at, [x, y] in "${against}"'s local space; the compiler places no point of its own`);
|
|
195
|
+
}
|
|
196
|
+
if (!Array.isArray(at) || at.length !== 2 || !at.every((v): v is number => typeof v === 'number')) {
|
|
197
|
+
throw new CompileError(`${who} states "at" ${JSON.stringify(at)}; it is [x, y], two numbers in "${against}"'s local space`);
|
|
198
|
+
}
|
|
199
|
+
const range = (node: Record<string, unknown>, field: string): { inward: number; outward: number } => {
|
|
200
|
+
const out: Record<'inward' | 'outward', number> = { inward: 0, outward: 0 };
|
|
201
|
+
for (const end of ['inward', 'outward'] as const) {
|
|
202
|
+
const v = node[end];
|
|
203
|
+
if (typeof v !== 'number') {
|
|
204
|
+
throw new CompileError(
|
|
205
|
+
`${who} states no "${field}${end}" — degrees from the link's rest angle, counted positive away from "${against}"; the range is [inward, outward], and the compiler states neither end for you`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
out[end] = v;
|
|
209
|
+
}
|
|
210
|
+
if (out.inward > out.outward) {
|
|
211
|
+
throw new CompileError(
|
|
212
|
+
`${who}: "${field}inward" ${out.inward} is greater than "${field}outward" ${out.outward} — the range is [inward, outward] in degrees counted positive away from "${against}", and this one holds no angle`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
};
|
|
217
|
+
const first = range(entry, '');
|
|
218
|
+
const lowerRaw = entry.lower;
|
|
219
|
+
let lower: { inward: number; outward: number } | null = null;
|
|
220
|
+
if (links.length > 1) {
|
|
221
|
+
if (!isObj(lowerRaw)) {
|
|
222
|
+
throw new CompileError(`${who} states no "lower" — the range [inward, outward] each link below the first may turn through off its own rest bend; the chain has ${links.length - 1} of them`);
|
|
223
|
+
}
|
|
224
|
+
lower = range(lowerRaw, 'lower.');
|
|
225
|
+
} else if (lowerRaw !== undefined) {
|
|
226
|
+
throw new CompileError(`${who} states "lower" and its chain has one link — there is no link below the first for it to hold`);
|
|
227
|
+
}
|
|
228
|
+
stops.push({ who, chain, links, against, at: [at[0], at[1]], inward: first.inward, outward: first.outward, lower, names: stopNames(chain, links) });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// -- 2. the generated names are new -------------------------------------------
|
|
232
|
+
const generatedBones = new Map<string, string>();
|
|
233
|
+
const generatedConstraints = new Map<string, string>();
|
|
234
|
+
const authoredConstraints = new Set(constraints.filter(isObj).map((c) => `${String(c.type)} ${String(c.name)}`));
|
|
235
|
+
for (const stop of stops) {
|
|
236
|
+
const { names } = stop;
|
|
237
|
+
const lowerDrawn = names.drawn.slice(1);
|
|
238
|
+
for (const name of [names.frame, names.rel, names.drawn[0], ...lowerDrawn, names.point]) {
|
|
239
|
+
if (authored.has(name)) throw new CompileError(`${stop.who} generates bone "${name}", which this rig already declares — rename the bone, or the stop's "chain"`);
|
|
240
|
+
const by = generatedBones.get(name);
|
|
241
|
+
if (by !== undefined) throw new CompileError(`${stop.who} generates bone "${name}", which stop "${by}" generates too — rename one "chain"`);
|
|
242
|
+
generatedBones.set(name, stop.chain);
|
|
243
|
+
}
|
|
244
|
+
for (const [type, name] of [['ik', names.aim], ['transform', names.copy], ...names.clamp.map((c) => ['transform', c] as const)] as const) {
|
|
245
|
+
const key = `${type} ${name}`;
|
|
246
|
+
if (authoredConstraints.has(key)) throw new CompileError(`${stop.who} generates ${type} constraint "${name}", which this rig already declares — rename the constraint, or the stop's "chain"`);
|
|
247
|
+
const by = generatedConstraints.get(key);
|
|
248
|
+
if (by !== undefined) throw new CompileError(`${stop.who} generates ${type} constraint "${name}", which stop "${by}" generates too — rename one "chain"`);
|
|
249
|
+
generatedConstraints.set(key, stop.chain);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -- 3. what swings each link ---------------------------------------------------
|
|
254
|
+
const writes = (c: unknown, bone: string): boolean => {
|
|
255
|
+
if (!isObj(c)) return false;
|
|
256
|
+
if (c.type === 'physics') return c.bone === bone;
|
|
257
|
+
if (c.type === 'ik' || c.type === 'transform' || c.type === 'path') return Array.isArray(c.bones) && c.bones.includes(bone);
|
|
258
|
+
return false;
|
|
259
|
+
};
|
|
260
|
+
const lastWriter = (stop: ReadStop, k: number): number => {
|
|
261
|
+
const link = stop.links[k];
|
|
262
|
+
let at = -1;
|
|
263
|
+
constraints.forEach((c, i) => {
|
|
264
|
+
if (writes(c, link)) at = i;
|
|
265
|
+
});
|
|
266
|
+
if (at < 0) {
|
|
267
|
+
throw new CompileError(
|
|
268
|
+
`${stop.who}: links[${k}] "${link}" is written by no constraint of this rig spec — a stop holds a link something swings (a physics, ik, transform or path constraint in "constraints"), and its copy and clamp run right after that. A physics constraint in the motion spec's table is emitted after every rig constraint, past the clamp; declare it here`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return at;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// -- 4. the tree the expansion makes, by name ----------------------------------
|
|
275
|
+
const parentOf = new Map<string, string | undefined>();
|
|
276
|
+
for (const [name, bone] of authored) parentOf.set(name, bone.parent);
|
|
277
|
+
for (const stop of stops) {
|
|
278
|
+
const { names, links } = stop;
|
|
279
|
+
parentOf.set(names.frame, authored.get(links[0])?.parent);
|
|
280
|
+
parentOf.set(names.rel, names.frame);
|
|
281
|
+
parentOf.set(names.drawn[0], names.frame);
|
|
282
|
+
for (let k = 1; k < links.length; k++) parentOf.set(names.drawn[k], authored.get(links[k])?.parent);
|
|
283
|
+
parentOf.set(names.point, stop.against);
|
|
284
|
+
}
|
|
285
|
+
const ancestry = (name: string): string[] => {
|
|
286
|
+
const out: string[] = [];
|
|
287
|
+
const seen = new Set<string>();
|
|
288
|
+
for (let at = parentOf.get(name); at !== undefined && !seen.has(at); at = parentOf.get(at)) {
|
|
289
|
+
seen.add(at);
|
|
290
|
+
out.push(at);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
for (const stop of stops) {
|
|
295
|
+
const { names, links } = stop;
|
|
296
|
+
for (let k = 1; k < links.length; k++) {
|
|
297
|
+
if (!ancestry(links[k]).includes(names.drawn[k - 1])) {
|
|
298
|
+
throw new CompileError(
|
|
299
|
+
`${stop.who}: links[${k}] "${links[k]}" does not hang below "${names.drawn[k - 1]}", the drawn link of links[${k - 1}] — a chain's next link hangs from the drawn link above it, so the clamp that holds that one carries this one; parent it there`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const held = new Set([names.frame, names.rel, ...names.drawn, ...links]);
|
|
304
|
+
const under = [stop.against, ...ancestry(stop.against)].find((b) => held.has(b));
|
|
305
|
+
if (under !== undefined) {
|
|
306
|
+
throw new CompileError(`${stop.who}: "against" "${stop.against}" hangs below "${under}", which the stop moves — the point would swing with the chain it holds off; name the leg`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// -- 5. the angles, off the setup pose as stated -------------------------------
|
|
311
|
+
const generated = new Map<string, RigBone>();
|
|
312
|
+
const known = (name: string): RigBone | undefined => authored.get(name) ?? generated.get(name);
|
|
313
|
+
const before = new Map<number, RigConstraint[]>();
|
|
314
|
+
const after = new Map<number, RigConstraint[]>();
|
|
315
|
+
const appended: RigConstraint[] = [];
|
|
316
|
+
const queue = (map: Map<number, RigConstraint[]>, at: number, c: RigConstraint): void => {
|
|
317
|
+
if (plant.appendConstraints) appended.push(c);
|
|
318
|
+
else map.set(at, [...(map.get(at) ?? []), c]);
|
|
319
|
+
};
|
|
320
|
+
const conformal = (m: BoneTransform): boolean => {
|
|
321
|
+
const scale = Math.max(Math.abs(m.a), Math.abs(m.b), Math.abs(m.c), Math.abs(m.d));
|
|
322
|
+
return m.a * m.d - m.b * m.c > 0 && Math.abs(m.a - m.d) <= 1e-9 * scale && Math.abs(m.b + m.c) <= 1e-9 * scale;
|
|
323
|
+
};
|
|
324
|
+
const clampOf = (name: string, source: string, target: string, lo: number, hi: number): RigTransformConstraint => ({
|
|
325
|
+
name,
|
|
326
|
+
type: 'transform',
|
|
327
|
+
source,
|
|
328
|
+
bones: [target],
|
|
329
|
+
localSource: true,
|
|
330
|
+
localTarget: true,
|
|
331
|
+
clamp: true,
|
|
332
|
+
properties: { rotate: { offset: lo, to: { rotate: { offset: lo, max: hi, scale: 1 } } } },
|
|
333
|
+
mixRotate: 1,
|
|
334
|
+
});
|
|
335
|
+
const bounds = (stop: ReadStop, rest: number, sign: number, range: { inward: number; outward: number }, field: string, what: string): [number, number] => {
|
|
336
|
+
const a = rest - sign * range.outward;
|
|
337
|
+
const b = rest - sign * range.inward;
|
|
338
|
+
for (const [end, value] of [['outward', a], ['inward', b]] as const) {
|
|
339
|
+
if (Math.abs(value) > BOUND) {
|
|
340
|
+
throw new CompileError(
|
|
341
|
+
`${stop.who}: "${field}${end}" ${range[end]} puts ${what}'s bound at ${value}°, past ±${BOUND}° — the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return [Math.min(a, b), Math.max(a, b)];
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (const stop of stops) {
|
|
349
|
+
const { names, links, who } = stop;
|
|
350
|
+
const link = authored.get(links[0]) as RigBone;
|
|
351
|
+
const parent = link.parent as string;
|
|
352
|
+
// The bones the angles are read through, root first: the first link's
|
|
353
|
+
// ancestry and the leg's, each parent before its child.
|
|
354
|
+
const chain: RigBone[] = [];
|
|
355
|
+
const listed = new Set<string>();
|
|
356
|
+
for (const tip of [links[0], stop.against]) {
|
|
357
|
+
for (const name of [...ancestry(tip).reverse(), tip]) {
|
|
358
|
+
if (listed.has(name)) continue;
|
|
359
|
+
const bone = known(name);
|
|
360
|
+
if (bone === undefined) {
|
|
361
|
+
throw new CompileError(`${who}: "${tip}" hangs below "${name}", which a later stop generates — declare that stop first, so its angles are known when this one is read`);
|
|
362
|
+
}
|
|
363
|
+
if (bone.from !== undefined) {
|
|
364
|
+
throw new CompileError(`${who}: "${name}" takes its place from the manifest ("from"), and a stop's angles are read off the setup pose this rig spec states — state its x, y and rotation`);
|
|
365
|
+
}
|
|
366
|
+
listed.add(name);
|
|
367
|
+
chain.push(bone);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const frames = computeExactFrameTransforms(chain);
|
|
371
|
+
const parentFrame = frames.get(parent) as BoneTransform;
|
|
372
|
+
if (!conformal(parentFrame)) {
|
|
373
|
+
throw new CompileError(
|
|
374
|
+
`${who}: "${parent}", where the frame hangs, is scaled unevenly, sheared or reflected at the setup pose — the frame's angles add to its parent's only through a rotation and an even scale, and inward is a turn's sense`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
for (let k = 1; k < links.length; k++) {
|
|
378
|
+
for (let at = authored.get(links[k])?.parent; at !== undefined && at !== names.drawn[k - 1]; at = parentOf.get(at)) {
|
|
379
|
+
const bone = authored.get(at);
|
|
380
|
+
if (bone === undefined) break;
|
|
381
|
+
const plain = (bone.scaleX ?? 1) === (bone.scaleY ?? 1) && (bone.scaleX ?? 1) > 0 && (bone.shearX ?? 0) === 0 && (bone.shearY ?? 0) === 0 && (bone.inherit ?? 'normal') === 'normal';
|
|
382
|
+
if (!plain) {
|
|
383
|
+
throw new CompileError(
|
|
384
|
+
`${who}: "${at}", between links[${k}] "${links[k]}" and the drawn link above it, is scaled unevenly, sheared, reflected or inherits other than normally — a lower clamp reads the link's turn in its parent, and inward is a turn's sense`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const linkFrame = frames.get(links[0]) as BoneTransform;
|
|
390
|
+
const point = toWorld(frames.get(stop.against) as BoneTransform, stop.at[0], stop.at[1]);
|
|
391
|
+
const target = plant.aimAtAgainst ? [(frames.get(stop.against) as BoneTransform).worldX, (frames.get(stop.against) as BoneTransform).worldY] : point;
|
|
392
|
+
const dx = target[0] - linkFrame.worldX;
|
|
393
|
+
const dy = target[1] - linkFrame.worldY;
|
|
394
|
+
if (dx === 0 && dy === 0) {
|
|
395
|
+
throw new CompileError(`${who}: "at" [${stop.at.join(', ')}] on "${stop.against}" sits on the root of "${links[0]}" at the setup pose — the frame aims from the root at the point, and there is no direction to a point it stands on`);
|
|
396
|
+
}
|
|
397
|
+
const aim = directionDegrees(dx, dy);
|
|
398
|
+
// Each angle is brought into (-180, 180] by one whole turn at most, so a
|
|
399
|
+
// link and an aim already in range subtract with no turn added and taken
|
|
400
|
+
// away again — the rest angle is the plain difference an author computes.
|
|
401
|
+
const rest = halfTurn(halfTurn(linkFrame.worldRotation) - aim);
|
|
402
|
+
if (rest === 0) {
|
|
403
|
+
throw new CompileError(
|
|
404
|
+
`${who}: "at" [${stop.at.join(', ')}] on "${stop.against}" lies on "${links[0]}"'s own line at the setup pose — inward is the turn from the link toward the point, and a point on the link's line has no side`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
// Inward turns the link toward the aim line: counter-clockwise when the link
|
|
408
|
+
// lies clockwise of it (a negative rest angle), clockwise otherwise.
|
|
409
|
+
const sign = (rest < 0 ? 1 : -1) * (plant.swapRange ? -1 : 1);
|
|
410
|
+
const [lo, hi] = bounds(stop, rest, sign, stop, '', `"${names.drawn[0]}"`);
|
|
411
|
+
|
|
412
|
+
const frame: RigBone = { name: names.frame, parent };
|
|
413
|
+
if (link.x !== undefined) frame.x = link.x;
|
|
414
|
+
if (link.y !== undefined) frame.y = link.y;
|
|
415
|
+
if (link.length !== undefined) frame.length = link.length;
|
|
416
|
+
frame.rotation = localDirectionDegrees(parentFrame, dx, dy);
|
|
417
|
+
const onFrame = (name: string): RigBone => {
|
|
418
|
+
const bone: RigBone = { name, parent: names.frame };
|
|
419
|
+
if (link.length !== undefined) bone.length = link.length;
|
|
420
|
+
bone.rotation = rest;
|
|
421
|
+
return bone;
|
|
422
|
+
};
|
|
423
|
+
generated.set(names.frame, frame);
|
|
424
|
+
generated.set(names.rel, onFrame(names.rel));
|
|
425
|
+
generated.set(names.drawn[0], onFrame(names.drawn[0]));
|
|
426
|
+
for (let k = 1; k < links.length; k++) {
|
|
427
|
+
const source = authored.get(links[k]) as RigBone;
|
|
428
|
+
const twin: RigBone = { name: names.drawn[k], parent: source.parent };
|
|
429
|
+
for (const field of TWIN_FIELDS) {
|
|
430
|
+
if (source[field] !== undefined) Object.assign(twin, { [field]: source[field] });
|
|
431
|
+
}
|
|
432
|
+
generated.set(names.drawn[k], twin);
|
|
433
|
+
}
|
|
434
|
+
generated.set(names.point, { name: names.point, parent: stop.against, x: stop.at[0], y: stop.at[1] });
|
|
435
|
+
|
|
436
|
+
const firstWriter = lastWriter(stop, 0);
|
|
437
|
+
const aimAt: RigIkConstraint = { name: names.aim, type: 'ik', bones: [names.frame], target: plant.aimAtAgainst ? stop.against : names.point, mix: 1 };
|
|
438
|
+
queue(before, firstWriter, aimAt);
|
|
439
|
+
queue(after, firstWriter, { name: names.copy, type: 'transform', source: links[0], bones: [names.rel], properties: { rotate: { to: { rotate: {} } } }, mixRotate: 1 });
|
|
440
|
+
queue(after, firstWriter, clampOf(names.clamp[0], names.rel, names.drawn[0], lo, hi));
|
|
441
|
+
for (let k = 1; k < links.length; k++) {
|
|
442
|
+
const source = authored.get(links[k]) as RigBone;
|
|
443
|
+
const [l, h] = bounds(stop, source.rotation ?? 0, sign, stop.lower as { inward: number; outward: number }, 'lower.', `"${names.drawn[k]}"`);
|
|
444
|
+
queue(after, lastWriter(stop, k), clampOf(names.clamp[k], links[k], names.drawn[k], l, h));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// -- 6. spliced in -------------------------------------------------------------
|
|
449
|
+
const bones: RigBone[] = [];
|
|
450
|
+
const firstOf = new Map<string, ReadStop>();
|
|
451
|
+
const lowerOf = new Map<string, { stop: ReadStop; k: number }>();
|
|
452
|
+
for (const stop of stops) {
|
|
453
|
+
firstOf.set(stop.links[0], stop);
|
|
454
|
+
for (let k = 1; k < stop.links.length; k++) lowerOf.set(stop.links[k], { stop, k });
|
|
455
|
+
}
|
|
456
|
+
const take = (name: string): RigBone => generated.get(name) as RigBone;
|
|
457
|
+
for (const bone of spec.bones) {
|
|
458
|
+
const first = isObj(bone) ? firstOf.get(bone.name) : undefined;
|
|
459
|
+
if (first !== undefined) bones.push(take(first.names.frame));
|
|
460
|
+
bones.push(bone);
|
|
461
|
+
if (first !== undefined) bones.push(take(first.names.rel), take(first.names.drawn[0]));
|
|
462
|
+
const lower = isObj(bone) ? lowerOf.get(bone.name) : undefined;
|
|
463
|
+
if (lower !== undefined) bones.push(take(lower.stop.names.drawn[lower.k]));
|
|
464
|
+
}
|
|
465
|
+
for (const stop of stops) bones.push(take(stop.names.point));
|
|
466
|
+
const out: RigConstraint[] = [];
|
|
467
|
+
constraints.forEach((c, i) => {
|
|
468
|
+
out.push(...(before.get(i) ?? []), c, ...(after.get(i) ?? []));
|
|
469
|
+
});
|
|
470
|
+
out.push(...appended);
|
|
471
|
+
return { ...spec, bones, constraints: out };
|
|
472
|
+
}
|