@hatiolab/figure-model 0.9.4 → 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 +16 -23
- package/dist/authoring-actions.js.map +1 -1
- package/dist/authoring-fastening.d.ts.map +1 -1
- package/dist/authoring-fastening.js +182 -13
- package/dist/authoring-fastening.js.map +1 -1
- package/dist/authoring-geometry.d.ts +17 -1
- package/dist/authoring-geometry.d.ts.map +1 -1
- package/dist/authoring-geometry.js +66 -5
- package/dist/authoring-geometry.js.map +1 -1
- package/dist/authoring-nodes.d.ts +20 -0
- package/dist/authoring-nodes.d.ts.map +1 -1
- package/dist/authoring-nodes.js +34 -0
- package/dist/authoring-nodes.js.map +1 -1
- package/dist/authoring-occupancy.d.ts.map +1 -1
- package/dist/authoring-occupancy.js +419 -109
- 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 +43 -23
- 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/face-overhang.d.ts +22 -2
- package/dist/face-overhang.d.ts.map +1 -1
- package/dist/face-overhang.js +139 -1
- package/dist/face-overhang.js.map +1 -1
- package/dist/fasten-place.d.ts +77 -0
- package/dist/fasten-place.d.ts.map +1 -1
- package/dist/fasten-place.js +214 -2
- package/dist/fasten-place.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 +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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 +69 -8
- 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 +9 -9
- 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-edit.d.ts +1 -1
- package/dist/shape-edit.js +1 -1
- package/dist/shape-extreme.d.ts +55 -4
- package/dist/shape-extreme.d.ts.map +1 -1
- package/dist/shape-extreme.js +118 -4
- package/dist/shape-extreme.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.d.ts +32 -8
- package/dist/size-edit.d.ts.map +1 -1
- package/dist/size-edit.js +68 -125
- package/dist/size-edit.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
|
-
import { fail, nodeById, writerOf, 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";
|
|
14
|
+
import { fail, nodeById, writerOf, throughSums, rowHolding, partOf, namer, constantOf, dropIfUnused, parentOf, round6, stringsIn } from "./authoring-nodes.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
|
/*
|
|
@@ -59,34 +60,6 @@ const linOf = (m, ref) => {
|
|
|
59
60
|
const constant = m.constants.find((c) => c.id === ref);
|
|
60
61
|
return constant ? { terms: [], c: constant.value } : { terms: [{ ref, k: 1 }], c: 0 };
|
|
61
62
|
};
|
|
62
|
-
/**
|
|
63
|
-
* The same sum read back through the sums and scalings that make it up, down to what no command rewrites: the inputs,
|
|
64
|
-
* and values that are not a sum (a row's count). A declared room that read a node a joint writes -- where a leg stands,
|
|
65
|
-
* a share of the figure's depth -- was broken by the next fastening, which writes that joint's nodes afresh: the gantry
|
|
66
|
-
* could not be re-fastened once its room was declared (UNKNOWN_REFERENCE, 2026-09-25).
|
|
67
|
-
*/
|
|
68
|
-
function linThroughSums(m, lin) {
|
|
69
|
-
const constant = (ref) => m.constants.find((c) => c.id === ref)?.value;
|
|
70
|
-
const ratio = (ref) => m.constants.find((c) => c.id === ref && c.unit === 'ratio')?.value;
|
|
71
|
-
const expand = (ref, k, depth) => {
|
|
72
|
-
const c = constant(ref);
|
|
73
|
-
if (c !== undefined)
|
|
74
|
-
return { terms: [], c: c * k };
|
|
75
|
-
const w = depth < 256 ? writerOf(m, ref) : undefined;
|
|
76
|
-
if (w?.op === 'add@1')
|
|
77
|
-
return w.args.reduce((sum, arg) => linAdd(sum, expand(arg, k, depth + 1)), { terms: [], c: 0 });
|
|
78
|
-
/* Only a length times a ratio: what is left is then a length, as each term of the room must be. */
|
|
79
|
-
if (w?.op === 'mul@1') {
|
|
80
|
-
const [a, b] = w.args;
|
|
81
|
-
if (ratio(b) !== undefined)
|
|
82
|
-
return expand(a, k * ratio(b), depth + 1);
|
|
83
|
-
if (ratio(a) !== undefined)
|
|
84
|
-
return expand(b, k * ratio(a), depth + 1);
|
|
85
|
-
}
|
|
86
|
-
return { terms: [{ ref, k }], c: 0 };
|
|
87
|
-
};
|
|
88
|
-
return linNorm(lin.terms.reduce((sum, t) => linAdd(sum, expand(t.ref, t.k, 0)), { terms: [], c: lin.c }));
|
|
89
|
-
}
|
|
90
63
|
/**
|
|
91
64
|
* 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
|
|
92
65
|
* figure's sizes make of the rest. A mimic of a leader whose stroke follows a length is the leader's ratio times that
|
|
@@ -161,7 +134,7 @@ function poseTurnOf(m, asset, part) {
|
|
|
161
134
|
const seatTurned = (m, asset, part) => (nodeById(m, `${part}.seat.rigid`)?.args.slice(3, 6) ?? []).some((ref) => angleOf(m, asset, ref) !== 0);
|
|
162
135
|
/**
|
|
163
136
|
* Whether a part, or anything carrying it, turns on a joint, stands turned by other than quarter turns, or is seated
|
|
164
|
-
* turned: a box cannot follow it,
|
|
137
|
+
* turned: a box cannot follow it, its reach about the point it turns at can (`reachOf`).
|
|
165
138
|
*/
|
|
166
139
|
function carriedByATurn(m, asset, id) {
|
|
167
140
|
for (let part = id; part; part = parentOf(m, part)) {
|
|
@@ -286,8 +259,9 @@ function sizeValue(m, ref, seen = 0) {
|
|
|
286
259
|
if (seen > 64)
|
|
287
260
|
return false;
|
|
288
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). */
|
|
289
263
|
if (input)
|
|
290
|
-
return input.role !== 'state' && input.min >= 0;
|
|
264
|
+
return input.role !== 'state' && (/^size\.[xyz]$/.test(input.id) || input.min >= 0);
|
|
291
265
|
const w = writerOf(m, ref);
|
|
292
266
|
const positive = (r) => (m.constants.find((c) => c.id === r)?.value ?? -1) > 0;
|
|
293
267
|
if (w?.op === 'geomean@1')
|
|
@@ -304,12 +278,20 @@ function sizeValue(m, ref, seen = 0) {
|
|
|
304
278
|
* How long a value can be, at most, as a sum that holds at every design size: each input's coefficient taken
|
|
305
279
|
* positive. Sound only over inputs that are never below zero (sizes), so any other input makes it unreadable.
|
|
306
280
|
*/
|
|
307
|
-
function magnitudeOf(m,
|
|
281
|
+
function magnitudeOf(m, sum) {
|
|
308
282
|
let out = { terms: [], c: 0 };
|
|
283
|
+
/* Read through its sums first: what cancels there -- a rim's place taken back out of a pose -- is not counted. */
|
|
284
|
+
const lin = throughSums(m, sum);
|
|
309
285
|
for (const t of lin.terms) {
|
|
310
286
|
const a = affineOf(m, t.ref);
|
|
311
|
-
if (!a)
|
|
312
|
-
|
|
287
|
+
if (!a) {
|
|
288
|
+
/* A value held by bounds (`boundsOf`): no larger than its bound -- √(x·y) ≤ (|x| + |y|) / 2, a largest or least ≤ the larger. */
|
|
289
|
+
const bound = boundedMagnitudeOf(m, t.ref);
|
|
290
|
+
if (!bound)
|
|
291
|
+
return null;
|
|
292
|
+
out = linAdd(out, bound, Math.abs(t.k));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
313
295
|
for (const u of a.terms) {
|
|
314
296
|
if (!sizeValue(m, u.ref))
|
|
315
297
|
return null;
|
|
@@ -321,13 +303,223 @@ function magnitudeOf(m, lin) {
|
|
|
321
303
|
return out;
|
|
322
304
|
}
|
|
323
305
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
306
|
+
* Places a fastening writes that are not sums (ADR-0140 3.9) are held by bounds, each a few sums (chief architect
|
|
307
|
+
* 2026-10-20): the room takes them as candidates, the least of the lows and the greatest of the highs, so it holds at
|
|
308
|
+
* every design size though it may be wider than the parts by a little -- and says by how much (`bounded`).
|
|
309
|
+
* - where a part lined up with a disc's rim stands, √((R − E)(R + E));
|
|
310
|
+
* - a point of an ellipsoid of two radii, r_i² · n_i / hypot(r·n), written as (r_i · r_i · n_i) / hypot: it lies on the
|
|
311
|
+
* ellipsoid, so between 0 and r_i on the side n_i points to.
|
|
312
|
+
* A root of sizes alone -- the one ratio a figure grows by -- is a size, read as it is (`sizeValue`).
|
|
313
|
+
*/
|
|
314
|
+
function boundedKind(m, ref) {
|
|
315
|
+
const w = writerOf(m, ref);
|
|
316
|
+
if (w?.op === 'geomean@1' && !sizeValue(m, ref))
|
|
317
|
+
return { root: [w.args[0], w.args[1]] };
|
|
318
|
+
if (w?.op !== 'div@1')
|
|
319
|
+
return undefined;
|
|
320
|
+
const top = writerOf(m, w.args[0]);
|
|
321
|
+
const k = top?.op === 'mul@1' ? m.constants.find((c) => c.id === top.args[1] && c.unit === 'ratio')?.value : undefined;
|
|
322
|
+
const square = k ? writerOf(m, top.args[0]) : undefined;
|
|
323
|
+
if (square?.op !== 'mul@1' || square.args[0] !== square.args[1])
|
|
324
|
+
return undefined;
|
|
325
|
+
return { point: { radius: square.args[0], sign: k > 0 ? 1 : -1 } };
|
|
326
|
+
}
|
|
327
|
+
const isBounded = (m, ref) => boundedKind(m, ref) !== undefined;
|
|
328
|
+
/**
|
|
329
|
+
* Sums that bound a value from above (`up`: its greatest is at least the value) or below (its least is at most it):
|
|
330
|
+
* √(x·y) ∈ [min(x, y), (x + y) / 2] a point of an ellipsoid ∈ [0, r_i] on the side n_i points to
|
|
331
|
+
* read through the sums and ratios that make it up; a term with a negative factor bounded the other way. What is
|
|
332
|
+
* inside the root -- the largest of R − E and 0 -- is read as it is, as the room reads any largest.
|
|
329
333
|
*/
|
|
330
|
-
function
|
|
334
|
+
function boundsOf(m, lin, up, seen = 0) {
|
|
335
|
+
const flat = throughSums(m, lin);
|
|
336
|
+
let out = [{ terms: [], c: flat.c }];
|
|
337
|
+
for (const t of flat.terms) {
|
|
338
|
+
let candidates = [{ terms: [t], c: 0 }];
|
|
339
|
+
const kind = seen < 64 ? boundedKind(m, t.ref) : undefined;
|
|
340
|
+
if (kind) {
|
|
341
|
+
const way = t.k > 0 ? up : !up;
|
|
342
|
+
let raw;
|
|
343
|
+
if ('root' in kind) {
|
|
344
|
+
const each = kind.root.map(ref => boundsOf(m, { terms: [{ ref, k: 1 }], c: 0 }, way, seen + 1));
|
|
345
|
+
raw = way ? each[0].flatMap(x => each[1].map(y => linAdd({ terms: [], c: 0 }, linAdd(x, y), 0.5))) : each.flat();
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
/* Toward its side the radius, away from it the middle. */
|
|
349
|
+
const far = kind.point.sign > 0 === way;
|
|
350
|
+
raw = far ? boundsOf(m, { terms: [{ ref: kind.point.radius, k: kind.point.sign }], c: 0 }, way, seen + 1) : [{ terms: [], c: 0 }];
|
|
351
|
+
}
|
|
352
|
+
candidates = raw.map(c => linAdd({ terms: [], c: 0 }, c, t.k));
|
|
353
|
+
}
|
|
354
|
+
out = out.flatMap(o => candidates.map(c => linAdd(o, c)));
|
|
355
|
+
}
|
|
356
|
+
return out.map(linNorm);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* How large a bounded root can be, as one sum of sizes: √(x·y) no more than (|x| + |y|) / 2; the largest or least of
|
|
360
|
+
* values inside it no more than the larger of them.
|
|
361
|
+
*/
|
|
362
|
+
function boundedMagnitudeOf(m, ref, inside = false) {
|
|
363
|
+
const w = writerOf(m, ref);
|
|
364
|
+
const kind = boundedKind(m, ref);
|
|
365
|
+
if (kind && 'point' in kind)
|
|
366
|
+
return magnitudeOf(m, { terms: [{ ref: kind.point.radius, k: 1 }], c: 0 });
|
|
367
|
+
if (!(kind || (inside && (w?.op === 'max@1' || w?.op === 'min@1'))))
|
|
368
|
+
return null;
|
|
369
|
+
const each = w.args.map(a => {
|
|
370
|
+
const lin = throughSums(m, { terms: [{ ref: a, k: 1 }], c: 0 });
|
|
371
|
+
let out = { terms: [], c: Math.abs(lin.c) };
|
|
372
|
+
for (const t of lin.terms) {
|
|
373
|
+
const size = affineOf(m, t.ref) ? magnitudeOf(m, { terms: [t], c: 0 }) : boundedMagnitudeOf(m, t.ref, true);
|
|
374
|
+
if (!size)
|
|
375
|
+
return null;
|
|
376
|
+
out = linAdd(out, size, Math.abs(t.k));
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
});
|
|
380
|
+
if (each.some(e => !e))
|
|
381
|
+
return null;
|
|
382
|
+
if (w.op === 'geomean@1')
|
|
383
|
+
return linAdd({ terms: [], c: 0 }, linAdd(each[0], each[1]), 0.5);
|
|
384
|
+
return each.reduce((a, b) => linAtLeast(a, b));
|
|
385
|
+
}
|
|
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
|
+
/** Every low candidate bounded from below and every high one from above, so the room holds whatever they come to. */
|
|
412
|
+
function boundedSides(m, low, high) {
|
|
413
|
+
for (const axis of AXES) {
|
|
414
|
+
low[axis] = low[axis].flatMap(l => boundsOf(m, l, false));
|
|
415
|
+
high[axis] = high[axis].flatMap(l => boundsOf(m, l, true));
|
|
416
|
+
}
|
|
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
|
+
}
|
|
513
|
+
/**
|
|
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.
|
|
521
|
+
*/
|
|
522
|
+
function reachOf(m, asset, id, ends) {
|
|
331
523
|
const chain = [];
|
|
332
524
|
for (let part = id; part; part = parentOf(m, part))
|
|
333
525
|
chain.push(part);
|
|
@@ -339,56 +531,149 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
339
531
|
: found, -1);
|
|
340
532
|
if (top < 0)
|
|
341
533
|
return `${id} is carried by no turn`;
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return null;
|
|
349
|
-
};
|
|
350
|
-
/* 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`;
|
|
351
540
|
const sizes = sizeRefsOf(m, id, id);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
for (const ref of seat?.args.slice(0, 3) ?? []) {
|
|
371
|
-
const bad = grow(linOf(m, ref), `where ${part} is fastened`);
|
|
372
|
-
if (bad)
|
|
373
|
-
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;
|
|
374
559
|
}
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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`);
|
|
379
593
|
if (bad)
|
|
380
594
|
return bad;
|
|
381
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));
|
|
382
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;
|
|
383
670
|
}
|
|
384
671
|
/*
|
|
385
672
|
The pivot: where the topmost turn is fastened, and everything above it. Above it nothing turns, but a slide may
|
|
386
673
|
carry it -- the telescopic conveyor's nose turns at the end of its third stage -- so the pivot is read at each
|
|
387
|
-
end of the controls, a
|
|
388
|
-
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).
|
|
389
676
|
*/
|
|
390
|
-
const pivotPart = chain[top];
|
|
391
|
-
const parent = parentOf(m, pivotPart);
|
|
392
677
|
const seat = nodeById(m, `${pivotPart}.seat.rigid`);
|
|
393
678
|
const seatAt = Object.fromEntries(AXES.map((a, i) => [a, seat ? linOf(m, seat.args[i]) : { terms: [], c: 0 }]));
|
|
394
679
|
const centres = [];
|
|
@@ -399,17 +684,38 @@ function reachOf(m, asset, id, ends, freezeless = true) {
|
|
|
399
684
|
return at;
|
|
400
685
|
centres.push(at);
|
|
401
686
|
}
|
|
402
|
-
return { centres,
|
|
687
|
+
return { centres, up, down };
|
|
403
688
|
}
|
|
404
689
|
/** The bounds the parts come to, as sums, with what could not be read named. */
|
|
405
690
|
function occupancyLins(asset, over) {
|
|
406
691
|
const m = asset.document.model;
|
|
407
692
|
const parts = [];
|
|
408
693
|
const skipped = [];
|
|
694
|
+
const bounded = [];
|
|
409
695
|
const low = { x: [], y: [], z: [] };
|
|
410
696
|
const high = { x: [], y: [], z: [] };
|
|
411
|
-
/*
|
|
412
|
-
|
|
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
|
+
};
|
|
413
719
|
/* A row's template stands for its copies (`repeatPart`): leaving it out left a conveyor's rollers out of its room. */
|
|
414
720
|
const rowOf = (id) => nodeById(m, `${id}.repeat`);
|
|
415
721
|
const ends = over === 'rest' ? ['start'] : ['min', 'max'];
|
|
@@ -536,14 +842,14 @@ function occupancyLins(asset, over) {
|
|
|
536
842
|
*/
|
|
537
843
|
for (const { run } of rows) {
|
|
538
844
|
for (const l of run.lows)
|
|
539
|
-
low[run.axis].push(linAdd(l, reach.
|
|
845
|
+
low[run.axis].push(linAdd(l, reach.down[run.axis], -1));
|
|
540
846
|
for (const h of run.highs)
|
|
541
|
-
high[run.axis].push(linAdd(h, reach.
|
|
847
|
+
high[run.axis].push(linAdd(h, reach.up[run.axis], 1));
|
|
542
848
|
}
|
|
543
849
|
for (const at of reach.centres)
|
|
544
850
|
for (const axis of AXES.filter(a => !rowAxes.has(a))) {
|
|
545
|
-
const lo = linAdd(at[axis], reach.
|
|
546
|
-
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);
|
|
547
853
|
low[axis].push(lo);
|
|
548
854
|
high[axis].push(hi);
|
|
549
855
|
climbed(lo, -1, low, axis);
|
|
@@ -551,12 +857,11 @@ function occupancyLins(asset, over) {
|
|
|
551
857
|
}
|
|
552
858
|
continue;
|
|
553
859
|
}
|
|
554
|
-
for (const at of reach.centres)
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
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
|
+
}
|
|
560
865
|
continue;
|
|
561
866
|
}
|
|
562
867
|
/*
|
|
@@ -646,7 +951,10 @@ function occupancyLins(asset, over) {
|
|
|
646
951
|
}
|
|
647
952
|
}
|
|
648
953
|
}
|
|
649
|
-
|
|
954
|
+
for (const part of parts)
|
|
955
|
+
if (boundsUnder(m, part).length)
|
|
956
|
+
bounded.push(part);
|
|
957
|
+
return { parts, skipped, bounded, low, high, reaches };
|
|
650
958
|
}
|
|
651
959
|
const valueOfLin = (values, l) => l.terms.reduce((s, t) => s + t.k * (values[t.ref] ?? NaN), l.c);
|
|
652
960
|
/** What the parts come to, and what a person is being asked to confirm. Reads the asset; changes nothing. */
|
|
@@ -656,12 +964,12 @@ export function proposeFigureOccupancy(asset, options = {}) {
|
|
|
656
964
|
if (over !== 'rest' && over !== 'range')
|
|
657
965
|
fail('SCHEMA', 'over', `${String(over)} is neither rest nor range`);
|
|
658
966
|
const source = structuredClone(asset);
|
|
659
|
-
const { parts, skipped, low, high,
|
|
660
|
-
for (const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
+
}
|
|
972
|
+
boundedSides(source.document.model, low, high);
|
|
665
973
|
if (!parts.length)
|
|
666
974
|
fail('OCCUPANCY_NO_PARTS', 'occupancy', `no part could be measured${skipped.length ? `: ${skipped.map(s => `${s.part} — ${s.reason}`).join('; ')}` : ''}`);
|
|
667
975
|
const values = compileFigureGraph(source.document.model).evaluate({ ...source.designInputs, ...source.stateDefaults })
|
|
@@ -673,7 +981,9 @@ export function proposeFigureOccupancy(asset, options = {}) {
|
|
|
673
981
|
max: round6(Math.max(...high[axis].map(l => valueOfLin(values, l))))
|
|
674
982
|
}
|
|
675
983
|
]));
|
|
676
|
-
|
|
984
|
+
const m = source.document.model;
|
|
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) };
|
|
677
987
|
}
|
|
678
988
|
/**
|
|
679
989
|
* The placement says which plane the mounting plane is. A part fastened to it in a way the new plane does not take --
|
|
@@ -714,13 +1024,13 @@ export function declareOccupancy(asset, a) {
|
|
|
714
1024
|
fail('OCCUPANCY_PADDING', `occupancy.${axis}`, `padding is millimetres, zero or more; got ${String(pad)}`);
|
|
715
1025
|
padding[axis] = pad;
|
|
716
1026
|
}
|
|
717
|
-
const { parts, low, high,
|
|
718
|
-
/*
|
|
719
|
-
for (const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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);
|
|
724
1034
|
for (const axis of AXES) {
|
|
725
1035
|
if (!padding[axis])
|
|
726
1036
|
continue;
|
|
@@ -733,7 +1043,7 @@ export function declareOccupancy(asset, a) {
|
|
|
733
1043
|
const args = [];
|
|
734
1044
|
const layout = AXES.flatMap(axis => [low[axis], high[axis]]).map(side => side
|
|
735
1045
|
.map((l) => {
|
|
736
|
-
const n =
|
|
1046
|
+
const n = throughSums(m, l);
|
|
737
1047
|
args.push(constantOf(m, 'mm', round6(n.c), 'd'));
|
|
738
1048
|
for (const t of n.terms)
|
|
739
1049
|
args.push(t.ref, constantOf(m, 'ratio', t.k, 'k'));
|