rig-c 2.35.1 → 2.37.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/package.json +1 -1
- package/src/core/animation.ts +203 -19
- package/src/core/deform.ts +11 -2
- package/src/core/entry.ts +5 -2
- package/src/core/raw.ts +99 -20
- package/src/core/track.ts +51 -7
- package/src/core/tracks.ts +1262 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.37.0",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/core/animation.ts
CHANGED
|
@@ -706,6 +706,108 @@ export function posedBones(doc: CompiledDocument, timelines: CoreAnimationTimeli
|
|
|
706
706
|
});
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
/**
|
|
710
|
+
* A player's bone-local adjustment (issue #1336): a function the live track
|
|
711
|
+
* (`openTrack`'s `adjust`, `./track.ts`) calls once per pose, after the
|
|
712
|
+
* animation's timelines have posed the bone locals at the step's time and
|
|
713
|
+
* before the world transforms, the constraints and the physics step — so a
|
|
714
|
+
* write is what the rest of the step reads: the bone's children, the
|
|
715
|
+
* attachments drawn through it, every constraint and the physics constraint
|
|
716
|
+
* stepped on top of it. It is spine-core's "application code" writing
|
|
717
|
+
* `Bone.pose` between `AnimationState.apply` and `updateWorldTransform`
|
|
718
|
+
* (`CO50` holds the two equal at tolerance 0).
|
|
719
|
+
*
|
|
720
|
+
* - **The view is the step's locals, posed from the setup pose.** Every pose
|
|
721
|
+
* of a walk is posed from the setup pose at its time (`./raw.ts`,
|
|
722
|
+
* `TrackState`), so a write lives for the pose it was written in and the
|
|
723
|
+
* next step's hook reads the timelines' value again, not the write. A
|
|
724
|
+
* channel no timeline keys reads its setup value on every step.
|
|
725
|
+
* - **A bone the hook does not write is untouched**: the timelines' bone
|
|
726
|
+
* object as it was, the setup bone itself where nothing keys it. A written
|
|
727
|
+
* bone is copied before the write, so the document is never written.
|
|
728
|
+
* - **A write that is not a finite number is refused by name**, the bone, the
|
|
729
|
+
* channel and the value — where the core refuses a non-finite value, by
|
|
730
|
+
* name, rather than posing a picture with it (`finite` in `./raw.ts`).
|
|
731
|
+
*/
|
|
732
|
+
export interface BoneLocals {
|
|
733
|
+
rotation: number;
|
|
734
|
+
x: number;
|
|
735
|
+
y: number;
|
|
736
|
+
scaleX: number;
|
|
737
|
+
scaleY: number;
|
|
738
|
+
shearX: number;
|
|
739
|
+
shearY: number;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The locals of a step's bones, by name (`BoneLocals`). */
|
|
743
|
+
export interface AdjustableLocals {
|
|
744
|
+
/** The document's bones, in its order. */
|
|
745
|
+
readonly names: readonly string[];
|
|
746
|
+
/** One bone's locals, read and written in place; a name the document has no bone for is refused by name. */
|
|
747
|
+
bone(name: string): BoneLocals;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** What `openTrack`'s `adjust` is: called once per pose with the step's locals (`BoneLocals`). */
|
|
751
|
+
export type LocalAdjust = (locals: AdjustableLocals) => void;
|
|
752
|
+
|
|
753
|
+
/** The seven channels a `BoneLocals` reads and writes, each with the value an unstated one reads as (`worldTransforms`' defaults). */
|
|
754
|
+
const LOCAL_CHANNELS = { rotation: 0, x: 0, y: 0, scaleX: 1, scaleY: 1, shearX: 0, shearY: 0 } as const;
|
|
755
|
+
type LocalChannel = keyof typeof LOCAL_CHANNELS;
|
|
756
|
+
|
|
757
|
+
/** One bone of a step's `bones`, read through and written through, copied on its first write (`BoneLocals`' second rule). */
|
|
758
|
+
class StepLocals implements BoneLocals {
|
|
759
|
+
constructor(
|
|
760
|
+
private readonly bones: ModelBone[],
|
|
761
|
+
private readonly index: number,
|
|
762
|
+
private readonly setup: ModelBone,
|
|
763
|
+
) {}
|
|
764
|
+
|
|
765
|
+
private read(channel: LocalChannel): number {
|
|
766
|
+
return this.bones[this.index][channel] ?? LOCAL_CHANNELS[channel];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private write(channel: LocalChannel, value: number): void {
|
|
770
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new CoreInputError(`the adjustment wrote bone "${this.setup.name}" ${channel} = ${String(value)}, not a finite number`);
|
|
771
|
+
if (this.bones[this.index] === this.setup) this.bones[this.index] = { ...this.setup };
|
|
772
|
+
this.bones[this.index][channel] = value;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
get rotation(): number { return this.read('rotation'); }
|
|
776
|
+
set rotation(v: number) { this.write('rotation', v); }
|
|
777
|
+
get x(): number { return this.read('x'); }
|
|
778
|
+
set x(v: number) { this.write('x', v); }
|
|
779
|
+
get y(): number { return this.read('y'); }
|
|
780
|
+
set y(v: number) { this.write('y', v); }
|
|
781
|
+
get scaleX(): number { return this.read('scaleX'); }
|
|
782
|
+
set scaleX(v: number) { this.write('scaleX', v); }
|
|
783
|
+
get scaleY(): number { return this.read('scaleY'); }
|
|
784
|
+
set scaleY(v: number) { this.write('scaleY', v); }
|
|
785
|
+
get shearX(): number { return this.read('shearX'); }
|
|
786
|
+
set shearX(v: number) { this.write('shearX', v); }
|
|
787
|
+
get shearY(): number { return this.read('shearY'); }
|
|
788
|
+
set shearY(v: number) { this.write('shearY', v); }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** The adjustment run over a step's `bones` (`posedBones`' array, in the document's order), which it may replace bone by bone (`BoneLocals`). */
|
|
792
|
+
function adjustLocals(doc: CompiledDocument, bones: ModelBone[], adjust: LocalAdjust): void {
|
|
793
|
+
const views = new Map<string, StepLocals>();
|
|
794
|
+
const names = doc.bones.map((b) => b.name);
|
|
795
|
+
adjust({
|
|
796
|
+
names,
|
|
797
|
+
bone(name: string): BoneLocals {
|
|
798
|
+
let view = views.get(name);
|
|
799
|
+
if (view === undefined) {
|
|
800
|
+
const index = names.indexOf(name);
|
|
801
|
+
if (index < 0) throw new CoreInputError(`the adjustment asked for bone ${JSON.stringify(name)}, which is not one of this document's bones`);
|
|
802
|
+
// A copy the timelines made is this step's own; the setup bone is the document's, copied on its first write.
|
|
803
|
+
view = new StepLocals(bones, index, doc.bones[index]);
|
|
804
|
+
views.set(name, view);
|
|
805
|
+
}
|
|
806
|
+
return view;
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
709
811
|
const clamp01 = (v: number): number => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
710
812
|
|
|
711
813
|
/**
|
|
@@ -722,22 +824,13 @@ export function posedSlots(
|
|
|
722
824
|
/** 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`). */
|
|
723
825
|
placeholders?: Map<string, string | null>,
|
|
724
826
|
): { rows: CoreSlotRow[]; conflicts: string[] } {
|
|
725
|
-
const resolve = plant.shown ?? shownAttachment;
|
|
726
827
|
const colour = plant.colour ?? readColour;
|
|
727
|
-
const blend = plant.blend ?? readBlend;
|
|
728
|
-
const round = plant.round ?? gridRound;
|
|
729
828
|
const byName = new Map(timelines.slots.map((s) => [s.name, s]));
|
|
730
|
-
|
|
731
|
-
const conflicts: string[] = [];
|
|
732
|
-
const active = activeBones(doc);
|
|
733
|
-
const gate = plant.slotTimelines ?? slotTimelinesApply;
|
|
734
|
-
for (const slot of doc.slots) {
|
|
829
|
+
return slotRowsFrom(doc, plant, sliders, placeholders, (slot, live) => {
|
|
735
830
|
let placeholder = slot.setup;
|
|
736
831
|
const light = slot.color === undefined ? [1, 1, 1, 1] : colour(slot.color);
|
|
737
832
|
const setupDark = slot.dark === undefined ? null : readColour(slot.dark);
|
|
738
833
|
const dark = setupDark === null ? null : [setupDark[0], setupDark[1], setupDark[2]];
|
|
739
|
-
// A slot on a bone the skin leaves inactive is not animated — not by the sample's timelines, not by a slider (`./skins.ts`).
|
|
740
|
-
const live = gate(doc, slot, active);
|
|
741
834
|
for (const tl of live ? (byName.get(slot.name)?.timelines ?? []) : []) {
|
|
742
835
|
if (tl.kind === 'attachment') {
|
|
743
836
|
const key = keyAt(tl.keys, t, plant);
|
|
@@ -753,6 +846,48 @@ export function posedSlots(
|
|
|
753
846
|
else if (dark !== null && setupDark !== null) dark[at - 4] = v === null ? setupDark[at - 4] : clamp01(v[i]);
|
|
754
847
|
});
|
|
755
848
|
}
|
|
849
|
+
return { placeholder, light, dark };
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* What a slot holds once the timelines have posed it and before the sliders:
|
|
855
|
+
* the placeholder it shows, its light colour and its dark one (`null` where
|
|
856
|
+
* the slot states none). `posedSlots` reads it off one animation at `t`; the
|
|
857
|
+
* live tracks off their mixed entries (`./tracks.ts`, issue #1336).
|
|
858
|
+
*/
|
|
859
|
+
export interface SlotBase {
|
|
860
|
+
placeholder: string | null;
|
|
861
|
+
light: number[];
|
|
862
|
+
dark: number[] | null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* The slot rows from each slot's `SlotBase` — `baseOf`, called once per slot
|
|
867
|
+
* in the document's order with whether the slot is animated at all — then the
|
|
868
|
+
* sliders, the resolution and the oracle's row: `posedSlots`' body past the
|
|
869
|
+
* timelines, which the live tracks share (`./tracks.ts`).
|
|
870
|
+
*/
|
|
871
|
+
export function slotRowsFrom(
|
|
872
|
+
doc: CompiledDocument,
|
|
873
|
+
plant: TimelinePlant,
|
|
874
|
+
sliders: readonly SliderApplication[],
|
|
875
|
+
placeholders: Map<string, string | null> | undefined,
|
|
876
|
+
baseOf: (slot: ModelSlot, live: boolean) => SlotBase,
|
|
877
|
+
): { rows: CoreSlotRow[]; conflicts: string[] } {
|
|
878
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
879
|
+
const blend = plant.blend ?? readBlend;
|
|
880
|
+
const round = plant.round ?? gridRound;
|
|
881
|
+
const rows: CoreSlotRow[] = [];
|
|
882
|
+
const conflicts: string[] = [];
|
|
883
|
+
const active = activeBones(doc);
|
|
884
|
+
const gate = plant.slotTimelines ?? slotTimelinesApply;
|
|
885
|
+
for (const slot of doc.slots) {
|
|
886
|
+
// A slot on a bone the skin leaves inactive is not animated — not by the sample's timelines, not by a slider (`./skins.ts`).
|
|
887
|
+
const live = gate(doc, slot, active);
|
|
888
|
+
const base = baseOf(slot, live);
|
|
889
|
+
let placeholder = base.placeholder;
|
|
890
|
+
const { light, dark } = base;
|
|
756
891
|
// The sliders, after the animation, in constraint order (`./constraints_slider.ts`).
|
|
757
892
|
// What the slot shows after the sample's own timelines: the attachments' deform and sequence timelines are matched against it (`./deform.ts`, *A switch*).
|
|
758
893
|
placeholders?.set(slot.name, placeholder);
|
|
@@ -778,6 +913,21 @@ export function posedSlots(
|
|
|
778
913
|
return { rows, conflicts };
|
|
779
914
|
}
|
|
780
915
|
|
|
916
|
+
/**
|
|
917
|
+
* One step of the stepped phase, as a walk hands it to the bones: the physics
|
|
918
|
+
* context and the clock before the step (`./constraints_physics.ts`), and the
|
|
919
|
+
* caller's adjustment of the locals (`LocalAdjust`). The adjustment rides here
|
|
920
|
+
* rather than on the plant because it is not a rule the core measured and a
|
|
921
|
+
* control plants back — it is the caller's input to one walk — and because
|
|
922
|
+
* the step context exists exactly where a walk is stepped: the live track's
|
|
923
|
+
* every pose but a `reset: 'setup'` pose 0, which no animation is applied to.
|
|
924
|
+
*/
|
|
925
|
+
export interface PoseStep {
|
|
926
|
+
ctx: PhysicsStepContext;
|
|
927
|
+
before: number;
|
|
928
|
+
adjust?: LocalAdjust;
|
|
929
|
+
}
|
|
930
|
+
|
|
781
931
|
/**
|
|
782
932
|
* The bone rows at `t`, in the oracle's shape and rounding. With `constraints`
|
|
783
933
|
* — the animation's constraint timelines — the document's constraints are
|
|
@@ -792,7 +942,7 @@ export function posedBoneRows(doc: CompiledDocument, timelines: CoreAnimationTim
|
|
|
792
942
|
}
|
|
793
943
|
|
|
794
944
|
/** `posedBoneRows`, with the world transforms the rows were read off — what the sample's attachments are posed through. */
|
|
795
|
-
export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[], step?:
|
|
945
|
+
export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant = {}, constraints?: CoreConstraintTimelines, sliders?: SliderApplication[], step?: PoseStep): { rows: CoreBoneRow[]; world: Map<string, CoreWorld> } {
|
|
796
946
|
const posed = posedBoneStep(doc, timelines, t, plant, constraints, sliders, step);
|
|
797
947
|
return { rows: stepRows(posed), world: posed.world };
|
|
798
948
|
}
|
|
@@ -804,7 +954,7 @@ export function posedBoneWorld(doc: CompiledDocument, timelines: CoreAnimationTi
|
|
|
804
954
|
* no row, so the scan's walk (`scanWalkIn` in `./raw.ts`, A10's) asks for
|
|
805
955
|
* this; the raw entry's walk still forms the rows it does not read, unchanged.
|
|
806
956
|
*/
|
|
807
|
-
export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step:
|
|
957
|
+
export function posedBoneWorldAlone(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): Map<string, CoreWorld> {
|
|
808
958
|
return posedBoneStep(doc, timelines, t, plant, constraints, sliders, step).world;
|
|
809
959
|
}
|
|
810
960
|
|
|
@@ -835,15 +985,15 @@ function stepRows(posed: PosedStep): CoreBoneRow[] {
|
|
|
835
985
|
}
|
|
836
986
|
|
|
837
987
|
/** `posedBoneWorld` up to its rows, refusing a bone the evaluator gave no transform (`PosedStep`). */
|
|
838
|
-
function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step:
|
|
839
|
-
const active = activeBones(doc);
|
|
988
|
+
function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant, constraints: CoreConstraintTimelines | undefined, sliders: SliderApplication[] | undefined, step: PoseStep | undefined): PosedStep {
|
|
840
989
|
const bones = posedBones(doc, timelines, t, plant);
|
|
990
|
+
// 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.
|
|
991
|
+
if (constraints !== undefined && step !== undefined) return steppedLocals(doc, bones, plant, () => pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, sliders, step);
|
|
992
|
+
const active = activeBones(doc);
|
|
993
|
+
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
994
|
+
if (step?.adjust !== undefined) adjustLocals(doc, bones, step.adjust);
|
|
841
995
|
let world = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
842
|
-
if (constraints !== undefined
|
|
843
|
-
// 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. No previous pass: under the step it is the previous step's, and `poseAnimations` leaves such bones out.
|
|
844
|
-
const records = stepPhysicsRecords(pathDeformed(doc, posedRecords(constraintRecords(doc), constraints, t), timelines, t, plant), constraints.physicsKeyed ?? [], t, active, step.ctx, step.before);
|
|
845
|
-
world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(records) : records, null, sliders, step.ctx, undefined, solverRules(plant.solver));
|
|
846
|
-
} else if (constraints !== undefined) {
|
|
996
|
+
if (constraints !== undefined) {
|
|
847
997
|
const setupRecords = constraintRecords(doc);
|
|
848
998
|
// 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`).
|
|
849
999
|
const records = pathDeformed(doc, posedRecords(setupRecords, constraints, t), timelines, t, plant);
|
|
@@ -855,6 +1005,40 @@ function posedBoneStep(doc: CompiledDocument, timelines: CoreAnimationTimelines,
|
|
|
855
1005
|
return { bones, world, active, round: plant.round ?? gridRound };
|
|
856
1006
|
}
|
|
857
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* One step of the stepped phase from locals already posed (issue #1336): the
|
|
1010
|
+
* caller's adjustment (`LocalAdjust`), the world transforms, the physics
|
|
1011
|
+
* records posed by `records` and stepped under the walk's context, then every
|
|
1012
|
+
* constraint. No previous pass: under the step it is the previous step's, and
|
|
1013
|
+
* `poseAnimations` leaves such bones out. A single animation's walk reaches it
|
|
1014
|
+
* through `posedBoneStep` with the locals `posedBones` posed; the live tracks
|
|
1015
|
+
* (`./tracks.ts`) with the locals their mixed entries posed — one pose path
|
|
1016
|
+
* past the timelines for both.
|
|
1017
|
+
*/
|
|
1018
|
+
function steppedLocals(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, records: () => CoreConstraintRecord[], physicsKeyed: NonNullable<CoreConstraintTimelines['physicsKeyed']>, t: number, sliders: SliderApplication[] | undefined, step: PoseStep): PosedStep {
|
|
1019
|
+
const active = activeBones(doc);
|
|
1020
|
+
// The caller's adjustment, after the timelines and before anything reads the locals (`LocalAdjust`).
|
|
1021
|
+
if (step.adjust !== undefined) adjustLocals(doc, bones, step.adjust);
|
|
1022
|
+
let world = (plant.evaluate ?? worldTransforms)(bones, active);
|
|
1023
|
+
const stepped = stepPhysicsRecords(records(), physicsKeyed, t, active, step.ctx, step.before);
|
|
1024
|
+
world = applyConstraints(bones, world, active, plant.constraints ? plant.constraints(stepped) : stepped, null, sliders, step.ctx, undefined, solverRules(plant.solver));
|
|
1025
|
+
for (const b of bones) if (world.get(b.name) === undefined) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
|
|
1026
|
+
return { bones, world, active, round: plant.round ?? gridRound };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* `steppedLocals`' world transforms, the oracle's rows formed beside them when
|
|
1031
|
+
* `rows` is set (as `posedBoneWorld` forms them and `posedBoneWorldAlone` does
|
|
1032
|
+
* not): the live tracks' entry to the stepped phase (`./tracks.ts`). The
|
|
1033
|
+
* document's constraint records as they stand — the live tracks mix no
|
|
1034
|
+
* constraint timeline — and no physics timeline keyed.
|
|
1035
|
+
*/
|
|
1036
|
+
export function steppedLocalsWorld(doc: CompiledDocument, bones: ModelBone[], plant: TimelinePlant, sliders: SliderApplication[], step: PoseStep, rows: boolean): Map<string, CoreWorld> {
|
|
1037
|
+
const posed = steppedLocals(doc, bones, plant, () => constraintRecords(doc), [], 0, sliders, step);
|
|
1038
|
+
if (rows) stepRows(posed);
|
|
1039
|
+
return posed.world;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
858
1042
|
/** 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. */
|
|
859
1043
|
function pathDeformed(doc: CompiledDocument, records: CoreConstraintRecord[], timelines: CoreAnimationTimelines, t: number, plant: TimelinePlant): CoreConstraintRecord[] {
|
|
860
1044
|
if (timelines.attachments.every((a) => a.deform === null)) return records;
|
package/src/core/deform.ts
CHANGED
|
@@ -536,6 +536,14 @@ export function attachmentStates(
|
|
|
536
536
|
sample: { timelines: CoreAnimationTimelines; t: number } | null,
|
|
537
537
|
sliders: readonly AttachmentApplication[],
|
|
538
538
|
plant: { deform?: DeformEvaluator; deformBlend?: DeformBlender; sequence?: SequenceEvaluator; slotTimelines?: SlotTimelineGate } = {},
|
|
539
|
+
/**
|
|
540
|
+
* The live tracks' deforms (`./tracks.ts`, issue #1336), in place of a
|
|
541
|
+
* sample's: each slot's deform array as the mixed entries left it (`null`
|
|
542
|
+
* none) and the records their deform timelines keyed on it — what the
|
|
543
|
+
* sample's own timelines would have set and seen. Absent on every other
|
|
544
|
+
* caller.
|
|
545
|
+
*/
|
|
546
|
+
mixed?: ReadonlyMap<string, { deform: number[] | null; seen: readonly string[] }>,
|
|
539
547
|
): { shown: ShownGeometry[]; why: string[] } {
|
|
540
548
|
const evalDeform = plant.deform ?? deformAt;
|
|
541
549
|
const blendOf = plant.deformBlend ?? blendDeform;
|
|
@@ -548,9 +556,10 @@ export function attachmentStates(
|
|
|
548
556
|
// A slot on a bone the skin leaves inactive is not animated: no deform, no frame, no slider switch (`./skins.ts`).
|
|
549
557
|
const live = gate(doc, slot, active);
|
|
550
558
|
let placeholder = placeholders.get(slot.name) ?? null;
|
|
551
|
-
|
|
559
|
+
const base = live ? mixed?.get(slot.name) : undefined;
|
|
560
|
+
let deform: number[] | null = base?.deform ?? null;
|
|
552
561
|
let frame: number | null = null;
|
|
553
|
-
const seen: string[] = [];
|
|
562
|
+
const seen: string[] = base === undefined ? [] : [...base.seen];
|
|
554
563
|
// What the slot shows now, the record its timelines are keyed as, and what a deform or a frame is computed on.
|
|
555
564
|
const state = (): { s: CoreShown; identity: string | null; vertices: ModelVertices | null; count: number | undefined } | null => {
|
|
556
565
|
if (placeholder === null) return null;
|
package/src/core/entry.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* browser and holds the bundle to zero `node:` strings.
|
|
17
17
|
*
|
|
18
18
|
* ⚠️ A symbol is listed here only because a dependant was seen using it,
|
|
19
|
-
* or asked for it in writing before it could be (`openTrack`, issue #1276
|
|
19
|
+
* or asked for it in writing before it could be (`openTrack`, issue #1276;
|
|
20
|
+
* `openTracks`, issue #1336).
|
|
20
21
|
* The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
|
|
21
22
|
* `./core`, and a symbol added here and not there is not promised.
|
|
22
23
|
*/
|
|
@@ -26,7 +27,9 @@ export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setup
|
|
|
26
27
|
export type { RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
|
|
27
28
|
export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
|
|
28
29
|
export { openTrack } from './track.ts';
|
|
29
|
-
export type { LiveTrack, TrackOptions } from './track.ts';
|
|
30
|
+
export type { LiveTrack, TrackOptions, AdjustableLocals, BoneLocals, LocalAdjust } from './track.ts';
|
|
31
|
+
export { openTracks } from './tracks.ts';
|
|
32
|
+
export type { LiveTracks, TracksOptions, MixOptions, QueueOptions, TrackEntryView } from './tracks.ts';
|
|
30
33
|
export type { WalkPose } from './walk.ts';
|
|
31
34
|
export { documentPageLookup, drawnRegions, regionPageUvs, meshPageUvs, readUvSequences } from './uvs.ts';
|
|
32
35
|
export type { DrawnRegion, UvPage, UvRegion, UvSource } from './uvs.ts';
|
package/src/core/raw.ts
CHANGED
|
@@ -114,12 +114,12 @@
|
|
|
114
114
|
* missing would draw a different picture in silence.
|
|
115
115
|
*/
|
|
116
116
|
import { activeBones, CoreInputError, drawWalkOf, poseSetup, rawNumber, readColour, shownAttachment, sourceOfDoc, underNoSkin, type CompiledDocument, type CorePlant, type CoreSlotRow } from './index.ts';
|
|
117
|
-
import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type TimelinePlant } from './animation.ts';
|
|
117
|
+
import { posedBoneWorld, posedBoneWorldAlone, posedSlots, type CoreAnimationTimelines, type LocalAdjust, type TimelinePlant } from './animation.ts';
|
|
118
118
|
import { constraintsAbsentWhy, pathAnimationsWhy } from './constraints.ts';
|
|
119
119
|
import { freshStepContext, steppedPreviousPassWhy } from './constraints_physics.ts';
|
|
120
120
|
import { attachmentStates } from './deform.ts';
|
|
121
|
-
import { drawOrderAt } from './draw_order.ts';
|
|
122
|
-
import { eventsFired } from './events.ts';
|
|
121
|
+
import { drawOrderAt, type CoreDrawOrderKey } from './draw_order.ts';
|
|
122
|
+
import { eventsFired, type CoreEventRow } from './events.ts';
|
|
123
123
|
import { REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type ShapeClipper } from './clipping.ts';
|
|
124
124
|
import { poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
|
|
125
125
|
import { RUNTIME_DEG, type CoreWorld } from './world.ts';
|
|
@@ -612,6 +612,8 @@ export interface TrackState<P> {
|
|
|
612
612
|
readonly resolve: NonNullable<TimelinePlant['shown']>;
|
|
613
613
|
readonly shape: PoseShape<P>;
|
|
614
614
|
readonly ctx: ReturnType<typeof freshStepContext>;
|
|
615
|
+
/** The caller's adjustment of each step's locals (`LocalAdjust` in `./animation.ts`): the live track's `adjust`, absent on every batch walk. */
|
|
616
|
+
readonly adjust?: LocalAdjust;
|
|
615
617
|
/** The running sum of the steps taken. */
|
|
616
618
|
trackTime: number;
|
|
617
619
|
/** The animation time of the step before, −1 before the first. */
|
|
@@ -631,12 +633,12 @@ function animationOf(doc: CompiledDocument, animation: string): CompiledDocument
|
|
|
631
633
|
* `reset: 'setup'` pose 0 is the setup pose, reset; under `animation` it is the
|
|
632
634
|
* animation applied at 0 and reset there, a step of 0 (`stepWalk`'s `first`).
|
|
633
635
|
*/
|
|
634
|
-
function openWalk<P>(doc: CompiledDocument, anim: CompiledDocument['animations'][number], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P
|
|
636
|
+
function openWalk<P>(doc: CompiledDocument, anim: CompiledDocument['animations'][number], plant: TimelinePlant, reset: RawReset, mode: WalkMode, shape: PoseShape<P>, adjust?: LocalAdjust): { track: TrackState<P>; first: P } {
|
|
635
637
|
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
636
638
|
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
637
639
|
const ctx = freshStepContext(plant.physicsStep);
|
|
638
640
|
if (mode.loop && mode.wrapResets !== false) ctx.loop = true;
|
|
639
|
-
const track: TrackState<P> = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, trackTime: 0, last: -1 };
|
|
641
|
+
const track: TrackState<P> = { doc, anim, duration: anim.timelines.duration, mode, plant, raw: { ...plant, ...roundPlant(mode) }, resolve: plant.shown ?? shownAttachment, shape, ctx, adjust, trackTime: 0, last: -1 };
|
|
640
642
|
if (reset === 'setup') {
|
|
641
643
|
// The reset taken at the setup pose, before the animation is applied (`src/deformmeasure.ts`'s `poseAt`): pose 0 is the setup pose.
|
|
642
644
|
const setup = poseSetup(doc, { ...plant, ...roundPlant(mode) }, ctx);
|
|
@@ -657,35 +659,112 @@ function openWalk<P>(doc: CompiledDocument, anim: CompiledDocument['animations']
|
|
|
657
659
|
* resets the physics there — and nothing else passes it.
|
|
658
660
|
*/
|
|
659
661
|
function stepWalk<P>(track: TrackState<P>, dt: number, first = false): P {
|
|
660
|
-
const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx } = track;
|
|
662
|
+
const { doc, anim, duration, mode, plant, raw, resolve, shape, ctx, adjust } = track;
|
|
661
663
|
track.trackTime += dt;
|
|
662
664
|
const trackTime = track.trackTime;
|
|
663
665
|
const last = track.last;
|
|
664
666
|
const t = mode.loop ? (mode.time ?? loopedTime)(trackTime, duration, trackTime - dt) : trackTime < duration ? trackTime : duration;
|
|
665
667
|
const before = ctx.time;
|
|
666
668
|
ctx.time += mode.clock === undefined ? dt : mode.clock(dt, t - (first ? 0 : Math.max(last, 0)));
|
|
669
|
+
// A10's looping walk fires no event here — its one reader reads none — nor cuts its drawn attachments through a clip (`NO_CLIPPED_ROWS`), so its
|
|
670
|
+
// `clipped` rows are empty. The live track's looping mode (`live`) fires its events across the wrap by the rule `./track.ts` states as measured.
|
|
671
|
+
const fire = plant.events ?? eventsFired;
|
|
672
|
+
const keys = anim.timelines.events;
|
|
673
|
+
const pose = poseStepFrom(doc, plant, resolve, shape, mode, ctx, first, trackTime, t, {
|
|
674
|
+
world: (sliders) => (shape.rows ? posedBoneWorld(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust }).world : posedBoneWorldAlone(doc, anim.timelines, t, raw, anim.constraints, sliders, { ctx, before, adjust })),
|
|
675
|
+
slots: (sliders, placeholders) => posedSlots(doc, anim.timelines, t, raw, sliders, placeholders),
|
|
676
|
+
drawOrder: anim.timelines.drawOrder,
|
|
677
|
+
at: t,
|
|
678
|
+
sample: { timelines: anim.timelines, t },
|
|
679
|
+
events: () => (mode.loop && mode.live !== true ? [] : mode.loop ? fire(keys, last, t, rawNumber, t < last) : fire(keys, last, t, rawNumber)),
|
|
680
|
+
});
|
|
681
|
+
track.last = t;
|
|
682
|
+
return pose;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* What decides one step's pose before it is assembled (issue #1336): the bone
|
|
687
|
+
* world through the stepped phase, filling the sliders it applied; the slot
|
|
688
|
+
* rows, filling the placeholders the timelines left; the draw-order keys and
|
|
689
|
+
* the time they are read at; the deform and sequence timelines' sample, or the
|
|
690
|
+
* mixed deforms in its place; the events, computed when called. A walk's step
|
|
691
|
+
* reads every one off one animation at its time (`stepWalk`); the live tracks
|
|
692
|
+
* off their mixed entries (`./tracks.ts`). Everything past these is one body,
|
|
693
|
+
* `poseStepFrom`.
|
|
694
|
+
*/
|
|
695
|
+
export interface StepPosers {
|
|
696
|
+
world: (sliders: SliderApplication[]) => Map<string, CoreWorld>;
|
|
697
|
+
slots: (sliders: readonly SliderApplication[], placeholders: Map<string, string | null>) => { rows: CoreSlotRow[]; conflicts: string[] };
|
|
698
|
+
drawOrder: readonly CoreDrawOrderKey[];
|
|
699
|
+
at: number;
|
|
700
|
+
sample: { timelines: CoreAnimationTimelines; t: number } | null;
|
|
701
|
+
mixed?: ReadonlyMap<string, { deform: number[] | null; seen: readonly string[] }>;
|
|
702
|
+
events: () => CoreEventRow[];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* One step's pose from what decided it (`StepPosers`): the bone world, the
|
|
707
|
+
* physics phase moved past the reset on `first`, the slot rows, the draw
|
|
708
|
+
* order (the keys' order, else the setup's, then each slider's key), the
|
|
709
|
+
* attachments drawn in it, and the pose assembled in `shape` — refused by name
|
|
710
|
+
* where the core leaves a block out, in that order. The one body a walk's step
|
|
711
|
+
* and a live tracks' step share.
|
|
712
|
+
*/
|
|
713
|
+
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 {
|
|
667
714
|
const sliders: SliderApplication[] = [];
|
|
668
|
-
const world =
|
|
715
|
+
const world = posers.world(sliders);
|
|
669
716
|
if (first) ctx.phase = 'update';
|
|
670
717
|
const placeholders = new Map<string, string | null>();
|
|
671
|
-
const slots =
|
|
718
|
+
const slots = posers.slots(sliders, placeholders);
|
|
672
719
|
if (slots.conflicts.length > 0) throw new CoreInputError(`the raw walk leaves the slots out: ${slots.conflicts.join('; ')}`);
|
|
673
720
|
const evalOrder = plant.drawOrder ?? drawOrderAt;
|
|
674
|
-
let order = evalOrder(doc.slots.length,
|
|
721
|
+
let order = evalOrder(doc.slots.length, posers.drawOrder, posers.at) ?? doc.slots.map((_s, k) => k);
|
|
675
722
|
for (const app of sliders) order = evalOrder(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
|
|
676
723
|
const drawOrder = order.map((k) => doc.slots[k].name);
|
|
677
|
-
const states = attachmentStates(doc, resolve, placeholders,
|
|
724
|
+
const states = attachmentStates(doc, resolve, placeholders, posers.sample, sliders, plant, posers.mixed);
|
|
678
725
|
if (states.why.length > 0) throw new CoreInputError(`the raw walk leaves the attachments out: ${states.why.join('; ')}`);
|
|
679
726
|
const rank = new Map(drawOrder.map((n, r) => [n, r]));
|
|
680
727
|
const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
728
|
+
const events = (): RawEvent[] => posers.events().map((e): RawEvent => [e[0], finite(e[1], 'event time'), e[2], finite(e[3], 'event float'), e[4]]);
|
|
729
|
+
return shape.step(doc, { trackTime, animationTime, world, slots: slots.rows, drawOrder, shown, events }, plant, mode);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The live tracks' side of the raw pose path (issue #1336, `./tracks.ts`):
|
|
734
|
+
* the refusals a walk makes before its first pose, a fresh physics context,
|
|
735
|
+
* the plant with the raw entry's rounding, and the two ways a pose is made —
|
|
736
|
+
* the setup pose reset (`reset: 'setup'`'s pose 0) and one step from what the
|
|
737
|
+
* mixed entries decided, assembled as the raw entry's pose (`RAW_SHAPE`,
|
|
738
|
+
* `RAW_MODE`: a non-finite value refused, every clipped row cut).
|
|
739
|
+
*/
|
|
740
|
+
export interface RawTracksPath {
|
|
741
|
+
readonly ctx: ReturnType<typeof freshStepContext>;
|
|
742
|
+
/** `plant` with the raw entry's rounding. */
|
|
743
|
+
readonly raw: TimelinePlant;
|
|
744
|
+
setupPose(): RawPose;
|
|
745
|
+
step(first: boolean, trackTime: number, animationTime: number, posers: StepPosers): RawPose;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** `RawTracksPath` opened on `doc`, refused by name where a walk on it would be (`openWalk`). */
|
|
749
|
+
export function openRawTracksPath(doc: CompiledDocument, plant: TimelinePlant): RawTracksPath {
|
|
750
|
+
const why = constraintsAbsentWhy(doc) ?? pathAnimationsWhy(doc) ?? steppedPreviousPassWhy(doc);
|
|
751
|
+
if (why !== null) throw new CoreInputError(`the raw walk leaves the bones out: ${why}`);
|
|
752
|
+
const ctx = freshStepContext(plant.physicsStep);
|
|
753
|
+
const raw: TimelinePlant = { ...plant, ...roundPlant(RAW_MODE) };
|
|
754
|
+
const resolve = plant.shown ?? shownAttachment;
|
|
755
|
+
return {
|
|
756
|
+
ctx,
|
|
757
|
+
raw,
|
|
758
|
+
setupPose: () => {
|
|
759
|
+
const setup = poseSetup(doc, raw, ctx);
|
|
760
|
+
const absent = setup.absent.filter(([block]) => block !== 'setup.clipped');
|
|
761
|
+
if (absent.length > 0) throw new CoreInputError(`the raw walk's reset pose leaves ${absent.map(([b, w]) => `${b} out (${w})`).join('; ')}`);
|
|
762
|
+
if (setup.world === null || setup.shown === null || setup.setup.slots === null || setup.setup.drawOrder === null) throw new CoreInputError('the raw walk\'s reset pose was not posed');
|
|
763
|
+
ctx.phase = 'update';
|
|
764
|
+
return RAW_SHAPE.setup(doc, setup.world, setup.setup.slots, setup.setup.drawOrder, setup.shown, plant, RAW_MODE);
|
|
765
|
+
},
|
|
766
|
+
step: (first, trackTime, animationTime, posers) => poseStepFrom(doc, plant, resolve, RAW_SHAPE, RAW_MODE, ctx, first, trackTime, animationTime, posers),
|
|
767
|
+
};
|
|
689
768
|
}
|
|
690
769
|
|
|
691
770
|
/** `walkIn`'s walk, each pose assembled in `shape` and handed to `visit` in order as it is posed (`poseRawAnimationEach`): the walk opened, then stepped over `steps`. */
|
|
@@ -706,8 +785,8 @@ function walkEach<P>(doc: CompiledDocument, animation: string, steps: readonly n
|
|
|
706
785
|
* `stepRawTrack` steps. Every pose is assembled as the raw entry's
|
|
707
786
|
* (`RAW_SHAPE`).
|
|
708
787
|
*/
|
|
709
|
-
export function openRawTrack(doc: CompiledDocument, animation: string, plant: TimelinePlant, reset: RawReset, mode: WalkMode): { track: TrackState<RawPose>; first: RawPose } {
|
|
710
|
-
return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE);
|
|
788
|
+
export function openRawTrack(doc: CompiledDocument, animation: string, plant: TimelinePlant, reset: RawReset, mode: WalkMode, adjust?: LocalAdjust): { track: TrackState<RawPose>; first: RawPose } {
|
|
789
|
+
return openWalk(doc, animationOf(doc, animation), plant, reset, mode, RAW_SHAPE, adjust);
|
|
711
790
|
}
|
|
712
791
|
|
|
713
792
|
/** One step of an opened raw track (`openRawTrack`): `walkIn`'s step, the step itself checked by the caller. */
|
package/src/core/track.ts
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* scan on A10's schedule and at 60 fps over three cycles — 157,771 poses, every
|
|
18
18
|
* one the same bits (the PR of issue #1276 carries the table).
|
|
19
19
|
*
|
|
20
|
+
* Several animations on several tracks, set, queued and mixed as spine-core's
|
|
21
|
+
* `AnimationState` mixes them, are `openTracks` (`./tracks.ts`, issue #1336):
|
|
22
|
+
* a second state machine over the same pose path past the timelines, which
|
|
23
|
+
* this track reaches with the locals its one animation posed.
|
|
24
|
+
*
|
|
20
25
|
* ## The walk
|
|
21
26
|
*
|
|
22
27
|
* - **Held** (`loop: false`): the raw entry's walk — the track time the
|
|
@@ -95,6 +100,41 @@
|
|
|
95
100
|
* and the events (4,009 of 4,009 on the tree row, 690 of them cutting 12,420
|
|
96
101
|
* rows).
|
|
97
102
|
*
|
|
103
|
+
* ## The adjustment (issue #1336)
|
|
104
|
+
*
|
|
105
|
+
* `adjust` is a player's own bone-local value — a breath's scale, a look's
|
|
106
|
+
* rotation — written onto the bones every pose, after the animation's
|
|
107
|
+
* timelines posed the locals at the step's time and before the world
|
|
108
|
+
* transforms, the constraints and the physics step (`LocalAdjust` in
|
|
109
|
+
* `./animation.ts`, which states the view's rules). It runs on every pose the
|
|
110
|
+
* animation is applied to — every `step`, and pose 0 under `reset:
|
|
111
|
+
* 'animation'` — and not on pose 0 under `reset: 'setup'`, the setup pose
|
|
112
|
+
* with nothing applied: a player applies its adjustment wherever it applies
|
|
113
|
+
* the animation state, the first frame included, and spine-core's reset at
|
|
114
|
+
* the setup pose applies none. Absent, every pose is the one the track posed
|
|
115
|
+
* before it existed, bit for bit (`CO46`, unchanged; `CO50` holds a hook that
|
|
116
|
+
* writes nothing to the same bits).
|
|
117
|
+
*
|
|
118
|
+
* `CO50` holds it to spine-core 4.3.13 on gallery/look, whose `head` carries
|
|
119
|
+
* the deformed head mesh and every face bone, and the physics-driven
|
|
120
|
+
* `ahoge_whip` two bones below it: the same `scaleY` written on `head` by the
|
|
121
|
+
* core's hook and on `Bone.pose` between `AnimationState.apply` and
|
|
122
|
+
* `updateWorldTransform(Physics.update)`, every bone's world transform and
|
|
123
|
+
* every drawn vertex equal at tolerance 0 over looping and held walks under
|
|
124
|
+
* both resets — spine-core's skeleton brought back to the setup pose
|
|
125
|
+
* (`setupPoseBones`) before each `apply`, which is the core's own reading:
|
|
126
|
+
* every pose is posed from the setup pose. Measured, and held red there:
|
|
127
|
+
*
|
|
128
|
+
* - **The write carried from frame to frame** — spine-core without that
|
|
129
|
+
* reset, where no timeline writes an unkeyed channel back, so a `scaleY`
|
|
130
|
+
* multiplied every frame compounds (every walk off from pose 2). The core
|
|
131
|
+
* carries no local from one step to the next; a value a player wants
|
|
132
|
+
* carried it keeps and writes again.
|
|
133
|
+
* - **The write at draw time** — made after `updateWorldTransform`, the
|
|
134
|
+
* bone's own matrix alone brought up to date: the bone moves, its
|
|
135
|
+
* children, the meshes they carry and the physics do not.
|
|
136
|
+
* - **Pose 0 not adjusted** under `reset: 'animation'`.
|
|
137
|
+
*
|
|
98
138
|
* ## The oracle chain
|
|
99
139
|
*
|
|
100
140
|
* Nothing here is held to spine-core directly but the events: the live track
|
|
@@ -106,19 +146,22 @@
|
|
|
106
146
|
*
|
|
107
147
|
* ⛔ **A track refused part-way stays refused.** A step is checked before
|
|
108
148
|
* anything moves; a refusal past that — a construct the core leaves out, a
|
|
109
|
-
* non-finite vertex — comes after the track time and the physics
|
|
110
|
-
* so every later `step` refuses naming the step it was refused
|
|
111
|
-
* caller opens the track again.
|
|
149
|
+
* non-finite vertex or write — comes after the track time and the physics
|
|
150
|
+
* have moved, so every later `step` refuses naming the step it was refused
|
|
151
|
+
* at, and the caller opens the track again. So does an error the caller's own
|
|
152
|
+
* `adjust` throws: it is thrown mid-step, after the same moves. Seeking is the caller's too: a physics rig has
|
|
112
153
|
* history, so a seek is "open, then step to the time".
|
|
113
154
|
*/
|
|
114
155
|
import { CoreInputError, type CompiledDocument } from './index.ts';
|
|
115
156
|
import { openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
116
|
-
import type { TimelinePlant } from './animation.ts';
|
|
157
|
+
import type { LocalAdjust, TimelinePlant } from './animation.ts';
|
|
158
|
+
export type { AdjustableLocals, BoneLocals, LocalAdjust } from './animation.ts';
|
|
117
159
|
|
|
118
|
-
/** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`)
|
|
160
|
+
/** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`) — both the caller's, neither with a default — and the caller's adjustment of every pose's bone locals (the header's *The adjustment*). */
|
|
119
161
|
export interface TrackOptions {
|
|
120
162
|
loop: boolean;
|
|
121
163
|
reset: RawReset;
|
|
164
|
+
adjust?: LocalAdjust;
|
|
122
165
|
}
|
|
123
166
|
|
|
124
167
|
/** A live track (`openTrack`): pose 0, then one pose per `step`. */
|
|
@@ -147,7 +190,8 @@ const LOOPED: WalkMode = { loop: true, keep: false, live: true };
|
|
|
147
190
|
export function openTrack(doc: CompiledDocument, animation: string, options: TrackOptions, plant: TimelinePlant = {}): LiveTrack {
|
|
148
191
|
if (typeof options !== 'object' || options === null || typeof options.loop !== 'boolean') throw new CoreInputError(`the live track's options.loop is ${JSON.stringify((options as Partial<TrackOptions> | null)?.loop)}, not true or false`);
|
|
149
192
|
if (options.reset !== 'animation' && options.reset !== 'setup') throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
|
|
150
|
-
|
|
193
|
+
if (options.adjust !== undefined && typeof options.adjust !== 'function') throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
|
|
194
|
+
const { track, first } = openRawTrack(doc, animation, plant, options.reset, options.loop ? LOOPED : HELD, options.adjust);
|
|
151
195
|
return new Track(track, first);
|
|
152
196
|
}
|
|
153
197
|
|
|
@@ -186,7 +230,7 @@ class Track implements LiveTrack {
|
|
|
186
230
|
this.latest = pose.animationTime;
|
|
187
231
|
return pose;
|
|
188
232
|
} catch (err) {
|
|
189
|
-
|
|
233
|
+
this.refused = `${n} (${err instanceof Error ? err.message : String(err)})`;
|
|
190
234
|
throw err;
|
|
191
235
|
}
|
|
192
236
|
}
|