@hatiolab/figure-model 0.3.2 → 0.3.4
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/README.md +15 -1
- package/dist/asset-types.d.ts +47 -6
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/asset.d.ts +11 -0
- package/dist/asset.d.ts.map +1 -1
- package/dist/asset.js +24 -10
- package/dist/asset.js.map +1 -1
- package/dist/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +8 -4
- package/dist/authoring-actions.js.map +1 -1
- package/dist/capabilities.js +28 -4
- package/dist/capabilities.js.map +1 -1
- package/dist/gate.d.ts +14 -2
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +45 -25
- package/dist/gate.js.map +1 -1
- package/dist/graph-types.d.ts +93 -7
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/graph.d.ts +7 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +380 -66
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/joint-list.d.ts.map +1 -1
- package/dist/joint-list.js +5 -3
- package/dist/joint-list.js.map +1 -1
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.js +1 -1
- package/docs/assembly-constraints.md +3 -3
- package/docs/asset-persistence.md +8 -8
- package/docs/authoring-transactions.md +2 -2
- package/docs/capability-bindings.md +1 -1
- package/docs/common-kernel-experiment.md +2 -2
- package/docs/completeness-review-2026-10-04.md +405 -0
- package/docs/core-semantics.md +2 -2
- package/docs/cutover.md +4 -4
- package/docs/document-contract.md +3 -3
- package/docs/dryroom-profile-validation.md +1 -1
- package/docs/editor-integration.md +4 -4
- package/docs/evaluation-changes-design.md +304 -0
- package/docs/evaluation-performance-summary-2026-10-05.md +218 -0
- package/docs/full-conveyor-validation.md +8 -8
- package/docs/full-dryroom-validation.md +5 -5
- package/docs/full-forklift-validation.md +5 -5
- package/docs/full-robot-validation.md +4 -4
- package/docs/layout-system.md +2 -2
- package/docs/legacy-risk-audit-2026-09-23.md +14 -14
- package/docs/minimal-model.md +3 -3
- package/docs/openusd-review.md +14 -14
- package/docs/operator-contracts.md +4 -4
- package/docs/prototypes/README.md +2 -2
- package/docs/prototypes/asset.schema.json +1 -1
- package/docs/prototypes/document.schema.json +1 -1
- package/docs/prototypes/editor-browser.mjs +2 -2
- package/docs/prototypes/linear-comparison.py +1 -1
- package/docs/prototypes/render-dryroom.py +1 -1
- package/docs/prototypes/render-full-dryroom.mjs +12 -12
- package/docs/prototypes/runtime-browser.mjs +3 -3
- package/docs/research-review.md +1 -1
- package/docs/review-2026-09-22.md +19 -19
- package/docs/runtime-extraction.md +10 -10
- package/docs/sample-validation-expanded.md +5 -5
- package/docs/sample-validation.md +5 -5
- package/docs/shape-dimension-contract.md +1 -1
- package/docs/spatial-contracts.md +2 -2
- package/docs/status.md +12 -1
- package/docs/storage-integration.md +5 -5
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -708,6 +708,62 @@ function checkedValue(value, type, path) {
|
|
|
708
708
|
}
|
|
709
709
|
return value;
|
|
710
710
|
}
|
|
711
|
+
/*
|
|
712
|
+
Who owns what an instance's evaluator returns (completeness review 2026-10-04, F1). Its slots hold the values it runs
|
|
713
|
+
on, and an unchanged one is handed on, as the same object, to the next result and stays in the results given before.
|
|
714
|
+
A caller's write into one used to reach the next evaluation (a shape's size.x written as 999 came back as 999).
|
|
715
|
+
|
|
716
|
+
Shapes are frozen where they are: they are made when a design value changes, and a renderer keeps its plan while a
|
|
717
|
+
body's shape is the same object. Everything else a result holds -- poses, placed things, rows, bodies -- is a frozen
|
|
718
|
+
copy of the slot's value, made when the value changes and handed on unchanged after that; the slots themselves stay
|
|
719
|
+
as they were made. Freezing the slots in place cost more than the copies: a frozen list of numbers holds them boxed,
|
|
720
|
+
so a pose step reading frozen poses ran 50% slower, and the 6-axis robot's evaluation with every joint moved took
|
|
721
|
+
35 us instead of 12 (2026-10-04, Node 24). A whole evaluation keeps nothing, and all of it is the caller's.
|
|
722
|
+
*/
|
|
723
|
+
function frozen(v) {
|
|
724
|
+
if (!v || typeof v !== 'object' || Object.isFrozen(v))
|
|
725
|
+
return v;
|
|
726
|
+
Object.freeze(v);
|
|
727
|
+
if (Array.isArray(v))
|
|
728
|
+
for (let i = 0; i < v.length; i++)
|
|
729
|
+
frozen(v[i]);
|
|
730
|
+
else
|
|
731
|
+
for (const k in v)
|
|
732
|
+
frozen(v[k]);
|
|
733
|
+
return v;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* The frozen copy of a value a result holds. Something already frozen -- a shape, or a copy made before -- is itself;
|
|
737
|
+
* \`made\` holds the copies made in this call, so a pose a body and a value both carry is copied once.
|
|
738
|
+
*/
|
|
739
|
+
function publicOf(v, made) {
|
|
740
|
+
if (!v || typeof v !== 'object' || Object.isFrozen(v))
|
|
741
|
+
return v;
|
|
742
|
+
let p = made.get(v);
|
|
743
|
+
if (p)
|
|
744
|
+
return p;
|
|
745
|
+
if (isPose(v)) {
|
|
746
|
+
const r = new Array(v.r.length);
|
|
747
|
+
for (let i = 0; i < r.length; i++)
|
|
748
|
+
r[i] = Object.freeze(v.r[i].slice());
|
|
749
|
+
p = { r: Object.freeze(r), t: Object.freeze(v.t.slice()) };
|
|
750
|
+
}
|
|
751
|
+
else if (Array.isArray(v)) {
|
|
752
|
+
p = new Array(v.length);
|
|
753
|
+
for (let i = 0; i < v.length; i++)
|
|
754
|
+
p[i] = publicOf(v[i], made);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
p = {};
|
|
758
|
+
for (const k in v)
|
|
759
|
+
p[k] = publicOf(v[k], made);
|
|
760
|
+
}
|
|
761
|
+
Object.freeze(p);
|
|
762
|
+
made.set(v, p);
|
|
763
|
+
return p;
|
|
764
|
+
}
|
|
765
|
+
/** A geometry node's bodies: a placement's one, a row's many. */
|
|
766
|
+
const bodiesOf = (g, id) => (Array.isArray(g) ? g : [{ id, ...g }]);
|
|
711
767
|
/** Whether a value's own keys are exactly these two, in this order -- without making the list of them. */
|
|
712
768
|
const keysAre = (v, a, b) => {
|
|
713
769
|
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
@@ -785,6 +841,13 @@ const finiteObjects = new WeakSet();
|
|
|
785
841
|
/** 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
842
|
const POSE_KINDS = ['transform', 'placed', 'assembly', 'collection', 'point'];
|
|
787
843
|
const notPose = (t) => (Array.isArray(t) || POSE_KINDS.includes(t?.kind) ? undefined : String(t?.kind ?? 'value'));
|
|
844
|
+
/**
|
|
845
|
+
* Compiles a graph whatever order its nodes are written in, and however deep its chains (the walk keeps its own stack).
|
|
846
|
+
* It holds no size limit of its own: the byte and node limits -- 1 MiB of JSON, nesting 64, \`FIGURE_DOCUMENT_NODE_LIMIT\`
|
|
847
|
+
* nodes -- are the document's and the asset's (\`compileFigureDocument\`, \`compileFigureAsset\`), the entrances for what
|
|
848
|
+
* arrives from outside. A caller compiling a graph directly answers for its size, and its compile and evaluation take
|
|
849
|
+
* time and memory in proportion to it.
|
|
850
|
+
*/
|
|
788
851
|
export function compileFigureGraph(source) {
|
|
789
852
|
return compileInternal(source);
|
|
790
853
|
}
|
|
@@ -843,41 +906,62 @@ function compileInternal(source) {
|
|
|
843
906
|
assign(output, n.id);
|
|
844
907
|
nodes.set(n.id, n);
|
|
845
908
|
}
|
|
909
|
+
/*
|
|
910
|
+
Depth first, in declaration order: a node is typed once every node it reads is (completeness review 2026-10-04, F3).
|
|
911
|
+
The walk keeps its own stack -- a chain of 2,000 nodes written last-first overflowed the engine's -- and gives the
|
|
912
|
+
order, the refusals and the cycle path the recursive walk gave: a cycle is named from where the walk entered it.
|
|
913
|
+
*/
|
|
846
914
|
const visited = new Set();
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (
|
|
915
|
+
const onPath = new Set();
|
|
916
|
+
const path = [];
|
|
917
|
+
const enter = (name) => {
|
|
918
|
+
if (onPath.has(name))
|
|
851
919
|
fail('CYCLE', name, [...active, name].join(' -> '));
|
|
852
920
|
active.push(name);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
921
|
+
onPath.add(name);
|
|
922
|
+
path.push({ n: nodes.get(name), at: 0, inputTypes: [] });
|
|
923
|
+
};
|
|
924
|
+
for (const root of nodes.keys()) {
|
|
925
|
+
if (visited.has(root))
|
|
926
|
+
continue;
|
|
927
|
+
enter(root);
|
|
928
|
+
while (path.length) {
|
|
929
|
+
const frame = path[path.length - 1];
|
|
930
|
+
const n = frame.n;
|
|
931
|
+
if (frame.at < n.args.length) {
|
|
932
|
+
const ref = n.args[frame.at];
|
|
933
|
+
if (!writers.has(ref))
|
|
934
|
+
fail('UNKNOWN_REFERENCE', n.id, ref);
|
|
935
|
+
if (!types.has(ref)) {
|
|
936
|
+
enter(writers.get(ref));
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
frame.inputTypes.push(types.get(ref));
|
|
940
|
+
frame.at++;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const op = ops[n.op];
|
|
944
|
+
const resultTypes = op.types(frame.inputTypes, n.id, n);
|
|
945
|
+
const depends = new Set();
|
|
946
|
+
for (const ref of n.args)
|
|
947
|
+
for (const s of dependence.get(ref) ?? [])
|
|
948
|
+
depends.add(s);
|
|
949
|
+
if (depends.size) {
|
|
950
|
+
if (op.state === undefined)
|
|
951
|
+
fail('STATE_POLICY', n.id, `${n.op} declares no policy for a state-dependent argument (${[...depends].join(', ')})`);
|
|
952
|
+
if (op.state === 'refuses')
|
|
953
|
+
fail('TOPOLOGY_STATE', n.id, `${n.op} must depend on design inputs only; it depends on ${[...depends].join(', ')}`);
|
|
954
|
+
}
|
|
955
|
+
op.ports.forEach((p, i) => types.set(n.outputs[p], resultTypes[i]));
|
|
956
|
+
if (depends.size)
|
|
957
|
+
Object.values(n.outputs).forEach(ref => dependence.set(ref, new Set(depends)));
|
|
958
|
+
path.pop();
|
|
959
|
+
active.pop();
|
|
960
|
+
onPath.delete(n.id);
|
|
961
|
+
visited.add(n.id);
|
|
962
|
+
order.push(n);
|
|
871
963
|
}
|
|
872
|
-
op.ports.forEach((p, i) => types.set(n.outputs[p], resultTypes[i]));
|
|
873
|
-
if (depends.size)
|
|
874
|
-
Object.values(n.outputs).forEach(ref => dependence.set(ref, new Set(depends)));
|
|
875
|
-
active.pop();
|
|
876
|
-
visited.add(name);
|
|
877
|
-
order.push(n);
|
|
878
964
|
}
|
|
879
|
-
for (const name of nodes.keys())
|
|
880
|
-
visit(name);
|
|
881
965
|
/** An instance evaluator's walk, made on first use (`walkOf`). */
|
|
882
966
|
let walk;
|
|
883
967
|
/** The pose analyses made so far, by what reads the graph (`poseAnalysisOf`). */
|
|
@@ -938,8 +1022,7 @@ function compileInternal(source) {
|
|
|
938
1022
|
changed.add(n.outputs[p]);
|
|
939
1023
|
});
|
|
940
1024
|
if (op.geometry) {
|
|
941
|
-
const
|
|
942
|
-
const own = Array.isArray(g) ? g : [{ id: n.id, ...g }];
|
|
1025
|
+
const own = bodiesOf(op.geometry(args, out, n), n.id);
|
|
943
1026
|
bodies.set(n.id, own);
|
|
944
1027
|
geometry.push(...own);
|
|
945
1028
|
}
|
|
@@ -970,25 +1053,126 @@ function compileInternal(source) {
|
|
|
970
1053
|
let ids = [];
|
|
971
1054
|
/* What reads this graph does not change under an evaluator: its analysis is looked up once. */
|
|
972
1055
|
let analysis;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1056
|
+
/* Every call, refused or not; a pose step's \`read\` and \`changed\` answer while it is still the last one. */
|
|
1057
|
+
let calls = 0;
|
|
1058
|
+
/* Whether the bodies are others than the last ones (a whole evaluation always is): the generation moves on. */
|
|
1059
|
+
const moved = (now, whole) => {
|
|
1060
|
+
const other = whole || now.length !== ids.length || now.some((id, i) => id !== ids[i]);
|
|
1061
|
+
if (other)
|
|
976
1062
|
generation++;
|
|
977
1063
|
ids = now;
|
|
1064
|
+
return other;
|
|
1065
|
+
};
|
|
1066
|
+
const evaluated = (result, whole) => {
|
|
1067
|
+
moved(result.geometry.map((g) => g.id), whole);
|
|
978
1068
|
return result;
|
|
979
1069
|
};
|
|
980
1070
|
const evaluate = (input) => {
|
|
1071
|
+
calls++;
|
|
981
1072
|
const previous = kept;
|
|
982
1073
|
kept = undefined;
|
|
983
1074
|
if (!previous) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
return evaluated(whole.result, true);
|
|
1075
|
+
kept = hold(run(input, undefined).kept);
|
|
1076
|
+
return evaluated({ values: { ...kept.values }, geometry: kept.geometry.slice() }, true);
|
|
987
1077
|
}
|
|
988
|
-
const result = step(input, previous);
|
|
1078
|
+
const result = step(input, previous, true);
|
|
989
1079
|
kept = previous;
|
|
990
1080
|
return evaluated(result, false);
|
|
991
1081
|
};
|
|
1082
|
+
/*
|
|
1083
|
+
An evaluation told as its changes (\`FigureGraphChanges\`, chosen with \`changes: true\`). It runs what an evaluation
|
|
1084
|
+
runs, in the same order; what a snapshot would publish waits for the next \`evaluate\` (\`Held.pending\`). Told: the
|
|
1085
|
+
bodies that ran, by slot -- every slot when the bodies are others than the last ones, with the new ids and keys.
|
|
1086
|
+
*/
|
|
1087
|
+
const changesOf = (input, why) => {
|
|
1088
|
+
const call = ++calls;
|
|
1089
|
+
const previous = kept;
|
|
1090
|
+
kept = undefined;
|
|
1091
|
+
/* What pose steps moved since the last evaluation: an evaluation drops their state, so it is taken first. */
|
|
1092
|
+
const posed = previous?.fast;
|
|
1093
|
+
let ran;
|
|
1094
|
+
if (!previous) {
|
|
1095
|
+
kept = hold(run(input, undefined).kept, false);
|
|
1096
|
+
ran = w().shapes;
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
ran = step(input, previous, false);
|
|
1100
|
+
kept = previous;
|
|
1101
|
+
}
|
|
1102
|
+
const held = kept;
|
|
1103
|
+
const now = [], keys = [], paths = [], indexes = [];
|
|
1104
|
+
const start = new Int32Array(w().nodes.length);
|
|
1105
|
+
for (const index of w().shapes) {
|
|
1106
|
+
start[index] = now.length;
|
|
1107
|
+
for (const b of held.bodies[index] ?? []) {
|
|
1108
|
+
now.push(b.id);
|
|
1109
|
+
keys.push(b.key);
|
|
1110
|
+
/* A row's copy keeps the path through its rows: a frozen copy, told with the structure only. */
|
|
1111
|
+
paths.push(b.path ? Object.freeze(b.path.slice()) : undefined);
|
|
1112
|
+
indexes.push(b.index);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const structure = moved(now, !previous);
|
|
1116
|
+
const told = structure ? w().shapes : posed?.wrote.size ? [...new Set([...ran, ...posed.wrote])].sort((a, b) => a - b) : ran;
|
|
1117
|
+
let count = 0;
|
|
1118
|
+
for (const index of told)
|
|
1119
|
+
count += held.bodies[index]?.length ?? 0;
|
|
1120
|
+
const bodies = new Int32Array(count), poses = new Float64Array(count * 12), shapes = new Array(count);
|
|
1121
|
+
let k = 0;
|
|
1122
|
+
for (const index of told)
|
|
1123
|
+
(held.bodies[index] ?? []).forEach((b, j) => {
|
|
1124
|
+
bodies[k] = start[index] + j;
|
|
1125
|
+
shapes[k] = b.shape ?? null;
|
|
1126
|
+
const o = k * 12;
|
|
1127
|
+
/* A body with no pose (a scalar provider) has no numbers: NaN, never a pose made up for it. */
|
|
1128
|
+
if (b.pose) {
|
|
1129
|
+
const r = b.pose.r, t = b.pose.t;
|
|
1130
|
+
poses[o] = r[0][0];
|
|
1131
|
+
poses[o + 1] = r[0][1];
|
|
1132
|
+
poses[o + 2] = r[0][2];
|
|
1133
|
+
poses[o + 3] = r[1][0];
|
|
1134
|
+
poses[o + 4] = r[1][1];
|
|
1135
|
+
poses[o + 5] = r[1][2];
|
|
1136
|
+
poses[o + 6] = r[2][0];
|
|
1137
|
+
poses[o + 7] = r[2][1];
|
|
1138
|
+
poses[o + 8] = r[2][2];
|
|
1139
|
+
poses[o + 9] = t[0];
|
|
1140
|
+
poses[o + 10] = t[1];
|
|
1141
|
+
poses[o + 11] = t[2];
|
|
1142
|
+
}
|
|
1143
|
+
else
|
|
1144
|
+
poses.fill(NaN, o, o + 12);
|
|
1145
|
+
k++;
|
|
1146
|
+
});
|
|
1147
|
+
const epoch = held.epoch;
|
|
1148
|
+
const live = () => {
|
|
1149
|
+
if (calls !== call)
|
|
1150
|
+
fail('STEP_EXPIRED', 'advance', 'read a step\'s changes before the next call on its evaluator');
|
|
1151
|
+
};
|
|
1152
|
+
return {
|
|
1153
|
+
kind: 'changes',
|
|
1154
|
+
generation,
|
|
1155
|
+
why,
|
|
1156
|
+
...(structure ? { structure: { ids: now, keys, paths, indexes } } : {}),
|
|
1157
|
+
bodies,
|
|
1158
|
+
poses,
|
|
1159
|
+
shapes,
|
|
1160
|
+
/* A number or a flag as it is; anything else a frozen copy made for this read -- never the evaluator's own. */
|
|
1161
|
+
read: (ref) => {
|
|
1162
|
+
live();
|
|
1163
|
+
const s = w().slot.get(ref);
|
|
1164
|
+
if (s === undefined)
|
|
1165
|
+
fail('TARGET_ABSENT', ref, 'no such value');
|
|
1166
|
+
return publicOf(held.slots[s], new Map());
|
|
1167
|
+
},
|
|
1168
|
+
changed: (ref) => {
|
|
1169
|
+
live();
|
|
1170
|
+
const s = w().slot.get(ref);
|
|
1171
|
+
return s !== undefined && (structure || held.mark[s] === epoch || !!posed?.moved.has(s));
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
};
|
|
1175
|
+
const w = () => walkOf();
|
|
992
1176
|
return {
|
|
993
1177
|
evaluate,
|
|
994
1178
|
/**
|
|
@@ -998,22 +1182,69 @@ function compileInternal(source) {
|
|
|
998
1182
|
*/
|
|
999
1183
|
advance(input, ordinary) {
|
|
1000
1184
|
if (kept && !ordinary) {
|
|
1185
|
+
const call = ++calls;
|
|
1001
1186
|
const previous = kept;
|
|
1002
1187
|
kept = undefined;
|
|
1003
1188
|
const step = poseStep(input, previous, (analysis ??= poseAnalysisOf(options.reads)));
|
|
1004
1189
|
kept = previous;
|
|
1005
|
-
if ('result' in step)
|
|
1006
|
-
|
|
1190
|
+
if ('result' in step) {
|
|
1191
|
+
/*
|
|
1192
|
+
A pose step is not a snapshot: \`read\` and \`changed\` look into the evaluator as this step left it, and
|
|
1193
|
+
the next call moves it on. Past that they are refused rather than answering with a later step's values.
|
|
1194
|
+
*/
|
|
1195
|
+
const { read, changed } = step.result;
|
|
1196
|
+
const live = () => {
|
|
1197
|
+
if (calls !== call)
|
|
1198
|
+
fail('STEP_EXPIRED', 'advance', 'read a pose step before the next call on its evaluator');
|
|
1199
|
+
};
|
|
1200
|
+
return {
|
|
1201
|
+
...step.result,
|
|
1202
|
+
generation,
|
|
1203
|
+
read: (ref) => (live(), read(ref)),
|
|
1204
|
+
changed: (ref) => (live(), changed(ref))
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
if (options.changes)
|
|
1208
|
+
return changesOf(input, step.why);
|
|
1007
1209
|
const result = evaluate(input);
|
|
1008
1210
|
return { kind: 'evaluation', result, generation, why: step.why };
|
|
1009
1211
|
}
|
|
1010
1212
|
const bound = !!kept;
|
|
1213
|
+
const why = { code: bound ? ordinary : 'NOT_BOUND' };
|
|
1214
|
+
if (options.changes)
|
|
1215
|
+
return changesOf(input, why);
|
|
1011
1216
|
const result = evaluate(input);
|
|
1012
|
-
return { kind: 'evaluation', result, generation, why
|
|
1217
|
+
return { kind: 'evaluation', result, generation, why };
|
|
1013
1218
|
},
|
|
1014
1219
|
/** Which inputs take the fast path for what reads this graph, and why the others do not. */
|
|
1015
1220
|
poseInputs() {
|
|
1016
1221
|
return poseAnalysisOf(options.reads).inputs.map(a => ({ ...a }));
|
|
1222
|
+
},
|
|
1223
|
+
/**
|
|
1224
|
+
* One value as the evaluator stands now -- after the last evaluation and the pose steps since -- without a
|
|
1225
|
+
* snapshot: a number or a flag as it is, anything else a frozen copy made for this read, the caller's to keep.
|
|
1226
|
+
* Refused (NOT_BOUND) before the first evaluation and after a refusal.
|
|
1227
|
+
*/
|
|
1228
|
+
value(ref) {
|
|
1229
|
+
if (!kept)
|
|
1230
|
+
fail('NOT_BOUND', ref, 'evaluate before reading a value');
|
|
1231
|
+
const s = w().slot.get(ref);
|
|
1232
|
+
if (s === undefined)
|
|
1233
|
+
fail('TARGET_ABSENT', ref, 'no such value');
|
|
1234
|
+
/* The pose steps' slots start as the evaluation's and move on with each step: they are the latest. */
|
|
1235
|
+
const fast = kept.fast;
|
|
1236
|
+
if (!fast)
|
|
1237
|
+
return publicOf(kept.slots[s], new Map());
|
|
1238
|
+
/*
|
|
1239
|
+
Except a row nothing reads, whose copies a pose step writes as poses alone (its slot stays the evaluation's):
|
|
1240
|
+
asked for, it is run from the steps' slots, as an evaluation would run it.
|
|
1241
|
+
*/
|
|
1242
|
+
const writer = w().nodes.find(node => node.outs.includes(s));
|
|
1243
|
+
if (writer && fast.wrote.has(writer.index) && !fast.moved.has(s)) {
|
|
1244
|
+
const out = writer.op.run(writer.args.map(a => fast.slots[a]), writer.n.id, writer.n);
|
|
1245
|
+
return publicOf(out[writer.outs.indexOf(s)], new Map());
|
|
1246
|
+
}
|
|
1247
|
+
return publicOf(fast.slots[s], new Map());
|
|
1017
1248
|
}
|
|
1018
1249
|
};
|
|
1019
1250
|
}
|
|
@@ -1072,30 +1303,68 @@ function compileInternal(source) {
|
|
|
1072
1303
|
readers,
|
|
1073
1304
|
reach: m.inputs.map((_, i) => reachOf([i])),
|
|
1074
1305
|
always,
|
|
1075
|
-
shapes: nodes.filter(node => node.op.geometry).map(node => node.index)
|
|
1306
|
+
shapes: nodes.filter(node => node.op.geometry).map(node => node.index),
|
|
1307
|
+
shapeSlot: keys.map(k => types.get(k)?.kind === 'shape')
|
|
1076
1308
|
};
|
|
1077
1309
|
return walk;
|
|
1078
1310
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
|
|
1311
|
+
/**
|
|
1312
|
+
* What an evaluator keeps between calls: its slots, its bodies by node, and the last result as a caller is given it --
|
|
1313
|
+
* frozen copies of the slots' values and bodies (\`publicOf\`), shapes frozen where they are.
|
|
1314
|
+
*/
|
|
1315
|
+
function hold(whole, publish = true) {
|
|
1081
1316
|
const w = walkOf();
|
|
1317
|
+
const slots = w.keys.map(k => whole.values.get(k));
|
|
1318
|
+
slots.forEach((v, s) => w.shapeSlot[s] && frozen(v));
|
|
1319
|
+
const bodies = w.nodes.map(node => whole.bodies.get(node.n.id));
|
|
1320
|
+
/*
|
|
1321
|
+
An evaluator told as changes publishes nothing until a snapshot is asked for: everything is pending, and the record
|
|
1322
|
+
holds its keys in the order a snapshot gives them.
|
|
1323
|
+
*/
|
|
1324
|
+
if (!publish) {
|
|
1325
|
+
const values = {};
|
|
1326
|
+
for (const k of w.keys)
|
|
1327
|
+
values[k] = undefined;
|
|
1328
|
+
return {
|
|
1329
|
+
raw: m.inputs.map(p => whole.input[p.id]),
|
|
1330
|
+
slots,
|
|
1331
|
+
bodies,
|
|
1332
|
+
shown: bodies.map(() => undefined),
|
|
1333
|
+
values,
|
|
1334
|
+
geometry: [],
|
|
1335
|
+
mark: new Int32Array(w.keys.length),
|
|
1336
|
+
ran: new Int32Array(w.nodes.length),
|
|
1337
|
+
epoch: 0,
|
|
1338
|
+
pending: { nodes: new Set(w.shapes.filter(index => bodies[index])), slots: new Set(w.keys.keys()) }
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const made = new Map();
|
|
1342
|
+
const shown = bodies.map(own => own?.map(b => publicOf(b, made)));
|
|
1343
|
+
const values = {};
|
|
1344
|
+
w.keys.forEach((k, s) => (values[k] = publicOf(slots[s], made)));
|
|
1345
|
+
const geometry = [];
|
|
1346
|
+
for (const index of w.shapes)
|
|
1347
|
+
if (shown[index])
|
|
1348
|
+
geometry.push(...shown[index]);
|
|
1082
1349
|
return {
|
|
1083
1350
|
raw: m.inputs.map(p => whole.input[p.id]),
|
|
1084
|
-
slots
|
|
1085
|
-
bodies
|
|
1086
|
-
|
|
1087
|
-
values
|
|
1088
|
-
geometry
|
|
1351
|
+
slots,
|
|
1352
|
+
bodies,
|
|
1353
|
+
shown,
|
|
1354
|
+
values,
|
|
1355
|
+
geometry,
|
|
1089
1356
|
mark: new Int32Array(w.keys.length),
|
|
1090
1357
|
ran: new Int32Array(w.nodes.length),
|
|
1091
|
-
epoch: 0
|
|
1358
|
+
epoch: 0,
|
|
1359
|
+
pending: { nodes: new Set(), slots: new Set() }
|
|
1092
1360
|
};
|
|
1093
1361
|
}
|
|
1094
1362
|
/*
|
|
1095
1363
|
One evaluation after the first. The result is the one a whole evaluation gives: the same refusals for the same
|
|
1096
1364
|
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
|
|
1098
|
-
caller does not change when the evaluator moves on, and a caller writing into one
|
|
1365
|
+
of its own -- a new values record and a new geometry list, copied from the evaluator's own, holding frozen values and
|
|
1366
|
+
bodies -- so a result kept by a caller does not change when the evaluator moves on, and a caller writing into one
|
|
1367
|
+
does not change the next.
|
|
1099
1368
|
*/
|
|
1100
1369
|
/*
|
|
1101
1370
|
Which inputs take the fast path (ADR-0108 decision 2), decided from everything each one reaches: every node it
|
|
@@ -1163,7 +1432,7 @@ function compileInternal(source) {
|
|
|
1163
1432
|
start[index] = at;
|
|
1164
1433
|
at += held.bodies[index]?.length ?? 0;
|
|
1165
1434
|
}
|
|
1166
|
-
return { raw: held.raw.slice(), slots: held.slots.slice(), mark: new Int32Array(w.keys.length), epoch: 0, start, bodyCount: at, prefix: new Map() };
|
|
1435
|
+
return { raw: held.raw.slice(), slots: held.slots.slice(), mark: new Int32Array(w.keys.length), epoch: 0, start, bodyCount: at, prefix: new Map(), wrote: new Set(), moved: new Set() };
|
|
1167
1436
|
}
|
|
1168
1437
|
/*
|
|
1169
1438
|
A pose step (ADR-0108): every changed input takes the fast path, so only poses move. The reach of the changed inputs
|
|
@@ -1175,13 +1444,18 @@ function compileInternal(source) {
|
|
|
1175
1444
|
function poseStep(input, held, analysis) {
|
|
1176
1445
|
const w = walkOf();
|
|
1177
1446
|
inputFields(input);
|
|
1178
|
-
|
|
1447
|
+
/*
|
|
1448
|
+
What moved is told against the pose steps' own inputs, or the evaluation's while no pose step has been bound since:
|
|
1449
|
+
an input that takes no fast path is found before the steps' state is made (it was made, and dropped, at every
|
|
1450
|
+
evaluation frame of a resize -- 2026-10-05).
|
|
1451
|
+
*/
|
|
1452
|
+
const raw = held.fast?.raw ?? held.raw;
|
|
1179
1453
|
const moved = [];
|
|
1180
1454
|
const values = [];
|
|
1181
1455
|
for (let i = 0; i < m.inputs.length; i++) {
|
|
1182
1456
|
const p = m.inputs[i];
|
|
1183
1457
|
const given = input[p.id];
|
|
1184
|
-
if (given ===
|
|
1458
|
+
if (given === raw[i])
|
|
1185
1459
|
continue;
|
|
1186
1460
|
values.push(inputValue(p, given));
|
|
1187
1461
|
moved.push(i);
|
|
@@ -1189,6 +1463,7 @@ function compileInternal(source) {
|
|
|
1189
1463
|
for (const i of moved)
|
|
1190
1464
|
if (!analysis.inputs[i].fast)
|
|
1191
1465
|
return { why: analysis.inputs[i] };
|
|
1466
|
+
const fast = (held.fast ??= bindFast(held));
|
|
1192
1467
|
const epoch = ++fast.epoch;
|
|
1193
1468
|
moved.forEach((i, k) => {
|
|
1194
1469
|
fast.raw[i] = input[m.inputs[i].id];
|
|
@@ -1252,12 +1527,14 @@ function compileInternal(source) {
|
|
|
1252
1527
|
finite(p, node.n.id);
|
|
1253
1528
|
write(slot++, p);
|
|
1254
1529
|
}
|
|
1530
|
+
fast.wrote.add(index);
|
|
1255
1531
|
continue;
|
|
1256
1532
|
}
|
|
1257
1533
|
const out = node.op.run(args, node.n.id, node.n);
|
|
1258
1534
|
node.outs.forEach((s, i) => {
|
|
1259
1535
|
fast.slots[s] = checkedValue(out[i], node.types[i], node.n.id);
|
|
1260
1536
|
fast.mark[s] = epoch;
|
|
1537
|
+
fast.moved.add(s);
|
|
1261
1538
|
});
|
|
1262
1539
|
if (node.op.geometry) {
|
|
1263
1540
|
const g = node.op.geometry(args, out, node.n);
|
|
@@ -1265,6 +1542,7 @@ function compileInternal(source) {
|
|
|
1265
1542
|
let slot = fast.start[index];
|
|
1266
1543
|
for (const body of own)
|
|
1267
1544
|
write(slot++, body.pose);
|
|
1545
|
+
fast.wrote.add(index);
|
|
1268
1546
|
}
|
|
1269
1547
|
}
|
|
1270
1548
|
const result = {
|
|
@@ -1272,11 +1550,12 @@ function compileInternal(source) {
|
|
|
1272
1550
|
generation: 0,
|
|
1273
1551
|
bodies: bodies.slice(0, written),
|
|
1274
1552
|
poses: poses.slice(0, written * 12),
|
|
1553
|
+
/* What it hands out is a frozen copy, as an evaluation's values are. */
|
|
1275
1554
|
read: (ref) => {
|
|
1276
1555
|
const s = w.slot.get(ref);
|
|
1277
1556
|
if (s === undefined)
|
|
1278
1557
|
fail('TARGET_ABSENT', ref, 'no such value');
|
|
1279
|
-
return fast.slots[s];
|
|
1558
|
+
return publicOf(fast.slots[s], new Map());
|
|
1280
1559
|
},
|
|
1281
1560
|
changed: (ref) => {
|
|
1282
1561
|
const s = w.slot.get(ref);
|
|
@@ -1285,7 +1564,7 @@ function compileInternal(source) {
|
|
|
1285
1564
|
};
|
|
1286
1565
|
return { result };
|
|
1287
1566
|
}
|
|
1288
|
-
function step(input, held) {
|
|
1567
|
+
function step(input, held, publish) {
|
|
1289
1568
|
const w = walkOf();
|
|
1290
1569
|
inputFields(input);
|
|
1291
1570
|
held.fast = undefined;
|
|
@@ -1320,7 +1599,7 @@ function compileInternal(source) {
|
|
|
1320
1599
|
}
|
|
1321
1600
|
visit = all.sort((a, b) => a - b);
|
|
1322
1601
|
}
|
|
1323
|
-
|
|
1602
|
+
const ran = [];
|
|
1324
1603
|
for (const index of visit) {
|
|
1325
1604
|
const node = w.nodes[index];
|
|
1326
1605
|
if (node.op.pure && !node.args.some(s => held.mark[s] === epoch))
|
|
@@ -1329,27 +1608,62 @@ function compileInternal(source) {
|
|
|
1329
1608
|
const out = node.op.run(args, node.n.id, node.n);
|
|
1330
1609
|
node.outs.forEach((s, i) => {
|
|
1331
1610
|
held.slots[s] = checkedValue(out[i], node.types[i], node.n.id);
|
|
1611
|
+
if (w.shapeSlot[s])
|
|
1612
|
+
frozen(held.slots[s]);
|
|
1332
1613
|
held.mark[s] = epoch;
|
|
1333
1614
|
touched.push(s);
|
|
1334
1615
|
});
|
|
1335
1616
|
if (node.op.geometry) {
|
|
1336
|
-
|
|
1337
|
-
held.
|
|
1338
|
-
|
|
1617
|
+
held.bodies[index] = bodiesOf(node.op.geometry(args, out, node.n), node.n.id);
|
|
1618
|
+
held.pending.nodes.add(index);
|
|
1619
|
+
ran.push(index);
|
|
1339
1620
|
}
|
|
1340
1621
|
}
|
|
1341
1622
|
for (const s of touched)
|
|
1342
|
-
held.
|
|
1343
|
-
if (
|
|
1623
|
+
held.pending.slots.add(s);
|
|
1624
|
+
if (!publish) {
|
|
1625
|
+
/*
|
|
1626
|
+
What ran is published again at the next snapshot: the copies made of it before are let go here, so a snapshot read
|
|
1627
|
+
once does not keep its changed bodies and values alive through the changes after it. A snapshot a caller kept is
|
|
1628
|
+
not touched; what did not run keeps its copies, the same objects in the next snapshot.
|
|
1629
|
+
*/
|
|
1630
|
+
for (const index of ran)
|
|
1631
|
+
held.shown[index] = undefined;
|
|
1632
|
+
if (ran.length)
|
|
1633
|
+
held.geometry = [];
|
|
1634
|
+
for (const s of touched)
|
|
1635
|
+
held.values[w.keys[s]] = undefined;
|
|
1636
|
+
return ran;
|
|
1637
|
+
}
|
|
1638
|
+
publishPending(held);
|
|
1639
|
+
return { values: { ...held.values }, geometry: held.geometry.slice() };
|
|
1640
|
+
}
|
|
1641
|
+
/*
|
|
1642
|
+
What a snapshot shows of what ran since the last one (\`Held.pending\`): frozen copies of the bodies and values made
|
|
1643
|
+
then, once each however many evaluations ran them, in the order a snapshot made them before -- bodies, then values,
|
|
1644
|
+
one copy of a pose both carry. What did not run keeps the copies the earlier snapshots hold.
|
|
1645
|
+
*/
|
|
1646
|
+
function publishPending(held) {
|
|
1647
|
+
const w = walkOf();
|
|
1648
|
+
const { nodes, slots } = held.pending;
|
|
1649
|
+
if (!nodes.size && !slots.size)
|
|
1650
|
+
return;
|
|
1651
|
+
const made = new Map();
|
|
1652
|
+
for (const index of [...nodes].sort((a, b) => a - b))
|
|
1653
|
+
held.shown[index] = held.bodies[index].map(b => publicOf(b, made));
|
|
1654
|
+
for (const s of slots)
|
|
1655
|
+
held.values[w.keys[s]] = publicOf(held.slots[s], made);
|
|
1656
|
+
if (nodes.size) {
|
|
1344
1657
|
const geometry = [];
|
|
1345
1658
|
for (const index of w.shapes) {
|
|
1346
|
-
const own = held.
|
|
1659
|
+
const own = held.shown[index];
|
|
1347
1660
|
if (own)
|
|
1348
1661
|
geometry.push(...own);
|
|
1349
1662
|
}
|
|
1350
1663
|
held.geometry = geometry;
|
|
1351
1664
|
}
|
|
1352
|
-
|
|
1665
|
+
nodes.clear();
|
|
1666
|
+
slots.clear();
|
|
1353
1667
|
}
|
|
1354
1668
|
}
|
|
1355
1669
|
//# sourceMappingURL=graph.js.map
|