@rig-c/core 2.70.0 → 2.71.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 +143 -8
- package/package.json +2 -1
- package/src/assertions/report.ts +1 -1
- package/src/compile.ts +48 -0
- package/src/core/animation.ts +56 -24
- 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 +23 -7
- package/src/depth.ts +17 -4
- package/src/motion.ts +4 -4
- package/src/runtime/curves.ts +106 -0
- package/src/runtime/index.ts +80 -0
- package/src/runtime/model.ts +719 -0
- package/src/runtime/pose.ts +1014 -0
- package/src/trackgen.ts +27 -4
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
|
package/src/core/raw.ts
CHANGED
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
* naming why (`CoreInputError`): a renderer handed a pose with a block
|
|
114
114
|
* missing would draw a different picture in silence.
|
|
115
115
|
*/
|
|
116
|
-
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber,
|
|
117
|
-
import {
|
|
116
|
+
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, shownAttachment, sourceOfDoc, underNoSkin, underSkin, viewColours, WHITE, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
|
|
117
|
+
import { posedBoneWorldAlone, posedSlots, type CoreAnimationTimelines, type LocalAdjust, type TimelinePlant } from './animation.ts';
|
|
118
118
|
import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
|
|
119
119
|
import { freshStepContext, rotatePhysics, steppedPreviousPassWhy, translatePhysics, DEFAULT_PHYSICS_ENVIRONMENT, type PhysicsEnvironment, type PhysicsStepContext } from './constraints_physics.ts';
|
|
120
120
|
import { attachmentStates } from './deform.ts';
|
|
@@ -342,7 +342,7 @@ function scanBones(doc: CompiledDocument, world: ReadonlyMap<string, CoreWorld>)
|
|
|
342
342
|
*/
|
|
343
343
|
function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode): { rows: CoreAttachmentRow[]; clips: CoreClipRow[]; drawnClipped: CoreClippedRow[] } {
|
|
344
344
|
const draw = drawWalkOf(doc, order, plant);
|
|
345
|
-
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices }, mode.loop && mode.live !== true && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
345
|
+
const geometry = poseGeometry(shown, world, sourceOfDoc(doc), mode.keep ? keptNumber : rawNumber, { region: plant.region, vertices: plant.vertices, deformedVertices: plant.deformedVertices }, mode.loop && mode.live !== true && plant.through === undefined ? { ...draw, through: NO_CLIPPED_ROWS } : draw);
|
|
346
346
|
if (geometry.attachments === null) throw new CoreInputError(`the raw pose leaves the attachments out: ${geometry.attachmentsWhy}`);
|
|
347
347
|
// The drawn rows (issue #964): the oracle's `clipped` rows where it is posed, and a concave or inverse clip cut through the core's own decomposition.
|
|
348
348
|
if (geometry.drawnClipped === null) throw new CoreInputError(`the raw pose leaves the clipped triangles out: ${geometry.drawnClippedWhy}`);
|
|
@@ -423,12 +423,20 @@ export interface ScanPlant {
|
|
|
423
423
|
|
|
424
424
|
/** Every region and mesh the shown records draw, with what the renderer reads past the vertices (the header's `drawn`). */
|
|
425
425
|
function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode = RAW_MODE): { drawn: RawDrawn[]; clips: RawClip[]; clipped: RawClipped[] } {
|
|
426
|
+
const stage = plant.stage;
|
|
427
|
+
if (stage !== undefined) stage.begin('geometry');
|
|
426
428
|
const geometry = drawnGeometry(doc, shown, world, order, plant, mode);
|
|
429
|
+
if (stage !== undefined) {
|
|
430
|
+
stage.end('geometry');
|
|
431
|
+
stage.begin('readout');
|
|
432
|
+
}
|
|
427
433
|
// Under a concave or inverse clip the oracle's `clipped` block is absent (the runtime's own triangle list); what the core draws is `drawnClipped`,
|
|
428
434
|
// its own decomposition, and the render samples each drawn triangle at its source triangle's affine UV, so the pixels are the decomposition's
|
|
429
435
|
// coverage alone (issue #964, `Mesh.source` in src/render.ts).
|
|
430
436
|
const rows = geometry.rows;
|
|
431
437
|
const drawn: RawDrawn[] = [];
|
|
438
|
+
// Each record's stated colour, read once per view (`viewColours`) and copied into the pose's own tuple, as the pose's arrays are its own.
|
|
439
|
+
const stated = viewColours(doc);
|
|
432
440
|
let k = 0;
|
|
433
441
|
for (const s of shown) {
|
|
434
442
|
const g = s.geometry;
|
|
@@ -436,7 +444,8 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
|
|
|
436
444
|
const row = rows[k++];
|
|
437
445
|
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
438
446
|
const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
439
|
-
const
|
|
447
|
+
const read = record?.color === undefined ? WHITE : stated(record.color);
|
|
448
|
+
const colour: [number, number, number, number] = [read[0], read[1], read[2], read[3]];
|
|
440
449
|
let uvs: readonly number[];
|
|
441
450
|
let triangles: readonly number[];
|
|
442
451
|
let hull: number | null = null;
|
|
@@ -454,6 +463,7 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
|
|
|
454
463
|
}
|
|
455
464
|
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], numbersOf(c[3], `clip vertex of slot "${c[0]}"`, mode)]);
|
|
456
465
|
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], numbersOf(c[3], `clipped vertex of slot "${c[0]}"`, mode), numbersOf(c[4], `clipped uv of slot "${c[0]}"`, mode), c[5]]);
|
|
466
|
+
if (stage !== undefined) stage.end('readout');
|
|
457
467
|
return { drawn, clips, clipped };
|
|
458
468
|
}
|
|
459
469
|
|
|
@@ -785,12 +795,16 @@ interface WalkStep {
|
|
|
785
795
|
* (`walkEach`) is one body: every number a pose carries is computed there and
|
|
786
796
|
* in the two assemblers below, by the same calls, whatever the shape. A shape
|
|
787
797
|
* chooses only what is assembled from what was posed — the scan drops the
|
|
788
|
-
* readings
|
|
798
|
+
* readings and copies nothing it reads is computed from — and keeps the
|
|
789
799
|
* order of every call that can refuse.
|
|
800
|
+
*
|
|
801
|
+
* No shape forms the oracle's bone rows: no walk pose carries them, and the
|
|
802
|
+
* rounding they are formed through refuses nothing, so every step poses the
|
|
803
|
+
* world alone (`posedBoneWorldAlone`). The rows are the sample's
|
|
804
|
+
* (`posedBoneRows`, `posedBoneWorld`), formed there as before. The core
|
|
805
|
+
* suite's `CO62` reads the walk's modules for a call that forms them.
|
|
790
806
|
*/
|
|
791
807
|
interface PoseShape<P> {
|
|
792
|
-
/** Whether a step forms the oracle's bone rows beside its world (`posedBoneWorld`), which no walk pose carries; `false` poses the world alone (`posedBoneWorldAlone`). */
|
|
793
|
-
rows: boolean;
|
|
794
808
|
/** The reset pose at the setup (`reset: 'setup'`). */
|
|
795
809
|
setup: (doc: CompiledDocument, world: Map<string, CoreWorld>, slots: CoreSlotRow[], drawOrder: string[], shown: ShownGeometry[], plant: CorePlant, mode: WalkMode) => P;
|
|
796
810
|
/** One step's pose. */
|
|
@@ -799,19 +813,21 @@ interface PoseShape<P> {
|
|
|
799
813
|
|
|
800
814
|
/** The raw entry's pose, assembled as it always was: at the reset the bones, then the drawn rows; at a step the drawn rows, the events, then the bones. */
|
|
801
815
|
const RAW_SHAPE: PoseShape<RawPose> = {
|
|
802
|
-
rows: true,
|
|
803
816
|
setup: (doc, world, slots, drawOrder, shown, plant, mode) => ({ trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...rawDrawn(doc, shown, world, drawOrder, plant, mode), events: [], shown: drawOrderOf(shown, drawOrder) }),
|
|
804
817
|
step: (doc, p, plant, mode) => {
|
|
805
818
|
const drawn = rawDrawn(doc, p.shown, p.world, p.drawOrder, plant, mode);
|
|
819
|
+
const stage = plant.stage;
|
|
820
|
+
if (stage !== undefined) stage.begin('readout');
|
|
806
821
|
const events = p.events();
|
|
807
|
-
|
|
822
|
+
const pose: RawPose = { trackTime: p.trackTime, animationTime: p.animationTime, bones: rawBones(doc, p.world), slots: p.slots, drawOrder: p.drawOrder, ...drawn, events, shown: p.shown };
|
|
823
|
+
if (stage !== undefined) stage.end('readout');
|
|
824
|
+
return pose;
|
|
808
825
|
},
|
|
809
826
|
};
|
|
810
827
|
|
|
811
828
|
/** The scan's pose (`ScanPose`), in the raw shape's order: the scan's walk is a looping one, which fires no event. */
|
|
812
829
|
function scanShape(scanPlant: ScanPlant): PoseShape<ScanPose> {
|
|
813
830
|
return {
|
|
814
|
-
rows: false,
|
|
815
831
|
setup: (doc, world, slots, drawOrder, shown, plant, mode) => {
|
|
816
832
|
const bones = scanBones(doc, world);
|
|
817
833
|
return { animationTime: 0, bones, slots, drawn: scanDrawn(doc, shown, world, drawOrder, plant, mode, scanPlant) };
|
|
@@ -908,7 +924,7 @@ function stepWalk<P>(track: TrackState<P>, dt: number, first = false): P {
|
|
|
908
924
|
const keys = anim.timelines.events;
|
|
909
925
|
const adjustment = adjust === undefined ? undefined : { callback: adjust, context: { trackTime, animation: anim.name, globalTime: trackTime } };
|
|
910
926
|
const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
|
|
911
|
-
world: (sliders) =>
|
|
927
|
+
world: (sliders) => posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjustment }),
|
|
912
928
|
slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
|
|
913
929
|
drawOrder: anim.timelines.drawOrder,
|
|
914
930
|
at: t,
|
|
@@ -948,20 +964,30 @@ export interface StepPosers {
|
|
|
948
964
|
* and a live tracks' step share.
|
|
949
965
|
*/
|
|
950
966
|
function poseStepFrom<P>(doc: CompiledDocument, plant: TimelinePlant, resolve: NonNullable<TimelinePlant['shown']>, shape: PoseShape<P>, mode: WalkMode, ctx: ReturnType<typeof freshStepContext>, first: boolean, trackTime: number, animationTime: number, posers: StepPosers): P {
|
|
967
|
+
const stage = plant.stage;
|
|
951
968
|
const sliders: SliderApplication[] = [];
|
|
952
969
|
const world = posers.world(sliders);
|
|
953
970
|
if (first) ctx.phase = 'update';
|
|
971
|
+
if (stage !== undefined) stage.begin('readout');
|
|
954
972
|
const placeholders = new Map<string, string | null>();
|
|
955
973
|
const slots = posers.slots(sliders, placeholders);
|
|
956
974
|
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
957
975
|
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
958
|
-
|
|
976
|
+
const baseOrder = evalOrder(doc.slots.length, posers.drawOrder, posers.at) ?? doc.slots.map((_s, k) => k);
|
|
977
|
+
let order = baseOrder;
|
|
959
978
|
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
960
979
|
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
980
|
+
if (stage !== undefined) {
|
|
981
|
+
stage.end('readout');
|
|
982
|
+
stage.begin('timeline');
|
|
983
|
+
}
|
|
961
984
|
const states = attachmentStates(doc, resolve, placeholders, posers.sample, sliders, plant, posers.mixed);
|
|
985
|
+
if (stage !== undefined) stage.end('timeline');
|
|
962
986
|
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
987
|
+
if (stage !== undefined) stage.begin('readout');
|
|
963
988
|
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
964
989
|
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
990
|
+
if (stage !== undefined) stage.end('readout');
|
|
965
991
|
const events = (): RawEvent[] => posers.events().map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
|
|
966
992
|
return shape.step(doc, { trackTime, animationTime, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
967
993
|
}
|
package/src/core/tracks.ts
CHANGED
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
* every later call refuses naming the call it was refused at.
|
|
161
161
|
*/
|
|
162
162
|
import type { ModelBone, ModelSlot, ModelVertices } from '../model.ts';
|
|
163
|
-
import { activeBones, CoreInputError, rawNumber,
|
|
163
|
+
import { activeBones, CoreInputError, rawNumber, shownAttachment, viewColours, WHITE, type CompiledDocument, type CoreShown, type StatedColour } from './index.ts';
|
|
164
164
|
import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
|
|
165
165
|
import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
|
|
166
166
|
import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
|
|
@@ -440,6 +440,9 @@ function trackComplete(e: Entry): number {
|
|
|
440
440
|
const signum = (v: number): number => (v > 0 ? 1 : v < 0 ? -1 : 0);
|
|
441
441
|
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
442
442
|
|
|
443
|
+
/** A slot's setup dark colour where it states none: never read, since such a slot carries no dark channel and a two-colour timeline returns before it (`applyColour`). */
|
|
444
|
+
const NO_DARK: readonly number[] = [0, 0, 0];
|
|
445
|
+
|
|
443
446
|
// ---------------------------------------------------------------------------
|
|
444
447
|
// one step's skeleton — the pose the entries are applied to
|
|
445
448
|
// ---------------------------------------------------------------------------
|
|
@@ -473,6 +476,8 @@ class Pose {
|
|
|
473
476
|
private readonly active: ReadonlySet<string>;
|
|
474
477
|
private readonly live: boolean[];
|
|
475
478
|
private readonly resolved = new Map<string, Resolved>();
|
|
479
|
+
/** The view's stated colours (`viewColours`). */
|
|
480
|
+
readonly stated: (hex: string) => StatedColour;
|
|
476
481
|
|
|
477
482
|
constructor(
|
|
478
483
|
private readonly doc: CompiledDocument,
|
|
@@ -480,12 +485,16 @@ class Pose {
|
|
|
480
485
|
) {
|
|
481
486
|
this.bones = [...doc.bones];
|
|
482
487
|
this.active = activeBones(doc);
|
|
483
|
-
|
|
488
|
+
// The view's stated colours, read once (`viewColours`) and copied into the step's own channels; a planted reader of the light colour is called as it always was.
|
|
489
|
+
const stated = viewColours(doc);
|
|
490
|
+
this.stated = stated;
|
|
491
|
+
const colour = plant.colour ?? stated;
|
|
484
492
|
const gate = plant.slotTimelines ?? slotTimelinesApply;
|
|
485
493
|
this.live = doc.slots.map((s) => gate(doc, s, this.active));
|
|
486
494
|
this.slots = doc.slots.map((slot): SlotState => {
|
|
487
|
-
const setupDark = slot.dark === undefined ? null :
|
|
488
|
-
|
|
495
|
+
const setupDark = slot.dark === undefined ? null : stated(slot.dark);
|
|
496
|
+
const setupLight = slot.color === undefined ? WHITE : colour(slot.color);
|
|
497
|
+
return { placeholder: slot.setup, light: [setupLight[0], setupLight[1], setupLight[2], setupLight[3]], dark: setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]], deform: null, seen: [], attachmentState: ATTACH_NONE };
|
|
489
498
|
});
|
|
490
499
|
}
|
|
491
500
|
|
|
@@ -1103,9 +1112,9 @@ class Mixer {
|
|
|
1103
1112
|
const s = pose.slots[tl.slot];
|
|
1104
1113
|
const l = s.light;
|
|
1105
1114
|
const d = s.dark;
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
const sd =
|
|
1115
|
+
// The setup colours, read only: the view's (`viewColours`), or a planted reader's light colour.
|
|
1116
|
+
const sl = slot.color === undefined ? WHITE : (this.plant.colour ?? pose.stated)(slot.color);
|
|
1117
|
+
const sd = slot.dark === undefined ? NO_DARK : pose.stated(slot.dark);
|
|
1109
1118
|
const clampLight = (): void => {
|
|
1110
1119
|
for (let k = 0; k < 4; k++) l[k] = clamp01(l[k]);
|
|
1111
1120
|
};
|
|
@@ -1466,7 +1475,10 @@ class Tracks implements LiveTracks {
|
|
|
1466
1475
|
if (typeof dt !== 'number' || !Number.isFinite(dt) || dt < 0) throw new CoreInputError(`the live tracks' step ${n} is ${String(dt)}, not a finite time at or above 0`);
|
|
1467
1476
|
return this.guard(`step ${n}`, () => {
|
|
1468
1477
|
this.time += dt;
|
|
1478
|
+
const stage = this.plant.stage;
|
|
1479
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1469
1480
|
this.mixer.update(dt);
|
|
1481
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1470
1482
|
const pose = this.pose(dt, false);
|
|
1471
1483
|
this.steps = n;
|
|
1472
1484
|
return pose;
|
|
@@ -1489,6 +1501,8 @@ class Tracks implements LiveTracks {
|
|
|
1489
1501
|
const ctx = path.ctx;
|
|
1490
1502
|
const before = ctx.time;
|
|
1491
1503
|
ctx.time += this.clock === undefined ? dt : this.clock(dt);
|
|
1504
|
+
const stage = this.plant.stage;
|
|
1505
|
+
if (stage !== undefined) stage.begin('timeline');
|
|
1492
1506
|
const pose = new Pose(doc, this.plant);
|
|
1493
1507
|
const animationTime = this.mixer.apply(pose);
|
|
1494
1508
|
const entry = this.mixer.tracks[0] ?? null;
|
|
@@ -1497,8 +1511,9 @@ class Tracks implements LiveTracks {
|
|
|
1497
1511
|
context: { trackTime: entry?.trackTime ?? null, animation: entry?.anim.name ?? null, globalTime: this.time },
|
|
1498
1512
|
};
|
|
1499
1513
|
const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
|
|
1514
|
+
if (stage !== undefined) stage.end('timeline');
|
|
1500
1515
|
return path.step(first, this.time, animationTime, {
|
|
1501
|
-
world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }
|
|
1516
|
+
world: (sliders) => steppedLocalsWorld(doc, pose.bones, path.raw, sliders, { ctx, before, adjustment }),
|
|
1502
1517
|
slots: (sliders, placeholders) => slotRowsFrom(doc, path.raw, sliders, placeholders, (_slot, _live): SlotBase => {
|
|
1503
1518
|
const s = pose.slots[doc.slots.indexOf(_slot)];
|
|
1504
1519
|
return { placeholder: s.placeholder, light: s.light, dark: s.dark };
|
package/src/core/vertices.ts
CHANGED
|
@@ -191,6 +191,8 @@ export type CoreClipRow = [string, string, string | null, Array<number | null>];
|
|
|
191
191
|
export type RegionPoser = (region: CoreRegionGeometry & { atlas: ModelAtlasRect }, bone: CoreWorld) => number[];
|
|
192
192
|
/** What computes a vertex array's world positions: `worldVertices` unless a caller passes another. */
|
|
193
193
|
export type VertexPoser = (vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords?: (v: number) => number) => number[];
|
|
194
|
+
/** What computes a deformed vertex array's world positions in one pass: `deformedWorldVertices` unless a caller passes another. */
|
|
195
|
+
export type DeformedVertexPoser = (vertices: ModelVertices, deform: readonly number[], bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>) => number[];
|
|
194
196
|
|
|
195
197
|
/** A deformed array's coordinates are read as they are (`./deform.ts`, `deformedVertices`): they are not the stored float32s. */
|
|
196
198
|
export const EXACT_COORDS = (v: number): number => v;
|
|
@@ -222,31 +224,157 @@ export function regionCorners(region: CoreRegionGeometry & { atlas: ModelAtlasRe
|
|
|
222
224
|
return out;
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/** A weighted vertex array as the model states it. */
|
|
228
|
+
export type WeightedVertices = Extract<ModelVertices, { weighted: true }>;
|
|
229
|
+
|
|
225
230
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
231
|
+
* One influence of a compiled weighted vertex (issue #1580): its bone as an
|
|
232
|
+
* index into `CompiledWeightedVertices.bones`, its raw coordinates (`x`, `y`)
|
|
233
|
+
* for a caller passing a custom `coords`, and its `Math.fround`-rounded
|
|
234
|
+
* coordinates (`fx`, `fy`) and `weight` for the default posing path.
|
|
230
235
|
*/
|
|
231
|
-
export
|
|
236
|
+
export interface CompiledBinding {
|
|
237
|
+
readonly bone: number;
|
|
238
|
+
readonly x: number;
|
|
239
|
+
readonly y: number;
|
|
240
|
+
readonly fx: number;
|
|
241
|
+
readonly fy: number;
|
|
242
|
+
readonly weight: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* A weighted vertex array's bone bindings compiled to integer indices into
|
|
247
|
+
* `bones` (issue #1580): `bones` lists the distinct bone names in exact
|
|
248
|
+
* first-occurrence order across `vertices.bindings`, and `bindings` keeps the
|
|
249
|
+
* outer vertex order and inner influence order untouched so the summation
|
|
250
|
+
* order and every posed coordinate stay bit-for-bit (`Object.is`) identical.
|
|
251
|
+
*/
|
|
252
|
+
export interface CompiledWeightedVertices {
|
|
253
|
+
readonly bones: readonly string[];
|
|
254
|
+
readonly bindings: ReadonlyArray<readonly CompiledBinding[]>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Each weighted vertex array's compiled index table, keyed on the `WeightedVertices` object itself. */
|
|
258
|
+
const compiledWeightedByVertices = new WeakMap<WeightedVertices, CompiledWeightedVertices>();
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compile `vertices`'s bone names to integer indices in first-occurrence order
|
|
262
|
+
* (issue #1580). With `mapIndex`, each binding's index is passed through it
|
|
263
|
+
* instead (the core suite's wrong-index plant).
|
|
264
|
+
*/
|
|
265
|
+
export function compileWeightedVertices(
|
|
266
|
+
vertices: WeightedVertices,
|
|
267
|
+
mapIndex?: (boneIndex: number, boneCount: number) => number,
|
|
268
|
+
): CompiledWeightedVertices {
|
|
269
|
+
const bones: string[] = [];
|
|
270
|
+
const indexByName = new Map<string, number>();
|
|
271
|
+
for (const influences of vertices.bindings) {
|
|
272
|
+
for (const binding of influences) {
|
|
273
|
+
if (!indexByName.has(binding.bone)) {
|
|
274
|
+
indexByName.set(binding.bone, bones.length);
|
|
275
|
+
bones.push(binding.bone);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const count = bones.length;
|
|
280
|
+
const bindings: CompiledBinding[][] = vertices.bindings.map((influences) =>
|
|
281
|
+
influences.map((binding) => {
|
|
282
|
+
const idx = indexByName.get(binding.bone) as number;
|
|
283
|
+
return {
|
|
284
|
+
bone: mapIndex === undefined ? idx : mapIndex(idx, count),
|
|
285
|
+
x: binding.x,
|
|
286
|
+
y: binding.y,
|
|
287
|
+
fx: Math.fround(binding.x),
|
|
288
|
+
fy: Math.fround(binding.y),
|
|
289
|
+
weight: Math.fround(binding.weight),
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
return { bones, bindings };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The cached `CompiledWeightedVertices` for `vertices`, compiled once per
|
|
298
|
+
* `WeightedVertices` object and never shared across two different vertex
|
|
299
|
+
* objects.
|
|
300
|
+
*/
|
|
301
|
+
export function compiledWeightedOf(vertices: WeightedVertices): CompiledWeightedVertices {
|
|
302
|
+
let kept = compiledWeightedByVertices.get(vertices);
|
|
303
|
+
if (kept === undefined) {
|
|
304
|
+
kept = compileWeightedVertices(vertices);
|
|
305
|
+
compiledWeightedByVertices.set(vertices, kept);
|
|
306
|
+
}
|
|
307
|
+
return kept;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resolve each distinct bone of `bones` once per pose call into a dense array
|
|
312
|
+
* indexed by `CompiledBinding.bone`. Because `bones` is in first-occurrence
|
|
313
|
+
* order across `vertices.bindings`, a missing bone throws the same error on
|
|
314
|
+
* the same bone name as a per-influence lookup.
|
|
315
|
+
*/
|
|
316
|
+
function resolveBindingBones(bones: readonly string[], world: ReadonlyMap<string, CoreWorld>): CoreWorld[] {
|
|
317
|
+
const transforms = new Array<CoreWorld>(bones.length);
|
|
318
|
+
for (let i = 0; i < bones.length; i++) {
|
|
319
|
+
const name = bones[i];
|
|
320
|
+
const t = world.get(name);
|
|
321
|
+
if (t === undefined) throw new Error(`a binding names bone "${name}", which has no world transform (readModel refuses it first)`);
|
|
322
|
+
transforms[i] = t;
|
|
323
|
+
}
|
|
324
|
+
return transforms;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Evaluate a compiled weighted vertex table against `world` (issue #1580),
|
|
329
|
+
* looking up each distinct bone once per call and summing influences in their
|
|
330
|
+
* original order.
|
|
331
|
+
*/
|
|
332
|
+
export function compiledWorldVertices(
|
|
333
|
+
compiled: CompiledWeightedVertices,
|
|
334
|
+
world: ReadonlyMap<string, CoreWorld>,
|
|
335
|
+
coords: (v: number) => number = Math.fround,
|
|
336
|
+
): number[] {
|
|
337
|
+
const transforms = resolveBindingBones(compiled.bones, world);
|
|
232
338
|
const out: number[] = [];
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
339
|
+
const fround = coords === Math.fround;
|
|
340
|
+
for (const influences of compiled.bindings) {
|
|
341
|
+
let wx = 0;
|
|
342
|
+
let wy = 0;
|
|
343
|
+
for (const binding of influences) {
|
|
344
|
+
const t = transforms[binding.bone];
|
|
345
|
+
const x = fround ? binding.fx : coords(binding.x);
|
|
346
|
+
const y = fround ? binding.fy : coords(binding.y);
|
|
347
|
+
const weight = binding.weight;
|
|
348
|
+
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
349
|
+
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
238
350
|
}
|
|
239
|
-
|
|
351
|
+
out.push(wx, wy);
|
|
240
352
|
}
|
|
241
|
-
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Evaluate a compiled weighted vertex table with `deform` in one pass
|
|
358
|
+
* (issues #1530, #1580), looking up each distinct bone once per call and
|
|
359
|
+
* summing influences in their original order.
|
|
360
|
+
*/
|
|
361
|
+
export function compiledDeformedWorldVertices(
|
|
362
|
+
compiled: CompiledWeightedVertices,
|
|
363
|
+
deform: readonly number[],
|
|
364
|
+
world: ReadonlyMap<string, CoreWorld>,
|
|
365
|
+
): number[] {
|
|
366
|
+
const transforms = resolveBindingBones(compiled.bones, world);
|
|
367
|
+
const out: number[] = [];
|
|
368
|
+
let k = 0;
|
|
369
|
+
for (const influences of compiled.bindings) {
|
|
242
370
|
let wx = 0;
|
|
243
371
|
let wy = 0;
|
|
244
372
|
for (const binding of influences) {
|
|
245
|
-
const t =
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const weight =
|
|
373
|
+
const t = transforms[binding.bone];
|
|
374
|
+
const x = binding.fx + deform[k];
|
|
375
|
+
const y = binding.fy + deform[k + 1];
|
|
376
|
+
k += 2;
|
|
377
|
+
const weight = binding.weight;
|
|
250
378
|
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
251
379
|
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
252
380
|
}
|
|
@@ -255,20 +383,42 @@ export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: R
|
|
|
255
383
|
return out;
|
|
256
384
|
}
|
|
257
385
|
|
|
386
|
+
/**
|
|
387
|
+
* A vertex array's world positions — the header's mesh rule, every stored
|
|
388
|
+
* number through `Math.fround`; with `coords`, the coordinates through it
|
|
389
|
+
* instead (a deformed array's are read as they are, `EXACT_COORDS`), the
|
|
390
|
+
* weights through `Math.fround` still. Weighted bone bindings are compiled once
|
|
391
|
+
* per vertex object to integer indices (`compiledWeightedOf`, issue #1580) so
|
|
392
|
+
* each pose looks up each distinct bone once rather than per influence.
|
|
393
|
+
*/
|
|
394
|
+
export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>, coords: (v: number) => number = Math.fround): number[] {
|
|
395
|
+
if (!vertices.weighted) {
|
|
396
|
+
const out: number[] = [];
|
|
397
|
+
for (let i = 0; i + 1 < vertices.xy.length; i += 2) {
|
|
398
|
+
const x = coords(vertices.xy[i]);
|
|
399
|
+
const y = coords(vertices.xy[i + 1]);
|
|
400
|
+
out.push(x * bone.a + y * bone.b + bone.worldX, x * bone.c + y * bone.d + bone.worldY);
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
return compiledWorldVertices(compiledWeightedOf(vertices), world, coords);
|
|
405
|
+
}
|
|
406
|
+
|
|
258
407
|
/**
|
|
259
408
|
* `worldVertices(deformedVertices(vertices, deform), bone, world, EXACT_COORDS)`
|
|
260
|
-
* in one pass (
|
|
409
|
+
* in one pass (issues #1530, #1580): the same operations on the same numbers in the
|
|
261
410
|
* same order — an unweighted vertex's coordinates are the deform array's,
|
|
262
411
|
* read as they are; a weighted binding's are its float32 coordinate plus the
|
|
263
|
-
* deform's pair, its weight through `Math.fround
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
412
|
+
* deform's pair, its weight through `Math.fround`, its bone transform indexed
|
|
413
|
+
* through `compiledWeightedOf` — without building the deformed array (and,
|
|
414
|
+
* weighted, one binding record per influence) on every pose only to map it
|
|
415
|
+
* again. A planted vertex poser still reads the deformed array (`poseGeometry`).
|
|
416
|
+
* Posed output is held equal by the core gate's sample attachments and by
|
|
417
|
+
* `tools/posed_frames.ts` across the change.
|
|
268
418
|
*/
|
|
269
419
|
export function deformedWorldVertices(vertices: ModelVertices, deform: readonly number[], bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>): number[] {
|
|
270
|
-
const out: number[] = [];
|
|
271
420
|
if (!vertices.weighted) {
|
|
421
|
+
const out: number[] = [];
|
|
272
422
|
for (let i = 0; i + 1 < deform.length; i += 2) {
|
|
273
423
|
const x = deform[i];
|
|
274
424
|
const y = deform[i + 1];
|
|
@@ -276,23 +426,7 @@ export function deformedWorldVertices(vertices: ModelVertices, deform: readonly
|
|
|
276
426
|
}
|
|
277
427
|
return out;
|
|
278
428
|
}
|
|
279
|
-
|
|
280
|
-
for (const influences of vertices.bindings) {
|
|
281
|
-
let wx = 0;
|
|
282
|
-
let wy = 0;
|
|
283
|
-
for (const binding of influences) {
|
|
284
|
-
const t = world.get(binding.bone);
|
|
285
|
-
if (t === undefined) throw new Error(`a binding names bone "${binding.bone}", which has no world transform (readModel refuses it first)`);
|
|
286
|
-
const x = Math.fround(binding.x) + deform[k];
|
|
287
|
-
const y = Math.fround(binding.y) + deform[k + 1];
|
|
288
|
-
k += 2;
|
|
289
|
-
const weight = Math.fround(binding.weight);
|
|
290
|
-
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
291
|
-
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
292
|
-
}
|
|
293
|
-
out.push(wx, wy);
|
|
294
|
-
}
|
|
295
|
-
return out;
|
|
429
|
+
return compiledDeformedWorldVertices(compiledWeightedOf(vertices), deform, world);
|
|
296
430
|
}
|
|
297
431
|
|
|
298
432
|
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -342,7 +476,10 @@ function readVertices(value: unknown, where: string, bones: ReadonlySet<string>,
|
|
|
342
476
|
for (const key of ['x', 'y', 'weight'] as const) if (!finite(b[key])) problems.push(`${at}: ${key} is ${JSON.stringify(b[key]) ?? 'absent'}, not a finite number`);
|
|
343
477
|
});
|
|
344
478
|
});
|
|
345
|
-
|
|
479
|
+
if (problems.length !== before) return undefined;
|
|
480
|
+
const weighted: WeightedVertices = { weighted: true, bindings: bindings as ModelBinding[][] };
|
|
481
|
+
compiledWeightedByVertices.set(weighted, compileWeightedVertices(weighted));
|
|
482
|
+
return weighted;
|
|
346
483
|
}
|
|
347
484
|
|
|
348
485
|
/**
|
|
@@ -529,11 +666,12 @@ export function poseGeometry(
|
|
|
529
666
|
world: ReadonlyMap<string, CoreWorld>,
|
|
530
667
|
sourceOf: SourceOf,
|
|
531
668
|
round: (v: number) => number | null,
|
|
532
|
-
plant: { region?: RegionPoser; vertices?: VertexPoser } = {},
|
|
669
|
+
plant: { region?: RegionPoser; vertices?: VertexPoser; deformedVertices?: DeformedVertexPoser } = {},
|
|
533
670
|
draw?: DrawWalk,
|
|
534
671
|
): { attachments: CoreAttachmentRow[] | null; attachmentsWhy: string | null; clips: CoreClipRow[]; clipped: CoreClippedRow[] | null; clippedWhy: string | null; drawnClipped: CoreClippedRow[] | null; drawnClippedWhy: string | null } {
|
|
535
672
|
const region = plant.region ?? regionCorners;
|
|
536
673
|
const vertices = plant.vertices ?? worldVertices;
|
|
674
|
+
const deformPoser = plant.deformedVertices ?? deformedWorldVertices;
|
|
537
675
|
const attachments: CoreAttachmentRow[] = [];
|
|
538
676
|
const clips: CoreClipRow[] = [];
|
|
539
677
|
const nulls: string[] = [];
|
|
@@ -548,7 +686,7 @@ export function poseGeometry(
|
|
|
548
686
|
// A deform array replaces the stored vertices, its coordinates read as they are (`./deform.ts`).
|
|
549
687
|
// Unplanted, the deformed positions are read in one pass (`deformedWorldVertices`, issue #1530) rather than through a deformed copy of the array.
|
|
550
688
|
const deform = s.deform;
|
|
551
|
-
const drawn = (v: ModelVertices): number[] => (deform === undefined ? vertices(v, bone, world) : plant.vertices === undefined ?
|
|
689
|
+
const drawn = (v: ModelVertices): number[] => (deform === undefined ? vertices(v, bone, world) : plant.vertices === undefined || plant.deformedVertices !== undefined ? deformPoser(v, deform, bone, world) : vertices(deformedVertices(v, deform), bone, world, EXACT_COORDS));
|
|
552
690
|
if (g.kind === 'region') {
|
|
553
691
|
const rect = s.frame === undefined ? g.region.atlas : (g.region.frames?.[s.frame] ?? g.region.atlas);
|
|
554
692
|
if (rect === null) {
|