@hatiolab/figure-model 0.3.2 → 0.3.3
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 +14 -4
- 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/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 +33 -6
- 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 +175 -52
- 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/completeness-review-2026-10-04.md +256 -0
- package/docs/status.md +12 -1
- 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,6 +1053,8 @@ 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;
|
|
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;
|
|
973
1058
|
const evaluated = (result, whole) => {
|
|
974
1059
|
const now = result.geometry.map((g) => g.id);
|
|
975
1060
|
if (whole || now.length !== ids.length || now.some((id, i) => id !== ids[i]))
|
|
@@ -978,12 +1063,12 @@ function compileInternal(source) {
|
|
|
978
1063
|
return result;
|
|
979
1064
|
};
|
|
980
1065
|
const evaluate = (input) => {
|
|
1066
|
+
calls++;
|
|
981
1067
|
const previous = kept;
|
|
982
1068
|
kept = undefined;
|
|
983
1069
|
if (!previous) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
return evaluated(whole.result, true);
|
|
1070
|
+
kept = hold(run(input, undefined).kept);
|
|
1071
|
+
return evaluated({ values: { ...kept.values }, geometry: kept.geometry.slice() }, true);
|
|
987
1072
|
}
|
|
988
1073
|
const result = step(input, previous);
|
|
989
1074
|
kept = previous;
|
|
@@ -998,12 +1083,28 @@ function compileInternal(source) {
|
|
|
998
1083
|
*/
|
|
999
1084
|
advance(input, ordinary) {
|
|
1000
1085
|
if (kept && !ordinary) {
|
|
1086
|
+
const call = ++calls;
|
|
1001
1087
|
const previous = kept;
|
|
1002
1088
|
kept = undefined;
|
|
1003
1089
|
const step = poseStep(input, previous, (analysis ??= poseAnalysisOf(options.reads)));
|
|
1004
1090
|
kept = previous;
|
|
1005
|
-
if ('result' in step)
|
|
1006
|
-
|
|
1091
|
+
if ('result' in step) {
|
|
1092
|
+
/*
|
|
1093
|
+
A pose step is not a snapshot: \`read\` and \`changed\` look into the evaluator as this step left it, and
|
|
1094
|
+
the next call moves it on. Past that they are refused rather than answering with a later step's values.
|
|
1095
|
+
*/
|
|
1096
|
+
const { read, changed } = step.result;
|
|
1097
|
+
const live = () => {
|
|
1098
|
+
if (calls !== call)
|
|
1099
|
+
fail('STEP_EXPIRED', 'advance', 'read a pose step before the next call on its evaluator');
|
|
1100
|
+
};
|
|
1101
|
+
return {
|
|
1102
|
+
...step.result,
|
|
1103
|
+
generation,
|
|
1104
|
+
read: (ref) => (live(), read(ref)),
|
|
1105
|
+
changed: (ref) => (live(), changed(ref))
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1007
1108
|
const result = evaluate(input);
|
|
1008
1109
|
return { kind: 'evaluation', result, generation, why: step.why };
|
|
1009
1110
|
}
|
|
@@ -1072,20 +1173,35 @@ function compileInternal(source) {
|
|
|
1072
1173
|
readers,
|
|
1073
1174
|
reach: m.inputs.map((_, i) => reachOf([i])),
|
|
1074
1175
|
always,
|
|
1075
|
-
shapes: nodes.filter(node => node.op.geometry).map(node => node.index)
|
|
1176
|
+
shapes: nodes.filter(node => node.op.geometry).map(node => node.index),
|
|
1177
|
+
shapeSlot: keys.map(k => types.get(k)?.kind === 'shape')
|
|
1076
1178
|
};
|
|
1077
1179
|
return walk;
|
|
1078
1180
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
|
|
1181
|
+
/**
|
|
1182
|
+
* What an evaluator keeps between calls: its slots, its bodies by node, and the last result as a caller is given it --
|
|
1183
|
+
* frozen copies of the slots' values and bodies (\`publicOf\`), shapes frozen where they are.
|
|
1184
|
+
*/
|
|
1185
|
+
function hold(whole) {
|
|
1081
1186
|
const w = walkOf();
|
|
1187
|
+
const slots = w.keys.map(k => whole.values.get(k));
|
|
1188
|
+
slots.forEach((v, s) => w.shapeSlot[s] && frozen(v));
|
|
1189
|
+
const bodies = w.nodes.map(node => whole.bodies.get(node.n.id));
|
|
1190
|
+
const made = new Map();
|
|
1191
|
+
const shown = bodies.map(own => own?.map(b => publicOf(b, made)));
|
|
1192
|
+
const values = {};
|
|
1193
|
+
w.keys.forEach((k, s) => (values[k] = publicOf(slots[s], made)));
|
|
1194
|
+
const geometry = [];
|
|
1195
|
+
for (const index of w.shapes)
|
|
1196
|
+
if (shown[index])
|
|
1197
|
+
geometry.push(...shown[index]);
|
|
1082
1198
|
return {
|
|
1083
1199
|
raw: m.inputs.map(p => whole.input[p.id]),
|
|
1084
|
-
slots
|
|
1085
|
-
bodies
|
|
1086
|
-
|
|
1087
|
-
values
|
|
1088
|
-
geometry
|
|
1200
|
+
slots,
|
|
1201
|
+
bodies,
|
|
1202
|
+
shown,
|
|
1203
|
+
values,
|
|
1204
|
+
geometry,
|
|
1089
1205
|
mark: new Int32Array(w.keys.length),
|
|
1090
1206
|
ran: new Int32Array(w.nodes.length),
|
|
1091
1207
|
epoch: 0
|
|
@@ -1094,8 +1210,9 @@ function compileInternal(source) {
|
|
|
1094
1210
|
/*
|
|
1095
1211
|
One evaluation after the first. The result is the one a whole evaluation gives: the same refusals for the same
|
|
1096
1212
|
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
|
|
1213
|
+
of its own -- a new values record and a new geometry list, copied from the evaluator's own, holding frozen values and
|
|
1214
|
+
bodies -- so a result kept by a caller does not change when the evaluator moves on, and a caller writing into one
|
|
1215
|
+
does not change the next.
|
|
1099
1216
|
*/
|
|
1100
1217
|
/*
|
|
1101
1218
|
Which inputs take the fast path (ADR-0108 decision 2), decided from everything each one reaches: every node it
|
|
@@ -1272,11 +1389,12 @@ function compileInternal(source) {
|
|
|
1272
1389
|
generation: 0,
|
|
1273
1390
|
bodies: bodies.slice(0, written),
|
|
1274
1391
|
poses: poses.slice(0, written * 12),
|
|
1392
|
+
/* What it hands out is a frozen copy, as an evaluation's values are. */
|
|
1275
1393
|
read: (ref) => {
|
|
1276
1394
|
const s = w.slot.get(ref);
|
|
1277
1395
|
if (s === undefined)
|
|
1278
1396
|
fail('TARGET_ABSENT', ref, 'no such value');
|
|
1279
|
-
return fast.slots[s];
|
|
1397
|
+
return publicOf(fast.slots[s], new Map());
|
|
1280
1398
|
},
|
|
1281
1399
|
changed: (ref) => {
|
|
1282
1400
|
const s = w.slot.get(ref);
|
|
@@ -1321,6 +1439,8 @@ function compileInternal(source) {
|
|
|
1321
1439
|
visit = all.sort((a, b) => a - b);
|
|
1322
1440
|
}
|
|
1323
1441
|
let reshaped = false;
|
|
1442
|
+
/* The copies made in this call, so a pose a body and a value both carry is copied once. */
|
|
1443
|
+
let made;
|
|
1324
1444
|
for (const index of visit) {
|
|
1325
1445
|
const node = w.nodes[index];
|
|
1326
1446
|
if (node.op.pure && !node.args.some(s => held.mark[s] === epoch))
|
|
@@ -1329,21 +1449,24 @@ function compileInternal(source) {
|
|
|
1329
1449
|
const out = node.op.run(args, node.n.id, node.n);
|
|
1330
1450
|
node.outs.forEach((s, i) => {
|
|
1331
1451
|
held.slots[s] = checkedValue(out[i], node.types[i], node.n.id);
|
|
1452
|
+
if (w.shapeSlot[s])
|
|
1453
|
+
frozen(held.slots[s]);
|
|
1332
1454
|
held.mark[s] = epoch;
|
|
1333
1455
|
touched.push(s);
|
|
1334
1456
|
});
|
|
1335
1457
|
if (node.op.geometry) {
|
|
1336
|
-
const
|
|
1337
|
-
held.bodies[index] =
|
|
1458
|
+
const own = bodiesOf(node.op.geometry(args, out, node.n), node.n.id);
|
|
1459
|
+
held.bodies[index] = own;
|
|
1460
|
+
held.shown[index] = own.map(b => publicOf(b, (made ??= new Map())));
|
|
1338
1461
|
reshaped = true;
|
|
1339
1462
|
}
|
|
1340
1463
|
}
|
|
1341
1464
|
for (const s of touched)
|
|
1342
|
-
held.values[w.keys[s]] = held.slots[s];
|
|
1465
|
+
held.values[w.keys[s]] = publicOf(held.slots[s], (made ??= new Map()));
|
|
1343
1466
|
if (reshaped) {
|
|
1344
1467
|
const geometry = [];
|
|
1345
1468
|
for (const index of w.shapes) {
|
|
1346
|
-
const own = held.
|
|
1469
|
+
const own = held.shown[index];
|
|
1347
1470
|
if (own)
|
|
1348
1471
|
geometry.push(...own);
|
|
1349
1472
|
}
|