spine-rigc 1.6.2 → 1.8.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/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
package/src/compile.ts
CHANGED
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
import { evaluateDeformTransform } from './deformgen.ts';
|
|
118
118
|
import { evaluateTrackDerive, TRACK_DERIVE_PROJECTIONS, type TrackDeriveMember } from './trackgen.ts';
|
|
119
119
|
import {
|
|
120
|
+
computeExactFrameTransforms,
|
|
120
121
|
computeWorldTransforms,
|
|
121
122
|
cropToSpineY,
|
|
122
123
|
normaliseDegrees,
|
|
@@ -128,6 +129,8 @@ import {
|
|
|
128
129
|
type BoneTransform,
|
|
129
130
|
} from './transform.ts';
|
|
130
131
|
import { emitSkeleton, PHYSICS_PARAMS, UNSTATED_REFERENCE_SCALE, type SkeletonHeader } from './emit_spine.ts';
|
|
132
|
+
import { curveLengthTable } from './core/constraints_path.ts';
|
|
133
|
+
import { frameRegionName } from './core/uvs.ts';
|
|
131
134
|
import {
|
|
132
135
|
isModelVertexAttachment,
|
|
133
136
|
type CarriedFromCompileResult,
|
|
@@ -1979,8 +1982,9 @@ function resolveFromAtlas(
|
|
|
1979
1982
|
* ⭐ *Either way* was not true for a pack that turns a region until issue #570:
|
|
1980
1983
|
* `extractRegion` refused a `rotate: 90`, so how the art was delivered decided
|
|
1981
1984
|
* whether a measurement could be taken at all — on a foreign pack, where turning
|
|
1982
|
-
* is the norm rather than the exception. It now
|
|
1983
|
-
*
|
|
1985
|
+
* is the norm rather than the exception. It now lifts every rotation, measured
|
|
1986
|
+
* as `extractRegion` states (`PKR02` against the runtime's own sampling, `PK29`
|
|
1987
|
+
* against the packer's fixture), which is what makes the sentence above a
|
|
1984
1988
|
* description rather than an aspiration.
|
|
1985
1989
|
*/
|
|
1986
1990
|
function partPlate(img: CompiledImage): Plate {
|
|
@@ -2561,8 +2565,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2561
2565
|
}
|
|
2562
2566
|
const boneNames = new Set(bones.map((b) => b.name));
|
|
2563
2567
|
let transforms: Map<string, BoneTransform>;
|
|
2568
|
+
// The frames a depth turn ceiling is measured in — the same bones without the
|
|
2569
|
+
// runtime's last bits (`computeExactFrameTransforms`, issue #1021). Nothing is
|
|
2570
|
+
// bound through them.
|
|
2571
|
+
let exactFrames: Map<string, BoneTransform>;
|
|
2564
2572
|
try {
|
|
2565
2573
|
transforms = computeWorldTransforms(bones);
|
|
2574
|
+
exactFrames = computeExactFrameTransforms(bones);
|
|
2566
2575
|
} catch (err) {
|
|
2567
2576
|
if (err instanceof TransformError) throw new CompileError(err.message);
|
|
2568
2577
|
throw err;
|
|
@@ -2740,7 +2749,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2740
2749
|
|
|
2741
2750
|
if (part) {
|
|
2742
2751
|
const perSlot: Record<string, SkinTableEntry> = {};
|
|
2743
|
-
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, rigSlot.bone) : null;
|
|
2752
|
+
const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, exactFrames, rigSlot.bone) : null;
|
|
2744
2753
|
for (const name of names) {
|
|
2745
2754
|
const img = images.find((im) => im.region === name);
|
|
2746
2755
|
if (!img) throw new CompileError(`internal: no image for attachment ${name}`);
|
|
@@ -2780,6 +2789,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2780
2789
|
images,
|
|
2781
2790
|
bones,
|
|
2782
2791
|
transforms,
|
|
2792
|
+
exactFrames,
|
|
2783
2793
|
meshBones,
|
|
2784
2794
|
meshes,
|
|
2785
2795
|
slotName: rigSlot.name,
|
|
@@ -3767,6 +3777,8 @@ interface AttachmentContext {
|
|
|
3767
3777
|
images: CompiledImage[];
|
|
3768
3778
|
bones: ModelBone[];
|
|
3769
3779
|
transforms: Map<string, BoneTransform>;
|
|
3780
|
+
/** The same bones in `computeExactFrameTransforms`' frames: what a depth turn ceiling is measured in, and nothing else (issue #1021). */
|
|
3781
|
+
exactFrames: Map<string, BoneTransform>;
|
|
3770
3782
|
meshBones: Set<string>;
|
|
3771
3783
|
meshes: CompileResult['meshes'];
|
|
3772
3784
|
slotName: string;
|
|
@@ -3802,10 +3814,12 @@ interface AttachmentContext {
|
|
|
3802
3814
|
* Linked meshes whose `source` is still unresolved — `resolveLinkedMeshes`
|
|
3803
3815
|
* empties it once every skin has been built.
|
|
3804
3816
|
*
|
|
3805
|
-
* ⭐ Deferred
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3808
|
-
*
|
|
3817
|
+
* ⭐ Deferred because the format resolves a link only once every skin is
|
|
3818
|
+
* read — the reader queues each one (`this.linkedMeshes`, `SkeletonJson.js:56`
|
|
3819
|
+
* filled at `:581`) and resolves the queue after the skins (`:427-449`) — so
|
|
3820
|
+
* a link may name a source in a skin or a slot this loop has not reached yet,
|
|
3821
|
+
* and resolving it in place would refuse a correct rig on nothing but
|
|
3822
|
+
* declaration order.
|
|
3809
3823
|
*/
|
|
3810
3824
|
links: PendingLink[];
|
|
3811
3825
|
}
|
|
@@ -4195,8 +4209,11 @@ export function pathChain(points: Array<[number, number]>, closed: boolean): Arr
|
|
|
4195
4209
|
}
|
|
4196
4210
|
|
|
4197
4211
|
/**
|
|
4198
|
-
* Cumulative
|
|
4199
|
-
* **the
|
|
4212
|
+
* Cumulative length at the end of each curve of the chain, in world units —
|
|
4213
|
+
* **the core's curve table** (`curveLengthTable` in
|
|
4214
|
+
* [`src/core/constraints_path.ts`](core/constraints_path.ts), issue #1015), the
|
|
4215
|
+
* one the core's path constraint walks at constant speed. This function only
|
|
4216
|
+
* flattens the chain and counts its curves.
|
|
4200
4217
|
*
|
|
4201
4218
|
* One entry per curve, which is what `lengths[curve]` indexes: the parser walks
|
|
4202
4219
|
* curves with `if (p > lengths[curve]) continue`, and reads `lengths[curveCount]`
|
|
@@ -4206,12 +4223,16 @@ export function pathChain(points: Array<[number, number]>, closed: boolean): Arr
|
|
|
4206
4223
|
* fact about the Bezier, it is the number the consumer of the field computes for
|
|
4207
4224
|
* itself when it is not given one. `PathConstraint.computeWorldPositions`
|
|
4208
4225
|
* (`PathConstraint.js:289-324` in `@esotericsoftware/spine-core` 4.3.13) measures
|
|
4209
|
-
* a `constantSpeed` path
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
* `
|
|
4213
|
-
*
|
|
4214
|
-
*
|
|
4226
|
+
* a `constantSpeed` path for itself, and the Spine editor's exported `lengths`
|
|
4227
|
+
* are the same number. What that number is was measured, not read: the core's
|
|
4228
|
+
* table — four forward-difference steps per curve at `t = 1/4` (`0.1875`,
|
|
4229
|
+
* `0.09375`, `0.75`, `0.16666667`), four `Math.sqrt` terms accumulated into a
|
|
4230
|
+
* running total carried across curves — is the rule its header states with the
|
|
4231
|
+
* readings that missed beside it, and `PS67`, `PS68` and `PS186` in
|
|
4232
|
+
* `selftest.ts` hold this function to `PathConstraint`'s own `curves` array read
|
|
4233
|
+
* off a posed skeleton, **bit for bit**. Since issue #1015 the core suite also
|
|
4234
|
+
* holds the swap: 100,000 seeded chains through this function and through the
|
|
4235
|
+
* arithmetic it replaced, 0 differing.
|
|
4215
4236
|
*
|
|
4216
4237
|
* 🚨 **What that computation is fed decides the number, and this comment once
|
|
4217
4238
|
* claimed more than was measured** (issue #804). It said the loop matched the
|
|
@@ -4223,8 +4244,8 @@ export function pathChain(points: Array<[number, number]>, closed: boolean): Arr
|
|
|
4223
4244
|
* vertices. rigc blends weighted vertices through its own setup transforms,
|
|
4224
4245
|
* which until #804 ignored bone scale, shear and `inherit` — 2.35× the
|
|
4225
4246
|
* runtime's own `curves` on a production rig's weighted path, 0.752× on a
|
|
4226
|
-
* 50/50 probe over a bone at scale 2. `computeWorldTransforms`
|
|
4227
|
-
*
|
|
4247
|
+
* 50/50 probe over a bone at scale 2. `computeWorldTransforms` honours all
|
|
4248
|
+
* three now, and the gap is gone on both.
|
|
4228
4249
|
* - **Constraints.** The runtime measures the pose its update order hands the
|
|
4229
4250
|
* path constraint at the first `updateWorldTransform`: a transform
|
|
4230
4251
|
* constraint on the path's slot bone ordered BEFORE the path constraint
|
|
@@ -4232,11 +4253,11 @@ export function pathChain(points: Array<[number, number]>, closed: boolean): Arr
|
|
|
4232
4253
|
* not. A production 4.2 export's four numbers are the constrained pose's to
|
|
4233
4254
|
* the digit, and its unconstrained setup measures 76.08 against 76.65 on the
|
|
4234
4255
|
* first curve. Reproducing that means solving every constraint type in
|
|
4235
|
-
* update order, which is posing — and `src/compile.ts` does not
|
|
4236
|
-
*
|
|
4237
|
-
*
|
|
4238
|
-
*
|
|
4239
|
-
*
|
|
4256
|
+
* update order, which is posing — and `src/compile.ts` does not pose. ⇒
|
|
4257
|
+
* **An omitted `lengths` is measured on the unconstrained setup pose**, and
|
|
4258
|
+
* a path whose bones a constraint moves at rest gets that figure. What
|
|
4259
|
+
* closes the gap for an editor export is not measuring at all: a stated
|
|
4260
|
+
* `lengths` is carried (`buildRigPath`).
|
|
4240
4261
|
*
|
|
4241
4262
|
* ⚠️ rigc measured this with a 64-chord sum until issue #560, and the comment
|
|
4242
4263
|
* that stood here argued the difference was inside anything's tolerance. It was
|
|
@@ -4247,46 +4268,17 @@ export function pathChain(points: Array<[number, number]>, closed: boolean): Arr
|
|
|
4247
4268
|
* two apart and since issue #716 rigc's own file can only where the two land on
|
|
4248
4269
|
* either side of a float's boundary. Under the six-decimal rounding that stood
|
|
4249
4270
|
* until then it could: on both measured rigs the two spellings differed on the
|
|
4250
|
-
* LAST curve, where the accumulated difference is largest.
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4253
|
-
*
|
|
4254
|
-
*
|
|
4255
|
-
*
|
|
4256
|
-
* running total carried across curves rather than restarted. Each of those is a
|
|
4257
|
-
* place where a more accurate line would emit a different file.
|
|
4271
|
+
* LAST curve, where the accumulated difference is largest.
|
|
4272
|
+
*
|
|
4273
|
+
* 🔒 The spelling is load-bearing: `Math.sqrt(dx * dx + dy * dy)` rather than
|
|
4274
|
+
* `Math.hypot`, `0.16666667` rather than `1 / 6`, and the running total carried
|
|
4275
|
+
* across curves rather than restarted. Each of those is a place where a more
|
|
4276
|
+
* accurate line would emit a different file.
|
|
4258
4277
|
*/
|
|
4259
4278
|
export function pathCurveLengths(chain: Array<[number, number]>): number[] {
|
|
4260
|
-
const
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
const [x1, y1] = chain[c];
|
|
4264
|
-
const [cx1, cy1] = chain[c + 1];
|
|
4265
|
-
const [cx2, cy2] = chain[c + 2];
|
|
4266
|
-
const [x2, y2] = chain[c + 3];
|
|
4267
|
-
const tmpx = (x1 - cx1 * 2 + cx2) * 0.1875;
|
|
4268
|
-
const tmpy = (y1 - cy1 * 2 + cy2) * 0.1875;
|
|
4269
|
-
const dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375;
|
|
4270
|
-
const dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375;
|
|
4271
|
-
let ddfx = tmpx * 2 + dddfx;
|
|
4272
|
-
let ddfy = tmpy * 2 + dddfy;
|
|
4273
|
-
let dfx = (cx1 - x1) * 0.75 + tmpx + dddfx * 0.16666667;
|
|
4274
|
-
let dfy = (cy1 - y1) * 0.75 + tmpy + dddfy * 0.16666667;
|
|
4275
|
-
total += Math.sqrt(dfx * dfx + dfy * dfy);
|
|
4276
|
-
dfx += ddfx;
|
|
4277
|
-
dfy += ddfy;
|
|
4278
|
-
ddfx += dddfx;
|
|
4279
|
-
ddfy += dddfy;
|
|
4280
|
-
total += Math.sqrt(dfx * dfx + dfy * dfy);
|
|
4281
|
-
dfx += ddfx;
|
|
4282
|
-
dfy += ddfy;
|
|
4283
|
-
total += Math.sqrt(dfx * dfx + dfy * dfy);
|
|
4284
|
-
dfx += ddfx + dddfx;
|
|
4285
|
-
dfy += ddfy + dddfy;
|
|
4286
|
-
total += Math.sqrt(dfx * dfx + dfy * dfy);
|
|
4287
|
-
out.push(total);
|
|
4288
|
-
}
|
|
4289
|
-
return out;
|
|
4279
|
+
const flat: number[] = [];
|
|
4280
|
+
for (const [x, y] of chain) flat.push(x, y);
|
|
4281
|
+
return curveLengthTable(flat, Math.max(0, Math.floor((chain.length - 1) / 3)));
|
|
4290
4282
|
}
|
|
4291
4283
|
|
|
4292
4284
|
/**
|
|
@@ -4516,20 +4508,22 @@ function meshTextureKeys(att: { name?: string; path?: string; image?: string; co
|
|
|
4516
4508
|
}
|
|
4517
4509
|
|
|
4518
4510
|
/**
|
|
4519
|
-
* The atlas region frame `i` of a sequence resolves to —
|
|
4520
|
-
* (`
|
|
4521
|
-
* with zeros to `digits`. `start` and `digits` take the parser's
|
|
4522
|
-
* (`readSequence`: 1 and 0)
|
|
4523
|
-
*
|
|
4524
|
-
* ⚠️
|
|
4525
|
-
*
|
|
4526
|
-
*
|
|
4527
|
-
*
|
|
4528
|
-
*
|
|
4511
|
+
* The atlas region frame `i` of a sequence resolves to — the core's
|
|
4512
|
+
* `frameRegionName` (`src/core/uvs.ts`, issue #1015): the stem, then `start + i`
|
|
4513
|
+
* left-padded with zeros to `digits`. `start` and `digits` take the parser's
|
|
4514
|
+
* defaults when the spec leaves them out (`readSequence`: 1 and 0).
|
|
4515
|
+
*
|
|
4516
|
+
* ⚠️ The same name is derived in `validate.ts` (`attachmentRegionLookups`, the
|
|
4517
|
+
* walk `A08` joins the atlas with) through the same function. What holds it to
|
|
4518
|
+
* the loader is a measurement, not this sentence: the core suite reads
|
|
4519
|
+
* `Sequence.getPath` and the parser's defaults off spine-core over 7,680 cases
|
|
4520
|
+
* and compares this function, the core's and A08's walk with them, and `A00`'s
|
|
4521
|
+
* round trip asks the runtime's own `getPath` for every frame a build emits.
|
|
4522
|
+
*
|
|
4523
|
+
* Exported for the selftest alone.
|
|
4529
4524
|
*/
|
|
4530
|
-
function sequenceFrameRegion(stem: string, seq: RigSequence, i: number): string {
|
|
4531
|
-
|
|
4532
|
-
return `${stem}${'0'.repeat(Math.max(0, (seq.digits ?? 0) - frame.length))}${frame}`;
|
|
4525
|
+
export function sequenceFrameRegion(stem: string, seq: RigSequence, i: number): string {
|
|
4526
|
+
return frameRegionName(stem, { count: seq.count, start: seq.start ?? 1, digits: seq.digits ?? 0, setup: seq.setup ?? 0 }, i);
|
|
4533
4527
|
}
|
|
4534
4528
|
|
|
4535
4529
|
/** The model's `sequence` record: the four fields exactly as the spec stated them. */
|
|
@@ -4796,8 +4790,8 @@ function bindNamedWeights(weights: RigMeshBinding[][], where: string, ctx: Attac
|
|
|
4796
4790
|
* an `image` under `--atlas-in` ENDED the build, by a refusal raised inside a
|
|
4797
4791
|
* measurement, on 36 of 42 atlases of the pack the card was filed from. The
|
|
4798
4792
|
* repair was to make the refusal unnecessary rather than to catch it: reading a
|
|
4799
|
-
* turned region is
|
|
4800
|
-
*
|
|
4793
|
+
* turned region is now what `extractRegion` does, measured against
|
|
4794
|
+
* `MeshAttachment.computeUVs` (`PKR02`), so there is no unmeasurable case left for this
|
|
4801
4795
|
* function to report and no catch here to keep reachable.
|
|
4802
4796
|
*/
|
|
4803
4797
|
function measureAuthoredFit(
|
|
@@ -5230,9 +5224,11 @@ function buildGeneratedMesh(
|
|
|
5230
5224
|
// refusals, because the ring's control angles need the transform and the bind
|
|
5231
5225
|
// needs the bone — and the two must refuse a bone the rig lacks in the same
|
|
5232
5226
|
// words whichever of them asks for it first.
|
|
5233
|
-
|
|
5227
|
+
// Two readings of one bone (issue #1021): the runtime's matrices to bind
|
|
5228
|
+
// through, and the exact frame to measure the control angles in.
|
|
5229
|
+
const resolve = (name: string, frames: Map<string, BoneTransform> = ctx.transforms): BoneTransform => {
|
|
5234
5230
|
if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`);
|
|
5235
|
-
const transform =
|
|
5231
|
+
const transform = frames.get(name);
|
|
5236
5232
|
if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`);
|
|
5237
5233
|
return transform;
|
|
5238
5234
|
};
|
|
@@ -5249,7 +5245,11 @@ function buildGeneratedMesh(
|
|
|
5249
5245
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5250
5246
|
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5251
5247
|
const place = (px: number, py: number): [number, number] => [anchor.worldX + px - w / 2, anchor.worldY + h / 2 - py];
|
|
5252
|
-
|
|
5248
|
+
// The inverse a control angle is measured through, in the exact frame: the
|
|
5249
|
+
// anchor's and the control's positions as the spec put them, not as the
|
|
5250
|
+
// runtime's last bits put them (issue #1021).
|
|
5251
|
+
const exactAnchor = resolve(ctx.anchorBone, ctx.exactFrames);
|
|
5252
|
+
const toPartLocal = (wx: number, wy: number): [number, number] => [wx - exactAnchor.worldX + w / 2, exactAnchor.worldY + h / 2 - wy];
|
|
5253
5253
|
let geometry;
|
|
5254
5254
|
try {
|
|
5255
5255
|
geometry =
|
|
@@ -5262,7 +5262,7 @@ function buildGeneratedMesh(
|
|
|
5262
5262
|
size: generator.size,
|
|
5263
5263
|
bias: generator.bias,
|
|
5264
5264
|
controlAngles: ringControlAngles(controls, generator.center, (name) => {
|
|
5265
|
-
const transform = resolve(name);
|
|
5265
|
+
const transform = resolve(name, ctx.exactFrames);
|
|
5266
5266
|
return toPartLocal(transform.worldX, transform.worldY);
|
|
5267
5267
|
}),
|
|
5268
5268
|
});
|
|
@@ -5371,7 +5371,12 @@ function sampleMeshDepth(
|
|
|
5371
5371
|
/**
|
|
5372
5372
|
* A part-local pixel to the BIND space this mesh's vertices are emitted in —
|
|
5373
5373
|
* the same composition `bindWeightedVertices` applies, handed in rather than
|
|
5374
|
-
* rebuilt so the two cannot diverge
|
|
5374
|
+
* rebuilt so the two cannot diverge — taken in the slot bone's exact frame
|
|
5375
|
+
* (`computeExactFrameTransforms`) rather than the runtime's. The two differ
|
|
5376
|
+
* in the last bits only, and those bits are the runtime's π rather than the
|
|
5377
|
+
* mesh: an unrotated bone reads `b = −2.3e-8` there, which turns the exact
|
|
5378
|
+
* zero this ceiling reads as "this axis cannot fold" into a fold at
|
|
5379
|
+
* 89.99997° (issue #1021; `TC03` and `TC06` are that claim).
|
|
5375
5380
|
*
|
|
5376
5381
|
* 🚨 The ceiling has to be taken in bind space and in no other. A deform
|
|
5377
5382
|
* offset is authored there, so a rotated slot bone MIXES the two axes: a
|
|
@@ -5800,7 +5805,8 @@ function buildGridAttachment(
|
|
|
5800
5805
|
throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
|
|
5801
5806
|
}
|
|
5802
5807
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5803
|
-
|
|
5808
|
+
const frame = ctx.exactFrames.get(ctx.anchorBone);
|
|
5809
|
+
if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
5804
5810
|
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
5805
5811
|
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
5806
5812
|
}
|
|
@@ -5815,7 +5821,7 @@ function buildGridAttachment(
|
|
|
5815
5821
|
'grid',
|
|
5816
5822
|
geometry.points,
|
|
5817
5823
|
geometry.triangles,
|
|
5818
|
-
(px, py) => toBoneLocal(
|
|
5824
|
+
(px, py) => toBoneLocal(frame, frame.worldX + px * toArt - w / 2, frame.worldY + h / 2 - py * toArt),
|
|
5819
5825
|
// The lattice's geometry takes nothing off the texels — its window is
|
|
5820
5826
|
// the plate's size, which is the atlas's own `offsets` — so a page
|
|
5821
5827
|
// whose file is not its declared size costs a grid only this one
|
|
@@ -6000,7 +6006,11 @@ function buildSegmentsAttachment(
|
|
|
6000
6006
|
|
|
6001
6007
|
// -- the placement ---------------------------------------------------------
|
|
6002
6008
|
const slotBone = ctx.transforms.get(ctx.anchorBone);
|
|
6003
|
-
|
|
6009
|
+
// The exact frame (issue #1021) is what the segments and the vertices are
|
|
6010
|
+
// MEASURED in — distances, falloff, `minWeight`, the shared-origin refusal —
|
|
6011
|
+
// and the runtime's is what the vertices are BOUND through (`place`).
|
|
6012
|
+
const exactSlotBone = ctx.exactFrames.get(ctx.anchorBone);
|
|
6013
|
+
if (!slotBone || !exactSlotBone) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
6004
6014
|
let anchorAt: [number, number] = [w / 2, h / 2];
|
|
6005
6015
|
if (generator.anchor !== undefined) {
|
|
6006
6016
|
const stated = finitePair(generator.anchor);
|
|
@@ -6013,10 +6023,12 @@ function buildSegmentsAttachment(
|
|
|
6013
6023
|
anchorAt = stated;
|
|
6014
6024
|
}
|
|
6015
6025
|
const [ax, ay] = anchorAt;
|
|
6016
|
-
const
|
|
6017
|
-
|
|
6018
|
-
|
|
6026
|
+
const placeIn = (bone: BoneTransform) => (px: number, py: number): [number, number] => [
|
|
6027
|
+
bone.worldX + (px - ax),
|
|
6028
|
+
bone.worldY + (cropToSpineY(py, h) - cropToSpineY(ay, h)),
|
|
6019
6029
|
];
|
|
6030
|
+
const place = placeIn(slotBone);
|
|
6031
|
+
const measuredAt = placeIn(exactSlotBone);
|
|
6020
6032
|
|
|
6021
6033
|
// -- the segments ----------------------------------------------------------
|
|
6022
6034
|
if (!Array.isArray(generator.bones) || generator.bones.length === 0) {
|
|
@@ -6040,7 +6052,7 @@ function buildSegmentsAttachment(
|
|
|
6040
6052
|
named.push(name);
|
|
6041
6053
|
refs.push(meshBoneRef(name, where, ctx));
|
|
6042
6054
|
}
|
|
6043
|
-
return { index, transform: ctx.
|
|
6055
|
+
return { index, transform: ctx.exactFrames.get(name)! };
|
|
6044
6056
|
};
|
|
6045
6057
|
const tipOf = (name: string, transform: BoneTransform, at: string, chained: boolean): [number, number] => {
|
|
6046
6058
|
const length = ctx.bones.find((b) => b.name === name)?.length ?? 0;
|
|
@@ -6108,7 +6120,7 @@ function buildSegmentsAttachment(
|
|
|
6108
6120
|
`${JSON.stringify(from === null ? span.from : span.to)}; both ends are [x, y] in the part's pixels, y down`,
|
|
6109
6121
|
);
|
|
6110
6122
|
}
|
|
6111
|
-
segments.push({ bone: index, a:
|
|
6123
|
+
segments.push({ bone: index, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
|
|
6112
6124
|
return;
|
|
6113
6125
|
}
|
|
6114
6126
|
throw new CompileError(
|
|
@@ -6120,7 +6132,7 @@ function buildSegmentsAttachment(
|
|
|
6120
6132
|
// -- the weights -----------------------------------------------------------
|
|
6121
6133
|
const falloff = { power, radius: f.radius, maxBones, minWeight };
|
|
6122
6134
|
const weights: MeshVertexWeight[][] = lattice.points.map(([px, py], v) => {
|
|
6123
|
-
const got = segmentShares(
|
|
6135
|
+
const got = segmentShares(measuredAt(px, py), segments, falloff);
|
|
6124
6136
|
if ('weights' in got) return got.weights;
|
|
6125
6137
|
throw new CompileError(
|
|
6126
6138
|
`${where}: vertex ${v}, at (${px}, ${py}) in the part's pixels, keeps no bone — the ${got.dropped.length} ` +
|
|
@@ -6299,7 +6311,8 @@ function buildContourAttachment(
|
|
|
6299
6311
|
throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
|
|
6300
6312
|
}
|
|
6301
6313
|
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
6302
|
-
|
|
6314
|
+
const frame = ctx.exactFrames.get(ctx.anchorBone);
|
|
6315
|
+
if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
|
|
6303
6316
|
if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
|
|
6304
6317
|
throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
|
|
6305
6318
|
}
|
|
@@ -6323,7 +6336,7 @@ function buildContourAttachment(
|
|
|
6323
6336
|
'contour',
|
|
6324
6337
|
geometry.points,
|
|
6325
6338
|
geometry.triangles,
|
|
6326
|
-
(px, py) => toBoneLocal(
|
|
6339
|
+
(px, py) => toBoneLocal(frame, frame.worldX + px * toArt - w / 2, frame.worldY + h / 2 - py * toArt),
|
|
6327
6340
|
alpha,
|
|
6328
6341
|
sheetGridOf(img, plate),
|
|
6329
6342
|
where,
|
|
@@ -7135,6 +7148,8 @@ function buildMesh(
|
|
|
7135
7148
|
manifest: FaceManifest,
|
|
7136
7149
|
bones: ModelBone[],
|
|
7137
7150
|
transforms: Map<string, BoneTransform>,
|
|
7151
|
+
/** The same bones in the exact frame (`computeExactFrameTransforms`): what the control angles are measured in. */
|
|
7152
|
+
exactFrames: Map<string, BoneTransform>,
|
|
7138
7153
|
anchorName: string,
|
|
7139
7154
|
): { attachment: ModelMeshAttachment; kind: 'ring' | 'ribbon' } {
|
|
7140
7155
|
const spec = part.mesh!;
|
|
@@ -7167,9 +7182,12 @@ function buildMesh(
|
|
|
7167
7182
|
// measures in crop pixels, y down, and Spine world is that crop flipped, so
|
|
7168
7183
|
// `cropH - worldY` is the whole of it. `spec.center` is in the same crop
|
|
7169
7184
|
// pixels, which is why the centre passed here is not the window-local
|
|
7170
|
-
// `centre` two lines up.
|
|
7185
|
+
// `centre` two lines up. The positions are read in the exact frame
|
|
7186
|
+
// (issue #1021): an angle is a statement about where the spec put the
|
|
7187
|
+
// bone, it orders the split and refuses a tie, and the runtime's last
|
|
7188
|
+
// bits turned an authored 90 into 89.99999734089101 in that refusal.
|
|
7171
7189
|
const controlAngles = ringControlAngles(controls, spec.center!, (name) => {
|
|
7172
|
-
const m =
|
|
7190
|
+
const m = exactFrames.get(name);
|
|
7173
7191
|
if (!m) throw new CompileError(`internal: no setup transform for control bone "${name}"`);
|
|
7174
7192
|
return [m.worldX, cropH - m.worldY];
|
|
7175
7193
|
});
|
|
@@ -7909,6 +7927,13 @@ export function deformGeometryOf(
|
|
|
7909
7927
|
// what makes that provable — the alternative, folding it into the world path
|
|
7910
7928
|
// below, would re-evaluate every existing `transform` key at world
|
|
7911
7929
|
// coordinates and emit a different file for a spec nobody edited.
|
|
7930
|
+
//
|
|
7931
|
+
// 🔸 Issue #1021: this reads the runtime's frame, not the exact one. The
|
|
7932
|
+
// offsets are added by the runtime to exactly these numbers, so the model is
|
|
7933
|
+
// evaluated on what the file holds; a compiler-bound vertex therefore carries
|
|
7934
|
+
// the runtime's last bits (`b = −2.3e-8` on an unrotated bone) into its
|
|
7935
|
+
// offset, about 1e-6 on gallery/look's hair locks. Evaluating on the authored
|
|
7936
|
+
// positions instead would need them carried here from the binder.
|
|
7912
7937
|
if (boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
7913
7938
|
return { ...common, setup: bindSpace, influenceBones: null, setupWhy: null };
|
|
7914
7939
|
}
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
*
|
|
45
45
|
* 1. **A table of the curves' lengths**, each curve four forward-difference
|
|
46
46
|
* steps (`0.1875`, `0.09375`, `0.75` and `0.16666667`, the running total
|
|
47
|
-
* carried across curves) — `
|
|
48
|
-
*
|
|
47
|
+
* carried across curves) — `curveLengthTable`, which `src/compile.ts`'s
|
|
48
|
+
* `pathCurveLengths` also calls (issue #1015). A ten-step table missed
|
|
49
|
+
* 460 of the grid's 976. The
|
|
49
50
|
* last entry is the path's length; `percent` multiplies the position by
|
|
50
51
|
* it. The stated `lengths` are not read: the stated total in their place
|
|
51
52
|
* missed 680 of 1,000.
|
|
@@ -217,6 +218,60 @@ import type { CoreCurve, CoreKey } from './animation.ts';
|
|
|
217
218
|
import type { CoreConstraintRecord } from './constraints.ts';
|
|
218
219
|
|
|
219
220
|
const RAD = RUNTIME_PI / 180;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The constant-speed walk's table of the curves' lengths — the header's *The
|
|
224
|
+
* walk*, step 1: each curve four forward-difference steps (`0.1875`,
|
|
225
|
+
* `0.09375`, `0.75`, `0.16666667`), the running total carried across curves,
|
|
226
|
+
* one cumulative entry per curve. `chain` is flat, `x, y` per point, knot
|
|
227
|
+
* handle handle knot …; `curveCount` curves are read off it.
|
|
228
|
+
*
|
|
229
|
+
* Exported because it has a second caller: an omitted `lengths` on a path
|
|
230
|
+
* attachment is measured by `src/compile.ts` through this function (issue
|
|
231
|
+
* #1015), so the number rigc writes into a file and the number the core walks
|
|
232
|
+
* are one computation. Measured as the header states it on the grid (a
|
|
233
|
+
* ten-step table missed 460 of 976) and held there by the core suite's `CP`
|
|
234
|
+
* controls.
|
|
235
|
+
*/
|
|
236
|
+
export function curveLengthTable(chain: readonly number[], curveCount: number): number[] {
|
|
237
|
+
const curveEnds: number[] = [];
|
|
238
|
+
let total = 0;
|
|
239
|
+
let x1 = chain[0];
|
|
240
|
+
let y1 = chain[1];
|
|
241
|
+
for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
|
|
242
|
+
const cx1 = chain[w];
|
|
243
|
+
const cy1 = chain[w + 1];
|
|
244
|
+
const cx2 = chain[w + 2];
|
|
245
|
+
const cy2 = chain[w + 3];
|
|
246
|
+
const x2 = chain[w + 4];
|
|
247
|
+
const y2 = chain[w + 5];
|
|
248
|
+
const h2x = (x1 - cx1 * 2 + cx2) * 0.1875;
|
|
249
|
+
const h2y = (y1 - cy1 * 2 + cy2) * 0.1875;
|
|
250
|
+
const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375;
|
|
251
|
+
const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375;
|
|
252
|
+
let d2x = h2x * 2 + d3x;
|
|
253
|
+
let d2y = h2y * 2 + d3y;
|
|
254
|
+
let d1x = (cx1 - x1) * 0.75 + h2x + d3x * 0.16666667;
|
|
255
|
+
let d1y = (cy1 - y1) * 0.75 + h2y + d3y * 0.16666667;
|
|
256
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
257
|
+
d1x += d2x;
|
|
258
|
+
d1y += d2y;
|
|
259
|
+
d2x += d3x;
|
|
260
|
+
d2y += d3y;
|
|
261
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
262
|
+
d1x += d2x;
|
|
263
|
+
d1y += d2y;
|
|
264
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
265
|
+
d1x += d2x + d3x;
|
|
266
|
+
d1y += d2y + d3y;
|
|
267
|
+
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
268
|
+
curveEnds.push(total);
|
|
269
|
+
x1 = x2;
|
|
270
|
+
y1 = y2;
|
|
271
|
+
}
|
|
272
|
+
return curveEnds;
|
|
273
|
+
}
|
|
274
|
+
|
|
220
275
|
const EPSILON = 0.00001;
|
|
221
276
|
|
|
222
277
|
/** The three modes a path constraint names, each as the model spells it once its first letter is lower-cased. */
|
|
@@ -556,47 +611,14 @@ function positionsOf(state: PathSolverState, c: CorePathRecord, g: CorePathGeome
|
|
|
556
611
|
chain.push(...P(0), ...P(1));
|
|
557
612
|
} else for (let k = 1; k < n - 1; k++) chain.push(...P(k));
|
|
558
613
|
const curveCount = closed ? n / 3 : n / 3 - 1;
|
|
559
|
-
const curveEnds
|
|
560
|
-
|
|
561
|
-
let x1 = chain[0];
|
|
562
|
-
let y1 = chain[1];
|
|
563
|
-
for (let i = 0, w = 2; i < curveCount; i++, w += 6) {
|
|
564
|
-
const cx1 = chain[w];
|
|
565
|
-
const cy1 = chain[w + 1];
|
|
566
|
-
const cx2 = chain[w + 2];
|
|
567
|
-
const cy2 = chain[w + 3];
|
|
568
|
-
const x2 = chain[w + 4];
|
|
569
|
-
const y2 = chain[w + 5];
|
|
570
|
-
const h2x = (x1 - cx1 * 2 + cx2) * 0.1875;
|
|
571
|
-
const h2y = (y1 - cy1 * 2 + cy2) * 0.1875;
|
|
572
|
-
const d3x = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375;
|
|
573
|
-
const d3y = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375;
|
|
574
|
-
let d2x = h2x * 2 + d3x;
|
|
575
|
-
let d2y = h2y * 2 + d3y;
|
|
576
|
-
let d1x = (cx1 - x1) * 0.75 + h2x + d3x * 0.16666667;
|
|
577
|
-
let d1y = (cy1 - y1) * 0.75 + h2y + d3y * 0.16666667;
|
|
578
|
-
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
579
|
-
d1x += d2x;
|
|
580
|
-
d1y += d2y;
|
|
581
|
-
d2x += d3x;
|
|
582
|
-
d2y += d3y;
|
|
583
|
-
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
584
|
-
d1x += d2x;
|
|
585
|
-
d1y += d2y;
|
|
586
|
-
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
587
|
-
d1x += d2x + d3x;
|
|
588
|
-
d1y += d2y + d3y;
|
|
589
|
-
total += Math.sqrt(d1x * d1x + d1y * d1y);
|
|
590
|
-
curveEnds.push(total);
|
|
591
|
-
x1 = x2;
|
|
592
|
-
y1 = y2;
|
|
593
|
-
}
|
|
614
|
+
const curveEnds = curveLengthTable(chain, curveCount);
|
|
615
|
+
const total = curveEnds.length > 0 ? curveEnds[curveEnds.length - 1] : 0;
|
|
594
616
|
if (c.positionMode === 'percent') position *= total;
|
|
595
617
|
const spaceScale = c.spacingMode === 'percent' ? total : c.spacingMode === 'proportional' ? total / spaceCount : 1;
|
|
596
618
|
const segmentEnds: number[] = new Array(10).fill(0);
|
|
597
619
|
let curveTotal = 0;
|
|
598
620
|
let lastCurve = -1;
|
|
599
|
-
let cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
|
|
621
|
+
let x1 = 0, y1 = 0, cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
|
|
600
622
|
for (let i = 0, o = 0, curve = 0, segment = 0; i < spaceCount; i++, o += 3) {
|
|
601
623
|
const space = spaces[i] * spaceScale;
|
|
602
624
|
position += space;
|
|
@@ -561,6 +561,27 @@ export function physicsState(ctx: PhysicsStepContext, name: string): PhysicsStat
|
|
|
561
561
|
const PI2 = RUNTIME_PI * 2;
|
|
562
562
|
const INV_PI2 = 1 / PI2;
|
|
563
563
|
|
|
564
|
+
/** What a physics constraint's step moves, by its five component fields. */
|
|
565
|
+
export interface PhysicsDrives {
|
|
566
|
+
x: boolean;
|
|
567
|
+
y: boolean;
|
|
568
|
+
/** `rotate` or `shearX` above 0: the two share the angular part of the step. */
|
|
569
|
+
rotateOrShearX: boolean;
|
|
570
|
+
scaleX: boolean;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Which parts of the step a constraint drives: a component above 0 drives its
|
|
575
|
+
* part and nothing else does — the test `stepPhysics` takes before each part
|
|
576
|
+
* of *The step* above, posed against the runtime with the rest of it. Exported
|
|
577
|
+
* because `src/ingest.ts` asks the same question of a raw file (a
|
|
578
|
+
* constraint that drives none of the four moves no bone, issue #731) and
|
|
579
|
+
* answers it through this function (issue #1015).
|
|
580
|
+
*/
|
|
581
|
+
export function physicsDrives(r: { x: number; y: number; rotate: number; shearX: number; scaleX: number }): PhysicsDrives {
|
|
582
|
+
return { x: r.x > 0, y: r.y > 0, rotateOrShearX: r.rotate > 0 || r.shearX > 0, scaleX: r.scaleX > 0 };
|
|
583
|
+
}
|
|
584
|
+
|
|
564
585
|
/**
|
|
565
586
|
* One physics constraint updated on its bone's world transform `w`
|
|
566
587
|
* (mutated), `length` the bone's length — the header's *The step*, every
|
|
@@ -572,10 +593,7 @@ export function stepPhysics(r: CorePhysicsRecord, w: CoreWorld, length: number,
|
|
|
572
593
|
const mix = r.mix;
|
|
573
594
|
if (mix === 0) return false;
|
|
574
595
|
const s = physicsState(ctx, r.name);
|
|
575
|
-
const x = r
|
|
576
|
-
const y = r.y > 0;
|
|
577
|
-
const rotateOrShearX = r.rotate > 0 || r.shearX > 0;
|
|
578
|
-
const scaleX = r.scaleX > 0;
|
|
596
|
+
const { x, y, rotateOrShearX, scaleX } = physicsDrives(r);
|
|
579
597
|
const l = length;
|
|
580
598
|
if (ctx.phase === 'reset') resetPhysicsState(s, ctx.time);
|
|
581
599
|
const delta = Math.max(ctx.time - s.lastTime, 0);
|