spine-rigc 2.52.0 → 2.53.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 +70 -1
- package/package.json +1 -1
- package/src/cli/shared.ts +5 -0
- package/src/compile.ts +126 -41
- package/src/mesh.ts +136 -0
- package/src/rig.ts +63 -4
- package/src/types.ts +14 -0
package/docs/AUTHORING.md
CHANGED
|
@@ -3119,7 +3119,7 @@ beside it say how a distance becomes a share.
|
|
|
3119
3119
|
| --- | --- |
|
|
3120
3120
|
| `cell` | **required.** The lattice's square, in the part's pixels — a whole number of at least 1. No default: it is a length on this art, and how fine a mesh the part needs is not something rigc can read off it |
|
|
3121
3121
|
| `bones` | **required, at least one entry.** Which segments may pull the part. An entry is one of three forms, below |
|
|
3122
|
-
| `falloff` | **required**, because `radius` is: `{ "power"?, "radius", "maxBones"?, "minWeight"? }` |
|
|
3122
|
+
| `falloff` | **required unless `influence` is stated**, because `radius` is: `{ "power"?, "radius", "maxBones"?, "minWeight"? }`. Stated beside `influence`, refused — the two are two models of one share |
|
|
3123
3123
|
| `radius` | **required.** Added to every distance before the power is taken, in the part's pixels, so a vertex ON a segment has a finite pull and the blend between two segments is about this wide. Positive |
|
|
3124
3124
|
| `power` | the exponent a pull falls off by: `w = 1 / (d + radius)^power`. Positive. Default `2` |
|
|
3125
3125
|
| `maxBones` | at most this many bones pull one vertex — the strongest ones. A whole number of at least 1. Default `4` |
|
|
@@ -3127,6 +3127,8 @@ beside it say how a distance becomes a share.
|
|
|
3127
3127
|
| `alpha` | the alpha at or above which a pixel counts as art, `1`..`255`. Default `1` — `contour`'s own |
|
|
3128
3128
|
| `anchor` | where the **slot bone** sits in the part's pixels, `[x, y]`, y down. Default the window's centre — the placement every generator on this route uses (the ⭐ above). State it when the art is not drawn centred on its bone: unlike a pinned generator, this one's weights depend on where the art is relative to the bones |
|
|
3129
3129
|
|
|
3130
|
+
| `influence` | instead of `falloff`: a pull that **reaches zero** at its own `radius` — `{ "radius", "across", "max", "shape" }`, every field required. Below |
|
|
3131
|
+
|
|
3130
3132
|
**The three forms of a `bones` entry**, each resolved by name against the rig:
|
|
3131
3133
|
|
|
3132
3134
|
| Entry | Its segment(s) |
|
|
@@ -3154,6 +3156,70 @@ of each segment, the strongest `maxBones` kept and normalised, any share under
|
|
|
3154
3156
|
`minWeight` dropped, the rest normalised again — so every vertex closes at 1 by
|
|
3155
3157
|
construction. The emitted mesh is a weighted one, bound by name.
|
|
3156
3158
|
|
|
3159
|
+
##### `influence` — a vertex follows every link near it (cloth)
|
|
3160
|
+
|
|
3161
|
+
`falloff`'s pull, `1 / (d + radius)^power`, is positive at **every** distance: a
|
|
3162
|
+
wider blend needs a larger `radius`, which flattens the blend everywhere at once,
|
|
3163
|
+
and a blend only ends where `minWeight` drops a share. A cloth surface wants the
|
|
3164
|
+
other model — each vertex blends the springs of the chains around it, so the
|
|
3165
|
+
sheet bends as one surface rather than as strips — and that needs a pull that
|
|
3166
|
+
falls to **exactly zero** at a stated distance. `influence` is that model.
|
|
3167
|
+
|
|
3168
|
+
```json
|
|
3169
|
+
"generator": {
|
|
3170
|
+
"kind": "segments",
|
|
3171
|
+
"cell": 8,
|
|
3172
|
+
"bones": [["skirt_0_0", "skirt_0_1", "skirt_0_2"], ["skirt_1_0", "skirt_1_1", "skirt_1_2"]],
|
|
3173
|
+
"influence": { "radius": 40, "across": true, "max": 6, "shape": "smooth" }
|
|
3174
|
+
}
|
|
3175
|
+
```
|
|
3176
|
+
|
|
3177
|
+
| Field | Meaning |
|
|
3178
|
+
| --- | --- |
|
|
3179
|
+
| `radius` | **required.** How far a link reaches, in the part's pixels (on this route, the skeleton's units): a link whose segment is `radius` or farther from a vertex never pulls it. Positive. No default — a length on this art |
|
|
3180
|
+
| `across` | **required.** `true`: every link of every `bones` entry within `radius` pulls. `false`: only the links of **one** entry — the entry holding the link nearest the vertex, the earlier entry on a tie — so a chain moves its own strip and nothing across. No default: whether a chain may pull its neighbour's cloth is the decision |
|
|
3181
|
+
| `max` | **required.** At most this many links pull one vertex — the strongest; equal pulls keep the order `bones` first names their bones. A whole number of at least 1 |
|
|
3182
|
+
| `shape` | **required.** How a pull falls from 1 at `d = 0` to 0 at `d = radius`, with `x = d / radius`: `"linear"` is `1 - x`; `"smooth"` is `1 - x²(3 - 2x)`, which also arrives at both ends with zero slope, so a link enters and leaves a vertex's blend without a crease |
|
|
3183
|
+
|
|
3184
|
+
Per vertex, in order: `d` is the distance to each link's **segment** — its
|
|
3185
|
+
nearest point, the reading `falloff` takes — at the setup pose; with `across:
|
|
3186
|
+
false` only the nearest entry's links stay; each link with `d < radius` pulls
|
|
3187
|
+
`shape(d / radius)` (a bone two segments name keeps the nearer, and a link whose
|
|
3188
|
+
pull is not above 0 in doubles — a hair inside the reach — pulls nothing); the
|
|
3189
|
+
strongest `max` are kept and normalised; a share the 6-decimal grid would write
|
|
3190
|
+
as 0 is dropped and the rest normalised again; the strongest closes the vertex
|
|
3191
|
+
at exactly 1. **The distance is to the segment, not to the bone's origin**,
|
|
3192
|
+
because a vertex on the middle of a link *is* on that link: an origin distance
|
|
3193
|
+
would put it half a link away, and hand the joint at the link's tip to the child
|
|
3194
|
+
whose origin sits there.
|
|
3195
|
+
|
|
3196
|
+
🔸 **One bone order by construction.** Every vertex lists its bones in the order
|
|
3197
|
+
`bones` names them, so the attachment has one order and
|
|
3198
|
+
`A52_WEIGHTED_VERTICES_SHARE_ONE_BONE_ORDER` holds without the compiler's
|
|
3199
|
+
ascending rewrite (§5.2) moving a byte — the order in the file is the spec's.
|
|
3200
|
+
|
|
3201
|
+
`build` prints a `reach` line under the `influence` line: the block as stated,
|
|
3202
|
+
how many vertices had **more links in reach than `max`** — the vertices the cap
|
|
3203
|
+
dropped a link from — and the most links in reach at one vertex (`widest`),
|
|
3204
|
+
which is the `max` that would drop none:
|
|
3205
|
+
|
|
3206
|
+
```
|
|
3207
|
+
influence max 6 bone(s) per vertex, mean 4.87, 0 of 450 vertices (0.00%) on a single bone
|
|
3208
|
+
reach radius 40px, smooth, across entries, max 6: 146 of 450 vertices had more links in reach than max (widest 10)
|
|
3209
|
+
```
|
|
3210
|
+
|
|
3211
|
+
⚠️ **What a tight cap costs, measured.** On the selftest's `skirt_probe`
|
|
3212
|
+
(`fixtures/public.ts`: six chains of five links, a physics constraint on every
|
|
3213
|
+
link, the hip swayed) with `radius: 40`, `rotate` physics on and `render --folds`
|
|
3214
|
+
at 30 fps, `max: 2` turned 106 of the 816 triangles over on its worst frame and
|
|
3215
|
+
`max: 6` turned 4; `max: 6` with `radius: 20` (no vertex capped) turned 105, and
|
|
3216
|
+
`across: false` 91. With the physics removed the same sway folds none at either
|
|
3217
|
+
cap. So on that piece the folds followed how many neighbouring links a vertex
|
|
3218
|
+
blends — a narrow reach and a tight cap over a wide reach both fold — and a
|
|
3219
|
+
wide reach with a cap that drops few links folded least. One piece and one
|
|
3220
|
+
sway: it says what to look at with `render --folds` on your own, not what `max`
|
|
3221
|
+
your cloth needs.
|
|
3222
|
+
|
|
3157
3223
|
`build` and `explain` print what it measured, under the mesh's line. This is
|
|
3158
3224
|
the selftest's own `segments_probe` (`fixtures/public.ts` in the repository) —
|
|
3159
3225
|
one 96x64 plate of two islands under two two-link chains, `cell: 8`,
|
|
@@ -3199,6 +3265,9 @@ rows.
|
|
|
3199
3265
|
| a chain link at its next link's origin | `… the two bones share their setup origin (…), so the link has no length and no direction to weight along` |
|
|
3200
3266
|
| an empty chain, a span with a malformed end, an entry of none of the three forms | `is an empty chain`; `both ends are [x, y] in the part's pixels`; `an entry is a bone name, a chain as a list of bone names, or { "bone", "from", "to" }` |
|
|
3201
3267
|
| no `falloff`, a `radius` or `power` at or below 0, a malformed `anchor`, a `cell` under 1, no `image` | each named by its field — `radius` has no default because it is a length on this art |
|
|
3268
|
+
| both `falloff` and `influence` | `the "segments" generator states both "falloff" and "influence" … a mesh is weighted by one of them: remove the other` |
|
|
3269
|
+
| an `influence` field absent or out of range — `radius` at or below 0, `across` not a boolean, `max` not a whole number of at least 1, `shape` not `"linear"` or `"smooth"` | each named with the value found and the one required: `"influence.max" is 0; it is how many links may pull one vertex …`, `"influence.shape" is "cubic"; one of "linear", "smooth" …` |
|
|
3270
|
+
| a vertex no link reaches under `influence` | `vertex 0, at (0, 0) in the part's pixels, is reached by no link — the nearest is "skirt_0_0" at 17.8885, and "influence.radius" is 4` — **refused, not handed the nearest link**: raise `radius` over that distance, or name a segment nearer that part of the art |
|
|
3202
3271
|
| a part on a packed page that declares a `scale:` | refused: `cell` and `radius` are the part's pixels and this generator does not convert them to a page's texels. Build from the loose part, or a pack at scale 1 |
|
|
3203
3272
|
|
|
3204
3273
|
#### 3.4.8 A skin that switches bones and constraints on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.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/cli/shared.ts
CHANGED
|
@@ -458,6 +458,11 @@ export function meshInfluenceNote(m: CompileResult['meshes'][number]): string {
|
|
|
458
458
|
`\n influence max ${inf.maxBones} bone(s) per vertex, mean ${inf.meanBones.toFixed(2)}, ` +
|
|
459
459
|
`${inf.singleBone} of ${m.vertices} vertices (${single.toFixed(2)}%) on a single bone` +
|
|
460
460
|
(unbound.length ? `; named and bound by no vertex: ${unbound.join(', ')}` : '') +
|
|
461
|
+
(inf.reach === undefined
|
|
462
|
+
? ''
|
|
463
|
+
: `\n reach radius ${inf.reach.radius}px, ${inf.reach.shape}, ` +
|
|
464
|
+
`${inf.reach.across ? 'across entries' : 'nearest entry only'}, max ${inf.reach.max}: ` +
|
|
465
|
+
`${inf.reach.capped} of ${m.vertices} vertices had more links in reach than max (widest ${inf.reach.widest})`) +
|
|
461
466
|
`\n lattice cell ${inf.cell}px, ${inf.cols}x${inf.rows} cells, ${inf.artCells} with art, ${inf.keptCells} kept` +
|
|
462
467
|
(inf.islands > 1 || joined > 0
|
|
463
468
|
? ` (${inf.islands} island(s) joined into one outline, ${joined} cell(s) added that hold no art)`
|
package/src/compile.ts
CHANGED
|
@@ -63,6 +63,8 @@ import {
|
|
|
63
63
|
type RigEvent,
|
|
64
64
|
type RigLinkedMeshAttachment,
|
|
65
65
|
type RigMeshAttachment,
|
|
66
|
+
type RigSegmentsFalloff,
|
|
67
|
+
RIG_INFLUENCE_SHAPES,
|
|
66
68
|
type RigMeshBinding,
|
|
67
69
|
type RigPathAttachment,
|
|
68
70
|
type RigRegionAttachment,
|
|
@@ -87,9 +89,12 @@ import {
|
|
|
87
89
|
ringControlAngles,
|
|
88
90
|
buildSegmentsLattice,
|
|
89
91
|
segmentShares,
|
|
92
|
+
influenceShares,
|
|
90
93
|
traceOutline,
|
|
91
94
|
type MeshBoneRef,
|
|
92
|
-
type
|
|
95
|
+
type EntrySegment,
|
|
96
|
+
type SegmentsInfluence,
|
|
97
|
+
type SegmentsFalloff,
|
|
93
98
|
type MeshFitReport,
|
|
94
99
|
type MeshGeometry,
|
|
95
100
|
type MeshVertexWeight,
|
|
@@ -6502,6 +6507,95 @@ function finitePair(v: unknown): [number, number] | null {
|
|
|
6502
6507
|
: null;
|
|
6503
6508
|
}
|
|
6504
6509
|
|
|
6510
|
+
/** The `falloff` block's four numbers, each refused by name when it cannot be one. */
|
|
6511
|
+
function segmentsFalloffOf(f: RigSegmentsFalloff, where: string): SegmentsFalloff {
|
|
6512
|
+
if (typeof f.radius !== 'number' || !Number.isFinite(f.radius) || !(f.radius > 0)) {
|
|
6513
|
+
throw new CompileError(
|
|
6514
|
+
`${where}: "falloff.radius" is ${JSON.stringify(f.radius)}; it is added to every distance before the power is ` +
|
|
6515
|
+
"taken, a positive number of the part's pixels — at 0 a vertex on a segment would carry an infinite pull",
|
|
6516
|
+
);
|
|
6517
|
+
}
|
|
6518
|
+
const power = f.power ?? SEGMENTS_DEFAULTS.power;
|
|
6519
|
+
if (typeof power !== 'number' || !Number.isFinite(power) || !(power > 0)) {
|
|
6520
|
+
throw new CompileError(
|
|
6521
|
+
`${where}: "falloff.power" is ${JSON.stringify(power)}; it is the exponent a pull falls off with distance by, ` +
|
|
6522
|
+
'a positive number — at 0 or below the far segment pulls as hard as the near one, or harder',
|
|
6523
|
+
);
|
|
6524
|
+
}
|
|
6525
|
+
const maxBones = f.maxBones ?? SEGMENTS_DEFAULTS.maxBones;
|
|
6526
|
+
if (typeof maxBones !== 'number' || !Number.isInteger(maxBones) || maxBones < 1) {
|
|
6527
|
+
throw new CompileError(
|
|
6528
|
+
`${where}: "falloff.maxBones" is ${JSON.stringify(maxBones)}; it is how many bones may pull one vertex, a ` +
|
|
6529
|
+
'whole number of at least 1',
|
|
6530
|
+
);
|
|
6531
|
+
}
|
|
6532
|
+
const minWeight = f.minWeight ?? SEGMENTS_DEFAULTS.minWeight;
|
|
6533
|
+
if (typeof minWeight !== 'number' || !Number.isFinite(minWeight) || minWeight < SEGMENTS_WEIGHT_STEP || minWeight >= 1) {
|
|
6534
|
+
throw new CompileError(
|
|
6535
|
+
`${where}: "falloff.minWeight" is ${JSON.stringify(minWeight)}; it is the share under which a bone's pull ` +
|
|
6536
|
+
`is dropped, so it lies in ${SEGMENTS_WEIGHT_STEP}..1 with 1 excluded — at 1 or above every share is dropped, ` +
|
|
6537
|
+
`and under ${SEGMENTS_WEIGHT_STEP}, one step of the 6-decimal grid weights are written on, a kept binding ` +
|
|
6538
|
+
'could be written as 0',
|
|
6539
|
+
);
|
|
6540
|
+
}
|
|
6541
|
+
return { power, radius: f.radius, maxBones, minWeight };
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
/**
|
|
6545
|
+
* The `influence` block's four fields (issue #1415), each refused by name: none
|
|
6546
|
+
* has a default, because the reach and the cap are judgements about this
|
|
6547
|
+
* cloth and the shape decides how a link enters a vertex's blend. Stated
|
|
6548
|
+
* beside `falloff` it is refused too — the two are two models of one share.
|
|
6549
|
+
*/
|
|
6550
|
+
function segmentsInfluenceOf(
|
|
6551
|
+
generator: Extract<NonNullable<RigMeshAttachment['generator']>, { kind: 'segments' }>,
|
|
6552
|
+
where: string,
|
|
6553
|
+
): SegmentsInfluence {
|
|
6554
|
+
const spec: unknown = generator.influence;
|
|
6555
|
+
if (generator.falloff !== undefined) {
|
|
6556
|
+
throw new CompileError(
|
|
6557
|
+
`${where}: the "segments" generator states both "falloff" and "influence". Both turn a distance into a ` +
|
|
6558
|
+
'share — "falloff" as 1 / (d + radius)^power over every segment, "influence" as a pull that reaches zero at ' +
|
|
6559
|
+
'its "radius" — and a mesh is weighted by one of them: remove the other',
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
|
|
6563
|
+
throw new CompileError(
|
|
6564
|
+
`${where}: "influence" is ${JSON.stringify(spec)}; it is { "radius", "across", "max", "shape" }, every one required`,
|
|
6565
|
+
);
|
|
6566
|
+
}
|
|
6567
|
+
const i = spec as Partial<Record<keyof SegmentsInfluence, unknown>>;
|
|
6568
|
+
if (typeof i.radius !== 'number' || !Number.isFinite(i.radius) || !(i.radius > 0)) {
|
|
6569
|
+
throw new CompileError(
|
|
6570
|
+
`${where}: "influence.radius" is ${JSON.stringify(i.radius) ?? 'absent'}; it is how far a link reaches, in ` +
|
|
6571
|
+
"the part's pixels, a positive number with no default — a length on this art: a link whose segment is that " +
|
|
6572
|
+
'far from a vertex or farther never pulls it',
|
|
6573
|
+
);
|
|
6574
|
+
}
|
|
6575
|
+
if (typeof i.across !== 'boolean') {
|
|
6576
|
+
throw new CompileError(
|
|
6577
|
+
`${where}: "influence.across" is ${JSON.stringify(i.across) ?? 'absent'}; it is true (every link within ` +
|
|
6578
|
+
'"radius" pulls, across the entries of "bones") or false (only the links of the entry nearest the vertex), ' +
|
|
6579
|
+
"with no default — whether a chain may pull its neighbour's cloth is the decision this field is",
|
|
6580
|
+
);
|
|
6581
|
+
}
|
|
6582
|
+
if (typeof i.max !== 'number' || !Number.isInteger(i.max) || i.max < 1) {
|
|
6583
|
+
throw new CompileError(
|
|
6584
|
+
`${where}: "influence.max" is ${JSON.stringify(i.max) ?? 'absent'}; it is how many links may pull one vertex, ` +
|
|
6585
|
+
'the strongest kept, a whole number of at least 1 with no default',
|
|
6586
|
+
);
|
|
6587
|
+
}
|
|
6588
|
+
const shape = RIG_INFLUENCE_SHAPES.find((name) => name === i.shape);
|
|
6589
|
+
if (shape === undefined) {
|
|
6590
|
+
throw new CompileError(
|
|
6591
|
+
`${where}: "influence.shape" is ${JSON.stringify(i.shape) ?? 'absent'}; one of ` +
|
|
6592
|
+
`${RIG_INFLUENCE_SHAPES.map((name) => JSON.stringify(name)).join(', ')} — how a pull falls from 1 on a link ` +
|
|
6593
|
+
'to 0 at "radius", with no default: "linear" is 1 - d/radius, "smooth" arrives at both ends with zero slope',
|
|
6594
|
+
);
|
|
6595
|
+
}
|
|
6596
|
+
return { radius: i.radius, across: i.across, max: i.max, shape };
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6505
6599
|
/**
|
|
6506
6600
|
* Build a `segments` mesh: a lattice over the part's alpha, weighted by distance
|
|
6507
6601
|
* to the bone segments `bones` names.
|
|
@@ -6550,45 +6644,18 @@ function buildSegmentsAttachment(
|
|
|
6550
6644
|
);
|
|
6551
6645
|
}
|
|
6552
6646
|
|
|
6553
|
-
// -- the falloff
|
|
6647
|
+
// -- the influence (issue #1415), or else the falloff ----------------------
|
|
6648
|
+
const influence = generator.influence === undefined ? null : segmentsInfluenceOf(generator, where);
|
|
6554
6649
|
const falloffSpec: unknown = generator.falloff;
|
|
6555
|
-
if (typeof falloffSpec !== 'object' || falloffSpec === null || Array.isArray(falloffSpec)) {
|
|
6650
|
+
if (influence === null && (typeof falloffSpec !== 'object' || falloffSpec === null || Array.isArray(falloffSpec))) {
|
|
6556
6651
|
throw new CompileError(
|
|
6557
6652
|
`${where}: the "segments" generator states no "falloff" block. Its "radius" — added to every distance, in ` +
|
|
6558
6653
|
"the part's pixels — has no default, because it is a length on this art: " +
|
|
6559
|
-
'"falloff": { "radius": <pixels> } is the least it takes'
|
|
6560
|
-
|
|
6561
|
-
}
|
|
6562
|
-
const f = generator.falloff;
|
|
6563
|
-
if (typeof f.radius !== 'number' || !Number.isFinite(f.radius) || !(f.radius > 0)) {
|
|
6564
|
-
throw new CompileError(
|
|
6565
|
-
`${where}: "falloff.radius" is ${JSON.stringify(f.radius)}; it is added to every distance before the power is ` +
|
|
6566
|
-
"taken, a positive number of the part's pixels — at 0 a vertex on a segment would carry an infinite pull",
|
|
6567
|
-
);
|
|
6568
|
-
}
|
|
6569
|
-
const power = f.power ?? SEGMENTS_DEFAULTS.power;
|
|
6570
|
-
if (typeof power !== 'number' || !Number.isFinite(power) || !(power > 0)) {
|
|
6571
|
-
throw new CompileError(
|
|
6572
|
-
`${where}: "falloff.power" is ${JSON.stringify(power)}; it is the exponent a pull falls off with distance by, ` +
|
|
6573
|
-
'a positive number — at 0 or below the far segment pulls as hard as the near one, or harder',
|
|
6574
|
-
);
|
|
6575
|
-
}
|
|
6576
|
-
const maxBones = f.maxBones ?? SEGMENTS_DEFAULTS.maxBones;
|
|
6577
|
-
if (typeof maxBones !== 'number' || !Number.isInteger(maxBones) || maxBones < 1) {
|
|
6578
|
-
throw new CompileError(
|
|
6579
|
-
`${where}: "falloff.maxBones" is ${JSON.stringify(maxBones)}; it is how many bones may pull one vertex, a ` +
|
|
6580
|
-
'whole number of at least 1',
|
|
6581
|
-
);
|
|
6582
|
-
}
|
|
6583
|
-
const minWeight = f.minWeight ?? SEGMENTS_DEFAULTS.minWeight;
|
|
6584
|
-
if (typeof minWeight !== 'number' || !Number.isFinite(minWeight) || minWeight < SEGMENTS_WEIGHT_STEP || minWeight >= 1) {
|
|
6585
|
-
throw new CompileError(
|
|
6586
|
-
`${where}: "falloff.minWeight" is ${JSON.stringify(minWeight)}; it is the share under which a bone's pull ` +
|
|
6587
|
-
`is dropped, so it lies in ${SEGMENTS_WEIGHT_STEP}..1 with 1 excluded — at 1 or above every share is dropped, ` +
|
|
6588
|
-
`and under ${SEGMENTS_WEIGHT_STEP}, one step of the 6-decimal grid weights are written on, a kept binding ` +
|
|
6589
|
-
'could be written as 0',
|
|
6654
|
+
'"falloff": { "radius": <pixels> } is the least it takes (or an "influence" block, whose pull reaches zero ' +
|
|
6655
|
+
'at its own "radius")',
|
|
6590
6656
|
);
|
|
6591
6657
|
}
|
|
6658
|
+
const falloff = influence === null ? segmentsFalloffOf(generator.falloff!, where) : null;
|
|
6592
6659
|
const threshold = generator.alpha ?? SEGMENTS_DEFAULTS.alpha;
|
|
6593
6660
|
|
|
6594
6661
|
// -- the lattice -----------------------------------------------------------
|
|
@@ -6650,7 +6717,7 @@ function buildSegmentsAttachment(
|
|
|
6650
6717
|
const parentOf = new Map(ctx.bones.map((b) => [b.name, b.parent ?? null]));
|
|
6651
6718
|
const named: string[] = [];
|
|
6652
6719
|
const refs: MeshBoneRef[] = [];
|
|
6653
|
-
const segments:
|
|
6720
|
+
const segments: EntrySegment[] = [];
|
|
6654
6721
|
const boneOf = (name: unknown, at: string): { index: number; transform: BoneTransform } => {
|
|
6655
6722
|
if (typeof name !== 'string' || !parentOf.has(name)) {
|
|
6656
6723
|
throw new CompileError(`${where}: ${at} names mesh bone ${JSON.stringify(name)}, which is not in the rig's bone list`);
|
|
@@ -6680,7 +6747,7 @@ function buildSegmentsAttachment(
|
|
|
6680
6747
|
const at = `"bones"[${i}]`;
|
|
6681
6748
|
if (typeof entry === 'string') {
|
|
6682
6749
|
const { index, transform } = boneOf(entry, at);
|
|
6683
|
-
segments.push({ bone: index, a: [transform.worldX, transform.worldY], b: tipOf(entry, transform, at, false) });
|
|
6750
|
+
segments.push({ bone: index, entry: i, a: [transform.worldX, transform.worldY], b: tipOf(entry, transform, at, false) });
|
|
6684
6751
|
return;
|
|
6685
6752
|
}
|
|
6686
6753
|
if (Array.isArray(entry)) {
|
|
@@ -6692,7 +6759,7 @@ function buildSegmentsAttachment(
|
|
|
6692
6759
|
const name = link.name as string;
|
|
6693
6760
|
const origin: [number, number] = [link.transform.worldX, link.transform.worldY];
|
|
6694
6761
|
if (k === links.length - 1) {
|
|
6695
|
-
segments.push({ bone: link.index, a: origin, b: tipOf(name, link.transform, `${at}[${k}]`, true) });
|
|
6762
|
+
segments.push({ bone: link.index, entry: i, a: origin, b: tipOf(name, link.transform, `${at}[${k}]`, true) });
|
|
6696
6763
|
return;
|
|
6697
6764
|
}
|
|
6698
6765
|
const next = links[k + 1];
|
|
@@ -6714,7 +6781,7 @@ function buildSegmentsAttachment(
|
|
|
6714
6781
|
`"${nextName}" its own position, or name "${name}" alone with a "length"`,
|
|
6715
6782
|
);
|
|
6716
6783
|
}
|
|
6717
|
-
segments.push({ bone: link.index, a: origin, b });
|
|
6784
|
+
segments.push({ bone: link.index, entry: i, a: origin, b });
|
|
6718
6785
|
});
|
|
6719
6786
|
return;
|
|
6720
6787
|
}
|
|
@@ -6729,7 +6796,7 @@ function buildSegmentsAttachment(
|
|
|
6729
6796
|
`${JSON.stringify(from === null ? span.from : span.to)}; both ends are [x, y] in the part's pixels, y down`,
|
|
6730
6797
|
);
|
|
6731
6798
|
}
|
|
6732
|
-
segments.push({ bone: index, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
|
|
6799
|
+
segments.push({ bone: index, entry: i, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
|
|
6733
6800
|
return;
|
|
6734
6801
|
}
|
|
6735
6802
|
throw new CompileError(
|
|
@@ -6739,10 +6806,27 @@ function buildSegmentsAttachment(
|
|
|
6739
6806
|
});
|
|
6740
6807
|
|
|
6741
6808
|
// -- the weights -----------------------------------------------------------
|
|
6742
|
-
|
|
6809
|
+
let capped = 0;
|
|
6810
|
+
let widest = 0;
|
|
6743
6811
|
const weights: MeshVertexWeight[][] = lattice.points.map(([px, py], v) => {
|
|
6744
|
-
|
|
6812
|
+
if (influence !== null) {
|
|
6813
|
+
const reached = influenceShares(measuredAt(px, py), segments, influence);
|
|
6814
|
+
if ('weights' in reached) {
|
|
6815
|
+
if (reached.inReach > influence.max) capped++;
|
|
6816
|
+
widest = Math.max(widest, reached.inReach);
|
|
6817
|
+
return reached.weights;
|
|
6818
|
+
}
|
|
6819
|
+
throw new CompileError(
|
|
6820
|
+
`${where}: vertex ${v}, at (${px}, ${py}) in the part's pixels, is reached by no link — the nearest is ` +
|
|
6821
|
+
`"${named[reached.nearest.bone]}" at ${reached.nearest.distance.toFixed(4)}, and "influence.radius" is ` +
|
|
6822
|
+
`${influence.radius}${influence.across ? '' : ' (with "across": false, the nearest entry\'s links only)'}. ` +
|
|
6823
|
+
'A vertex needs a link, and rigc will not hand it one the reach does not: raise "radius" over that distance, ' +
|
|
6824
|
+
'or name a segment nearer this part of the art',
|
|
6825
|
+
);
|
|
6826
|
+
}
|
|
6827
|
+
const got = segmentShares(measuredAt(px, py), segments, falloff!);
|
|
6745
6828
|
if ('weights' in got) return got.weights;
|
|
6829
|
+
const { maxBones, minWeight } = falloff!;
|
|
6746
6830
|
throw new CompileError(
|
|
6747
6831
|
`${where}: vertex ${v}, at (${px}, ${py}) in the part's pixels, keeps no bone — the ${got.dropped.length} ` +
|
|
6748
6832
|
`strongest pull(s) share it as ${got.dropped.map((d) => `"${named[d.bone]}" ${d.share.toFixed(4)}`).join(', ')}, ` +
|
|
@@ -6803,6 +6887,7 @@ function buildSegmentsAttachment(
|
|
|
6803
6887
|
artCells: lattice.report.artCells,
|
|
6804
6888
|
keptCells: lattice.report.keptCells,
|
|
6805
6889
|
islands: lattice.report.islands,
|
|
6890
|
+
...(influence === null ? {} : { reach: { ...influence, capped, widest } }),
|
|
6806
6891
|
},
|
|
6807
6892
|
});
|
|
6808
6893
|
return {
|
package/src/mesh.ts
CHANGED
|
@@ -2432,3 +2432,139 @@ export function segmentShares(
|
|
|
2432
2432
|
});
|
|
2433
2433
|
return { weights };
|
|
2434
2434
|
}
|
|
2435
|
+
|
|
2436
|
+
// ---------------------------------------------------------------------------
|
|
2437
|
+
// influence — a falloff that reaches zero at a stated distance (issue #1415)
|
|
2438
|
+
// ---------------------------------------------------------------------------
|
|
2439
|
+
//
|
|
2440
|
+
// `segmentShares` above never lets a segment go: `1 / (d + r)^p` is positive at
|
|
2441
|
+
// every distance, so the only way to widen a blend is a larger `r`, which
|
|
2442
|
+
// flattens it everywhere at once, and the only way to end one is `minWeight`,
|
|
2443
|
+
// a share rather than a distance. A cloth surface wants the other model: a
|
|
2444
|
+
// vertex follows every link NEAR it, by a pull that falls to exactly zero at a
|
|
2445
|
+
// stated reach, so a link enters and leaves a vertex's blend continuously.
|
|
2446
|
+
//
|
|
2447
|
+
// 1. **Distance.** `d` is the distance from the vertex to each link's
|
|
2448
|
+
// segment — its nearest point, `segmentDistance`, the same reading the
|
|
2449
|
+
// falloff takes — at the setup pose. To the segment rather than to the
|
|
2450
|
+
// bone's origin: a vertex on the middle of a link IS on that link, and an
|
|
2451
|
+
// origin metric would put it half a link away while handing the joint at
|
|
2452
|
+
// the link's tip to the child whose origin sits there.
|
|
2453
|
+
// 2. **Across.** With `across: false` only the entry (a chain, a bone, a
|
|
2454
|
+
// span) holding the nearest link is kept — the earlier entry on a tie.
|
|
2455
|
+
// 3. **Reach.** A link with `d < radius` pulls `shape(d / radius)`, and one
|
|
2456
|
+
// whose pull is not above 0 — a hair inside the reach, in doubles — pulls
|
|
2457
|
+
// nothing and is out of reach; a bone two segments name keeps the nearer.
|
|
2458
|
+
// A vertex no link reaches is refused by the caller, naming the nearest
|
|
2459
|
+
// distance.
|
|
2460
|
+
// 4. **Cap.** The strongest `max` pulls are kept — equal pulls in the order
|
|
2461
|
+
// the bones were first named — and normalised; a share the 6-decimal grid
|
|
2462
|
+
// would write as 0 is dropped and the rest normalised again.
|
|
2463
|
+
// 5. **Order.** The kept bones are written in the order they were first
|
|
2464
|
+
// named, on every vertex, so an attachment has one bone order by
|
|
2465
|
+
// construction (`A52_WEIGHTED_VERTICES_SHARE_ONE_BONE_ORDER`); the
|
|
2466
|
+
// strongest closes the vertex at exactly 1 on the grid.
|
|
2467
|
+
|
|
2468
|
+
/** One candidate segment with the `bones` entry it came from — what `across: false` chooses between. */
|
|
2469
|
+
export interface EntrySegment extends WorldSegment {
|
|
2470
|
+
/** Index of the `bones` entry this segment belongs to. */
|
|
2471
|
+
entry: number;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/** The four fields an `influence` block states — every one of them required by the caller. */
|
|
2475
|
+
export interface SegmentsInfluence {
|
|
2476
|
+
radius: number;
|
|
2477
|
+
across: boolean;
|
|
2478
|
+
max: number;
|
|
2479
|
+
shape: 'linear' | 'smooth';
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
/** A pull at `x = d / radius` in `[0, 1]`: 1 at the segment, 0 at the reach. */
|
|
2483
|
+
export function influencePull(shape: SegmentsInfluence['shape'], x: number): number {
|
|
2484
|
+
return shape === 'linear' ? 1 - x : 1 - x * x * (3 - 2 * x);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
/** The steps a selftest plant may switch off, one key each — never passed by a build. */
|
|
2488
|
+
export interface InfluencePlant {
|
|
2489
|
+
ignoreCap?: true;
|
|
2490
|
+
ignoreReach?: true;
|
|
2491
|
+
ignoreAcross?: true;
|
|
2492
|
+
skipNormalise?: true;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
/** What `influenceShares` found at one vertex. */
|
|
2496
|
+
export type InfluenceShares =
|
|
2497
|
+
| {
|
|
2498
|
+
/** The bindings, in first-named bone order, closing at exactly 1 on the 6-decimal grid. */
|
|
2499
|
+
weights: MeshVertexWeight[];
|
|
2500
|
+
/** How many bones were in reach before the cap — over `max` means the cap dropped some. */
|
|
2501
|
+
inReach: number;
|
|
2502
|
+
}
|
|
2503
|
+
| {
|
|
2504
|
+
/** No link is within `radius`: the nearest one's distance, and its bone. */
|
|
2505
|
+
nearest: { bone: number; distance: number };
|
|
2506
|
+
};
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* The shares on one vertex under an `influence` block — see the section header.
|
|
2510
|
+
* `plant` exists for the selftest's negative controls: each key switches one
|
|
2511
|
+
* step off, so a control can show its own check going red on weights that
|
|
2512
|
+
* break exactly that step. A build never passes it.
|
|
2513
|
+
*/
|
|
2514
|
+
export function influenceShares(
|
|
2515
|
+
p: readonly [number, number],
|
|
2516
|
+
segments: readonly EntrySegment[],
|
|
2517
|
+
influence: SegmentsInfluence,
|
|
2518
|
+
plant: InfluencePlant = {},
|
|
2519
|
+
): InfluenceShares {
|
|
2520
|
+
const distances = segments.map((s) => segmentDistance(p, s.a, s.b));
|
|
2521
|
+
let home = -1;
|
|
2522
|
+
let homeD = Infinity;
|
|
2523
|
+
let nearestBone = 0;
|
|
2524
|
+
distances.forEach((d, i) => {
|
|
2525
|
+
if (d < homeD) {
|
|
2526
|
+
homeD = d;
|
|
2527
|
+
home = segments[i].entry;
|
|
2528
|
+
nearestBone = segments[i].bone;
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
const byBone = new Map<number, number>();
|
|
2532
|
+
segments.forEach((s, i) => {
|
|
2533
|
+
if (!influence.across && plant.ignoreAcross === undefined && s.entry !== home) return;
|
|
2534
|
+
const d = distances[i];
|
|
2535
|
+
if (!(d < influence.radius) && plant.ignoreReach === undefined) return;
|
|
2536
|
+
const was = byBone.get(s.bone);
|
|
2537
|
+
if (was === undefined || d < was) byBone.set(s.bone, d);
|
|
2538
|
+
});
|
|
2539
|
+
// Strongest first; equal pulls by the order the bones were first named. A link a hair inside the reach can
|
|
2540
|
+
// pull 0 — or a rounding's worth under it — in doubles (`1 - x²(3 - 2x)` at x one ulp under 1): it pulls
|
|
2541
|
+
// nothing, so it is out of reach like the links past it, and no share is ever made up for it.
|
|
2542
|
+
const pulls = [...byBone.entries()]
|
|
2543
|
+
.map(([bone, d]) => ({ bone, pull: influencePull(influence.shape, Math.min(d / influence.radius, 1)) }))
|
|
2544
|
+
.filter((e) => e.pull > 0 || plant.ignoreReach !== undefined)
|
|
2545
|
+
.sort((x, y) => y.pull - x.pull || x.bone - y.bone);
|
|
2546
|
+
if (pulls.length === 0) return { nearest: { bone: nearestBone, distance: homeD } };
|
|
2547
|
+
const inReach = pulls.length;
|
|
2548
|
+
const capped = plant.ignoreCap === undefined ? pulls.slice(0, influence.max) : pulls;
|
|
2549
|
+
// Only the planted `ignoreReach` keeps a pull of 0 (a link at or past the reach): it is given one grid step,
|
|
2550
|
+
// so the plant still binds that link, which is the plant's whole point.
|
|
2551
|
+
const raw = capped.map((e) => ({ bone: e.bone, pull: e.pull > 0 ? e.pull : 0.000001 }));
|
|
2552
|
+
let sum = 0;
|
|
2553
|
+
for (const e of raw) sum += e.pull;
|
|
2554
|
+
const normalise = plant.skipNormalise === undefined;
|
|
2555
|
+
const first = raw.map((e) => ({ bone: e.bone, share: normalise ? e.pull / sum : e.pull }));
|
|
2556
|
+
const kept = first.filter((e) => r6(e.share) !== 0);
|
|
2557
|
+
let sum2 = 0;
|
|
2558
|
+
for (const e of kept) sum2 += e.share;
|
|
2559
|
+
const rounded = kept.map((e) => r6(normalise ? e.share / sum2 : e.share));
|
|
2560
|
+
// `kept[0]` is the strongest — the filter keeps the order — and it closes the vertex at exactly 1.
|
|
2561
|
+
if (normalise) {
|
|
2562
|
+
let others = 0;
|
|
2563
|
+
for (let k = 1; k < rounded.length; k++) others += rounded[k];
|
|
2564
|
+
rounded[0] = r6(1 - others);
|
|
2565
|
+
}
|
|
2566
|
+
const weights: MeshVertexWeight[] = kept
|
|
2567
|
+
.map((e, k) => ({ bone: 'control' as const, control: e.bone, weight: rounded[k] }))
|
|
2568
|
+
.sort((x, y) => x.control - y.control);
|
|
2569
|
+
return { weights, inReach };
|
|
2570
|
+
}
|
package/src/rig.ts
CHANGED
|
@@ -683,6 +683,53 @@ export interface RigSegmentsFalloff {
|
|
|
683
683
|
minWeight?: number;
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
+
/** The two curves an `influence` pull falls to zero along — see `RigSegmentsInfluence.shape`. */
|
|
687
|
+
export const RIG_INFLUENCE_SHAPES = ['linear', 'smooth'] as const;
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* A falloff that **reaches zero** at a stated distance, so a vertex follows
|
|
691
|
+
* every chain link near it and no link far from it (issue #1415) — the model a
|
|
692
|
+
* cloth surface needs, where a vertex between two chains blends the springs of
|
|
693
|
+
* both and the surface bends as one sheet rather than as strips.
|
|
694
|
+
*
|
|
695
|
+
* Per vertex: `d` is the distance to each link's **segment** (its nearest
|
|
696
|
+
* point, the same distance `falloff` measures), read at the setup pose. A link
|
|
697
|
+
* with `d < radius` pulls `shape(d / radius)` — one whose pull is not above 0
|
|
698
|
+
* in doubles, a hair inside the reach, pulls nothing; the strongest `max` pulls are
|
|
699
|
+
* kept — equal pulls in the order `bones` first names their bones — and
|
|
700
|
+
* normalised; a share the 6-decimal grid would write as 0 is dropped and the
|
|
701
|
+
* rest normalised again. Each vertex lists its bones in the order `bones`
|
|
702
|
+
* names them, so the attachment has one bone order by construction (A52).
|
|
703
|
+
*
|
|
704
|
+
* ⭐ Every number is a field and none has a default: the reach and the cap are
|
|
705
|
+
* judgements about this cloth, and the shape is the one thing that decides
|
|
706
|
+
* how a link enters a vertex's blend.
|
|
707
|
+
*/
|
|
708
|
+
export interface RigSegmentsInfluence {
|
|
709
|
+
/**
|
|
710
|
+
* **Required.** How far a link reaches, in the part's pixels (the skeleton's
|
|
711
|
+
* units on this generator's route): a link whose segment is `radius` or
|
|
712
|
+
* farther from a vertex never pulls it. Positive.
|
|
713
|
+
*/
|
|
714
|
+
radius: number;
|
|
715
|
+
/**
|
|
716
|
+
* **Required.** `true`: every link of every `bones` entry within `radius`
|
|
717
|
+
* pulls. `false`: only the links of ONE entry — the entry holding the link
|
|
718
|
+
* nearest the vertex, the earlier entry on a tie — so a chain moves its own
|
|
719
|
+
* strip and nothing across.
|
|
720
|
+
*/
|
|
721
|
+
across: boolean;
|
|
722
|
+
/** **Required.** At most this many links pull one vertex — the strongest. A whole number of at least 1. */
|
|
723
|
+
max: number;
|
|
724
|
+
/**
|
|
725
|
+
* **Required.** How a pull falls from 1 at `d = 0` to 0 at `d = radius`, with
|
|
726
|
+
* `x = d / radius`: `"linear"` is `1 - x`; `"smooth"` is `1 - x²(3 - 2x)`,
|
|
727
|
+
* which also arrives at both ends with zero slope, so a link enters and leaves
|
|
728
|
+
* a vertex's blend without a crease.
|
|
729
|
+
*/
|
|
730
|
+
shape: (typeof RIG_INFLUENCE_SHAPES)[number];
|
|
731
|
+
}
|
|
732
|
+
|
|
686
733
|
/**
|
|
687
734
|
* A lattice over the part's alpha, weighted by distance to named bone segments
|
|
688
735
|
* (`buildSegmentsLattice` and `segmentWeights` in [`mesh.ts`](mesh.ts)).
|
|
@@ -703,8 +750,14 @@ export interface RigSegmentsGenerator {
|
|
|
703
750
|
* or a `RigSegmentSpan` stated outright.
|
|
704
751
|
*/
|
|
705
752
|
bones: Array<string | string[] | RigSegmentSpan>;
|
|
706
|
-
/**
|
|
707
|
-
|
|
753
|
+
/**
|
|
754
|
+
* The falloff — see `RigSegmentsFalloff`. Exactly one of `falloff` and
|
|
755
|
+
* `influence` is stated: both turn a distance into a share, in two models,
|
|
756
|
+
* and a mesh weighted by both would be weighted by neither.
|
|
757
|
+
*/
|
|
758
|
+
falloff?: RigSegmentsFalloff;
|
|
759
|
+
/** A falloff that reaches zero at a stated distance — see `RigSegmentsInfluence`. */
|
|
760
|
+
influence?: RigSegmentsInfluence;
|
|
708
761
|
/** Alpha at or above which a pixel counts as art, 1..255. Default 1 — `contour`'s own. */
|
|
709
762
|
alpha?: number;
|
|
710
763
|
/**
|
|
@@ -1965,8 +2018,9 @@ export const RIG_KEYS = {
|
|
|
1965
2018
|
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
1966
2019
|
RigContourGenerator: ['kind', 'tolerance', 'margin', 'maxVertices', 'alpha', 'depth', 'soft'],
|
|
1967
2020
|
RigGridGenerator: ['kind', 'us', 'vs', 'cols', 'rows', 'depth', 'soft'],
|
|
1968
|
-
RigSegmentsGenerator: ['kind', 'cell', 'bones', 'falloff', 'alpha', 'anchor'],
|
|
2021
|
+
RigSegmentsGenerator: ['kind', 'cell', 'bones', 'falloff', 'influence', 'alpha', 'anchor'],
|
|
1969
2022
|
RigSegmentsFalloff: ['power', 'radius', 'maxBones', 'minWeight'],
|
|
2023
|
+
RigSegmentsInfluence: ['radius', 'across', 'max', 'shape'],
|
|
1970
2024
|
RigSegmentSpan: ['bone', 'from', 'to'],
|
|
1971
2025
|
RigMeshBias: ['axis_deg', 'ramp'],
|
|
1972
2026
|
RigDepthMap: ['image', 'near', 'zScale', 'gamma', 'contrast', 'bias'],
|
|
@@ -2101,8 +2155,11 @@ export const RIG_TYPES = {
|
|
|
2101
2155
|
soft: 'object',
|
|
2102
2156
|
},
|
|
2103
2157
|
RigGridGenerator: { kind: 'enum', us: 'number[]', vs: 'number[]', cols: 'number', rows: 'number', depth: 'object', soft: 'object' },
|
|
2104
|
-
RigSegmentsGenerator: {
|
|
2158
|
+
RigSegmentsGenerator: {
|
|
2159
|
+
kind: 'enum', cell: 'number', bones: 'mixed', falloff: 'object', influence: 'object', alpha: 'number', anchor: 'number[]',
|
|
2160
|
+
},
|
|
2105
2161
|
RigSegmentsFalloff: { power: 'number', radius: 'number', maxBones: 'number', minWeight: 'number' },
|
|
2162
|
+
RigSegmentsInfluence: { radius: 'number', across: 'boolean', max: 'number', shape: 'enum' },
|
|
2106
2163
|
RigSegmentSpan: { bone: 'string', from: 'number[]', to: 'number[]' },
|
|
2107
2164
|
RigMeshBias: { axis_deg: 'number', ramp: 'number[]' },
|
|
2108
2165
|
RigDepthMap: { image: 'string', near: 'enum', zScale: 'number', gamma: 'number', contrast: 'number', bias: 'number' },
|
|
@@ -2170,6 +2227,7 @@ export const RIG_ENUMS = {
|
|
|
2170
2227
|
RigContourGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2171
2228
|
RigGridGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2172
2229
|
RigSegmentsGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2230
|
+
RigSegmentsInfluence: { shape: { owner: 'segmentsInfluenceOf' } },
|
|
2173
2231
|
RigDepthMap: { near: { owner: 'sampleMeshDepth' } },
|
|
2174
2232
|
} as const satisfies SpecEnumTable<typeof RIG_TYPES>;
|
|
2175
2233
|
|
|
@@ -2438,6 +2496,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2438
2496
|
at(gen.depth, 'RigDepthMap', `${who} generator.depth`, [...here, 'generator', 'depth']);
|
|
2439
2497
|
at(gen.soft, 'RigSoftRegion', `${who} generator.soft`, [...here, 'generator', 'soft']);
|
|
2440
2498
|
at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`, [...here, 'generator', 'falloff']);
|
|
2499
|
+
at(gen.influence, 'RigSegmentsInfluence', `${who} generator.influence`, [...here, 'generator', 'influence']);
|
|
2441
2500
|
for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
|
|
2442
2501
|
at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`, [...here, 'generator', 'bones', i]);
|
|
2443
2502
|
}
|
package/src/types.ts
CHANGED
|
@@ -1679,6 +1679,20 @@ export interface CompileResult {
|
|
|
1679
1679
|
artCells: number;
|
|
1680
1680
|
keptCells: number;
|
|
1681
1681
|
islands: number;
|
|
1682
|
+
/**
|
|
1683
|
+
* Present only when the mesh is weighted by an `influence` block (issue
|
|
1684
|
+
* #1415): the block as stated, how many vertices had more links in reach
|
|
1685
|
+
* than `max` — the vertices the cap dropped a link from — and the most
|
|
1686
|
+
* links in reach at any one vertex, which is the `max` that would drop none.
|
|
1687
|
+
*/
|
|
1688
|
+
reach?: {
|
|
1689
|
+
radius: number;
|
|
1690
|
+
across: boolean;
|
|
1691
|
+
max: number;
|
|
1692
|
+
shape: 'linear' | 'smooth';
|
|
1693
|
+
capped: number;
|
|
1694
|
+
widest: number;
|
|
1695
|
+
};
|
|
1682
1696
|
};
|
|
1683
1697
|
/**
|
|
1684
1698
|
* What a depth map put on this mesh's vertices, when one was named.
|