@hatiolab/figure-model 0.9.6 → 0.11.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/CHANGELOG.md +41 -5
- package/dist/asset-types.d.ts +24 -56
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/asset-types.js.map +1 -1
- package/dist/asset.d.ts +0 -7
- package/dist/asset.d.ts.map +1 -1
- package/dist/asset.js +11 -45
- package/dist/asset.js.map +1 -1
- package/dist/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +5 -27
- package/dist/authoring-actions.js.map +1 -1
- package/dist/authoring-bindings.js +1 -1
- package/dist/authoring-bindings.js.map +1 -1
- package/dist/authoring-dimensions.js +3 -3
- package/dist/authoring-fastening.d.ts +16 -2
- package/dist/authoring-fastening.d.ts.map +1 -1
- package/dist/authoring-fastening.js +106 -47
- package/dist/authoring-fastening.js.map +1 -1
- package/dist/authoring-nodes.d.ts +4 -4
- package/dist/authoring-nodes.d.ts.map +1 -1
- package/dist/authoring-nodes.js +7 -7
- package/dist/authoring-nodes.js.map +1 -1
- package/dist/authoring-placement.d.ts +40 -0
- package/dist/authoring-placement.d.ts.map +1 -0
- package/dist/{authoring-occupancy.js → authoring-placement.js} +225 -238
- package/dist/authoring-placement.js.map +1 -0
- package/dist/authoring-proportion.d.ts.map +1 -1
- package/dist/authoring-proportion.js +28 -12
- package/dist/authoring-proportion.js.map +1 -1
- package/dist/authoring-size.js +1 -1
- package/dist/authoring-types.d.ts +57 -91
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/authoring-types.js +8 -8
- package/dist/authoring-types.js.map +1 -1
- package/dist/body.d.ts +19 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +37 -0
- package/dist/body.js.map +1 -0
- package/dist/clip-keeping.d.ts.map +1 -1
- package/dist/clip-keeping.js +7 -5
- package/dist/clip-keeping.js.map +1 -1
- package/dist/command-preview.js +4 -4
- package/dist/command-preview.js.map +1 -1
- package/dist/contract.d.ts +0 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/editor-workspace.js +1 -1
- package/dist/editor-workspace.js.map +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +7 -8
- package/dist/examples.js.map +1 -1
- package/dist/face-overhang.js +2 -2
- package/dist/face-overhang.js.map +1 -1
- package/dist/fasten-place.d.ts +18 -0
- package/dist/fasten-place.d.ts.map +1 -1
- package/dist/fasten-place.js +43 -7
- package/dist/fasten-place.js.map +1 -1
- package/dist/gate.d.ts +15 -32
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +34 -101
- package/dist/gate.js.map +1 -1
- package/dist/graph-types.d.ts +2 -2
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +6 -67
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/joint-fields.d.ts +6 -8
- package/dist/joint-fields.d.ts.map +1 -1
- package/dist/joint-fields.js +6 -6
- package/dist/joint-fields.js.map +1 -1
- package/dist/joint-list.d.ts +11 -9
- package/dist/joint-list.d.ts.map +1 -1
- package/dist/joint-list.js +75 -53
- package/dist/joint-list.js.map +1 -1
- package/dist/joint-motions.d.ts +30 -0
- package/dist/joint-motions.d.ts.map +1 -0
- package/dist/joint-motions.js +28 -0
- package/dist/joint-motions.js.map +1 -0
- package/dist/joint-part-edits.d.ts +1 -10
- package/dist/joint-part-edits.d.ts.map +1 -1
- package/dist/joint-part-edits.js +101 -98
- package/dist/joint-part-edits.js.map +1 -1
- package/dist/joint-writing.d.ts +3 -4
- package/dist/joint-writing.d.ts.map +1 -1
- package/dist/joint-writing.js +229 -170
- package/dist/joint-writing.js.map +1 -1
- 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/part-edit.d.ts +1 -8
- package/dist/part-edit.d.ts.map +1 -1
- package/dist/part-edit.js +19 -34
- package/dist/part-edit.js.map +1 -1
- package/dist/proposal.d.ts +4 -11
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +6 -13
- package/dist/proposal.js.map +1 -1
- package/dist/recipe-from-parts.d.ts.map +1 -1
- package/dist/recipe-from-parts.js +12 -4
- package/dist/recipe-from-parts.js.map +1 -1
- package/dist/recipe.d.ts.map +1 -1
- package/dist/recipe.js +11 -9
- 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 +33 -30
- package/dist/release-contract.js.map +1 -1
- package/dist/release-metrics.d.ts +14 -14
- package/dist/release-metrics.d.ts.map +1 -1
- package/dist/release-metrics.js +13 -29
- package/dist/release-metrics.js.map +1 -1
- package/dist/resize-plan.d.ts +0 -5
- package/dist/resize-plan.d.ts.map +1 -1
- package/dist/resize-plan.js +4 -29
- package/dist/resize-plan.js.map +1 -1
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +44 -48
- package/dist/robots.js.map +1 -1
- package/dist/size-edit.js +3 -3
- package/dist/size-edit.js.map +1 -1
- package/dist/surface.js +2 -2
- package/docs/asset-persistence.md +1 -1
- package/docs/attachment-response-spec-review-2026-10-24.md +56 -0
- package/docs/document-contract.md +0 -1
- package/docs/editor-integration.md +2 -0
- package/docs/modeller-interaction-performance-2026-10-07.md +1 -1
- package/docs/operator-contracts.md +3 -18
- package/docs/performance-followup-2026-10-05.md +1 -1
- package/docs/review-2026-09-22.md +2 -2
- package/docs/shape-dimension-contract.md +27 -25
- package/docs/status.md +12 -2
- package/package.json +1 -1
- package/dist/authoring-occupancy.d.ts +0 -28
- package/dist/authoring-occupancy.d.ts.map +0 -1
- package/dist/authoring-occupancy.js.map +0 -1
- package/dist/room.d.ts +0 -53
- package/dist/room.d.ts.map +0 -1
- package/dist/room.js +0 -101
- package/dist/room.js.map +0 -1
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
/*
|
|
5
|
-
* The room
|
|
5
|
+
* The room a figure sweeps as it moves (ADR-0145 2.3), worked out from its parts and joints; where it is placed from, and
|
|
6
|
+
* the face it is mounted by.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { figurePlacementOf } from "./asset.js";
|
|
8
9
|
import { compileFigureGraph } from "./graph.js";
|
|
9
10
|
import { figureShapeDescriptionOf } from "./shape-ops.js";
|
|
10
11
|
import { FigureContractError } from "./contract.js";
|
|
@@ -13,31 +14,7 @@ import { FIGURE_PLACEMENTS } from "./asset-types.js";
|
|
|
13
14
|
import { FIGURE_FACES } from "./authoring-types.js";
|
|
14
15
|
import { fail, nodeById, writerOf, throughSums, rowHolding, partOf, namer, constantOf, dropIfUnused, parentOf, round6, stringsIn } from "./authoring-nodes.js";
|
|
15
16
|
import { isReach, sizeRefsOf, occupiedExtentsOf, angleOf, turnMatrix, mul3, standingRefsOf, half, axisTurn, offCentreEndsOf, IDENTITY } from "./authoring-geometry.js";
|
|
16
|
-
|
|
17
|
-
function fingerprintOf(asset) {
|
|
18
|
-
/*
|
|
19
|
-
The room and the placement are left out: neither changes what the room would measure (ADR-0097). Nor do the clips,
|
|
20
|
-
their groups and the data connections -- the room is measured over the joints' ranges, not what plays them -- and a
|
|
21
|
-
command is applied with the groups and their clips held aside (clip-keeping.ts): counted, a room proposed on the
|
|
22
|
-
figure as it is was stale against the same figure without them, and a figure with a motion group could not declare
|
|
23
|
-
its room again (the robot dog, 2026-10-19).
|
|
24
|
-
*/
|
|
25
|
-
const text = JSON.stringify({
|
|
26
|
-
...asset,
|
|
27
|
-
placement: undefined,
|
|
28
|
-
occupancy: asset.occupancy ? { ...asset.occupancy, bounds: null } : undefined,
|
|
29
|
-
drivers: undefined,
|
|
30
|
-
motionGroups: undefined,
|
|
31
|
-
dataInterface: undefined
|
|
32
|
-
});
|
|
33
|
-
let h1 = 0x811c9dc5;
|
|
34
|
-
let h2 = 0x01000193;
|
|
35
|
-
for (let i = 0; i < text.length; i++) {
|
|
36
|
-
h1 = Math.imul(h1 ^ text.charCodeAt(i), 0x01000193) >>> 0;
|
|
37
|
-
h2 = Math.imul(h2 + text.charCodeAt(i), 0x85ebca6b) >>> 0;
|
|
38
|
-
}
|
|
39
|
-
return `${h1.toString(16).padStart(8, '0')}${h2.toString(16).padStart(8, '0')}:${text.length}`;
|
|
40
|
-
}
|
|
17
|
+
import { motionStagesOf } from "./authoring-fastening.js";
|
|
41
18
|
const linAdd = (a, b, k = 1) => ({
|
|
42
19
|
terms: [...a.terms, ...b.terms.map(t => ({ ref: t.ref, k: t.k * k }))],
|
|
43
20
|
c: a.c + b.c * k
|
|
@@ -60,14 +37,74 @@ const linOf = (m, ref) => {
|
|
|
60
37
|
const constant = m.constants.find((c) => c.id === ref);
|
|
61
38
|
return constant ? { terms: [], c: constant.value } : { terms: [{ ref, k: 1 }], c: 0 };
|
|
62
39
|
};
|
|
40
|
+
/** Most controls a part's slides are read over corner by corner: 2^n corners, so a bound is kept on n. */
|
|
41
|
+
const CORNER_CONTROLS = 10;
|
|
42
|
+
/** The state inputs a value reads, through whatever writes it. */
|
|
43
|
+
function statesReaching(m, ref, out = new Set(), depth = 0) {
|
|
44
|
+
if (m.inputs.some((i) => i.id === ref && i.role === 'state'))
|
|
45
|
+
out.add(ref);
|
|
46
|
+
const w = depth < 256 ? writerOf(m, ref) : undefined;
|
|
47
|
+
for (const a of w?.args ?? [])
|
|
48
|
+
if (typeof a === 'string')
|
|
49
|
+
statesReaching(m, a, out, depth + 1);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every corner of the controls that slide a part or what carries it: each control at its least or its greatest, apart
|
|
54
|
+
* from the others. Two slides that pull one way and the other reach furthest with one at each end, which all-least
|
|
55
|
+
* and all-greatest miss (review 2026-10-24, F1: a block 70 mm out of the room it was given). A control two slides read
|
|
56
|
+
* takes one end for both. More controls than `CORNER_CONTROLS` are too many corners to read one by one: they are
|
|
57
|
+
* given back to be bounded apart (`wideReadingOf`).
|
|
58
|
+
*/
|
|
59
|
+
function cornersOf(m, id) {
|
|
60
|
+
const states = new Set();
|
|
61
|
+
for (let part = id; part; part = parentOf(m, part))
|
|
62
|
+
for (const { motion } of motionStagesOf(m, part))
|
|
63
|
+
if (motion.op === 'axis-slide@1')
|
|
64
|
+
statesReaching(m, motion.args[3], states);
|
|
65
|
+
const list = [...states].sort();
|
|
66
|
+
if (list.length > CORNER_CONTROLS)
|
|
67
|
+
return { many: list };
|
|
68
|
+
if (list.length <= 1)
|
|
69
|
+
return ['min', 'max'];
|
|
70
|
+
return Array.from({ length: 2 ** list.length }, (_, n) => Object.fromEntries(list.map((s, i) => [s, (n >> i) & 1 ? 'max' : 'min'])));
|
|
71
|
+
}
|
|
72
|
+
/** Said where too many controls slide a part for this reading to go corner by corner. */
|
|
73
|
+
const tooManyCorners = (id, many) => `${id} is slid by ${many.length} controls; this reading goes over at most ${CORNER_CONTROLS} of them corner by corner`;
|
|
74
|
+
/**
|
|
75
|
+
* Where a part slid by more controls than its corners can be read over is on one axis: a bound, never a sample (review
|
|
76
|
+
* 2026-10-24, F5). Its centre is linear in each control apart -- a slide's travel is its control times a length, and a
|
|
77
|
+
* follower's a sum of its leaders' -- so from every control at its least, each control taken to its greatest moves it
|
|
78
|
+
* by a sum d; wherever the controls stand, it is within the sum of every |d| either way of that. |d| is read as sizes,
|
|
79
|
+
* which are never below zero; a d that is not a sum of sizes cannot be bounded and is said.
|
|
80
|
+
*/
|
|
81
|
+
function wideReadingOf(m, asset, id, axis, states) {
|
|
82
|
+
const least = Object.fromEntries(states.map(s => [s, 'min']));
|
|
83
|
+
const from = centreLin(m, asset, id, axis, least);
|
|
84
|
+
if (typeof from === 'string')
|
|
85
|
+
return from;
|
|
86
|
+
let spread = { terms: [], c: 0 };
|
|
87
|
+
for (const s of states) {
|
|
88
|
+
const moved = centreLin(m, asset, id, axis, { ...least, [s]: 'max' });
|
|
89
|
+
if (typeof moved === 'string')
|
|
90
|
+
return moved;
|
|
91
|
+
const size = magnitudeOf(m, linAdd(moved, from, -1));
|
|
92
|
+
if (!size)
|
|
93
|
+
return `${id}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
94
|
+
spread = linAdd(spread, size);
|
|
95
|
+
}
|
|
96
|
+
return [linAdd(from, spread, -1), linAdd(from, spread)];
|
|
97
|
+
}
|
|
63
98
|
/**
|
|
64
99
|
* How far a slide has gone when its control is at one end of its range, as a sum: each control at that end, what the
|
|
65
100
|
* figure's sizes make of the rest. A mimic of a leader whose stroke follows a length is the leader's ratio times that
|
|
66
101
|
* length times its multiplier -- read through, not refused: the telescopic conveyor's second and third stages were
|
|
67
|
-
* left out of
|
|
102
|
+
* left out of the room it was given, 2600 and 5200 mm past it (2026-09-25).
|
|
68
103
|
*/
|
|
69
104
|
function travelAt(m, asset, motion, bound) {
|
|
70
|
-
const pick = (i) =>
|
|
105
|
+
const pick = (i) => typeof bound === 'object'
|
|
106
|
+
? (bound[i.id] === 'min' ? i.min : i.max)
|
|
107
|
+
: bound === 'start' ? (asset.stateDefaults[i.id] ?? i.min) : bound === 'min' ? i.min : i.max;
|
|
71
108
|
const constant = (r) => m.constants.find((c) => c.id === r)?.value;
|
|
72
109
|
const stateOf = (r) => m.inputs.find((i) => i.id === r && i.role === 'state');
|
|
73
110
|
/* A value no control reaches: it is the same at either end, so it stays a term of the sum. */
|
|
@@ -130,6 +167,8 @@ function poseTurnOf(m, asset, part) {
|
|
|
130
167
|
return null;
|
|
131
168
|
return turn.every(isQuarter) ? turnMatrix(turn) : 'other';
|
|
132
169
|
}
|
|
170
|
+
/** Whether any axis a part moves on turns (ADR-0159: a part may move on several). */
|
|
171
|
+
const turnsOnAJoint = (m, part) => motionStagesOf(m, part).some(s => s.motion.op === 'axis-turn@1');
|
|
133
172
|
/** Whether a part's seat is turned: its joint frame turned by `origin.parentRotate` (ADR-0107). */
|
|
134
173
|
const seatTurned = (m, asset, part) => (nodeById(m, `${part}.seat.rigid`)?.args.slice(3, 6) ?? []).some((ref) => angleOf(m, asset, ref) !== 0);
|
|
135
174
|
/**
|
|
@@ -138,7 +177,7 @@ const seatTurned = (m, asset, part) => (nodeById(m, `${part}.seat.rigid`)?.args.
|
|
|
138
177
|
*/
|
|
139
178
|
function carriedByATurn(m, asset, id) {
|
|
140
179
|
for (let part = id; part; part = parentOf(m, part)) {
|
|
141
|
-
if (
|
|
180
|
+
if (turnsOnAJoint(m, part))
|
|
142
181
|
return true;
|
|
143
182
|
if (poseTurnOf(m, asset, part) === 'other' || seatTurned(m, asset, part))
|
|
144
183
|
return true;
|
|
@@ -178,8 +217,8 @@ function centreVec(m, asset, id, bound, from) {
|
|
|
178
217
|
}
|
|
179
218
|
for (const [i, a] of AXES.entries())
|
|
180
219
|
v[a] = linAdd(v[a], linOf(m, pose.args[i]));
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
/* Slides add up in any order, and a slide along a line through a point is the slide: the point is not read. */
|
|
221
|
+
for (const { motion } of motionStagesOf(m, part)) {
|
|
183
222
|
if (motion.op !== 'axis-slide@1')
|
|
184
223
|
return `${part} turns, so its reach is not a box this command can add up`;
|
|
185
224
|
const unit = motion.args
|
|
@@ -383,31 +422,6 @@ function boundedMagnitudeOf(m, ref, inside = false) {
|
|
|
383
422
|
return linAdd({ terms: [], c: 0 }, linAdd(each[0], each[1]), 0.5);
|
|
384
423
|
return each.reduce((a, b) => linAtLeast(a, b));
|
|
385
424
|
}
|
|
386
|
-
/** The values held by bounds a part's place reads, up what carries it: its room may be wider than it there. */
|
|
387
|
-
function boundsUnder(m, id) {
|
|
388
|
-
const found = new Set();
|
|
389
|
-
for (let part = id; part; part = parentOf(m, part))
|
|
390
|
-
for (const node of [nodeById(m, `${part}.pose`), nodeById(m, `${part}.seat.rigid`)])
|
|
391
|
-
for (const ref of (node?.args ?? []).slice(0, 3))
|
|
392
|
-
for (const t of throughSums(m, linOf(m, ref)).terms)
|
|
393
|
-
if (isBounded(m, t.ref))
|
|
394
|
-
found.add(t.ref);
|
|
395
|
-
return [...found];
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* How much wider than a bounded value its room may be, at the figure's values, in mm: the greatest of its bound over
|
|
399
|
-
* the value -- (x + y) / 2 − √(x·y) for a root, r_i − |p_i| for a point of an ellipsoid (chief architect 2026-10-20).
|
|
400
|
-
*/
|
|
401
|
-
function boundedWidthOf(m, values, refs) {
|
|
402
|
-
let most = 0;
|
|
403
|
-
for (const ref of refs) {
|
|
404
|
-
const kind = boundedKind(m, ref);
|
|
405
|
-
const v = Math.abs(values[ref] ?? 0);
|
|
406
|
-
const wide = 'root' in kind ? (values[kind.root[0]] + values[kind.root[1]]) / 2 - v : Math.abs(values[kind.point.radius]) - v;
|
|
407
|
-
most = Math.max(most, wide);
|
|
408
|
-
}
|
|
409
|
-
return Math.round(most * 10) / 10;
|
|
410
|
-
}
|
|
411
425
|
/** Every low candidate bounded from below and every high one from above, so the room holds whatever they come to. */
|
|
412
426
|
function boundedSides(m, low, high) {
|
|
413
427
|
for (const axis of AXES) {
|
|
@@ -418,29 +432,38 @@ function boundedSides(m, low, high) {
|
|
|
418
432
|
const transposedTimes = (r, d) => [0, 1, 2].map(j => r[0][j] * d[0] + r[1][j] * d[1] + r[2][j] * d[2]);
|
|
419
433
|
/** A turn as `rigid@1` writes it, in degrees about X, Y and Z: Rx·Ry·Rz, at any angle (`turnMatrix` takes quarter turns). */
|
|
420
434
|
const rigidTurnOf = ([x, y, z]) => mul3(mul3(axisTurn('x', x), axisTurn('y', y)), axisTurn('z', z));
|
|
435
|
+
/** Each component's largest size, which a unit direction never passes. */
|
|
436
|
+
const largestOf = (d) => d.mid.map((v, i) => Math.min(1, Math.abs(v) + d.spread[i]));
|
|
437
|
+
/** The same directions, each component held to [−1, 1]. */
|
|
438
|
+
function heldToUnit(mid, spread) {
|
|
439
|
+
const lo = mid.map((v, i) => Math.max(-1, v - spread[i]));
|
|
440
|
+
const hi = mid.map((v, i) => Math.min(1, v + spread[i]));
|
|
441
|
+
return { mid: lo.map((l, i) => (l + hi[i]) / 2), spread: lo.map((l, i) => (hi[i] - l) / 2) };
|
|
442
|
+
}
|
|
421
443
|
/** The directions seen from a frame turned by `r` from this one (a point there is r·x here). */
|
|
422
444
|
function turnedBy(d, r) {
|
|
423
|
-
|
|
424
|
-
return { one: transposedTimes(r, d.one) };
|
|
425
|
-
return { most: [0, 1, 2].map(j => Math.min(1, d.most.reduce((sum, v, i) => sum + Math.abs(r[i][j]) * v, 0))) };
|
|
445
|
+
return heldToUnit(transposedTimes(r, d.mid), [0, 1, 2].map(j => d.spread.reduce((sum, v, i) => sum + Math.abs(r[i][j]) * v, 0)));
|
|
426
446
|
}
|
|
427
|
-
/** The
|
|
428
|
-
function
|
|
447
|
+
/** The least and the greatest of a + b cos t + c sin t for t from lo to hi, in radians; all the way round without a range. */
|
|
448
|
+
function rangeOver(a, b, c, range) {
|
|
429
449
|
if (!range)
|
|
430
|
-
return Math.
|
|
450
|
+
return [a - Math.hypot(b, c), a + Math.hypot(b, c)];
|
|
431
451
|
const [lo, hi] = range;
|
|
432
|
-
const at = (t) =>
|
|
433
|
-
let most = Math.max(at(lo), at(hi));
|
|
452
|
+
const at = (t) => a + b * Math.cos(t) + c * Math.sin(t);
|
|
453
|
+
let least = Math.min(at(lo), at(hi)), most = Math.max(at(lo), at(hi));
|
|
434
454
|
/* Where its slope is zero: t = atan2(c, b) and half a turn from it, every whole turn over. */
|
|
435
455
|
const turn = 2 * Math.PI;
|
|
436
456
|
for (const t0 of [Math.atan2(c, b), Math.atan2(c, b) + Math.PI])
|
|
437
|
-
for (let t = t0 + Math.ceil((lo - t0) / turn) * turn; t <= hi; t += turn)
|
|
457
|
+
for (let t = t0 + Math.ceil((lo - t0) / turn) * turn; t <= hi; t += turn) {
|
|
458
|
+
least = Math.min(least, at(t));
|
|
438
459
|
most = Math.max(most, at(t));
|
|
439
|
-
|
|
460
|
+
}
|
|
461
|
+
return [least, most];
|
|
440
462
|
}
|
|
441
463
|
/**
|
|
442
464
|
* The directions seen from a frame turned about the unit axis `u` by any angle in `range` (radians; null all the way
|
|
443
|
-
* round): R(t) = u uᵀ + (I − u uᵀ) cos t + [u]× sin t, each entry a + b cos t + c sin t.
|
|
465
|
+
* round): R(t) = u uᵀ + (I − u uᵀ) cos t + [u]× sin t, each entry a + b cos t + c sin t. The middle is turned through
|
|
466
|
+
* the range exactly; the spread about it by each entry's largest.
|
|
444
467
|
*/
|
|
445
468
|
function sweptBy(d, u, range) {
|
|
446
469
|
const K = [
|
|
@@ -455,18 +478,13 @@ function sweptBy(d, u, range) {
|
|
|
455
478
|
const t = range[0];
|
|
456
479
|
return turnedBy(d, [0, 1, 2].map(i => [0, 1, 2].map(j => A(i, j) + B(i, j) * Math.cos(t) + C(i, j) * Math.sin(t))));
|
|
457
480
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return { most: parts.map(([a, b, c]) => Math.min(1, largestOver(a, b, c, range))) };
|
|
466
|
-
}
|
|
467
|
-
return {
|
|
468
|
-
most: [0, 1, 2].map(j => Math.min(1, d.most.reduce((s, v, i) => s + largestOver(A(i, j), B(i, j), C(i, j), range) * v, 0)))
|
|
469
|
-
};
|
|
481
|
+
const sum = (f, j) => [0, 1, 2].reduce((s, i) => s + f(i, j) * d.mid[i], 0);
|
|
482
|
+
const ends = [0, 1, 2].map(j => rangeOver(sum(A, j), sum(B, j), sum(C, j), range));
|
|
483
|
+
const spread = [0, 1, 2].map(j => d.spread.reduce((s, v, i) => {
|
|
484
|
+
const [l, h] = rangeOver(A(i, j), B(i, j), C(i, j), range);
|
|
485
|
+
return s + Math.max(Math.abs(l), Math.abs(h)) * v;
|
|
486
|
+
}, 0));
|
|
487
|
+
return heldToUnit(ends.map(([l, h]) => (l + h) / 2), ends.map(([l, h], j) => (h - l) / 2 + spread[j]));
|
|
470
488
|
}
|
|
471
489
|
/**
|
|
472
490
|
* The angles a turn's control can give it, in degrees: its limits, scaled and moved as the angle is written from it.
|
|
@@ -512,19 +530,19 @@ function angleRangeOf(m, ref, depth = 0) {
|
|
|
512
530
|
}
|
|
513
531
|
/**
|
|
514
532
|
* Where a part carried by a turn can reach, on each side of each axis, from the point the first turn above it turns
|
|
515
|
-
* about (ADR-0087, amendments 2026-10-21). Down the chain from there each step adds what it can add that way:
|
|
516
|
-
* the directions
|
|
517
|
-
*
|
|
518
|
-
* adds its own
|
|
533
|
+
* about (ADR-0087, amendments 2026-10-21). Down the chain from there each step adds what it can add that way: the
|
|
534
|
+
* middle of the directions times the step, sign included, and their spread -- none until a joint has turned them
|
|
535
|
+
* through its range -- times its size; no more than its length, when its coordinates are numbers. A slide from nothing
|
|
536
|
+
* to a sum of sizes adds only toward the way it goes. The part adds its own extent the same way. Each step's largest taken on its own (the triangle inequality), it holds at
|
|
519
537
|
* every angle the joints can reach and every design size: a bound, not a sample. Read by the proposal; the release
|
|
520
538
|
* gate still checks the parts against it.
|
|
521
539
|
*/
|
|
522
|
-
function reachOf(m, asset, id
|
|
540
|
+
function reachOf(m, asset, id) {
|
|
523
541
|
const chain = [];
|
|
524
542
|
for (let part = id; part; part = parentOf(m, part))
|
|
525
543
|
chain.push(part);
|
|
526
544
|
/* The topmost part that turns on a joint, or stands turned by an angle a box cannot follow (a fan's blade at 120°). */
|
|
527
|
-
const top = chain.reduce((found, part, i) =>
|
|
545
|
+
const top = chain.reduce((found, part, i) => turnsOnAJoint(m, part) ||
|
|
528
546
|
poseTurnOf(m, asset, part) === 'other' ||
|
|
529
547
|
seatTurned(m, asset, part)
|
|
530
548
|
? i
|
|
@@ -544,34 +562,29 @@ function reachOf(m, asset, id, ends) {
|
|
|
544
562
|
const constants = (refs) => refs.map(r => m.constants.find((c) => c.id === r)?.value);
|
|
545
563
|
const angles = (refs) => refs.map(r => angleOf(m, asset, r));
|
|
546
564
|
const reachToward = (e) => {
|
|
547
|
-
let d = {
|
|
565
|
+
let d = { mid: transposedTimes(frame, e), spread: [0, 0, 0] };
|
|
548
566
|
let out = { terms: [], c: 0 };
|
|
549
|
-
const
|
|
550
|
-
/* A step of three coordinates, in the frame the directions are seen from. */
|
|
567
|
+
const tiny = (v) => Math.abs(v) < 1e-12;
|
|
568
|
+
/* A step of three coordinates, in the frame the directions are seen from: the middle times it, the spread times its size. */
|
|
551
569
|
const step = (refs, what) => {
|
|
552
|
-
if ('one' in d) {
|
|
553
|
-
const v = d.one;
|
|
554
|
-
refs.forEach((ref, i) => {
|
|
555
|
-
if (Math.abs(v[i]) > 1e-12)
|
|
556
|
-
out = linAdd(out, linOf(m, ref), v[i]);
|
|
557
|
-
});
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
const w = d.most;
|
|
561
570
|
const numbers = constants(refs);
|
|
562
571
|
if (numbers.every(n => n !== undefined)) {
|
|
563
|
-
const across = numbers.reduce((s, n, i) => s +
|
|
572
|
+
const across = numbers.reduce((s, n, i) => s + d.mid[i] * n + d.spread[i] * Math.abs(n), 0);
|
|
573
|
+
/* No unit direction takes a step further than its length. */
|
|
574
|
+
const w = largestOf(d);
|
|
564
575
|
const length = Math.hypot(...numbers.map((n, i) => (w[i] > 0 ? n : 0)));
|
|
565
576
|
out = linAdd(out, { terms: [], c: Math.min(across, length) });
|
|
566
577
|
return null;
|
|
567
578
|
}
|
|
568
579
|
for (const [i, ref] of refs.entries()) {
|
|
569
|
-
if (!
|
|
580
|
+
if (!tiny(d.mid[i]))
|
|
581
|
+
out = linAdd(out, linOf(m, ref), d.mid[i]);
|
|
582
|
+
if (tiny(d.spread[i]))
|
|
570
583
|
continue;
|
|
571
584
|
const size = magnitudeOf(m, linOf(m, ref));
|
|
572
585
|
if (!size)
|
|
573
586
|
return `${what} is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
574
|
-
out = linAdd(out, size,
|
|
587
|
+
out = linAdd(out, size, d.spread[i]);
|
|
575
588
|
}
|
|
576
589
|
return null;
|
|
577
590
|
};
|
|
@@ -579,9 +592,13 @@ function reachOf(m, asset, id, ends) {
|
|
|
579
592
|
const a = angles(refs);
|
|
580
593
|
if (a.every(t => t === 0))
|
|
581
594
|
return;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
595
|
+
if (a.every(t => t !== null && Number.isFinite(t)))
|
|
596
|
+
d = turnedBy(d, rigidTurnOf(a));
|
|
597
|
+
else {
|
|
598
|
+
/* A turn this command cannot read: any direction no longer than the largest it was. */
|
|
599
|
+
const reach = Math.min(1, largestOf(d).reduce((s, v) => s + v, 0));
|
|
600
|
+
d = { mid: [0, 0, 0], spread: [reach, reach, reach] };
|
|
601
|
+
}
|
|
585
602
|
};
|
|
586
603
|
for (let i = top; i >= 0; i--) {
|
|
587
604
|
const part = chain[i];
|
|
@@ -595,8 +612,13 @@ function reachOf(m, asset, id, ends) {
|
|
|
595
612
|
}
|
|
596
613
|
turnTo(seat.args.slice(3, 6));
|
|
597
614
|
}
|
|
598
|
-
|
|
599
|
-
|
|
615
|
+
/* Its axes in order, each about the point it passes: there, turned or slid, and back (ADR-0159). */
|
|
616
|
+
for (const { motion, through, back } of motionStagesOf(m, part)) {
|
|
617
|
+
if (through) {
|
|
618
|
+
const bad = step(through.args.slice(0, 3), `the point ${part} turns about`);
|
|
619
|
+
if (bad)
|
|
620
|
+
return bad;
|
|
621
|
+
}
|
|
600
622
|
const unit = constants(motion.args.slice(0, 3));
|
|
601
623
|
if (unit.some(u => u === undefined))
|
|
602
624
|
return `${part}'s ${motion.op === 'axis-turn@1' ? 'turn' : 'slide'} axis is not a constant`;
|
|
@@ -607,17 +629,44 @@ function reachOf(m, asset, id, ends) {
|
|
|
607
629
|
d = sweptBy(d, u, range && [(range[0] * Math.PI) / 180, (range[1] * Math.PI) / 180]);
|
|
608
630
|
}
|
|
609
631
|
else if (motion.op === 'axis-slide@1') {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
632
|
+
/*
|
|
633
|
+
Its travel lies between the two ends of its control. One end at nothing and the other a sum of sizes: it
|
|
634
|
+
goes only that way, so the middle adds it only toward that side. Otherwise the further end, either way.
|
|
635
|
+
*/
|
|
636
|
+
const mid = d.mid.reduce((s, v, k) => s + v * u[k], 0);
|
|
637
|
+
const spread = d.spread.reduce((s, v, k) => s + v * Math.abs(u[k]), 0);
|
|
638
|
+
/* Every corner of the controls it reads: a sum of leaders pulling apart goes furthest with them at either end. */
|
|
639
|
+
const states = [...statesReaching(m, motion.args[3])].sort();
|
|
640
|
+
if (states.length > CORNER_CONTROLS)
|
|
641
|
+
return `${part}'s slide reads ${states.length} controls; how far it reaches is read over at most ${CORNER_CONTROLS}`;
|
|
642
|
+
const corners = states.length <= 1 ? ['min', 'max'] : Array.from({ length: 2 ** states.length }, (_, n) => Object.fromEntries(states.map((st, i) => [st, (n >> i) & 1 ? 'max' : 'min'])));
|
|
643
|
+
const travels = corners.map(corner => travelAt(m, asset, motion, corner));
|
|
644
|
+
if (travels.some(t => !t))
|
|
645
|
+
return `${part}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
646
|
+
const [lo, hi] = travels.length === 2 ? travels : [null, null];
|
|
647
|
+
const nothing = (l) => !linNorm(l).terms.length && Math.abs(l.c) < 1e-12;
|
|
648
|
+
const sizes = (l) => l.c >= 0 && linNorm(l).terms.every(t => t.k > 0 && sizeValue(m, t.ref));
|
|
649
|
+
const other = lo && hi ? (nothing(lo) ? hi : nothing(hi) ? lo : null) : null;
|
|
650
|
+
const way = other && (sizes(other) ? 1 : sizes(linAdd({ terms: [], c: 0 }, other, -1)) ? -1 : 0);
|
|
651
|
+
if (other && way)
|
|
652
|
+
out = linAdd(out, other, way * (Math.max(way * mid, 0) + spread));
|
|
653
|
+
else {
|
|
654
|
+
const further = travels.reduce((sum, l) => {
|
|
655
|
+
const size = sum && magnitudeOf(m, l);
|
|
656
|
+
return size && sum && linAtLeast(sum, size);
|
|
657
|
+
}, { terms: [], c: 0 });
|
|
658
|
+
if (!further)
|
|
615
659
|
return `${part}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
616
|
-
out = linAdd(out,
|
|
660
|
+
out = linAdd(out, further, Math.abs(mid) + spread);
|
|
617
661
|
}
|
|
618
662
|
}
|
|
619
663
|
else
|
|
620
664
|
return `${part} moves by ${motion.op}, which this command does not add up`;
|
|
665
|
+
if (back) {
|
|
666
|
+
const bad = step(back.args.slice(0, 3), `the point ${part} turns about`);
|
|
667
|
+
if (bad)
|
|
668
|
+
return bad;
|
|
669
|
+
}
|
|
621
670
|
}
|
|
622
671
|
const pose = nodeById(m, `${part}.pose`);
|
|
623
672
|
if (!pose)
|
|
@@ -629,27 +678,33 @@ function reachOf(m, asset, id, ends) {
|
|
|
629
678
|
}
|
|
630
679
|
/*
|
|
631
680
|
Its own extent on every axis, as far as the directions reach across it: half its size either way about its
|
|
632
|
-
origin, or from face to face where they do not sit about it (a portal stands on 0, `offCentreEndsOf`)
|
|
681
|
+
origin, or from face to face where they do not sit about it (a portal stands on 0, `offCentreEndsOf`) -- the
|
|
682
|
+
middle at the face it points to, the spread times the further face.
|
|
633
683
|
*/
|
|
634
|
-
const w = most(d);
|
|
635
684
|
for (const [i, axis] of AXES.entries()) {
|
|
636
|
-
|
|
685
|
+
const mid = d.mid[i], spread = d.spread[i];
|
|
686
|
+
if (tiny(mid) && tiny(spread))
|
|
637
687
|
continue;
|
|
638
688
|
const ends = offCentre[axis];
|
|
639
|
-
if (ends
|
|
640
|
-
const e =
|
|
641
|
-
|
|
689
|
+
if (ends) {
|
|
690
|
+
const e = mid > 0 ? ends[1] : ends[0];
|
|
691
|
+
if (!tiny(mid))
|
|
692
|
+
out = linAdd(out, { terms: [{ ref: e.ref, k: e.k * mid }], c: 0 });
|
|
693
|
+
if (tiny(spread))
|
|
694
|
+
continue;
|
|
695
|
+
const further = ends.reduce((sum, x) => {
|
|
696
|
+
const size = sum && magnitudeOf(m, { terms: [{ ref: x.ref, k: x.k }], c: 0 });
|
|
697
|
+
return size && sum && linAtLeast(sum, size);
|
|
698
|
+
}, { terms: [], c: 0 });
|
|
699
|
+
if (!further)
|
|
700
|
+
return `${id}'s size is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
701
|
+
out = linAdd(out, further, spread);
|
|
642
702
|
continue;
|
|
643
703
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
const size = sum && magnitudeOf(m, { terms: [{ ref: e.ref, k: e.k }], c: 0 });
|
|
647
|
-
return size && sum && linAtLeast(sum, size);
|
|
648
|
-
}, { terms: [], c: 0 })
|
|
649
|
-
: magnitudeOf(m, { terms: [{ ref: sizes[axis].ref, k: 0.5 * sizes[axis].k }], c: 0 });
|
|
650
|
-
if (!reach)
|
|
704
|
+
const half = magnitudeOf(m, { terms: [{ ref: sizes[axis].ref, k: 0.5 * sizes[axis].k }], c: 0 });
|
|
705
|
+
if (!half)
|
|
651
706
|
return `${id}'s size is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
652
|
-
out = linAdd(out,
|
|
707
|
+
out = linAdd(out, half, Math.min(1, Math.abs(mid) + spread));
|
|
653
708
|
}
|
|
654
709
|
return linNorm(out);
|
|
655
710
|
};
|
|
@@ -671,15 +726,18 @@ function reachOf(m, asset, id, ends) {
|
|
|
671
726
|
/*
|
|
672
727
|
The pivot: where the topmost turn is fastened, and everything above it. Above it nothing turns, but a slide may
|
|
673
728
|
carry it -- the telescopic conveyor's nose turns at the end of its third stage -- so the pivot is read at each
|
|
674
|
-
|
|
675
|
-
the way (it had been refused, and the nose left out of the room).
|
|
729
|
+
corner of the controls (`cornersOf`), a reach from each: a slide is a straight line, so the reaches from its ends
|
|
730
|
+
bound it all the way (it had been refused, and the nose left out of the room).
|
|
676
731
|
*/
|
|
677
732
|
const seat = nodeById(m, `${pivotPart}.seat.rigid`);
|
|
678
733
|
const seatAt = Object.fromEntries(AXES.map((a, i) => [a, seat ? linOf(m, seat.args[i]) : { terms: [], c: 0 }]));
|
|
679
734
|
const centres = [];
|
|
680
|
-
|
|
735
|
+
const corners = parent ? cornersOf(m, parent) : ['min'];
|
|
736
|
+
if (!Array.isArray(corners))
|
|
737
|
+
return tooManyCorners(parent, corners.many);
|
|
738
|
+
for (const corner of corners) {
|
|
681
739
|
/* The point it turns about, in the frame of what carries it, carried there -- turned with it if that stands turned. */
|
|
682
|
-
const at = parent ? centreVec(m, asset, parent,
|
|
740
|
+
const at = parent ? centreVec(m, asset, parent, corner, structuredClone(seatAt)) : seatAt;
|
|
683
741
|
if (typeof at === 'string')
|
|
684
742
|
return at;
|
|
685
743
|
centres.push(at);
|
|
@@ -687,11 +745,10 @@ function reachOf(m, asset, id, ends) {
|
|
|
687
745
|
return { centres, up, down };
|
|
688
746
|
}
|
|
689
747
|
/** The bounds the parts come to, as sums, with what could not be read named. */
|
|
690
|
-
function
|
|
748
|
+
function reachLins(asset) {
|
|
691
749
|
const m = asset.document.model;
|
|
692
750
|
const parts = [];
|
|
693
751
|
const skipped = [];
|
|
694
|
-
const bounded = [];
|
|
695
752
|
const low = { x: [], y: [], z: [] };
|
|
696
753
|
const high = { x: [], y: [], z: [] };
|
|
697
754
|
/*
|
|
@@ -718,7 +775,6 @@ function occupancyLins(asset, over) {
|
|
|
718
775
|
};
|
|
719
776
|
/* A row's template stands for its copies (`repeatPart`): leaving it out left a conveyor's rollers out of its room. */
|
|
720
777
|
const rowOf = (id) => nodeById(m, `${id}.repeat`);
|
|
721
|
-
const ends = over === 'rest' ? ['start'] : ['min', 'max'];
|
|
722
778
|
/* A part's size along the figure's axes, whichever quarter turns it and what carries it stand in. */
|
|
723
779
|
const sizesOf = (id, turns) => {
|
|
724
780
|
/* A turned chain's reach is read from one size per axis (reachOf), so a part there must have one. */
|
|
@@ -746,7 +802,10 @@ function occupancyLins(asset, over) {
|
|
|
746
802
|
return `${part} is a row whose run this command cannot read`;
|
|
747
803
|
const theirs = standingRefsOf(m, asset, parent, parent, false)[axis];
|
|
748
804
|
const half = { terms: [{ ref: theirs.ref, k: 0.5 * theirs.k }], c: 0 };
|
|
749
|
-
const
|
|
805
|
+
const corners = cornersOf(m, parent);
|
|
806
|
+
if (!Array.isArray(corners))
|
|
807
|
+
return tooManyCorners(parent, corners.many);
|
|
808
|
+
const centres = corners.map(corner => centreLin(m, asset, parent, axis, corner));
|
|
750
809
|
const bad = centres.find(c => typeof c === 'string');
|
|
751
810
|
if (bad)
|
|
752
811
|
return String(bad);
|
|
@@ -829,7 +888,7 @@ function occupancyLins(asset, over) {
|
|
|
829
888
|
onto[axis].push(climbs.reduce((sum, c) => linAdd(sum, c.reach), at));
|
|
830
889
|
};
|
|
831
890
|
if (turns) {
|
|
832
|
-
const reach = reachOf(m, asset, place.id
|
|
891
|
+
const reach = reachOf(m, asset, place.id);
|
|
833
892
|
if (typeof reach === 'string') {
|
|
834
893
|
skipped.push({ part: place.id, reason: reach });
|
|
835
894
|
continue;
|
|
@@ -865,13 +924,22 @@ function occupancyLins(asset, over) {
|
|
|
865
924
|
continue;
|
|
866
925
|
}
|
|
867
926
|
/*
|
|
868
|
-
|
|
927
|
+
Every corner of the controls is a candidate (`cornersOf`), and which one is the low end is not decided here: a travel of
|
|
869
928
|
−40 mm puts the low end at the control's maximum (designer's counterexample 2026-09-23, where the
|
|
870
929
|
proposal read −50..10 for a part that needs −90..50). Both candidates go into the same min and the same
|
|
871
930
|
max, so the answer holds whichever way the travel runs, and holds at every design size, because the
|
|
872
931
|
choice is made when the graph is evaluated rather than when the proposal is written.
|
|
873
932
|
*/
|
|
874
|
-
const
|
|
933
|
+
const corners = cornersOf(m, place.id);
|
|
934
|
+
/* A row's copies are read corner by corner against its part's: too many controls for that is said, not bounded. */
|
|
935
|
+
if (!Array.isArray(corners) && rows.some(r => r.part !== place.id)) {
|
|
936
|
+
skipped.push({ part: place.id, reason: tooManyCorners(place.id, corners.many) });
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const readings = AXES.map(axis => ({
|
|
940
|
+
axis,
|
|
941
|
+
at: Array.isArray(corners) ? corners.map(corner => centreLin(m, asset, place.id, axis, corner)) : wideReadingOf(m, asset, place.id, axis, corners.many)
|
|
942
|
+
})).map(r => ({ axis: r.axis, at: typeof r.at === 'string' ? [r.at] : r.at }));
|
|
875
943
|
const bad = readings.flatMap(r => r.at).find(v => typeof v === 'string');
|
|
876
944
|
if (bad) {
|
|
877
945
|
skipped.push({ part: place.id, reason: String(bad) });
|
|
@@ -889,7 +957,8 @@ function occupancyLins(asset, over) {
|
|
|
889
957
|
Carried by a row's part: its copies stand where that part's copies do, offset as it is from it. That part's
|
|
890
958
|
centres lie half its length in from the run's ends.
|
|
891
959
|
*/
|
|
892
|
-
|
|
960
|
+
/* Read corner by corner: a part slid by too many controls for that was said above. */
|
|
961
|
+
const theirs = corners.map(corner => centreLin(m, asset, template, run.axis, corner));
|
|
893
962
|
const bad = theirs.find(c => typeof c === 'string');
|
|
894
963
|
if (bad) {
|
|
895
964
|
offRun = String(bad);
|
|
@@ -951,39 +1020,34 @@ function occupancyLins(asset, over) {
|
|
|
951
1020
|
}
|
|
952
1021
|
}
|
|
953
1022
|
}
|
|
954
|
-
|
|
955
|
-
if (boundsUnder(m, part).length)
|
|
956
|
-
bounded.push(part);
|
|
957
|
-
return { parts, skipped, bounded, low, high, reaches };
|
|
1023
|
+
return { parts, skipped, low, high, reaches };
|
|
958
1024
|
}
|
|
959
1025
|
const valueOfLin = (values, l) => l.terms.reduce((s, t) => s + t.k * (values[t.ref] ?? NaN), l.c);
|
|
960
|
-
/**
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1026
|
+
/**
|
|
1027
|
+
* The room a figure sweeps (ADR-0145 2.3): every part over every joint's range, at the authored size or at `sizes`
|
|
1028
|
+
* (design inputs, by id). Computed, never declared -- it holds the parts by what it is. Undefined for a figure with no
|
|
1029
|
+
* part to measure. Reads the asset; changes nothing.
|
|
1030
|
+
*/
|
|
1031
|
+
export function figureMotionRangeOf(asset, sizes = {}) {
|
|
966
1032
|
const source = structuredClone(asset);
|
|
967
|
-
const { parts, skipped,
|
|
1033
|
+
const { parts, skipped, low, high, reaches } = reachLins(source);
|
|
968
1034
|
for (const axis of AXES) {
|
|
969
1035
|
low[axis].push(...reaches.low[axis]);
|
|
970
1036
|
high[axis].push(...reaches.high[axis]);
|
|
971
1037
|
}
|
|
972
1038
|
boundedSides(source.document.model, low, high);
|
|
973
1039
|
if (!parts.length)
|
|
974
|
-
|
|
975
|
-
const values = compileFigureGraph(source.document.model).evaluate({ ...source.designInputs, ...source.stateDefaults })
|
|
1040
|
+
return undefined;
|
|
1041
|
+
const values = compileFigureGraph(source.document.model).evaluate({ ...source.designInputs, ...source.stateDefaults, ...sizes })
|
|
976
1042
|
.values;
|
|
977
|
-
const
|
|
1043
|
+
const bounds = Object.fromEntries(AXES.map(axis => [
|
|
978
1044
|
axis,
|
|
979
1045
|
{
|
|
980
1046
|
min: round6(Math.min(...low[axis].map(l => valueOfLin(values, l)))),
|
|
981
1047
|
max: round6(Math.max(...high[axis].map(l => valueOfLin(values, l))))
|
|
982
1048
|
}
|
|
983
1049
|
]));
|
|
984
|
-
|
|
985
|
-
const held = bounded.map(part => ({ part, mm: boundedWidthOf(m, values, boundsUnder(m, part)) }));
|
|
986
|
-
return { over, at, parts, skipped, ...(held.length ? { bounded: held } : {}), basis: fingerprintOf(source) };
|
|
1050
|
+
return { bounds, parts, skipped };
|
|
987
1051
|
}
|
|
988
1052
|
/**
|
|
989
1053
|
* The placement says which plane the mounting plane is. A part fastened to it in a way the new plane does not take --
|
|
@@ -1007,86 +1071,9 @@ export function setPlacement(asset, a) {
|
|
|
1007
1071
|
asset.placement = a.placement;
|
|
1008
1072
|
return asset;
|
|
1009
1073
|
}
|
|
1010
|
-
|
|
1011
|
-
const m = asset.document.model;
|
|
1012
|
-
if (!a.proposal || (a.proposal.over !== 'rest' && a.proposal.over !== 'range'))
|
|
1013
|
-
fail('SCHEMA', 'occupancy', 'a proposal is required; propose first, then confirm');
|
|
1014
|
-
const now = fingerprintOf(asset);
|
|
1015
|
-
if (a.proposal.basis !== now)
|
|
1016
|
-
fail('OCCUPANCY_STALE', 'occupancy', 'the figure changed since this was proposed, so the volume it would declare is not the one that was looked at; propose again and confirm that');
|
|
1017
|
-
/* Declared with a placement, the placement is set by the same command as on its own: one value, one check. */
|
|
1018
|
-
if (a.placement !== undefined)
|
|
1019
|
-
setPlacement(asset, { kind: 'set-placement', placement: a.placement });
|
|
1020
|
-
const padding = {};
|
|
1021
|
-
for (const axis of AXES) {
|
|
1022
|
-
const pad = a.padding?.[axis] ?? 0;
|
|
1023
|
-
if (!(Number.isFinite(pad) && pad >= 0))
|
|
1024
|
-
fail('OCCUPANCY_PADDING', `occupancy.${axis}`, `padding is millimetres, zero or more; got ${String(pad)}`);
|
|
1025
|
-
padding[axis] = pad;
|
|
1026
|
-
}
|
|
1027
|
-
const { parts, low, high, reaches } = occupancyLins(asset, a.proposal.over);
|
|
1028
|
-
/* What the parts carried by a turn reach either way about their pivots, on each axis. */
|
|
1029
|
-
for (const axis of AXES) {
|
|
1030
|
-
low[axis].push(...reaches.low[axis]);
|
|
1031
|
-
high[axis].push(...reaches.high[axis]);
|
|
1032
|
-
}
|
|
1033
|
-
boundedSides(m, low, high);
|
|
1034
|
-
for (const axis of AXES) {
|
|
1035
|
-
if (!padding[axis])
|
|
1036
|
-
continue;
|
|
1037
|
-
low[axis] = low[axis].map((l) => ({ ...l, c: l.c - padding[axis] }));
|
|
1038
|
-
high[axis] = high[axis].map((l) => ({ ...l, c: l.c + padding[axis] }));
|
|
1039
|
-
}
|
|
1040
|
-
if (parts.join('|') !== a.proposal.parts.join('|'))
|
|
1041
|
-
fail('OCCUPANCY_STALE', 'occupancy', `the figure changed since the proposal was made (${a.proposal.parts.join(', ')} then, ${parts.join(', ')} now); propose again and confirm that`);
|
|
1042
|
-
/* One node for the six bounds (affine-bounds@1): each candidate its constant, then a value and a ratio per term. */
|
|
1043
|
-
const args = [];
|
|
1044
|
-
const layout = AXES.flatMap(axis => [low[axis], high[axis]]).map(side => side
|
|
1045
|
-
.map((l) => {
|
|
1046
|
-
const n = throughSums(m, l);
|
|
1047
|
-
args.push(constantOf(m, 'mm', round6(n.c), 'd'));
|
|
1048
|
-
for (const t of n.terms)
|
|
1049
|
-
args.push(t.ref, constantOf(m, 'ratio', t.k, 'k'));
|
|
1050
|
-
return n.terms.length;
|
|
1051
|
-
})
|
|
1052
|
-
.join('_'));
|
|
1053
|
-
const id = namer(m, 'occupancy')('bounds');
|
|
1054
|
-
const port = (p) => `${id}.${p}`;
|
|
1055
|
-
m.nodes.push({
|
|
1056
|
-
id,
|
|
1057
|
-
op: 'affine-bounds@1',
|
|
1058
|
-
args,
|
|
1059
|
-
params: { layout: `L${layout.join('.')}` },
|
|
1060
|
-
outputs: Object.fromEntries(['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax'].map(p => [p, port(p)]))
|
|
1061
|
-
});
|
|
1062
|
-
const bounds = Object.fromEntries(AXES.map(axis => [axis, { min: port(`${axis}min`), max: port(`${axis}max`) }]));
|
|
1063
|
-
const was = stringsIn(asset.occupancy?.bounds);
|
|
1064
|
-
asset.occupancy = { ...(asset.occupancy?.contact ? { contact: asset.occupancy.contact } : {}), bounds };
|
|
1065
|
-
/*
|
|
1066
|
-
The room declared before goes with what fed only it. Left in the graph, it kept reading where parts had been
|
|
1067
|
-
fastened, and the next fastening was refused over a room no one read (the gantry, 2026-09-25).
|
|
1068
|
-
*/
|
|
1069
|
-
for (const ref of was)
|
|
1070
|
-
dropIfUnused(asset, ref);
|
|
1071
|
-
/*
|
|
1072
|
-
A part's own measure the old room alone still read -- the part follows something else now -- goes too: kept, it
|
|
1073
|
-
was a dial that changed nothing (the turntable after every part followed the one ratio, 2026-09-25). So does its
|
|
1074
|
-
own position or turn, once a fastening places it: a floor part's height position stayed as a field that moved
|
|
1075
|
-
nothing (browser check, ADR-0097, 2026-09-27). The room's refit names them before it is confirmed (figureRoomRefitOf).
|
|
1076
|
-
*/
|
|
1077
|
-
const placed = new Set(m.nodes.filter((n) => n.op === 'place@1' || n.op === 'member@1').map((n) => n.id));
|
|
1078
|
-
for (const input of [...m.inputs])
|
|
1079
|
-
if (input.role === 'design' &&
|
|
1080
|
-
placed.has(String(input.id).split('.')[0]) &&
|
|
1081
|
-
/\.(dimension|size|position|rotation)\.[a-z]+$/.test(input.id))
|
|
1082
|
-
dropIfUnused(asset, input.id, input.id);
|
|
1083
|
-
return asset;
|
|
1084
|
-
}
|
|
1085
|
-
/** "This face is what the figure is mounted on." Kept apart from how much room the figure takes up. */
|
|
1074
|
+
/** "This face is what the figure is mounted on." */
|
|
1086
1075
|
export function setMountingFace(asset, a) {
|
|
1087
1076
|
const m = asset.document.model;
|
|
1088
|
-
if (!asset.occupancy)
|
|
1089
|
-
fail('OCCUPANCY_UNDECLARED', 'occupancy', 'declare the room the figure takes up before naming the face it stands on');
|
|
1090
1077
|
if (!Object.hasOwn(FIGURE_FACES, String(a.face)))
|
|
1091
1078
|
fail('SCHEMA', a.part, `${String(a.face)} is not a face`);
|
|
1092
1079
|
const { place } = partOf(m, a.part, a.part);
|
|
@@ -1099,16 +1086,16 @@ export function setMountingFace(asset, a) {
|
|
|
1099
1086
|
outputs: { pose: `${id}.value` },
|
|
1100
1087
|
params: { frame: `${id}.frame`, name: a.face }
|
|
1101
1088
|
});
|
|
1102
|
-
asset.
|
|
1089
|
+
asset.mountingFace = {
|
|
1103
1090
|
plane: 'mounting-plane',
|
|
1104
1091
|
surface: { placement: place.outputs.placed, feature: `${id}.value` }
|
|
1105
1092
|
};
|
|
1106
1093
|
return asset;
|
|
1107
1094
|
}
|
|
1108
1095
|
export function clearMountingFace(asset) {
|
|
1109
|
-
if (!asset.
|
|
1110
|
-
fail('EDIT_TARGET', '
|
|
1111
|
-
delete asset.
|
|
1096
|
+
if (!asset.mountingFace)
|
|
1097
|
+
fail('EDIT_TARGET', 'mountingFace', 'this figure names no mounting face');
|
|
1098
|
+
delete asset.mountingFace;
|
|
1112
1099
|
return asset;
|
|
1113
1100
|
}
|
|
1114
|
-
//# sourceMappingURL=authoring-
|
|
1101
|
+
//# sourceMappingURL=authoring-placement.js.map
|