@hatiolab/figure-model 0.2.13 → 0.2.15
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/dist/asset-types.d.ts +28 -4
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/capabilities.js +84 -45
- package/dist/capabilities.js.map +1 -1
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +3 -4
- package/dist/cost.js.map +1 -1
- package/dist/graph-types.d.ts +55 -4
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/graph.d.ts +2 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +360 -35
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/render-batches.d.ts +27 -2
- package/dist/render-batches.d.ts.map +1 -1
- package/dist/render-batches.js +99 -3
- package/dist/render-batches.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +7 -19
- package/dist/score.js.map +1 -1
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -643,6 +643,22 @@ for (const [name, op] of Object.entries(ops))
|
|
|
643
643
|
const IMPURE_OPERATORS = [];
|
|
644
644
|
for (const [name, op] of Object.entries(ops))
|
|
645
645
|
op.pure = !IMPURE_OPERATORS.includes(name);
|
|
646
|
+
/*
|
|
647
|
+
The operators a pose step may run (ADR-0108 decision 3): arithmetic, transforms, and what carries a pose -- placing,
|
|
648
|
+
assembling, repeating, selecting. Not a shape, a layout, a pitch or a feature of a shape: an input that reaches one
|
|
649
|
+
changes what is drawn, not only where. An operator added without a form here is left out of the fast path, and the
|
|
650
|
+
test that holds \`FIGURE_POSE_EXCLUDED_OPERATORS\` still says so.
|
|
651
|
+
*/
|
|
652
|
+
const POSE_FORMS = [
|
|
653
|
+
'add@1', 'mul@1', 'div@1', 'sin@1', 'geomean@1', 'curve@1', 'at-least@1', 'max@1', 'min@1', 'affine-bounds@1', 'between@1',
|
|
654
|
+
'assembly@1', 'repeat@1', 'select-item@1', 'rigid@1', 'axis-turn@1', 'axis-slide@1', 'compose@1', 'attach@1',
|
|
655
|
+
'point@1', 'map-point@1', 'distance@1', 'place@1', 'member@1'
|
|
656
|
+
];
|
|
657
|
+
for (const [name, op] of Object.entries(ops))
|
|
658
|
+
if (op.pure && POSE_FORMS.includes(name))
|
|
659
|
+
op.poseForm = 'run';
|
|
660
|
+
/** The operators a pose step does not run: an input whose reach holds one goes by an ordinary evaluation. */
|
|
661
|
+
export const FIGURE_POSE_EXCLUDED_OPERATORS = Object.freeze(Object.keys(ops).filter(k => !ops[k].poseForm));
|
|
646
662
|
/** Every operator's state policy, by name, for the guard test and for readers of the contract. */
|
|
647
663
|
export const FIGURE_OPERATOR_STATE_POLICY = Object.freeze(Object.fromEntries(Object.entries(ops).map(([k, op]) => [k, op.state])));
|
|
648
664
|
/** Every operator's purity, by name, for the guard test and for readers of the evaluator. */
|
|
@@ -675,9 +691,64 @@ function checkedValue(value, type, path) {
|
|
|
675
691
|
fail('COUNT', path, 'nonnegative safe integer required');
|
|
676
692
|
return value;
|
|
677
693
|
}
|
|
678
|
-
|
|
694
|
+
/*
|
|
695
|
+
By the value's type, where it says where the numbers are: a transform's are its r and t; a placed thing's are its
|
|
696
|
+
pose, its shape being the very object its shape node produced and this check already passed. The same numbers in
|
|
697
|
+
the same order as the walk below, without its bookkeeping for the two kinds a moving figure makes most of.
|
|
698
|
+
*/
|
|
699
|
+
if (type.kind === 'transform' && isPose(value))
|
|
700
|
+
finitePose(value, path);
|
|
701
|
+
else if (type.kind === 'placed' && keysAre(value, 'shape', 'pose') && finiteObjects.has(value.shape) && isPose(value.pose))
|
|
702
|
+
finitePose(value.pose, path);
|
|
703
|
+
else {
|
|
704
|
+
finite(value, path);
|
|
705
|
+
/* A shape is what copies and placed things carry again and again: it is remembered, nothing else is. */
|
|
706
|
+
if (type.kind === 'shape' && value && typeof value === 'object')
|
|
707
|
+
finiteObjects.add(value);
|
|
708
|
+
}
|
|
679
709
|
return value;
|
|
680
710
|
}
|
|
711
|
+
/** Whether a value's own keys are exactly these two, in this order -- without making the list of them. */
|
|
712
|
+
const keysAre = (v, a, b) => {
|
|
713
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
714
|
+
return false;
|
|
715
|
+
let n = 0;
|
|
716
|
+
for (const k in v) {
|
|
717
|
+
if (!Object.hasOwn(v, k))
|
|
718
|
+
continue;
|
|
719
|
+
if (n > 1 || k !== (n === 0 ? a : b))
|
|
720
|
+
return false;
|
|
721
|
+
n++;
|
|
722
|
+
}
|
|
723
|
+
return n === 2;
|
|
724
|
+
};
|
|
725
|
+
const isPose = (v) => keysAre(v, 'r', 't') && Array.isArray(v.r) && Array.isArray(v.t);
|
|
726
|
+
/** A list's numbers in order, the first one not finite refused; anything else in it walked as \`finite\` walks it. */
|
|
727
|
+
function finiteList(list, path) {
|
|
728
|
+
for (let i = 0; i < list.length; i++) {
|
|
729
|
+
if (!(i in list))
|
|
730
|
+
continue;
|
|
731
|
+
const x = list[i];
|
|
732
|
+
if (typeof x === 'number') {
|
|
733
|
+
if (!Number.isFinite(x))
|
|
734
|
+
fail('NON_FINITE', path, String(x));
|
|
735
|
+
}
|
|
736
|
+
else
|
|
737
|
+
finite(x, path);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function finitePose(p, path) {
|
|
741
|
+
for (let i = 0; i < p.r.length; i++) {
|
|
742
|
+
if (!(i in p.r))
|
|
743
|
+
continue;
|
|
744
|
+
const row = p.r[i];
|
|
745
|
+
if (Array.isArray(row))
|
|
746
|
+
finiteList(row, path);
|
|
747
|
+
else
|
|
748
|
+
finite(row, path);
|
|
749
|
+
}
|
|
750
|
+
finiteList(p.t, path);
|
|
751
|
+
}
|
|
681
752
|
/*
|
|
682
753
|
Every number in a value is finite: the first one that is not, depth first in key order, is refused. A plain loop:
|
|
683
754
|
a repeat's output holds every copy's pose, and the walk with a closure and forEach per level was most of what
|
|
@@ -695,10 +766,25 @@ function finite(v, path) {
|
|
|
695
766
|
}
|
|
696
767
|
else if (v && typeof v === 'object') {
|
|
697
768
|
const keys = Object.keys(v);
|
|
769
|
+
/* A pose -- two lists of numbers -- is walked: remembering it costs more than walking it. */
|
|
770
|
+
const small = keys.length <= 2 && Array.isArray(v[keys[0]]) && (keys.length < 2 || Array.isArray(v[keys[1]]));
|
|
771
|
+
if (!small && finiteObjects.has(v))
|
|
772
|
+
return;
|
|
698
773
|
for (let i = 0; i < keys.length; i++)
|
|
699
774
|
finite(v[keys[i]], path);
|
|
700
775
|
}
|
|
701
776
|
}
|
|
777
|
+
/*
|
|
778
|
+
Shapes already found finite. A computed value is never written after it is made (the evaluator hands unchanged
|
|
779
|
+
values on as the same objects), so one found finite stays so: a row's twenty-four copies carry the one shape of the
|
|
780
|
+
member they copy, and that shape -- most of the numbers -- was walked again for every copy at every turn (51% of a
|
|
781
|
+
turning roller row, 2026-10-03). Only shapes are remembered: remembering every object kept a table the size of the
|
|
782
|
+
busiest moment (4 MB after 500 conveyors were made and dropped).
|
|
783
|
+
*/
|
|
784
|
+
const finiteObjects = new WeakSet();
|
|
785
|
+
/** What a pose step may change: numbers, transforms, and what carries a pose. A shape, a layout, a count or a flag may not. */
|
|
786
|
+
const POSE_KINDS = ['transform', 'placed', 'assembly', 'collection', 'point'];
|
|
787
|
+
const notPose = (t) => (Array.isArray(t) || POSE_KINDS.includes(t?.kind) ? undefined : String(t?.kind ?? 'value'));
|
|
702
788
|
export function compileFigureGraph(source) {
|
|
703
789
|
return compileInternal(source);
|
|
704
790
|
}
|
|
@@ -794,6 +880,29 @@ function compileInternal(source) {
|
|
|
794
880
|
visit(name);
|
|
795
881
|
/** An instance evaluator's walk, made on first use (`walkOf`). */
|
|
796
882
|
let walk;
|
|
883
|
+
/** The pose analyses made so far, by what reads the graph (`poseAnalysisOf`). */
|
|
884
|
+
const analyses = new Map();
|
|
885
|
+
/*
|
|
886
|
+
The one check of an input record (ADR-0108 C1): a whole evaluation, an instance's next evaluation and a pose step
|
|
887
|
+
all take their inputs through these two, so a value refused one way is refused every way.
|
|
888
|
+
*/
|
|
889
|
+
const inputIds = new Set(m.inputs.map(p => p.id));
|
|
890
|
+
function inputFields(input) {
|
|
891
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
892
|
+
fail('SCHEMA', 'input values', 'object required');
|
|
893
|
+
for (const p of m.inputs)
|
|
894
|
+
if (!Object.hasOwn(input, p.id))
|
|
895
|
+
fail('SCHEMA', 'input values', `missing ${p.id}`);
|
|
896
|
+
for (const k of Object.keys(input))
|
|
897
|
+
if (!inputIds.has(k))
|
|
898
|
+
fail('SCHEMA', 'input values', `unknown ${k}`);
|
|
899
|
+
}
|
|
900
|
+
function inputValue(p, given) {
|
|
901
|
+
const raw = num(given, p.id);
|
|
902
|
+
if (raw < p.min || raw > p.max)
|
|
903
|
+
fail('INPUT_RANGE', p.id, `${raw} outside [${p.min},${p.max}]`);
|
|
904
|
+
return checkedValue(raw * unit(p.unit, p.id)[1], types.get(p.id), p.id);
|
|
905
|
+
}
|
|
797
906
|
/*
|
|
798
907
|
One evaluation. Given the last one, only what the changed inputs reach runs again (user's decision 2026-09-26: a
|
|
799
908
|
figure whose controls move re-evaluated every body every frame -- 2.2 us a body, 26.7 ms for sixty turning
|
|
@@ -802,14 +911,11 @@ function compileInternal(source) {
|
|
|
802
911
|
tests compare the two over random sequences of inputs.
|
|
803
912
|
*/
|
|
804
913
|
function run(input, previous) {
|
|
805
|
-
|
|
914
|
+
inputFields(input);
|
|
806
915
|
const values = new Map(), geometry = [], bodies = new Map(), changed = new Set();
|
|
807
916
|
for (const p of m.inputs) {
|
|
808
|
-
|
|
809
|
-
if (
|
|
810
|
-
fail('INPUT_RANGE', p.id, `${raw} outside [${p.min},${p.max}]`);
|
|
811
|
-
values.set(p.id, checkedValue(raw * unit(p.unit, p.id)[1], types.get(p.id), p.id));
|
|
812
|
-
if (!previous || previous.input[p.id] !== raw)
|
|
917
|
+
values.set(p.id, inputValue(p, input[p.id]));
|
|
918
|
+
if (!previous || previous.input[p.id] !== input[p.id])
|
|
813
919
|
changed.add(p.id);
|
|
814
920
|
}
|
|
815
921
|
for (const p of m.constants)
|
|
@@ -853,20 +959,61 @@ function compileInternal(source) {
|
|
|
853
959
|
* (chief architect's ruling 2026-09-26: the compiled graph is shared, the last values belong to the instance -- sixty
|
|
854
960
|
* conveyors of one type sharing one cache would break it at every call). A throw drops what it kept.
|
|
855
961
|
*/
|
|
856
|
-
evaluator() {
|
|
962
|
+
evaluator(options = {}) {
|
|
857
963
|
let kept;
|
|
964
|
+
/*
|
|
965
|
+
The generation (ADR-0108): while it stands, body slot n is the same body -- the same id at the same place in the
|
|
966
|
+
geometry. It moves on with a whole evaluation (the first, and the one after a refusal) and with an evaluation
|
|
967
|
+
whose bodies differ from the last one's.
|
|
968
|
+
*/
|
|
969
|
+
let generation = 0;
|
|
970
|
+
let ids = [];
|
|
971
|
+
/* What reads this graph does not change under an evaluator: its analysis is looked up once. */
|
|
972
|
+
let analysis;
|
|
973
|
+
const evaluated = (result, whole) => {
|
|
974
|
+
const now = result.geometry.map((g) => g.id);
|
|
975
|
+
if (whole || now.length !== ids.length || now.some((id, i) => id !== ids[i]))
|
|
976
|
+
generation++;
|
|
977
|
+
ids = now;
|
|
978
|
+
return result;
|
|
979
|
+
};
|
|
980
|
+
const evaluate = (input) => {
|
|
981
|
+
const previous = kept;
|
|
982
|
+
kept = undefined;
|
|
983
|
+
if (!previous) {
|
|
984
|
+
const whole = run(input, undefined);
|
|
985
|
+
kept = hold(whole.kept, whole.result);
|
|
986
|
+
return evaluated(whole.result, true);
|
|
987
|
+
}
|
|
988
|
+
const result = step(input, previous);
|
|
989
|
+
kept = previous;
|
|
990
|
+
return evaluated(result, false);
|
|
991
|
+
};
|
|
858
992
|
return {
|
|
859
|
-
evaluate
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
993
|
+
evaluate,
|
|
994
|
+
/**
|
|
995
|
+
* One step (ADR-0108): poses alone when every changed input takes the fast path, an evaluation otherwise --
|
|
996
|
+
* with why. The inputs are checked as an evaluation checks them; a refusal drops what was kept, as it does there.
|
|
997
|
+
* \`evaluate\` (\`ordinary\` names why) takes no fast path.
|
|
998
|
+
*/
|
|
999
|
+
advance(input, ordinary) {
|
|
1000
|
+
if (kept && !ordinary) {
|
|
1001
|
+
const previous = kept;
|
|
1002
|
+
kept = undefined;
|
|
1003
|
+
const step = poseStep(input, previous, (analysis ??= poseAnalysisOf(options.reads)));
|
|
1004
|
+
kept = previous;
|
|
1005
|
+
if ('result' in step)
|
|
1006
|
+
return { ...step.result, generation };
|
|
1007
|
+
const result = evaluate(input);
|
|
1008
|
+
return { kind: 'evaluation', result, generation, why: step.why };
|
|
866
1009
|
}
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
return result;
|
|
1010
|
+
const bound = !!kept;
|
|
1011
|
+
const result = evaluate(input);
|
|
1012
|
+
return { kind: 'evaluation', result, generation, why: { code: bound ? ordinary : 'NOT_BOUND' } };
|
|
1013
|
+
},
|
|
1014
|
+
/** Which inputs take the fast path for what reads this graph, and why the others do not. */
|
|
1015
|
+
poseInputs() {
|
|
1016
|
+
return poseAnalysisOf(options.reads).inputs.map(a => ({ ...a }));
|
|
870
1017
|
}
|
|
871
1018
|
};
|
|
872
1019
|
}
|
|
@@ -920,13 +1067,12 @@ function compileInternal(source) {
|
|
|
920
1067
|
const always = [...new Set([...impure.map(node => node.index), ...reachOf(impure.flatMap(node => node.outs))])].sort((a, b) => a - b);
|
|
921
1068
|
walk = {
|
|
922
1069
|
keys,
|
|
1070
|
+
slot,
|
|
923
1071
|
nodes,
|
|
924
|
-
|
|
1072
|
+
readers,
|
|
925
1073
|
reach: m.inputs.map((_, i) => reachOf([i])),
|
|
926
1074
|
always,
|
|
927
|
-
shapes: nodes.filter(node => node.op.geometry).map(node => node.index)
|
|
928
|
-
scale: m.inputs.map(p => unit(p.unit, p.id)[1]),
|
|
929
|
-
inputTypes: m.inputs.map(p => types.get(p.id))
|
|
1075
|
+
shapes: nodes.filter(node => node.op.geometry).map(node => node.index)
|
|
930
1076
|
};
|
|
931
1077
|
return walk;
|
|
932
1078
|
}
|
|
@@ -951,16 +1097,198 @@ function compileInternal(source) {
|
|
|
951
1097
|
of its own -- a new values record and a new geometry list, copied from the evaluator's own -- so a result kept by a
|
|
952
1098
|
caller does not change when the evaluator moves on, and a caller writing into one does not change the next.
|
|
953
1099
|
*/
|
|
1100
|
+
/*
|
|
1101
|
+
Which inputs take the fast path (ADR-0108 decision 2), decided from everything each one reaches: every node it
|
|
1102
|
+
reaches has a pose form, every value it changes is a number or carries a pose, and nothing outside the graph that
|
|
1103
|
+
may not change reads one. Made once per plan and what reads it.
|
|
1104
|
+
*/
|
|
1105
|
+
function poseAnalysisOf(reads = {}) {
|
|
1106
|
+
const key = JSON.stringify([reads.forbidden ?? {}, [...(reads.external ?? [])].sort()]);
|
|
1107
|
+
const held = analyses.get(key);
|
|
1108
|
+
if (held)
|
|
1109
|
+
return held;
|
|
1110
|
+
const w = walkOf();
|
|
1111
|
+
const forbidden = reads.forbidden ?? {};
|
|
1112
|
+
const external = new Set(reads.external ?? []);
|
|
1113
|
+
const inputs = m.inputs.map((p, i) => {
|
|
1114
|
+
const kind = notPose(types.get(p.id));
|
|
1115
|
+
if (kind)
|
|
1116
|
+
return { fast: false, code: 'INPUT_NOT_POSE', input: p.id, node: p.id, reaches: kind };
|
|
1117
|
+
if (Object.hasOwn(forbidden, p.id))
|
|
1118
|
+
return { fast: false, code: 'INPUT_NOT_POSE', input: p.id, node: p.id, reaches: forbidden[p.id] };
|
|
1119
|
+
for (const index of w.reach[i]) {
|
|
1120
|
+
const node = w.nodes[index];
|
|
1121
|
+
if (!node.op.poseForm)
|
|
1122
|
+
return { fast: false, code: 'UNSUPPORTED_OPERATOR', input: p.id, node: node.n.id, op: node.n.op };
|
|
1123
|
+
for (const [k, s] of node.outs.entries()) {
|
|
1124
|
+
const kind = notPose(node.types[k]);
|
|
1125
|
+
if (kind)
|
|
1126
|
+
return { fast: false, code: 'INPUT_NOT_POSE', input: p.id, node: node.n.id, reaches: kind };
|
|
1127
|
+
const ref = w.keys[s];
|
|
1128
|
+
if (Object.hasOwn(forbidden, ref))
|
|
1129
|
+
return { fast: false, code: 'INPUT_NOT_POSE', input: p.id, node: node.n.id, reaches: forbidden[ref] };
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return { fast: true };
|
|
1133
|
+
});
|
|
1134
|
+
/* A row whose copies nothing reads -- no node, nothing outside -- is written by its poses alone. */
|
|
1135
|
+
const terminal = w.nodes.map(node => node.n.op === 'repeat@1' && !node.n.params?.within && node.outs.every(s => !w.readers[s].length && !external.has(w.keys[s])));
|
|
1136
|
+
const made = { inputs, terminal, visits: new Map() };
|
|
1137
|
+
analyses.set(key, made);
|
|
1138
|
+
return made;
|
|
1139
|
+
}
|
|
1140
|
+
/* The nodes a set of changed inputs reaches, once each, in order -- made once per set and kept with the analysis. */
|
|
1141
|
+
function visitOf(moved, analysis) {
|
|
1142
|
+
const w = walkOf();
|
|
1143
|
+
if (moved.length === 1)
|
|
1144
|
+
return w.reach[moved[0]];
|
|
1145
|
+
const key = moved.join();
|
|
1146
|
+
let visit = analysis.visits.get(key);
|
|
1147
|
+
if (!visit) {
|
|
1148
|
+
const seen = new Set();
|
|
1149
|
+
for (const i of moved)
|
|
1150
|
+
for (const n of w.reach[i])
|
|
1151
|
+
seen.add(n);
|
|
1152
|
+
visit = [...seen].sort((a, b) => a - b);
|
|
1153
|
+
if (analysis.visits.size < 256)
|
|
1154
|
+
analysis.visits.set(key, visit);
|
|
1155
|
+
}
|
|
1156
|
+
return visit;
|
|
1157
|
+
}
|
|
1158
|
+
function bindFast(held) {
|
|
1159
|
+
const w = walkOf();
|
|
1160
|
+
const start = new Int32Array(w.nodes.length).fill(-1);
|
|
1161
|
+
let at = 0;
|
|
1162
|
+
for (const index of w.shapes) {
|
|
1163
|
+
start[index] = at;
|
|
1164
|
+
at += held.bodies[index]?.length ?? 0;
|
|
1165
|
+
}
|
|
1166
|
+
return { raw: held.raw.slice(), slots: held.slots.slice(), mark: new Int32Array(w.keys.length), epoch: 0, start, bodyCount: at, prefix: new Map() };
|
|
1167
|
+
}
|
|
1168
|
+
/*
|
|
1169
|
+
A pose step (ADR-0108): every changed input takes the fast path, so only poses move. The reach of the changed inputs
|
|
1170
|
+
runs once, in order, each node by its own run as an evaluation runs it -- the same operations in the same order, so
|
|
1171
|
+
the poses are an evaluation's to the bit (same engine). A row nothing reads has its copies composed straight into the
|
|
1172
|
+
pose list: compose(compose(pose, offset), member), the offset part made once while the row's pose stands.
|
|
1173
|
+
Returns undefined, having changed nothing, when an input does not take the fast path.
|
|
1174
|
+
*/
|
|
1175
|
+
function poseStep(input, held, analysis) {
|
|
1176
|
+
const w = walkOf();
|
|
1177
|
+
inputFields(input);
|
|
1178
|
+
const fast = (held.fast ??= bindFast(held));
|
|
1179
|
+
const moved = [];
|
|
1180
|
+
const values = [];
|
|
1181
|
+
for (let i = 0; i < m.inputs.length; i++) {
|
|
1182
|
+
const p = m.inputs[i];
|
|
1183
|
+
const given = input[p.id];
|
|
1184
|
+
if (given === fast.raw[i])
|
|
1185
|
+
continue;
|
|
1186
|
+
values.push(inputValue(p, given));
|
|
1187
|
+
moved.push(i);
|
|
1188
|
+
}
|
|
1189
|
+
for (const i of moved)
|
|
1190
|
+
if (!analysis.inputs[i].fast)
|
|
1191
|
+
return { why: analysis.inputs[i] };
|
|
1192
|
+
const epoch = ++fast.epoch;
|
|
1193
|
+
moved.forEach((i, k) => {
|
|
1194
|
+
fast.raw[i] = input[m.inputs[i].id];
|
|
1195
|
+
fast.slots[i] = values[k];
|
|
1196
|
+
fast.mark[i] = epoch;
|
|
1197
|
+
});
|
|
1198
|
+
const visit = visitOf(moved, analysis);
|
|
1199
|
+
const bodies = new Int32Array(fast.bodyCount);
|
|
1200
|
+
const poses = new Float64Array(fast.bodyCount * 12);
|
|
1201
|
+
let written = 0;
|
|
1202
|
+
const write = (slot, pose) => {
|
|
1203
|
+
const r = pose.r, t = pose.t, o = written * 12;
|
|
1204
|
+
bodies[written++] = slot;
|
|
1205
|
+
poses[o] = r[0][0];
|
|
1206
|
+
poses[o + 1] = r[0][1];
|
|
1207
|
+
poses[o + 2] = r[0][2];
|
|
1208
|
+
poses[o + 3] = r[1][0];
|
|
1209
|
+
poses[o + 4] = r[1][1];
|
|
1210
|
+
poses[o + 5] = r[1][2];
|
|
1211
|
+
poses[o + 6] = r[2][0];
|
|
1212
|
+
poses[o + 7] = r[2][1];
|
|
1213
|
+
poses[o + 8] = r[2][2];
|
|
1214
|
+
poses[o + 9] = t[0];
|
|
1215
|
+
poses[o + 10] = t[1];
|
|
1216
|
+
poses[o + 11] = t[2];
|
|
1217
|
+
};
|
|
1218
|
+
for (const index of visit) {
|
|
1219
|
+
const node = w.nodes[index];
|
|
1220
|
+
let reached = false;
|
|
1221
|
+
for (const s of node.args)
|
|
1222
|
+
if (fast.mark[s] === epoch) {
|
|
1223
|
+
reached = true;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
if (!reached)
|
|
1227
|
+
continue;
|
|
1228
|
+
const args = new Array(node.args.length);
|
|
1229
|
+
for (let k = 0; k < node.args.length; k++)
|
|
1230
|
+
args[k] = fast.slots[node.args[k]];
|
|
1231
|
+
if (analysis.terminal[index]) {
|
|
1232
|
+
const [layout, assembly, pose, rise] = args;
|
|
1233
|
+
const axis = ['x', 'y', 'z'].indexOf(node.n.params.axis), up = ['x', 'y', 'z'].indexOf(node.n.params.riseAxis);
|
|
1234
|
+
/* The offsets stand while the row's own pose and rise do (its layout is never a pose step's to change). */
|
|
1235
|
+
const steady = fast.mark[node.args[2]] !== epoch && (node.args[3] === undefined || fast.mark[node.args[3]] !== epoch);
|
|
1236
|
+
let prefix = steady ? fast.prefix.get(index) : undefined;
|
|
1237
|
+
if (!prefix) {
|
|
1238
|
+
prefix = [];
|
|
1239
|
+
for (let i = 0; i < layout.count; i++) {
|
|
1240
|
+
const t = [0, 0, 0];
|
|
1241
|
+
t[axis] = layout.centers[i];
|
|
1242
|
+
if (rise !== undefined)
|
|
1243
|
+
t[up] = i * rise;
|
|
1244
|
+
prefix.push(compose(pose, { r: identity(), t }));
|
|
1245
|
+
}
|
|
1246
|
+
fast.prefix.set(index, prefix);
|
|
1247
|
+
}
|
|
1248
|
+
let slot = fast.start[index];
|
|
1249
|
+
for (let i = 0; i < layout.count; i++)
|
|
1250
|
+
for (const member of assembly.members) {
|
|
1251
|
+
const p = compose(prefix[i], member.pose);
|
|
1252
|
+
finite(p, node.n.id);
|
|
1253
|
+
write(slot++, p);
|
|
1254
|
+
}
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const out = node.op.run(args, node.n.id, node.n);
|
|
1258
|
+
node.outs.forEach((s, i) => {
|
|
1259
|
+
fast.slots[s] = checkedValue(out[i], node.types[i], node.n.id);
|
|
1260
|
+
fast.mark[s] = epoch;
|
|
1261
|
+
});
|
|
1262
|
+
if (node.op.geometry) {
|
|
1263
|
+
const g = node.op.geometry(args, out, node.n);
|
|
1264
|
+
const own = Array.isArray(g) ? g : [g];
|
|
1265
|
+
let slot = fast.start[index];
|
|
1266
|
+
for (const body of own)
|
|
1267
|
+
write(slot++, body.pose);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const result = {
|
|
1271
|
+
kind: 'poses',
|
|
1272
|
+
generation: 0,
|
|
1273
|
+
bodies: bodies.slice(0, written),
|
|
1274
|
+
poses: poses.slice(0, written * 12),
|
|
1275
|
+
read: (ref) => {
|
|
1276
|
+
const s = w.slot.get(ref);
|
|
1277
|
+
if (s === undefined)
|
|
1278
|
+
fail('TARGET_ABSENT', ref, 'no such value');
|
|
1279
|
+
return fast.slots[s];
|
|
1280
|
+
},
|
|
1281
|
+
changed: (ref) => {
|
|
1282
|
+
const s = w.slot.get(ref);
|
|
1283
|
+
return s !== undefined && fast.mark[s] === epoch;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
return { result };
|
|
1287
|
+
}
|
|
954
1288
|
function step(input, held) {
|
|
955
1289
|
const w = walkOf();
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
for (const p of m.inputs)
|
|
959
|
-
if (!Object.hasOwn(input, p.id))
|
|
960
|
-
fail('SCHEMA', 'input values', `missing ${p.id}`);
|
|
961
|
-
for (const k of Object.keys(input))
|
|
962
|
-
if (!w.ids.has(k))
|
|
963
|
-
fail('SCHEMA', 'input values', `unknown ${k}`);
|
|
1290
|
+
inputFields(input);
|
|
1291
|
+
held.fast = undefined;
|
|
964
1292
|
const epoch = ++held.epoch;
|
|
965
1293
|
const touched = [];
|
|
966
1294
|
let moved = -1;
|
|
@@ -970,11 +1298,8 @@ function compileInternal(source) {
|
|
|
970
1298
|
/* The same number as last time passed every check then. */
|
|
971
1299
|
if (given === held.raw[i])
|
|
972
1300
|
return;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
fail('INPUT_RANGE', p.id, `${raw} outside [${p.min},${p.max}]`);
|
|
976
|
-
held.slots[i] = checkedValue(raw * w.scale[i], w.inputTypes[i], p.id);
|
|
977
|
-
held.raw[i] = raw;
|
|
1301
|
+
held.slots[i] = inputValue(p, given);
|
|
1302
|
+
held.raw[i] = given;
|
|
978
1303
|
held.mark[i] = epoch;
|
|
979
1304
|
touched.push(i);
|
|
980
1305
|
if (moved >= 0)
|