@hatiolab/figure-model 0.7.3 → 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 +53 -8
- package/dist/authoring-fastening.d.ts.map +1 -1
- package/dist/authoring-fastening.js +158 -242
- package/dist/authoring-fastening.js.map +1 -1
- package/dist/authoring-geometry.d.ts +13 -46
- package/dist/authoring-geometry.d.ts.map +1 -1
- package/dist/authoring-geometry.js +28 -180
- 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 +23 -12
- package/dist/authoring-occupancy.js.map +1 -1
- package/dist/authoring-proportion.d.ts.map +1 -1
- package/dist/authoring-proportion.js +15 -3
- package/dist/authoring-proportion.js.map +1 -1
- package/dist/authoring-types.d.ts +69 -32
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/authoring-types.js +14 -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 -61
- 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 +7 -2
- package/dist/joint-writing.d.ts.map +1 -1
- package/dist/joint-writing.js +224 -119
- 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 +4 -4
- 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 +24 -22
- package/dist/robots.js.map +1 -1
- package/dist/shape-edit.d.ts.map +1 -1
- package/dist/shape-edit.js +7 -3
- 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 +40 -11
- package/dist/size-edit.js.map +1 -1
- package/package.json +1 -1
package/dist/joint-writing.js
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
* The joint records (ADR-0093): checking one, writing its nodes, and writing every joint again after a command.
|
|
6
6
|
*/
|
|
7
7
|
import { compileFigureAssetUnjointed } from "./asset.js";
|
|
8
|
-
import { compileFigureGraph } from "./graph.js";
|
|
8
|
+
import { compileFigureGraph, figureShapeFaces } from "./graph.js";
|
|
9
9
|
import { remappedFaces } from "./shape-edit.js";
|
|
10
10
|
import { REPEAT_LIMIT, AXES } from "./words.js";
|
|
11
11
|
import { FIGURE_INSTANCE_SIZE_DEFAULT } from "./instance-size.js";
|
|
12
|
-
import { FIGURE_FACES, FIGURE_JOINT_TYPES, figureMimicLeadersOf, followsSomething, followedTermsOf } from "./authoring-types.js";
|
|
12
|
+
import { FIGURE_FACES, FIGURE_JOINT_TYPES, FIGURE_JOINT_PIVOTS, figureMimicLeadersOf, followsSomething, followedTermsOf } from "./authoring-types.js";
|
|
13
13
|
import { fail, nodeById, partOf, namer, constantOf, measureRef, sumOf, stableDimension, ownInput, holdingDeclared, dropIfUnused, parentOf, round6 } from "./authoring-nodes.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { mul3, neg, half, ownSpansOf, IDENTITY, transpose3, mv3, eulerOfAny, rpyMatrix, isQuarterMatrix, receivingAxisOf, rpyOf } from "./authoring-geometry.js";
|
|
15
|
+
import { placeOnFace, addMotion, removeMotion, reseat, turnSeat } from "./authoring-fastening.js";
|
|
16
|
+
import { figureShapeExtremeAlong, lin, plus, times, writeFigureReach } from "./shape-extreme.js";
|
|
16
17
|
const everyKey = () => (...keys) => keys;
|
|
17
18
|
const JOINT_FIELDS = {
|
|
18
19
|
joint: everyKey()('id', 'type', 'body0', 'body1', 'origin', 'axis', 'lowerLimit', 'upperLimit', 'travel', 'state', 'mimic', 'repeat'),
|
|
19
|
-
body0: everyKey()('part', 'face', 'plane'),
|
|
20
|
-
body1: everyKey()('part', '
|
|
21
|
-
origin: everyKey()('facing', 'gap', 'align', '
|
|
20
|
+
body0: everyKey()('part', 'face', 'plane', 'at'),
|
|
21
|
+
body1: everyKey()('part', 'at'),
|
|
22
|
+
origin: everyKey()('facing', 'gap', 'align', 'childRotate', 'pivot', 'offset', 'parentRotate'),
|
|
22
23
|
axes: everyKey()('x', 'y', 'z'),
|
|
23
24
|
alignment: everyKey()('mm'),
|
|
24
25
|
measure: everyKey()('from', 'times', 'plus', 'sum'),
|
|
@@ -59,13 +60,18 @@ function knownJointFields(j, path) {
|
|
|
59
60
|
only(o, JOINT_FIELDS.origin, `${path}.origin`);
|
|
60
61
|
if (o && typeof o === 'object') {
|
|
61
62
|
measure(o.gap, `${path}.origin.gap`);
|
|
62
|
-
only(o.
|
|
63
|
+
only(o.childRotate, JOINT_FIELDS.axes, `${path}.origin.childRotate`);
|
|
64
|
+
only(o.parentRotate, JOINT_FIELDS.axes, `${path}.origin.parentRotate`);
|
|
65
|
+
only(o.offset, JOINT_FIELDS.axes, `${path}.origin.offset`);
|
|
66
|
+
for (const [axis, d] of Object.entries(o.offset && typeof o.offset === 'object' ? o.offset : {}))
|
|
67
|
+
measure(d, `${path}.origin.offset.${axis}`);
|
|
63
68
|
only(o.align, JOINT_FIELDS.axes, `${path}.origin.align`);
|
|
64
69
|
for (const [axis, a] of Object.entries(o.align && typeof o.align === 'object' ? o.align : {})) {
|
|
65
70
|
only(a, JOINT_FIELDS.alignment, `${path}.origin.align.${axis}`);
|
|
66
71
|
measure(a?.mm, `${path}.origin.align.${axis}.mm`);
|
|
67
72
|
}
|
|
68
73
|
}
|
|
74
|
+
only(j.axis, JOINT_FIELDS.axes, `${path}.axis`);
|
|
69
75
|
if (j.travel && typeof j.travel === 'object') {
|
|
70
76
|
only(j.travel, JOINT_FIELDS.travel, `${path}.travel`);
|
|
71
77
|
source(j.travel.source, `${path}.travel.source`);
|
|
@@ -97,6 +103,61 @@ export function jointAbove(asset, part) {
|
|
|
97
103
|
return asset.joints?.find(j => j.body1.part === part);
|
|
98
104
|
}
|
|
99
105
|
export const stateIdOf = (j) => j.state?.id ?? j.id;
|
|
106
|
+
/** Whether a joint is placed at its parent's origin rather than on a face (ADR-0107 decision 4). */
|
|
107
|
+
export const isOriginPlaced = (j) => !!j.body0 && 'at' in j.body0;
|
|
108
|
+
/** A record's axis as a unit vector when it is written as one; undefined for a token. Zero or not finite is refused. */
|
|
109
|
+
export function jointVectorOf(axis, path) {
|
|
110
|
+
if (!axis || typeof axis !== 'object')
|
|
111
|
+
return undefined;
|
|
112
|
+
const v = AXES.map(a => axis[a]);
|
|
113
|
+
if (!v.every(x => typeof x === 'number' && Number.isFinite(x)))
|
|
114
|
+
fail('JOINT_SCHEMA', `${path}.axis`, 'an axis vector is three finite numbers { x, y, z }');
|
|
115
|
+
const n = Math.hypot(...v);
|
|
116
|
+
if (!(n > 1e-9))
|
|
117
|
+
fail('JOINT_SCHEMA', `${path}.axis`, 'an axis vector is not zero');
|
|
118
|
+
return v.map(x => x / n);
|
|
119
|
+
}
|
|
120
|
+
const hasParentRotate = (j) => AXES.some(a => (j.origin?.parentRotate?.[a] ?? 0) !== 0);
|
|
121
|
+
/**
|
|
122
|
+
* The joint frame's own words (ADR-0107, contract 2026-10-03): the pivot, the offset and the parent end's rotation, and
|
|
123
|
+
* the ends placed by an origin. A joint at its parent's origin is placed by offset and parentRotate alone: a face's
|
|
124
|
+
* words are refused by name. A joint frame turned by other than quarter turns turns the child after it is lined up, so
|
|
125
|
+
* its sides are not level with an edge: min or max is refused with it (ADR-0107 Q-B, kept on the frame's side by
|
|
126
|
+
* ADR-0140 3.10).
|
|
127
|
+
*/
|
|
128
|
+
function checkJointFrames(j, path) {
|
|
129
|
+
const o = j.origin ?? {};
|
|
130
|
+
const body0 = j.body0;
|
|
131
|
+
const body1 = j.body1;
|
|
132
|
+
if ('at' in body0 && body0.at !== 'origin')
|
|
133
|
+
fail('JOINT_SCHEMA', `${path}.body0`, `${String(body0.at)} is not a place on a part; at: 'origin' is`);
|
|
134
|
+
if ('at' in body1 && body1.at !== 'origin')
|
|
135
|
+
fail('JOINT_SCHEMA', `${path}.body1`, `${String(body1.at)} is not a place on a part; at: 'origin' is`);
|
|
136
|
+
if ('at' in body0 && ('face' in body0 || 'plane' in body0))
|
|
137
|
+
fail('JOINT_SCHEMA', `${path}.body0`, "a joint hangs at a part's origin or on a face, not both");
|
|
138
|
+
if (isOriginPlaced(j)) {
|
|
139
|
+
for (const key of ['align', 'gap', 'facing', 'childRotate', 'pivot'])
|
|
140
|
+
if (o[key] !== undefined)
|
|
141
|
+
fail('JOINT_SCHEMA', `${path}.origin.${key}`, `${key} places a part on a face; ${body0.part}'s origin is a point and a frame, placed by offset and parentRotate`);
|
|
142
|
+
}
|
|
143
|
+
else if ('at' in body1)
|
|
144
|
+
fail('JOINT_SCHEMA', `${path}.body1`, `a child is placed by its origin only at its parent's origin; ${'plane' in body0 ? 'the mounting plane' : `${body0.part}'s ${String(body0.face)}`} is a face`);
|
|
145
|
+
if (o.pivot !== undefined && !FIGURE_JOINT_PIVOTS.includes(o.pivot))
|
|
146
|
+
fail('JOINT_SCHEMA', `${path}.origin.pivot`, `${String(o.pivot)} is not a pivot; ${FIGURE_JOINT_PIVOTS.join(', ')}`);
|
|
147
|
+
if (o.pivot === 'child-face' && o.facing === 'centre')
|
|
148
|
+
fail('JOINT_SCHEMA', `${path}.origin.pivot`, `${body1.part} is fastened by its middle, not by a face; child-face has no face to turn about -- it turns about its middle already (child-centre)`);
|
|
149
|
+
if (o.parentRotate !== undefined) {
|
|
150
|
+
if (!o.parentRotate ||
|
|
151
|
+
typeof o.parentRotate !== 'object' ||
|
|
152
|
+
Object.values(o.parentRotate).some(v => typeof v !== 'number' || !Number.isFinite(v)))
|
|
153
|
+
fail('JOINT_SCHEMA', `${path}.origin.parentRotate`, 'a rotation is degrees about x, y and z');
|
|
154
|
+
const edges = AXES.filter(a => o.align?.[a] === 'min' || o.align?.[a] === 'max');
|
|
155
|
+
if (edges.length && !isQuarterMatrix(rpyMatrix(o.parentRotate)))
|
|
156
|
+
fail('JOINT_SCHEMA', `${path}.origin.parentRotate`, `the joint frame is turned by other than quarter turns; ${body1.part}'s sides are not level with the edges it is lined up with (${edges.join(', ')}) -- place it by the centre or a distance`);
|
|
157
|
+
}
|
|
158
|
+
if (o.offset !== undefined && (!o.offset || typeof o.offset !== 'object'))
|
|
159
|
+
fail('JOINT_SCHEMA', `${path}.origin.offset`, 'an offset is a length along x, y and z');
|
|
160
|
+
}
|
|
100
161
|
/** A record that is well formed on its own, and fits the tree the others make. */
|
|
101
162
|
function checkJoint(asset, j) {
|
|
102
163
|
const m = asset.document.model;
|
|
@@ -113,6 +174,7 @@ function checkJoint(asset, j) {
|
|
|
113
174
|
fail('JOINT_SCHEMA', path, 'a joint hangs from a part or the mounting plane (body0)');
|
|
114
175
|
if ('part' in j.body0)
|
|
115
176
|
partOf(m, j.body0.part, path);
|
|
177
|
+
checkJointFrames(j, path);
|
|
116
178
|
const others = (asset.joints ?? []).filter(o => o.id !== j.id);
|
|
117
179
|
const above = others.find(o => o.body1.part === j.body1.part);
|
|
118
180
|
if (above)
|
|
@@ -128,8 +190,8 @@ function checkJoint(asset, j) {
|
|
|
128
190
|
}
|
|
129
191
|
const moving = j.type !== 'fixed';
|
|
130
192
|
const limited = j.lowerLimit !== undefined || j.upperLimit !== undefined;
|
|
131
|
-
if (moving && !['X', 'Y', 'Z'].includes(j.axis))
|
|
132
|
-
fail('JOINT_SCHEMA', path, `a ${j.type} joint has an axis, X, Y or Z`);
|
|
193
|
+
if (moving && !['X', 'Y', 'Z'].includes(j.axis) && !jointVectorOf(j.axis, path))
|
|
194
|
+
fail('JOINT_SCHEMA', path, `a ${j.type} joint has an axis, X, Y or Z, or a vector { x, y, z }`);
|
|
133
195
|
if (!moving && (j.axis !== undefined || limited || j.mimic !== undefined || j.travel !== undefined))
|
|
134
196
|
fail('JOINT_SCHEMA', path, 'a fixed joint has no axis, limits, mimic or travel');
|
|
135
197
|
if (j.travel !== undefined && j.type !== 'prismatic')
|
|
@@ -176,11 +238,9 @@ function checkJoint(asset, j) {
|
|
|
176
238
|
if (j.mimic.offset !== undefined && !Number.isFinite(j.mimic.offset))
|
|
177
239
|
fail('JOINT_SCHEMA', path, "a mimic's offset is a number in the joint's unit");
|
|
178
240
|
}
|
|
179
|
-
if (j.origin?.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Object.values(j.origin.turn).some(t => !Number.isFinite(t) || Math.abs(t / 90 - Math.round(t / 90)) > 1e-9))
|
|
183
|
-
fail('JOINT_SCHEMA', `${path}.origin.turn`, "a turn is in quarter turns; an angle about the face's normal is its twist");
|
|
241
|
+
if (j.origin?.childRotate !== undefined &&
|
|
242
|
+
(!j.origin.childRotate || typeof j.origin.childRotate !== 'object' || Object.values(j.origin.childRotate).some(t => !Number.isFinite(t))))
|
|
243
|
+
fail('JOINT_SCHEMA', `${path}.origin.childRotate`, 'a pose is degrees about x, y and z');
|
|
184
244
|
/*
|
|
185
245
|
A row repeats its part and everything fastened to it (ADR-0093, 2026-09-26): a part fastened to a part in a row is
|
|
186
246
|
on every copy. A row inside a row is a grid; it runs along another axis than the rows around it.
|
|
@@ -210,9 +270,17 @@ function checkJoint(asset, j) {
|
|
|
210
270
|
const row = `${path}.repeat`;
|
|
211
271
|
if (!('part' in j.body0))
|
|
212
272
|
fail('JOINT_SCHEMA', row, 'a row fills the part it is fastened to; the mounting plane has no length to fill');
|
|
273
|
+
/* A row runs across a face, from edge to edge: a frame moved, turned or placed at an origin is not covered yet. */
|
|
274
|
+
if (isOriginPlaced(j))
|
|
275
|
+
fail('JOINT_SCHEMA', row, 'a row runs across the face it is fastened to; a joint placed at an origin has none');
|
|
276
|
+
for (const key of ['offset', 'parentRotate'])
|
|
277
|
+
if (j.origin?.[key] !== undefined)
|
|
278
|
+
fail('JOINT_SCHEMA', row, `a row with its joint frame moved or turned (origin.${key}) is not covered yet`);
|
|
279
|
+
if ((j.origin?.pivot ?? 'seat') !== 'seat')
|
|
280
|
+
fail('JOINT_SCHEMA', row, `a row turning about ${j.origin.pivot} is not covered yet; its copies turn where they are fastened`);
|
|
213
281
|
if (!['X', 'Y', 'Z'].includes(r.axis))
|
|
214
282
|
fail('JOINT_SCHEMA', row, `a row runs along X, Y or Z, not ${String(r.axis)}`);
|
|
215
|
-
if (r.axis.toLowerCase() ===
|
|
283
|
+
if (r.axis.toLowerCase() === receivingAxisOf(asset.document.model, asset, j))
|
|
216
284
|
fail('JOINT_SCHEMA', row, `a row runs across the face it is fastened to; ${r.axis} is out of that face`);
|
|
217
285
|
if (typeof r.pitch === 'number' ? !(Number.isFinite(r.pitch) && r.pitch > 0) : !followsSomething(r.pitch))
|
|
218
286
|
fail('GEOMETRY_DOMAIN', row, 'a row has a positive pitch, or one that follows a length');
|
|
@@ -232,75 +300,59 @@ function checkJoint(asset, j) {
|
|
|
232
300
|
* are chosen the same way each time, so writing a record again gives the same nodes.
|
|
233
301
|
*/
|
|
234
302
|
function writeJoint(asset, j) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
303
|
+
if (isOriginPlaced(j)) {
|
|
304
|
+
placeAtOrigin(asset, j);
|
|
305
|
+
return moveJoint(asset, j, IDENTITY);
|
|
306
|
+
}
|
|
238
307
|
/*
|
|
239
|
-
The record speaks
|
|
240
|
-
|
|
241
|
-
|
|
308
|
+
The record speaks in the parent's own frame (ADR-0140 3.11): its face's name, the axes of the alignment and of the
|
|
309
|
+
child's pose are the parent's, whatever way the parent stands -- the seat is written in that frame, so it follows
|
|
310
|
+
the parent as it turns or moves. The child meets the face by its real shape in the pose it is given.
|
|
242
311
|
*/
|
|
243
|
-
|
|
312
|
+
placeOnFace(asset, j, { pivot: j.origin?.pivot, offset: j.origin?.offset, parentRotate: j.origin?.parentRotate, path: `joints.${j.id}` });
|
|
313
|
+
moveJoint(asset, j, IDENTITY);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A joint at its parent's origin (ADR-0107 decision 4, contract 2026-10-03): F_parent = T(δ)·Rpy in the parent's own
|
|
317
|
+
* frame, as URDF reads a joint's origin -- no standing turn put in, no face measured. The child stands by its own origin
|
|
318
|
+
* there (F_child = I), or by its centre (F_child = T(its centre)), unturned.
|
|
319
|
+
*/
|
|
320
|
+
function placeAtOrigin(asset, j) {
|
|
321
|
+
const m = asset.document.model;
|
|
322
|
+
const child = j.body1.part;
|
|
323
|
+
const parent = j.body0.part;
|
|
244
324
|
const path = `joints.${j.id}`;
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const own = AXES.map(a => j.origin?.turn?.[a] ?? 0);
|
|
248
|
-
/* The child as it stands: its quarter turn, then its twist about the normal of the face it is fastened by. */
|
|
249
|
-
const childFace = childFaceOf(m, j);
|
|
250
|
-
const normal = FIGURE_FACES[childFace];
|
|
251
|
-
const standing = j.origin?.twist ? mul3(axisTurn(normal, j.origin.twist), turnMatrix(own)) : turnMatrix(own);
|
|
252
|
-
/* In the parent's frame: its twist about the face's normal there, and the quarter turn that is left. */
|
|
253
|
-
const inParent = mul3(back, standing);
|
|
254
|
-
const localNormal = axisOf(mv3(back, FACE_VECTOR[childFace]), `${j.body1.part}'s ${childFace}`, parent ?? '', back, path);
|
|
255
|
-
/* The record's twist, about the normal as it lies in the parent's frame; what is left is a quarter turn. */
|
|
256
|
-
/*
|
|
257
|
-
In the parent's frame the child stands twisted about the face's normal -- by its own twist, and back by its
|
|
258
|
-
parent's -- after a quarter turn. The twist is the one nearest what the record asks for.
|
|
259
|
-
*/
|
|
260
|
-
const alongNormal = mv3(back, AXES.map(a => (a === normal ? 1 : 0)));
|
|
261
|
-
const asked = (j.origin?.twist ?? 0) * Math.sign(alongNormal[AXES.indexOf(localNormal.axis)]);
|
|
262
|
-
const { quarter, deg } = splitTwist(inParent, localNormal.axis, asked, path);
|
|
325
|
+
const { pose } = partOf(m, child, child);
|
|
326
|
+
const noTurn = constantOf(m, 'deg', 0, 'a');
|
|
263
327
|
const old = pose.args.slice(3, 6);
|
|
264
|
-
pose.args = [
|
|
265
|
-
...pose.args.slice(0, 3),
|
|
266
|
-
...(turned || j.origin?.twist ? eulerOf(quarter) : own).map(t => constantOf(m, 'deg', t, 'a'))
|
|
267
|
-
];
|
|
328
|
+
pose.args = [...pose.args.slice(0, 3), noTurn, noTurn, noTurn];
|
|
268
329
|
for (const ref of old)
|
|
269
330
|
dropIfUnused(asset, ref, ownInput(m, child, ref) ? ref : undefined);
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
/* The measuring was done on the quarter turn; the part stands with its twist. */
|
|
296
|
-
if (deg) {
|
|
297
|
-
const now = pose.args.slice(3, 6);
|
|
298
|
-
pose.args = [...pose.args.slice(0, 3), ...eulerOfAny(inParent).map(t => constantOf(m, 'deg', t, 'a'))];
|
|
299
|
-
for (const ref of now)
|
|
300
|
-
dropIfUnused(asset, ref);
|
|
301
|
-
}
|
|
302
|
-
if (j.repeat && turned)
|
|
303
|
-
fail('JOINT_SCHEMA', `joints.${j.id}.repeat`, `${parent} stands turned; a row across a turned part is not covered yet`);
|
|
331
|
+
const name = namer(m, `${child}.on.${parent}`);
|
|
332
|
+
const zero = constantOf(m, 'mm', 0, 'd');
|
|
333
|
+
/* By its middle: the middle of its real extent (ADR-0140 3.4), from its origin -- or by its origin, nothing. */
|
|
334
|
+
const centre = (x) => {
|
|
335
|
+
if ('at' in j.body1)
|
|
336
|
+
return zero;
|
|
337
|
+
const along = (sign) => figureShapeExtremeAlong(m, child, AXES.map(a => (a === x ? sign : 0)), path);
|
|
338
|
+
return writeFigureReach(m, name, times(plus(along(1), times(along(-1), -1)), -0.5), `own-centre.${x}`);
|
|
339
|
+
};
|
|
340
|
+
const shift = j.origin?.offset ?? {};
|
|
341
|
+
const seat = AXES.map(x => shift[x] === undefined ? zero : measureRef(m, asset, name, shift[x], `offset.${x}`, `${path}.origin.offset.${x}`));
|
|
342
|
+
reseat(asset, child, `${parent}.local`, { seat, pose: AXES.map(centre) });
|
|
343
|
+
if (hasParentRotate(j))
|
|
344
|
+
turnSeat(asset, child, rpyMatrix(j.origin.parentRotate), j.origin.parentRotate, IDENTITY);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A moving joint's motion, at the joint frame, about or along its axis turned into the parent's frame by `back`: a token
|
|
348
|
+
* is one of the parent's axes, either way along it (a parent standing twisted refuses one that is not); a vector goes
|
|
349
|
+
* as it is, as its unit vector (URDF's axis). A row is laid out last, once its part's motion is written.
|
|
350
|
+
*/
|
|
351
|
+
function moveJoint(asset, j, back) {
|
|
352
|
+
const m = asset.document.model;
|
|
353
|
+
const child = j.body1.part;
|
|
354
|
+
const parent = 'part' in j.body0 ? j.body0.part : undefined;
|
|
355
|
+
const path = `joints.${j.id}`;
|
|
304
356
|
/* A row is laid out last, once its part's motion is written: every copy moves on it (a roller row spinning). */
|
|
305
357
|
if (j.type === 'fixed') {
|
|
306
358
|
if (j.repeat)
|
|
@@ -319,10 +371,17 @@ function writeJoint(asset, j) {
|
|
|
319
371
|
k: t.multiplier
|
|
320
372
|
}));
|
|
321
373
|
/* The axis turned into the parent's frame too: one of its axes, either way along it. */
|
|
322
|
-
const
|
|
323
|
-
const
|
|
374
|
+
const vector = jointVectorOf(j.axis, path);
|
|
375
|
+
const along = mv3(back, vector ?? AXES.map(a => (a === j.axis.toLowerCase() ? 1 : 0)));
|
|
376
|
+
const axisIndex = vector ? 0 : along.findIndex(v => Math.abs(Math.abs(v) - 1) < 1e-9);
|
|
324
377
|
if (axisIndex < 0)
|
|
325
378
|
fail('JOINT_SCHEMA', path, `${parent} stands twisted; the axis ${j.axis} is not level with an axis in it`);
|
|
379
|
+
/* Twelve places, as a twist's factors: the same record writes the same constants however the vector was turned. */
|
|
380
|
+
const direction = vector
|
|
381
|
+
? along.map(v => (Math.abs(v) < 1e-12 ? 0 : Math.round(v * 1e12) / 1e12))
|
|
382
|
+
: along[axisIndex] > 0
|
|
383
|
+
? 1
|
|
384
|
+
: -1;
|
|
326
385
|
addMotion(asset, {
|
|
327
386
|
kind: 'add-motion',
|
|
328
387
|
part: child,
|
|
@@ -353,7 +412,7 @@ function writeJoint(asset, j) {
|
|
|
353
412
|
c: j.mimic.offset ?? 0,
|
|
354
413
|
sum: Object.hasOwn(j.mimic, 'sum')
|
|
355
414
|
}
|
|
356
|
-
: undefined,
|
|
415
|
+
: undefined, direction);
|
|
357
416
|
if (j.repeat)
|
|
358
417
|
repeatPart(asset, j);
|
|
359
418
|
}
|
|
@@ -372,21 +431,20 @@ function repeatPart(asset, j) {
|
|
|
372
431
|
const theirs = ownSpansOf(m, parent, path)[axis];
|
|
373
432
|
if (!theirs)
|
|
374
433
|
fail('EDIT_TARGET', path, `${parent} has no two faces along ${axis}; there is no length to fill`);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
fail('EDIT_TARGET', path, `${part} has no two faces along ${axis}; its copies cannot be spaced`);
|
|
434
|
+
/* The part's reach both ways along the row, in its pose (ADR-0140 3.2): its extent, and its middle from its origin. */
|
|
435
|
+
const C = rpyMatrix(j.origin?.childRotate ?? {});
|
|
436
|
+
const along = (sign) => figureShapeExtremeAlong(m, part, mv3(transpose3(C), AXES.map(a => (a === axis ? sign : 0))), path);
|
|
437
|
+
const [ahead, behind] = [along(1), along(-1)];
|
|
380
438
|
const { place, pose } = partOf(m, part, path);
|
|
381
439
|
const seat = nodeById(m, `${part}.seat.rigid`);
|
|
382
440
|
const name = namer(m, `${part}.row`);
|
|
383
441
|
const zero = constantOf(m, 'mm', 0, 'd');
|
|
384
442
|
/* The seat moves to the row's start; each copy's centre is where the layout puts it from there. */
|
|
385
443
|
seat.args = seat.args.map((ref, i) => (i === k ? sumOf(m, name, theirs.lo, 'start') : ref));
|
|
386
|
-
const own =
|
|
444
|
+
const own = writeFigureReach(m, name, times(plus(ahead, times(behind, -1)), -0.5), 'own-centre');
|
|
387
445
|
pose.args = pose.args.map((ref, i) => (i === k ? own : ref));
|
|
388
446
|
const length = sumOf(m, name, [...theirs.hi, ...neg(theirs.lo)], 'length');
|
|
389
|
-
const extent =
|
|
447
|
+
const extent = writeFigureReach(m, name, plus(ahead, behind), 'extent');
|
|
390
448
|
const centred = constantOf(m, 'ratio', 0.5, 'k');
|
|
391
449
|
const limit = constantOf(m, 'count', REPEAT_LIMIT, 'limit');
|
|
392
450
|
const count = j.repeat.count;
|
|
@@ -512,8 +570,9 @@ export function stripJoint(asset, j) {
|
|
|
512
570
|
}
|
|
513
571
|
/**
|
|
514
572
|
* Every length a joint record holds that may follow something, read as a measure, with a way to write it back: the
|
|
515
|
-
* gap, a row's pitch, a slide's travel (its one-term form names the source `source`, and without one is `plus` mm)
|
|
516
|
-
*
|
|
573
|
+
* gap, a row's pitch, a slide's travel (its one-term form names the source `source`, and without one is `plus` mm),
|
|
574
|
+
* each offset across the face and each of the joint frame's offset (ADR-0107). Renaming a part and taking one away
|
|
575
|
+
* read and write them all alike.
|
|
517
576
|
*/
|
|
518
577
|
export function jointMeasures(j) {
|
|
519
578
|
const out = [];
|
|
@@ -557,6 +616,9 @@ export function jointMeasures(j) {
|
|
|
557
616
|
set: v => (how.mm = v)
|
|
558
617
|
});
|
|
559
618
|
}
|
|
619
|
+
for (const axis of Object.keys(j.origin?.offset ?? {}))
|
|
620
|
+
if (j.origin.offset[axis] !== undefined)
|
|
621
|
+
out.push({ at: `offset.${axis}`, get: () => j.origin.offset[axis], set: v => (j.origin.offset[axis] = v) });
|
|
560
622
|
return out;
|
|
561
623
|
}
|
|
562
624
|
/** The same measure with each term's source put through `to`, in the form it was written. */
|
|
@@ -610,11 +672,23 @@ export function rewriteJoints(asset) {
|
|
|
610
672
|
for (const n of asset.document.model.nodes)
|
|
611
673
|
if (n.op === 'member@1' && !nodeById(asset.document.model, `${n.id}.repeat`))
|
|
612
674
|
n.op = 'place@1';
|
|
675
|
+
/*
|
|
676
|
+
What the joints' nodes read before they go: a reach reads a shape's own values -- a pyramid's apex, a drawn
|
|
677
|
+
section's corners -- and an edit that took one away left it while the old nodes still read it. Once the joints are
|
|
678
|
+
written again, a part's own input nothing reads any more goes too.
|
|
679
|
+
*/
|
|
680
|
+
const m = asset.document.model;
|
|
681
|
+
const read = new Set(m.nodes.filter(n => joints.some(j => n.id.startsWith(`${j.body1.part}.on.`))).flatMap(n => n.args));
|
|
613
682
|
for (const j of [...order].reverse())
|
|
614
683
|
stripJoint(asset, j);
|
|
615
684
|
for (const j of order)
|
|
616
685
|
writeJoint(asset, j);
|
|
617
686
|
gatherRows(asset, joints);
|
|
687
|
+
for (const ref of read) {
|
|
688
|
+
const owner = m.nodes.find(n => n.op === 'place@1' && ref.startsWith(`${n.id}.`))?.id;
|
|
689
|
+
if (owner && ownInput(m, owner, ref))
|
|
690
|
+
dropIfUnused(asset, ref, ref);
|
|
691
|
+
}
|
|
618
692
|
// A joint's value as the author left it (「지금 자세를 기본으로」) is theirs, not the record's.
|
|
619
693
|
for (const j of order)
|
|
620
694
|
if (j.type !== 'fixed' && !j.mimic && keep[stateIdOf(j)] !== undefined)
|
|
@@ -625,20 +699,14 @@ export function setJoint(asset, joint) {
|
|
|
625
699
|
// As JSON: a field left undefined is a field not there, which is how the asset stores it.
|
|
626
700
|
const j = JSON.parse(JSON.stringify(joint ?? null));
|
|
627
701
|
checkJoint(asset, j);
|
|
628
|
-
/*
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
j.origin = { ...(j.origin ?? {}), twist };
|
|
637
|
-
else if (turn.some(t => t !== 0))
|
|
638
|
-
j.origin = {
|
|
639
|
-
...(j.origin ?? {}),
|
|
640
|
-
turn: Object.fromEntries(AXES.map((a, i) => [a, turn[i]]))
|
|
641
|
-
};
|
|
702
|
+
/*
|
|
703
|
+
The child's pose now, in the parent's frame, becomes the record's childRotate the first time it is fastened -- by a
|
|
704
|
+
face, at any angle (ADR-0140 3.6): it keeps the pose it has. At an origin it stands unturned.
|
|
705
|
+
*/
|
|
706
|
+
if (!jointAbove(asset, j.body1.part) && !j.origin?.childRotate && !isOriginPlaced(j)) {
|
|
707
|
+
const pose = childRotateOf(asset, j.body1.part, j.body0);
|
|
708
|
+
if (pose)
|
|
709
|
+
j.origin = { ...(j.origin ?? {}), childRotate: pose };
|
|
642
710
|
}
|
|
643
711
|
const joints = (asset.joints ?? []).filter(o => o.id !== j.id);
|
|
644
712
|
const followers = joints.filter(o => figureMimicLeadersOf(o).some(t => t.joint === j.id));
|
|
@@ -676,10 +744,20 @@ export function applyFaceRemap(asset, part, remap) {
|
|
|
676
744
|
continue;
|
|
677
745
|
}
|
|
678
746
|
const face = faceOf(r.to, path);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
747
|
+
/*
|
|
748
|
+
An end placed by an origin is on no face, so there is no face to move it to (ADR-0107 decision 4): refused by
|
|
749
|
+
name, never skipped -- the caller shows it. At a parent's origin the child is placed by its origin or centre, so
|
|
750
|
+
a face for the child is refused here too, not later by the joint's own check under another path.
|
|
751
|
+
*/
|
|
752
|
+
/* The child end is on no face of its own (ADR-0140 3.2): only the end it hangs from has one to move to. */
|
|
753
|
+
if (j.body0?.part !== part)
|
|
754
|
+
fail('EDIT_TARGET', path, `${r.ref.id} fastens ${part} by no face of ${part}'s own; only the end it hangs from, on ${'part' in j.body0 ? j.body0.part : 'the plane'}, is on a face`);
|
|
755
|
+
const end = j.body0;
|
|
756
|
+
if ('at' in end)
|
|
757
|
+
fail('EDIT_TARGET', path, end === j.body0
|
|
758
|
+
? `${r.ref.id} hangs at ${part}'s origin, not on a face of it`
|
|
759
|
+
: `${part} is placed by its own origin on ${r.ref.id}, not by a face of it`);
|
|
760
|
+
end.face = face;
|
|
683
761
|
faces.add(face);
|
|
684
762
|
}
|
|
685
763
|
else if (r?.ref?.kind === 'feature') {
|
|
@@ -731,6 +809,33 @@ export function removeJoint(asset, id) {
|
|
|
731
809
|
// Parts hanging from it are written again: they hang from where it stands now.
|
|
732
810
|
rewriteJoints(asset);
|
|
733
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* A part's pose now, in the frame of what it would hang from -- the parent's own frame, or the plane's, after the joint
|
|
814
|
+
* frame's own turn (`parentRotate`) -- as degrees about x, y and z (Rz·Ry·Rx), six places: nothing when it stands
|
|
815
|
+
* square to it. Read from the figure as drawn at rest: every joint at 0, its reference pose.
|
|
816
|
+
*/
|
|
817
|
+
function childRotateOf(asset, part, body0, parentRotate) {
|
|
818
|
+
const rest = Object.fromEntries((asset.joints ?? []).filter(j => j.type !== 'fixed' && !j.mimic).map(j => [stateIdOf(j), 0]));
|
|
819
|
+
const compiled = compileFigureAssetUnjointed(asset);
|
|
820
|
+
/* A control whose range does not hold 0 is read where it stands: its reference is still 0, but it cannot be drawn there. */
|
|
821
|
+
const geometry = (() => {
|
|
822
|
+
try {
|
|
823
|
+
return compiled.evaluate(rest).geometry;
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
return compiled.evaluate().geometry;
|
|
827
|
+
}
|
|
828
|
+
})();
|
|
829
|
+
const of = (id) => geometry.find(g => g.id === id && g.pose)?.pose?.r;
|
|
830
|
+
const mine = of(part);
|
|
831
|
+
if (!mine)
|
|
832
|
+
return undefined;
|
|
833
|
+
const theirs = body0 && 'part' in body0 ? of(body0.part) : IDENTITY;
|
|
834
|
+
if (!theirs)
|
|
835
|
+
return undefined;
|
|
836
|
+
const pose = rpyOf(mul3(transpose3(mul3(theirs, rpyMatrix(parentRotate ?? {}))), mine));
|
|
837
|
+
return Object.keys(pose).length ? pose : undefined;
|
|
838
|
+
}
|
|
734
839
|
/** `attach` is the modeller's word for a fixed joint: the record says it, whatever joint was there keeps its id and motion. */
|
|
735
840
|
export function fastenAsJoint(asset, a) {
|
|
736
841
|
const was = jointAbove(asset, a.part);
|
|
@@ -745,19 +850,19 @@ export function fastenAsJoint(asset, a) {
|
|
|
745
850
|
let id = a.part;
|
|
746
851
|
for (let k = 2; !was && taken.has(id); k++)
|
|
747
852
|
id = `${a.part}-${k}`;
|
|
853
|
+
const body0 = to?.plane !== undefined ? { plane: to.plane } : { part: to?.part, face: to?.face };
|
|
854
|
+
/* Its pose now, in the new parent's frame (ADR-0140 3.6): it is fastened as it stands. */
|
|
855
|
+
const pose = childRotateOf(asset, a.part, body0, was?.origin?.parentRotate);
|
|
748
856
|
setJoint(asset, {
|
|
749
857
|
...(was ?? { id, type: 'fixed' }),
|
|
750
|
-
body0
|
|
751
|
-
body1: { part: a.part
|
|
858
|
+
body0,
|
|
859
|
+
body1: { part: a.part },
|
|
752
860
|
origin: {
|
|
753
|
-
...(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
: was?.origin?.twist
|
|
759
|
-
? { twist: was.origin.twist }
|
|
760
|
-
: {}),
|
|
861
|
+
...(pose ? { childRotate: pose } : {}),
|
|
862
|
+
/* The joint frame's own words stay the joint's, as its motion does. */
|
|
863
|
+
...(was?.origin?.pivot ? { pivot: was.origin.pivot } : {}),
|
|
864
|
+
...(was?.origin?.offset ? { offset: was.origin.offset } : {}),
|
|
865
|
+
...(was?.origin?.parentRotate ? { parentRotate: was.origin.parentRotate } : {}),
|
|
761
866
|
...(a.facing ? { facing: a.facing } : {}),
|
|
762
867
|
...(a.gap !== undefined ? { gap: a.gap } : {}),
|
|
763
868
|
...(a.align ? { align: a.align } : {})
|