@rig-c/parts 1.25.0 → 1.27.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/cli.ts +20 -5
- package/docs/AUTHORING.md +61 -38
- package/docs/MIGRATION.md +1 -1
- package/docs/STABILITY.md +7 -7
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +131 -25
- package/src/build.ts +23 -9
- package/src/check.ts +220 -40
- package/src/cloth.ts +45 -6
- package/src/config.ts +80 -11
- package/src/headbox.ts +68 -34
- package/src/propose.ts +506 -41
- package/src/rig.ts +74 -6
- package/src/scene.ts +87 -7
- package/src/squeeze.ts +1 -1
package/src/rig.ts
CHANGED
|
@@ -388,6 +388,57 @@ export function roundShares(list: readonly Influence[]): Influence[] {
|
|
|
388
388
|
return kept;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Issue #392: on a lattice mesh whose segments include sibling branch chains
|
|
393
|
+
* forked from the same chain link with `r >= grid` (`split.hold_tip: true`),
|
|
394
|
+
* the distance rule still concentrates weights along the fork row where the
|
|
395
|
+
* trunk splits into branches and across adjacent trees' ruffled hem steps.
|
|
396
|
+
* Four Laplacian passes (`FORK_SMOOTH_PASSES`, self share 0.5, floor 0.005, cap
|
|
397
|
+
* 8 influences) diffuse each vertex's shares across its triangle neighbours
|
|
398
|
+
* before rounding with {@link roundShares}.
|
|
399
|
+
*/
|
|
400
|
+
export const FORK_SMOOTH_PASSES = 4;
|
|
401
|
+
export const FORK_SMOOTH_SELF = 0.5;
|
|
402
|
+
export const FORK_SMOOTH_FLOOR = 0.005;
|
|
403
|
+
export const FORK_SMOOTH_MAX_INFLUENCES = 8;
|
|
404
|
+
|
|
405
|
+
export function smoothForkShares(raw: ReadonlyArray<readonly Influence[]>, triangles: readonly number[]): Influence[][] {
|
|
406
|
+
const adj: number[][] = raw.map(() => []);
|
|
407
|
+
const addEdge = (u: number, v: number): void => {
|
|
408
|
+
if (!adj[u].includes(v)) adj[u].push(v);
|
|
409
|
+
if (!adj[v].includes(u)) adj[v].push(u);
|
|
410
|
+
};
|
|
411
|
+
for (let i = 0; i + 2 < triangles.length; i += 3) {
|
|
412
|
+
const a = triangles[i];
|
|
413
|
+
const b = triangles[i + 1];
|
|
414
|
+
const c = triangles[i + 2];
|
|
415
|
+
addEdge(a, b);
|
|
416
|
+
addEdge(b, c);
|
|
417
|
+
addEdge(c, a);
|
|
418
|
+
}
|
|
419
|
+
let cur: Influence[][] = raw.map((s) => [...s]);
|
|
420
|
+
for (let p = 0; p < FORK_SMOOTH_PASSES; p++) {
|
|
421
|
+
cur = cur.map((own, vi) => {
|
|
422
|
+
const nb = adj[vi];
|
|
423
|
+
if (nb.length === 0) return own;
|
|
424
|
+
const m = new Map<string, number>();
|
|
425
|
+
for (const { bone, weight } of own) m.set(bone, weight * FORK_SMOOTH_SELF);
|
|
426
|
+
const nbShare = (1 - FORK_SMOOTH_SELF) / nb.length;
|
|
427
|
+
for (const u of nb) {
|
|
428
|
+
for (const { bone, weight } of cur[u]) m.set(bone, (m.get(bone) ?? 0) + weight * nbShare);
|
|
429
|
+
}
|
|
430
|
+
const kept = [...m.entries()]
|
|
431
|
+
.filter(([, w]) => w >= FORK_SMOOTH_FLOOR)
|
|
432
|
+
.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
433
|
+
.slice(0, FORK_SMOOTH_MAX_INFLUENCES);
|
|
434
|
+
const sum = kept.reduce((s, [, w]) => s + w, 0);
|
|
435
|
+
return roundShares(kept.map(([bone, w]) => ({ bone, weight: w / sum })));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return cur;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
391
442
|
interface Bone {
|
|
392
443
|
name: string;
|
|
393
444
|
parent: string | null;
|
|
@@ -957,7 +1008,8 @@ export function buildRig(
|
|
|
957
1008
|
}
|
|
958
1009
|
e.points.forEach(([x, y], k) => {
|
|
959
1010
|
const n = `${e.chain}${k}`;
|
|
960
|
-
|
|
1011
|
+
const inherit = e.link_inherit?.[n];
|
|
1012
|
+
B.set(n, { name: n, parent: k === 0 ? e.parent : names[names.length - 1], x, y, ...(inherit === undefined ? {} : { inherit }) });
|
|
961
1013
|
if (names.length > 0) child.set(names[names.length - 1], [x, y]);
|
|
962
1014
|
names.push(n);
|
|
963
1015
|
});
|
|
@@ -1854,6 +1906,7 @@ export function buildRig(
|
|
|
1854
1906
|
const segsAt = dressSegsAt(p, segs);
|
|
1855
1907
|
let emptied: { vertex: number; at: Point; capped: Influence[] } | null = null;
|
|
1856
1908
|
let emptiedMore = 0;
|
|
1909
|
+
const rawShares: Influence[][] = [];
|
|
1857
1910
|
for (const [vi, [vx0, vy0]] of lm.vertices.entries()) {
|
|
1858
1911
|
const wx = vx0 + ox;
|
|
1859
1912
|
const wy = vy0 + oy;
|
|
@@ -1865,7 +1918,26 @@ export function buildRig(
|
|
|
1865
1918
|
else emptiedMore++;
|
|
1866
1919
|
continue;
|
|
1867
1920
|
}
|
|
1868
|
-
|
|
1921
|
+
rawShares.push(limits.floor === 'continuous' ? roundShares(shares) : latticeShares(shares));
|
|
1922
|
+
}
|
|
1923
|
+
if (emptied !== null) {
|
|
1924
|
+
problems.push(nothingKeptProblem(p.name, limits.floor, emptied, emptiedMore));
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const allChainLinks = new Set([...chains.values()].flat());
|
|
1928
|
+
const forkCounts = new Map<string, number>();
|
|
1929
|
+
for (const sp of mesh.segments) {
|
|
1930
|
+
if (typeof sp !== 'string') continue;
|
|
1931
|
+
const links = chains.get(sp);
|
|
1932
|
+
const par = links !== undefined && links.length > 0 ? (B.get(links[0])?.parent ?? null) : null;
|
|
1933
|
+
if (par !== null && allChainLinks.has(par)) forkCounts.set(par, (forkCounts.get(par) ?? 0) + 1);
|
|
1934
|
+
}
|
|
1935
|
+
const heldFork = heat === null && mesh.r >= mesh.grid && mesh.influences === undefined && [...forkCounts.values()].some((c) => c >= 2);
|
|
1936
|
+
const finalShares = heldFork ? smoothForkShares(rawShares, lm.triangles) : rawShares;
|
|
1937
|
+
for (const [vi, [vx0, vy0]] of lm.vertices.entries()) {
|
|
1938
|
+
const wx = vx0 + ox;
|
|
1939
|
+
const wy = vy0 + oy;
|
|
1940
|
+
const ent: WeightEntry[] = finalShares[vi].map(({ bone, weight }) => {
|
|
1869
1941
|
const [x, y] = toBoneLocal(world.get(bone) as BoneTransform, spineX(wx), spineY(wy));
|
|
1870
1942
|
return { bone, x: places(x), y: places(y), weight };
|
|
1871
1943
|
});
|
|
@@ -1873,10 +1945,6 @@ export function buildRig(
|
|
|
1873
1945
|
infl += ent.length;
|
|
1874
1946
|
maxInfl = Math.max(maxInfl, ent.length);
|
|
1875
1947
|
}
|
|
1876
|
-
if (emptied !== null) {
|
|
1877
|
-
problems.push(nothingKeptProblem(p.name, limits.floor, emptied, emptiedMore));
|
|
1878
|
-
continue;
|
|
1879
|
-
}
|
|
1880
1948
|
const uvs: number[] = [];
|
|
1881
1949
|
for (const [x, y] of lm.vertices) uvs.push(pyRound(x / w, 6), pyRound(y / h, 6));
|
|
1882
1950
|
skin[p.name] = { [p.name]: { type: 'mesh', image: file, width: w, height: h, uvs, triangles: lm.triangles, hull: lm.hull, weights } };
|
package/src/scene.ts
CHANGED
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* `rig/images/`, `parts.json` and `check/check.json`:
|
|
17
17
|
*
|
|
18
18
|
* - **Names, totally.** Every bone, slot, attachment (skin entry), constraint
|
|
19
|
-
* (its name and every bone or slot it names), `
|
|
19
|
+
* (its name and every bone or slot it names), `stops` entry (its `chain`,
|
|
20
|
+
* its `links` and its `against`; issue #431), `invariants.detached` bone,
|
|
20
21
|
* `invariants.axes` entry (its name, its `through` and its `bones`; issue #239),
|
|
21
22
|
* track, group and easing of character `<id>` is renamed `<id>:<name>`
|
|
22
23
|
* ({@link prefixed}); `root` is the one shared root. A name resolves to
|
|
23
24
|
* exactly one character, and nothing resolves across characters without its
|
|
24
|
-
* prefix: a constraint naming a bone its own build does not declare is
|
|
25
|
+
* prefix: a constraint or stop naming a bone its own build does not declare is
|
|
25
26
|
* refused (`SCENE_NAME_PREFIXED` when another character has it,
|
|
26
27
|
* `SCENE_NAME_RESOLVES` when none does).
|
|
27
28
|
* - **Images are the one name that cannot carry `:`.** rig-c names an
|
|
@@ -44,8 +45,8 @@
|
|
|
44
45
|
* is local to a bone the shift already moved. In Spine's axes it is the
|
|
45
46
|
* character's stage corner carried to the canvas's ({@link rootShift}),
|
|
46
47
|
* through `src/coords.ts`. The root is shared, so a character whose idle
|
|
47
|
-
* keys it, or whose constraint names it, is refused
|
|
48
|
-
* it would move every character.
|
|
48
|
+
* keys it, or whose constraint or stop names it, is refused
|
|
49
|
+
* (`SCENE_ROOT_SHARED`): it would move every character.
|
|
49
50
|
* - **The scale, with the offset (issue #197).** `characters[].scale`, a
|
|
50
51
|
* positive number and 1 when absent, sizes the character about its stage's
|
|
51
52
|
* top-left corner: a point p of its root frame lands at `scale * p + shift`
|
|
@@ -53,7 +54,8 @@
|
|
|
53
54
|
* Spine's own loader multiplies by its `scale` (`SkeletonJson.js`): bone
|
|
54
55
|
* `x`, `y`, `length`; region `x`, `y` (and its drawn size, through
|
|
55
56
|
* `scaleX`/`scaleY`); each weight's bind `x`, `y`; the constraints' lengths
|
|
56
|
-
* ({@link scaledConstraint});
|
|
57
|
+
* ({@link scaledConstraint}); each stop's `at` offset in its `against` bone's
|
|
58
|
+
* local frame ({@link scaledStop}; issue #431); the translate keys and their curves'
|
|
57
59
|
* values. Spine also scales the skeleton's `referenceScale`, which the
|
|
58
60
|
* composed skeleton holds once for every character, so a physics
|
|
59
61
|
* constraint with a nonzero `wind` or `gravity` cannot be scaled per
|
|
@@ -91,6 +93,7 @@
|
|
|
91
93
|
*/
|
|
92
94
|
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
93
95
|
import { dirname, join, resolve } from 'node:path';
|
|
96
|
+
import type { RigStop } from 'rig-c/src/rig.ts';
|
|
94
97
|
import { checkStage, gateThroughRigc, type Log } from './build.ts';
|
|
95
98
|
import { ACCEPTABLE_BARS, barsNotMet, type CheckReport, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, type PackMode, requirementsNotPass, type RigcRunner } from './check.ts';
|
|
96
99
|
import { CONSTRAINT_BONE_FIELDS, ROOT_BONE } from './config.ts';
|
|
@@ -382,7 +385,9 @@ export interface CharacterBuild {
|
|
|
382
385
|
const BONE_KEYS = ['name', 'parent', 'length', 'rotation', 'x', 'y'];
|
|
383
386
|
const SLOT_KEYS = ['name', 'bone', 'attachment'];
|
|
384
387
|
const ATTACHMENT_KEYS = ['type', 'image', 'width', 'height', 'uvs', 'triangles', 'hull', 'weights', 'x', 'y', 'rotation'];
|
|
385
|
-
const RIG_KEYS = ['spec', 'name', 'images', 'skeleton', 'bones', 'slots', 'skins', 'constraints', 'invariants'];
|
|
388
|
+
const RIG_KEYS = ['spec', 'name', 'images', 'skeleton', 'bones', 'slots', 'skins', 'constraints', 'stops', 'invariants'];
|
|
389
|
+
const STOP_KEYS = ['chain', 'links', 'against', 'at', 'inward', 'outward', 'lower'];
|
|
390
|
+
const STOP_LOWER_KEYS = ['inward', 'outward'];
|
|
386
391
|
const INVARIANT_KEYS = ['idleDrivesMeshes', 'detached', 'axes'];
|
|
387
392
|
const MOTION_KEYS = ['spec', 'archetype', 'cut', 'easings', 'groups', 'animations'];
|
|
388
393
|
const IDLE_KEYS = ['duration', 'loop', 'note', 'tracks'];
|
|
@@ -501,6 +506,48 @@ export function readCharacterBuild(c: SceneCharacter, problems: Problem[], accep
|
|
|
501
506
|
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} rig.json bones[0]`, detail: `is ${show(root)}; {"name": "${ROOT_BONE}", "x": 0, "y": 0}, as the rig stage writes it, is required — the composed root is shared, so a character's root must carry no transform of its own` });
|
|
502
507
|
}
|
|
503
508
|
(Array.isArray(r.slots) ? r.slots : []).forEach((s, i) => unknown(`rig.json slots[${i}]`, s, SLOT_KEYS));
|
|
509
|
+
if ('stops' in r) {
|
|
510
|
+
if (!Array.isArray(r.stops)) {
|
|
511
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} rig.json stops`, detail: `is ${show(r.stops)}; an array of stop entries is required` });
|
|
512
|
+
} else {
|
|
513
|
+
r.stops.forEach((st, i) => {
|
|
514
|
+
const where = `rig.json stops[${i}]`;
|
|
515
|
+
unknown(where, st, STOP_KEYS);
|
|
516
|
+
if (!isRecord(st)) return;
|
|
517
|
+
if (typeof st.chain !== 'string' || st.chain === '') {
|
|
518
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.chain`, detail: `is ${show(st.chain)}; a non-empty chain name is required` });
|
|
519
|
+
}
|
|
520
|
+
if (!(Array.isArray(st.links) && st.links.length > 0 && st.links.every((l) => typeof l === 'string' && l !== ''))) {
|
|
521
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.links`, detail: `is ${show(st.links)}; a non-empty list of bone names is required` });
|
|
522
|
+
}
|
|
523
|
+
if (!('against' in st)) {
|
|
524
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.against`, detail: 'is absent; a bone name is required' });
|
|
525
|
+
}
|
|
526
|
+
if (!(Array.isArray(st.at) && st.at.length === 2 && st.at.every((n) => typeof n === 'number' && Number.isFinite(n)))) {
|
|
527
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.at`, detail: `is ${show(st.at)}; [x, y], two finite numbers in against's local frame, is required` });
|
|
528
|
+
}
|
|
529
|
+
const checkBounds = (bWhere: string, bObj: Record<string, unknown>): void => {
|
|
530
|
+
const inOk = typeof bObj.inward === 'number' && Number.isFinite(bObj.inward);
|
|
531
|
+
const outOk = typeof bObj.outward === 'number' && Number.isFinite(bObj.outward);
|
|
532
|
+
if (!inOk) problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${bWhere}.inward`, detail: `is ${show(bObj.inward)}; a finite number of degrees is required` });
|
|
533
|
+
if (!outOk) problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${bWhere}.outward`, detail: `is ${show(bObj.outward)}; a finite number of degrees is required` });
|
|
534
|
+
if (inOk && outOk && (bObj.inward as number) > (bObj.outward as number)) {
|
|
535
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${bWhere}.inward`, detail: `is ${String(bObj.inward)}, above outward ${String(bObj.outward)}; inward <= outward is required` });
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
checkBounds(where, st);
|
|
539
|
+
if ('lower' in st) {
|
|
540
|
+
unknown(`${where}.lower`, st.lower, STOP_LOWER_KEYS);
|
|
541
|
+
if (isRecord(st.lower)) checkBounds(`${where}.lower`, st.lower);
|
|
542
|
+
if (Array.isArray(st.links) && st.links.length === 1) {
|
|
543
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.lower`, detail: `is ${show(st.lower)} on a 1-link stop; lower holds the links below the first, so leave lower out when links has 1 bone` });
|
|
544
|
+
}
|
|
545
|
+
} else if (Array.isArray(st.links) && st.links.length > 1) {
|
|
546
|
+
problems.push({ code: 'SCENE_BUILD_FIELD_KNOWN', object: `${who} ${where}.lower`, detail: `is absent and required when the stop holds ${st.links.length} links (more than 1); state lower: { inward, outward } for the links below the first` });
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
504
551
|
if (isRecord(r.invariants)) unknown('rig.json invariants', r.invariants, INVARIANT_KEYS);
|
|
505
552
|
const skins = isRecord(r.skins) ? r.skins : {};
|
|
506
553
|
unknown('rig.json skins', skins, ['default']);
|
|
@@ -732,6 +779,12 @@ export function scaledConstraint(con: Record<string, unknown>, s: number): Recor
|
|
|
732
779
|
return out;
|
|
733
780
|
}
|
|
734
781
|
|
|
782
|
+
/** A stop of a character at scale `s` (issue #431): its `at` offset in `against`'s local frame scaled by `s`, its degree bounds unchanged; `s` 1 returns `stop` as given. */
|
|
783
|
+
export function scaledStop(stop: RigStop, s: number): RigStop {
|
|
784
|
+
if (s === 1) return stop;
|
|
785
|
+
return { ...stop, at: [scaledBy(stop.at[0], s), scaledBy(stop.at[1], s)] };
|
|
786
|
+
}
|
|
787
|
+
|
|
735
788
|
/** A bone track's keys at scale `s`: a length property's values, and its curve's value control points, scaled; any other track as given. */
|
|
736
789
|
function scaledTrack<T extends { property: string; keys: unknown[] }>(t: T, s: number): T {
|
|
737
790
|
if (s === 1 || !SCALED_TRACK_PROPERTIES.includes(t.property)) return t;
|
|
@@ -962,6 +1015,11 @@ export function composeScene(
|
|
|
962
1015
|
fail('SCENE_NAME_RESOLVES', `${where}.slot`, `names the slot "${con.slot}", which character "${b.id}" does not declare`);
|
|
963
1016
|
}
|
|
964
1017
|
});
|
|
1018
|
+
(b.rig.stops ?? []).forEach((st, k) => {
|
|
1019
|
+
const where = `character "${b.id}" rig.json stops[${k}] (stop "${String(st.chain)}")`;
|
|
1020
|
+
st.links.forEach((n, j) => ownBone(b.id, n, `${where}.links[${j}]`));
|
|
1021
|
+
ownBone(b.id, st.against, `${where}.against`);
|
|
1022
|
+
});
|
|
965
1023
|
for (const [k, d] of (b.rig.invariants?.detached ?? []).entries()) {
|
|
966
1024
|
ownBone(b.id, d.bone, `character "${b.id}" rig.json invariants.detached[${k}].bone`);
|
|
967
1025
|
ownBone(b.id, d.notUnder, `character "${b.id}" rig.json invariants.detached[${k}].notUnder`);
|
|
@@ -988,6 +1046,11 @@ export function composeScene(
|
|
|
988
1046
|
});
|
|
989
1047
|
if (named.includes(ROOT_BONE)) fail('SCENE_ROOT_SHARED', `${where} rig.json constraints[${k}] (${String(con.type)} "${String(con.name)}")`, `names "${ROOT_BONE}", which the scene shares by every character; a constraint on it, or following it, would reach every character — name a bone of the character's own`);
|
|
990
1048
|
});
|
|
1049
|
+
(b.rig.stops ?? []).forEach((st, k) => {
|
|
1050
|
+
if ([...st.links, st.against].includes(ROOT_BONE)) {
|
|
1051
|
+
fail('SCENE_ROOT_SHARED', `${where} rig.json stops[${k}] (stop "${String(st.chain)}")`, `names "${ROOT_BONE}", which the scene shares by every character; a stop on it, or against it, would reach every character — name a bone of the character's own`);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
991
1054
|
}
|
|
992
1055
|
const whys = [...new Set(ready.map((b) => b.rig.invariants?.idleDrivesMeshes?.why).filter((w): w is string => w !== undefined))];
|
|
993
1056
|
if (whys.length > 1) {
|
|
@@ -1034,6 +1097,7 @@ export function composeScene(
|
|
|
1034
1097
|
images.push([PLATE_IMAGE, plate.bytes]);
|
|
1035
1098
|
}
|
|
1036
1099
|
const constraints: Array<Record<string, unknown>> = [];
|
|
1100
|
+
const stops: RigStop[] = [];
|
|
1037
1101
|
const detached: NonNullable<NonNullable<RigSpec['invariants']>['detached']> = [];
|
|
1038
1102
|
const axes: NonNullable<NonNullable<RigSpec['invariants']>['axes']> = [];
|
|
1039
1103
|
scene.characters.forEach((c) => {
|
|
@@ -1043,7 +1107,8 @@ export function composeScene(
|
|
|
1043
1107
|
const [dx, dy] = rootShift(b.rig.skeleton, stage, c.offset, s);
|
|
1044
1108
|
// At a scale other than 1 every length the character carries is scaled (issue #197): each bone's x, y and length,
|
|
1045
1109
|
// each region's x, y (and its drawn size, by scaleX/scaleY), each weight's bind x, y, the constraints' lengths
|
|
1046
|
-
// (scaledConstraint)
|
|
1110
|
+
// (scaledConstraint), each stop's at offset in against's local frame (scaledStop; issue #431) and the translate keys
|
|
1111
|
+
// (scaledTrack); what sits in root's own frame is then shifted.
|
|
1047
1112
|
for (const bone of b.rig.bones) {
|
|
1048
1113
|
if (bone.name === ROOT_BONE) continue;
|
|
1049
1114
|
const first = bone.parent === ROOT_BONE;
|
|
@@ -1095,6 +1160,15 @@ export function composeScene(
|
|
|
1095
1160
|
if (type === 'path' && typeof con.slot === 'string') out.slot = prefixed(id, con.slot);
|
|
1096
1161
|
constraints.push(out);
|
|
1097
1162
|
}
|
|
1163
|
+
for (const st of b.rig.stops ?? []) {
|
|
1164
|
+
const scaled = scaledStop(st, s);
|
|
1165
|
+
stops.push({
|
|
1166
|
+
...scaled,
|
|
1167
|
+
chain: prefixed(id, st.chain),
|
|
1168
|
+
links: st.links.map((l) => P(id, l)),
|
|
1169
|
+
against: P(id, st.against),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1098
1172
|
for (const d of b.rig.invariants?.detached ?? []) detached.push({ ...d, bone: P(id, d.bone), notUnder: P(id, d.notUnder) });
|
|
1099
1173
|
// An axis entry's name is unique within one rig, and two characters each carry a "face": the name is prefixed too.
|
|
1100
1174
|
for (const ax of b.rig.invariants?.axes ?? []) axes.push({ name: prefixed(id, ax.name), through: [P(id, ax.through[0]), P(id, ax.through[1])], bones: ax.bones.map((n) => P(id, n)), tolerance: ax.tolerance });
|
|
@@ -1106,6 +1180,7 @@ export function composeScene(
|
|
|
1106
1180
|
const name = ids.join('+');
|
|
1107
1181
|
const rig: RigSpec = { spec: 'rigc-rig/1', name, images: 'images', skeleton: stage, bones, slots, skins: { default: skin as unknown as RigSpec['skins']['default'] } };
|
|
1108
1182
|
if (constraints.length > 0) rig.constraints = constraints;
|
|
1183
|
+
if (stops.length > 0) rig.stops = stops;
|
|
1109
1184
|
const invariants: NonNullable<RigSpec['invariants']> = {};
|
|
1110
1185
|
if (whys.length === 1) invariants.idleDrivesMeshes = { why: whys[0] };
|
|
1111
1186
|
if (detached.length > 0) invariants.detached = detached;
|
|
@@ -1251,6 +1326,11 @@ export function unprefixedNames(rig: RigSpec, motion: MotionSpec, ids: readonly
|
|
|
1251
1326
|
}
|
|
1252
1327
|
if (c.type === 'path') name(`constraint ${String(c.name)} slot`, c.slot, false);
|
|
1253
1328
|
}
|
|
1329
|
+
for (const s of rig.stops ?? []) {
|
|
1330
|
+
name('stop chain', s.chain, false);
|
|
1331
|
+
for (const l of s.links) name(`stop ${s.chain} link`, l, false);
|
|
1332
|
+
name(`stop ${s.chain} against`, s.against, false);
|
|
1333
|
+
}
|
|
1254
1334
|
for (const d of rig.invariants?.detached ?? []) {
|
|
1255
1335
|
name('invariants.detached bone', d.bone, false);
|
|
1256
1336
|
name('invariants.detached notUnder', d.notUnder, false);
|
package/src/squeeze.ts
CHANGED
|
@@ -318,7 +318,7 @@ export function branchSqueezes(
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
for (const [mname, m] of Object.entries(spec.meshes)) {
|
|
321
|
-
if ('contour' in m || 'auto' in m || m.rule === 'heat') continue;
|
|
321
|
+
if ('contour' in m || 'auto' in m || m.rule === 'heat' || m.r >= m.grid) continue;
|
|
322
322
|
const byFork = new Map<string, string[]>();
|
|
323
323
|
for (const sp of m.segments) {
|
|
324
324
|
if (typeof sp !== 'string') continue;
|