@things-factory/figure-ui 10.1.46 → 10.1.48
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/client/modeller/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +61 -81
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +254 -262
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +566 -223
- package/client/modeller/v3-fields.ts +84 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-joint-fan.ts +292 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +101 -9
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/modeller/v3-thumbnail.ts +8 -4
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +4 -1
- package/dist-client/modeller/figure-v3-links.js +64 -82
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +4 -1
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +44 -37
- package/dist-client/modeller/v3-canvas.js +230 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +73 -1
- package/dist-client/modeller/v3-editor.js +593 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +39 -0
- package/dist-client/modeller/v3-fields.js +69 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
- package/dist-client/modeller/v3-joint-fan.js +264 -0
- package/dist-client/modeller/v3-joint-fan.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +35 -1
- package/dist-client/modeller/v3-links.js +101 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/modeller/v3-thumbnail.js +6 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +71 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-joint-fan.test.ts +210 -0
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +89 -6
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-palette-icons.test.ts +22 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-thumbnail.test.ts +29 -0
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +90 -6
- package/translations/ja.json +92 -8
- package/translations/ko.json +90 -6
- package/translations/ms.json +92 -8
- package/translations/zh.json +92 -8
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { compileV3Asset, compileV3Graph, v3WorldBoxesOf, v3WritersOf } from '@hatiolab/figure-model';
|
|
2
|
+
/*
|
|
3
|
+
* Where the selected part turns, and how far -- a translucent fan on the editing board (V2's joint gizmo, inventory
|
|
4
|
+
* 3.38). An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and
|
|
5
|
+
* pushed each slider to its end to find out. The fan lies in the plane the part turns in, at the pivot, open from the
|
|
6
|
+
* control's lowest value to its highest; the edge at the value the figure holds now is where the part is.
|
|
7
|
+
*
|
|
8
|
+
* It is found by evaluating, not by reading the graph's shape: the frame that carries the part is sampled across the
|
|
9
|
+
* control's range, and a point at a fixed distance from the pivot traces the fan. So the direction is right whatever
|
|
10
|
+
* the slope of the control, and a joint inside other joints is drawn where its parents put it.
|
|
11
|
+
*
|
|
12
|
+
* Two ways a part turns are read:
|
|
13
|
+
* - a motion made in the modeller (`add-motion`): `<part>.motion`, the frame it moves is its seat, the pivot the
|
|
14
|
+
* seat's origin;
|
|
15
|
+
* - a joint the converter wrote from V2 (`<joint>.motion` · `<joint>.chain`): the part is composed on the joint's
|
|
16
|
+
* chain, the pivot is the joint's origin (`<joint>.frameOnParent`).
|
|
17
|
+
* A slide has no fan. Nothing is drawn when the turn cannot be found -- a fan in some guessed plane would point the
|
|
18
|
+
* wrong way.
|
|
19
|
+
*/
|
|
20
|
+
/** The fan's name on the board. It is not a part: not in the list, not saved, not picked. */
|
|
21
|
+
export const JOINT_FAN = '__joint-fan';
|
|
22
|
+
/** How thick the fan is: thin enough not to hide the part, thick enough to be seen edge-on. */
|
|
23
|
+
const THICKNESS = 6;
|
|
24
|
+
/** About one segment every six degrees, so a full turn reads as a circle. */
|
|
25
|
+
const STEP_DEG = 6;
|
|
26
|
+
const apply = (p, v) => [0, 1, 2].map(i => p.r[i][0] * v[0] + p.r[i][1] * v[1] + p.r[i][2] * v[2] + p.t[i]);
|
|
27
|
+
const sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
28
|
+
const dot = (a, b) => a.reduce((s, v, i) => s + v * b[i], 0);
|
|
29
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
30
|
+
const norm = (a) => {
|
|
31
|
+
const l = Math.hypot(...a);
|
|
32
|
+
return l > 1e-9 ? a.map(v => v / l) : null;
|
|
33
|
+
};
|
|
34
|
+
/** How far outside a part's box a pivot may lie and still be on the part: the kernel's contact skin. */
|
|
35
|
+
const SEAM = 1;
|
|
36
|
+
/** An origin this deep in a part's box (a share of its half-measure, on every axis) is housed by the part. */
|
|
37
|
+
const HOUSED = 0.5;
|
|
38
|
+
const AXIS_NAMES = ['x', 'y', 'z'];
|
|
39
|
+
function turnOf(asset, part, values, box) {
|
|
40
|
+
const m = asset.document.model;
|
|
41
|
+
const node = (id) => m.nodes.find(n => n.id === id);
|
|
42
|
+
const constant = (ref) => m.constants.find(c => c.id === ref)?.value;
|
|
43
|
+
const valueOf = (n) => (n ? values[Object.values(n.outputs)[0]] : undefined);
|
|
44
|
+
const controlOf = (ref, seen = new Set()) => {
|
|
45
|
+
if (!ref || seen.has(ref))
|
|
46
|
+
return null;
|
|
47
|
+
seen.add(ref);
|
|
48
|
+
const input = m.inputs.find(i => i.id === ref && i.role === 'state');
|
|
49
|
+
if (input)
|
|
50
|
+
return { id: input.id, min: input.min ?? 0, max: input.max ?? 0 };
|
|
51
|
+
const writer = m.nodes.find(n => Object.values(n.outputs).includes(ref));
|
|
52
|
+
for (const arg of writer?.args ?? []) {
|
|
53
|
+
const found = controlOf(arg, seen);
|
|
54
|
+
if (found)
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
const axisOf = (motion) => {
|
|
60
|
+
const axis = motion.args.slice(0, 3).map(constant);
|
|
61
|
+
return axis.every(v => typeof v === 'number') ? norm(axis) : null;
|
|
62
|
+
};
|
|
63
|
+
/* Made in the modeller: the part's own motion turns its seat about the seat's origin. */
|
|
64
|
+
const own = node(`${part}.motion`);
|
|
65
|
+
if (own) {
|
|
66
|
+
if (own.op !== 'axis-turn@1')
|
|
67
|
+
return null;
|
|
68
|
+
const axis = axisOf(own);
|
|
69
|
+
const control = controlOf(own.args[3]);
|
|
70
|
+
const at = valueOf(node(`${part}.pose`));
|
|
71
|
+
if (!axis || !control || !at)
|
|
72
|
+
return null;
|
|
73
|
+
return { carrier: own.id, pivot: [0, 0, 0], axis, toward: at.t, control, turnedPart: part };
|
|
74
|
+
}
|
|
75
|
+
/*
|
|
76
|
+
Written by the converter: a joint (`<joint>.motion`, its origin `<joint>.frameOnParent`) carries its own part and
|
|
77
|
+
everything fastened below it, through its chain (the first joint of a chain has only its attachment). A part shows
|
|
78
|
+
the joint it is to that part of the mechanism (user, 2026-09-24: 「elbow-housing itself is a axis」 · 「please check
|
|
79
|
+
all that logic and concept」), tested in this order on the joints as they stand now:
|
|
80
|
+
1. housing -- a joint's origin lies deep inside the part, which is centred on it: the shoulder housing shows the
|
|
81
|
+
shoulder and the elbow housing the elbow, though another joint carries them;
|
|
82
|
+
2. turned -- the joint that carries the part, when its axis passes through the part: through its middle for a
|
|
83
|
+
body that rolls on it (the wrist roll, the tool flange, the gripper body), through its root for a link (the
|
|
84
|
+
upper arm, the forearm);
|
|
85
|
+
3. otherwise none: the pedestal (the base's axis runs through it but turns it not), the fingers.
|
|
86
|
+
An axis passing through a part is not enough by itself: the wrist roll's axis runs the forearm's length and the
|
|
87
|
+
tool roll's through the wrist pitch, and neither turns them (an earlier rule said so, and was wrong).
|
|
88
|
+
*/
|
|
89
|
+
if (!box)
|
|
90
|
+
return null;
|
|
91
|
+
const carrierOf = (joint) => (node(`${joint}.chain`) ? `${joint}.chain` : `${joint}.attach`);
|
|
92
|
+
const mine = node(`${part}.moved`)?.args.find(arg => /\.(chain|attach)\.value$/.test(arg))?.replace(/\.value$/, '');
|
|
93
|
+
const centre = AXIS_NAMES.map(a => box.centre[a]);
|
|
94
|
+
const half = AXIS_NAMES.map(a => box.extent[a] / 2);
|
|
95
|
+
const joints = m.nodes
|
|
96
|
+
.filter(n => n.op === 'axis-turn@1' && n.id.endsWith('.motion') && node(n.id.replace(/\.motion$/, '.frameOnParent')))
|
|
97
|
+
.map(motion => {
|
|
98
|
+
const joint = motion.id.replace(/\.motion$/, '');
|
|
99
|
+
const carrier = carrierOf(joint);
|
|
100
|
+
const frame = valueOf(node(carrier));
|
|
101
|
+
const origin = valueOf(node(`${joint}.frameOnParent`));
|
|
102
|
+
const local = axisOf(motion);
|
|
103
|
+
if (!frame || !origin || !local)
|
|
104
|
+
return null;
|
|
105
|
+
const at = apply(frame, origin.t);
|
|
106
|
+
const along = norm([0, 1, 2].map(i => dot(frame.r[i], local)));
|
|
107
|
+
if (!along)
|
|
108
|
+
return null;
|
|
109
|
+
const off = sub(centre, at);
|
|
110
|
+
return {
|
|
111
|
+
joint,
|
|
112
|
+
motion,
|
|
113
|
+
carrier,
|
|
114
|
+
origin,
|
|
115
|
+
/* How deep the origin lies: 0 at the centre, 1 on the box's face, on the axis where it is shallowest. */
|
|
116
|
+
depth: Math.max(...[0, 1, 2].map(i => Math.abs(off[i]) / (half[i] + SEAM))),
|
|
117
|
+
fromCentre: Math.hypot(...off),
|
|
118
|
+
through: lineMeetsBox(at, along, centre, half)
|
|
119
|
+
};
|
|
120
|
+
})
|
|
121
|
+
.filter((c) => !!c);
|
|
122
|
+
const housing = joints.filter(c => c.depth <= HOUSED).sort((a, b) => a.fromCentre - b.fromCentre)[0];
|
|
123
|
+
const carrying = joints.find(c => c.carrier === mine);
|
|
124
|
+
const chosen = housing ?? (carrying && carrying.through ? carrying : undefined);
|
|
125
|
+
if (!chosen)
|
|
126
|
+
return null;
|
|
127
|
+
const axis = axisOf(chosen.motion);
|
|
128
|
+
const control = controlOf(chosen.motion.args[3]);
|
|
129
|
+
if (!axis || !control)
|
|
130
|
+
return null;
|
|
131
|
+
/* The reference edge points at what the joint turns: of the parts it carries, the one farthest off its axis. */
|
|
132
|
+
const flatOff = (t) => {
|
|
133
|
+
const rel = sub(t, chosen.origin.t);
|
|
134
|
+
return Math.hypot(...sub(rel, axis.map(v => v * dot(rel, axis))));
|
|
135
|
+
};
|
|
136
|
+
const turned = m.nodes
|
|
137
|
+
.filter(n => n.id.endsWith('.moved') && n.args.includes(`${chosen.carrier}.value`))
|
|
138
|
+
.map(n => ({ id: n.id.replace(/\.moved$/, ''), t: valueOf(node(n.id.replace(/\.moved$/, '.pose')))?.t }))
|
|
139
|
+
.filter((one) => !!one.t)
|
|
140
|
+
.sort((a, b) => flatOff(b.t) - flatOff(a.t))[0];
|
|
141
|
+
return { carrier: chosen.carrier, pivot: chosen.origin.t, axis, toward: turned?.t ?? chosen.origin.t, control, turnedPart: turned?.id ?? part };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The fan for the part, as a body to stand on the board, or null when the part does not turn. `values` are the
|
|
145
|
+
* inputs the figure is drawn with now (a pose in hand included), so the fan follows the arm as it is posed.
|
|
146
|
+
*/
|
|
147
|
+
export function jointFanOf(asset, part, values = {}) {
|
|
148
|
+
if (!part)
|
|
149
|
+
return null;
|
|
150
|
+
let graph;
|
|
151
|
+
try {
|
|
152
|
+
graph = compileV3Graph(asset.document.model);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const inputs = { ...asset.designInputs, ...asset.stateDefaults, ...values };
|
|
158
|
+
const at = (over) => graph.evaluate({ ...inputs, ...over }).values;
|
|
159
|
+
let now;
|
|
160
|
+
try {
|
|
161
|
+
now = at({});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
let box;
|
|
167
|
+
try {
|
|
168
|
+
box = v3WorldBoxesOf(compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, ...pick(values, asset.designInputs) }, stateDefaults: { ...asset.stateDefaults, ...pick(values, asset.stateDefaults) } })
|
|
169
|
+
.evaluate()
|
|
170
|
+
.geometry.filter(g => g.id === part), v3WritersOf(asset))[0];
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
box = undefined;
|
|
174
|
+
}
|
|
175
|
+
const turn = turnOf(asset, part, now, box);
|
|
176
|
+
if (!turn)
|
|
177
|
+
return null;
|
|
178
|
+
const carrierRef = Object.values(asset.document.model.nodes.find(n => n.id === turn.carrier).outputs)[0];
|
|
179
|
+
const carrierNow = now[carrierRef];
|
|
180
|
+
if (!carrierNow)
|
|
181
|
+
return null;
|
|
182
|
+
/* The reference edge points from the pivot toward the part, in the plane it turns in; any perpendicular if it sits on the axis. */
|
|
183
|
+
const rel = sub(turn.toward, turn.pivot);
|
|
184
|
+
const inPlane = sub(rel, turn.axis.map(v => v * dot(rel, turn.axis)));
|
|
185
|
+
const d = norm(inPlane) ?? norm(cross(turn.axis, Math.abs(turn.axis[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]));
|
|
186
|
+
/*
|
|
187
|
+
Sized from what the joint turns, as V2 sized it from the joint's child: selecting the elbow housing or the forearm
|
|
188
|
+
shows the elbow's one fan, not a smaller one for the smaller part (user, 2026-09-24).
|
|
189
|
+
*/
|
|
190
|
+
const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(asset, turn.turnedPart, inputs), Math.hypot(...inPlane)) * 0.6));
|
|
191
|
+
const edge = turn.pivot.map((v, i) => v + d[i] * radius);
|
|
192
|
+
const { min, max } = turn.control;
|
|
193
|
+
if (!(max > min))
|
|
194
|
+
return null;
|
|
195
|
+
const steps = Math.max(2, Math.ceil((max - min) / STEP_DEG));
|
|
196
|
+
const pivot = apply(carrierNow, turn.pivot);
|
|
197
|
+
const traced = [];
|
|
198
|
+
try {
|
|
199
|
+
for (let i = 0; i <= steps; i++) {
|
|
200
|
+
const s = min + ((max - min) * i) / steps;
|
|
201
|
+
traced.push(apply(at({ [turn.control.id]: s })[carrierRef], edge));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/* The fan's own frame: x toward the first traced point, y along the axis as it stands now, z completing them. */
|
|
208
|
+
const axisNow = norm([0, 1, 2].map(i => dot(carrierNow.r[i], turn.axis)));
|
|
209
|
+
const first = norm(sub(traced[0], pivot));
|
|
210
|
+
if (!axisNow || !first)
|
|
211
|
+
return null;
|
|
212
|
+
const zAxis = cross(first, axisNow);
|
|
213
|
+
const r = [0, 1, 2].map(i => [first[i], axisNow[i], zAxis[i]]);
|
|
214
|
+
const local = (p) => {
|
|
215
|
+
const v = sub(p, pivot);
|
|
216
|
+
return { x: dot(v, first), y: dot(v, zAxis) };
|
|
217
|
+
};
|
|
218
|
+
const full = max - min >= 359.999;
|
|
219
|
+
const profile = [...(full ? [] : [{ x: 0, y: 0 }]), ...traced.map(local)].map(p => ({ x: Math.round(p.x * 1000) / 1000, y: Math.round(p.y * 1000) / 1000 }));
|
|
220
|
+
return {
|
|
221
|
+
id: JOINT_FAN,
|
|
222
|
+
shape: { provider: 'polygon', dimensions: { height: THICKNESS }, profile },
|
|
223
|
+
pose: { t: pivot, r },
|
|
224
|
+
material: { token: '__joint-fan', transparent: true },
|
|
225
|
+
/** Which control turns it: the joint the fan shows. */
|
|
226
|
+
turns: turn.control.id
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** The values in hand that belong to a record's keys (design inputs, or state defaults). */
|
|
230
|
+
function pick(values, of) {
|
|
231
|
+
return Object.fromEntries(Object.entries(values).filter(([id]) => id in of));
|
|
232
|
+
}
|
|
233
|
+
/** A part's largest own measure (a box side, a diameter, a length) at the inputs given; 0 when it cannot be read. */
|
|
234
|
+
function largestMeasureOf(asset, part, inputs) {
|
|
235
|
+
try {
|
|
236
|
+
const body = compileV3Asset({ ...asset, designInputs: pick(inputs, asset.designInputs), stateDefaults: pick(inputs, asset.stateDefaults) })
|
|
237
|
+
.evaluate()
|
|
238
|
+
.geometry.find(g => g.id === part);
|
|
239
|
+
const dims = Object.entries(body?.shape?.dimensions ?? {}).map(([k, v]) => (k.startsWith('radius') ? v * 2 : v));
|
|
240
|
+
return dims.length ? Math.max(...dims) : 0;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Whether the line through `at` along `along` passes through the box (centre, half-measures), within the skin. */
|
|
247
|
+
function lineMeetsBox(at, along, centre, half) {
|
|
248
|
+
let lo = -Infinity, hi = Infinity;
|
|
249
|
+
for (let i = 0; i < 3; i++) {
|
|
250
|
+
const min = centre[i] - half[i] - SEAM, max = centre[i] + half[i] + SEAM;
|
|
251
|
+
if (Math.abs(along[i]) < 1e-12) {
|
|
252
|
+
if (at[i] < min || at[i] > max)
|
|
253
|
+
return false;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const t1 = (min - at[i]) / along[i], t2 = (max - at[i]) / along[i];
|
|
257
|
+
lo = Math.max(lo, Math.min(t1, t2));
|
|
258
|
+
hi = Math.min(hi, Math.max(t1, t2));
|
|
259
|
+
if (lo > hi)
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=v3-joint-fan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-joint-fan.js","sourceRoot":"","sources":["../../client/modeller/v3-joint-fan.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileV3Asset, compileV3Graph, v3WorldBoxesOf, v3WritersOf } from '@hatiolab/figure-model'\nimport type { V3Asset } from '@hatiolab/figure-model'\n\nimport type { V3RenderPart } from './v3-renderer.js'\n\n/*\n * Where the selected part turns, and how far -- a translucent fan on the editing board (V2's joint gizmo, inventory\n * 3.38). An axis name says nothing about which way a part swings; on a six-axis arm the author went to the preview and\n * pushed each slider to its end to find out. The fan lies in the plane the part turns in, at the pivot, open from the\n * control's lowest value to its highest; the edge at the value the figure holds now is where the part is.\n *\n * It is found by evaluating, not by reading the graph's shape: the frame that carries the part is sampled across the\n * control's range, and a point at a fixed distance from the pivot traces the fan. So the direction is right whatever\n * the slope of the control, and a joint inside other joints is drawn where its parents put it.\n *\n * Two ways a part turns are read:\n * - a motion made in the modeller (`add-motion`): `<part>.motion`, the frame it moves is its seat, the pivot the\n * seat's origin;\n * - a joint the converter wrote from V2 (`<joint>.motion` · `<joint>.chain`): the part is composed on the joint's\n * chain, the pivot is the joint's origin (`<joint>.frameOnParent`).\n * A slide has no fan. Nothing is drawn when the turn cannot be found -- a fan in some guessed plane would point the\n * wrong way.\n */\n\n/** The fan's name on the board. It is not a part: not in the list, not saved, not picked. */\nexport const JOINT_FAN = '__joint-fan'\n\n/** How thick the fan is: thin enough not to hide the part, thick enough to be seen edge-on. */\nconst THICKNESS = 6\n/** About one segment every six degrees, so a full turn reads as a circle. */\nconst STEP_DEG = 6\n\ntype Pose = { r: number[][]; t: number[] }\ninterface Node {\n id: string\n op: string\n args: string[]\n outputs: Record<string, string>\n params?: Record<string, string>\n}\n\nconst apply = (p: Pose, v: number[]) => [0, 1, 2].map(i => p.r[i]![0]! * v[0]! + p.r[i]![1]! * v[1]! + p.r[i]![2]! * v[2]! + p.t[i]!)\nconst sub = (a: number[], b: number[]) => a.map((v, i) => v - b[i]!)\nconst dot = (a: number[], b: number[]) => a.reduce((s, v, i) => s + v * b[i]!, 0)\nconst cross = (a: number[], b: number[]) => [a[1]! * b[2]! - a[2]! * b[1]!, a[2]! * b[0]! - a[0]! * b[2]!, a[0]! * b[1]! - a[1]! * b[0]!]\nconst norm = (a: number[]) => {\n const l = Math.hypot(...a)\n return l > 1e-9 ? a.map(v => v / l) : null\n}\n\n/** How far outside a part's box a pivot may lie and still be on the part: the kernel's contact skin. */\nconst SEAM = 1\n/** An origin this deep in a part's box (a share of its half-measure, on every axis) is housed by the part. */\nconst HOUSED = 0.5\nconst AXIS_NAMES = ['x', 'y', 'z'] as const\ntype WorldBox = { centre: Record<'x' | 'y' | 'z', number>; extent: Record<'x' | 'y' | 'z', number> }\n\ninterface Turn {\n /** The node whose value carries the moving frame into the figure's frame. */\n carrier: string\n /** The pivot, in the moving frame. */\n pivot: number[]\n /** The axis, in the moving frame. */\n axis: number[]\n /** Where the part sits, in the moving frame -- which way the fan's reference edge points. */\n toward: number[]\n /** The part the joint turns -- the fan is sized from it, so a joint shows one fan whichever part is selected. */\n turnedPart: string\n /** The state input that turns it. */\n control: { id: string; min: number; max: number }\n}\n\nfunction turnOf(asset: V3Asset, part: string, values: Record<string, unknown>, box: WorldBox | undefined): Turn | null {\n const m = asset.document.model as unknown as { nodes: Node[]; constants: { id: string; value: number }[]; inputs: { id: string; role?: string; min?: number; max?: number }[] }\n const node = (id: string) => m.nodes.find(n => n.id === id)\n const constant = (ref: string) => m.constants.find(c => c.id === ref)?.value\n const valueOf = (n: Node | undefined) => (n ? values[Object.values(n.outputs)[0]!] : undefined) as Pose | undefined\n const controlOf = (ref: string | undefined, seen = new Set<string>()): Turn['control'] | null => {\n if (!ref || seen.has(ref)) return null\n seen.add(ref)\n const input = m.inputs.find(i => i.id === ref && i.role === 'state')\n if (input) return { id: input.id, min: input.min ?? 0, max: input.max ?? 0 }\n const writer = m.nodes.find(n => Object.values(n.outputs).includes(ref))\n for (const arg of writer?.args ?? []) {\n const found = controlOf(arg, seen)\n if (found) return found\n }\n return null\n }\n const axisOf = (motion: Node) => {\n const axis = motion.args.slice(0, 3).map(constant)\n return axis.every(v => typeof v === 'number') ? norm(axis as number[]) : null\n }\n\n /* Made in the modeller: the part's own motion turns its seat about the seat's origin. */\n const own = node(`${part}.motion`)\n if (own) {\n if (own.op !== 'axis-turn@1') return null\n const axis = axisOf(own)\n const control = controlOf(own.args[3])\n const at = valueOf(node(`${part}.pose`))\n if (!axis || !control || !at) return null\n return { carrier: own.id, pivot: [0, 0, 0], axis, toward: at.t, control, turnedPart: part }\n }\n\n /*\n Written by the converter: a joint (`<joint>.motion`, its origin `<joint>.frameOnParent`) carries its own part and\n everything fastened below it, through its chain (the first joint of a chain has only its attachment). A part shows\n the joint it is to that part of the mechanism (user, 2026-09-24: 「elbow-housing itself is a axis」 · 「please check\n all that logic and concept」), tested in this order on the joints as they stand now:\n 1. housing -- a joint's origin lies deep inside the part, which is centred on it: the shoulder housing shows the\n shoulder and the elbow housing the elbow, though another joint carries them;\n 2. turned -- the joint that carries the part, when its axis passes through the part: through its middle for a\n body that rolls on it (the wrist roll, the tool flange, the gripper body), through its root for a link (the\n upper arm, the forearm);\n 3. otherwise none: the pedestal (the base's axis runs through it but turns it not), the fingers.\n An axis passing through a part is not enough by itself: the wrist roll's axis runs the forearm's length and the\n tool roll's through the wrist pitch, and neither turns them (an earlier rule said so, and was wrong).\n */\n if (!box) return null\n const carrierOf = (joint: string) => (node(`${joint}.chain`) ? `${joint}.chain` : `${joint}.attach`)\n const mine = node(`${part}.moved`)?.args.find(arg => /\\.(chain|attach)\\.value$/.test(arg))?.replace(/\\.value$/, '')\n const centre = AXIS_NAMES.map(a => box.centre[a])\n const half = AXIS_NAMES.map(a => box.extent[a] / 2)\n const joints = m.nodes\n .filter(n => n.op === 'axis-turn@1' && n.id.endsWith('.motion') && node(n.id.replace(/\\.motion$/, '.frameOnParent')))\n .map(motion => {\n const joint = motion.id.replace(/\\.motion$/, '')\n const carrier = carrierOf(joint)\n const frame = valueOf(node(carrier))\n const origin = valueOf(node(`${joint}.frameOnParent`))\n const local = axisOf(motion)\n if (!frame || !origin || !local) return null\n const at = apply(frame, origin.t)\n const along = norm([0, 1, 2].map(i => dot(frame.r[i]!, local)))\n if (!along) return null\n const off = sub(centre, at)\n return {\n joint,\n motion,\n carrier,\n origin,\n /* How deep the origin lies: 0 at the centre, 1 on the box's face, on the axis where it is shallowest. */\n depth: Math.max(...[0, 1, 2].map(i => Math.abs(off[i]!) / (half[i]! + SEAM))),\n fromCentre: Math.hypot(...off),\n through: lineMeetsBox(at, along, centre, half)\n }\n })\n .filter((c): c is NonNullable<typeof c> => !!c)\n const housing = joints.filter(c => c.depth <= HOUSED).sort((a, b) => a.fromCentre - b.fromCentre)[0]\n const carrying = joints.find(c => c.carrier === mine)\n const chosen = housing ?? (carrying && carrying.through ? carrying : undefined)\n if (!chosen) return null\n const axis = axisOf(chosen.motion)\n const control = controlOf(chosen.motion.args[3])\n if (!axis || !control) return null\n /* The reference edge points at what the joint turns: of the parts it carries, the one farthest off its axis. */\n const flatOff = (t: number[]) => {\n const rel = sub(t, chosen.origin.t)\n return Math.hypot(...sub(rel, axis.map(v => v * dot(rel, axis))))\n }\n const turned = m.nodes\n .filter(n => n.id.endsWith('.moved') && n.args.includes(`${chosen.carrier}.value`))\n .map(n => ({ id: n.id.replace(/\\.moved$/, ''), t: valueOf(node(n.id.replace(/\\.moved$/, '.pose')))?.t }))\n .filter((one): one is { id: string; t: number[] } => !!one.t)\n .sort((a, b) => flatOff(b.t) - flatOff(a.t))[0]\n return { carrier: chosen.carrier, pivot: chosen.origin.t, axis, toward: turned?.t ?? chosen.origin.t, control, turnedPart: turned?.id ?? part }\n}\n\n/**\n * The fan for the part, as a body to stand on the board, or null when the part does not turn. `values` are the\n * inputs the figure is drawn with now (a pose in hand included), so the fan follows the arm as it is posed.\n */\nexport function jointFanOf(asset: V3Asset, part: string, values: Record<string, number> = {}): V3RenderPart | null {\n if (!part) return null\n let graph: ReturnType<typeof compileV3Graph>\n try {\n graph = compileV3Graph(asset.document.model as never)\n } catch {\n return null\n }\n const inputs = { ...asset.designInputs, ...asset.stateDefaults, ...values }\n const at = (over: Record<string, number>) => graph.evaluate({ ...inputs, ...over }).values as Record<string, unknown>\n let now: Record<string, unknown>\n try {\n now = at({})\n } catch {\n return null\n }\n let box: WorldBox | undefined\n try {\n box = v3WorldBoxesOf(\n compileV3Asset({ ...asset, designInputs: { ...asset.designInputs, ...pick(values, asset.designInputs) }, stateDefaults: { ...asset.stateDefaults, ...pick(values, asset.stateDefaults) } })\n .evaluate()\n .geometry.filter(g => g.id === part),\n v3WritersOf(asset)\n )[0]\n } catch {\n box = undefined\n }\n const turn = turnOf(asset, part, now, box)\n if (!turn) return null\n const carrierRef = Object.values(asset.document.model.nodes.find(n => n.id === turn.carrier)!.outputs)[0]!\n const carrierNow = now[carrierRef] as Pose | undefined\n if (!carrierNow) return null\n\n /* The reference edge points from the pivot toward the part, in the plane it turns in; any perpendicular if it sits on the axis. */\n const rel = sub(turn.toward, turn.pivot)\n const inPlane = sub(rel, turn.axis.map(v => v * dot(rel, turn.axis)))\n const d = norm(inPlane) ?? norm(cross(turn.axis, Math.abs(turn.axis[0]!) < 0.9 ? [1, 0, 0] : [0, 1, 0]))!\n /*\n Sized from what the joint turns, as V2 sized it from the joint's child: selecting the elbow housing or the forearm\n shows the elbow's one fan, not a smaller one for the smaller part (user, 2026-09-24).\n */\n const radius = Math.max(60, Math.round(Math.max(largestMeasureOf(asset, turn.turnedPart, inputs), Math.hypot(...inPlane)) * 0.6))\n const edge = turn.pivot.map((v, i) => v + d[i]! * radius)\n\n const { min, max } = turn.control\n if (!(max > min)) return null\n const steps = Math.max(2, Math.ceil((max - min) / STEP_DEG))\n const pivot = apply(carrierNow, turn.pivot)\n const traced: number[][] = []\n try {\n for (let i = 0; i <= steps; i++) {\n const s = min + ((max - min) * i) / steps\n traced.push(apply(at({ [turn.control.id]: s })[carrierRef] as Pose, edge))\n }\n } catch {\n return null\n }\n\n /* The fan's own frame: x toward the first traced point, y along the axis as it stands now, z completing them. */\n const axisNow = norm([0, 1, 2].map(i => dot(carrierNow.r[i]!, turn.axis)))\n const first = norm(sub(traced[0]!, pivot))\n if (!axisNow || !first) return null\n const zAxis = cross(first, axisNow)\n const r = [0, 1, 2].map(i => [first[i]!, axisNow[i]!, zAxis[i]!])\n const local = (p: number[]) => {\n const v = sub(p, pivot)\n return { x: dot(v, first), y: dot(v, zAxis) }\n }\n const full = max - min >= 359.999\n const profile = [...(full ? [] : [{ x: 0, y: 0 }]), ...traced.map(local)].map(p => ({ x: Math.round(p.x * 1000) / 1000, y: Math.round(p.y * 1000) / 1000 }))\n\n return {\n id: JOINT_FAN,\n shape: { provider: 'polygon', dimensions: { height: THICKNESS }, profile },\n pose: { t: pivot, r },\n material: { token: '__joint-fan', transparent: true },\n /** Which control turns it: the joint the fan shows. */\n turns: turn.control.id\n } as unknown as V3RenderPart\n}\n\n/** The values in hand that belong to a record's keys (design inputs, or state defaults). */\nfunction pick(values: Record<string, number>, of: Record<string, number>): Record<string, number> {\n return Object.fromEntries(Object.entries(values).filter(([id]) => id in of))\n}\n\n/** A part's largest own measure (a box side, a diameter, a length) at the inputs given; 0 when it cannot be read. */\nfunction largestMeasureOf(asset: V3Asset, part: string, inputs: Record<string, number>): number {\n try {\n const body = compileV3Asset({ ...asset, designInputs: pick(inputs, asset.designInputs), stateDefaults: pick(inputs, asset.stateDefaults) })\n .evaluate()\n .geometry.find(g => g.id === part) as { shape?: { dimensions?: Record<string, number> } } | undefined\n const dims = Object.entries(body?.shape?.dimensions ?? {}).map(([k, v]) => (k.startsWith('radius') ? v * 2 : v))\n return dims.length ? Math.max(...dims) : 0\n } catch {\n return 0\n }\n}\n\n/** Whether the line through `at` along `along` passes through the box (centre, half-measures), within the skin. */\nfunction lineMeetsBox(at: number[], along: number[], centre: number[], half: number[]): boolean {\n let lo = -Infinity,\n hi = Infinity\n for (let i = 0; i < 3; i++) {\n const min = centre[i]! - half[i]! - SEAM,\n max = centre[i]! + half[i]! + SEAM\n if (Math.abs(along[i]!) < 1e-12) {\n if (at[i]! < min || at[i]! > max) return false\n continue\n }\n const t1 = (min - at[i]!) / along[i]!,\n t2 = (max - at[i]!) / along[i]!\n lo = Math.max(lo, Math.min(t1, t2))\n hi = Math.min(hi, Math.max(t1, t2))\n if (lo > hi) return false\n }\n return true\n}\n\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect';
|
|
2
|
+
export interface KeyLike {
|
|
3
|
+
key: string;
|
|
4
|
+
ctrlKey?: boolean;
|
|
5
|
+
metaKey?: boolean;
|
|
6
|
+
shiftKey?: boolean;
|
|
7
|
+
altKey?: boolean;
|
|
8
|
+
/** The element the key was pressed in -- the first of the event's composed path. */
|
|
9
|
+
target?: {
|
|
10
|
+
tagName?: string;
|
|
11
|
+
isContentEditable?: boolean;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function keyActionOf(e: KeyLike, selected: string): KeyAction | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const TYPING = new Set(['INPUT', 'SELECT', 'TEXTAREA']);
|
|
2
|
+
export function keyActionOf(e, selected) {
|
|
3
|
+
if (e.target && (TYPING.has(String(e.target.tagName).toUpperCase()) || e.target.isContentEditable))
|
|
4
|
+
return null;
|
|
5
|
+
if (e.altKey)
|
|
6
|
+
return null;
|
|
7
|
+
const mod = !!(e.ctrlKey || e.metaKey);
|
|
8
|
+
const key = e.key.toLowerCase();
|
|
9
|
+
if (mod && key === 'z')
|
|
10
|
+
return e.shiftKey ? 'redo' : 'undo';
|
|
11
|
+
if (mod && key === 'y')
|
|
12
|
+
return 'redo';
|
|
13
|
+
if (!selected)
|
|
14
|
+
return null;
|
|
15
|
+
if (mod && key === 'd')
|
|
16
|
+
return 'duplicate';
|
|
17
|
+
if (!mod && (key === 'delete' || key === 'backspace'))
|
|
18
|
+
return 'remove';
|
|
19
|
+
if (!mod && key === 'escape')
|
|
20
|
+
return 'deselect';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=v3-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-keys.js","sourceRoot":"","sources":["../../client/modeller/v3-keys.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;AAEvD,MAAM,UAAU,WAAW,CAAC,CAAU,EAAE,QAAgB;IACtD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/G,IAAI,CAAC,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAA;IAC/B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAC3D,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,MAAM,CAAA;IACrC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC1B,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,WAAW,CAAA;IAC1C,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,WAAW,CAAC;QAAE,OAAO,QAAQ,CAAA;IACtE,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAA;IAC/C,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["/*\n * The modeller's keys (V2 had undo and redo on Ctrl+Z / Ctrl+Y; these add what a person reaches for next). Read apart\n * from the element so the rule is tested without a browser.\n *\n * Typing is never taken: a key pressed in a field belongs to the field -- Backspace in a name box deletes a letter,\n * not the part.\n */\nexport type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect'\n\nexport interface KeyLike {\n key: string\n ctrlKey?: boolean\n metaKey?: boolean\n shiftKey?: boolean\n altKey?: boolean\n /** The element the key was pressed in -- the first of the event's composed path. */\n target?: { tagName?: string; isContentEditable?: boolean } | null\n}\n\nconst TYPING = new Set(['INPUT', 'SELECT', 'TEXTAREA'])\n\nexport function keyActionOf(e: KeyLike, selected: string): KeyAction | null {\n if (e.target && (TYPING.has(String(e.target.tagName).toUpperCase()) || e.target.isContentEditable)) return null\n if (e.altKey) return null\n const mod = !!(e.ctrlKey || e.metaKey)\n const key = e.key.toLowerCase()\n if (mod && key === 'z') return e.shiftKey ? 'redo' : 'undo'\n if (mod && key === 'y') return 'redo'\n if (!selected) return null\n if (mod && key === 'd') return 'duplicate'\n if (!mod && (key === 'delete' || key === 'backspace')) return 'remove'\n if (!mod && key === 'escape') return 'deselect'\n return null\n}\n"]}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import type { V3SizeRange } from '@hatiolab/figure-model';
|
|
1
2
|
import type { V3Asset, V3BindingPlace, V3Face } from '@hatiolab/figure-model';
|
|
2
3
|
export type Axis = 'x' | 'y' | 'z';
|
|
3
4
|
export declare const AXES: readonly Axis[];
|
|
4
5
|
/** What one dimension of a part is worked out from. */
|
|
5
6
|
export type Follow = {
|
|
6
7
|
of: 'own';
|
|
7
|
-
}
|
|
8
|
+
}
|
|
9
|
+
/** `times` 1 is the instance size itself; any other is a share of it -- V2's proportional rule. */
|
|
10
|
+
| {
|
|
8
11
|
of: 'instance';
|
|
9
12
|
axis: Axis;
|
|
13
|
+
times: number;
|
|
10
14
|
} | {
|
|
11
15
|
of: 'part';
|
|
12
16
|
part: string;
|
|
@@ -31,6 +35,14 @@ export declare const hasInstanceSize: (asset: V3Asset, axis: Axis) => boolean;
|
|
|
31
35
|
export declare function fastenedTo(asset: V3Asset, part: string): string | null;
|
|
32
36
|
/** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
|
|
33
37
|
export declare function canFollow(asset: V3Asset, part: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The axes whose size can follow something, by the shape's own measures: a box's X · Y · Z; a cylinder's radius (read
|
|
40
|
+
* as X) and length (Y) -- its shape takes those two, so it has no Z of its own (V2's sizing cards had the cylinder's
|
|
41
|
+
* two as well).
|
|
42
|
+
*/
|
|
43
|
+
export declare function followableAxes(asset: V3Asset, part: string): Axis[];
|
|
44
|
+
/** What a part's own measure on an axis is now (a box's side, a cylinder's radius or length), at the design as it stands. */
|
|
45
|
+
export declare function dimensionNow(asset: V3Asset, part: string, axis: Axis): number | undefined;
|
|
34
46
|
/** Whether `attach` can take this part: a box, or a cylinder (figure-model measures its faces since 0.1.47). */
|
|
35
47
|
export declare function canFasten(asset: V3Asset, part: string): boolean;
|
|
36
48
|
/**
|
|
@@ -38,6 +50,11 @@ export declare function canFasten(asset: V3Asset, part: string): boolean;
|
|
|
38
50
|
* converter writes V2's anchor rules that way. `fix-position` makes one its own number where it is now.
|
|
39
51
|
*/
|
|
40
52
|
export declare function positionsWorkedOut(asset: V3Asset, part: string): Axis[];
|
|
53
|
+
/**
|
|
54
|
+
* Whether the part's position on an axis is a share of the instance size (`scale-position`): `size.<axis> × share`.
|
|
55
|
+
* Such a position is worked out too, but it already grows with the figure, so it is not offered again.
|
|
56
|
+
*/
|
|
57
|
+
export declare function positionIsShare(asset: V3Asset, part: string, axis: Axis): boolean;
|
|
41
58
|
/** How a part moves, if it does: slide or turn, along which axis, driven by which control. */
|
|
42
59
|
export interface Motion {
|
|
43
60
|
kind: 'slide' | 'turn';
|
|
@@ -65,3 +82,20 @@ export declare function placeIdFor(asset: V3Asset, part: string, role: 'seat' |
|
|
|
65
82
|
export declare const holdsThings: (asset: V3Asset) => boolean;
|
|
66
83
|
/** What `accepts` a person typed: names split on commas, trimmed, blanks and repeats dropped. */
|
|
67
84
|
export declare const acceptsOf: (typed: string) => string[];
|
|
85
|
+
/**
|
|
86
|
+
* The sizes a board may give this figure: the range it declares now, and the range measured afresh -- the default
|
|
87
|
+
* (1 mm to ten times the authored size) narrowed only where the figure cannot follow, by the converter's own functions.
|
|
88
|
+
* A range measured when a part filled what the others left (ROBOT_ARM: Y from 1,854 mm) stays narrow after the part is
|
|
89
|
+
* made to grow with the figure; measuring again gives the figure its whole range back. Nothing when no size is declared.
|
|
90
|
+
*/
|
|
91
|
+
export declare function instanceRangeOf(asset: V3Asset): {
|
|
92
|
+
declared: V3SizeRange;
|
|
93
|
+
measured: V3SizeRange;
|
|
94
|
+
} | undefined;
|
|
95
|
+
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
96
|
+
export declare function rangeDiffers(a: V3SizeRange, b: V3SizeRange): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
|
|
99
|
+
* architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
|
|
100
|
+
*/
|
|
101
|
+
export declare function turnsAboutAJoint(asset: V3Asset): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v3AttachmentsOf, v3BindingsOf } from '@hatiolab/figure-model';
|
|
1
|
+
import { compileV3Graph, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model';
|
|
2
2
|
export const AXES = ['x', 'y', 'z'];
|
|
3
3
|
const modelOf = (asset) => asset.document.model;
|
|
4
4
|
/**
|
|
@@ -8,18 +8,26 @@ const modelOf = (asset) => asset.document.model;
|
|
|
8
8
|
*/
|
|
9
9
|
export function followOf(asset, part, axis) {
|
|
10
10
|
const m = modelOf(asset);
|
|
11
|
+
/*
|
|
12
|
+
A link routes the dimension through `<part>.dimension.<axis>`; before any link the shape reads its measure directly,
|
|
13
|
+
and that may already be worked out -- the converter writes V2's rules there (ROBOT_ARM's flange filled what was left).
|
|
14
|
+
*/
|
|
11
15
|
const node = m.nodes.find(n => n.id === `${part}.dimension.${axis}`);
|
|
12
|
-
|
|
16
|
+
const start = node ? node.args[0] : m.nodes.find(n => n.id === `${part}.shape`)?.args[AXES.indexOf(axis)];
|
|
17
|
+
if (!start || m.constants.some(c => c.id === start))
|
|
13
18
|
return { of: 'own' };
|
|
14
19
|
const writerOf = (ref) => m.nodes.find(n => Object.values(n.outputs).includes(ref));
|
|
15
20
|
const seen = new Set();
|
|
21
|
+
const constant = (ref) => m.constants.find(c => c.id === ref);
|
|
22
|
+
/* The constant factors met on the way back: a share of the instance size is `size × share`. */
|
|
23
|
+
let times = 1;
|
|
16
24
|
const walk = (ref) => {
|
|
17
25
|
if (seen.has(ref))
|
|
18
26
|
return { of: 'expression' };
|
|
19
27
|
seen.add(ref);
|
|
20
28
|
const instance = /^size\.([xyz])$/.exec(ref);
|
|
21
29
|
if (instance)
|
|
22
|
-
return { of: 'instance', axis: instance[1] };
|
|
30
|
+
return { of: 'instance', axis: instance[1], times: Math.round(times * 1e6) / 1e6 };
|
|
23
31
|
const other = /^(.+)\.dimension\.([xyz])\.value$/.exec(ref);
|
|
24
32
|
if (other)
|
|
25
33
|
return { of: 'part', part: other[1], axis: other[2] };
|
|
@@ -30,10 +38,23 @@ export function followOf(asset, part, axis) {
|
|
|
30
38
|
if (!writer)
|
|
31
39
|
return { of: 'expression' };
|
|
32
40
|
/* Follow the one argument that is not a constant factor. */
|
|
33
|
-
const onward = writer.args.filter(arg => !
|
|
34
|
-
|
|
41
|
+
const onward = writer.args.filter(arg => !constant(arg));
|
|
42
|
+
if (onward.length !== 1)
|
|
43
|
+
return { of: 'expression' };
|
|
44
|
+
/*
|
|
45
|
+
Only a product is a share of its source. A sum with a number is not (V2's 「fill what is left」 is the size plus a
|
|
46
|
+
negative gap -- ROBOT_ARM's tool flange), nor anything else; those read as worked out some other way.
|
|
47
|
+
*/
|
|
48
|
+
const factors = writer.args.filter(arg => constant(arg)).map(arg => constant(arg).value);
|
|
49
|
+
if (writer.op === 'mul@1')
|
|
50
|
+
times *= factors.reduce((a, b) => a * b, 1);
|
|
51
|
+
else if (writer.op === 'div@1' && writer.args[0] === onward[0] && factors.length === 1 && factors[0] !== 0)
|
|
52
|
+
times /= factors[0];
|
|
53
|
+
else if (factors.some(v => v !== 0) || !['mul@1', 'div@1', 'add@1', 'sub@1'].includes(writer.op))
|
|
54
|
+
return { of: 'expression' };
|
|
55
|
+
return walk(onward[0]);
|
|
35
56
|
};
|
|
36
|
-
return walk(
|
|
57
|
+
return walk(start);
|
|
37
58
|
}
|
|
38
59
|
/** Whether the asset declares an instance size on an axis — only then can a dimension follow it. */
|
|
39
60
|
export const hasInstanceSize = (asset, axis) => modelOf(asset).inputs.some(i => i.id === `size.${axis}` && i.role === 'design');
|
|
@@ -43,8 +64,37 @@ export function fastenedTo(asset, part) {
|
|
|
43
64
|
}
|
|
44
65
|
/** Whether a dimension of this part can follow something — box-shaped, whose three sizes are X, Y and Z. */
|
|
45
66
|
export function canFollow(asset, part) {
|
|
67
|
+
return followableAxes(asset, part).length > 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The axes whose size can follow something, by the shape's own measures: a box's X · Y · Z; a cylinder's radius (read
|
|
71
|
+
* as X) and length (Y) -- its shape takes those two, so it has no Z of its own (V2's sizing cards had the cylinder's
|
|
72
|
+
* two as well).
|
|
73
|
+
*/
|
|
74
|
+
export function followableAxes(asset, part) {
|
|
46
75
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`);
|
|
47
|
-
|
|
76
|
+
if (!shape)
|
|
77
|
+
return [];
|
|
78
|
+
if (shape.op === 'box-shape@1' || shape.op === 'rounded-box@1')
|
|
79
|
+
return [...AXES];
|
|
80
|
+
if (shape.op === 'cylinder-shape@1')
|
|
81
|
+
return ['x', 'y'];
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
/** What a part's own measure on an axis is now (a box's side, a cylinder's radius or length), at the design as it stands. */
|
|
85
|
+
export function dimensionNow(asset, part, axis) {
|
|
86
|
+
const m = modelOf(asset);
|
|
87
|
+
const ref = m.nodes.find(n => n.id === `${part}.shape`)?.args[AXES.indexOf(axis)];
|
|
88
|
+
if (!ref)
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const values = compileV3Graph(asset.document.model).evaluate({ ...asset.designInputs, ...asset.stateDefaults }).values;
|
|
92
|
+
const value = values[ref];
|
|
93
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
48
98
|
}
|
|
49
99
|
/** Whether `attach` can take this part: a box, or a cylinder (figure-model measures its faces since 0.1.47). */
|
|
50
100
|
export function canFasten(asset, part) {
|
|
@@ -64,6 +114,16 @@ export function positionsWorkedOut(asset, part) {
|
|
|
64
114
|
const constant = (ref) => m.constants.some(c => c.id === ref);
|
|
65
115
|
return AXES.filter((_axis, i) => !own(pose.args[i]) && !constant(pose.args[i]));
|
|
66
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Whether the part's position on an axis is a share of the instance size (`scale-position`): `size.<axis> × share`.
|
|
119
|
+
* Such a position is worked out too, but it already grows with the figure, so it is not offered again.
|
|
120
|
+
*/
|
|
121
|
+
export function positionIsShare(asset, part, axis) {
|
|
122
|
+
const m = modelOf(asset);
|
|
123
|
+
const ref = m.nodes.find(n => n.id === `${part}.pose`)?.args[AXES.indexOf(axis)];
|
|
124
|
+
const writer = ref ? m.nodes.find(n => Object.values(n.outputs).includes(ref)) : undefined;
|
|
125
|
+
return !!writer && writer.op === 'mul@1' && writer.args[0] === `size.${axis}` && m.constants.some(c => c.id === writer.args[1]);
|
|
126
|
+
}
|
|
67
127
|
export function motionOf(asset, part) {
|
|
68
128
|
const m = modelOf(asset);
|
|
69
129
|
const node = m.nodes.find(n => n.id === `${part}.motion`);
|
|
@@ -98,7 +158,7 @@ export function choiceOf(follow) {
|
|
|
98
158
|
case 'own':
|
|
99
159
|
return 'own';
|
|
100
160
|
case 'instance':
|
|
101
|
-
return `instance:${follow.axis}`;
|
|
161
|
+
return follow.times === 1 ? `instance:${follow.axis}` : `share:${follow.axis}`;
|
|
102
162
|
case 'part':
|
|
103
163
|
return `part:${follow.part}:${follow.axis}`;
|
|
104
164
|
case 'input':
|
|
@@ -138,4 +198,37 @@ export function placeIdFor(asset, part, role) {
|
|
|
138
198
|
export const holdsThings = (asset) => (asset.document.capabilities.names ?? []).some(n => n === 'Capacity' || n === 'FlowNode');
|
|
139
199
|
/** What `accepts` a person typed: names split on commas, trimmed, blanks and repeats dropped. */
|
|
140
200
|
export const acceptsOf = (typed) => [...new Set(typed.split(',').map(one => one.trim()).filter(Boolean))];
|
|
201
|
+
/**
|
|
202
|
+
* The sizes a board may give this figure: the range it declares now, and the range measured afresh -- the default
|
|
203
|
+
* (1 mm to ten times the authored size) narrowed only where the figure cannot follow, by the converter's own functions.
|
|
204
|
+
* A range measured when a part filled what the others left (ROBOT_ARM: Y from 1,854 mm) stays narrow after the part is
|
|
205
|
+
* made to grow with the figure; measuring again gives the figure its whole range back. Nothing when no size is declared.
|
|
206
|
+
*/
|
|
207
|
+
export function instanceRangeOf(asset) {
|
|
208
|
+
const inputs = modelOf(asset).inputs;
|
|
209
|
+
const declared = {};
|
|
210
|
+
const authored = {};
|
|
211
|
+
for (const axis of AXES) {
|
|
212
|
+
const input = inputs.find(i => i.id === `size.${axis}` && i.role === 'design');
|
|
213
|
+
const at = asset.designInputs[`size.${axis}`];
|
|
214
|
+
if (!input || typeof at !== 'number')
|
|
215
|
+
return undefined;
|
|
216
|
+
declared[axis] = { min: input.min ?? 0, max: input.max ?? Infinity };
|
|
217
|
+
authored[axis] = at;
|
|
218
|
+
}
|
|
219
|
+
const start = defaultInstanceSizeRange(authored);
|
|
220
|
+
const measured = foldInstanceSizeRange(start, v3InstanceSizeLimitsOf(asset, start)).range;
|
|
221
|
+
return { declared, measured };
|
|
222
|
+
}
|
|
223
|
+
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
224
|
+
export function rangeDiffers(a, b) {
|
|
225
|
+
return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
|
|
229
|
+
* architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
|
|
230
|
+
*/
|
|
231
|
+
export function turnsAboutAJoint(asset) {
|
|
232
|
+
return modelOf(asset).nodes.some(n => n.op === 'axis-turn@1');
|
|
233
|
+
}
|
|
141
234
|
//# sourceMappingURL=v3-links.js.map
|