@hatiolab/figure-model 0.2.12 → 0.2.14

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/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,17 +691,100 @@ function checkedValue(value, type, path) {
675
691
  fail('COUNT', path, 'nonnegative safe integer required');
676
692
  return value;
677
693
  }
678
- const check = (v) => {
679
- if (typeof v === 'number')
680
- num(v, path);
681
- else if (Array.isArray(v))
682
- v.forEach(check);
683
- else if (v && typeof v === 'object')
684
- Object.values(v).forEach(check);
685
- };
686
- check(value);
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
+ }
687
709
  return value;
688
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
+ }
752
+ /*
753
+ Every number in a value is finite: the first one that is not, depth first in key order, is refused. A plain loop:
754
+ a repeat's output holds every copy's pose, and the walk with a closure and forEach per level was most of what
755
+ rerunning a turning roller row cost (64% of 24 us; 17 us after, 2026-10-03).
756
+ */
757
+ function finite(v, path) {
758
+ if (typeof v === 'number') {
759
+ if (!Number.isFinite(v))
760
+ fail('NON_FINITE', path, String(v));
761
+ }
762
+ else if (Array.isArray(v)) {
763
+ for (let i = 0; i < v.length; i++)
764
+ if (i in v)
765
+ finite(v[i], path);
766
+ }
767
+ else if (v && typeof v === 'object') {
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;
773
+ for (let i = 0; i < keys.length; i++)
774
+ finite(v[keys[i]], path);
775
+ }
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'));
689
788
  export function compileFigureGraph(source) {
690
789
  return compileInternal(source);
691
790
  }
@@ -779,6 +878,31 @@ function compileInternal(source) {
779
878
  }
780
879
  for (const name of nodes.keys())
781
880
  visit(name);
881
+ /** An instance evaluator's walk, made on first use (`walkOf`). */
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
+ }
782
906
  /*
783
907
  One evaluation. Given the last one, only what the changed inputs reach runs again (user's decision 2026-09-26: a
784
908
  figure whose controls move re-evaluated every body every frame -- 2.2 us a body, 26.7 ms for sixty turning
@@ -787,14 +911,11 @@ function compileInternal(source) {
787
911
  tests compare the two over random sequences of inputs.
788
912
  */
789
913
  function run(input, previous) {
790
- fields(input, m.inputs.map(p => p.id), [], 'input values');
914
+ inputFields(input);
791
915
  const values = new Map(), geometry = [], bodies = new Map(), changed = new Set();
792
916
  for (const p of m.inputs) {
793
- const raw = num(input[p.id], p.id);
794
- if (raw < p.min || raw > p.max)
795
- fail('INPUT_RANGE', p.id, `${raw} outside [${p.min},${p.max}]`);
796
- values.set(p.id, checkedValue(raw * unit(p.unit, p.id)[1], types.get(p.id), p.id));
797
- 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])
798
919
  changed.add(p.id);
799
920
  }
800
921
  for (const p of m.constants)
@@ -838,18 +959,397 @@ function compileInternal(source) {
838
959
  * (chief architect's ruling 2026-09-26: the compiled graph is shared, the last values belong to the instance -- sixty
839
960
  * conveyors of one type sharing one cache would break it at every call). A throw drops what it kept.
840
961
  */
841
- evaluator() {
962
+ evaluator(options = {}) {
842
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
+ };
843
992
  return {
844
- evaluate(input) {
845
- const previous = kept;
846
- kept = undefined;
847
- const next = run(input, previous);
848
- kept = next.kept;
849
- return next.result;
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 };
1009
+ }
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 }));
850
1017
  }
851
1018
  };
852
1019
  }
853
1020
  };
