@rig-c/parts 1.10.0 → 1.12.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 +7 -7
- package/docs/AUTHORING.md +159 -24
- package/docs/STABILITY.md +3 -2
- package/package.json +2 -2
- package/src/assemble.ts +85 -2
- package/src/build.ts +5 -1
- package/src/config.ts +51 -1
- package/src/heat.ts +42 -6
- package/src/mesh.ts +61 -0
- package/src/meshdraft.ts +10 -9
- package/src/rig.ts +101 -20
- package/src/weights.ts +69 -11
package/src/weights.ts
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
* rounding. Ties in the sort keep the order the bones first appeared among the
|
|
19
19
|
* segments (Python's `sorted` is stable, and so is `Array.prototype.sort`).
|
|
20
20
|
*
|
|
21
|
+
* The cap and the floor's form are the author's per mesh in the lattice and
|
|
22
|
+
* contour modes (issue #284, `meshes.<part>.influences`): a declared
|
|
23
|
+
* `maxInfluences` (a whole number, or `null`: no cap) replaces the 4, and
|
|
24
|
+
* `floor: "continuous"` replaces the drop at 0.03 by the continuous floor
|
|
25
|
+
* ({@link FloorForm}). Absent, both are the constants above, and every byte is
|
|
26
|
+
* the one written before the field existed.
|
|
27
|
+
*
|
|
21
28
|
* Distances are the reference's arithmetic, operation for operation — the
|
|
22
29
|
* projection `t = clip(((p - a) . ab) / |ab|^2, 0, 1)` and `sqrt(dx^2 + dy^2)`,
|
|
23
30
|
* not `Math.hypot`, whose last bit differs — because a weight rounded at the
|
|
@@ -31,12 +38,36 @@
|
|
|
31
38
|
*/
|
|
32
39
|
import type { Point } from './config.ts';
|
|
33
40
|
|
|
34
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* At most this many bones pull one vertex where a mesh declares no cap: the reference implementation's `[:4]`,
|
|
43
|
+
* carried over as a port constant, and nothing more. The format sets no limit — spine-core 4.3.13 reads a weighted
|
|
44
|
+
* vertex as a bone count followed by that many bindings, with no bound on the count (`SkeletonJson.js`,
|
|
45
|
+
* `SkeletonBinary.js`, and `computeWorldVertices` in `attachments/Attachment.js`) — and rig-c 2.40.0 neither refuses
|
|
46
|
+
* nor warns above four: its one assertion on a mesh's weights, `A20_MESH_WEIGHTS_COHERENT`, checks that they are
|
|
47
|
+
* coherent (every vertex bound, every weight finite and non-negative, each vertex summing to 1), not how many there
|
|
48
|
+
* are. Read and cited in `docs/evidence/weight-cap.md` (issue #284, Stage E). A mesh may declare its own cap
|
|
49
|
+
* (`meshes.<part>.influences.maxInfluences`).
|
|
50
|
+
*/
|
|
35
51
|
export const MAX_INFLUENCES = 4;
|
|
36
52
|
|
|
37
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* A normalised weight under this is dropped before the final normalisation — the reference's `>= 0.03`. Under the
|
|
55
|
+
* continuous floor ({@link FloorForm}) it is subtracted from every kept share instead.
|
|
56
|
+
*/
|
|
38
57
|
export const MIN_WEIGHT = 0.03;
|
|
39
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The floor's form (issue #284): `"cut"` — the reference's, and what an absent form means: the capped shares,
|
|
61
|
+
* normalised, any under the floor dropped, the rest normalised again — or `"continuous"`: the capped shares,
|
|
62
|
+
* normalised, the floor subtracted from every one, those at or below zero dropped, the rest normalised again. Under
|
|
63
|
+
* the continuous floor a share reaches zero exactly where the cut would drop it, so a bone fades out of a vertex
|
|
64
|
+
* instead of leaving it in one step; this is Stage A's "continuous floor" (`tools/tear_survey.ts`), the form Stage E
|
|
65
|
+
* swept at every cap (`docs/evidence/weight-cap.md`).
|
|
66
|
+
*/
|
|
67
|
+
export type FloorForm = 'cut' | 'continuous';
|
|
68
|
+
|
|
69
|
+
export const FLOOR_FORMS: readonly FloorForm[] = ['cut', 'continuous'];
|
|
70
|
+
|
|
40
71
|
/**
|
|
41
72
|
* The exponent of the distance rule where a mesh declares none: `1/(d + r)²`,
|
|
42
73
|
* the reference's rule. At this value the weight is computed as
|
|
@@ -76,20 +107,42 @@ export interface Influence {
|
|
|
76
107
|
|
|
77
108
|
/**
|
|
78
109
|
* The cap and the floor `influences()` applies. The lattice and the contour
|
|
79
|
-
* mode
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
110
|
+
* mode run at {@link DEFAULT_LIMITS} (the reference's `[:4]` and `>= 0.03`)
|
|
111
|
+
* unless the mesh declares `influences` (issue #284; {@link meshLimits}); the
|
|
112
|
+
* automatic mode (`src/automesh.ts`, issue #126) passes the author's numbers
|
|
113
|
+
* instead, because rig-c's reduction is handed explicit influence limits on
|
|
114
|
+
* every weighted call and the 0.03 floor is not one the author wrote (P19).
|
|
115
|
+
* The arithmetic is the same either way, so with the defaults every byte the
|
|
116
|
+
* two older modes write is unchanged.
|
|
85
117
|
*/
|
|
86
118
|
export interface InfluenceLimits {
|
|
87
119
|
maxInfluences: number;
|
|
88
120
|
minWeight: number;
|
|
121
|
+
/** The floor's form; absent is `"cut"`, the arithmetic as it always was. */
|
|
122
|
+
floor?: FloorForm;
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
export const DEFAULT_LIMITS: InfluenceLimits = { maxInfluences: MAX_INFLUENCES, minWeight: MIN_WEIGHT };
|
|
92
126
|
|
|
127
|
+
/** `meshes.<part>.influences` in the lattice and contour modes (issue #284): either field may be absent. */
|
|
128
|
+
export interface MeshInfluences {
|
|
129
|
+
/** A whole number, 1 or more, or `null`, no cap — every bone the mesh's segments name is kept (the config's spelling of a bound declared absent, as `maxOvershoot` and `weightJump`). Absent: {@link MAX_INFLUENCES}. */
|
|
130
|
+
maxInfluences?: number | null;
|
|
131
|
+
/** Absent: `"cut"`. */
|
|
132
|
+
floor?: FloorForm;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The limits a lattice or contour mesh runs at: {@link DEFAULT_LIMITS} itself when it declares no `influences`, so
|
|
137
|
+
* nothing about such a mesh changes; otherwise its declared cap (`null` is a count no vertex reaches) and floor form,
|
|
138
|
+
* with the floor's value {@link MIN_WEIGHT} either way — the declaration chooses the form of the cut, not where it falls.
|
|
139
|
+
*/
|
|
140
|
+
export function meshLimits(declared: MeshInfluences | undefined): InfluenceLimits {
|
|
141
|
+
if (declared === undefined) return DEFAULT_LIMITS;
|
|
142
|
+
const cap = declared.maxInfluences;
|
|
143
|
+
return { maxInfluences: cap === undefined ? MAX_INFLUENCES : cap === null ? Number.MAX_SAFE_INTEGER : cap, minWeight: MIN_WEIGHT, floor: declared.floor ?? 'cut' };
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
/**
|
|
94
147
|
* The influences on one vertex at `p` (rig pixels), before rounding, under
|
|
95
148
|
* `w = 1/(d + r)^exponent`. The rig stage passes every mesh's exponent
|
|
@@ -110,14 +163,19 @@ export function influences(p: Point, segments: readonly Segment[], r: number, li
|
|
|
110
163
|
|
|
111
164
|
/**
|
|
112
165
|
* The cap and the floor, on raw per-bone values in first-appearance order: sorted (stable), the top
|
|
113
|
-
* `maxInfluences` normalised,
|
|
114
|
-
*
|
|
166
|
+
* `maxInfluences` normalised, then the floor in its form ({@link FloorForm}) — under `"cut"` those under `minWeight`
|
|
167
|
+
* dropped, under `"continuous"` `minWeight` subtracted from each and those at or below zero dropped — and the rest
|
|
168
|
+
* normalised again. The distance rule above and bone heat (`src/heat.ts`, issue #161) both end here, so the two
|
|
169
|
+
* rules share one cap and one floor.
|
|
115
170
|
*/
|
|
116
171
|
export function capAndFloor(raw: ReadonlyArray<readonly [string, number]>, limits: InfluenceLimits): Influence[] {
|
|
117
172
|
const top = [...raw].sort((x, y) => y[1] - x[1]).slice(0, limits.maxInfluences);
|
|
118
173
|
let s = 0;
|
|
119
174
|
for (const [, v] of top) s += v;
|
|
120
|
-
const kept =
|
|
175
|
+
const kept =
|
|
176
|
+
limits.floor === 'continuous'
|
|
177
|
+
? top.map(([bone, v]) => ({ bone, weight: v / s - limits.minWeight })).filter((e) => e.weight > 0)
|
|
178
|
+
: top.map(([bone, v]) => ({ bone, weight: v / s })).filter((e) => e.weight >= limits.minWeight);
|
|
121
179
|
let s2 = 0;
|
|
122
180
|
for (const e of kept) s2 += e.weight;
|
|
123
181
|
return kept.map((e) => ({ bone: e.bone, weight: e.weight / s2 }));
|