@hatiolab/figure-model 0.9.6 → 0.11.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/CHANGELOG.md +41 -5
- package/dist/asset-types.d.ts +24 -56
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/asset-types.js.map +1 -1
- package/dist/asset.d.ts +0 -7
- package/dist/asset.d.ts.map +1 -1
- package/dist/asset.js +11 -45
- package/dist/asset.js.map +1 -1
- package/dist/authoring-actions.d.ts.map +1 -1
- package/dist/authoring-actions.js +5 -27
- package/dist/authoring-actions.js.map +1 -1
- package/dist/authoring-bindings.js +1 -1
- package/dist/authoring-bindings.js.map +1 -1
- package/dist/authoring-dimensions.js +3 -3
- package/dist/authoring-fastening.d.ts +16 -2
- package/dist/authoring-fastening.d.ts.map +1 -1
- package/dist/authoring-fastening.js +106 -47
- package/dist/authoring-fastening.js.map +1 -1
- package/dist/authoring-nodes.d.ts +4 -4
- package/dist/authoring-nodes.d.ts.map +1 -1
- package/dist/authoring-nodes.js +7 -7
- package/dist/authoring-nodes.js.map +1 -1
- package/dist/authoring-placement.d.ts +40 -0
- package/dist/authoring-placement.d.ts.map +1 -0
- package/dist/{authoring-occupancy.js → authoring-placement.js} +225 -238
- package/dist/authoring-placement.js.map +1 -0
- package/dist/authoring-proportion.d.ts.map +1 -1
- package/dist/authoring-proportion.js +28 -12
- package/dist/authoring-proportion.js.map +1 -1
- package/dist/authoring-size.js +1 -1
- package/dist/authoring-types.d.ts +57 -91
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/authoring-types.js +8 -8
- package/dist/authoring-types.js.map +1 -1
- package/dist/body.d.ts +19 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +37 -0
- package/dist/body.js.map +1 -0
- package/dist/clip-keeping.d.ts.map +1 -1
- package/dist/clip-keeping.js +7 -5
- package/dist/clip-keeping.js.map +1 -1
- package/dist/command-preview.js +4 -4
- package/dist/command-preview.js.map +1 -1
- package/dist/contract.d.ts +0 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/editor-workspace.js +1 -1
- package/dist/editor-workspace.js.map +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +7 -8
- package/dist/examples.js.map +1 -1
- package/dist/face-overhang.js +2 -2
- package/dist/face-overhang.js.map +1 -1
- package/dist/fasten-place.d.ts +18 -0
- package/dist/fasten-place.d.ts.map +1 -1
- package/dist/fasten-place.js +43 -7
- package/dist/fasten-place.js.map +1 -1
- package/dist/gate.d.ts +15 -32
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +34 -101
- package/dist/gate.js.map +1 -1
- package/dist/graph-types.d.ts +2 -2
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +6 -67
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/joint-fields.d.ts +6 -8
- package/dist/joint-fields.d.ts.map +1 -1
- package/dist/joint-fields.js +6 -6
- package/dist/joint-fields.js.map +1 -1
- package/dist/joint-list.d.ts +11 -9
- package/dist/joint-list.d.ts.map +1 -1
- package/dist/joint-list.js +75 -53
- package/dist/joint-list.js.map +1 -1
- package/dist/joint-motions.d.ts +30 -0
- package/dist/joint-motions.d.ts.map +1 -0
- package/dist/joint-motions.js +28 -0
- package/dist/joint-motions.js.map +1 -0
- package/dist/joint-part-edits.d.ts +1 -10
- package/dist/joint-part-edits.d.ts.map +1 -1
- package/dist/joint-part-edits.js +101 -98
- package/dist/joint-part-edits.js.map +1 -1
- package/dist/joint-writing.d.ts +3 -4
- package/dist/joint-writing.d.ts.map +1 -1
- package/dist/joint-writing.js +229 -170
- package/dist/joint-writing.js.map +1 -1
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.d.ts.map +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/kernel-version.js.map +1 -1
- package/dist/part-edit.d.ts +1 -8
- package/dist/part-edit.d.ts.map +1 -1
- package/dist/part-edit.js +19 -34
- package/dist/part-edit.js.map +1 -1
- package/dist/proposal.d.ts +4 -11
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +6 -13
- package/dist/proposal.js.map +1 -1
- package/dist/recipe-from-parts.d.ts.map +1 -1
- package/dist/recipe-from-parts.js +12 -4
- package/dist/recipe-from-parts.js.map +1 -1
- package/dist/recipe.d.ts.map +1 -1
- package/dist/recipe.js +11 -9
- package/dist/recipe.js.map +1 -1
- package/dist/release-contract.d.ts +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +33 -30
- package/dist/release-contract.js.map +1 -1
- package/dist/release-metrics.d.ts +14 -14
- package/dist/release-metrics.d.ts.map +1 -1
- package/dist/release-metrics.js +13 -29
- package/dist/release-metrics.js.map +1 -1
- package/dist/resize-plan.d.ts +0 -5
- package/dist/resize-plan.d.ts.map +1 -1
- package/dist/resize-plan.js +4 -29
- package/dist/resize-plan.js.map +1 -1
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +44 -48
- package/dist/robots.js.map +1 -1
- package/dist/size-edit.js +3 -3
- package/dist/size-edit.js.map +1 -1
- package/dist/surface.js +2 -2
- package/docs/asset-persistence.md +1 -1
- package/docs/attachment-response-spec-review-2026-10-24.md +56 -0
- package/docs/document-contract.md +0 -1
- package/docs/editor-integration.md +2 -0
- package/docs/modeller-interaction-performance-2026-10-07.md +1 -1
- package/docs/operator-contracts.md +3 -18
- package/docs/performance-followup-2026-10-05.md +1 -1
- package/docs/review-2026-09-22.md +2 -2
- package/docs/shape-dimension-contract.md +27 -25
- package/docs/status.md +12 -2
- package/package.json +1 -1
- package/dist/authoring-occupancy.d.ts +0 -28
- package/dist/authoring-occupancy.d.ts.map +0 -1
- package/dist/authoring-occupancy.js.map +0 -1
- package/dist/room.d.ts +0 -53
- package/dist/room.d.ts.map +0 -1
- package/dist/room.js +0 -101
- package/dist/room.js.map +0 -1
package/dist/joint-writing.js
CHANGED
|
@@ -9,14 +9,16 @@ 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_INPUTS } from "./instance-size.js";
|
|
12
|
-
import { FIGURE_FACES,
|
|
12
|
+
import { FIGURE_FACES, FIGURE_MOTION_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
14
|
import { mul3, neg, half, ownSpansOf, IDENTITY, transpose3, mv3, eulerOfAny, rpyMatrix, isQuarterMatrix, receivingAxisOf, rpyOf } from "./authoring-geometry.js";
|
|
15
15
|
import { placeOnFace, addMotion, removeMotion, reseat, turnSeat } from "./authoring-fastening.js";
|
|
16
16
|
import { figureShapeExtremeAlong, lin, plus, times, writeFigureReach } from "./shape-extreme.js";
|
|
17
|
+
import { figureAxesOf, figureMotionsOf, isMovingJoint, motionNodeIdOf, motionStateIdOf } from "./joint-motions.js";
|
|
17
18
|
const everyKey = () => (...keys) => keys;
|
|
18
19
|
const JOINT_FIELDS = {
|
|
19
|
-
joint: everyKey()('id', '
|
|
20
|
+
joint: everyKey()('id', 'body0', 'body1', 'origin', 'motion', 'repeat'),
|
|
21
|
+
motion: everyKey()('id', 'type', 'axis', 'through', 'lowerLimit', 'upperLimit', 'travel', 'state', 'mimic'),
|
|
20
22
|
body0: everyKey()('part', 'face', 'surface', 'plane', 'at'),
|
|
21
23
|
surface: everyKey()('toward', 'around', 'along'),
|
|
22
24
|
body1: everyKey()('part', 'at'),
|
|
@@ -28,8 +30,8 @@ const JOINT_FIELDS = {
|
|
|
28
30
|
source: everyKey()('of', 'axis', 'input', 'part'),
|
|
29
31
|
travel: everyKey()('source', 'times', 'plus', 'sum'),
|
|
30
32
|
state: everyKey()('id', 'label', 'start', 'sweep'),
|
|
31
|
-
mimic: everyKey()('
|
|
32
|
-
leader: everyKey()('
|
|
33
|
+
mimic: everyKey()('motion', 'multiplier', 'offset', 'sum'),
|
|
34
|
+
leader: everyKey()('motion', 'multiplier'),
|
|
33
35
|
repeat: everyKey()('axis', 'pitch', 'count', 'rise'),
|
|
34
36
|
rise: everyKey()('axis', 'mm')
|
|
35
37
|
};
|
|
@@ -79,18 +81,29 @@ function knownJointFields(j, path) {
|
|
|
79
81
|
measure(a?.mm, `${path}.origin.align.${axis}.mm`);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
if (j.motion !== undefined && !Array.isArray(j.motion))
|
|
85
|
+
fail('JOINT_SCHEMA', `${path}.motion`, 'a motion is a list of axes');
|
|
86
|
+
for (const [k, a] of (Array.isArray(j.motion) ? j.motion : []).entries()) {
|
|
87
|
+
const at = `${path}.motion.${k}`;
|
|
88
|
+
only(a, JOINT_FIELDS.motion, at);
|
|
89
|
+
if (!a || typeof a !== 'object')
|
|
90
|
+
continue;
|
|
91
|
+
only(a.axis, JOINT_FIELDS.axes, `${at}.axis`);
|
|
92
|
+
only(a.through, JOINT_FIELDS.axes, `${at}.through`);
|
|
93
|
+
for (const [axis, d] of Object.entries(a.through && typeof a.through === 'object' ? a.through : {}))
|
|
94
|
+
measure(d, `${at}.through.${axis}`);
|
|
95
|
+
if (a.travel && typeof a.travel === 'object') {
|
|
96
|
+
only(a.travel, JOINT_FIELDS.travel, `${at}.travel`);
|
|
97
|
+
source(a.travel.source, `${at}.travel.source`);
|
|
98
|
+
if (Array.isArray(a.travel.sum))
|
|
99
|
+
measure({ sum: a.travel.sum }, `${at}.travel`);
|
|
100
|
+
}
|
|
101
|
+
only(a.state, JOINT_FIELDS.state, `${at}.state`);
|
|
102
|
+
only(a.mimic, JOINT_FIELDS.mimic, `${at}.mimic`);
|
|
103
|
+
if (Array.isArray(a.mimic?.sum))
|
|
104
|
+
for (const [i, t] of a.mimic.sum.entries())
|
|
105
|
+
only(t, JOINT_FIELDS.leader, `${at}.mimic.sum.${i}`);
|
|
88
106
|
}
|
|
89
|
-
only(j.state, JOINT_FIELDS.state, `${path}.state`);
|
|
90
|
-
only(j.mimic, JOINT_FIELDS.mimic, `${path}.mimic`);
|
|
91
|
-
if (Array.isArray(j.mimic?.sum))
|
|
92
|
-
for (const [i, t] of j.mimic.sum.entries())
|
|
93
|
-
only(t, JOINT_FIELDS.leader, `${path}.mimic.sum.${i}`);
|
|
94
107
|
only(j.repeat, JOINT_FIELDS.repeat, `${path}.repeat`);
|
|
95
108
|
if (j.repeat && typeof j.repeat === 'object') {
|
|
96
109
|
measure(j.repeat.pitch, `${path}.repeat.pitch`);
|
|
@@ -110,7 +123,6 @@ export function figureAttachmentsOf(asset) {
|
|
|
110
123
|
export function jointAbove(asset, part) {
|
|
111
124
|
return asset.joints?.find(j => j.body1.part === part);
|
|
112
125
|
}
|
|
113
|
-
export const stateIdOf = (j) => j.state?.id ?? j.id;
|
|
114
126
|
/** Whether a joint is placed at its parent's origin rather than on a face (ADR-0107 decision 4). */
|
|
115
127
|
export const isOriginPlaced = (j) => !!j.body0 && 'at' in j.body0;
|
|
116
128
|
/** A record's axis as a unit vector when it is written as one; undefined for a token. Zero or not finite is refused. */
|
|
@@ -204,6 +216,83 @@ function checkJointFrames(j, path) {
|
|
|
204
216
|
if (o.offset !== undefined && (!o.offset || typeof o.offset !== 'object'))
|
|
205
217
|
fail('JOINT_SCHEMA', `${path}.origin.offset`, 'an offset is a length along x, y and z');
|
|
206
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* A joint's axes (ADR-0159): each turns or slides about an axis, has its limits -- a revolute one may have none and
|
|
221
|
+
* turns without end, a slide always has them -- or follows other axes. Its id is unique among every joint's axes and
|
|
222
|
+
* every other joint's id, so a follower, a control and the joint list name one thing. Two axes in a row on one line
|
|
223
|
+
* are two: each its own control, range and followers, though the pose they make is one turn (review 2026-10-24).
|
|
224
|
+
*/
|
|
225
|
+
function checkMotion(others, j, path) {
|
|
226
|
+
const motions = figureMotionsOf(j);
|
|
227
|
+
const theirs = figureAxesOf(others).map(a => a.motion);
|
|
228
|
+
const ids = new Set([...theirs.map(a => a.id), ...others.map(o => o.id)]);
|
|
229
|
+
for (const [k, a] of motions.entries()) {
|
|
230
|
+
const at = `${path}.motion.${k}`;
|
|
231
|
+
const record = j.motion[k];
|
|
232
|
+
if (!record || typeof record !== 'object')
|
|
233
|
+
fail('JOINT_SCHEMA', at, 'an axis is { type, axis }');
|
|
234
|
+
if (!FIGURE_MOTION_TYPES.includes(a.type))
|
|
235
|
+
fail('JOINT_SCHEMA', at, `${String(a.type)} is not a motion; ${FIGURE_MOTION_TYPES.join(', ')}`);
|
|
236
|
+
if (record.id !== undefined && (typeof record.id !== 'string' || !record.id.trim()))
|
|
237
|
+
fail('JOINT_SCHEMA', `${at}.id`, 'an axis id is a name');
|
|
238
|
+
if (ids.has(a.id))
|
|
239
|
+
fail('JOINT_SCHEMA', `${at}.id`, `${a.id} names another axis already; an axis id is unique among every joint's axes`);
|
|
240
|
+
ids.add(a.id);
|
|
241
|
+
if (!['X', 'Y', 'Z'].includes(a.axis) && !jointVectorOf(a.axis, at))
|
|
242
|
+
fail('JOINT_SCHEMA', at, `a ${a.type} axis is X, Y or Z, or a vector { x, y, z }`);
|
|
243
|
+
for (const [x, d] of Object.entries(a.through ?? {}))
|
|
244
|
+
if (d !== undefined && (typeof d === 'number' ? !Number.isFinite(d) : !followsSomething(d)))
|
|
245
|
+
fail('JOINT_SCHEMA', `${at}.through.${x}`, 'the point an axis passes is millimetres, or a length that follows something');
|
|
246
|
+
if (a.travel !== undefined && a.type !== 'prismatic')
|
|
247
|
+
fail('JOINT_SCHEMA', at, 'only a prismatic axis travels a length');
|
|
248
|
+
const limited = a.lowerLimit !== undefined || a.upperLimit !== undefined;
|
|
249
|
+
if (a.type === 'prismatic' && a.travel && !limited) {
|
|
250
|
+
record.lowerLimit = 0;
|
|
251
|
+
record.upperLimit = 1;
|
|
252
|
+
}
|
|
253
|
+
/* A revolute axis without limits turns without end (USD); with them, both are given. A slide always has them. */
|
|
254
|
+
if (!a.mimic && (limited || a.type === 'prismatic')) {
|
|
255
|
+
const [lo, hi] = [record.lowerLimit, record.upperLimit];
|
|
256
|
+
if (!(Number.isFinite(lo) && Number.isFinite(hi) && lo < hi))
|
|
257
|
+
fail('JOINT_SCHEMA', at, `a ${a.type} axis has both limits, lower below upper`);
|
|
258
|
+
/*
|
|
259
|
+
The rest pose is every axis at 0 -- the kinematic reference it is read at (joint-list.ts) -- and 0 need not be a
|
|
260
|
+
value it may take: an axis limited to 10..80 still has its reference at 0 (ADR-0107 J4). Its starting value, the
|
|
261
|
+
stored state default, is held to its limits as every control's is (addMotion).
|
|
262
|
+
*/
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [k, a] of motions.entries()) {
|
|
266
|
+
if (!a.mimic)
|
|
267
|
+
continue;
|
|
268
|
+
const at = `${path}.motion.${k}`;
|
|
269
|
+
/*
|
|
270
|
+
One leader or a sum of them (ADR-0107 decision 2), each held to the same rules: an axis that leads itself -- so a
|
|
271
|
+
follower is never a leader and no loop can close -- that moves the way this one does (degrees are not added to
|
|
272
|
+
millimetres), named once, with a finite multiplier.
|
|
273
|
+
*/
|
|
274
|
+
const sum = Object.hasOwn(a.mimic, 'sum');
|
|
275
|
+
if (sum && !(Array.isArray(a.mimic.sum) && a.mimic.sum.length))
|
|
276
|
+
fail('JOINT_SCHEMA', at, 'a mimic sum follows one axis or more');
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
for (const t of figureMimicLeadersOf(a)) {
|
|
279
|
+
const leader = [...theirs, ...motions].find(o => o.id === t?.motion && o.id !== a.id);
|
|
280
|
+
if (!leader)
|
|
281
|
+
fail('JOINT_SCHEMA', at, `${String(t?.motion)} is not a moving axis to follow`);
|
|
282
|
+
if (seen.has(leader.id))
|
|
283
|
+
fail('JOINT_SCHEMA', at, `${leader.id} is followed twice; a sum names each axis once`);
|
|
284
|
+
seen.add(leader.id);
|
|
285
|
+
if (leader.mimic)
|
|
286
|
+
fail('JOINT_SCHEMA', at, `${leader.id} follows another axis; follow the one it follows`);
|
|
287
|
+
if ((leader.type === 'prismatic') !== (a.type === 'prismatic'))
|
|
288
|
+
fail('JOINT_SCHEMA', at, `${leader.id} ${leader.type === 'prismatic' ? 'slides' : 'turns'}; an axis follows ones that move the same way: a slide a slide, a turn a turn`);
|
|
289
|
+
if (!Number.isFinite(t.multiplier))
|
|
290
|
+
fail('JOINT_SCHEMA', at, 'a mimic has a multiplier');
|
|
291
|
+
}
|
|
292
|
+
if (a.mimic.offset !== undefined && !Number.isFinite(a.mimic.offset))
|
|
293
|
+
fail('JOINT_SCHEMA', at, "a mimic's offset is a number in the axis's unit");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
207
296
|
/** A record that is well formed on its own, and fits the tree the others make. */
|
|
208
297
|
function checkJoint(asset, j) {
|
|
209
298
|
const m = asset.document.model;
|
|
@@ -211,8 +300,6 @@ function checkJoint(asset, j) {
|
|
|
211
300
|
if (!j || typeof j.id !== 'string' || !j.id.trim())
|
|
212
301
|
fail('JOINT_SCHEMA', 'joints', 'a joint has an id');
|
|
213
302
|
knownJointFields(j, path);
|
|
214
|
-
if (!FIGURE_JOINT_TYPES.includes(j.type))
|
|
215
|
-
fail('JOINT_SCHEMA', path, `${String(j.type)} is not a joint type; ${FIGURE_JOINT_TYPES.join(', ')}`);
|
|
216
303
|
if (!j.body1 || typeof j.body1.part !== 'string')
|
|
217
304
|
fail('JOINT_SCHEMA', path, 'a joint carries a part (body1)');
|
|
218
305
|
partOf(m, j.body1.part, path);
|
|
@@ -234,56 +321,7 @@ function checkJoint(asset, j) {
|
|
|
234
321
|
up = next && 'part' in next ? next.part : undefined;
|
|
235
322
|
}
|
|
236
323
|
}
|
|
237
|
-
|
|
238
|
-
const limited = j.lowerLimit !== undefined || j.upperLimit !== undefined;
|
|
239
|
-
if (moving && !['X', 'Y', 'Z'].includes(j.axis) && !jointVectorOf(j.axis, path))
|
|
240
|
-
fail('JOINT_SCHEMA', path, `a ${j.type} joint has an axis, X, Y or Z, or a vector { x, y, z }`);
|
|
241
|
-
if (!moving && (j.axis !== undefined || limited || j.mimic !== undefined || j.travel !== undefined))
|
|
242
|
-
fail('JOINT_SCHEMA', path, 'a fixed joint has no axis, limits, mimic or travel');
|
|
243
|
-
if (j.travel !== undefined && j.type !== 'prismatic')
|
|
244
|
-
fail('JOINT_SCHEMA', path, 'only a prismatic joint travels a length');
|
|
245
|
-
if (j.type === 'prismatic' && j.travel && !limited) {
|
|
246
|
-
j.lowerLimit = 0;
|
|
247
|
-
j.upperLimit = 1;
|
|
248
|
-
}
|
|
249
|
-
/* A revolute joint without limits turns without end (USD); with them, both are given. A slide always has them. */
|
|
250
|
-
if (moving && !j.mimic && (limited || j.type === 'prismatic')) {
|
|
251
|
-
const [lo, hi] = [j.lowerLimit, j.upperLimit];
|
|
252
|
-
if (!(Number.isFinite(lo) && Number.isFinite(hi) && lo < hi))
|
|
253
|
-
fail('JOINT_SCHEMA', path, `a ${j.type} joint has both limits, lower below upper`);
|
|
254
|
-
/*
|
|
255
|
-
The rest pose is every joint at 0 -- the kinematic reference its axis is read at (joint-list.ts) -- and 0 need not
|
|
256
|
-
be a value the joint may take: a joint limited to 10..80 still has its reference at 0 (ADR-0107 J4). Its starting
|
|
257
|
-
value, the stored state default, is held to its limits as every control's is (addMotion).
|
|
258
|
-
*/
|
|
259
|
-
}
|
|
260
|
-
if (j.mimic) {
|
|
261
|
-
/*
|
|
262
|
-
One leader or a sum of them (ADR-0107 decision 2), each held to the same rules: a moving joint that leads itself
|
|
263
|
-
-- so a follower is never a leader and no loop can close -- that moves the way this one does (degrees are not
|
|
264
|
-
added to millimetres), named once, with a finite multiplier.
|
|
265
|
-
*/
|
|
266
|
-
const sum = Object.hasOwn(j.mimic, 'sum');
|
|
267
|
-
if (sum && !(Array.isArray(j.mimic.sum) && j.mimic.sum.length))
|
|
268
|
-
fail('JOINT_SCHEMA', path, 'a mimic sum follows one joint or more');
|
|
269
|
-
const seen = new Set();
|
|
270
|
-
for (const t of figureMimicLeadersOf(j)) {
|
|
271
|
-
const leader = others.find(o => o.id === t?.joint);
|
|
272
|
-
if (!leader || leader.type === 'fixed')
|
|
273
|
-
fail('JOINT_SCHEMA', path, `${String(t?.joint)} is not a moving joint to follow`);
|
|
274
|
-
if (seen.has(leader.id))
|
|
275
|
-
fail('JOINT_SCHEMA', path, `${leader.id} is followed twice; a sum names each joint once`);
|
|
276
|
-
seen.add(leader.id);
|
|
277
|
-
if (leader.mimic)
|
|
278
|
-
fail('JOINT_SCHEMA', path, `${leader.id} follows another joint; follow the one it follows`);
|
|
279
|
-
if ((leader.type === 'prismatic') !== (j.type === 'prismatic'))
|
|
280
|
-
fail('JOINT_SCHEMA', path, `${leader.id} ${leader.type === 'prismatic' ? 'slides' : 'turns'}; a joint follows ones that move the same way: a slide a slide, a turn a turn`);
|
|
281
|
-
if (!Number.isFinite(t.multiplier))
|
|
282
|
-
fail('JOINT_SCHEMA', path, 'a mimic has a multiplier');
|
|
283
|
-
}
|
|
284
|
-
if (j.mimic.offset !== undefined && !Number.isFinite(j.mimic.offset))
|
|
285
|
-
fail('JOINT_SCHEMA', path, "a mimic's offset is a number in the joint's unit");
|
|
286
|
-
}
|
|
324
|
+
checkMotion(others, j, path);
|
|
287
325
|
if (j.origin?.childRotate !== undefined &&
|
|
288
326
|
(!j.origin.childRotate || typeof j.origin.childRotate !== 'object' || Object.values(j.origin.childRotate).some(t => !Number.isFinite(t))))
|
|
289
327
|
fail('JOINT_SCHEMA', `${path}.origin.childRotate`, 'a pose is degrees about x, y and z');
|
|
@@ -392,78 +430,84 @@ function placeAtOrigin(asset, j) {
|
|
|
392
430
|
turnSeat(asset, child, rpyMatrix(j.origin.parentRotate), j.origin.parentRotate, IDENTITY);
|
|
393
431
|
}
|
|
394
432
|
/**
|
|
395
|
-
* A moving joint's
|
|
396
|
-
* is one of the parent's axes, either way along it (a parent standing twisted refuses one that
|
|
397
|
-
* as it is, as its unit vector (URDF's axis).
|
|
433
|
+
* A moving joint's axes, in order (ADR-0159), each at the joint frame about or along its axis turned into the parent's
|
|
434
|
+
* frame by `back`: a token is one of the parent's axes, either way along it (a parent standing twisted refuses one that
|
|
435
|
+
* is not); a vector goes as it is, as its unit vector (URDF's axis). Each passes its point (`through`) and is carried by
|
|
436
|
+
* the ones before it. A row is laid out last, once its part's motion is written.
|
|
398
437
|
*/
|
|
399
438
|
function moveJoint(asset, j, back) {
|
|
400
439
|
const m = asset.document.model;
|
|
401
440
|
const child = j.body1.part;
|
|
402
441
|
const parent = 'part' in j.body0 ? j.body0.part : undefined;
|
|
403
442
|
const path = `joints.${j.id}`;
|
|
404
|
-
/* A row is laid out last, once its part's motion is written: every copy moves on it (a roller row spinning). */
|
|
405
|
-
if (j.type === 'fixed') {
|
|
406
|
-
if (j.repeat)
|
|
407
|
-
repeatPart(asset, j);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
const kind = j.type === 'prismatic' ? 'slide' : 'turn';
|
|
411
|
-
const unit = j.type === 'prismatic' ? (j.travel ? 'ratio' : 'mm') : 'deg';
|
|
412
|
-
const range = j.lowerLimit !== undefined && j.upperLimit !== undefined
|
|
413
|
-
? { min: j.lowerLimit, max: j.upperLimit }
|
|
414
|
-
: j.type === 'revolute'
|
|
415
|
-
? { min: -180, max: 180 }
|
|
416
|
-
: { min: 0, max: 1 };
|
|
417
|
-
const leaders = figureMimicLeadersOf(j).map(t => ({
|
|
418
|
-
leader: (asset.joints ?? []).find(o => o.id === t.joint),
|
|
419
|
-
k: t.multiplier
|
|
420
|
-
}));
|
|
421
|
-
/* The axis turned into the parent's frame too: one of its axes, either way along it. */
|
|
422
|
-
const vector = jointVectorOf(j.axis, path);
|
|
423
|
-
const along = mv3(back, vector ?? AXES.map(a => (a === j.axis.toLowerCase() ? 1 : 0)));
|
|
424
|
-
const axisIndex = vector ? 0 : along.findIndex(v => Math.abs(Math.abs(v) - 1) < 1e-9);
|
|
425
|
-
if (axisIndex < 0)
|
|
426
|
-
fail('JOINT_SCHEMA', path, `${parent} stands twisted; the axis ${j.axis} is not level with an axis in it`);
|
|
427
|
-
/* Twelve places, as a twist's factors: the same record writes the same constants however the vector was turned. */
|
|
428
|
-
const direction = vector
|
|
429
|
-
? along.map(v => (Math.abs(v) < 1e-12 ? 0 : Math.round(v * 1e12) / 1e12))
|
|
430
|
-
: along[axisIndex] > 0
|
|
431
|
-
? 1
|
|
432
|
-
: -1;
|
|
433
|
-
addMotion(asset, {
|
|
434
|
-
kind: 'add-motion',
|
|
435
|
-
part: child,
|
|
436
|
-
motion: { kind, axis: AXES[axisIndex] },
|
|
437
|
-
state: {
|
|
438
|
-
id: stateIdOf(j),
|
|
439
|
-
unit,
|
|
440
|
-
min: range.min,
|
|
441
|
-
max: range.max,
|
|
442
|
-
start: j.state?.start ?? Math.min(range.max, Math.max(range.min, 0)),
|
|
443
|
-
...(j.state?.label !== undefined ? { label: j.state.label } : {}),
|
|
444
|
-
...(j.state?.sweep !== undefined ? { sweep: j.state.sweep } : {})
|
|
445
|
-
},
|
|
446
|
-
...(j.travel ? { travel: j.travel } : {}),
|
|
447
|
-
replace: true
|
|
448
|
-
},
|
|
449
443
|
/*
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
444
|
+
What an axis's value is worked out from comes first, the order axes are carried in after: each keeps its place
|
|
445
|
+
(`<part>.motion.<k>`), but an axis that follows another of its joint is written once its leader's distance is there
|
|
446
|
+
-- else it read the leader's control, a share, as millimetres (review 2026-10-24, F3). A leader leads itself, so
|
|
447
|
+
the axes that lead themselves are written first.
|
|
453
448
|
*/
|
|
454
|
-
j
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
449
|
+
const order = figureMotionsOf(j)
|
|
450
|
+
.map((a, k) => ({ a, k }))
|
|
451
|
+
.sort((x, y) => Number(!!x.a.mimic) - Number(!!y.a.mimic) || x.k - y.k);
|
|
452
|
+
for (const { a, k } of order) {
|
|
453
|
+
const at = `${path}.motion.${k}`;
|
|
454
|
+
const kind = a.type === 'prismatic' ? 'slide' : 'turn';
|
|
455
|
+
const unit = a.type === 'prismatic' ? (a.travel ? 'ratio' : 'mm') : 'deg';
|
|
456
|
+
const range = a.lowerLimit !== undefined && a.upperLimit !== undefined
|
|
457
|
+
? { min: a.lowerLimit, max: a.upperLimit }
|
|
458
|
+
: a.type === 'revolute'
|
|
459
|
+
? { min: -180, max: 180 }
|
|
460
|
+
: { min: 0, max: 1 };
|
|
461
|
+
const leaders = figureMimicLeadersOf(a).map(t => ({ leader: figureAxisOf(asset, t.motion), k: t.multiplier }));
|
|
462
|
+
/* The axis turned into the parent's frame too: one of its axes, either way along it. */
|
|
463
|
+
const vector = jointVectorOf(a.axis, at);
|
|
464
|
+
const along = mv3(back, vector ?? AXES.map(x => (x === a.axis.toLowerCase() ? 1 : 0)));
|
|
465
|
+
const axisIndex = vector ? 0 : along.findIndex(v => Math.abs(Math.abs(v) - 1) < 1e-9);
|
|
466
|
+
if (axisIndex < 0)
|
|
467
|
+
fail('JOINT_SCHEMA', at, `${parent} stands twisted; the axis ${a.axis} is not level with an axis in it`);
|
|
468
|
+
/* Twelve places, as a twist's factors: the same record writes the same constants however the vector was turned. */
|
|
469
|
+
const direction = vector
|
|
470
|
+
? along.map(v => (Math.abs(v) < 1e-12 ? 0 : Math.round(v * 1e12) / 1e12))
|
|
471
|
+
: along[axisIndex] > 0
|
|
472
|
+
? 1
|
|
473
|
+
: -1;
|
|
474
|
+
addMotion(asset, {
|
|
475
|
+
kind: 'add-motion',
|
|
476
|
+
part: child,
|
|
477
|
+
motion: { kind, axis: AXES[axisIndex] },
|
|
478
|
+
state: {
|
|
479
|
+
id: motionStateIdOf(a),
|
|
480
|
+
unit,
|
|
481
|
+
min: range.min,
|
|
482
|
+
max: range.max,
|
|
483
|
+
start: a.state?.start ?? Math.min(range.max, Math.max(range.min, 0)),
|
|
484
|
+
...(a.state?.label !== undefined ? { label: a.state.label } : {}),
|
|
485
|
+
...(a.state?.sweep !== undefined ? { sweep: a.state.sweep } : {})
|
|
486
|
+
},
|
|
487
|
+
...(a.travel ? { travel: a.travel } : {}),
|
|
488
|
+
replace: true
|
|
489
|
+
},
|
|
490
|
+
/*
|
|
491
|
+
A follower follows how far its leader has gone -- its distance or angle -- not the leader's control: a leader whose
|
|
492
|
+
stroke is a share of a length has a control of 0..1, and multiplying that gave a follower a number with no length
|
|
493
|
+
(the telescopic conveyor, 2026-09-25). For a leader whose control is its distance the two are one value.
|
|
494
|
+
*/
|
|
495
|
+
a.mimic
|
|
496
|
+
? {
|
|
497
|
+
terms: leaders.map(({ leader, k: times }) => ({
|
|
498
|
+
of: nodeById(m, motionNodeIdOf(leader.joint.body1.part, leader.k))?.args[3] ?? motionStateIdOf(leader.motion),
|
|
499
|
+
k: times
|
|
500
|
+
})),
|
|
501
|
+
c: a.mimic.offset ?? 0,
|
|
502
|
+
sum: Object.hasOwn(a.mimic, 'sum')
|
|
503
|
+
}
|
|
504
|
+
: undefined, direction, { k, through: a.through, path: at });
|
|
505
|
+
}
|
|
464
506
|
if (j.repeat)
|
|
465
507
|
repeatPart(asset, j);
|
|
466
508
|
}
|
|
509
|
+
/** The axis that goes by this id, among the asset's joints. */
|
|
510
|
+
const figureAxisOf = (asset, id) => figureAxesOf(asset.joints).find(a => a.motion.id === id);
|
|
467
511
|
/**
|
|
468
512
|
* A fastened part made a row (r3 §8): its placement becomes the template of a `repeat@1`. The row starts at the
|
|
469
513
|
* parent's lower edge along the axis and runs to its upper edge, so its length is the parent's and follows it; each
|
|
@@ -618,9 +662,9 @@ export function stripJoint(asset, j) {
|
|
|
618
662
|
}
|
|
619
663
|
/**
|
|
620
664
|
* Every length a joint record holds that may follow something, read as a measure, with a way to write it back: the
|
|
621
|
-
* gap, a row's pitch,
|
|
622
|
-
* each offset across the face and each of the joint frame's offset (ADR-0107).
|
|
623
|
-
* read and write them all alike.
|
|
665
|
+
* gap, a row's pitch, each slide's travel (its one-term form names the source `source`, and without one is `plus` mm)
|
|
666
|
+
* and each axis's point (ADR-0159), each offset across the face and each of the joint frame's offset (ADR-0107).
|
|
667
|
+
* Renaming a part and taking one away read and write them all alike.
|
|
624
668
|
*/
|
|
625
669
|
export function jointMeasures(j) {
|
|
626
670
|
const out = [];
|
|
@@ -628,32 +672,37 @@ export function jointMeasures(j) {
|
|
|
628
672
|
out.push({ at: 'gap', get: () => j.origin.gap, set: v => (j.origin.gap = v) });
|
|
629
673
|
if (j.repeat)
|
|
630
674
|
out.push({ at: 'pitch', get: () => j.repeat.pitch, set: v => (j.repeat.pitch = v) });
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
675
|
+
for (const [k, a] of (j.motion ?? []).entries()) {
|
|
676
|
+
if (a.travel) {
|
|
677
|
+
const t = a.travel;
|
|
678
|
+
out.push({
|
|
679
|
+
at: `motion.${k}.travel`,
|
|
680
|
+
get: () => Object.hasOwn(t, 'sum')
|
|
681
|
+
? { sum: t.sum, ...(t.plus !== undefined ? { plus: t.plus } : {}) }
|
|
682
|
+
: t.source
|
|
683
|
+
? {
|
|
684
|
+
from: t.source,
|
|
685
|
+
...(t.times !== undefined ? { times: t.times } : {}),
|
|
686
|
+
...(t.plus !== undefined ? { plus: t.plus } : {})
|
|
687
|
+
}
|
|
688
|
+
: (t.plus ?? 0),
|
|
689
|
+
set: v => {
|
|
690
|
+
const f = followedTermsOf(v);
|
|
691
|
+
a.travel = !f
|
|
692
|
+
? { plus: v }
|
|
693
|
+
: f.sum
|
|
694
|
+
? { sum: f.terms, ...(f.plus ? { plus: f.plus } : {}) }
|
|
695
|
+
: {
|
|
696
|
+
source: f.terms[0].from,
|
|
697
|
+
...(f.terms[0].times !== undefined ? { times: f.terms[0].times } : {}),
|
|
698
|
+
...(v.plus !== undefined ? { plus: v.plus } : {})
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
for (const axis of Object.keys(a.through ?? {}))
|
|
704
|
+
if (a.through[axis] !== undefined)
|
|
705
|
+
out.push({ at: `motion.${k}.through.${axis}`, get: () => a.through[axis], set: v => (a.through[axis] = v) });
|
|
657
706
|
}
|
|
658
707
|
for (const axis of Object.keys(j.origin?.align ?? {})) {
|
|
659
708
|
const how = j.origin.align[axis];
|
|
@@ -678,17 +727,19 @@ export function mapMeasureSources(measure, to) {
|
|
|
678
727
|
return { ...measure, sum: f.terms.map(t => ({ ...t, from: to(t.from) })) };
|
|
679
728
|
return { ...measure, from: to(f.terms[0].from) };
|
|
680
729
|
}
|
|
681
|
-
/** Joints in an order each can be written in: its parent's joint and the joints it mimics first. */
|
|
730
|
+
/** Joints in an order each can be written in: its parent's joint and the joints whose axes it mimics first. */
|
|
682
731
|
function writingOrder(joints) {
|
|
683
732
|
const done = new Set();
|
|
684
733
|
const out = [];
|
|
685
734
|
const byChild = new Map(joints.map(j => [j.body1.part, j]));
|
|
735
|
+
const jointOf = new Map(figureAxesOf(joints).map(a => [a.motion.id, a.joint.id]));
|
|
686
736
|
while (out.length < joints.length) {
|
|
687
737
|
const ready = joints.filter(j => {
|
|
688
738
|
if (done.has(j.id))
|
|
689
739
|
return false;
|
|
690
740
|
const up = 'part' in j.body0 ? byChild.get(j.body0.part) : undefined;
|
|
691
|
-
|
|
741
|
+
const leaders = figureMotionsOf(j).flatMap(a => figureMimicLeadersOf(a).map(t => jointOf.get(t.motion)));
|
|
742
|
+
return (!up || done.has(up.id)) && leaders.every(id => id === j.id || done.has(id));
|
|
692
743
|
});
|
|
693
744
|
if (!ready.length)
|
|
694
745
|
fail('JOINT_TREE', 'joints', 'the joints do not make a tree');
|
|
@@ -737,10 +788,10 @@ export function rewriteJoints(asset) {
|
|
|
737
788
|
if (owner && ownInput(m, owner, ref))
|
|
738
789
|
dropIfUnused(asset, ref, ref);
|
|
739
790
|
}
|
|
740
|
-
//
|
|
741
|
-
for (const
|
|
742
|
-
if (
|
|
743
|
-
asset.stateDefaults[
|
|
791
|
+
// An axis's value as the author left it (「지금 자세를 기본으로」) is theirs, not the record's.
|
|
792
|
+
for (const { motion } of figureAxesOf(order))
|
|
793
|
+
if (!motion.mimic && keep[motionStateIdOf(motion)] !== undefined)
|
|
794
|
+
asset.stateDefaults[motionStateIdOf(motion)] = keep[motionStateIdOf(motion)];
|
|
744
795
|
});
|
|
745
796
|
}
|
|
746
797
|
export function setJoint(asset, joint) {
|
|
@@ -757,9 +808,13 @@ export function setJoint(asset, joint) {
|
|
|
757
808
|
j.origin = { ...(j.origin ?? {}), childRotate: pose };
|
|
758
809
|
}
|
|
759
810
|
const joints = (asset.joints ?? []).filter(o => o.id !== j.id);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
811
|
+
/* An axis something follows stays: the joint written again without it would leave its followers following nothing. */
|
|
812
|
+
const was = (asset.joints ?? []).find(o => o.id === j.id);
|
|
813
|
+
const staying = new Set(figureMotionsOf(j).map(a => a.id));
|
|
814
|
+
const going = (was ? figureMotionsOf(was) : []).map(a => a.id).filter(id => !staying.has(id));
|
|
815
|
+
const followers = followersOf(joints, going);
|
|
816
|
+
if (followers.length)
|
|
817
|
+
fail('JOINT_SCHEMA', `joints.${j.id}`, `${followers.map(f => f.id).join(', ')} follow ${going.join(', ')}; ${j.id} cannot stop moving that way`);
|
|
763
818
|
asset.joints = [...joints, j];
|
|
764
819
|
rewriteJoints(asset);
|
|
765
820
|
}
|
|
@@ -823,12 +878,14 @@ export function applyFaceRemap(asset, part, remap) {
|
|
|
823
878
|
}
|
|
824
879
|
remappedFaces.set(asset, faces);
|
|
825
880
|
}
|
|
881
|
+
/** The joints with an axis that follows one of these axes. */
|
|
882
|
+
const followersOf = (joints, axes) => joints.filter(o => figureMotionsOf(o).some(a => figureMimicLeadersOf(a).some(t => axes.includes(t.motion))));
|
|
826
883
|
export function removeJoint(asset, id) {
|
|
827
884
|
const joints = (asset.joints ?? []);
|
|
828
885
|
const j = joints.find(o => o.id === id);
|
|
829
886
|
if (!j)
|
|
830
887
|
fail('EDIT_TARGET', `joints.${id}`, `there is no joint ${id}`);
|
|
831
|
-
const followers = joints.filter(o =>
|
|
888
|
+
const followers = followersOf(joints.filter(o => o.id !== id), figureMotionsOf(j).map(a => a.id));
|
|
832
889
|
if (followers.length)
|
|
833
890
|
fail('JOINT_SCHEMA', `joints.${id}`, `${followers.map(f => f.id).join(', ')} follow ${id}; remove them first`);
|
|
834
891
|
const m = asset.document.model;
|
|
@@ -863,7 +920,9 @@ export function removeJoint(asset, id) {
|
|
|
863
920
|
* square to it. Read from the figure as drawn at rest: every joint at 0, its reference pose.
|
|
864
921
|
*/
|
|
865
922
|
function childRotateOf(asset, part, body0, parentRotate) {
|
|
866
|
-
const rest = Object.fromEntries((asset.joints
|
|
923
|
+
const rest = Object.fromEntries(figureAxesOf(asset.joints)
|
|
924
|
+
.filter(a => !a.motion.mimic)
|
|
925
|
+
.map(a => [motionStateIdOf(a.motion), 0]));
|
|
867
926
|
const compiled = compileFigureAssetUnjointed(asset);
|
|
868
927
|
/* A control whose range does not hold 0 is read where it stands: its reference is still 0, but it cannot be drawn there. */
|
|
869
928
|
const geometry = (() => {
|
|
@@ -929,7 +988,7 @@ export function fastenAsJoint(asset, a, standing = asset) {
|
|
|
929
988
|
/* Its pose now, in the new parent's frame (ADR-0140 3.6): it is fastened as it stands. */
|
|
930
989
|
const pose = childRotateOf(standing, a.part, body0, was?.origin?.parentRotate);
|
|
931
990
|
setJoint(asset, {
|
|
932
|
-
...(was ?? { id
|
|
991
|
+
...(was ?? { id }),
|
|
933
992
|
body0,
|
|
934
993
|
body1: { part: a.part },
|
|
935
994
|
origin: {
|