@hatiolab/figure-model 0.9.5 → 0.9.6
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/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +13 -20
- package/dist/authoring-actions.js.map +1 -1
- 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-occupancy.d.ts.map +1 -1
- package/dist/authoring-occupancy.js +284 -75
- package/dist/authoring-occupancy.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 +28 -98
- package/dist/authoring-size.js.map +1 -1
- package/dist/authoring-types.d.ts +10 -22
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/contract.d.ts +4 -16
- 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/gate.d.ts.map +1 -1
- package/dist/gate.js +28 -8
- package/dist/gate.js.map +1 -1
- package/dist/graph-types.d.ts +15 -2
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/graph.js +15 -6
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/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-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.js +1 -1
- package/dist/part-edit.d.ts.map +1 -1
- package/dist/part-edit.js +2 -2
- package/dist/part-edit.js.map +1 -1
- package/dist/proposal.js +7 -7
- 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/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +7 -2
- package/dist/release-contract.js.map +1 -1
- package/dist/release-metrics.d.ts.map +1 -1
- package/dist/release-metrics.js +6 -2
- package/dist/release-metrics.js.map +1 -1
- package/dist/resize-plan.d.ts.map +1 -1
- package/dist/resize-plan.js +4 -9
- package/dist/resize-plan.js.map +1 -1
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +13 -4
- package/dist/room.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/package.json +1 -1
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { upgradeFigurePlacement, figurePlacementOf } from "./asset.js";
|
|
8
8
|
import { compileFigureGraph } from "./graph.js";
|
|
9
|
+
import { figureShapeDescriptionOf } from "./shape-ops.js";
|
|
9
10
|
import { FigureContractError } from "./contract.js";
|
|
10
11
|
import { AXES } from "./words.js";
|
|
11
12
|
import { FIGURE_PLACEMENTS } from "./asset-types.js";
|
|
12
13
|
import { FIGURE_FACES } from "./authoring-types.js";
|
|
13
14
|
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
|
+
import { isReach, sizeRefsOf, occupiedExtentsOf, angleOf, turnMatrix, mul3, standingRefsOf, half, axisTurn, offCentreEndsOf, IDENTITY } from "./authoring-geometry.js";
|
|
15
16
|
/** A figure as one value, for saying "this is the figure I read". The declaration itself is left out of it. */
|
|
16
17
|
function fingerprintOf(asset) {
|
|
17
18
|
/*
|
|
@@ -133,7 +134,7 @@ function poseTurnOf(m, asset, part) {
|
|
|
133
134
|
const seatTurned = (m, asset, part) => (nodeById(m, `${part}.seat.rigid`)?.args.slice(3, 6) ?? []).some((ref) => angleOf(m, asset, ref) !== 0);
|
|
134
135
|
/**
|
|
135
136
|
* 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,
|
|
137
|
+
* turned: a box cannot follow it, its reach about the point it turns at can (`reachOf`).
|
|
137
138
|
*/
|
|
138
139
|
function carriedByATurn(m, asset, id) {
|
|
139
140
|
for (let part = id; part; part = parentOf(m, part)) {
|
|
@@ -258,8 +259,9 @@ function sizeValue(m, ref, seen = 0) {
|
|
|
258
259
|
if (seen > 64)
|
|
259
260
|
return false;
|
|
260
261
|
const input = m.inputs.find((i) => i.id === ref);
|
|
262
|
+
/* The instance size is never below zero, its smallest end given or not (ADR-0087 decision 6). */
|
|
261
263
|
if (input)
|
|
262
|
-
return input.role !== 'state' && input.min >= 0;
|
|
264
|
+
return input.role !== 'state' && (/^size\.[xyz]$/.test(input.id) || input.min >= 0);
|
|
263
265
|
const w = writerOf(m, ref);
|
|
264
266
|
const positive = (r) => (m.constants.find((c) => c.id === r)?.value ?? -1) > 0;
|
|
265
267
|
if (w?.op === 'geomean@1')
|
|
@@ -413,14 +415,111 @@ function boundedSides(m, low, high) {
|
|
|
413
415
|
high[axis] = high[axis].flatMap(l => boundsOf(m, l, true));
|
|
414
416
|
}
|
|
415
417
|
}
|
|
418
|
+
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
|
+
/** 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
|
+
const rigidTurnOf = ([x, y, z]) => mul3(mul3(axisTurn('x', x), axisTurn('y', y)), axisTurn('z', z));
|
|
421
|
+
/** The directions seen from a frame turned by `r` from this one (a point there is r·x here). */
|
|
422
|
+
function turnedBy(d, r) {
|
|
423
|
+
if ('one' in d)
|
|
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))) };
|
|
426
|
+
}
|
|
427
|
+
/** The largest |a + b cos t + c sin t| for t from lo to hi, in radians; all the way round without a range. */
|
|
428
|
+
function largestOver(a, b, c, range) {
|
|
429
|
+
if (!range)
|
|
430
|
+
return Math.abs(a) + Math.hypot(b, c);
|
|
431
|
+
const [lo, hi] = range;
|
|
432
|
+
const at = (t) => Math.abs(a + b * Math.cos(t) + c * Math.sin(t));
|
|
433
|
+
let most = Math.max(at(lo), at(hi));
|
|
434
|
+
/* Where its slope is zero: t = atan2(c, b) and half a turn from it, every whole turn over. */
|
|
435
|
+
const turn = 2 * Math.PI;
|
|
436
|
+
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)
|
|
438
|
+
most = Math.max(most, at(t));
|
|
439
|
+
return most;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 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.
|
|
444
|
+
*/
|
|
445
|
+
function sweptBy(d, u, range) {
|
|
446
|
+
const K = [
|
|
447
|
+
[0, -u[2], u[1]],
|
|
448
|
+
[u[2], 0, -u[0]],
|
|
449
|
+
[-u[1], u[0], 0]
|
|
450
|
+
];
|
|
451
|
+
const A = (i, j) => u[i] * u[j];
|
|
452
|
+
const B = (i, j) => (i === j ? 1 : 0) - u[i] * u[j];
|
|
453
|
+
const C = (i, j) => K[i][j];
|
|
454
|
+
if (range && Math.abs(range[1] - range[0]) < 1e-12) {
|
|
455
|
+
const t = range[0];
|
|
456
|
+
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
|
+
}
|
|
458
|
+
if ('one' in d) {
|
|
459
|
+
const v = d.one;
|
|
460
|
+
const sum = (f, j) => [0, 1, 2].reduce((s, i) => s + f(i, j) * v[i], 0);
|
|
461
|
+
const parts = [0, 1, 2].map(j => [sum(A, j), sum(B, j), sum(C, j)]);
|
|
462
|
+
/* Along the axis it is turned about, a direction does not move. */
|
|
463
|
+
if (parts.every(([, b, c]) => Math.abs(b) < 1e-12 && Math.abs(c) < 1e-12))
|
|
464
|
+
return d;
|
|
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
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The angles a turn's control can give it, in degrees: its limits, scaled and moved as the angle is written from it.
|
|
473
|
+
* Null -- all the way round -- for a control without limits or that turns a whole turn, and for an angle written any
|
|
474
|
+
* other way.
|
|
475
|
+
*/
|
|
476
|
+
function angleRangeOf(m, ref, depth = 0) {
|
|
477
|
+
if (depth > 64)
|
|
478
|
+
return null;
|
|
479
|
+
const constant = m.constants.find((c) => c.id === ref);
|
|
480
|
+
if (constant)
|
|
481
|
+
return constant.unit === 'deg' ? [constant.value, constant.value] : null;
|
|
482
|
+
const input = m.inputs.find((i) => i.id === ref);
|
|
483
|
+
if (input) {
|
|
484
|
+
if (input.role !== 'state' || input.unit !== 'deg')
|
|
485
|
+
return null;
|
|
486
|
+
const { min, max } = input;
|
|
487
|
+
return Number.isFinite(min) && Number.isFinite(max) && max - min < 360 ? [min, max] : null;
|
|
488
|
+
}
|
|
489
|
+
const w = writerOf(m, ref);
|
|
490
|
+
if (w?.op === 'add@1') {
|
|
491
|
+
let lo = 0, hi = 0;
|
|
492
|
+
for (const arg of w.args) {
|
|
493
|
+
const r = angleRangeOf(m, arg, depth + 1);
|
|
494
|
+
if (!r)
|
|
495
|
+
return null;
|
|
496
|
+
lo += r[0];
|
|
497
|
+
hi += r[1];
|
|
498
|
+
}
|
|
499
|
+
return hi - lo < 360 ? [lo, hi] : null;
|
|
500
|
+
}
|
|
501
|
+
if (w?.op === 'mul@1' && w.args.length === 2) {
|
|
502
|
+
const factor = (arg) => m.constants.find((c) => c.id === arg && c.unit === 'ratio')?.value;
|
|
503
|
+
const [a, b] = w.args;
|
|
504
|
+
const k = factor(a) ?? factor(b);
|
|
505
|
+
const r = factor(a) !== undefined ? angleRangeOf(m, b, depth + 1) : factor(b) !== undefined ? angleRangeOf(m, a, depth + 1) : null;
|
|
506
|
+
if (k === undefined || !r)
|
|
507
|
+
return null;
|
|
508
|
+
const ends = [r[0] * k, r[1] * k].sort((x, y) => x - y);
|
|
509
|
+
return ends[1] - ends[0] < 360 ? ends : null;
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
416
513
|
/**
|
|
417
|
-
* Where a part carried by a turn can reach
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* the
|
|
514
|
+
* 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: while
|
|
516
|
+
* the directions are one, what it adds, sign included; once a joint has turned them through its range, each axis's
|
|
517
|
+
* largest times the size of that coordinate -- no more than its length, when its coordinates are numbers. The part
|
|
518
|
+
* adds its own half size the same way. Each step's largest taken on its own (the triangle inequality), it holds at
|
|
519
|
+
* every angle the joints can reach and every design size: a bound, not a sample. Read by the proposal; the release
|
|
520
|
+
* gate still checks the parts against it.
|
|
422
521
|
*/
|
|
423
|
-
function reachOf(m, asset, id, ends
|
|
522
|
+
function reachOf(m, asset, id, ends) {
|
|
424
523
|
const chain = [];
|
|
425
524
|
for (let part = id; part; part = parentOf(m, part))
|
|
426
525
|
chain.push(part);
|
|
@@ -432,56 +531,149 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
432
531
|
: found, -1);
|
|
433
532
|
if (top < 0)
|
|
434
533
|
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. */
|
|
534
|
+
const pivotPart = chain[top];
|
|
535
|
+
const parent = parentOf(m, pivotPart);
|
|
536
|
+
/* Above the pivot every part stands in quarter turns: the frame it turns in is a signed axis permutation. */
|
|
537
|
+
const frame = parent ? worldTurnOf(m, asset, parent) : IDENTITY;
|
|
538
|
+
if (!frame)
|
|
539
|
+
return `${pivotPart} is carried by a part turned by other than quarter turns`;
|
|
444
540
|
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
|
-
for (const ref of seat?.args.slice(0, 3) ?? []) {
|
|
464
|
-
const bad = grow(linOf(m, ref), `where ${part} is fastened`);
|
|
465
|
-
if (bad)
|
|
466
|
-
return bad;
|
|
541
|
+
const { shape } = partOf(m, id, id);
|
|
542
|
+
const faces = figureShapeDescriptionOf(shape)?.faces;
|
|
543
|
+
const offCentre = Object.fromEntries(AXES.map(axis => [axis, offCentreEndsOf(shape, faces, axis)]));
|
|
544
|
+
const constants = (refs) => refs.map(r => m.constants.find((c) => c.id === r)?.value);
|
|
545
|
+
const angles = (refs) => refs.map(r => angleOf(m, asset, r));
|
|
546
|
+
const reachToward = (e) => {
|
|
547
|
+
let d = { one: transposedTimes(frame, e) };
|
|
548
|
+
let out = { terms: [], c: 0 };
|
|
549
|
+
const most = (dir) => ('one' in dir ? dir.one.map(Math.abs) : dir.most);
|
|
550
|
+
/* A step of three coordinates, in the frame the directions are seen from. */
|
|
551
|
+
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;
|
|
467
559
|
}
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
560
|
+
const w = d.most;
|
|
561
|
+
const numbers = constants(refs);
|
|
562
|
+
if (numbers.every(n => n !== undefined)) {
|
|
563
|
+
const across = numbers.reduce((s, n, i) => s + w[i] * Math.abs(n), 0);
|
|
564
|
+
const length = Math.hypot(...numbers.map((n, i) => (w[i] > 0 ? n : 0)));
|
|
565
|
+
out = linAdd(out, { terms: [], c: Math.min(across, length) });
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
for (const [i, ref] of refs.entries()) {
|
|
569
|
+
if (!w[i])
|
|
570
|
+
continue;
|
|
571
|
+
const size = magnitudeOf(m, linOf(m, ref));
|
|
572
|
+
if (!size)
|
|
573
|
+
return `${what} is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
574
|
+
out = linAdd(out, size, w[i]);
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
};
|
|
578
|
+
const turnTo = (refs) => {
|
|
579
|
+
const a = angles(refs);
|
|
580
|
+
if (a.every(t => t === 0))
|
|
581
|
+
return;
|
|
582
|
+
d = a.every(t => t !== null && Number.isFinite(t))
|
|
583
|
+
? turnedBy(d, rigidTurnOf(a))
|
|
584
|
+
: { most: [0, 1, 2].map(() => Math.min(1, most(d).reduce((s, v) => s + v, 0))) };
|
|
585
|
+
};
|
|
586
|
+
for (let i = top; i >= 0; i--) {
|
|
587
|
+
const part = chain[i];
|
|
588
|
+
const seat = nodeById(m, `${part}.seat.rigid`);
|
|
589
|
+
if (seat) {
|
|
590
|
+
/* The pivot's own seat is where it turns: it is the centre, not a step. */
|
|
591
|
+
if (i < top) {
|
|
592
|
+
const bad = step(seat.args.slice(0, 3), `where ${part} is fastened`);
|
|
472
593
|
if (bad)
|
|
473
594
|
return bad;
|
|
474
595
|
}
|
|
596
|
+
turnTo(seat.args.slice(3, 6));
|
|
597
|
+
}
|
|
598
|
+
const motion = nodeById(m, `${part}.motion`);
|
|
599
|
+
if (motion) {
|
|
600
|
+
const unit = constants(motion.args.slice(0, 3));
|
|
601
|
+
if (unit.some(u => u === undefined))
|
|
602
|
+
return `${part}'s ${motion.op === 'axis-turn@1' ? 'turn' : 'slide'} axis is not a constant`;
|
|
603
|
+
const n = Math.hypot(...unit);
|
|
604
|
+
const u = unit.map(v => v / n);
|
|
605
|
+
if (motion.op === 'axis-turn@1') {
|
|
606
|
+
const range = angleRangeOf(m, motion.args[3]);
|
|
607
|
+
d = sweptBy(d, u, range && [(range[0] * Math.PI) / 180, (range[1] * Math.PI) / 180]);
|
|
608
|
+
}
|
|
609
|
+
else if (motion.op === 'axis-slide@1') {
|
|
610
|
+
const along = most(d).reduce((s, v, k) => s + v * Math.abs(u[k]), 0);
|
|
611
|
+
for (const end of ['min', 'max']) {
|
|
612
|
+
const travel = travelAt(m, asset, motion, end);
|
|
613
|
+
const size = travel && magnitudeOf(m, travel);
|
|
614
|
+
if (!size)
|
|
615
|
+
return `${part}'s travel is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
616
|
+
out = linAdd(out, size, along);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else
|
|
620
|
+
return `${part} moves by ${motion.op}, which this command does not add up`;
|
|
621
|
+
}
|
|
622
|
+
const pose = nodeById(m, `${part}.pose`);
|
|
623
|
+
if (!pose)
|
|
624
|
+
return `${part} has no pose this command can read`;
|
|
625
|
+
const bad = step(pose.args.slice(0, 3), `${part}'s place`);
|
|
626
|
+
if (bad)
|
|
627
|
+
return bad;
|
|
628
|
+
turnTo(pose.args.slice(3, 6));
|
|
475
629
|
}
|
|
630
|
+
/*
|
|
631
|
+
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`).
|
|
633
|
+
*/
|
|
634
|
+
const w = most(d);
|
|
635
|
+
for (const [i, axis] of AXES.entries()) {
|
|
636
|
+
if (!w[i])
|
|
637
|
+
continue;
|
|
638
|
+
const ends = offCentre[axis];
|
|
639
|
+
if (ends && 'one' in d) {
|
|
640
|
+
const e = d.one[i] > 0 ? ends[1] : ends[0];
|
|
641
|
+
out = linAdd(out, { terms: [{ ref: e.ref, k: e.k * d.one[i] }], c: 0 });
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const reach = ends
|
|
645
|
+
? ends.reduce((sum, e) => {
|
|
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)
|
|
651
|
+
return `${id}'s size is not a sum of sizes, so how far it reaches cannot be bounded`;
|
|
652
|
+
out = linAdd(out, reach, w[i]);
|
|
653
|
+
}
|
|
654
|
+
return linNorm(out);
|
|
655
|
+
};
|
|
656
|
+
const up = {};
|
|
657
|
+
const down = {};
|
|
658
|
+
for (const [i, axis] of AXES.entries()) {
|
|
659
|
+
const e = [0, 0, 0];
|
|
660
|
+
e[i] = 1;
|
|
661
|
+
const high = reachToward(e);
|
|
662
|
+
if (typeof high === 'string')
|
|
663
|
+
return high;
|
|
664
|
+
e[i] = -1;
|
|
665
|
+
const low = reachToward(e);
|
|
666
|
+
if (typeof low === 'string')
|
|
667
|
+
return low;
|
|
668
|
+
up[axis] = high;
|
|
669
|
+
down[axis] = low;
|
|
476
670
|
}
|
|
477
671
|
/*
|
|
478
672
|
The pivot: where the topmost turn is fastened, and everything above it. Above it nothing turns, but a slide may
|
|
479
673
|
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).
|
|
674
|
+
end of the controls, a reach from each: a slide is a straight line, so the reaches from its two ends bound it all
|
|
675
|
+
the way (it had been refused, and the nose left out of the room).
|
|
482
676
|
*/
|
|
483
|
-
const pivotPart = chain[top];
|
|
484
|
-
const parent = parentOf(m, pivotPart);
|
|
485
677
|
const seat = nodeById(m, `${pivotPart}.seat.rigid`);
|
|
486
678
|
const seatAt = Object.fromEntries(AXES.map((a, i) => [a, seat ? linOf(m, seat.args[i]) : { terms: [], c: 0 }]));
|
|
487
679
|
const centres = [];
|
|
@@ -492,7 +684,7 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
492
684
|
return at;
|
|
493
685
|
centres.push(at);
|
|
494
686
|
}
|
|
495
|
-
return { centres,
|
|
687
|
+
return { centres, up, down };
|
|
496
688
|
}
|
|
497
689
|
/** The bounds the parts come to, as sums, with what could not be read named. */
|
|
498
690
|
function occupancyLins(asset, over) {
|
|
@@ -502,8 +694,28 @@ function occupancyLins(asset, over) {
|
|
|
502
694
|
const bounded = [];
|
|
503
695
|
const low = { x: [], y: [], z: [] };
|
|
504
696
|
const high = { x: [], y: [], z: [] };
|
|
505
|
-
/*
|
|
506
|
-
|
|
697
|
+
/*
|
|
698
|
+
Parts carried by the same turn each bound their own reach about its pivot, and the room takes the largest when the
|
|
699
|
+
graph is evaluated (ADR-0087, 2026-10-21). One ball per pivot, its radius the larger of theirs term by term, was
|
|
700
|
+
every branch added up: each part's sizes and places are values of its own, and a humanoid's torso, head and two arms
|
|
701
|
+
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
|
|
702
|
+
number and sizes, never below zero -- is left out.
|
|
703
|
+
*/
|
|
704
|
+
const reaches = {
|
|
705
|
+
low: { x: [], y: [], z: [] },
|
|
706
|
+
high: { x: [], y: [], z: [] }
|
|
707
|
+
};
|
|
708
|
+
const atLeast = (a, b) => {
|
|
709
|
+
const gap = linNorm(linAdd(a, b, -1));
|
|
710
|
+
return gap.c >= -1e-9 && gap.terms.every(t => t.k > 0 && sizeValue(m, t.ref));
|
|
711
|
+
};
|
|
712
|
+
const addReach = (side, axis, lin) => {
|
|
713
|
+
const list = reaches[side][axis];
|
|
714
|
+
const further = (a, b) => (side === 'high' ? atLeast(a, b) : atLeast(b, a));
|
|
715
|
+
if (list.some(o => further(o, lin)))
|
|
716
|
+
return;
|
|
717
|
+
reaches[side][axis] = [...list.filter(o => !further(lin, o)), lin];
|
|
718
|
+
};
|
|
507
719
|
/* A row's template stands for its copies (`repeatPart`): leaving it out left a conveyor's rollers out of its room. */
|
|
508
720
|
const rowOf = (id) => nodeById(m, `${id}.repeat`);
|
|
509
721
|
const ends = over === 'rest' ? ['start'] : ['min', 'max'];
|
|
@@ -630,14 +842,14 @@ function occupancyLins(asset, over) {
|
|
|
630
842
|
*/
|
|
631
843
|
for (const { run } of rows) {
|
|
632
844
|
for (const l of run.lows)
|
|
633
|
-
low[run.axis].push(linAdd(l, reach.
|
|
845
|
+
low[run.axis].push(linAdd(l, reach.down[run.axis], -1));
|
|
634
846
|
for (const h of run.highs)
|
|
635
|
-
high[run.axis].push(linAdd(h, reach.
|
|
847
|
+
high[run.axis].push(linAdd(h, reach.up[run.axis], 1));
|
|
636
848
|
}
|
|
637
849
|
for (const at of reach.centres)
|
|
638
850
|
for (const axis of AXES.filter(a => !rowAxes.has(a))) {
|
|
639
|
-
const lo = linAdd(at[axis], reach.
|
|
640
|
-
const hi = linAdd(at[axis], reach.
|
|
851
|
+
const lo = linAdd(at[axis], reach.down[axis], -1);
|
|
852
|
+
const hi = linAdd(at[axis], reach.up[axis], 1);
|
|
641
853
|
low[axis].push(lo);
|
|
642
854
|
high[axis].push(hi);
|
|
643
855
|
climbed(lo, -1, low, axis);
|
|
@@ -645,12 +857,11 @@ function occupancyLins(asset, over) {
|
|
|
645
857
|
}
|
|
646
858
|
continue;
|
|
647
859
|
}
|
|
648
|
-
for (const at of reach.centres)
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
860
|
+
for (const at of reach.centres)
|
|
861
|
+
for (const axis of AXES) {
|
|
862
|
+
addReach('low', axis, linNorm(linAdd(at[axis], reach.down[axis], -1)));
|
|
863
|
+
addReach('high', axis, linNorm(linAdd(at[axis], reach.up[axis], 1)));
|
|
864
|
+
}
|
|
654
865
|
continue;
|
|
655
866
|
}
|
|
656
867
|
/*
|
|
@@ -743,7 +954,7 @@ function occupancyLins(asset, over) {
|
|
|
743
954
|
for (const part of parts)
|
|
744
955
|
if (boundsUnder(m, part).length)
|
|
745
956
|
bounded.push(part);
|
|
746
|
-
return { parts, skipped, bounded, low, high,
|
|
957
|
+
return { parts, skipped, bounded, low, high, reaches };
|
|
747
958
|
}
|
|
748
959
|
const valueOfLin = (values, l) => l.terms.reduce((s, t) => s + t.k * (values[t.ref] ?? NaN), l.c);
|
|
749
960
|
/** What the parts come to, and what a person is being asked to confirm. Reads the asset; changes nothing. */
|
|
@@ -753,12 +964,11 @@ export function proposeFigureOccupancy(asset, options = {}) {
|
|
|
753
964
|
if (over !== 'rest' && over !== 'range')
|
|
754
965
|
fail('SCHEMA', 'over', `${String(over)} is neither rest nor range`);
|
|
755
966
|
const source = structuredClone(asset);
|
|
756
|
-
const { parts, skipped, bounded, low, high,
|
|
757
|
-
for (const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
967
|
+
const { parts, skipped, bounded, low, high, reaches } = occupancyLins(source, over);
|
|
968
|
+
for (const axis of AXES) {
|
|
969
|
+
low[axis].push(...reaches.low[axis]);
|
|
970
|
+
high[axis].push(...reaches.high[axis]);
|
|
971
|
+
}
|
|
762
972
|
boundedSides(source.document.model, low, high);
|
|
763
973
|
if (!parts.length)
|
|
764
974
|
fail('OCCUPANCY_NO_PARTS', 'occupancy', `no part could be measured${skipped.length ? `: ${skipped.map(s => `${s.part} — ${s.reason}`).join('; ')}` : ''}`);
|
|
@@ -814,13 +1024,12 @@ export function declareOccupancy(asset, a) {
|
|
|
814
1024
|
fail('OCCUPANCY_PADDING', `occupancy.${axis}`, `padding is millimetres, zero or more; got ${String(pad)}`);
|
|
815
1025
|
padding[axis] = pad;
|
|
816
1026
|
}
|
|
817
|
-
const { parts, low, high,
|
|
818
|
-
/*
|
|
819
|
-
for (const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
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
|
+
}
|
|
824
1033
|
boundedSides(m, low, high);
|
|
825
1034
|
for (const axis of AXES) {
|
|
826
1035
|
if (!padding[axis])
|