@rig-c/core 2.70.0 → 2.72.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 +222 -13
- package/package.json +2 -1
- package/src/assertions/report.ts +1 -1
- package/src/compile.ts +126 -24
- package/src/core/animation.ts +56 -24
- package/src/core/events.ts +17 -5
- package/src/core/index.ts +62 -1
- package/src/core/raw.ts +38 -12
- package/src/core/tracks.ts +23 -8
- package/src/core/vertices.ts +182 -44
- package/src/deformgen.ts +25 -9
- package/src/depth.ts +17 -4
- package/src/meshcompare.ts +146 -22
- package/src/motion.ts +4 -4
- package/src/rig.ts +7 -4
- package/src/runtime/curves.ts +106 -0
- package/src/runtime/index.ts +84 -0
- package/src/runtime/model.ts +736 -0
- package/src/runtime/pose.ts +1054 -0
- package/src/trackgen.ts +27 -4
package/src/compile.ts
CHANGED
|
@@ -3011,10 +3011,50 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3011
3011
|
const meshes: CompileResult['meshes'] = [];
|
|
3012
3012
|
// `skin/slot/attachment` -> per-vertex z, for the attachments that named a
|
|
3013
3013
|
// depth map. Read at deform-key time; never emitted. See `AttachmentContext`.
|
|
3014
|
-
const attachmentDepths = new Map<string, number[]>();
|
|
3014
|
+
const attachmentDepths = new Map<string, number[] | { z: number[]; geometry: DeformGeometry }>();
|
|
3015
3015
|
// The same for a `soft` mask's per-vertex level, which a deform envelope's
|
|
3016
3016
|
// `mask` reads (issue #1413). Never emitted either.
|
|
3017
3017
|
const attachmentSoftMasks = new Map<string, DeformSoftMask>();
|
|
3018
|
+
// `skin/slot/attachment` -> per-axis narrowing mode when a deform turn key on
|
|
3019
|
+
// that attachment declares `narrowing: false` (issue #1584), so `sampleMeshDepth`
|
|
3020
|
+
// evaluates `turnCeiling` with the `asin(|A₀ / A_axis|)` closed form that
|
|
3021
|
+
// matches `A39` on the keyed turn.
|
|
3022
|
+
const attachmentTurnNarrowing = new Map<string, { yaw?: boolean; pitch?: boolean }>();
|
|
3023
|
+
const attachmentTurnNarrowingBy = new Map<string, { yaw?: { mode: boolean; anim: string }; pitch?: { mode: boolean; anim: string } }>();
|
|
3024
|
+
withMotionSource(() => {
|
|
3025
|
+
for (const [turnAnimName, turnAnim] of Object.entries(motion.animations ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
|
|
3026
|
+
for (const turnTrack of turnAnim.deform ?? []) {
|
|
3027
|
+
const turnDeformSkin = turnTrack.skin ?? DEFAULT_SKIN;
|
|
3028
|
+
const turnTriple = `${turnDeformSkin}/${turnTrack.slot}/${turnTrack.attachment}`;
|
|
3029
|
+
for (const turnKey of turnTrack.keys ?? []) {
|
|
3030
|
+
const turnSpec = turnKey.transform as { kind?: unknown; narrowing?: unknown } | undefined;
|
|
3031
|
+
if (turnSpec && (turnSpec.kind === 'yaw' || turnSpec.kind === 'pitch')) {
|
|
3032
|
+
const turnAxis = turnSpec.kind;
|
|
3033
|
+
const turnNarrowMode = turnSpec.narrowing !== false;
|
|
3034
|
+
const turnPrev = attachmentTurnNarrowingBy.get(turnTriple)?.[turnAxis];
|
|
3035
|
+
if (turnPrev !== undefined && turnPrev.mode !== turnNarrowMode) {
|
|
3036
|
+
const turnFalseAnim = turnNarrowMode ? turnPrev.anim : turnAnimName;
|
|
3037
|
+
const turnTrueAnim = turnNarrowMode ? turnAnimName : turnPrev.anim;
|
|
3038
|
+
throw new CompileError(
|
|
3039
|
+
`deform ${turnTriple} keys "${turnAxis}" with narrowing=false in animation "${turnFalseAnim}" and with narrowing=true in animation "${turnTrueAnim}". ` +
|
|
3040
|
+
'An attachment\'s turn ceiling on one axis is either asin(|A₀ / A_axis|) (without narrowing) or atan(|A₀ / A_axis|) (with narrowing) — ' +
|
|
3041
|
+
`mixing both on one attachment and axis would report one ceiling where two apply. Use one "narrowing" setting across every "${turnAxis}" key on ${turnTriple}.`,
|
|
3042
|
+
'CE_COMPILE_DEFORM_TURN_NARROWING_MIXED',
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
3045
|
+
const curBy = attachmentTurnNarrowingBy.get(turnTriple) ?? {};
|
|
3046
|
+
if (curBy[turnAxis] === undefined) curBy[turnAxis] = { mode: turnNarrowMode, anim: turnAnimName };
|
|
3047
|
+
attachmentTurnNarrowingBy.set(turnTriple, curBy);
|
|
3048
|
+
if (!turnNarrowMode) {
|
|
3049
|
+
const cur = attachmentTurnNarrowing.get(turnTriple) ?? {};
|
|
3050
|
+
cur[turnAxis] = false;
|
|
3051
|
+
attachmentTurnNarrowing.set(turnTriple, cur);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
});
|
|
3018
3058
|
|
|
3019
3059
|
for (const rigSlot of rig.slots) {
|
|
3020
3060
|
const part = partBySlot.get(rigSlot.name);
|
|
@@ -3183,6 +3223,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3183
3223
|
imagesDir,
|
|
3184
3224
|
depths: attachmentDepths,
|
|
3185
3225
|
softMasks: attachmentSoftMasks,
|
|
3226
|
+
turnNarrowing: attachmentTurnNarrowing.get(`${skinName}/${rigSlot.name}/${placeholder}`),
|
|
3186
3227
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
3187
3228
|
atlasRegions: atlasIn === null ? null : atlasIn.byName,
|
|
3188
3229
|
links: pendingLinks,
|
|
@@ -3688,6 +3729,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3688
3729
|
if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
|
|
3689
3730
|
throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`, 'CE_COMPILE_DEFORM_TIMELINE_DUPLICATE');
|
|
3690
3731
|
}
|
|
3732
|
+
const sampledDepth = attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`);
|
|
3691
3733
|
const keys = compileDeformTrack(
|
|
3692
3734
|
track,
|
|
3693
3735
|
motion,
|
|
@@ -3695,10 +3737,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3695
3737
|
anim.duration,
|
|
3696
3738
|
{
|
|
3697
3739
|
...deformGeometryOf(attachment, at, transforms),
|
|
3698
|
-
depth:
|
|
3740
|
+
depth: (Array.isArray(sampledDepth) ? sampledDepth : sampledDepth?.z) ?? null,
|
|
3699
3741
|
soft: attachmentSoftMasks.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3700
3742
|
},
|
|
3701
3743
|
deformTransforms,
|
|
3744
|
+
sampledDepth !== undefined && !Array.isArray(sampledDepth) ? sampledDepth.geometry : null,
|
|
3702
3745
|
);
|
|
3703
3746
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3704
3747
|
attachmentTimelinesOf(skinName, track.slot).set(track.attachment, { deform: keys });
|
|
@@ -4326,13 +4369,19 @@ interface AttachmentContext {
|
|
|
4326
4369
|
* round trip and then vanish — and the whole point of this table is to be
|
|
4327
4370
|
* read by `evaluateDeformTransform` at key time and never emitted at all.
|
|
4328
4371
|
*/
|
|
4329
|
-
depths: Map<string, number[]>;
|
|
4372
|
+
depths: Map<string, number[] | { z: number[]; geometry: DeformGeometry }>;
|
|
4330
4373
|
/**
|
|
4331
4374
|
* A `soft` mask's file and its sampled level per vertex, keyed like `depths`
|
|
4332
4375
|
* — what a deform envelope's `mask` reads at key time (issue #1413). Never
|
|
4333
4376
|
* emitted, for `depths`' reason.
|
|
4334
4377
|
*/
|
|
4335
4378
|
softMasks: Map<string, DeformSoftMask>;
|
|
4379
|
+
/**
|
|
4380
|
+
* Per-axis `narrowing` mode when a deform turn key on this attachment states
|
|
4381
|
+
* `"narrowing": false` (issue #1584), so `sampleMeshDepth` computes the turn
|
|
4382
|
+
* ceiling with `asin(|A₀ / A_axis|)` on that axis.
|
|
4383
|
+
*/
|
|
4384
|
+
turnNarrowing?: { yaw?: boolean; pitch?: boolean };
|
|
4336
4385
|
/** Every slot the rig declares — a clipping attachment's `end` resolves here. */
|
|
4337
4386
|
slotNames: Set<string>;
|
|
4338
4387
|
/**
|
|
@@ -5708,7 +5757,12 @@ function buildRigMesh(
|
|
|
5708
5757
|
if (authored && att.generator) {
|
|
5709
5758
|
throw new CompileError(`${where}: a mesh is either authored geometry or a generator, never both`, 'CE_COMPILE_MESH_AUTHORED_AND_GENERATED');
|
|
5710
5759
|
}
|
|
5711
|
-
if (att.generator)
|
|
5760
|
+
if (att.generator) {
|
|
5761
|
+
if (att.depth !== undefined) {
|
|
5762
|
+
throw new CompileError(`${where}: attachment "depth" describes authored geometry; a generated mesh states its depth on the contour or grid generator instead`, 'CE_COMPILE_DEPTH_AUTHORED_REQUIRED');
|
|
5763
|
+
}
|
|
5764
|
+
return buildGeneratedMesh(att, att.generator, placeholder, where, ctx);
|
|
5765
|
+
}
|
|
5712
5766
|
if (att.vertices && att.weights) {
|
|
5713
5767
|
throw new CompileError(
|
|
5714
5768
|
`${where}: a mesh gives geometry as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`, 'CE_COMPILE_MESH_GEOMETRY_SOURCES_CONFLICT',
|
|
@@ -5786,6 +5840,35 @@ function buildRigMesh(
|
|
|
5786
5840
|
height: f32(height),
|
|
5787
5841
|
};
|
|
5788
5842
|
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
5843
|
+
let depth: ReturnType<typeof sampleMeshDepth> | undefined;
|
|
5844
|
+
if (att.depth !== undefined) {
|
|
5845
|
+
if (img === undefined) {
|
|
5846
|
+
throw new CompileError(`${where}: an authored mesh with "depth" needs an "image" to locate the part's pixel grid`, 'CE_COMPILE_DEPTH_IMAGE_REQUIRED');
|
|
5847
|
+
}
|
|
5848
|
+
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
5849
|
+
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
|
|
5850
|
+
const geometry = deformGeometryOf(out, where, ctx.transforms, anchor);
|
|
5851
|
+
if (geometry.setup === null) {
|
|
5852
|
+
throw new CompileError(`${where}: depth needs resolved setup positions — ${geometry.setupWhy}`, 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED');
|
|
5853
|
+
}
|
|
5854
|
+
const world = geometry.setup;
|
|
5855
|
+
const plate = partPlate(img);
|
|
5856
|
+
const toArt = img.width / plate.width;
|
|
5857
|
+
// The generator's window is centred on the slot bone's setup WORLD origin.
|
|
5858
|
+
// UVs describe texture placement, not a vertex's position in that window.
|
|
5859
|
+
const points: Array<[number, number]> = [];
|
|
5860
|
+
for (let v = 0; v < world.length; v += 2) {
|
|
5861
|
+
points.push([(world[v] - anchor.worldX + img.width / 2) / toArt,
|
|
5862
|
+
(img.height / 2 - (world[v + 1] - anchor.worldY)) / toArt]);
|
|
5863
|
+
}
|
|
5864
|
+
depth = sampleMeshDepth(att.depth, 'authored', points, att.triangles,
|
|
5865
|
+
(px, py) => [anchor.worldX + px * toArt - img.width / 2, anchor.worldY + img.height / 2 - py * toArt],
|
|
5866
|
+
img.pageGrid === undefined ? plateAlpha(plate) : { unlocated: img.pageGrid.said },
|
|
5867
|
+
sheetGridOf(img, plate), where, ctx,
|
|
5868
|
+
Array.from({ length: world.length / 2 }, (_, v) => [world[2 * v], world[2 * v + 1]]));
|
|
5869
|
+
geometry.depth = depth.z;
|
|
5870
|
+
ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${placeholder}`, { z: depth.z, geometry });
|
|
5871
|
+
}
|
|
5789
5872
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
5790
5873
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
5791
5874
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -5796,6 +5879,7 @@ function buildRigMesh(
|
|
|
5796
5879
|
ctx.meshes.push({
|
|
5797
5880
|
slot: ctx.slotName,
|
|
5798
5881
|
kind: 'authored',
|
|
5882
|
+
...(depth === undefined ? {} : { depth: depth.summary }),
|
|
5799
5883
|
attachments: [ctx.slotName],
|
|
5800
5884
|
vertices: uvCount / 2,
|
|
5801
5885
|
triangles: att.triangles.length / 3,
|
|
@@ -6157,8 +6241,8 @@ function buildGeneratedMesh(
|
|
|
6157
6241
|
* not invent tolerances.
|
|
6158
6242
|
*/
|
|
6159
6243
|
function sampleMeshDepth(
|
|
6160
|
-
spec:
|
|
6161
|
-
kind: 'contour' | 'grid',
|
|
6244
|
+
spec: RigDepthMap,
|
|
6245
|
+
kind: 'contour' | 'grid' | 'authored',
|
|
6162
6246
|
points: ReadonlyArray<readonly [number, number]>,
|
|
6163
6247
|
/** The mesh's own triangle list, for the turn ceiling. */
|
|
6164
6248
|
triangles: ReadonlyArray<number>,
|
|
@@ -6197,6 +6281,8 @@ function sampleMeshDepth(
|
|
|
6197
6281
|
grid: SheetGrid,
|
|
6198
6282
|
where: string,
|
|
6199
6283
|
ctx: AttachmentContext,
|
|
6284
|
+
/** Authored setup-world vertices, already in the depth turn's evaluation space. */
|
|
6285
|
+
turnPoints?: ReadonlyArray<readonly [number, number]>,
|
|
6200
6286
|
): {
|
|
6201
6287
|
z: number[];
|
|
6202
6288
|
/** The tone-curved 0..1 nearness per vertex, which `bind` thresholds. */
|
|
@@ -6280,7 +6366,9 @@ function sampleMeshDepth(
|
|
|
6280
6366
|
kind === 'contour'
|
|
6281
6367
|
? 'A contour mesh puts every vertex ON the silhouette and pushes it out by the margin, so a sheet cut to ' +
|
|
6282
6368
|
'the art stops short of every one of them. Dilate the sheet past the mesh margin, or lower the margin.'
|
|
6283
|
-
:
|
|
6369
|
+
: kind === 'authored'
|
|
6370
|
+
? 'The authored setup vertices reach outside the sheet coverage. Extend the sheet to cover their bilinear taps, or correct the setup geometry.'
|
|
6371
|
+
: 'A grid spans the whole part window, corners included, while a sheet is usually cut to the art — so the ' +
|
|
6284
6372
|
'lattice reaches past it wherever the art does not fill the window. Dilate the sheet to the window, or ' +
|
|
6285
6373
|
'state "us"/"vs" that keep the lattice inside the art.';
|
|
6286
6374
|
throw new CompileError(
|
|
@@ -6321,9 +6409,10 @@ function sampleMeshDepth(
|
|
|
6321
6409
|
range: [f32(lo), f32(hi)],
|
|
6322
6410
|
...(partAlpha instanceof Uint8Array ? { undrawn } : { undrawn: null, unlocated: partAlpha.unlocated }),
|
|
6323
6411
|
ceiling: turnCeiling(
|
|
6324
|
-
points.map(([px, py]) => toBind(px, py)),
|
|
6412
|
+
turnPoints ?? points.map(([px, py]) => toBind(px, py)),
|
|
6325
6413
|
z,
|
|
6326
6414
|
triangles,
|
|
6415
|
+
ctx.turnNarrowing === undefined ? undefined : { narrowing: ctx.turnNarrowing },
|
|
6327
6416
|
),
|
|
6328
6417
|
},
|
|
6329
6418
|
};
|
|
@@ -8689,7 +8778,7 @@ export interface DeformGeometry {
|
|
|
8689
8778
|
/** Why `setup` is null, phrased for the refusal. Null when it is not. */
|
|
8690
8779
|
setupWhy: string | null;
|
|
8691
8780
|
/**
|
|
8692
|
-
* Per-vertex `z`, when this attachment
|
|
8781
|
+
* Per-vertex `z`, when this attachment or its generator named a depth map.
|
|
8693
8782
|
*
|
|
8694
8783
|
* Not measured from the attachment like everything else here — there
|
|
8695
8784
|
* is nowhere in the format to measure it FROM, which is the whole reason the
|
|
@@ -8723,6 +8812,8 @@ export function deformGeometryOf(
|
|
|
8723
8812
|
where: string,
|
|
8724
8813
|
/** Every bone's setup world transform, by name. */
|
|
8725
8814
|
transforms: Map<string, BoneTransform>,
|
|
8815
|
+
/** Force the world-offset path for an authored depth turn, including rigid meshes. */
|
|
8816
|
+
depthAnchor?: BoneTransform,
|
|
8726
8817
|
): DeformGeometry {
|
|
8727
8818
|
const type = att.kind;
|
|
8728
8819
|
let worldVerticesLength: number;
|
|
@@ -8768,6 +8859,15 @@ export function deformGeometryOf(
|
|
|
8768
8859
|
const vertices = att.vertices;
|
|
8769
8860
|
const weighted = vertices.weighted;
|
|
8770
8861
|
if (!vertices.weighted) {
|
|
8862
|
+
if (depthAnchor !== undefined) {
|
|
8863
|
+
return {
|
|
8864
|
+
weighted, deformLength: worldVerticesLength, vertexCount: worldVerticesLength / 2,
|
|
8865
|
+
boneCounts: Array(worldVerticesLength / 2).fill(1),
|
|
8866
|
+
setup: setupWorldVertices(vertices, depthAnchor, transforms, where).flat(),
|
|
8867
|
+
influenceBones: Array(worldVerticesLength / 2).fill(depthAnchor),
|
|
8868
|
+
setupWhy: null, depth: null, soft: null,
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8771
8871
|
return {
|
|
8772
8872
|
weighted,
|
|
8773
8873
|
deformLength: worldVerticesLength,
|
|
@@ -8831,7 +8931,7 @@ export function deformGeometryOf(
|
|
|
8831
8931
|
// the runtime's last bits (`b = −2.3e-8` on an unrotated bone) into its
|
|
8832
8932
|
// offset, about 1e-6 on gallery/look's hair locks. Evaluating on the authored
|
|
8833
8933
|
// positions instead would need them carried here from the binder.
|
|
8834
|
-
if (boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
8934
|
+
if (depthAnchor === undefined && boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
8835
8935
|
return { ...common, setup: bindSpace, influenceBones: null, setupWhy: null };
|
|
8836
8936
|
}
|
|
8837
8937
|
// -- several influences, evaluated in world (issue #389) -------------------
|
|
@@ -9007,6 +9107,7 @@ function compileDeformTrack(
|
|
|
9007
9107
|
duration: number,
|
|
9008
9108
|
geometry: DeformGeometry,
|
|
9009
9109
|
generated: CompileResult['deformTransforms'],
|
|
9110
|
+
depthWorld: DeformGeometry | null = null,
|
|
9010
9111
|
): ModelKey[] {
|
|
9011
9112
|
const skin = track.skin ?? 'default';
|
|
9012
9113
|
const where = `animation "${animName}" deform ${skin}/${track.slot}/${track.attachment}`;
|
|
@@ -9032,6 +9133,7 @@ function compileDeformTrack(
|
|
|
9032
9133
|
// definite, which the arithmetic does for any weighting that closes at 1 and
|
|
9033
9134
|
// for no weighting that does not (issue #389).
|
|
9034
9135
|
if (key.transform !== undefined) {
|
|
9136
|
+
const evaluated = ('depth' in key.transform && key.transform.depth === true ? depthWorld : null) ?? geometry;
|
|
9035
9137
|
if (key.vertices !== undefined && key.vertices !== null) {
|
|
9036
9138
|
throw new CompileError(
|
|
9037
9139
|
`${where} (t=${key.t}): the key carries both a "transform" and a "vertices" run, and they are two answers to ` +
|
|
@@ -9042,33 +9144,33 @@ function compileDeformTrack(
|
|
|
9042
9144
|
if (key.offset !== undefined || key.fromVertex !== undefined) {
|
|
9043
9145
|
throw new CompileError(
|
|
9044
9146
|
`${where} (t=${key.t}): the key states a "transform" and a start index. A transform is a model of the whole ` +
|
|
9045
|
-
`attachment and is evaluated over all ${
|
|
9147
|
+
`attachment and is evaluated over all ${evaluated.vertexCount} of its vertices, so it always starts at deform ` +
|
|
9046
9148
|
'index 0. A model applied to part of a run leaves a step at the run\'s edge — write the partial run by hand ' +
|
|
9047
9149
|
'if that is what you mean.', 'CE_COMPILE_DEFORM_TRANSFORM_START_INDEX',
|
|
9048
9150
|
);
|
|
9049
9151
|
}
|
|
9050
|
-
if (
|
|
9152
|
+
if (evaluated.setup === null) {
|
|
9051
9153
|
throw new CompileError(
|
|
9052
9154
|
`${where} (t=${key.t}): a "transform" states where every vertex of this attachment goes, and this one has a ` +
|
|
9053
|
-
`vertex the arithmetic cannot place — ${
|
|
9155
|
+
`vertex the arithmetic cannot place — ${evaluated.setupWhy}. Fix the weights, or write the bind-space pairs ` +
|
|
9054
9156
|
'yourself and start the run with "offset".', 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED',
|
|
9055
9157
|
);
|
|
9056
9158
|
}
|
|
9057
9159
|
const report = evaluateDeformTransform(
|
|
9058
9160
|
key.transform,
|
|
9059
|
-
|
|
9161
|
+
evaluated.setup,
|
|
9060
9162
|
onModelGrid,
|
|
9061
9163
|
`${where} (t=${key.t})`,
|
|
9062
|
-
|
|
9063
|
-
|
|
9164
|
+
evaluated.depth,
|
|
9165
|
+
evaluated.soft,
|
|
9064
9166
|
);
|
|
9065
|
-
if (report.offsets.length !==
|
|
9167
|
+
if (report.offsets.length !== evaluated.vertexCount * 2) {
|
|
9066
9168
|
// Unreachable while `setup` is one pair per vertex, which is the whole
|
|
9067
9169
|
// reason both are derived from the same walk. Stated rather than
|
|
9068
9170
|
// assumed: a silent mismatch here is the overrun A35 exists for.
|
|
9069
9171
|
throw new CompileError(
|
|
9070
9172
|
`${where} (t=${key.t}): the transform produced ${report.offsets.length} numbers for ` +
|
|
9071
|
-
`${
|
|
9173
|
+
`${evaluated.vertexCount} vertices`, 'CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT',
|
|
9072
9174
|
);
|
|
9073
9175
|
}
|
|
9074
9176
|
// -- one displacement per vertex -> one pair per influence (issue #389) -
|
|
@@ -9080,11 +9182,11 @@ function compileDeformTrack(
|
|
|
9080
9182
|
// displacement it expands is the ROUNDED one the report prints, so a
|
|
9081
9183
|
// reader can reproduce every emitted pair from the audit rather than from
|
|
9082
9184
|
// a second evaluation.
|
|
9083
|
-
const run = expandDeformToInfluences(report.offsets,
|
|
9084
|
-
if (run.length !==
|
|
9185
|
+
const run = expandDeformToInfluences(report.offsets, evaluated);
|
|
9186
|
+
if (run.length !== evaluated.deformLength) {
|
|
9085
9187
|
throw new CompileError(
|
|
9086
9188
|
`${where} (t=${key.t}): the transform expanded to ${run.length} numbers and this attachment's deform ` +
|
|
9087
|
-
`array is ${
|
|
9189
|
+
`array is ${evaluated.deformLength} long`, 'CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH',
|
|
9088
9190
|
);
|
|
9089
9191
|
}
|
|
9090
9192
|
generated.push({
|
|
@@ -9100,14 +9202,14 @@ function compileDeformTrack(
|
|
|
9100
9202
|
// other way to know they were not evaluated alike. Said only on the path
|
|
9101
9203
|
// where it is not the attachment's own space, so every existing report
|
|
9102
9204
|
// prints exactly the lines it printed before.
|
|
9103
|
-
...(
|
|
9205
|
+
...(evaluated.influenceBones === null
|
|
9104
9206
|
? {}
|
|
9105
9207
|
: {
|
|
9106
9208
|
expanded: run,
|
|
9107
9209
|
derived: [
|
|
9108
9210
|
...report.derived,
|
|
9109
|
-
`evaluated at setup WORLD positions, because ${
|
|
9110
|
-
`${
|
|
9211
|
+
`evaluated at setup WORLD positions, because ${evaluated.vertexCount} vertices carry ` +
|
|
9212
|
+
`${evaluated.deformLength / 2} influences; each displacement D is written into every influence as ` +
|
|
9111
9213
|
'Mᵢ⁻¹·D, so the runtime composes Σwᵢ·Mᵢ·Mᵢ⁻¹·D = D',
|
|
9112
9214
|
],
|
|
9113
9215
|
}),
|
package/src/core/animation.ts
CHANGED
|
@@ -189,11 +189,12 @@ import {
|
|
|
189
189
|
foldInheritMode,
|
|
190
190
|
gridRound,
|
|
191
191
|
readBlend,
|
|
192
|
-
readColour,
|
|
193
192
|
shownAttachment,
|
|
194
193
|
shownRow,
|
|
195
194
|
slidersKeyingWhy,
|
|
196
195
|
sourceOfDoc,
|
|
196
|
+
viewColours,
|
|
197
|
+
WHITE,
|
|
197
198
|
type CompiledDocument,
|
|
198
199
|
type CoreAnimation,
|
|
199
200
|
type CoreBoneRow,
|
|
@@ -834,12 +835,15 @@ export function posedSlots(
|
|
|
834
835
|
/** Filled with each slot's placeholder once the sample's own timelines have moved it, before the sliders — what the attachments are posed from (`attachmentStates`). */
|
|
835
836
|
placeholders?: Map<string, string | null>,
|
|
836
837
|
): { rows: CoreSlotRow[]; conflicts: string[] } {
|
|
837
|
-
|
|
838
|
+
// The view's stated colours, read once (`viewColours`); a planted reader of the light colour is called as it always was.
|
|
839
|
+
const stated = viewColours(doc);
|
|
840
|
+
const colour = plant.colour ?? stated;
|
|
838
841
|
const byName = new Map(timelines.slots.map((s) => [s.name, s]));
|
|
839
842
|
return slotRowsFrom(doc, plant, sliders, placeholders, (slot, live) => {
|
|
840
843
|
let placeholder = slot.setup;
|
|
841
|
-
const
|
|
842
|
-
const
|
|
844
|
+
const setupLight = slot.color === undefined ? WHITE : colour(slot.color);
|
|
845
|
+
const light = [setupLight[0], setupLight[1], setupLight[2], setupLight[3]];
|
|
846
|
+
const setupDark = slot.dark === undefined ? null : stated(slot.dark);
|
|
843
847
|
const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
|
|
844
848
|
for (const tl of live ? (byName.get(slot.name)?.timelines ?? []) : []) {
|
|
845
849
|
if (tl.kind === 'attachment') {
|
|
@@ -848,7 +852,6 @@ export function posedSlots(
|
|
|
848
852
|
continue;
|
|
849
853
|
}
|
|
850
854
|
const v = valuesAt(tl.keys, t, plant);
|
|
851
|
-
const setupLight = slot.color === undefined ? [1, 1, 1, 1] : colour(slot.color);
|
|
852
855
|
const channels = SLOT_COLOUR_TIMELINES[tl.kind].channels;
|
|
853
856
|
channels.forEach((name, i) => {
|
|
854
857
|
const at = { r: 0, g: 1, b: 2, a: 3, r2: 4, g2: 5, b2: 6 }[name];
|
|
@@ -955,15 +958,22 @@ export function posedBoneRows(doc: CompiledDocument, timelines: CoreAnimationTim
|
|
|
955
958
|
/** `posedBoneRows`, with the world transforms the rows were read off — what the sample's attachments are posed through. */
|
|
956
959
|
export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[], step?: PoseStep): { rows: CoreBoneRow[]; world: Map<string, CoreWorld> } {
|
|
957
960
|
const posed = posedBoneStep(doc, timelines, t, plant, constraints, sliders, step);
|
|
958
|
-
|
|
961
|
+
const stage = plant.stage;
|
|
962
|
+
if (stage !== undefined) stage.begin('readout');
|
|
963
|
+
const rows = stepRows(posed);
|
|
964
|
+
if (stage !== undefined) stage.end('readout');
|
|
965
|
+
return { rows, world: posed.world };
|
|
959
966
|
}
|
|
960
967
|
|
|
961
968
|
/**
|
|
962
969
|
* `posedBoneWorld`'s world transforms without its rows (issue #1179, the
|
|
963
970
|
* second part): the same pose by the same operations in the same order, since
|
|
964
971
|
* `stepRows` only reads the world once it is posed. A walk reads `world` and
|
|
965
|
-
* no row, so the scan's
|
|
966
|
-
*
|
|
972
|
+
* no row, so every walk asks for this — the scan's (`scanWalkIn` in
|
|
973
|
+
* `./raw.ts`, A10's), the raw entry's batch walks and the live track's step.
|
|
974
|
+
* The rows were the only thing the other variant adds, and their rounding
|
|
975
|
+
* (`rawNumber`, `keptNumber`) refuses nothing, so leaving them out moves no
|
|
976
|
+
* number and no refusal of a walk.
|
|
967
977
|
*/
|
|
968
978
|
export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): Map<string, CoreWorld> {
|
|
969
979
|
return posedBoneStep(doc, timelines, t, plant, constraints, sliders, step).world;
|
|
@@ -997,20 +1007,34 @@ function stepRows(posed: PosedStep): CoreBoneRow[] {
|
|
|
997
1007
|
|
|
998
1008
|
/** `posedBoneWorld` up to its rows, refusing a bone the evaluator gave no transform (`PosedStep`). */
|
|
999
1009
|
function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): PosedStep {
|
|
1010
|
+
const stage = plant.stage;
|
|
1011
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1000
1012
|
const bones = posedBones(doc, timelines, t, plant);
|
|
1013
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1001
1014
|
// One step of the stepped phase (issue #956, `./constraints_physics.ts`): the physics records posed by their timelines and stepped under the walk's context; the deformed curve a path walks as above.
|
|
1002
|
-
if (constraints !== undefined && step !== undefined)
|
|
1015
|
+
if (constraints !== undefined && step !== undefined) {
|
|
1016
|
+
return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
|
|
1017
|
+
}
|
|
1003
1018
|
const active = activeBones(doc);
|
|
1004
1019
|
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
1005
|
-
if (step?.adjustment !== undefined)
|
|
1006
|
-
|
|
1020
|
+
if (step?.adjustment !== undefined) {
|
|
1021
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1022
|
+
adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
|
|
1023
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1024
|
+
}
|
|
1025
|
+
if (stage !== undefined) stage.begin('world');
|
|
1026
|
+
const initialWorld = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
1027
|
+
if (stage !== undefined) stage.end('world');
|
|
1028
|
+
let world = initialWorld;
|
|
1007
1029
|
if (constraints !== undefined) {
|
|
1030
|
+
if (stage !== undefined) stage.begin('constraints');
|
|
1008
1031
|
const setupRecords = constraintRecords(doc);
|
|
1009
1032
|
// A path walks the curve the sample's deform timelines left on its slot (`./deform.ts`); a slider's deform of a walked path leaves the bones out (`pathAnimationsWhy`).
|
|
1010
1033
|
const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
|
|
1011
1034
|
// A path constraint may read a slot bone the runtime has not yet brought up to date in this pass, as the previous pass left it (`./constraints_path.ts`, *Which slot bone*); `poseAnimations` holds that pass to the setup pose's reading.
|
|
1012
1035
|
const previous = setupRecords.some((r) => r.kind === 'path') ? applyConstraints(doc.bones, (plant.evaluate ?? worldTransforms)(doc.bones, active), active, plant.constraints ? plant.constraints(setupRecords) : setupRecords, null, undefined, undefined, undefined, solverRules(plant.solver)) : null;
|
|
1013
|
-
world = applyConstraints(bones,
|
|
1036
|
+
world = applyConstraints(bones, initialWorld, active, plant.constraints ? plant.constraints(records) : records, previous, sliders, undefined, undefined, solverRules(plant.solver));
|
|
1037
|
+
if (stage !== undefined) stage.end('constraints');
|
|
1014
1038
|
}
|
|
1015
1039
|
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
1016
1040
|
return { bones, world, active, round: plant.round ?? gridRound };
|
|
@@ -1027,27 +1051,35 @@ function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines,
|
|
|
1027
1051
|
* past the timelines for both.
|
|
1028
1052
|
*/
|
|
1029
1053
|
function steppedLocals(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, records: () => CoreConstraintRecord[], physicsKeyed: NonNullable<CoreConstraintTimelines['physicsKeyed']>, t: number, sliders: SliderApplication[] | undefined, step: PoseStep): PosedStep {
|
|
1054
|
+
const stage = plant.stage;
|
|
1030
1055
|
const active = activeBones(doc);
|
|
1031
1056
|
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
1032
|
-
if (step.adjustment !== undefined)
|
|
1033
|
-
|
|
1057
|
+
if (step.adjustment !== undefined) {
|
|
1058
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1059
|
+
adjustLocals(doc, bones, step.adjustment.callback, step.adjustment.context);
|
|
1060
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1061
|
+
}
|
|
1062
|
+
if (stage !== undefined) stage.begin('world');
|
|
1063
|
+
const initialWorld = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
1064
|
+
if (stage !== undefined) {
|
|
1065
|
+
stage.end('world');
|
|
1066
|
+
stage.begin('constraints');
|
|
1067
|
+
}
|
|
1034
1068
|
const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
|
|
1035
|
-
world = applyConstraints(bones,
|
|
1069
|
+
const world = applyConstraints(bones, initialWorld, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, undefined, solverRules(plant.solver));
|
|
1070
|
+
if (stage !== undefined) stage.end('constraints');
|
|
1036
1071
|
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
1037
1072
|
return { bones, world, active, round: plant.round ?? gridRound };
|
|
1038
1073
|
}
|
|
1039
1074
|
|
|
1040
1075
|
/**
|
|
1041
|
-
* `steppedLocals`' world transforms, the oracle's rows
|
|
1042
|
-
* `
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
* constraint timeline — and no physics timeline keyed.
|
|
1076
|
+
* `steppedLocals`' world transforms, without the oracle's rows (as
|
|
1077
|
+
* `posedBoneWorldAlone` poses them): the live tracks' entry to the stepped
|
|
1078
|
+
* phase (`./tracks.ts`). The document's constraint records as they stand —
|
|
1079
|
+
* the live tracks mix no constraint timeline — and no physics timeline keyed.
|
|
1046
1080
|
*/
|
|
1047
|
-
export function steppedLocalsWorld(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, sliders: SliderApplication[], step: PoseStep
|
|
1048
|
-
|
|
1049
|
-
if (rows) stepRows(posed);
|
|
1050
|
-
return posed.world;
|
|
1081
|
+
export function steppedLocalsWorld(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, sliders: SliderApplication[], step: PoseStep): Map<string, CoreWorld> {
|
|
1082
|
+
return steppedLocals(doc, bones, plant, () => constraintRecords(doc), [], 0, sliders, step).world;
|
|
1051
1083
|
}
|
|
1052
1084
|
|
|
1053
1085
|
/** Each path record with the curve its slot shows deformed by the sample's own deform timelines at `t` (`./deform.ts`), or as it was. */
|
package/src/core/events.ts
CHANGED
|
@@ -33,6 +33,10 @@ import { gridRound } from './index.ts';
|
|
|
33
33
|
|
|
34
34
|
/** An event definition, as the model states it (`ModelEvent`): the payload a key falls back to. */
|
|
35
35
|
export interface CoreEventDef {
|
|
36
|
+
/** Audio metadata retained additively; the five-column oracle row is unchanged. */
|
|
37
|
+
audio?: string;
|
|
38
|
+
volume?: number;
|
|
39
|
+
balance?: number;
|
|
36
40
|
name: string;
|
|
37
41
|
int: number;
|
|
38
42
|
float: number;
|
|
@@ -41,6 +45,9 @@ export interface CoreEventDef {
|
|
|
41
45
|
|
|
42
46
|
/** One event key, as read: its time (float32) and the payload it fires with, each field resolved. */
|
|
43
47
|
export interface CoreEventKey {
|
|
48
|
+
/** Audio delivery values, as observed from the public EventTimeline API. */
|
|
49
|
+
volume?: number;
|
|
50
|
+
balance?: number;
|
|
44
51
|
time: number;
|
|
45
52
|
name: string;
|
|
46
53
|
int: number;
|
|
@@ -55,8 +62,8 @@ const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'obj
|
|
|
55
62
|
const EVENT_DEF_FIELDS = ['name', 'int', 'float', 'string', 'audio', 'volume', 'balance'] as const;
|
|
56
63
|
const EVENT_KEY_FIELDS = ['time', 'name', 'int', 'float', 'string', 'volume', 'balance'] as const;
|
|
57
64
|
|
|
58
|
-
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string } {
|
|
59
|
-
const out: { int?: number; float?: number; string?: string } = {};
|
|
65
|
+
function payload(raw: Record<string, unknown>, at: string, problems: string[]): { int?: number; float?: number; string?: string; volume?: number; balance?: number } {
|
|
66
|
+
const out: { int?: number; float?: number; string?: string; volume?: number; balance?: number } = {};
|
|
60
67
|
if (raw.int !== undefined) {
|
|
61
68
|
if (typeof raw.int !== 'number' || !Number.isInteger(raw.int)) problems.push(`${at}: int is ${JSON.stringify(raw.int)}, not a whole number`);
|
|
62
69
|
else out.int = raw.int;
|
|
@@ -69,7 +76,10 @@ function payload(raw: Record<string, unknown>, at: string, problems: string[]):
|
|
|
69
76
|
if (typeof raw.string !== 'string') problems.push(`${at}: string is ${JSON.stringify(raw.string)}, not a string`);
|
|
70
77
|
else out.string = raw.string;
|
|
71
78
|
}
|
|
72
|
-
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined
|
|
79
|
+
for (const key of ['volume', 'balance'] as const) if (raw[key] !== undefined) {
|
|
80
|
+
if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key])) problems.push(`${at}: ${key} is ${JSON.stringify(raw[key])}, not a finite number`);
|
|
81
|
+
else out[key] = raw[key];
|
|
82
|
+
}
|
|
73
83
|
return out;
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -90,7 +100,7 @@ export function readEventDefs(value: unknown, problems: string[]): Map<string, C
|
|
|
90
100
|
if (raw.audio !== undefined && typeof raw.audio !== 'string') problems.push(`${at} "${raw.name}": audio is ${JSON.stringify(raw.audio)}, not a string`);
|
|
91
101
|
if (out.has(raw.name)) problems.push(`${at}: event "${raw.name}" is declared twice`);
|
|
92
102
|
const p = payload(raw, `${at} "${raw.name}"`, problems);
|
|
93
|
-
out.set(raw.name, { name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
103
|
+
out.set(raw.name, { audio: typeof raw.audio === 'string' ? raw.audio : '', volume: p.volume ?? 0, balance: p.balance ?? 0, name: raw.name, int: p.int ?? 0, float: p.float ?? 0, string: p.string ?? '' });
|
|
94
104
|
});
|
|
95
105
|
return out;
|
|
96
106
|
}
|
|
@@ -121,7 +131,9 @@ export function readEventKeys(value: unknown, label: string, defs: ReadonlyMap<s
|
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
133
|
const p = payload(raw, at, problems);
|
|
124
|
-
|
|
134
|
+
// Observed oracle behavior: a voiced key's omitted balance falls back to
|
|
135
|
+
// the definition's volume, not its balance. A silent event keeps both at 0.
|
|
136
|
+
out.push({ volume: def.audio ? p.volume ?? def.volume ?? 0 : 0, balance: def.audio ? p.balance ?? def.volume ?? 0 : 0, time: Math.fround(typeof t === 'number' && Number.isFinite(t) ? t : 0), name: def.name, int: p.int ?? def.int, float: p.float ?? def.float, string: p.string ?? def.string });
|
|
125
137
|
});
|
|
126
138
|
return out;
|
|
127
139
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -176,7 +176,7 @@ import type { ModelAtlasRect, ModelBone, ModelEditorOrder, ModelPage, ModelPageR
|
|
|
176
176
|
import { worldTransforms, type CoreWorld } from './world.ts';
|
|
177
177
|
import { readAnimationTimelines, type CoreAnimationTimelines } from './animation.ts';
|
|
178
178
|
import { readEventDefs, type CoreEventDef } from './events.ts';
|
|
179
|
-
import { poseGeometry, readGeometry, type CoreAttachmentRow, type CoreClipRow, type CoreGeometry, type DrawWalk, type MeshArraysOf, type RegionPoser, type ShownGeometry, type VertexPoser } from './vertices.ts';
|
|
179
|
+
import { poseGeometry, readGeometry, type CoreAttachmentRow, type CoreClipRow, type CoreGeometry, type DeformedVertexPoser, type DrawWalk, type MeshArraysOf, type RegionPoser, type ShownGeometry, type VertexPoser } from './vertices.ts';
|
|
180
180
|
import type { CoreClippedRow, ShapeClipper, TriangleClipper } from './clipping.ts';
|
|
181
181
|
import { applyConstraints, constraintsAbsentWhy, readConstraintRecord, readConstraintTimelines, solverRules, type ConstraintPlant, type CoreConstraintRecord, type CoreConstraintTimelines, type SolverRules } from './constraints.ts';
|
|
182
182
|
import { readPathRecord } from './constraints_path.ts';
|
|
@@ -1062,6 +1062,23 @@ export type BlendReader = (slot: ModelSlot) => CoreBlendMode;
|
|
|
1062
1062
|
/** What reads a stated light colour into its four channels: `readColour` unless a caller passes another. */
|
|
1063
1063
|
export type ColourReader = (hex: string) => [number, number, number, number];
|
|
1064
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* The five pose stages a step passes through when instrumented (`StageHooks`,
|
|
1067
|
+
* issue #1589):
|
|
1068
|
+
* - `timeline`: timeline evaluation, animated bone local copy, attachment/deform/sequence timeline states
|
|
1069
|
+
* - `world`: bone hierarchy world-transform composition (`worldTransforms`)
|
|
1070
|
+
* - `constraints`: constraint record posing, physics step, and constraint solver orchestration (`applyConstraints`)
|
|
1071
|
+
* - `geometry`: region corners, mesh/deformed world vertices, and triangle clipping (`drawnGeometry`)
|
|
1072
|
+
* - `readout`: slot rows, draw-order resolution/sorting, bone rows, drawn row packaging, and events
|
|
1073
|
+
*/
|
|
1074
|
+
export type CoreStage = 'timeline' | 'world' | 'constraints' | 'geometry' | 'readout';
|
|
1075
|
+
|
|
1076
|
+
/** Zero-cost-when-absent per-stage instrumentation callbacks on `CorePlant` (issue #1589). */
|
|
1077
|
+
export interface StageHooks {
|
|
1078
|
+
begin(stage: CoreStage): void;
|
|
1079
|
+
end(stage: CoreStage): void;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1065
1082
|
/**
|
|
1066
1083
|
* What the core poses with, each part replaceable. The core suite's plants
|
|
1067
1084
|
* pass a copy of one of them — a mode's sign flipped, the wrong skin, a
|
|
@@ -1077,6 +1094,8 @@ export interface CorePlant {
|
|
|
1077
1094
|
region?: RegionPoser;
|
|
1078
1095
|
/** A vertex array's world positions (`worldVertices` in `./vertices.ts`). */
|
|
1079
1096
|
vertices?: VertexPoser;
|
|
1097
|
+
/** A deformed vertex array's world positions (`deformedWorldVertices` in `./vertices.ts`). */
|
|
1098
|
+
deformedVertices?: DeformedVertexPoser;
|
|
1080
1099
|
/** A drawn mesh's read-only UVs and triangles (`meshArraysOf` in `./vertices.ts`) — the raw entry's `drawn`; nothing but a control passes one. */
|
|
1081
1100
|
meshArrays?: MeshArraysOf;
|
|
1082
1101
|
/** The ik, transform and path constraints as posed, rewritten before they are applied (`./constraints.ts`). */
|
|
@@ -1107,6 +1126,8 @@ export interface CorePlant {
|
|
|
1107
1126
|
* `rawNumber`, the unrounded double.
|
|
1108
1127
|
*/
|
|
1109
1128
|
round?: (v: number) => number | null;
|
|
1129
|
+
/** Per-stage instrumentation callbacks for `tools/pose_bench.ts` (issue #1589); absent on every uninstrumented walk. */
|
|
1130
|
+
stage?: StageHooks;
|
|
1110
1131
|
}
|
|
1111
1132
|
|
|
1112
1133
|
/** The document's ik, transform and path constraint records, in its order — what `applyConstraints` runs. */
|
|
@@ -1166,6 +1187,46 @@ export function readColour(hex: string): [number, number, number, number] {
|
|
|
1166
1187
|
return [channel(0), channel(1), channel(2), hex.length === 8 ? channel(3) : 1];
|
|
1167
1188
|
}
|
|
1168
1189
|
|
|
1190
|
+
/** A stated colour's channels, read once and shared (`viewColours`): read, or copied before a write — never handed out. */
|
|
1191
|
+
export type StatedColour = Readonly<[number, number, number, number]>;
|
|
1192
|
+
|
|
1193
|
+
/** Each view's stated colours, by spelling, read once (`viewColours`). */
|
|
1194
|
+
const coloursOfView = new WeakMap<CompiledDocument, Map<string, StatedColour>>();
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* The view's reader of a stated colour: `readColour(hex)` read the first time
|
|
1198
|
+
* the view meets that spelling and kept, frozen, for every pose after it. The
|
|
1199
|
+
* slot rows read every slot's colours on every pose, and a drawn row its
|
|
1200
|
+
* record's (the parse was a measured share of a step). The reading is the
|
|
1201
|
+
* spelling's alone, so the cache is keyed on the spelling rather than on a
|
|
1202
|
+
* slot or a record — a colour respelled in place reads its new spelling, and
|
|
1203
|
+
* a view built anew starts its own. The tuple is shared by every pose, so no
|
|
1204
|
+
* caller hands it out or writes it (`StatedColour` is read-only): a pose's
|
|
1205
|
+
* channels are copied out of it. It is not frozen because a frozen array is
|
|
1206
|
+
* read several times slower under JavaScriptCore, measured on Bun 1.4.4, and
|
|
1207
|
+
* the read is the whole of what the cache is for. The core suite's `CO63`
|
|
1208
|
+
* holds a pose's colours to their spelling and to the pose.
|
|
1209
|
+
*/
|
|
1210
|
+
export function viewColours(doc: CompiledDocument): (hex: string) => StatedColour {
|
|
1211
|
+
let kept = coloursOfView.get(doc);
|
|
1212
|
+
if (kept === undefined) {
|
|
1213
|
+
kept = new Map();
|
|
1214
|
+
coloursOfView.set(doc, kept);
|
|
1215
|
+
}
|
|
1216
|
+
const colours = kept;
|
|
1217
|
+
return (hex) => {
|
|
1218
|
+
let c = colours.get(hex);
|
|
1219
|
+
if (c === undefined) {
|
|
1220
|
+
c = readColour(hex);
|
|
1221
|
+
colours.set(hex, c);
|
|
1222
|
+
}
|
|
1223
|
+
return c;
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** A slot or record that states no light colour: white, opaque. */
|
|
1228
|
+
export const WHITE: StatedColour = [1, 1, 1, 1];
|
|
1229
|
+
|
|
1169
1230
|
/**
|
|
1170
1231
|
* A slot's blend mode: `Normal` when it states none, else the stated
|
|
1171
1232
|
* spelling folded (the header's measurement). `readModel` has already refused
|