1021
+ /*
1022
+ The walk an instance's evaluator takes after its first evaluation (chief architect's ruling 2026-10-03, ADR-0087
1023
+ amendment 1). The whole order was visited at every call, every value moved into a new Map and every input checked
1024
+ again, so a conveyor whose turn reruns 5 of its 320 nodes still paid for 320 (39 of 73 us). Here every value has a
1025
+ slot and every input the nodes it reaches, in order: a call visits only those, and an unchanged value stays in its
1026
+ slot. Made once per plan, shared by its evaluators, never written after it is made.
1027
+ */
1028
+ function walkOf() {
1029
+ if (walk)
1030
+ return walk;
1031
+ const slot = new Map();
1032
+ const keys = [];
1033
+ const add = (ref) => {
1034
+ slot.set(ref, keys.length);
1035
+ keys.push(ref);
1036
+ };
1037
+ m.inputs.forEach(p => add(p.id));
1038
+ m.constants.forEach(p => add(p.id));
1039
+ const nodes = order.map((n, index) => {
1040
+ const op = ops[n.op];
1041
+ op.ports.forEach(p => add(n.outputs[p]));
1042
+ return { n, op, index, args: n.args.map(ref => slot.get(ref)), outs: op.ports.map(p => slot.get(n.outputs[p])), types: op.ports.map(p => types.get(n.outputs[p])) };
1043
+ });
1044
+ const readers = keys.map(() => []);
1045
+ for (const node of nodes)
1046
+ for (const s of node.args)
1047
+ readers[s].push(node.index);
1048
+ /* The nodes a value reaches, in order: what must run again when it changes. */
1049
+ const reachOf = (start) => {
1050
+ const seen = new Uint8Array(nodes.length);
1051
+ const stack = [...start];
1052
+ while (stack.length) {
1053
+ const s = stack.pop();
1054
+ for (const i of readers[s]) {
1055
+ if (seen[i])
1056
+ continue;
1057
+ seen[i] = 1;
1058
+ stack.push(...nodes[i].outs);
1059
+ }
1060
+ }
1061
+ const out = [];
1062
+ seen.forEach((v, i) => v && out.push(i));
1063
+ return out;
1064
+ };
1065
+ /* A node that is not pure runs at every evaluation, and so does every node it reaches. */
1066
+ const impure = nodes.filter(node => !node.op.pure);
1067
+ const always = [...new Set([...impure.map(node => node.index), ...reachOf(impure.flatMap(node => node.outs))])].sort((a, b) => a - b);
1068
+ walk = {
1069
+ keys,
1070
+ slot,
1071
+ nodes,
1072
+ readers,
1073
+ reach: m.inputs.map((_, i) => reachOf([i])),
1074
+ always,
1075
+ shapes: nodes.filter(node => node.op.geometry).map(node => node.index)
1076
+ };
1077
+ return walk;
1078
+ }
1079
+ /** What an evaluator keeps between calls: its slots, its bodies by node, and its own copy of the last result. */
1080
+ function hold(whole, result) {
1081
+ const w = walkOf();
1082
+ return {
1083
+ raw: m.inputs.map(p => whole.input[p.id]),
1084
+ slots: w.keys.map(k => whole.values.get(k)),
1085
+ bodies: w.nodes.map(node => whole.bodies.get(node.n.id)),
1086
+ /* Copies: the caller owns what it was given, and what it does with it never reaches the next evaluation. */
1087
+ values: { ...result.values },
1088
+ geometry: result.geometry.slice(),
1089
+ mark: new Int32Array(w.keys.length),
1090
+ ran: new Int32Array(w.nodes.length),
1091
+ epoch: 0
1092
+ };
1093
+ }
1094
+ /*
1095
+ One evaluation after the first. The result is the one a whole evaluation gives: the same refusals for the same
1096
+ inputs, the same values in the same order, an unchanged body as the same object. What is returned is a snapshot
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
1098
+ caller does not change when the evaluator moves on, and a caller writing into one does not change the next.
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
+ }
1288
+ function step(input, held) {
1289
+ const w = walkOf();
1290
+ inputFields(input);
1291
+ held.fast = undefined;
1292
+ const epoch = ++held.epoch;
1293
+ const touched = [];
1294
+ let moved = -1;
1295
+ let several = false;
1296
+ m.inputs.forEach((p, i) => {
1297
+ const given = input[p.id];
1298
+ /* The same number as last time passed every check then. */
1299
+ if (given === held.raw[i])
1300
+ return;
1301
+ held.slots[i] = inputValue(p, given);
1302
+ held.raw[i] = given;
1303
+ held.mark[i] = epoch;
1304
+ touched.push(i);
1305
+ if (moved >= 0)
1306
+ several = true;
1307
+ moved = i;
1308
+ });
1309
+ /* The nodes to visit, in order: what the changed inputs reach, and what runs at every evaluation. */
1310
+ let visit;
1311
+ if (!several && !w.always.length)
1312
+ visit = moved >= 0 ? w.reach[moved] : [];
1313
+ else {
1314
+ const all = [];
1315
+ for (const i of [...touched.map(t => w.reach[t]), w.always])
1316
+ for (const n of i)
1317
+ if (held.ran[n] !== epoch) {
1318
+ held.ran[n] = epoch;
1319
+ all.push(n);
1320
+ }
1321
+ visit = all.sort((a, b) => a - b);
1322
+ }
1323
+ let reshaped = false;
1324
+ for (const index of visit) {
1325
+ const node = w.nodes[index];
1326
+ if (node.op.pure && !node.args.some(s => held.mark[s] === epoch))
1327
+ continue;
1328
+ const args = node.args.map(s => held.slots[s]);
1329
+ const out = node.op.run(args, node.n.id, node.n);
1330
+ node.outs.forEach((s, i) => {
1331
+ held.slots[s] = checkedValue(out[i], node.types[i], node.n.id);
1332
+ held.mark[s] = epoch;
1333
+ touched.push(s);
1334
+ });
1335
+ if (node.op.geometry) {
1336
+ const g = node.op.geometry(args, out, node.n);
1337
+ held.bodies[index] = Array.isArray(g) ? g : [{ id: node.n.id, ...g }];
1338
+ reshaped = true;
1339
+ }
1340
+ }
1341
+ for (const s of touched)
1342
+ held.values[w.keys[s]] = held.slots[s];
1343
+ if (reshaped) {
1344
+ const geometry = [];
1345
+ for (const index of w.shapes) {
1346
+ const own = held.bodies[index];
1347
+ if (own)
1348
+ geometry.push(...own);
1349
+ }
1350
+ held.geometry = geometry;
1351
+ }
1352
+ return { values: { ...held.values }, geometry: held.geometry.slice() };
1353
+ }
854
1354
  }
855
1355
  //# sourceMappingURL=graph.js.map