@hatiolab/figure-model 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-fastening.d.ts +19 -10
- package/dist/authoring-fastening.d.ts.map +1 -1
- package/dist/authoring-fastening.js +129 -356
- package/dist/authoring-fastening.js.map +1 -1
- package/dist/authoring-geometry.d.ts +9 -45
- package/dist/authoring-geometry.d.ts.map +1 -1
- package/dist/authoring-geometry.js +25 -184
- package/dist/authoring-geometry.js.map +1 -1
- package/dist/authoring-occupancy.d.ts +3 -3
- package/dist/authoring-occupancy.d.ts.map +1 -1
- package/dist/authoring-occupancy.js +11 -9
- package/dist/authoring-occupancy.js.map +1 -1
- package/dist/authoring-proportion.js +2 -2
- package/dist/authoring-proportion.js.map +1 -1
- package/dist/authoring-types.d.ts +37 -35
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/authoring-types.js +12 -0
- package/dist/authoring-types.js.map +1 -1
- package/dist/command-preview.d.ts +11 -33
- package/dist/command-preview.d.ts.map +1 -1
- package/dist/command-preview.js +2 -66
- package/dist/command-preview.js.map +1 -1
- package/dist/contract.d.ts +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/examples.js +4 -4
- package/dist/examples.js.map +1 -1
- package/dist/face-overhang.d.ts +105 -0
- package/dist/face-overhang.d.ts.map +1 -0
- package/dist/face-overhang.js +357 -0
- package/dist/face-overhang.js.map +1 -0
- package/dist/fasten-place.d.ts +35 -0
- package/dist/fasten-place.d.ts.map +1 -0
- package/dist/fasten-place.js +91 -0
- package/dist/fasten-place.js.map +1 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +7 -4
- package/dist/gate.js.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +16 -2
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/joint-fields.d.ts +12 -0
- package/dist/joint-fields.d.ts.map +1 -0
- package/dist/joint-fields.js +20 -0
- package/dist/joint-fields.js.map +1 -0
- package/dist/joint-writing.d.ts.map +1 -1
- package/dist/joint-writing.js +86 -125
- 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/proposal.js +3 -3
- package/dist/proposal.js.map +1 -1
- package/dist/recipe.js +1 -1
- package/dist/recipe.js.map +1 -1
- package/dist/resize-plan.js +1 -1
- package/dist/resize-plan.js.map +1 -1
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +18 -17
- package/dist/robots.js.map +1 -1
- package/dist/shape-edit.js +4 -5
- package/dist/shape-edit.js.map +1 -1
- package/dist/shape-extreme.d.ts +49 -0
- package/dist/shape-extreme.d.ts.map +1 -0
- package/dist/shape-extreme.js +262 -0
- package/dist/shape-extreme.js.map +1 -0
- package/dist/shape-ops.d.ts +6 -7
- package/dist/shape-ops.d.ts.map +1 -1
- package/dist/shape-ops.js +72 -37
- package/dist/shape-ops.js.map +1 -1
- package/dist/size-edit.d.ts.map +1 -1
- package/dist/size-edit.js +33 -12
- package/dist/size-edit.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import { type Axis } from './words.ts';
|
|
2
|
-
import { type
|
|
2
|
+
import { type FigureJoint, type FigureJointPivot, type FigureMeasure, type FigureMotionRequest } from './authoring-types.ts';
|
|
3
3
|
type Dynamic = any;
|
|
4
4
|
/**
|
|
5
|
-
* A joint frame's own words, as `writeJoint` hands them
|
|
6
|
-
*
|
|
5
|
+
* A joint frame's own words, as `writeJoint` hands them on (ADR-0107, contract 2026-10-03): the pivot, the offset and
|
|
6
|
+
* the parent end's rotation.
|
|
7
7
|
*/
|
|
8
8
|
export interface JointFrame {
|
|
9
9
|
pivot?: FigureJointPivot;
|
|
10
10
|
offset?: Partial<Record<Axis, FigureMeasure>>;
|
|
11
11
|
parentRotate?: Partial<Record<Axis, number>>;
|
|
12
|
-
back: number[][];
|
|
13
12
|
path: string;
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A part fastened, in the pose `childRotate` gives it, to a face of its parent or to the mounting plane (ADR-0140 3):
|
|
16
|
+
* its real shape meets the face -- by its outermost point toward it (`meet`), away from it (`flush`), or their middle
|
|
17
|
+
* (`centre`) -- and its real extent lines up across it. Everything is said in the parent's own frame (3.11), and
|
|
18
|
+
* written as graph nodes of the shapes' own values (3.3), so it holds as either is resized.
|
|
19
|
+
*
|
|
20
|
+
* Along the face's outward normal n (a coordinate c, the face at c_f, σ its sign on its axis):
|
|
21
|
+
* meet c = c_f + σ·(gap + e(−n)) flush c = c_f − σ·(gap + e(+n))
|
|
22
|
+
* centre c = c_f + σ·(gap + (e(−n) − e(+n)) / 2)
|
|
23
|
+
* and across it on each axis u, by the reaches e(±u):
|
|
24
|
+
* centre c = mid + mm − (e(+u) − e(−u)) / 2 min c = lo + e(−u) max c = hi − e(+u)
|
|
25
|
+
* where e(d) is how far the part reaches along d in its pose (figureShapeExtremeAlong). The seat -- the point it turns
|
|
26
|
+
* about -- is the point it is lined up by, on the face (3.10): the edge it is level with -- a hinge (ADR-0107 decision 1)
|
|
27
|
+
* -- or the middle of its real extent; or its pivot. The pose is the part's origin from there.
|
|
28
|
+
*/
|
|
29
|
+
export declare function placeOnFace(asset: Dynamic, j: FigureJoint, frame: JointFrame): void;
|
|
21
30
|
/**
|
|
22
31
|
* The seat turned by a joint frame's parent-end rotation (`rigid@1` turns X, then Y, then Z). Its angles are named by
|
|
23
32
|
* the rotation the record gives, and by the parent's standing turn when that is not none -- not by their value, which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authoring-fastening.d.ts","sourceRoot":"","sources":["../src/authoring-fastening.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,YAAY,CAAA;AAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"authoring-fastening.d.ts","sourceRoot":"","sources":["../src/authoring-fastening.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,YAAY,CAAA;AAC5C,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAA;AAkC7B,KAAK,OAAO,GAAG,GAAG,CAAA;AAElB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAA;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,IAAI,EAAE,MAAM,CAAA;CACb;AAKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CA2GnF;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EAAE,EAAE,EACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EACrC,IAAI,EAAE,MAAM,EAAE,EAAE,QAkBjB;AAED;;;GAGG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CAuE/G;AAQD,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,mBAAmB,EACtB,MAAM,CAAC,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,EAC7E,IAAI,GAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,EAAM,GAC1B,OAAO,CAiLT;AAED,qGAAqG;AACrG,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAsBhG"}
|
|
@@ -7,289 +7,145 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { figurePlacementOf } from "./asset.js";
|
|
9
9
|
import { AXES } from "./words.js";
|
|
10
|
-
import {
|
|
10
|
+
import { FIGURE_FACINGS } from "./authoring-types.js";
|
|
11
|
+
import { figureShapeExtremeAlong, lin, plus, times, writeFigureReach } from "./shape-extreme.js";
|
|
11
12
|
import { fail, nodeById, writerOf, partOf, namer, constantOf, scaled, measureRef, termRefs, sumOf, sourceRef, ownInput, dropIfUnused, parentOf, round6, sizeInputOf } from "./authoring-nodes.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
13
|
+
import { half, facesOfPart, planeOf, ownSpansOf, transpose3, isIdentity, eulerOfAny, rpyMatrix, mv3, IDENTITY } from "./authoring-geometry.js";
|
|
14
|
+
const unitAlong = (axis, sign) => AXES.map(x => (x === axis ? sign : 0));
|
|
15
|
+
/**
|
|
16
|
+
* A part fastened, in the pose `childRotate` gives it, to a face of its parent or to the mounting plane (ADR-0140 3):
|
|
17
|
+
* its real shape meets the face -- by its outermost point toward it (`meet`), away from it (`flush`), or their middle
|
|
18
|
+
* (`centre`) -- and its real extent lines up across it. Everything is said in the parent's own frame (3.11), and
|
|
19
|
+
* written as graph nodes of the shapes' own values (3.3), so it holds as either is resized.
|
|
20
|
+
*
|
|
21
|
+
* Along the face's outward normal n (a coordinate c, the face at c_f, σ its sign on its axis):
|
|
22
|
+
* meet c = c_f + σ·(gap + e(−n)) flush c = c_f − σ·(gap + e(+n))
|
|
23
|
+
* centre c = c_f + σ·(gap + (e(−n) − e(+n)) / 2)
|
|
24
|
+
* and across it on each axis u, by the reaches e(±u):
|
|
25
|
+
* centre c = mid + mm − (e(+u) − e(−u)) / 2 min c = lo + e(−u) max c = hi − e(+u)
|
|
26
|
+
* where e(d) is how far the part reaches along d in its pose (figureShapeExtremeAlong). The seat -- the point it turns
|
|
27
|
+
* about -- is the point it is lined up by, on the face (3.10): the edge it is level with -- a hinge (ADR-0107 decision 1)
|
|
28
|
+
* -- or the middle of its real extent; or its pivot. The pose is the part's origin from there.
|
|
29
|
+
*/
|
|
30
|
+
export function placeOnFace(asset, j, frame) {
|
|
14
31
|
const m = asset.document.model;
|
|
15
|
-
const
|
|
16
|
-
const path =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const ownFace = byCentre
|
|
88
|
-
? mineAlong && !mineAlong.centred
|
|
89
|
-
? [...half(mineAlong.lo), ...half(mineAlong.hi)]
|
|
90
|
-
: []
|
|
91
|
-
: SIGN[a.face] > 0
|
|
92
|
-
? mineAlong.hi
|
|
93
|
-
: mineAlong.lo;
|
|
94
|
-
offsetTerms[axis] = byCentre ? neg(ownFace) : [...neg(ownFace), { ref: gap, k: outward }];
|
|
95
|
-
offset[axis] = byCentre
|
|
96
|
-
? ownFace.length
|
|
97
|
-
? sumOf(m, name, neg(ownFace), `along.${axis}`)
|
|
98
|
-
: zero
|
|
99
|
-
: sumOf(m, name, [...neg(ownFace), { ref: gap, k: outward }], `along.${axis}`);
|
|
100
|
-
/*
|
|
101
|
-
Across the face. Centred by default; min and max keep the two parts' edges level as either is resized -- and the
|
|
102
|
-
point fastened at is where those edges meet: the seat on the target's edge, the part's own edge on it. A door
|
|
103
|
-
fastened level with the frame's min edge turns about that edge, as a hinge does (2026-09-25); centred, the point
|
|
104
|
-
is the middle of the face. A shape not centred on its origin (a portal stands on its bottom) is centred by its
|
|
105
|
-
faces, not by its origin.
|
|
106
|
-
*/
|
|
107
|
-
for (const other of AXES.filter(x => x !== axis)) {
|
|
108
|
-
const how = a.align?.[other] ?? 'centre';
|
|
109
|
-
const t = theirs[other], o = mine[other];
|
|
110
|
-
const middle = (span) => (span && !span.centred ? [...half(span.lo), ...half(span.hi)] : []);
|
|
111
|
-
const centreOf = (span, hint) => middle(span).length ? sumOf(m, name, middle(span), hint) : constantOf(m, 'mm', 0, 'd');
|
|
112
|
-
const ownCentre = () => (middle(o).length ? sumOf(m, name, neg(middle(o)), `own-centre.${other}`) : zero);
|
|
113
|
-
offsetTerms[other] = neg(middle(o));
|
|
114
|
-
if (how === 'centre') {
|
|
115
|
-
seatTerms[other] = middle(t);
|
|
116
|
-
translation[other] = centreOf(t, `centre.${other}`);
|
|
117
|
-
offset[other] = ownCentre();
|
|
118
|
-
}
|
|
119
|
-
else if (typeof how === 'object' && how && Object.hasOwn(how, 'mm')) {
|
|
120
|
-
const mm = measureRef(m, asset, name, how.mm, `align.${other}`, path);
|
|
121
|
-
seatTerms[other] = [...middle(t), { ref: mm, k: 1 }];
|
|
122
|
-
translation[other] = middle(t).length
|
|
123
|
-
? sumOf(m, name, [...middle(t), { ref: mm, k: 1 }], `align.${other}.from-centre`)
|
|
124
|
-
: mm;
|
|
125
|
-
offset[other] = ownCentre();
|
|
32
|
+
const child = j.body1.part;
|
|
33
|
+
const path = frame.path;
|
|
34
|
+
const to = j.body0;
|
|
35
|
+
const o = j.origin ?? {};
|
|
36
|
+
const facing = o.facing ?? 'meet';
|
|
37
|
+
if (!FIGURE_FACINGS.includes(facing))
|
|
38
|
+
fail('JOINT_SCHEMA', `${path}.origin.facing`, `${String(facing)} is not a facing; ${FIGURE_FACINGS.join(', ')}`);
|
|
39
|
+
const onPlane = 'plane' in to;
|
|
40
|
+
const name = namer(m, `${child}.on.${onPlane ? 'plane' : to.part}`);
|
|
41
|
+
/* The face: its axis, its outward sign and where it lies, in the parent's own frame; across it, the parent's edges. */
|
|
42
|
+
let axis, sign, plane;
|
|
43
|
+
const edges = {};
|
|
44
|
+
if (onPlane) {
|
|
45
|
+
const placement = figurePlacementOf(asset);
|
|
46
|
+
axis = placement === 'wall' ? 'z' : 'y';
|
|
47
|
+
sign = 1;
|
|
48
|
+
plane = [];
|
|
49
|
+
if (placement === 'wall' && facing !== 'meet')
|
|
50
|
+
fail('ATTACH_FACE', path, `the figure hangs on a wall; ${child} stands out of it (meet), not ${facing}`);
|
|
51
|
+
if (facing === 'centre')
|
|
52
|
+
fail('JOINT_SCHEMA', `${path}.origin.facing`, 'a part stands on the mounting plane or hangs from it; its middle is not on it');
|
|
53
|
+
for (const x of AXES.filter(x => x !== axis))
|
|
54
|
+
if (sizeInputOf(m, x)) {
|
|
55
|
+
const size = sourceRef(m, asset, { of: 'instance', axis: x }, path);
|
|
56
|
+
edges[x] = { lo: [{ ref: size, k: -0.5 }], hi: [{ ref: size, k: 0.5 }] };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (to.part === child)
|
|
61
|
+
fail('ATTACH_CYCLE', path, 'a part cannot be fastened to itself');
|
|
62
|
+
const target = facesOfPart(m, to.part, path);
|
|
63
|
+
const face = Object.hasOwn(target.faces, String(to.face)) ? target.faces[to.face] : undefined;
|
|
64
|
+
if (!face)
|
|
65
|
+
fail('SCHEMA', path, `${to.part} has no face ${String(to.face)}; it has ${Object.keys(target.faces).join(', ')}`);
|
|
66
|
+
axis = face.axis;
|
|
67
|
+
sign = face.normal;
|
|
68
|
+
plane = planeOf(target.shape, face);
|
|
69
|
+
const spans = ownSpansOf(m, to.part, path);
|
|
70
|
+
for (const x of AXES.filter(x => x !== axis))
|
|
71
|
+
if (spans[x])
|
|
72
|
+
edges[x] = { lo: spans[x].lo, hi: spans[x].hi };
|
|
73
|
+
}
|
|
74
|
+
/* The child's pose in the parent's frame, and how far its real shape reaches along a direction there. */
|
|
75
|
+
const rotate = o.childRotate ?? {};
|
|
76
|
+
const C = rpyMatrix(rotate);
|
|
77
|
+
const reach = (d) => figureShapeExtremeAlong(m, child, mv3(transpose3(C), d), path);
|
|
78
|
+
const gap = o.gap === undefined ? lin([]) : lin([{ ref: measureRef(m, asset, name, o.gap, `gap.${axis}`, path), k: 1 }]);
|
|
79
|
+
const [toward, away] = [reach(unitAlong(axis, -sign)), reach(unitAlong(axis, sign))];
|
|
80
|
+
const at = {};
|
|
81
|
+
const seat = {};
|
|
82
|
+
const face = lin(plane);
|
|
83
|
+
at[axis] =
|
|
84
|
+
facing === 'meet'
|
|
85
|
+
? plus(face, times(plus(gap, toward), sign))
|
|
86
|
+
: facing === 'flush'
|
|
87
|
+
? plus(face, times(plus(gap, away), -sign))
|
|
88
|
+
: plus(face, times(plus(gap, times(plus(toward, times(away, -1)), 0.5)), sign));
|
|
89
|
+
/* On the face; by its middle (centre), at that middle, gap and all -- it turns in place, a roller spinning. */
|
|
90
|
+
seat[axis] = facing === 'centre' ? plus(at[axis], times(plus(reach(unitAlong(axis, 1)), times(reach(unitAlong(axis, -1)), -1)), 0.5)) : face;
|
|
91
|
+
for (const u of AXES.filter(x => x !== axis)) {
|
|
92
|
+
const [hi, lo] = [reach(unitAlong(u, 1)), reach(unitAlong(u, -1))];
|
|
93
|
+
/* The middle of its real extent, from its origin: (e(+u) − e(−u)) / 2. */
|
|
94
|
+
const middle = times(plus(hi, times(lo, -1)), 0.5);
|
|
95
|
+
const how = o.align?.[u] ?? 'centre';
|
|
96
|
+
const edge = edges[u];
|
|
97
|
+
if (how === 'min' || how === 'max') {
|
|
98
|
+
if (!edge)
|
|
99
|
+
fail(onPlane ? 'SCHEMA' : 'ATTACH_FACE', path, onPlane ? `the plane has no edges to line ${child} up with on ${u}; declare the figure's size, or give the centre or a measured offset` : `${to.part} has no edges along ${u} to line ${child} up with; give the centre or a measured offset`);
|
|
100
|
+
at[u] = how === 'min' ? plus(lin(edge.lo), lo) : plus(lin(edge.hi), times(hi, -1));
|
|
101
|
+
/* Level with an edge, it turns about that edge, as a hinge does (ADR-0107 decision 1). */
|
|
102
|
+
seat[u] = lin(how === 'min' ? edge.lo : edge.hi);
|
|
103
|
+
continue;
|
|
126
104
|
}
|
|
127
|
-
else if (how === '
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
fail('ATTACH_FACE', path, `${a.part} has no edges along ${other} to line up; give the centre or a measured offset`);
|
|
132
|
-
seatTerms[other] = how === 'min' ? t.lo : t.hi;
|
|
133
|
-
offsetTerms[other] = neg(how === 'min' ? o.lo : o.hi);
|
|
134
|
-
translation[other] = sumOf(m, name, how === 'min' ? t.lo : t.hi, `edge.${other}`);
|
|
135
|
-
offset[other] = sumOf(m, name, neg(how === 'min' ? o.lo : o.hi), `own-edge.${other}`);
|
|
105
|
+
else if (how === 'centre' || (typeof how === 'object' && how && Object.hasOwn(how, 'mm'))) {
|
|
106
|
+
const mid = edge ? lin([...half(edge.lo), ...half(edge.hi)]) : lin([]);
|
|
107
|
+
const off = typeof how === 'object' ? lin([{ ref: measureRef(m, asset, name, how.mm, `align.${u}`, path), k: 1 }]) : lin([]);
|
|
108
|
+
at[u] = plus(mid, off, times(middle, -1));
|
|
136
109
|
}
|
|
137
110
|
else
|
|
138
111
|
fail('SCHEMA', path, `${String(how)} is not an alignment`);
|
|
112
|
+
seat[u] = plus(at[u], middle);
|
|
139
113
|
}
|
|
140
|
-
/*
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
blade, about the hub. With no twist this is the same place written as before.
|
|
144
|
-
*/
|
|
145
|
-
let twisted;
|
|
146
|
-
if (twist && twist.deg) {
|
|
147
|
-
const [p, q] = ACROSS[axis];
|
|
148
|
-
for (const other of [p, q]) {
|
|
149
|
-
const how = a.align?.[other] ?? 'centre';
|
|
150
|
-
if (how === 'min' || how === 'max')
|
|
151
|
-
fail('ATTACH_FACE', path, `${a.part} is twisted ${round6(twist.deg)}° about ${axis}; its sides are not level with ${to.part}'s edges -- place it by the centre or a distance`);
|
|
152
|
-
}
|
|
153
|
-
const across = (other) => {
|
|
154
|
-
const how = a.align?.[other] ?? 'centre';
|
|
155
|
-
const o = mine[other];
|
|
156
|
-
const own = o && !o.centred ? neg([...half(o.lo), ...half(o.hi)]) : [];
|
|
157
|
-
return typeof how === 'object' && how && Object.hasOwn(how, 'mm')
|
|
158
|
-
? [{ ref: measureRef(m, asset, name, how.mm, `twist.${other}`, path), k: 1 }, ...own]
|
|
159
|
-
: own;
|
|
160
|
-
};
|
|
161
|
-
const [vp, vq] = [across(p), across(q)];
|
|
162
|
-
for (const other of [p, q]) {
|
|
163
|
-
const t = theirs[other];
|
|
164
|
-
seatTerms[other] = t && !t.centred ? [...half(t.lo), ...half(t.hi)] : [];
|
|
165
|
-
translation[other] =
|
|
166
|
-
t && !t.centred
|
|
167
|
-
? sumOf(m, name, [...half(t.lo), ...half(t.hi)], `twist-centre.${other}`)
|
|
168
|
-
: constantOf(m, 'mm', 0, 'd');
|
|
169
|
-
}
|
|
170
|
-
twisted = twistRotator(m, name, axis, twist.deg);
|
|
171
|
-
[offset[p], offset[q]] = twisted.rotate(vp, vq, 'twist');
|
|
172
|
-
offsetTerms[p] = [{ ref: offset[p], k: 1 }];
|
|
173
|
-
offsetTerms[q] = [{ ref: offset[q], k: 1 }];
|
|
174
|
-
}
|
|
175
|
-
const placed = jointFrameOf(asset, name, a.part, { seat: translation, pose: offset, seatTerms, offsetTerms, normal: axis, face: a.face, mine, twisted }, frame);
|
|
176
|
-
reseat(asset, a.part, `${to.part}.local`, { seat: placed.seat, pose: placed.pose });
|
|
177
|
-
placed.finish();
|
|
178
|
-
return asset;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* A vector across a face, as sums, turned by a twist about the face's normal: its two components as nodes. The turn's
|
|
182
|
-
* factors are named by the angle, as the record gives it -- not by their value, which may carry float noise that
|
|
183
|
-
* depends on how it was computed, so a rewrite named them apart (JOINT_DRIFT, the architect's fan, 2026-09-25). Their
|
|
184
|
-
* values are rounded to twelve places, which is the second defence.
|
|
185
|
-
*/
|
|
186
|
-
function twistRotator(m, name, axis, deg) {
|
|
187
|
-
const [p, q] = ACROSS[axis];
|
|
188
|
-
const exact = (v) => {
|
|
189
|
-
const r = Math.round(v * 1e12) / 1e12;
|
|
190
|
-
return r === 0 ? 0 : r;
|
|
191
|
-
};
|
|
192
|
-
const tag = String(round6(deg)).replace('-', 'm').replace('.', '_');
|
|
193
|
-
const factor = (fn) => {
|
|
194
|
-
const id = `twist.${fn}.${tag}`;
|
|
195
|
-
const rad = (deg * Math.PI) / 180;
|
|
196
|
-
if (!m.constants.some((c) => c.id === id))
|
|
197
|
-
m.constants.push({
|
|
198
|
-
id,
|
|
199
|
-
unit: 'ratio',
|
|
200
|
-
value: exact(fn === 'cos' ? Math.cos(rad) : fn === 'sin' ? Math.sin(rad) : -Math.sin(rad))
|
|
201
|
-
});
|
|
202
|
-
return id;
|
|
203
|
-
};
|
|
204
|
-
const node = (op, args, hint) => {
|
|
205
|
-
const id = name(hint);
|
|
206
|
-
m.nodes.push({ id, op, args, outputs: { value: `${id}.value` } });
|
|
207
|
-
return `${id}.value`;
|
|
208
|
-
};
|
|
209
|
-
const rotate = (up, uq, hint) => {
|
|
210
|
-
const sp = sumOf(m, name, up, `${hint}-own.${p}`), sq = sumOf(m, name, uq, `${hint}-own.${q}`);
|
|
211
|
-
return [
|
|
212
|
-
node('add@1', [node('mul@1', [sp, factor('cos')], `${hint}ed.${p}.cos`), node('mul@1', [sq, factor('nsin')], `${hint}ed.${p}.sin`)], `${hint}ed.${p}`),
|
|
213
|
-
node('add@1', [node('mul@1', [sp, factor('sin')], `${hint}ed.${q}.sin`), node('mul@1', [sq, factor('cos')], `${hint}ed.${q}.cos`)], `${hint}ed.${q}`)
|
|
214
|
-
];
|
|
215
|
-
};
|
|
216
|
-
return { p, q, rotate };
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* The seat and the child's offset from it, moved for the joint frame's own words (ADR-0107, contract 2026-10-03). A
|
|
220
|
-
* is the seat `attach` worked out (the point fastened at), o the offset, c the child's point to turn about, as it stands:
|
|
221
|
-
*
|
|
222
|
-
* pivot P = A (seat), or A + o + c (child-face, child-centre)
|
|
223
|
-
* seat = P + B·offset, turned by B·Rpy·Bᵀ (F_parent = T(P)·B·T(δ)·Rpy)
|
|
224
|
-
* pose = A − P + o, turned as before (inverse(F_child) = T(Bᵀ(A−P))·[R_c | Bᵀo])
|
|
225
|
-
*
|
|
226
|
-
* With none of them nothing is written but what `attach` wrote. `finish` turns the seat once it is written, and drops
|
|
227
|
-
* what the seat and pose no longer read.
|
|
228
|
-
*/
|
|
229
|
-
function jointFrameOf(asset, name, part, at, frame) {
|
|
230
|
-
const m = asset.document.model;
|
|
231
|
-
const seat = AXES.map(x => at.seat[x]);
|
|
232
|
-
const pose = AXES.map(x => at.pose[x]);
|
|
233
|
-
const pivot = frame?.pivot ?? 'seat';
|
|
234
|
-
const shift = frame?.offset ?? {};
|
|
235
|
-
const turned = !!frame && AXES.some(x => (frame.parentRotate?.[x] ?? 0) !== 0);
|
|
236
|
-
if (!frame || (pivot === 'seat' && !AXES.some(x => shift[x] !== undefined && shift[x] !== 0) && !turned))
|
|
237
|
-
return { seat, pose, finish: () => { } };
|
|
238
|
-
const superseded = new Set();
|
|
239
|
-
if (pivot !== 'seat') {
|
|
240
|
-
/* c: the child's fastened face's centre, or its middle, in the seat frame -- turned by its quarter turn, then its twist. */
|
|
241
|
-
const middle = (span) => (span && !span.centred ? [...half(span.lo), ...half(span.hi)] : []);
|
|
242
|
-
const c = { x: [], y: [], z: [] };
|
|
114
|
+
/* Where it turns (3.10): its lined-up middle on the face, the middle of its real extent, or the centre of its flat face on the face. */
|
|
115
|
+
const pivot = frame.pivot ?? 'seat';
|
|
116
|
+
if (pivot === 'child-centre')
|
|
243
117
|
for (const x of AXES)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
seat[
|
|
256
|
-
|
|
257
|
-
}
|
|
118
|
+
seat[x] = plus(at[x], times(plus(reach(unitAlong(x, 1)), times(reach(unitAlong(x, -1)), -1)), 0.5));
|
|
119
|
+
if (pivot === 'child-face') {
|
|
120
|
+
const own = mv3(transpose3(C), unitAlong(axis, -sign));
|
|
121
|
+
const k = own.findIndex(v => Math.abs(Math.abs(v) - 1) < 1e-9);
|
|
122
|
+
const spans = facing === 'meet' && k >= 0 ? ownSpansOf(m, child, path) : {};
|
|
123
|
+
const along = k >= 0 ? spans[AXES[k]] : undefined;
|
|
124
|
+
if (!along)
|
|
125
|
+
fail('JOINT_SCHEMA', `${path}.origin.pivot`, `${child} meets ${onPlane ? 'the plane' : `${to.part}'s ${to.face}`} at a point or a line${facing === 'meet' ? '' : ` (${facing})`}, not by a face of its own; child-face has no face to turn about -- child-centre or seat`);
|
|
126
|
+
/* The face's centre in the child's own frame, turned into the parent's and placed with the child. */
|
|
127
|
+
const centre = AXES.map((x, i) => (i === k ? (own[k] > 0 ? along.hi : along.lo) : spans[x] ? [...half(spans[x].lo), ...half(spans[x].hi)] : []));
|
|
128
|
+
AXES.forEach((x, i) => {
|
|
129
|
+
seat[x] = plus(at[x], ...AXES.map((_, c) => lin(centre[c].map(t => ({ ref: t.ref, k: t.k * C[i][c] })))));
|
|
130
|
+
});
|
|
258
131
|
}
|
|
259
|
-
/* The
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
terms.push({ ref: measureRef(m, asset, name, d, `offset.${y}`, `${frame.path}.origin.offset.${y}`), k: back[i][j] });
|
|
268
|
-
}
|
|
269
|
-
return terms;
|
|
132
|
+
/* The pose is the child's origin from the seat; the seat moves by the frame's offset (3.10, ADR-0107). */
|
|
133
|
+
const pose = AXES.map(x => plus(at[x], times(seat[x], -1)));
|
|
134
|
+
const shift = frame.offset ?? {};
|
|
135
|
+
const seatAt = AXES.map(x => (shift[x] === undefined || shift[x] === 0 ? seat[x] : plus(seat[x], lin([{ ref: measureRef(m, asset, name, shift[x], `offset.${x}`, `${path}.origin.offset.${x}`), k: 1 }]))));
|
|
136
|
+
const write = (r, h) => writeFigureReach(m, name, r, h);
|
|
137
|
+
reseat(asset, child, onPlane ? asset.document.capabilities.assetFrame : `${to.part}.local`, {
|
|
138
|
+
seat: seatAt.map((r, i) => write(r, `seat.${AXES[i]}`)),
|
|
139
|
+
pose: pose.map((r, i) => write(r, `pose.${AXES[i]}`))
|
|
270
140
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
turnSeat(asset, part, mul3(mul3(back, rpyMatrix(frame.parentRotate)), transpose3(back)), frame.parentRotate, back);
|
|
280
|
-
for (const ref of superseded)
|
|
281
|
-
if (!seat.includes(ref) && !pose.includes(ref))
|
|
282
|
-
dropIfUnused(asset, ref);
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
/** Terms with one entry per reference, their factors added; a reference whose factors cancel, or a constant 0, is gone. */
|
|
287
|
-
function combined(m, terms) {
|
|
288
|
-
const by = new Map();
|
|
289
|
-
for (const t of terms)
|
|
290
|
-
by.set(t.ref, (by.get(t.ref) ?? 0) + t.k);
|
|
291
|
-
const nought = (ref) => m.constants.find((c) => c.id === ref)?.value === 0;
|
|
292
|
-
return [...by].filter(([ref, k]) => Math.abs(k) > 1e-12 && !nought(ref)).map(([ref, k]) => ({ ref, k }));
|
|
141
|
+
/* The child's own pose: childRotate is Rz·Ry·Rx (3.6); rigid@1 takes its angles X then Y then Z, so they are worked out. */
|
|
142
|
+
const { pose: node } = partOf(m, child, path);
|
|
143
|
+
const old = node.args.slice(3, 6);
|
|
144
|
+
node.args = [...node.args.slice(0, 3), ...eulerOfAny(C, true).map(t => constantOf(m, 'deg', t, 'a'))];
|
|
145
|
+
for (const ref of old)
|
|
146
|
+
dropIfUnused(asset, ref, ownInput(m, child, ref) ? ref : undefined);
|
|
147
|
+
if (AXES.some(x => (frame.parentRotate?.[x] ?? 0) !== 0))
|
|
148
|
+
turnSeat(asset, child, rpyMatrix(frame.parentRotate), frame.parentRotate, IDENTITY);
|
|
293
149
|
}
|
|
294
150
|
/**
|
|
295
151
|
* The seat turned by a joint frame's parent-end rotation (`rigid@1` turns X, then Y, then Z). Its angles are named by
|
|
@@ -316,89 +172,6 @@ export function turnSeat(asset, part, r, rotate, back) {
|
|
|
316
172
|
for (const ref of old)
|
|
317
173
|
dropIfUnused(asset, ref);
|
|
318
174
|
}
|
|
319
|
-
function standOnPlane(asset, a, path, twist, frame) {
|
|
320
|
-
const m = asset.document.model;
|
|
321
|
-
const to = a.to;
|
|
322
|
-
if (to.plane !== 'mounting-plane')
|
|
323
|
-
fail('SCHEMA', path, `${String(to.plane)} is not a plane a part can stand on`);
|
|
324
|
-
if (a.facing !== undefined && a.facing !== 'meet')
|
|
325
|
-
fail('SCHEMA', path, 'a part standing on the plane meets it; flush says nothing here');
|
|
326
|
-
/* Which plane it is, the figure's placement says: the floor or the ceiling is Y = 0, a wall is Z = 0 (ADR-0093). */
|
|
327
|
-
const placement = figurePlacementOf(asset);
|
|
328
|
-
const takes = planeFacesOf(placement);
|
|
329
|
-
const normal = placement === 'wall' ? 'z' : 'y';
|
|
330
|
-
if (!takes.includes(a.face))
|
|
331
|
-
fail('ATTACH_FACE', path, placement === 'wall'
|
|
332
|
-
? `${String(a.face)} faces along ${FIGURE_FACES[a.face] ?? 'no axis'}; the figure hangs on a wall, the plane Z = 0, so a part is fastened to it by its back`
|
|
333
|
-
: `${String(a.face)} faces along ${FIGURE_FACES[a.face] ?? 'no axis'}; the plane faces along y, so a part stands on it by its bottom or hangs from it by its top`);
|
|
334
|
-
if (parentOf(m, a.part) && !a.replace)
|
|
335
|
-
fail('ATTACH_REPLACED', path, `${a.part} is already fastened to ${parentOf(m, a.part)}; pass replace to stand it on the plane instead`);
|
|
336
|
-
const mine = standingSpansOf(m, asset, a.part, path);
|
|
337
|
-
if (!mine[normal] || !Object.hasOwn(facesOfPart(m, a.part, path).faces, a.face))
|
|
338
|
-
fail('EDIT_TARGET', path, `${a.part} has no flat ${a.face} face`);
|
|
339
|
-
const name = namer(m, `${a.part}.on.plane`);
|
|
340
|
-
/* Out of the plane: a bottom stands up from the floor, a back comes out of the wall, a top hangs down. */
|
|
341
|
-
const outward = a.face === 'top' ? -1 : 1;
|
|
342
|
-
const gap = measureRef(m, asset, name, a.gap, `gap.${normal}`, path);
|
|
343
|
-
const translation = { x: '', y: '', z: '' };
|
|
344
|
-
/* The same as sums, for a pivot that is not the seat (`jointFrameOf`). */
|
|
345
|
-
const seatTerms = { x: [], y: [], z: [] };
|
|
346
|
-
const offsetTerms = { x: [], y: [], z: [] };
|
|
347
|
-
offsetTerms[normal] = [...neg(outward > 0 ? mine[normal].lo : mine[normal].hi), { ref: gap, k: outward }];
|
|
348
|
-
translation[normal] = sumOf(m, name, [...neg(outward > 0 ? mine[normal].lo : mine[normal].hi), { ref: gap, k: outward }], `stand.${normal}`);
|
|
349
|
-
const zero = constantOf(m, 'mm', 0, 'd');
|
|
350
|
-
const pose = { x: zero, y: zero, z: zero };
|
|
351
|
-
pose[normal] = translation[normal];
|
|
352
|
-
for (const other of AXES.filter(x => x !== normal)) {
|
|
353
|
-
const how = a.align?.[other] ?? 'centre';
|
|
354
|
-
const span = mine[other];
|
|
355
|
-
if (how === 'centre')
|
|
356
|
-
translation[other] = constantOf(m, 'mm', 0, 'd');
|
|
357
|
-
else if (typeof how === 'object' && how && Object.hasOwn(how, 'mm')) {
|
|
358
|
-
translation[other] = measureRef(m, asset, name, how.mm, `align.${other}`, path);
|
|
359
|
-
seatTerms[other] = [{ ref: translation[other], k: 1 }];
|
|
360
|
-
}
|
|
361
|
-
else if ((how === 'min' || how === 'max') && sizeInputOf(m, other)) {
|
|
362
|
-
/*
|
|
363
|
-
The plane has no edges, but a figure that declares its size has a box on it: its sides are half the size
|
|
364
|
-
either way of the origin. A part level with one keeps to it when the figure is resized -- a door's post at
|
|
365
|
-
the figure's left, a gantry's legs at its corners (2026-09-25).
|
|
366
|
-
*/
|
|
367
|
-
if (!span)
|
|
368
|
-
fail('ATTACH_FACE', path, `${a.part} has no edges along ${other} to line up with the figure's side`);
|
|
369
|
-
const s = how === 'min' ? -1 : 1;
|
|
370
|
-
seatTerms[other] = [{ ref: sourceRef(m, asset, { of: 'instance', axis: other }, path), k: s * 0.5 }];
|
|
371
|
-
offsetTerms[other] = neg(how === 'min' ? span.lo : span.hi);
|
|
372
|
-
translation[other] = sumOf(m, name, [{ ref: sourceRef(m, asset, { of: 'instance', axis: other }, path), k: s * 0.5 }], `figure-edge.${other}`);
|
|
373
|
-
pose[other] = sumOf(m, name, neg(how === 'min' ? span.lo : span.hi), `own-edge.${other}`);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
else
|
|
377
|
-
fail('SCHEMA', path, `the plane has no edges to line ${a.part} up with on ${other}; declare the figure's size, or give the centre or a measured offset`);
|
|
378
|
-
/* A shape not centred on its origin stands centred by its faces. */
|
|
379
|
-
if (span && !span.centred) {
|
|
380
|
-
offsetTerms[other] = neg([...half(span.lo), ...half(span.hi)]);
|
|
381
|
-
pose[other] = sumOf(m, name, neg([...half(span.lo), ...half(span.hi)]), `own-centre.${other}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// The point it stands on is on the plane, under its centre: a turn about the plane's normal turns it where it stands.
|
|
385
|
-
const seat = AXES.map(x => (x === normal ? zero : translation[x]));
|
|
386
|
-
/* Its pose turns it about its centre, twist and all (`writeJoint`): a point of it to turn about turns with it. */
|
|
387
|
-
const twisted = frame?.pivot && frame.pivot !== 'seat' && twist?.deg ? twistRotator(m, name, twist.axis, twist.deg) : undefined;
|
|
388
|
-
const placed = jointFrameOf(asset, name, a.part, {
|
|
389
|
-
seat: { x: seat[0], y: seat[1], z: seat[2] },
|
|
390
|
-
pose,
|
|
391
|
-
seatTerms,
|
|
392
|
-
offsetTerms,
|
|
393
|
-
normal,
|
|
394
|
-
face: a.face,
|
|
395
|
-
mine,
|
|
396
|
-
twisted
|
|
397
|
-
}, frame);
|
|
398
|
-
reseat(asset, a.part, asset.document.capabilities.assetFrame, { seat: placed.seat, pose: placed.pose });
|
|
399
|
-
placed.finish();
|
|
400
|
-
return asset;
|
|
401
|
-
}
|
|
402
175
|
/**
|
|
403
176
|
* Put a part's pose on a new frame with a new translation, and rebuild the chain that carries it to the asset.
|
|
404
177
|
* The part's own id, its shape and its `placed` reference all stay as they were.
|