@hatiolab/figure-model 0.2.14 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-actions.d.ts +68 -25
- package/dist/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +331 -71
- package/dist/authoring-actions.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/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/joint-list.d.ts +76 -0
- package/dist/joint-list.d.ts.map +1 -0
- package/dist/joint-list.js +259 -0
- package/dist/joint-list.js.map +1 -0
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.d.ts.map +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/kernel-version.js.map +1 -1
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +5 -4
- package/dist/proposal.js.map +1 -1
- package/dist/recipe.d.ts +11 -2
- package/dist/recipe.d.ts.map +1 -1
- package/dist/recipe.js +3 -1
- package/dist/recipe.js.map +1 -1
- package/dist/release-contract.d.ts +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +83 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-metrics.d.ts +3 -0
- package/dist/release-metrics.d.ts.map +1 -1
- package/dist/release-metrics.js +13 -2
- package/dist/release-metrics.js.map +1 -1
- package/dist/render-batches.d.ts +44 -2
- package/dist/render-batches.d.ts.map +1 -1
- package/dist/render-batches.js +168 -3
- package/dist/render-batches.js.map +1 -1
- package/dist/robots.d.ts +23 -26
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +52 -20
- package/dist/robots.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +11 -20
- package/dist/score.js.map +1 -1
- package/package.json +2 -1
|
@@ -47,6 +47,101 @@ export const FIGURE_FACES = { left: 'x', right: 'x', bottom: 'y', top: 'y', back
|
|
|
47
47
|
export const FIGURE_JOINT_TYPES = ['fixed', 'revolute', 'prismatic'];
|
|
48
48
|
const SIGN = { left: -1, right: 1, bottom: -1, top: 1, back: -1, front: 1 };
|
|
49
49
|
const OPPOSITE = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' };
|
|
50
|
+
/** The joints a follower follows, each with its multiplier: one for a one-leader mimic, none for a joint that leads itself. */
|
|
51
|
+
export function figureMimicLeadersOf(j) {
|
|
52
|
+
const mimic = j.mimic;
|
|
53
|
+
if (!mimic || typeof mimic !== 'object')
|
|
54
|
+
return [];
|
|
55
|
+
if (Object.hasOwn(mimic, 'sum'))
|
|
56
|
+
return Array.isArray(mimic.sum) ? mimic.sum : [];
|
|
57
|
+
return [{ joint: mimic.joint, multiplier: mimic.multiplier }];
|
|
58
|
+
}
|
|
59
|
+
/** Whether a length is written as something to follow: one source, or a sum of one term or more, each with a source. */
|
|
60
|
+
const followsSomething = (measure) => {
|
|
61
|
+
const terms = followedTermsOf(measure)?.terms;
|
|
62
|
+
return Array.isArray(terms) && terms.length > 0 && terms.every(t => !!t?.from);
|
|
63
|
+
};
|
|
64
|
+
/** A length that follows something, as its terms and its margin; undefined for a number (or nothing). */
|
|
65
|
+
function followedTermsOf(measure) {
|
|
66
|
+
if (!measure || typeof measure !== 'object')
|
|
67
|
+
return undefined;
|
|
68
|
+
if (Object.hasOwn(measure, 'sum')) {
|
|
69
|
+
const m = measure;
|
|
70
|
+
return { terms: m.sum, plus: m.plus ?? 0, sum: true };
|
|
71
|
+
}
|
|
72
|
+
const m = measure;
|
|
73
|
+
return { terms: [{ from: m.from, ...(m.times !== undefined ? { times: m.times } : {}) }], plus: m.plus ?? 0, sum: false };
|
|
74
|
+
}
|
|
75
|
+
const everyKey = () => (...keys) => keys;
|
|
76
|
+
const JOINT_FIELDS = {
|
|
77
|
+
joint: everyKey()('id', 'type', 'body0', 'body1', 'origin', 'axis', 'lowerLimit', 'upperLimit', 'travel', 'state', 'mimic', 'repeat'),
|
|
78
|
+
body0: everyKey()('part', 'face', 'plane'),
|
|
79
|
+
body1: everyKey()('part', 'face'),
|
|
80
|
+
origin: everyKey()('facing', 'gap', 'align', 'turn', 'twist'),
|
|
81
|
+
axes: everyKey()('x', 'y', 'z'),
|
|
82
|
+
alignment: everyKey()('mm'),
|
|
83
|
+
measure: everyKey()('from', 'times', 'plus', 'sum'),
|
|
84
|
+
term: everyKey()('from', 'times'),
|
|
85
|
+
source: everyKey()('of', 'axis', 'input', 'part'),
|
|
86
|
+
travel: everyKey()('source', 'times', 'plus', 'sum'),
|
|
87
|
+
state: everyKey()('id', 'label', 'start', 'sweep'),
|
|
88
|
+
mimic: everyKey()('joint', 'multiplier', 'offset', 'sum'),
|
|
89
|
+
leader: everyKey()('joint', 'multiplier'),
|
|
90
|
+
repeat: everyKey()('axis', 'pitch', 'count', 'rise'),
|
|
91
|
+
rise: everyKey()('axis', 'mm')
|
|
92
|
+
};
|
|
93
|
+
/** Refuses, by name, a key of a joint record or of an object in it that this kernel does not know. A key left undefined is no field. */
|
|
94
|
+
function knownJointFields(j, path) {
|
|
95
|
+
const only = (o, allowed, at) => {
|
|
96
|
+
if (!o || typeof o !== 'object' || Array.isArray(o))
|
|
97
|
+
return;
|
|
98
|
+
for (const [k, v] of Object.entries(o))
|
|
99
|
+
if (v !== undefined && !allowed.includes(k))
|
|
100
|
+
fail('JOINT_SCHEMA', at, `${k} is a field this kernel does not know`);
|
|
101
|
+
};
|
|
102
|
+
const source = (s, at) => only(s, JOINT_FIELDS.source, at);
|
|
103
|
+
const measure = (m, at) => {
|
|
104
|
+
if (!m || typeof m !== 'object')
|
|
105
|
+
return;
|
|
106
|
+
only(m, JOINT_FIELDS.measure, at);
|
|
107
|
+
source(m.from, `${at}.from`);
|
|
108
|
+
if (Array.isArray(m.sum))
|
|
109
|
+
for (const [i, t] of m.sum.entries()) {
|
|
110
|
+
only(t, JOINT_FIELDS.term, `${at}.sum.${i}`);
|
|
111
|
+
source(t?.from, `${at}.sum.${i}.from`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
only(j, JOINT_FIELDS.joint, path);
|
|
115
|
+
only(j.body0, JOINT_FIELDS.body0, `${path}.body0`);
|
|
116
|
+
only(j.body1, JOINT_FIELDS.body1, `${path}.body1`);
|
|
117
|
+
const o = j.origin;
|
|
118
|
+
only(o, JOINT_FIELDS.origin, `${path}.origin`);
|
|
119
|
+
if (o && typeof o === 'object') {
|
|
120
|
+
measure(o.gap, `${path}.origin.gap`);
|
|
121
|
+
only(o.turn, JOINT_FIELDS.axes, `${path}.origin.turn`);
|
|
122
|
+
only(o.align, JOINT_FIELDS.axes, `${path}.origin.align`);
|
|
123
|
+
for (const [axis, a] of Object.entries(o.align && typeof o.align === 'object' ? o.align : {})) {
|
|
124
|
+
only(a, JOINT_FIELDS.alignment, `${path}.origin.align.${axis}`);
|
|
125
|
+
measure(a?.mm, `${path}.origin.align.${axis}.mm`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (j.travel && typeof j.travel === 'object') {
|
|
129
|
+
only(j.travel, JOINT_FIELDS.travel, `${path}.travel`);
|
|
130
|
+
source(j.travel.source, `${path}.travel.source`);
|
|
131
|
+
if (Array.isArray(j.travel.sum))
|
|
132
|
+
measure({ sum: j.travel.sum }, `${path}.travel`);
|
|
133
|
+
}
|
|
134
|
+
only(j.state, JOINT_FIELDS.state, `${path}.state`);
|
|
135
|
+
only(j.mimic, JOINT_FIELDS.mimic, `${path}.mimic`);
|
|
136
|
+
if (Array.isArray(j.mimic?.sum))
|
|
137
|
+
for (const [i, t] of j.mimic.sum.entries())
|
|
138
|
+
only(t, JOINT_FIELDS.leader, `${path}.mimic.sum.${i}`);
|
|
139
|
+
only(j.repeat, JOINT_FIELDS.repeat, `${path}.repeat`);
|
|
140
|
+
if (j.repeat && typeof j.repeat === 'object') {
|
|
141
|
+
measure(j.repeat.pitch, `${path}.repeat.pitch`);
|
|
142
|
+
only(j.repeat.rise, JOINT_FIELDS.rise, `${path}.repeat.rise`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
50
145
|
const clone = (asset) => structuredClone(asset);
|
|
51
146
|
const nodeById = (m, id) => m.nodes.find((n) => n.id === id);
|
|
52
147
|
const writerOf = (m, ref) => m.nodes.find((n) => Object.values(n.outputs).includes(ref));
|
|
@@ -300,9 +395,29 @@ function measureRef(m, asset, name, measure, hint, path) {
|
|
|
300
395
|
fail('SCHEMA', path, 'a finite length is required');
|
|
301
396
|
return constantOf(m, 'mm', measure, 'd');
|
|
302
397
|
}
|
|
303
|
-
if (!measure || typeof measure !== 'object' || !measure.from)
|
|
398
|
+
if (!measure || typeof measure !== 'object' || !(Object.hasOwn(measure, 'sum') || measure.from))
|
|
304
399
|
fail('SCHEMA', path, 'a length is a number of millimetres, or something to follow');
|
|
305
|
-
|
|
400
|
+
const { terms, plus, sum } = followedTermsOf(measure);
|
|
401
|
+
const refs = termRefs(m, asset, terms, plus, path);
|
|
402
|
+
/* One term is written as it always was; a sum adds its terms, then the margin (the same add@1 and mul@1 nodes). */
|
|
403
|
+
if (!sum)
|
|
404
|
+
return scaled(m, name, refs[0].ref, refs[0].k, plus, hint);
|
|
405
|
+
return scaled(m, name, sumOf(m, name, refs, `${hint}.sum`), 1, plus, hint);
|
|
406
|
+
}
|
|
407
|
+
/** Each term of a followed length as a graph reference and its multiple, every one checked as a one-term length is. */
|
|
408
|
+
function termRefs(m, asset, terms, plus, path) {
|
|
409
|
+
if (!Array.isArray(terms) || !terms.length)
|
|
410
|
+
fail('SCHEMA', path, 'a sum has one term or more');
|
|
411
|
+
if (!Number.isFinite(plus))
|
|
412
|
+
fail('SCHEMA', path, 'finite times and plus required');
|
|
413
|
+
return terms.map((t, i) => {
|
|
414
|
+
const at = terms.length > 1 ? `${path}.sum.${i}` : path;
|
|
415
|
+
if (!t || typeof t !== 'object' || !t.from)
|
|
416
|
+
fail('SCHEMA', at, 'a length is a number of millimetres, or something to follow');
|
|
417
|
+
if (!Number.isFinite(t.times ?? 1))
|
|
418
|
+
fail('SCHEMA', at, 'finite times and plus required');
|
|
419
|
+
return { ref: sourceRef(m, asset, t.from, at), k: t.times ?? 1 };
|
|
420
|
+
});
|
|
306
421
|
}
|
|
307
422
|
/** Σ k·ref, written as nodes, with nothing emitted for a weight of zero and no node for a single term. */
|
|
308
423
|
function sumOf(m, name, terms, hint) {
|
|
@@ -407,19 +522,23 @@ function linkDimension(asset, a) {
|
|
|
407
522
|
const mine = stableDimension(m, a.part, a.axis, path);
|
|
408
523
|
if (followsAlready(m, a.part, a.axis) && !a.replace)
|
|
409
524
|
fail('DIMENSION_LINKED', path, `${a.part}'s ${a.axis} already follows something; pass replace to change what it follows, or unlink it first`);
|
|
410
|
-
const
|
|
525
|
+
const sum = Object.hasOwn(a, 'sum');
|
|
526
|
+
const terms = sum ? a.sum : [{ from: a.source, ...(a.times !== undefined ? { times: a.times } : {}) }];
|
|
411
527
|
const plus = a.plus ?? 0;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
528
|
+
/* Each term is checked for a loop as a one-term link is: none may read the measure it would set. */
|
|
529
|
+
for (const t of Array.isArray(terms) ? terms : [])
|
|
530
|
+
if (t?.from?.of === 'part' && t.from.part === a.part && t.from.axis === a.axis)
|
|
531
|
+
fail('DIMENSION_CYCLE', path, `${a.part}'s ${a.axis} cannot follow itself`);
|
|
532
|
+
const refs = termRefs(m, asset, terms, plus, path);
|
|
533
|
+
for (const [i, { ref }] of refs.entries()) {
|
|
534
|
+
const source = terms[i].from;
|
|
535
|
+
if (ref === mine)
|
|
536
|
+
fail('DIMENSION_CYCLE', path, `${a.part}'s ${a.axis} cannot follow itself`);
|
|
537
|
+
if (reaches(m, ref, mine))
|
|
538
|
+
fail('DIMENSION_CYCLE', path, `${String(source.of === 'part' ? source.part : source.of)} already follows ${a.part}'s ${a.axis}`);
|
|
539
|
+
}
|
|
421
540
|
const name = namer(m, `${a.part}.${a.axis}`);
|
|
422
|
-
const value = scaled(m, name,
|
|
541
|
+
const value = sum ? scaled(m, name, sumOf(m, name, refs, 'follows.sum'), 1, plus, 'follows') : scaled(m, name, refs[0].ref, refs[0].k, plus, 'follows');
|
|
423
542
|
const node = nodeById(m, `${a.part}.dimension.${a.axis}`);
|
|
424
543
|
const was = node.args[0];
|
|
425
544
|
const owned = ownInput(m, a.part, was) ? was : `${a.part}.size.${a.axis}`;
|
|
@@ -1236,6 +1355,7 @@ function checkJoint(asset, j) {
|
|
|
1236
1355
|
const path = `joints.${String(j?.id)}`;
|
|
1237
1356
|
if (!j || typeof j.id !== 'string' || !j.id.trim())
|
|
1238
1357
|
fail('JOINT_SCHEMA', 'joints', 'a joint has an id');
|
|
1358
|
+
knownJointFields(j, path);
|
|
1239
1359
|
if (!FIGURE_JOINT_TYPES.includes(j.type))
|
|
1240
1360
|
fail('JOINT_SCHEMA', path, `${String(j.type)} is not a joint type; ${FIGURE_JOINT_TYPES.join(', ')}`);
|
|
1241
1361
|
if (!j.body1 || typeof j.body1.part !== 'string')
|
|
@@ -1275,19 +1395,38 @@ function checkJoint(asset, j) {
|
|
|
1275
1395
|
const [lo, hi] = [j.lowerLimit, j.upperLimit];
|
|
1276
1396
|
if (!(Number.isFinite(lo) && Number.isFinite(hi) && lo < hi))
|
|
1277
1397
|
fail('JOINT_SCHEMA', path, `a ${j.type} joint has both limits, lower below upper`);
|
|
1278
|
-
|
|
1279
|
-
|
|
1398
|
+
/*
|
|
1399
|
+
The rest pose is every joint at 0 -- the kinematic reference its axis is read at (joint-list.ts) -- and 0 need not
|
|
1400
|
+
be a value the joint may take: a joint limited to 10..80 still has its reference at 0 (ADR-0107 J4). Its starting
|
|
1401
|
+
value, the stored state default, is held to its limits as every control's is (addMotion).
|
|
1402
|
+
*/
|
|
1280
1403
|
}
|
|
1281
1404
|
if (j.mimic) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1405
|
+
/*
|
|
1406
|
+
One leader or a sum of them (ADR-0107 decision 2), each held to the same rules: a moving joint that leads itself
|
|
1407
|
+
-- so a follower is never a leader and no loop can close -- that moves the way this one does (degrees are not
|
|
1408
|
+
added to millimetres), named once, with a finite multiplier.
|
|
1409
|
+
*/
|
|
1410
|
+
const sum = Object.hasOwn(j.mimic, 'sum');
|
|
1411
|
+
if (sum && !(Array.isArray(j.mimic.sum) && j.mimic.sum.length))
|
|
1412
|
+
fail('JOINT_SCHEMA', path, 'a mimic sum follows one joint or more');
|
|
1413
|
+
const seen = new Set();
|
|
1414
|
+
for (const t of figureMimicLeadersOf(j)) {
|
|
1415
|
+
const leader = others.find(o => o.id === t?.joint);
|
|
1416
|
+
if (!leader || leader.type === 'fixed')
|
|
1417
|
+
fail('JOINT_SCHEMA', path, `${String(t?.joint)} is not a moving joint to follow`);
|
|
1418
|
+
if (seen.has(leader.id))
|
|
1419
|
+
fail('JOINT_SCHEMA', path, `${leader.id} is followed twice; a sum names each joint once`);
|
|
1420
|
+
seen.add(leader.id);
|
|
1421
|
+
if (leader.mimic)
|
|
1422
|
+
fail('JOINT_SCHEMA', path, `${leader.id} follows another joint; follow the one it follows`);
|
|
1423
|
+
if ((leader.type === 'prismatic') !== (j.type === 'prismatic'))
|
|
1424
|
+
fail('JOINT_SCHEMA', path, `${leader.id} ${leader.type === 'prismatic' ? 'slides' : 'turns'}; a joint follows ones that move the same way: a slide a slide, a turn a turn`);
|
|
1425
|
+
if (!Number.isFinite(t.multiplier))
|
|
1426
|
+
fail('JOINT_SCHEMA', path, 'a mimic has a multiplier');
|
|
1427
|
+
}
|
|
1428
|
+
if (j.mimic.offset !== undefined && !Number.isFinite(j.mimic.offset))
|
|
1429
|
+
fail('JOINT_SCHEMA', path, "a mimic's offset is a number in the joint's unit");
|
|
1291
1430
|
}
|
|
1292
1431
|
if (j.origin?.twist !== undefined && !Number.isFinite(j.origin.twist))
|
|
1293
1432
|
fail('JOINT_SCHEMA', `${path}.origin.twist`, 'a twist is a number of degrees');
|
|
@@ -1324,7 +1463,7 @@ function checkJoint(asset, j) {
|
|
|
1324
1463
|
fail('JOINT_SCHEMA', row, `a row runs along X, Y or Z, not ${String(r.axis)}`);
|
|
1325
1464
|
if (r.axis.toLowerCase() === FIGURE_FACES[childFaceOf(asset.document.model, j)])
|
|
1326
1465
|
fail('JOINT_SCHEMA', row, `a row runs across the face it is fastened to; ${r.axis} is out of that face`);
|
|
1327
|
-
if (typeof r.pitch === 'number' ? !(Number.isFinite(r.pitch) && r.pitch > 0) : !(r.pitch
|
|
1466
|
+
if (typeof r.pitch === 'number' ? !(Number.isFinite(r.pitch) && r.pitch > 0) : !followsSomething(r.pitch))
|
|
1328
1467
|
fail('GEOMETRY_DOMAIN', row, 'a row has a positive pitch, or one that follows a length');
|
|
1329
1468
|
if (r.count !== undefined && !(Number.isSafeInteger(r.count) && r.count >= 1 && r.count <= REPEAT_LIMIT))
|
|
1330
1469
|
fail('GEOMETRY_DOMAIN', row, `a row has 1 to ${REPEAT_LIMIT} copies`);
|
|
@@ -1407,7 +1546,7 @@ function writeJoint(asset, j) {
|
|
|
1407
1546
|
const kind = j.type === 'prismatic' ? 'slide' : 'turn';
|
|
1408
1547
|
const unit = j.type === 'prismatic' ? (j.travel ? 'ratio' : 'mm') : 'deg';
|
|
1409
1548
|
const range = j.lowerLimit !== undefined && j.upperLimit !== undefined ? { min: j.lowerLimit, max: j.upperLimit } : j.type === 'revolute' ? { min: -180, max: 180 } : { min: 0, max: 1 };
|
|
1410
|
-
const
|
|
1549
|
+
const leaders = figureMimicLeadersOf(j).map(t => ({ leader: (asset.joints ?? []).find(o => o.id === t.joint), k: t.multiplier }));
|
|
1411
1550
|
/* The axis turned into the parent's frame too: one of its axes, either way along it. */
|
|
1412
1551
|
const along = mv3(back, AXES.map(a => (a === j.axis.toLowerCase() ? 1 : 0)));
|
|
1413
1552
|
const axisIndex = along.findIndex(v => Math.abs(Math.abs(v) - 1) < 1e-9);
|
|
@@ -1417,7 +1556,7 @@ function writeJoint(asset, j) {
|
|
|
1417
1556
|
kind: 'add-motion',
|
|
1418
1557
|
part: child,
|
|
1419
1558
|
motion: { kind, axis: AXES[axisIndex] },
|
|
1420
|
-
state: { id: stateIdOf(j), unit, min: range.min, max: range.max, start: j.state?.start ?? 0, ...(j.state?.label !== undefined ? { label: j.state.label } : {}), ...(j.state?.sweep !== undefined ? { sweep: j.state.sweep } : {}) },
|
|
1559
|
+
state: { id: stateIdOf(j), unit, min: range.min, max: range.max, start: j.state?.start ?? Math.min(range.max, Math.max(range.min, 0)), ...(j.state?.label !== undefined ? { label: j.state.label } : {}), ...(j.state?.sweep !== undefined ? { sweep: j.state.sweep } : {}) },
|
|
1421
1560
|
...(j.travel ? { travel: j.travel } : {}),
|
|
1422
1561
|
replace: true
|
|
1423
1562
|
},
|
|
@@ -1426,7 +1565,13 @@ function writeJoint(asset, j) {
|
|
|
1426
1565
|
stroke is a share of a length has a control of 0..1, and multiplying that gave a follower a number with no length
|
|
1427
1566
|
(the telescopic conveyor, 2026-09-25). For a leader whose control is its distance the two are one value.
|
|
1428
1567
|
*/
|
|
1429
|
-
|
|
1568
|
+
j.mimic
|
|
1569
|
+
? {
|
|
1570
|
+
terms: leaders.map(({ leader, k }) => ({ of: nodeById(m, `${leader.body1.part}.motion`)?.args[3] ?? stateIdOf(leader), k })),
|
|
1571
|
+
c: j.mimic.offset ?? 0,
|
|
1572
|
+
sum: Object.hasOwn(j.mimic, 'sum')
|
|
1573
|
+
}
|
|
1574
|
+
: undefined, along[axisIndex] > 0 ? 1 : -1);
|
|
1430
1575
|
if (j.repeat)
|
|
1431
1576
|
repeatPart(asset, j);
|
|
1432
1577
|
}
|
|
@@ -1711,7 +1856,45 @@ function stripJoint(asset, j) {
|
|
|
1711
1856
|
dropIfUnused(asset, ref, ownInput(m, child, ref) ? ref : undefined);
|
|
1712
1857
|
place.args = [place.args[0], pose.outputs.pose];
|
|
1713
1858
|
}
|
|
1714
|
-
/**
|
|
1859
|
+
/**
|
|
1860
|
+
* Every length a joint record holds that may follow something, read as a measure, with a way to write it back: the
|
|
1861
|
+
* gap, a row's pitch, a slide's travel (its one-term form names the source `source`, and without one is `plus` mm)
|
|
1862
|
+
* and each offset across the face. Renaming a part and taking one away read and write them all alike.
|
|
1863
|
+
*/
|
|
1864
|
+
function jointMeasures(j) {
|
|
1865
|
+
const out = [];
|
|
1866
|
+
if (j.origin?.gap !== undefined)
|
|
1867
|
+
out.push({ at: 'gap', get: () => j.origin.gap, set: v => (j.origin.gap = v) });
|
|
1868
|
+
if (j.repeat)
|
|
1869
|
+
out.push({ at: 'pitch', get: () => j.repeat.pitch, set: v => (j.repeat.pitch = v) });
|
|
1870
|
+
if (j.travel) {
|
|
1871
|
+
const t = j.travel;
|
|
1872
|
+
out.push({
|
|
1873
|
+
at: 'travel',
|
|
1874
|
+
get: () => (Object.hasOwn(t, 'sum') ? { sum: t.sum, ...(t.plus !== undefined ? { plus: t.plus } : {}) } : t.source ? { from: t.source, ...(t.times !== undefined ? { times: t.times } : {}), ...(t.plus !== undefined ? { plus: t.plus } : {}) } : (t.plus ?? 0)),
|
|
1875
|
+
set: v => {
|
|
1876
|
+
const f = followedTermsOf(v);
|
|
1877
|
+
j.travel = !f ? { plus: v } : f.sum ? { sum: f.terms, ...(f.plus ? { plus: f.plus } : {}) } : { source: f.terms[0].from, ...(f.terms[0].times !== undefined ? { times: f.terms[0].times } : {}), ...(v.plus !== undefined ? { plus: v.plus } : {}) };
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
for (const axis of Object.keys(j.origin?.align ?? {})) {
|
|
1882
|
+
const how = j.origin.align[axis];
|
|
1883
|
+
if (how && typeof how === 'object' && Object.hasOwn(how, 'mm'))
|
|
1884
|
+
out.push({ at: `align.${axis}`, get: () => how.mm, set: v => (how.mm = v) });
|
|
1885
|
+
}
|
|
1886
|
+
return out;
|
|
1887
|
+
}
|
|
1888
|
+
/** The same measure with each term's source put through `to`, in the form it was written. */
|
|
1889
|
+
function mapMeasureSources(measure, to) {
|
|
1890
|
+
const f = followedTermsOf(measure);
|
|
1891
|
+
if (!f)
|
|
1892
|
+
return measure;
|
|
1893
|
+
if (f.sum)
|
|
1894
|
+
return { ...measure, sum: f.terms.map(t => ({ ...t, from: to(t.from) })) };
|
|
1895
|
+
return { ...measure, from: to(f.terms[0].from) };
|
|
1896
|
+
}
|
|
1897
|
+
/** Joints in an order each can be written in: its parent's joint and the joints it mimics first. */
|
|
1715
1898
|
function writingOrder(joints) {
|
|
1716
1899
|
const done = new Set();
|
|
1717
1900
|
const out = [];
|
|
@@ -1721,7 +1904,7 @@ function writingOrder(joints) {
|
|
|
1721
1904
|
if (done.has(j.id))
|
|
1722
1905
|
return false;
|
|
1723
1906
|
const up = 'part' in j.body0 ? byChild.get(j.body0.part) : undefined;
|
|
1724
|
-
return (!up || done.has(up.id)) && (
|
|
1907
|
+
return (!up || done.has(up.id)) && figureMimicLeadersOf(j).every(t => done.has(t.joint));
|
|
1725
1908
|
});
|
|
1726
1909
|
if (!ready.length)
|
|
1727
1910
|
fail('JOINT_TREE', 'joints', 'the joints do not make a tree');
|
|
@@ -1742,8 +1925,10 @@ function rewriteJoints(asset) {
|
|
|
1742
1925
|
2026-09-25). The names are made first, so every joint measures the same references.
|
|
1743
1926
|
*/
|
|
1744
1927
|
for (const j of joints)
|
|
1745
|
-
|
|
1746
|
-
|
|
1928
|
+
for (const { at, get } of jointMeasures(j))
|
|
1929
|
+
for (const t of followedTermsOf(get())?.terms ?? [])
|
|
1930
|
+
if (t?.from?.of === 'part')
|
|
1931
|
+
stableDimension(asset.document.model, t.from.part, t.from.axis, `joints.${j.id}.${at}`);
|
|
1747
1932
|
const order = writingOrder(joints);
|
|
1748
1933
|
const keep = { ...asset.stateDefaults };
|
|
1749
1934
|
/* What hangs from a row's part is drawn through the row; it is a part of its own again while the joints are written. */
|
|
@@ -1765,7 +1950,11 @@ function rewriteJoints(asset) {
|
|
|
1765
1950
|
* nodes are made from them (ADR-0093). Renaming or deleting a part in the graph alone left records naming a part that
|
|
1766
1951
|
* was no longer there, and the author could not save (TARGET_ABSENT on joints.*, 2026-09-25).
|
|
1767
1952
|
*/
|
|
1768
|
-
/**
|
|
1953
|
+
/**
|
|
1954
|
+
* The records follow a part's new name -- as a joint's parent, as its child, and as the joint a follower follows -- and
|
|
1955
|
+
* the nodes are made again from them. Nothing is frozen: a length that followed the part still follows it, and the
|
|
1956
|
+
* stored design inputs keep their values under the carried names, so the figure stands as it did at every size.
|
|
1957
|
+
*/
|
|
1769
1958
|
export function renameFigurePartInJoints(asset, from, to) {
|
|
1770
1959
|
const joints = asset.joints;
|
|
1771
1960
|
if (!joints?.length)
|
|
@@ -1790,14 +1979,23 @@ export function renameFigurePartInJoints(asset, from, to) {
|
|
|
1790
1979
|
if (out.occupancy)
|
|
1791
1980
|
out.occupancy = carried(out.occupancy);
|
|
1792
1981
|
out.designInputs = Object.fromEntries(Object.entries(out.designInputs ?? {}).map(([k, v]) => [carried(k), v]));
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1982
|
+
/* A length that follows the part's measure follows it under its new name, in each term of a sum too. */
|
|
1983
|
+
const source = (s) => (s?.of === 'part' ? { ...s, part: name(s.part) } : s);
|
|
1984
|
+
asset.joints = joints.map(was => {
|
|
1985
|
+
const j = structuredClone(was);
|
|
1986
|
+
for (const { get, set } of jointMeasures(j))
|
|
1987
|
+
set(mapMeasureSources(get(), source));
|
|
1988
|
+
return {
|
|
1989
|
+
...j,
|
|
1990
|
+
id: name(j.id),
|
|
1991
|
+
body0: 'part' in j.body0 ? { ...j.body0, part: name(j.body0.part) } : j.body0,
|
|
1992
|
+
body1: { ...j.body1, part: name(j.body1.part) },
|
|
1993
|
+
...(j.mimic
|
|
1994
|
+
? { mimic: Object.hasOwn(j.mimic, 'sum') ? { ...j.mimic, sum: figureMimicLeadersOf(j).map(t => ({ ...t, joint: name(t.joint) })) } : { ...j.mimic, joint: name(j.mimic.joint) } }
|
|
1995
|
+
: {}),
|
|
1996
|
+
...(j.state?.id !== undefined ? { state: { ...j.state, id: name(j.state.id) } } : {})
|
|
1997
|
+
};
|
|
1998
|
+
});
|
|
1801
1999
|
rewriteJoints(asset);
|
|
1802
2000
|
}
|
|
1803
2001
|
/**
|
|
@@ -1889,6 +2087,11 @@ export function releaseFigurePartJoints(asset, part, options = {}) {
|
|
|
1889
2087
|
const children = joints.filter(j => 'part' in j.body0 && j.body0.part === part);
|
|
1890
2088
|
if (own?.repeat && children.length)
|
|
1891
2089
|
fail('PART_ROW', part, `${children.map(j => j.body1.part).join(', ')} ${children.length > 1 ? 'hang' : 'hangs'} in the row ${part}, on every copy; take ${children.length > 1 ? 'them' : 'it'} off first`);
|
|
2090
|
+
/*
|
|
2091
|
+
Every number frozen below -- a follower's range and start, a measure that followed the part, a value read from it --
|
|
2092
|
+
is evaluated at the asset's stored design inputs and state defaults: the figure as it is authored, so its placement
|
|
2093
|
+
there is what it was. At another instance size the frozen numbers no longer follow.
|
|
2094
|
+
*/
|
|
1892
2095
|
const inputs = (over = {}) => ({ ...a.designInputs, ...a.stateDefaults, ...over });
|
|
1893
2096
|
const controls = m.inputs.filter((i) => i.role === 'state');
|
|
1894
2097
|
const rest = Object.fromEntries(controls.map((i) => [i.id, Math.min(i.max, Math.max(i.min, 0))]));
|
|
@@ -1899,47 +2102,78 @@ export function releaseFigurePartJoints(asset, part, options = {}) {
|
|
|
1899
2102
|
const going = new Set([...(own ? [own.id] : []), ...children.map(j => j.id)]);
|
|
1900
2103
|
/* A follower has no control of its own, so its own record's nominal id is not taken. */
|
|
1901
2104
|
const taken = new Set([...m.inputs.map((i) => i.id), ...joints.filter(j => !j.mimic).map(stateIdOf)]);
|
|
1902
|
-
for (const f of joints.filter(o => o.
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2105
|
+
for (const f of joints.filter(o => figureMimicLeadersOf(o).some(t => going.has(t.joint)) && !going.has(o.id))) {
|
|
2106
|
+
/*
|
|
2107
|
+
A sum whose leaders all go is a control of its own over what the sum came to, as one leader's follower is. One
|
|
2108
|
+
whose other leaders stay would follow them and stand still by the rest -- what it should then be is not said, so
|
|
2109
|
+
it is refused by name rather than made up.
|
|
2110
|
+
*/
|
|
2111
|
+
const staying = figureMimicLeadersOf(f).filter(t => !going.has(t.joint));
|
|
2112
|
+
if (staying.length)
|
|
2113
|
+
fail('PART_FOLLOWED', part, `${f.id} follows ${staying.map(t => t.joint).join(', ')} as well as a joint that goes with ${part}; make ${f.id} follow only the joints that stay first`);
|
|
2114
|
+
const terms = figureMimicLeadersOf(f).map(t => {
|
|
2115
|
+
const leader = joints.find(o => o.id === t.joint);
|
|
2116
|
+
const quantity = nodeById(m, `${leader.body1.part}.motion`)?.args[3] ?? stateIdOf(leader);
|
|
2117
|
+
const state = m.inputs.find((i) => i.id === stateIdOf(leader));
|
|
2118
|
+
const distance = (value) => Number(graph.evaluate(inputs({ [stateIdOf(leader)]: value })).values[quantity]);
|
|
2119
|
+
const [a, b] = [distance(state?.min ?? 0) * t.multiplier, distance(state?.max ?? 1) * t.multiplier];
|
|
2120
|
+
return { lo: Math.min(a, b), hi: Math.max(a, b), now: Number(now[quantity]) * t.multiplier };
|
|
2121
|
+
});
|
|
1908
2122
|
const turn = f.type === 'revolute';
|
|
1909
2123
|
const toUnit = (base) => round6(turn ? (base * 180) / Math.PI : base);
|
|
1910
2124
|
const offset = (f.mimic.offset ?? 0) * (turn ? Math.PI / 180 : 1);
|
|
1911
|
-
|
|
1912
|
-
|
|
2125
|
+
/*
|
|
2126
|
+
Each leader moves on its own control, so the sum's least and greatest are each term's, added (a negative multiplier
|
|
2127
|
+
swaps a term's ends, above). When leaders are driven by one upstream input -- not the case today, a leader leads
|
|
2128
|
+
itself -- the summed range is conservative: it holds every value the sum can take, not the exact reach.
|
|
2129
|
+
*/
|
|
2130
|
+
const total = (pick) => toUnit(terms.reduce((sum, t, i) => (i ? sum + pick(t) : pick(t)), 0) + offset);
|
|
2131
|
+
const [lo, hi] = [total(t => t.lo), total(t => t.hi)];
|
|
1913
2132
|
let id = f.body1.part;
|
|
1914
2133
|
for (let n = 2; taken.has(id); n++)
|
|
1915
2134
|
id = `${f.body1.part}-${n}`;
|
|
1916
2135
|
taken.add(id);
|
|
1917
|
-
const start = Math.min(hi, Math.max(lo,
|
|
2136
|
+
const start = Math.min(hi, Math.max(lo, total(t => t.now)));
|
|
1918
2137
|
const { mimic: _led, travel: _share, ...alone } = f;
|
|
1919
2138
|
joints = joints.map(o => o.id === f.id ? { ...alone, lowerLimit: lo, upperLimit: hi, state: { ...(f.state ?? {}), id, start } } : o);
|
|
1920
2139
|
notice.controls.push({ part: f.body1.part, control: id, min: lo, max: hi });
|
|
1921
2140
|
}
|
|
1922
|
-
/*
|
|
2141
|
+
/*
|
|
2142
|
+
A value in a record that followed the part: the number it is now -- at the stored design inputs (above). In a sum, each term that reads the part becomes
|
|
2143
|
+
its number, added to the margin, and the terms that read something else still follow; no term left, the number.
|
|
2144
|
+
*/
|
|
1923
2145
|
const freezeMeasure = (measure, where) => {
|
|
1924
|
-
|
|
2146
|
+
const f = followedTermsOf(measure);
|
|
2147
|
+
if (!f || !f.terms.some(t => t.from?.of === 'part' && t.from.part === part))
|
|
1925
2148
|
return measure;
|
|
1926
2149
|
const probe = structuredClone(m);
|
|
1927
|
-
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
2150
|
+
let frozen = 0;
|
|
2151
|
+
const rest = [];
|
|
2152
|
+
for (const t of f.terms) {
|
|
2153
|
+
if (!(t.from.of === 'part' && t.from.part === part)) {
|
|
2154
|
+
rest.push(t);
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const ref = stableDimension(probe, part, t.from.axis, where);
|
|
2158
|
+
frozen += Number(compileFigureGraph(probe).evaluate(inputs()).values[ref]) * (t.times ?? 1);
|
|
2159
|
+
}
|
|
2160
|
+
if (!rest.length) {
|
|
2161
|
+
const value = round6(frozen + f.plus);
|
|
2162
|
+
notice.fixed.push({ part: where, value, unit: 'mm' });
|
|
2163
|
+
return value;
|
|
2164
|
+
}
|
|
2165
|
+
notice.fixed.push({ part: where, value: round6(frozen), unit: 'mm' });
|
|
2166
|
+
return { sum: rest, ...(round6(frozen + f.plus) ? { plus: round6(frozen + f.plus) } : {}) };
|
|
1931
2167
|
};
|
|
1932
2168
|
joints = joints.map(j => {
|
|
1933
2169
|
if (j.id === own?.id || children.includes(j))
|
|
1934
2170
|
return j;
|
|
1935
2171
|
const out = structuredClone(j);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
const plus = freezeMeasure({ from: out.travel.source, times: out.travel.times, plus: out.travel.plus }, j.body1.part);
|
|
1942
|
-
out.travel = { plus };
|
|
2172
|
+
for (const { get, set } of jointMeasures(out)) {
|
|
2173
|
+
const was = get();
|
|
2174
|
+
const fixed = freezeMeasure(was, j.body1.part);
|
|
2175
|
+
if (fixed !== was)
|
|
2176
|
+
set(fixed);
|
|
1943
2177
|
}
|
|
1944
2178
|
return out;
|
|
1945
2179
|
});
|
|
@@ -2152,7 +2386,7 @@ function setJoint(asset, joint) {
|
|
|
2152
2386
|
j.origin = { ...(j.origin ?? {}), turn: Object.fromEntries(AXES.map((a, i) => [a, turn[i]])) };
|
|
2153
2387
|
}
|
|
2154
2388
|
const joints = (asset.joints ?? []).filter(o => o.id !== j.id);
|
|
2155
|
-
const followers = joints.filter(o => o.
|
|
2389
|
+
const followers = joints.filter(o => figureMimicLeadersOf(o).some(t => t.joint === j.id));
|
|
2156
2390
|
if (followers.length && j.type === 'fixed')
|
|
2157
2391
|
fail('JOINT_SCHEMA', `joints.${j.id}`, `${followers.map(f => f.id).join(', ')} follow ${j.id}; it cannot stop moving`);
|
|
2158
2392
|
asset.joints = [...joints, j];
|
|
@@ -2213,7 +2447,7 @@ function removeJoint(asset, id) {
|
|
|
2213
2447
|
const j = joints.find(o => o.id === id);
|
|
2214
2448
|
if (!j)
|
|
2215
2449
|
fail('EDIT_TARGET', `joints.${id}`, `there is no joint ${id}`);
|
|
2216
|
-
const followers = joints.filter(o => o.
|
|
2450
|
+
const followers = joints.filter(o => figureMimicLeadersOf(o).some(t => t.joint === id));
|
|
2217
2451
|
if (followers.length)
|
|
2218
2452
|
fail('JOINT_SCHEMA', `joints.${id}`, `${followers.map(f => f.id).join(', ')} follow ${id}; remove them first`);
|
|
2219
2453
|
const m = asset.document.model;
|
|
@@ -2315,10 +2549,28 @@ function addMotion(asset, a, follow, sign = 1) {
|
|
|
2315
2549
|
const name = namer(m, `${a.part}.motion`);
|
|
2316
2550
|
let quantity = st?.id;
|
|
2317
2551
|
if (follow) {
|
|
2318
|
-
/*
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2552
|
+
/*
|
|
2553
|
+
A mimic: the leader's value times k plus c, and no control of its own. A sum is each leader's value times its own
|
|
2554
|
+
k, added up, then c -- written with the same mul@1 and add@1 a one-leader mimic is, so a pose step runs it as it
|
|
2555
|
+
runs that (ADR-0108), and the gate reads it by the same operators.
|
|
2556
|
+
*/
|
|
2557
|
+
if (!follow.sum) {
|
|
2558
|
+
const scaledId = name('mimic');
|
|
2559
|
+
m.nodes.push({ id: scaledId, op: 'mul@1', args: [follow.terms[0].of, constantOf(m, 'ratio', follow.terms[0].k, 'k')], outputs: { value: `${scaledId}.value` } });
|
|
2560
|
+
quantity = `${scaledId}.value`;
|
|
2561
|
+
}
|
|
2562
|
+
else
|
|
2563
|
+
for (const [i, t] of follow.terms.entries()) {
|
|
2564
|
+
const scaledId = name(`mimic.${i}`);
|
|
2565
|
+
m.nodes.push({ id: scaledId, op: 'mul@1', args: [t.of, constantOf(m, 'ratio', t.k, 'k')], outputs: { value: `${scaledId}.value` } });
|
|
2566
|
+
if (!i)
|
|
2567
|
+
quantity = `${scaledId}.value`;
|
|
2568
|
+
else {
|
|
2569
|
+
const added = name(`mimic.${i}.sum`);
|
|
2570
|
+
m.nodes.push({ id: added, op: 'add@1', args: [quantity, `${scaledId}.value`], outputs: { value: `${added}.value` } });
|
|
2571
|
+
quantity = `${added}.value`;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2322
2574
|
if (follow.c) {
|
|
2323
2575
|
const plus = name('mimic.offset');
|
|
2324
2576
|
/* The offset is in the follower's own unit: millimetres for a slide, degrees for a turn. */
|
|
@@ -2375,8 +2627,16 @@ function addMotion(asset, a, follow, sign = 1) {
|
|
|
2375
2627
|
if (!a.travel)
|
|
2376
2628
|
fail('SCHEMA', path, 'a ratio control needs travel: how far the part goes at 1');
|
|
2377
2629
|
const travel = a.travel;
|
|
2378
|
-
|
|
2379
|
-
|
|
2630
|
+
let length;
|
|
2631
|
+
if (Object.hasOwn(travel, 'sum')) {
|
|
2632
|
+
/* A stroke that is a sum of lengths: the terms added, then the margin, as any followed length (ADR-0107). */
|
|
2633
|
+
const plus = travel.plus ?? 0;
|
|
2634
|
+
length = scaled(m, name, sumOf(m, name, termRefs(m, asset, travel.sum, plus, `${path}.travel`), 'travel.sum'), 1, plus, 'travel');
|
|
2635
|
+
}
|
|
2636
|
+
else {
|
|
2637
|
+
const base = travel.source ? sourceRef(m, asset, travel.source, path) : constantOf(m, 'mm', travel.plus ?? 0, 'd');
|
|
2638
|
+
length = travel.source ? scaled(m, name, base, travel.times ?? 1, travel.plus ?? 0, 'travel') : base;
|
|
2639
|
+
}
|
|
2380
2640
|
const id = name('distance');
|
|
2381
2641
|
m.nodes.push({ id, op: 'mul@1', args: [quantity, length], outputs: { value: `${id}.value` } });
|
|
2382
2642
|
quantity = `${id}.value`;
|