@hatiolab/figure-model 0.9.5 → 0.10.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/README.md +1 -1
- 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 +18 -47
- 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-geometry.d.ts +7 -0
- package/dist/authoring-geometry.d.ts.map +1 -1
- package/dist/authoring-geometry.js +25 -2
- package/dist/authoring-geometry.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} +325 -223
- package/dist/authoring-placement.js.map +1 -0
- package/dist/authoring-proportion.d.ts.map +1 -1
- package/dist/authoring-proportion.js +3 -4
- package/dist/authoring-proportion.js.map +1 -1
- package/dist/authoring-size.d.ts +5 -10
- package/dist/authoring-size.d.ts.map +1 -1
- package/dist/authoring-size.js +29 -99
- package/dist/authoring-size.js.map +1 -1
- package/dist/authoring-types.d.ts +10 -79
- package/dist/authoring-types.d.ts.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/command-preview.js +4 -4
- package/dist/command-preview.js.map +1 -1
- package/dist/contract.d.ts +4 -19
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/create.d.ts +5 -3
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +3 -7
- package/dist/create.js.map +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +1 -2
- 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.js +7 -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 +60 -107
- package/dist/gate.js.map +1 -1
- package/dist/graph-types.d.ts +17 -4
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +21 -73
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/instance-size.d.ts +58 -32
- package/dist/instance-size.d.ts.map +1 -1
- package/dist/instance-size.js +155 -41
- package/dist/instance-size.js.map +1 -1
- package/dist/joint-list.d.ts.map +1 -1
- package/dist/joint-list.js +3 -3
- package/dist/joint-list.js.map +1 -1
- 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 +6 -64
- package/dist/joint-part-edits.js.map +1 -1
- package/dist/joint-writing.d.ts.map +1 -1
- package/dist/joint-writing.js +9 -4
- 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 +17 -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 +10 -17
- package/dist/proposal.js.map +1 -1
- package/dist/recipe-from-parts.js +5 -5
- package/dist/recipe-from-parts.js.map +1 -1
- package/dist/recipe.js +1 -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 +39 -31
- package/dist/release-contract.js.map +1 -1
- package/dist/release-metrics.d.ts +6 -14
- package/dist/release-metrics.d.ts.map +1 -1
- package/dist/release-metrics.js +14 -31
- 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 +8 -38
- package/dist/resize-plan.js.map +1 -1
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +1 -2
- package/dist/robots.js.map +1 -1
- package/dist/shape-provider.d.ts.map +1 -1
- package/dist/shape-provider.js +4 -3
- package/dist/shape-provider.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/document-contract.md +0 -1
- package/docs/operator-contracts.md +3 -18
- package/docs/shape-dimension-contract.md +27 -25
- 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 -92
- package/dist/room.js.map +0 -1
|
@@ -2,41 +2,18 @@
|
|
|
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";
|
|
10
|
+
import { figureShapeDescriptionOf } from "./shape-ops.js";
|
|
9
11
|
import { FigureContractError } from "./contract.js";
|
|
10
12
|
import { AXES } from "./words.js";
|
|
11
13
|
import { FIGURE_PLACEMENTS } from "./asset-types.js";
|
|
12
14
|
import { FIGURE_FACES } from "./authoring-types.js";
|
|
13
15
|
import { fail, nodeById, writerOf, throughSums, rowHolding, partOf, namer, constantOf, dropIfUnused, parentOf, round6, stringsIn } from "./authoring-nodes.js";
|
|
14
|
-
import { isReach, sizeRefsOf, occupiedExtentsOf, angleOf, turnMatrix, mul3, standingRefsOf, half, IDENTITY } from "./authoring-geometry.js";
|
|
15
|
-
/** A figure as one value, for saying "this is the figure I read". The declaration itself is left out of it. */
|
|
16
|
-
function fingerprintOf(asset) {
|
|
17
|
-
/*
|
|
18
|
-
The room and the placement are left out: neither changes what the room would measure (ADR-0097). Nor do the clips,
|
|
19
|
-
their groups and the data connections -- the room is measured over the joints' ranges, not what plays them -- and a
|
|
20
|
-
command is applied with the groups and their clips held aside (clip-keeping.ts): counted, a room proposed on the
|
|
21
|
-
figure as it is was stale against the same figure without them, and a figure with a motion group could not declare
|
|
22
|
-
its room again (the robot dog, 2026-10-19).
|
|
23
|
-
*/
|
|
24
|
-
const text = JSON.stringify({
|
|
25
|
-
...asset,
|
|
26
|
-
placement: undefined,
|
|
27
|
-
occupancy: asset.occupancy ? { ...asset.occupancy, bounds: null } : undefined,
|
|
28
|
-
drivers: undefined,
|
|
29
|
-
motionGroups: undefined,
|
|
30
|
-
dataInterface: undefined
|
|
31
|
-
});
|
|
32
|
-
let h1 = 0x811c9dc5;
|
|
33
|
-
let h2 = 0x01000193;
|
|
34
|
-
for (let i = 0; i < text.length; i++) {
|
|
35
|
-
h1 = Math.imul(h1 ^ text.charCodeAt(i), 0x01000193) >>> 0;
|
|
36
|
-
h2 = Math.imul(h2 + text.charCodeAt(i), 0x85ebca6b) >>> 0;
|
|
37
|
-
}
|
|
38
|
-
return `${h1.toString(16).padStart(8, '0')}${h2.toString(16).padStart(8, '0')}:${text.length}`;
|
|
39
|
-
}
|
|
16
|
+
import { isReach, sizeRefsOf, occupiedExtentsOf, angleOf, turnMatrix, mul3, standingRefsOf, half, axisTurn, offCentreEndsOf, IDENTITY } from "./authoring-geometry.js";
|
|
40
17
|
const linAdd = (a, b, k = 1) => ({
|
|
41
18
|
terms: [...a.terms, ...b.terms.map(t => ({ ref: t.ref, k: t.k * k }))],
|
|
42
19
|
c: a.c + b.c * k
|
|
@@ -63,7 +40,7 @@ const linOf = (m, ref) => {
|
|
|
63
40
|
* 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
|
|
64
41
|
* figure's sizes make of the rest. A mimic of a leader whose stroke follows a length is the leader's ratio times that
|
|
65
42
|
* length times its multiplier -- read through, not refused: the telescopic conveyor's second and third stages were
|
|
66
|
-
* left out of
|
|
43
|
+
* left out of the room it was given, 2600 and 5200 mm past it (2026-09-25).
|
|
67
44
|
*/
|
|
68
45
|
function travelAt(m, asset, motion, bound) {
|
|
69
46
|
const pick = (i) => bound === 'start' ? (asset.stateDefaults[i.id] ?? i.min) : bound === 'min' ? i.min : i.max;
|
|
@@ -133,7 +110,7 @@ function poseTurnOf(m, asset, part) {
|
|
|
133
110
|
const seatTurned = (m, asset, part) => (nodeById(m, `${part}.seat.rigid`)?.args.slice(3, 6) ?? []).some((ref) => angleOf(m, asset, ref) !== 0);
|
|
134
111
|
/**
|
|
135
112
|
* Whether a part, or anything carrying it, turns on a joint, stands turned by other than quarter turns, or is seated
|
|
136
|
-
* turned: a box cannot follow it,
|
|
113
|
+
* turned: a box cannot follow it, its reach about the point it turns at can (`reachOf`).
|
|
137
114
|
*/
|
|
138
115
|
function carriedByATurn(m, asset, id) {
|
|
139
116
|
for (let part = id; part; part = parentOf(m, part)) {
|
|
@@ -258,8 +235,9 @@ function sizeValue(m, ref, seen = 0) {
|
|
|
258
235
|
if (seen > 64)
|
|
259
236
|
return false;
|
|
260
237
|
const input = m.inputs.find((i) => i.id === ref);
|
|
238
|
+
/* The instance size is never below zero, its smallest end given or not (ADR-0087 decision 6). */
|
|
261
239
|
if (input)
|
|
262
|
-
return input.role !== 'state' && input.min >= 0;
|
|
240
|
+
return input.role !== 'state' && (/^size\.[xyz]$/.test(input.id) || input.min >= 0);
|
|
263
241
|
const w = writerOf(m, ref);
|
|
264
242
|
const positive = (r) => (m.constants.find((c) => c.id === r)?.value ?? -1) > 0;
|
|
265
243
|
if (w?.op === 'geomean@1')
|
|
@@ -381,31 +359,6 @@ function boundedMagnitudeOf(m, ref, inside = false) {
|
|
|
381
359
|
return linAdd({ terms: [], c: 0 }, linAdd(each[0], each[1]), 0.5);
|
|
382
360
|
return each.reduce((a, b) => linAtLeast(a, b));
|
|
383
361
|
}
|
|
384
|
-
/** The values held by bounds a part's place reads, up what carries it: its room may be wider than it there. */
|
|
385
|
-
function boundsUnder(m, id) {
|
|
386
|
-
const found = new Set();
|
|
387
|
-
for (let part = id; part; part = parentOf(m, part))
|
|
388
|
-
for (const node of [nodeById(m, `${part}.pose`), nodeById(m, `${part}.seat.rigid`)])
|
|
389
|
-
for (const ref of (node?.args ?? []).slice(0, 3))
|
|
390
|
-
for (const t of throughSums(m, linOf(m, ref)).terms)
|
|
391
|
-
if (isBounded(m, t.ref))
|
|
392
|
-
found.add(t.ref);
|
|
393
|
-
return [...found];
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* How much wider than a bounded value its room may be, at the figure's values, in mm: the greatest of its bound over
|
|
397
|
-
* 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).
|
|
398
|
-
*/
|
|
399
|
-
function boundedWidthOf(m, values, refs) {
|
|
400
|
-
let most = 0;
|
|
401
|
-
for (const ref of refs) {
|
|
402
|
-
const kind = boundedKind(m, ref);
|
|
403
|
-
const v = Math.abs(values[ref] ?? 0);
|
|
404
|
-
const wide = 'root' in kind ? (values[kind.root[0]] + values[kind.root[1]]) / 2 - v : Math.abs(values[kind.point.radius]) - v;
|
|
405
|
-
most = Math.max(most, wide);
|
|
406
|
-
}
|
|
407
|
-
return Math.round(most * 10) / 10;
|
|
408
|
-
}
|
|
409
362
|
/** Every low candidate bounded from below and every high one from above, so the room holds whatever they come to. */
|
|
410
363
|
function boundedSides(m, low, high) {
|
|
411
364
|
for (const axis of AXES) {
|
|
@@ -413,14 +366,115 @@ function boundedSides(m, low, high) {
|
|
|
413
366
|
high[axis] = high[axis].flatMap(l => boundsOf(m, l, true));
|
|
414
367
|
}
|
|
415
368
|
}
|
|
369
|
+
const transposedTimes = (r, d) => [0, 1, 2].map(j => r[0][j] * d[0] + r[1][j] * d[1] + r[2][j] * d[2]);
|
|
370
|
+
/** A turn as `rigid@1` writes it, in degrees about X, Y and Z: Rx·Ry·Rz, at any angle (`turnMatrix` takes quarter turns). */
|
|
371
|
+
const rigidTurnOf = ([x, y, z]) => mul3(mul3(axisTurn('x', x), axisTurn('y', y)), axisTurn('z', z));
|
|
372
|
+
/** Each component's largest size, which a unit direction never passes. */
|
|
373
|
+
const largestOf = (d) => d.mid.map((v, i) => Math.min(1, Math.abs(v) + d.spread[i]));
|
|
374
|
+
/** The same directions, each component held to [−1, 1]. */
|
|
375
|
+
function heldToUnit(mid, spread) {
|
|
376
|
+
const lo = mid.map((v, i) => Math.max(-1, v - spread[i]));
|
|
377
|
+
const hi = mid.map((v, i) => Math.min(1, v + spread[i]));
|
|
378
|
+
return { mid: lo.map((l, i) => (l + hi[i]) / 2), spread: lo.map((l, i) => (hi[i] - l) / 2) };
|
|
379
|
+
}
|
|
380
|
+
/** The directions seen from a frame turned by `r` from this one (a point there is r·x here). */
|
|
381
|
+
function turnedBy(d, r) {
|
|
382
|
+
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)));
|
|
383
|
+
}
|
|
384
|
+
/** 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. */
|
|
385
|
+
function rangeOver(a, b, c, range) {
|
|
386
|
+
if (!range)
|
|
387
|
+
return [a - Math.hypot(b, c), a + Math.hypot(b, c)];
|
|
388
|
+
const [lo, hi] = range;
|
|
389
|
+
const at = (t) => a + b * Math.cos(t) + c * Math.sin(t);
|
|
390
|
+
let least = Math.min(at(lo), at(hi)), most = Math.max(at(lo), at(hi));
|
|
391
|
+
/* Where its slope is zero: t = atan2(c, b) and half a turn from it, every whole turn over. */
|
|
392
|
+
const turn = 2 * Math.PI;
|
|
393
|
+
for (const t0 of [Math.atan2(c, b), Math.atan2(c, b) + Math.PI])
|
|
394
|
+
for (let t = t0 + Math.ceil((lo - t0) / turn) * turn; t <= hi; t += turn) {
|
|
395
|
+
least = Math.min(least, at(t));
|
|
396
|
+
most = Math.max(most, at(t));
|
|
397
|
+
}
|
|
398
|
+
return [least, most];
|
|
399
|
+
}
|
|
416
400
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* ball is the pivot plus and minus its radius on each axis. Read by the proposal; the release gate still checks
|
|
421
|
-
* the parts against it.
|
|
401
|
+
* The directions seen from a frame turned about the unit axis `u` by any angle in `range` (radians; null all the way
|
|
402
|
+
* 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
|
|
403
|
+
* the range exactly; the spread about it by each entry's largest.
|
|
422
404
|
*/
|
|
423
|
-
function
|
|
405
|
+
function sweptBy(d, u, range) {
|
|
406
|
+
const K = [
|
|
407
|
+
[0, -u[2], u[1]],
|
|
408
|
+
[u[2], 0, -u[0]],
|
|
409
|
+
[-u[1], u[0], 0]
|
|
410
|
+
];
|
|
411
|
+
const A = (i, j) => u[i] * u[j];
|
|
412
|
+
const B = (i, j) => (i === j ? 1 : 0) - u[i] * u[j];
|
|
413
|
+
const C = (i, j) => K[i][j];
|
|
414
|
+
if (range && Math.abs(range[1] - range[0]) < 1e-12) {
|
|
415
|
+
const t = range[0];
|
|
416
|
+
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))));
|
|
417
|
+
}
|
|
418
|
+
const sum = (f, j) => [0, 1, 2].reduce((s, i) => s + f(i, j) * d.mid[i], 0);
|
|
419
|
+
const ends = [0, 1, 2].map(j => rangeOver(sum(A, j), sum(B, j), sum(C, j), range));
|
|
420
|
+
const spread = [0, 1, 2].map(j => d.spread.reduce((s, v, i) => {
|
|
421
|
+
const [l, h] = rangeOver(A(i, j), B(i, j), C(i, j), range);
|
|
422
|
+
return s + Math.max(Math.abs(l), Math.abs(h)) * v;
|
|
423
|
+
}, 0));
|
|
424
|
+
return heldToUnit(ends.map(([l, h]) => (l + h) / 2), ends.map(([l, h], j) => (h - l) / 2 + spread[j]));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The angles a turn's control can give it, in degrees: its limits, scaled and moved as the angle is written from it.
|
|
428
|
+
* Null -- all the way round -- for a control without limits or that turns a whole turn, and for an angle written any
|
|
429
|
+
* other way.
|
|
430
|
+
*/
|
|
431
|
+
function angleRangeOf(m, ref, depth = 0) {
|
|
432
|
+
if (depth > 64)
|
|
433
|
+
return null;
|
|
434
|
+
const constant = m.constants.find((c) => c.id === ref);
|
|
435
|
+
if (constant)
|
|
436
|
+
return constant.unit === 'deg' ? [constant.value, constant.value] : null;
|
|
437
|
+
const input = m.inputs.find((i) => i.id === ref);
|
|
438
|
+
if (input) {
|
|
439
|
+
if (input.role !== 'state' || input.unit !== 'deg')
|
|
440
|
+
return null;
|
|
441
|
+
const { min, max } = input;
|
|
442
|
+
return Number.isFinite(min) && Number.isFinite(max) && max - min < 360 ? [min, max] : null;
|
|
443
|
+
}
|
|
444
|
+
const w = writerOf(m, ref);
|
|
445
|
+
if (w?.op === 'add@1') {
|
|
446
|
+
let lo = 0, hi = 0;
|
|
447
|
+
for (const arg of w.args) {
|
|
448
|
+
const r = angleRangeOf(m, arg, depth + 1);
|
|
449
|
+
if (!r)
|
|
450
|
+
return null;
|
|
451
|
+
lo += r[0];
|
|
452
|
+
hi += r[1];
|
|
453
|
+
}
|
|
454
|
+
return hi - lo < 360 ? [lo, hi] : null;
|
|
455
|
+
}
|
|
456
|
+
if (w?.op === 'mul@1' && w.args.length === 2) {
|
|
457
|
+
const factor = (arg) => m.constants.find((c) => c.id === arg && c.unit === 'ratio')?.value;
|
|
458
|
+
const [a, b] = w.args;
|
|
459
|
+
const k = factor(a) ?? factor(b);
|
|
460
|
+
const r = factor(a) !== undefined ? angleRangeOf(m, b, depth + 1) : factor(b) !== undefined ? angleRangeOf(m, a, depth + 1) : null;
|
|
461
|
+
if (k === undefined || !r)
|
|
462
|
+
return null;
|
|
463
|
+
const ends = [r[0] * k, r[1] * k].sort((x, y) => x - y);
|
|
464
|
+
return ends[1] - ends[0] < 360 ? ends : null;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Where a part carried by a turn can reach, on each side of each axis, from the point the first turn above it turns
|
|
470
|
+
* about (ADR-0087, amendments 2026-10-21). Down the chain from there each step adds what it can add that way: the
|
|
471
|
+
* middle of the directions times the step, sign included, and their spread -- none until a joint has turned them
|
|
472
|
+
* through its range -- times its size; no more than its length, when its coordinates are numbers. A slide from nothing
|
|
473
|
+
* 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
|
|
474
|
+
* every angle the joints can reach and every design size: a bound, not a sample. Read by the proposal; the release
|
|
475
|
+
* gate still checks the parts against it.
|
|
476
|
+
*/
|
|
477
|
+
function reachOf(m, asset, id, ends) {
|
|
424
478
|
const chain = [];
|
|
425
479
|
for (let part = id; part; part = parentOf(m, part))
|
|
426
480
|
chain.push(part);
|
|
@@ -432,56 +486,170 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
432
486
|
: found, -1);
|
|
433
487
|
if (top < 0)
|
|
434
488
|
return `${id} is carried by no turn`;
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return null;
|
|
442
|
-
};
|
|
443
|
-
/* Its half size on every axis of its own, whichever way it stands: the ball holds it at any angle. */
|
|
489
|
+
const pivotPart = chain[top];
|
|
490
|
+
const parent = parentOf(m, pivotPart);
|
|
491
|
+
/* Above the pivot every part stands in quarter turns: the frame it turns in is a signed axis permutation. */
|
|
492
|
+
const frame = parent ? worldTurnOf(m, asset, parent) : IDENTITY;
|
|
493
|
+
if (!frame)
|
|
494
|
+
return `${pivotPart} is carried by a part turned by other than quarter turns`;
|
|
444
495
|
const sizes = sizeRefsOf(m, id, id);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const bad = grow(linOf(m, ref), `where ${part} is fastened`);
|
|
465
|
-
if (bad)
|
|
466
|
-
return bad;
|
|
496
|
+
const { shape } = partOf(m, id, id);
|
|
497
|
+
const faces = figureShapeDescriptionOf(shape)?.faces;
|
|
498
|
+
const offCentre = Object.fromEntries(AXES.map(axis => [axis, offCentreEndsOf(shape, faces, axis)]));
|
|
499
|
+
const constants = (refs) => refs.map(r => m.constants.find((c) => c.id === r)?.value);
|
|
500
|
+
const angles = (refs) => refs.map(r => angleOf(m, asset, r));
|
|
501
|
+
const reachToward = (e) => {
|
|
502
|
+
let d = { mid: transposedTimes(frame, e), spread: [0, 0, 0] };
|
|
503
|
+
let out = { terms: [], c: 0 };
|
|
504
|
+
const tiny = (v) => Math.abs(v) < 1e-12;
|
|
505
|
+
/* A step of three coordinates, in the frame the directions are seen from: the middle times it, the spread times its size. */
|
|
506
|
+
const step = (refs, what) => {
|
|
507
|
+
const numbers = constants(refs);
|
|
508
|
+
if (numbers.every(n => n !== undefined)) {
|
|
509
|
+
const across = numbers.reduce((s, n, i) => s + d.mid[i] * n + d.spread[i] * Math.abs(n), 0);
|
|
510
|
+
/* No unit direction takes a step further than its length. */
|
|
511
|
+
const w = largestOf(d);
|
|
512
|
+
const length = Math.hypot(...numbers.map((n, i) => (w[i] > 0 ? n : 0)));
|
|
513
|
+
out = linAdd(out, { terms: [], c: Math.min(across, length) });
|
|
514
|
+
return null;
|
|
467
515
|
}
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
516
|
+
for (const [i, ref] of refs.entries()) {
|
|
517
|
+
if (!tiny(d.mid[i]))
|
|
518
|
+
out = linAdd(out, linOf(m, ref), d.mid[i]);
|
|
519
|
+
if (tiny(d.spread[i]))
|
|
520
|
+
continue;
|
|
521
|
+
const size = magnitudeOf(m, linOf(m, ref));
|
|
522
|
+
if (!size)
|
|
523
|
+
return `${what} is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
524
|
+
out = linAdd(out, size, d.spread[i]);
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
};
|
|
528
|
+
const turnTo = (refs) => {
|
|
529
|
+
const a = angles(refs);
|
|
530
|
+
if (a.every(t => t === 0))
|
|
531
|
+
return;
|
|
532
|
+
if (a.every(t => t !== null && Number.isFinite(t)))
|
|
533
|
+
d = turnedBy(d, rigidTurnOf(a));
|
|
534
|
+
else {
|
|
535
|
+
/* A turn this command cannot read: any direction no longer than the largest it was. */
|
|
536
|
+
const reach = Math.min(1, largestOf(d).reduce((s, v) => s + v, 0));
|
|
537
|
+
d = { mid: [0, 0, 0], spread: [reach, reach, reach] };
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
for (let i = top; i >= 0; i--) {
|
|
541
|
+
const part = chain[i];
|
|
542
|
+
const seat = nodeById(m, `${part}.seat.rigid`);
|
|
543
|
+
if (seat) {
|
|
544
|
+
/* The pivot's own seat is where it turns: it is the centre, not a step. */
|
|
545
|
+
if (i < top) {
|
|
546
|
+
const bad = step(seat.args.slice(0, 3), `where ${part} is fastened`);
|
|
472
547
|
if (bad)
|
|
473
548
|
return bad;
|
|
474
549
|
}
|
|
550
|
+
turnTo(seat.args.slice(3, 6));
|
|
551
|
+
}
|
|
552
|
+
const motion = nodeById(m, `${part}.motion`);
|
|
553
|
+
if (motion) {
|
|
554
|
+
const unit = constants(motion.args.slice(0, 3));
|
|
555
|
+
if (unit.some(u => u === undefined))
|
|
556
|
+
return `${part}'s ${motion.op === 'axis-turn@1' ? 'turn' : 'slide'} axis is not a constant`;
|
|
557
|
+
const n = Math.hypot(...unit);
|
|
558
|
+
const u = unit.map(v => v / n);
|
|
559
|
+
if (motion.op === 'axis-turn@1') {
|
|
560
|
+
const range = angleRangeOf(m, motion.args[3]);
|
|
561
|
+
d = sweptBy(d, u, range && [(range[0] * Math.PI) / 180, (range[1] * Math.PI) / 180]);
|
|
562
|
+
}
|
|
563
|
+
else if (motion.op === 'axis-slide@1') {
|
|
564
|
+
/*
|
|
565
|
+
Its travel lies between the two ends of its control. One end at nothing and the other a sum of sizes: it
|
|
566
|
+
goes only that way, so the middle adds it only toward that side. Otherwise the further end, either way.
|
|
567
|
+
*/
|
|
568
|
+
const mid = d.mid.reduce((s, v, k) => s + v * u[k], 0);
|
|
569
|
+
const spread = d.spread.reduce((s, v, k) => s + v * Math.abs(u[k]), 0);
|
|
570
|
+
const [lo, hi] = ['min', 'max'].map(end => travelAt(m, asset, motion, end));
|
|
571
|
+
if (!lo || !hi)
|
|
572
|
+
return `${part}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
573
|
+
const nothing = (l) => !linNorm(l).terms.length && Math.abs(l.c) < 1e-12;
|
|
574
|
+
const sizes = (l) => l.c >= 0 && linNorm(l).terms.every(t => t.k > 0 && sizeValue(m, t.ref));
|
|
575
|
+
const other = nothing(lo) ? hi : nothing(hi) ? lo : null;
|
|
576
|
+
const way = other && (sizes(other) ? 1 : sizes(linAdd({ terms: [], c: 0 }, other, -1)) ? -1 : 0);
|
|
577
|
+
if (other && way)
|
|
578
|
+
out = linAdd(out, other, way * (Math.max(way * mid, 0) + spread));
|
|
579
|
+
else {
|
|
580
|
+
const further = [lo, hi].reduce((sum, l) => {
|
|
581
|
+
const size = sum && magnitudeOf(m, l);
|
|
582
|
+
return size && sum && linAtLeast(sum, size);
|
|
583
|
+
}, { terms: [], c: 0 });
|
|
584
|
+
if (!further)
|
|
585
|
+
return `${part}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
586
|
+
out = linAdd(out, further, Math.abs(mid) + spread);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else
|
|
590
|
+
return `${part} moves by ${motion.op}, which this command does not add up`;
|
|
591
|
+
}
|
|
592
|
+
const pose = nodeById(m, `${part}.pose`);
|
|
593
|
+
if (!pose)
|
|
594
|
+
return `${part} has no pose this command can read`;
|
|
595
|
+
const bad = step(pose.args.slice(0, 3), `${part}'s place`);
|
|
596
|
+
if (bad)
|
|
597
|
+
return bad;
|
|
598
|
+
turnTo(pose.args.slice(3, 6));
|
|
475
599
|
}
|
|
600
|
+
/*
|
|
601
|
+
Its own extent on every axis, as far as the directions reach across it: half its size either way about its
|
|
602
|
+
origin, or from face to face where they do not sit about it (a portal stands on 0, `offCentreEndsOf`) -- the
|
|
603
|
+
middle at the face it points to, the spread times the further face.
|
|
604
|
+
*/
|
|
605
|
+
for (const [i, axis] of AXES.entries()) {
|
|
606
|
+
const mid = d.mid[i], spread = d.spread[i];
|
|
607
|
+
if (tiny(mid) && tiny(spread))
|
|
608
|
+
continue;
|
|
609
|
+
const ends = offCentre[axis];
|
|
610
|
+
if (ends) {
|
|
611
|
+
const e = mid > 0 ? ends[1] : ends[0];
|
|
612
|
+
if (!tiny(mid))
|
|
613
|
+
out = linAdd(out, { terms: [{ ref: e.ref, k: e.k * mid }], c: 0 });
|
|
614
|
+
if (tiny(spread))
|
|
615
|
+
continue;
|
|
616
|
+
const further = ends.reduce((sum, x) => {
|
|
617
|
+
const size = sum && magnitudeOf(m, { terms: [{ ref: x.ref, k: x.k }], c: 0 });
|
|
618
|
+
return size && sum && linAtLeast(sum, size);
|
|
619
|
+
}, { terms: [], c: 0 });
|
|
620
|
+
if (!further)
|
|
621
|
+
return `${id}'s size is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
622
|
+
out = linAdd(out, further, spread);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const half = magnitudeOf(m, { terms: [{ ref: sizes[axis].ref, k: 0.5 * sizes[axis].k }], c: 0 });
|
|
626
|
+
if (!half)
|
|
627
|
+
return `${id}'s size is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
628
|
+
out = linAdd(out, half, Math.min(1, Math.abs(mid) + spread));
|
|
629
|
+
}
|
|
630
|
+
return linNorm(out);
|
|
631
|
+
};
|
|
632
|
+
const up = {};
|
|
633
|
+
const down = {};
|
|
634
|
+
for (const [i, axis] of AXES.entries()) {
|
|
635
|
+
const e = [0, 0, 0];
|
|
636
|
+
e[i] = 1;
|
|
637
|
+
const high = reachToward(e);
|
|
638
|
+
if (typeof high === 'string')
|
|
639
|
+
return high;
|
|
640
|
+
e[i] = -1;
|
|
641
|
+
const low = reachToward(e);
|
|
642
|
+
if (typeof low === 'string')
|
|
643
|
+
return low;
|
|
644
|
+
up[axis] = high;
|
|
645
|
+
down[axis] = low;
|
|
476
646
|
}
|
|
477
647
|
/*
|
|
478
648
|
The pivot: where the topmost turn is fastened, and everything above it. Above it nothing turns, but a slide may
|
|
479
649
|
carry it -- the telescopic conveyor's nose turns at the end of its third stage -- so the pivot is read at each
|
|
480
|
-
end of the controls, a
|
|
481
|
-
way (it had been refused, and the nose left out of the room).
|
|
650
|
+
end of the controls, a reach from each: a slide is a straight line, so the reaches from its two ends bound it all
|
|
651
|
+
the way (it had been refused, and the nose left out of the room).
|
|
482
652
|
*/
|
|
483
|
-
const pivotPart = chain[top];
|
|
484
|
-
const parent = parentOf(m, pivotPart);
|
|
485
653
|
const seat = nodeById(m, `${pivotPart}.seat.rigid`);
|
|
486
654
|
const seatAt = Object.fromEntries(AXES.map((a, i) => [a, seat ? linOf(m, seat.args[i]) : { terms: [], c: 0 }]));
|
|
487
655
|
const centres = [];
|
|
@@ -492,21 +660,40 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
492
660
|
return at;
|
|
493
661
|
centres.push(at);
|
|
494
662
|
}
|
|
495
|
-
return { centres,
|
|
663
|
+
return { centres, up, down };
|
|
496
664
|
}
|
|
497
665
|
/** The bounds the parts come to, as sums, with what could not be read named. */
|
|
498
|
-
function
|
|
666
|
+
function reachLins(asset) {
|
|
499
667
|
const m = asset.document.model;
|
|
500
668
|
const parts = [];
|
|
501
669
|
const skipped = [];
|
|
502
|
-
const bounded = [];
|
|
503
670
|
const low = { x: [], y: [], z: [] };
|
|
504
671
|
const high = { x: [], y: [], z: [] };
|
|
505
|
-
/*
|
|
506
|
-
|
|
672
|
+
/*
|
|
673
|
+
Parts carried by the same turn each bound their own reach about its pivot, and the room takes the largest when the
|
|
674
|
+
graph is evaluated (ADR-0087, 2026-10-21). One ball per pivot, its radius the larger of theirs term by term, was
|
|
675
|
+
every branch added up: each part's sizes and places are values of its own, and a humanoid's torso, head and two arms
|
|
676
|
+
under its waist came to 8210 mm across for a body 300 mm wide. A reach that another one passes at every size -- by a
|
|
677
|
+
number and sizes, never below zero -- is left out.
|
|
678
|
+
*/
|
|
679
|
+
const reaches = {
|
|
680
|
+
low: { x: [], y: [], z: [] },
|
|
681
|
+
high: { x: [], y: [], z: [] }
|
|
682
|
+
};
|
|
683
|
+
const atLeast = (a, b) => {
|
|
684
|
+
const gap = linNorm(linAdd(a, b, -1));
|
|
685
|
+
return gap.c >= -1e-9 && gap.terms.every(t => t.k > 0 && sizeValue(m, t.ref));
|
|
686
|
+
};
|
|
687
|
+
const addReach = (side, axis, lin) => {
|
|
688
|
+
const list = reaches[side][axis];
|
|
689
|
+
const further = (a, b) => (side === 'high' ? atLeast(a, b) : atLeast(b, a));
|
|
690
|
+
if (list.some(o => further(o, lin)))
|
|
691
|
+
return;
|
|
692
|
+
reaches[side][axis] = [...list.filter(o => !further(lin, o)), lin];
|
|
693
|
+
};
|
|
507
694
|
/* A row's template stands for its copies (`repeatPart`): leaving it out left a conveyor's rollers out of its room. */
|
|
508
695
|
const rowOf = (id) => nodeById(m, `${id}.repeat`);
|
|
509
|
-
const ends =
|
|
696
|
+
const ends = ['min', 'max'];
|
|
510
697
|
/* A part's size along the figure's axes, whichever quarter turns it and what carries it stand in. */
|
|
511
698
|
const sizesOf = (id, turns) => {
|
|
512
699
|
/* A turned chain's reach is read from one size per axis (reachOf), so a part there must have one. */
|
|
@@ -630,14 +817,14 @@ function occupancyLins(asset, over) {
|
|
|
630
817
|
*/
|
|
631
818
|
for (const { run } of rows) {
|
|
632
819
|
for (const l of run.lows)
|
|
633
|
-
low[run.axis].push(linAdd(l, reach.
|
|
820
|
+
low[run.axis].push(linAdd(l, reach.down[run.axis], -1));
|
|
634
821
|
for (const h of run.highs)
|
|
635
|
-
high[run.axis].push(linAdd(h, reach.
|
|
822
|
+
high[run.axis].push(linAdd(h, reach.up[run.axis], 1));
|
|
636
823
|
}
|
|
637
824
|
for (const at of reach.centres)
|
|
638
825
|
for (const axis of AXES.filter(a => !rowAxes.has(a))) {
|
|
639
|
-
const lo = linAdd(at[axis], reach.
|
|
640
|
-
const hi = linAdd(at[axis], reach.
|
|
826
|
+
const lo = linAdd(at[axis], reach.down[axis], -1);
|
|
827
|
+
const hi = linAdd(at[axis], reach.up[axis], 1);
|
|
641
828
|
low[axis].push(lo);
|
|
642
829
|
high[axis].push(hi);
|
|
643
830
|
climbed(lo, -1, low, axis);
|
|
@@ -645,12 +832,11 @@ function occupancyLins(asset, over) {
|
|
|
645
832
|
}
|
|
646
833
|
continue;
|
|
647
834
|
}
|
|
648
|
-
for (const at of reach.centres)
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
835
|
+
for (const at of reach.centres)
|
|
836
|
+
for (const axis of AXES) {
|
|
837
|
+
addReach('low', axis, linNorm(linAdd(at[axis], reach.down[axis], -1)));
|
|
838
|
+
addReach('high', axis, linNorm(linAdd(at[axis], reach.up[axis], 1)));
|
|
839
|
+
}
|
|
654
840
|
continue;
|
|
655
841
|
}
|
|
656
842
|
/*
|
|
@@ -740,40 +926,34 @@ function occupancyLins(asset, over) {
|
|
|
740
926
|
}
|
|
741
927
|
}
|
|
742
928
|
}
|
|
743
|
-
|
|
744
|
-
if (boundsUnder(m, part).length)
|
|
745
|
-
bounded.push(part);
|
|
746
|
-
return { parts, skipped, bounded, low, high, balls: [...balls.values()] };
|
|
929
|
+
return { parts, skipped, low, high, reaches };
|
|
747
930
|
}
|
|
748
931
|
const valueOfLin = (values, l) => l.terms.reduce((s, t) => s + t.k * (values[t.ref] ?? NaN), l.c);
|
|
749
|
-
/**
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
932
|
+
/**
|
|
933
|
+
* The room a figure sweeps (ADR-0145 2.3): every part over every joint's range, at the authored size or at `sizes`
|
|
934
|
+
* (design inputs, by id). Computed, never declared -- it holds the parts by what it is. Undefined for a figure with no
|
|
935
|
+
* part to measure. Reads the asset; changes nothing.
|
|
936
|
+
*/
|
|
937
|
+
export function figureMotionRangeOf(asset, sizes = {}) {
|
|
755
938
|
const source = structuredClone(asset);
|
|
756
|
-
const { parts, skipped,
|
|
757
|
-
for (const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
939
|
+
const { parts, skipped, low, high, reaches } = reachLins(source);
|
|
940
|
+
for (const axis of AXES) {
|
|
941
|
+
low[axis].push(...reaches.low[axis]);
|
|
942
|
+
high[axis].push(...reaches.high[axis]);
|
|
943
|
+
}
|
|
762
944
|
boundedSides(source.document.model, low, high);
|
|
763
945
|
if (!parts.length)
|
|
764
|
-
|
|
765
|
-
const values = compileFigureGraph(source.document.model).evaluate({ ...source.designInputs, ...source.stateDefaults })
|
|
946
|
+
return undefined;
|
|
947
|
+
const values = compileFigureGraph(source.document.model).evaluate({ ...source.designInputs, ...source.stateDefaults, ...sizes })
|
|
766
948
|
.values;
|
|
767
|
-
const
|
|
949
|
+
const bounds = Object.fromEntries(AXES.map(axis => [
|
|
768
950
|
axis,
|
|
769
951
|
{
|
|
770
952
|
min: round6(Math.min(...low[axis].map(l => valueOfLin(values, l)))),
|
|
771
953
|
max: round6(Math.max(...high[axis].map(l => valueOfLin(values, l))))
|
|
772
954
|
}
|
|
773
955
|
]));
|
|
774
|
-
|
|
775
|
-
const held = bounded.map(part => ({ part, mm: boundedWidthOf(m, values, boundsUnder(m, part)) }));
|
|
776
|
-
return { over, at, parts, skipped, ...(held.length ? { bounded: held } : {}), basis: fingerprintOf(source) };
|
|
956
|
+
return { bounds, parts, skipped };
|
|
777
957
|
}
|
|
778
958
|
/**
|
|
779
959
|
* The placement says which plane the mounting plane is. A part fastened to it in a way the new plane does not take --
|
|
@@ -797,87 +977,9 @@ export function setPlacement(asset, a) {
|
|
|
797
977
|
asset.placement = a.placement;
|
|
798
978
|
return asset;
|
|
799
979
|
}
|
|
800
|
-
|
|
801
|
-
const m = asset.document.model;
|
|
802
|
-
if (!a.proposal || (a.proposal.over !== 'rest' && a.proposal.over !== 'range'))
|
|
803
|
-
fail('SCHEMA', 'occupancy', 'a proposal is required; propose first, then confirm');
|
|
804
|
-
const now = fingerprintOf(asset);
|
|
805
|
-
if (a.proposal.basis !== now)
|
|
806
|
-
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');
|
|
807
|
-
/* Declared with a placement, the placement is set by the same command as on its own: one value, one check. */
|
|
808
|
-
if (a.placement !== undefined)
|
|
809
|
-
setPlacement(asset, { kind: 'set-placement', placement: a.placement });
|
|
810
|
-
const padding = {};
|
|
811
|
-
for (const axis of AXES) {
|
|
812
|
-
const pad = a.padding?.[axis] ?? 0;
|
|
813
|
-
if (!(Number.isFinite(pad) && pad >= 0))
|
|
814
|
-
fail('OCCUPANCY_PADDING', `occupancy.${axis}`, `padding is millimetres, zero or more; got ${String(pad)}`);
|
|
815
|
-
padding[axis] = pad;
|
|
816
|
-
}
|
|
817
|
-
const { parts, low, high, balls } = occupancyLins(asset, a.proposal.over);
|
|
818
|
-
/* A turning chain's ball, its reach either way about its pivot on each axis. */
|
|
819
|
-
for (const ball of balls)
|
|
820
|
-
for (const axis of AXES) {
|
|
821
|
-
low[axis].push(linAdd(ball.centre[axis], ball.radius, -1));
|
|
822
|
-
high[axis].push(linAdd(ball.centre[axis], ball.radius, 1));
|
|
823
|
-
}
|
|
824
|
-
boundedSides(m, low, high);
|
|
825
|
-
for (const axis of AXES) {
|
|
826
|
-
if (!padding[axis])
|
|
827
|
-
continue;
|
|
828
|
-
low[axis] = low[axis].map((l) => ({ ...l, c: l.c - padding[axis] }));
|
|
829
|
-
high[axis] = high[axis].map((l) => ({ ...l, c: l.c + padding[axis] }));
|
|
830
|
-
}
|
|
831
|
-
if (parts.join('|') !== a.proposal.parts.join('|'))
|
|
832
|
-
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`);
|
|
833
|
-
/* One node for the six bounds (affine-bounds@1): each candidate its constant, then a value and a ratio per term. */
|
|
834
|
-
const args = [];
|
|
835
|
-
const layout = AXES.flatMap(axis => [low[axis], high[axis]]).map(side => side
|
|
836
|
-
.map((l) => {
|
|
837
|
-
const n = throughSums(m, l);
|
|
838
|
-
args.push(constantOf(m, 'mm', round6(n.c), 'd'));
|
|
839
|
-
for (const t of n.terms)
|
|
840
|
-
args.push(t.ref, constantOf(m, 'ratio', t.k, 'k'));
|
|
841
|
-
return n.terms.length;
|
|
842
|
-
})
|
|
843
|
-
.join('_'));
|
|
844
|
-
const id = namer(m, 'occupancy')('bounds');
|
|
845
|
-
const port = (p) => `${id}.${p}`;
|
|
846
|
-
m.nodes.push({
|
|
847
|
-
id,
|
|
848
|
-
op: 'affine-bounds@1',
|
|
849
|
-
args,
|
|
850
|
-
params: { layout: `L${layout.join('.')}` },
|
|
851
|
-
outputs: Object.fromEntries(['xmin', 'xmax', 'ymin', 'ymax', 'zmin', 'zmax'].map(p => [p, port(p)]))
|
|
852
|
-
});
|
|
853
|
-
const bounds = Object.fromEntries(AXES.map(axis => [axis, { min: port(`${axis}min`), max: port(`${axis}max`) }]));
|
|
854
|
-
const was = stringsIn(asset.occupancy?.bounds);
|
|
855
|
-
asset.occupancy = { ...(asset.occupancy?.contact ? { contact: asset.occupancy.contact } : {}), bounds };
|
|
856
|
-
/*
|
|
857
|
-
The room declared before goes with what fed only it. Left in the graph, it kept reading where parts had been
|
|
858
|
-
fastened, and the next fastening was refused over a room no one read (the gantry, 2026-09-25).
|
|
859
|
-
*/
|
|
860
|
-
for (const ref of was)
|
|
861
|
-
dropIfUnused(asset, ref);
|
|
862
|
-
/*
|
|
863
|
-
A part's own measure the old room alone still read -- the part follows something else now -- goes too: kept, it
|
|
864
|
-
was a dial that changed nothing (the turntable after every part followed the one ratio, 2026-09-25). So does its
|
|
865
|
-
own position or turn, once a fastening places it: a floor part's height position stayed as a field that moved
|
|
866
|
-
nothing (browser check, ADR-0097, 2026-09-27). The room's refit names them before it is confirmed (figureRoomRefitOf).
|
|
867
|
-
*/
|
|
868
|
-
const placed = new Set(m.nodes.filter((n) => n.op === 'place@1' || n.op === 'member@1').map((n) => n.id));
|
|
869
|
-
for (const input of [...m.inputs])
|
|
870
|
-
if (input.role === 'design' &&
|
|
871
|
-
placed.has(String(input.id).split('.')[0]) &&
|
|
872
|
-
/\.(dimension|size|position|rotation)\.[a-z]+$/.test(input.id))
|
|
873
|
-
dropIfUnused(asset, input.id, input.id);
|
|
874
|
-
return asset;
|
|
875
|
-
}
|
|
876
|
-
/** "This face is what the figure is mounted on." Kept apart from how much room the figure takes up. */
|
|
980
|
+
/** "This face is what the figure is mounted on." */
|
|
877
981
|
export function setMountingFace(asset, a) {
|
|
878
982
|
const m = asset.document.model;
|
|
879
|
-
if (!asset.occupancy)
|
|
880
|
-
fail('OCCUPANCY_UNDECLARED', 'occupancy', 'declare the room the figure takes up before naming the face it stands on');
|
|
881
983
|
if (!Object.hasOwn(FIGURE_FACES, String(a.face)))
|
|
882
984
|
fail('SCHEMA', a.part, `${String(a.face)} is not a face`);
|
|
883
985
|
const { place } = partOf(m, a.part, a.part);
|
|
@@ -890,16 +992,16 @@ export function setMountingFace(asset, a) {
|
|
|
890
992
|
outputs: { pose: `${id}.value` },
|
|
891
993
|
params: { frame: `${id}.frame`, name: a.face }
|
|
892
994
|
});
|
|
893
|
-
asset.
|
|
995
|
+
asset.mountingFace = {
|
|
894
996
|
plane: 'mounting-plane',
|
|
895
997
|
surface: { placement: place.outputs.placed, feature: `${id}.value` }
|
|
896
998
|
};
|
|
897
999
|
return asset;
|
|
898
1000
|
}
|
|
899
1001
|
export function clearMountingFace(asset) {
|
|
900
|
-
if (!asset.
|
|
901
|
-
fail('EDIT_TARGET', '
|
|
902
|
-
delete asset.
|
|
1002
|
+
if (!asset.mountingFace)
|
|
1003
|
+
fail('EDIT_TARGET', 'mountingFace', 'this figure names no mounting face');
|
|
1004
|
+
delete asset.mountingFace;
|
|
903
1005
|
return asset;
|
|
904
1006
|
}
|
|
905
|
-
//# sourceMappingURL=authoring-
|
|
1007
|
+
//# sourceMappingURL=authoring-placement.js.map
|