partforge 0.41.0 → 0.44.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/README.md +31 -10
- package/bin/cli.js +100 -27
- package/docs/AUTHORING-PARTS.md +126 -14
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +48 -7
- package/skills/partforge/SKILL.md +17 -3
- package/src/app-embed-test.js +1 -1
- package/src/app-hinged-box.js +12 -0
- package/src/framework/animation-controls.js +243 -0
- package/src/framework/animation.js +217 -0
- package/src/framework/app.css +32 -0
- package/src/framework/assembly.js +1 -1
- package/src/framework/backend-select.js +25 -0
- package/src/framework/camera-tween.js +58 -0
- package/src/framework/chrome.css +16 -0
- package/src/framework/controls.js +13 -3
- package/src/framework/cutaway-gizmo-scene.js +244 -0
- package/src/framework/cutaway-gizmo.js +80 -243
- package/src/framework/default-view.js +46 -0
- package/src/framework/download.js +7 -2
- package/src/framework/export-controller.js +13 -2
- package/src/framework/geometry/probe.js +3 -22
- package/src/framework/jobs.js +9 -40
- package/src/framework/lint/finding.js +4 -0
- package/src/framework/lint/index.js +7 -3
- package/src/framework/lint/rules-animations.js +404 -0
- package/src/framework/lint/rules-place.js +76 -0
- package/src/framework/lint/rules-shape.js +12 -0
- package/src/framework/lint/rules-verify.js +2 -2
- package/src/framework/mount.js +93 -18
- package/src/{testing → framework/oracle}/build.js +1 -1
- package/src/{testing → framework/oracle}/bvh.js +1 -1
- package/src/{testing → framework/oracle}/measure.js +1 -1
- package/src/{testing → framework/oracle}/min-wall.js +1 -1
- package/src/{testing → framework/oracle}/verify.js +3 -3
- package/src/framework/param-deps.js +1 -1
- package/src/framework/part-model.js +48 -0
- package/src/framework/pick-request/client.js +11 -3
- package/src/framework/pick-request/endpoint.js +60 -0
- package/src/framework/pick-request/index.js +6 -0
- package/src/framework/pick-request/server.js +222 -34
- package/src/framework/pick-request/token-store.js +31 -0
- package/src/framework/pose-fast-path.js +12 -1
- package/src/framework/pose-probe-core.js +129 -0
- package/src/framework/pose-probe.js +7 -123
- package/src/framework/regen-loop.js +10 -3
- package/src/framework/safe-name.js +26 -0
- package/src/framework/verify-metrics.js +4 -4
- package/src/framework/view-state.js +25 -21
- package/src/framework/view-tabs.js +22 -7
- package/src/framework/viewer-controls.js +5 -26
- package/src/framework/viewer.js +58 -17
- package/src/hinged-box-worker.js +3 -0
- package/src/index.js +1 -1
- package/src/parts/hinged-box.js +94 -0
- package/src/testing/render.js +19 -8
- package/src/testing.js +15 -8
- package/types/derive.d.ts +14 -0
- package/types/geometry.d.ts +117 -0
- package/types/index.d.ts +240 -0
- package/types/kernel.d.ts +409 -0
- package/types/lint.d.ts +85 -0
- package/types/part.d.ts +381 -0
- package/types/testing.d.ts +362 -0
- package/types/worker.d.ts +21 -0
- /package/src/{testing → framework/oracle}/assert-dsl.js +0 -0
- /package/src/{testing → framework/oracle}/cases.js +0 -0
- /package/src/{testing → framework/oracle}/dfm-profiles.js +0 -0
- /package/src/{testing → framework/oracle}/gaps.js +0 -0
- /package/src/{testing → framework/oracle}/mesh.js +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { CUTAWAY_OVERLAY_RENDER_ORDER } from "./cutaway-render.js";
|
|
3
|
+
|
|
4
|
+
const GIZMO_RENDER_ORDER = CUTAWAY_OVERLAY_RENDER_ORDER + 1;
|
|
5
|
+
const HANDLE_HOVER_THICKNESS = 1.6;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the cutaway gizmo's three.js scene graph: the ghost plane, the
|
|
9
|
+
* translate arrow, the two rotation arcs, and the invisible hit proxies that
|
|
10
|
+
* stand in for them while picking.
|
|
11
|
+
*
|
|
12
|
+
* Pure construction - the objects come back detached, so mounting them into a
|
|
13
|
+
* scene, posing them, and reacting to the pointer all stay in
|
|
14
|
+
* `createCutawayGizmo`.
|
|
15
|
+
*
|
|
16
|
+
* `theme` is one palette out of the gizmo's theme table. Colors are baked in
|
|
17
|
+
* here rather than written as literals so the very first rendered frame
|
|
18
|
+
* already agrees with the palette that later appearance updates re-derive.
|
|
19
|
+
*/
|
|
20
|
+
export function buildGizmoScene(theme) {
|
|
21
|
+
const geometries = new Set();
|
|
22
|
+
const materials = new Set();
|
|
23
|
+
|
|
24
|
+
const group = new THREE.Group();
|
|
25
|
+
|
|
26
|
+
const fill = new THREE.Mesh(
|
|
27
|
+
new THREE.PlaneGeometry(1, 1),
|
|
28
|
+
new THREE.MeshBasicMaterial({
|
|
29
|
+
color: theme.fill,
|
|
30
|
+
opacity: 0.18,
|
|
31
|
+
transparent: true,
|
|
32
|
+
depthTest: true,
|
|
33
|
+
depthWrite: false,
|
|
34
|
+
side: THREE.DoubleSide,
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
geometries.add(fill.geometry);
|
|
38
|
+
materials.add(fill.material);
|
|
39
|
+
|
|
40
|
+
const borderGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
41
|
+
new THREE.Vector3(-0.5, -0.5, 0),
|
|
42
|
+
new THREE.Vector3(0.5, -0.5, 0),
|
|
43
|
+
new THREE.Vector3(0.5, 0.5, 0),
|
|
44
|
+
new THREE.Vector3(-0.5, 0.5, 0),
|
|
45
|
+
]);
|
|
46
|
+
const borderMaterial = new THREE.LineBasicMaterial({
|
|
47
|
+
color: theme.border,
|
|
48
|
+
opacity: 1,
|
|
49
|
+
transparent: true,
|
|
50
|
+
depthTest: false,
|
|
51
|
+
depthWrite: false,
|
|
52
|
+
});
|
|
53
|
+
const border = new THREE.LineLoop(borderGeometry, borderMaterial);
|
|
54
|
+
border.renderOrder = GIZMO_RENDER_ORDER;
|
|
55
|
+
geometries.add(borderGeometry);
|
|
56
|
+
materials.add(borderMaterial);
|
|
57
|
+
|
|
58
|
+
const handleRoot = new THREE.Group();
|
|
59
|
+
const translateVisualRoot = new THREE.Group();
|
|
60
|
+
const arcRoot = new THREE.Group();
|
|
61
|
+
const translateMaterial = new THREE.MeshBasicMaterial({
|
|
62
|
+
color: theme.translate,
|
|
63
|
+
transparent: true,
|
|
64
|
+
depthTest: true,
|
|
65
|
+
depthWrite: true,
|
|
66
|
+
});
|
|
67
|
+
const rotateXMaterial = new THREE.MeshBasicMaterial({
|
|
68
|
+
color: theme.rotateX,
|
|
69
|
+
transparent: true,
|
|
70
|
+
depthTest: true,
|
|
71
|
+
depthWrite: true,
|
|
72
|
+
});
|
|
73
|
+
const rotateYMaterial = new THREE.MeshBasicMaterial({
|
|
74
|
+
color: theme.rotateY,
|
|
75
|
+
transparent: true,
|
|
76
|
+
depthTest: true,
|
|
77
|
+
depthWrite: true,
|
|
78
|
+
});
|
|
79
|
+
materials.add(translateMaterial);
|
|
80
|
+
materials.add(rotateXMaterial);
|
|
81
|
+
materials.add(rotateYMaterial);
|
|
82
|
+
|
|
83
|
+
const shaftGeometry = new THREE.CylinderGeometry(0.025, 0.025, 0.58, 12);
|
|
84
|
+
const shaftHoverGeometry = new THREE.CylinderGeometry(
|
|
85
|
+
0.025 * HANDLE_HOVER_THICKNESS,
|
|
86
|
+
0.025 * HANDLE_HOVER_THICKNESS,
|
|
87
|
+
0.58,
|
|
88
|
+
12,
|
|
89
|
+
);
|
|
90
|
+
const shaft = new THREE.Mesh(shaftGeometry, translateMaterial);
|
|
91
|
+
shaft.rotation.x = Math.PI / 2;
|
|
92
|
+
shaft.position.z = 0.29;
|
|
93
|
+
const coneGeometry = new THREE.ConeGeometry(0.075, 0.2, 16);
|
|
94
|
+
const coneHoverGeometry = new THREE.ConeGeometry(
|
|
95
|
+
0.075 * HANDLE_HOVER_THICKNESS,
|
|
96
|
+
0.2,
|
|
97
|
+
16,
|
|
98
|
+
);
|
|
99
|
+
const cone = new THREE.Mesh(coneGeometry, translateMaterial);
|
|
100
|
+
cone.rotation.x = Math.PI / 2;
|
|
101
|
+
cone.position.z = 0.68;
|
|
102
|
+
geometries.add(shaftGeometry);
|
|
103
|
+
geometries.add(shaftHoverGeometry);
|
|
104
|
+
geometries.add(coneGeometry);
|
|
105
|
+
geometries.add(coneHoverGeometry);
|
|
106
|
+
|
|
107
|
+
const ringXGeometry = new THREE.TorusGeometry(
|
|
108
|
+
0.42,
|
|
109
|
+
0.015,
|
|
110
|
+
8,
|
|
111
|
+
64,
|
|
112
|
+
Math.PI,
|
|
113
|
+
);
|
|
114
|
+
const ringX = new THREE.Mesh(ringXGeometry, rotateXMaterial);
|
|
115
|
+
const ringXHoverGeometry = new THREE.TorusGeometry(
|
|
116
|
+
0.42,
|
|
117
|
+
0.015 * HANDLE_HOVER_THICKNESS,
|
|
118
|
+
8,
|
|
119
|
+
64,
|
|
120
|
+
Math.PI,
|
|
121
|
+
);
|
|
122
|
+
ringX.quaternion
|
|
123
|
+
.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -Math.PI / 2)
|
|
124
|
+
.multiply(new THREE.Quaternion().setFromAxisAngle(
|
|
125
|
+
new THREE.Vector3(0, 1, 0),
|
|
126
|
+
Math.PI / 2,
|
|
127
|
+
));
|
|
128
|
+
const ringYGeometry = new THREE.TorusGeometry(
|
|
129
|
+
0.42,
|
|
130
|
+
0.015,
|
|
131
|
+
8,
|
|
132
|
+
64,
|
|
133
|
+
Math.PI,
|
|
134
|
+
);
|
|
135
|
+
const ringY = new THREE.Mesh(ringYGeometry, rotateYMaterial);
|
|
136
|
+
const ringYHoverGeometry = new THREE.TorusGeometry(
|
|
137
|
+
0.42,
|
|
138
|
+
0.015 * HANDLE_HOVER_THICKNESS,
|
|
139
|
+
8,
|
|
140
|
+
64,
|
|
141
|
+
Math.PI,
|
|
142
|
+
);
|
|
143
|
+
ringY.rotation.x = -Math.PI / 2;
|
|
144
|
+
geometries.add(ringXGeometry);
|
|
145
|
+
geometries.add(ringXHoverGeometry);
|
|
146
|
+
geometries.add(ringYGeometry);
|
|
147
|
+
geometries.add(ringYHoverGeometry);
|
|
148
|
+
|
|
149
|
+
const hitMaterial = new THREE.MeshBasicMaterial({
|
|
150
|
+
color: 0xffffff,
|
|
151
|
+
opacity: 0,
|
|
152
|
+
transparent: true,
|
|
153
|
+
depthWrite: false,
|
|
154
|
+
});
|
|
155
|
+
materials.add(hitMaterial);
|
|
156
|
+
const translateHitGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.95, 10);
|
|
157
|
+
const translateHit = new THREE.Mesh(translateHitGeometry, hitMaterial);
|
|
158
|
+
translateHit.rotation.x = Math.PI / 2;
|
|
159
|
+
translateHit.position.z = 0.38;
|
|
160
|
+
translateHit.userData.cutawayHandle = "translate";
|
|
161
|
+
const rotateXHitGeometry = new THREE.TorusGeometry(
|
|
162
|
+
0.42,
|
|
163
|
+
0.12,
|
|
164
|
+
8,
|
|
165
|
+
48,
|
|
166
|
+
Math.PI,
|
|
167
|
+
);
|
|
168
|
+
const rotateXHit = new THREE.Mesh(rotateXHitGeometry, hitMaterial);
|
|
169
|
+
rotateXHit.quaternion.copy(ringX.quaternion);
|
|
170
|
+
rotateXHit.userData.cutawayHandle = "rotate-x";
|
|
171
|
+
const rotateYHitGeometry = new THREE.TorusGeometry(
|
|
172
|
+
0.42,
|
|
173
|
+
0.12,
|
|
174
|
+
8,
|
|
175
|
+
48,
|
|
176
|
+
Math.PI,
|
|
177
|
+
);
|
|
178
|
+
const rotateYHit = new THREE.Mesh(rotateYHitGeometry, hitMaterial);
|
|
179
|
+
rotateYHit.quaternion.copy(ringY.quaternion);
|
|
180
|
+
rotateYHit.userData.cutawayHandle = "rotate-y";
|
|
181
|
+
geometries.add(translateHitGeometry);
|
|
182
|
+
geometries.add(rotateXHitGeometry);
|
|
183
|
+
geometries.add(rotateYHitGeometry);
|
|
184
|
+
|
|
185
|
+
translateVisualRoot.add(shaft, cone);
|
|
186
|
+
arcRoot.add(ringX, ringY, rotateXHit, rotateYHit);
|
|
187
|
+
handleRoot.add(translateVisualRoot, translateHit, arcRoot);
|
|
188
|
+
group.add(fill, border);
|
|
189
|
+
|
|
190
|
+
const handles = {
|
|
191
|
+
translate: translateHit,
|
|
192
|
+
rotateX: rotateXHit,
|
|
193
|
+
rotateY: rotateYHit,
|
|
194
|
+
};
|
|
195
|
+
const handleVisuals = {
|
|
196
|
+
translate: translateVisualRoot,
|
|
197
|
+
rotateX: ringX,
|
|
198
|
+
rotateY: ringY,
|
|
199
|
+
};
|
|
200
|
+
const handleAppearance = {
|
|
201
|
+
translate: {
|
|
202
|
+
visual: translateVisualRoot,
|
|
203
|
+
material: translateMaterial,
|
|
204
|
+
geometryPairs: [
|
|
205
|
+
{ mesh: shaft, normal: shaftGeometry, hovered: shaftHoverGeometry },
|
|
206
|
+
{ mesh: cone, normal: coneGeometry, hovered: coneHoverGeometry },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
"rotate-x": {
|
|
210
|
+
visual: ringX,
|
|
211
|
+
material: rotateXMaterial,
|
|
212
|
+
geometryPairs: [
|
|
213
|
+
{ mesh: ringX, normal: ringXGeometry, hovered: ringXHoverGeometry },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
"rotate-y": {
|
|
217
|
+
visual: ringY,
|
|
218
|
+
material: rotateYMaterial,
|
|
219
|
+
geometryPairs: [
|
|
220
|
+
{ mesh: ringY, normal: ringYGeometry, hovered: ringYHoverGeometry },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Every geometry and material above - including the thickened hover variants
|
|
226
|
+
// that are swapped in and out of the meshes - is owned by this scene and
|
|
227
|
+
// nothing else. The caller disposes it exactly once, at teardown.
|
|
228
|
+
function dispose() {
|
|
229
|
+
for (const geometry of geometries) geometry.dispose();
|
|
230
|
+
for (const material of materials) material.dispose();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
group,
|
|
235
|
+
fill,
|
|
236
|
+
border,
|
|
237
|
+
handleRoot,
|
|
238
|
+
arcRoot,
|
|
239
|
+
handles,
|
|
240
|
+
handleVisuals,
|
|
241
|
+
handleAppearance,
|
|
242
|
+
dispose,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
signedAngleAroundAxis,
|
|
5
5
|
snapQuaternionToAxis,
|
|
6
6
|
} from "./cutaway-math.js";
|
|
7
|
-
import {
|
|
7
|
+
import { buildGizmoScene } from "./cutaway-gizmo-scene.js";
|
|
8
8
|
|
|
9
|
+
// The single source of truth for gizmo colors: the scene is constructed from
|
|
10
|
+
// `dark` and `updateAppearance` re-derives from whichever mode is active, so a
|
|
11
|
+
// palette edit here reaches the first frame as well as every later one.
|
|
9
12
|
const THEMES = {
|
|
10
13
|
dark: {
|
|
11
14
|
fill: 0x65bff5,
|
|
@@ -23,6 +26,8 @@ const THEMES = {
|
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
|
|
29
|
+
export { THEMES as CUTAWAY_GIZMO_THEMES };
|
|
30
|
+
|
|
26
31
|
const TRANSLATION_SCREEN_ALIGNMENT = 0.9;
|
|
27
32
|
const ROTATION_SCREEN_ALIGNMENT = 0.15;
|
|
28
33
|
// A 120 px perpendicular drag rotates the plane by 90 degrees.
|
|
@@ -30,11 +35,9 @@ const SCREEN_ROTATION_RADIANS_PER_PIXEL = Math.PI / 240;
|
|
|
30
35
|
const SCREEN_AXIS_EPSILON_SQ = 1e-8;
|
|
31
36
|
// Reserve the visually shared center for the end-on translation handle.
|
|
32
37
|
const TRANSLATE_CENTER_RADIUS_PX = 22;
|
|
33
|
-
const GIZMO_RENDER_ORDER = CUTAWAY_OVERLAY_RENDER_ORDER + 1;
|
|
34
38
|
const GHOST_OFFSET_FACTOR = 0.001;
|
|
35
39
|
const MIN_GHOST_OFFSET = 0.01;
|
|
36
40
|
const MAX_GHOST_OFFSET = 0.25;
|
|
37
|
-
const HANDLE_HOVER_THICKNESS = 1.6;
|
|
38
41
|
const HANDLE_HOVER_WHITE_MIX = 0.28;
|
|
39
42
|
const WHITE = new THREE.Color(0xffffff);
|
|
40
43
|
|
|
@@ -50,211 +53,20 @@ export function createCutawayGizmo({
|
|
|
50
53
|
onDragChange = () => {},
|
|
51
54
|
pickHandle,
|
|
52
55
|
}) {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
depthWrite: false,
|
|
65
|
-
side: THREE.DoubleSide,
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
geometries.add(fill.geometry);
|
|
69
|
-
materials.add(fill.material);
|
|
70
|
-
|
|
71
|
-
const borderGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
72
|
-
new THREE.Vector3(-0.5, -0.5, 0),
|
|
73
|
-
new THREE.Vector3(0.5, -0.5, 0),
|
|
74
|
-
new THREE.Vector3(0.5, 0.5, 0),
|
|
75
|
-
new THREE.Vector3(-0.5, 0.5, 0),
|
|
76
|
-
]);
|
|
77
|
-
const borderMaterial = new THREE.LineBasicMaterial({
|
|
78
|
-
color: 0xa8dcff,
|
|
79
|
-
opacity: 1,
|
|
80
|
-
transparent: true,
|
|
81
|
-
depthTest: false,
|
|
82
|
-
depthWrite: false,
|
|
83
|
-
});
|
|
84
|
-
const border = new THREE.LineLoop(borderGeometry, borderMaterial);
|
|
85
|
-
border.renderOrder = GIZMO_RENDER_ORDER;
|
|
86
|
-
geometries.add(borderGeometry);
|
|
87
|
-
materials.add(borderMaterial);
|
|
88
|
-
|
|
89
|
-
const handleRoot = new THREE.Group();
|
|
90
|
-
const translateVisualRoot = new THREE.Group();
|
|
91
|
-
const arcRoot = new THREE.Group();
|
|
92
|
-
const translateMaterial = new THREE.MeshBasicMaterial({
|
|
93
|
-
color: 0x36d399,
|
|
94
|
-
transparent: true,
|
|
95
|
-
depthTest: true,
|
|
96
|
-
depthWrite: true,
|
|
97
|
-
});
|
|
98
|
-
const rotateXMaterial = new THREE.MeshBasicMaterial({
|
|
99
|
-
color: 0xff6b7a,
|
|
100
|
-
transparent: true,
|
|
101
|
-
depthTest: true,
|
|
102
|
-
depthWrite: true,
|
|
103
|
-
});
|
|
104
|
-
const rotateYMaterial = new THREE.MeshBasicMaterial({
|
|
105
|
-
color: 0x5aa9ff,
|
|
106
|
-
transparent: true,
|
|
107
|
-
depthTest: true,
|
|
108
|
-
depthWrite: true,
|
|
109
|
-
});
|
|
110
|
-
materials.add(translateMaterial);
|
|
111
|
-
materials.add(rotateXMaterial);
|
|
112
|
-
materials.add(rotateYMaterial);
|
|
113
|
-
|
|
114
|
-
const shaftGeometry = new THREE.CylinderGeometry(0.025, 0.025, 0.58, 12);
|
|
115
|
-
const shaftHoverGeometry = new THREE.CylinderGeometry(
|
|
116
|
-
0.025 * HANDLE_HOVER_THICKNESS,
|
|
117
|
-
0.025 * HANDLE_HOVER_THICKNESS,
|
|
118
|
-
0.58,
|
|
119
|
-
12,
|
|
120
|
-
);
|
|
121
|
-
const shaft = new THREE.Mesh(shaftGeometry, translateMaterial);
|
|
122
|
-
shaft.rotation.x = Math.PI / 2;
|
|
123
|
-
shaft.position.z = 0.29;
|
|
124
|
-
const coneGeometry = new THREE.ConeGeometry(0.075, 0.2, 16);
|
|
125
|
-
const coneHoverGeometry = new THREE.ConeGeometry(
|
|
126
|
-
0.075 * HANDLE_HOVER_THICKNESS,
|
|
127
|
-
0.2,
|
|
128
|
-
16,
|
|
129
|
-
);
|
|
130
|
-
const cone = new THREE.Mesh(coneGeometry, translateMaterial);
|
|
131
|
-
cone.rotation.x = Math.PI / 2;
|
|
132
|
-
cone.position.z = 0.68;
|
|
133
|
-
geometries.add(shaftGeometry);
|
|
134
|
-
geometries.add(shaftHoverGeometry);
|
|
135
|
-
geometries.add(coneGeometry);
|
|
136
|
-
geometries.add(coneHoverGeometry);
|
|
137
|
-
|
|
138
|
-
const ringXGeometry = new THREE.TorusGeometry(
|
|
139
|
-
0.42,
|
|
140
|
-
0.015,
|
|
141
|
-
8,
|
|
142
|
-
64,
|
|
143
|
-
Math.PI,
|
|
144
|
-
);
|
|
145
|
-
const ringX = new THREE.Mesh(ringXGeometry, rotateXMaterial);
|
|
146
|
-
const ringXHoverGeometry = new THREE.TorusGeometry(
|
|
147
|
-
0.42,
|
|
148
|
-
0.015 * HANDLE_HOVER_THICKNESS,
|
|
149
|
-
8,
|
|
150
|
-
64,
|
|
151
|
-
Math.PI,
|
|
152
|
-
);
|
|
153
|
-
ringX.quaternion
|
|
154
|
-
.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -Math.PI / 2)
|
|
155
|
-
.multiply(new THREE.Quaternion().setFromAxisAngle(
|
|
156
|
-
new THREE.Vector3(0, 1, 0),
|
|
157
|
-
Math.PI / 2,
|
|
158
|
-
));
|
|
159
|
-
const ringYGeometry = new THREE.TorusGeometry(
|
|
160
|
-
0.42,
|
|
161
|
-
0.015,
|
|
162
|
-
8,
|
|
163
|
-
64,
|
|
164
|
-
Math.PI,
|
|
165
|
-
);
|
|
166
|
-
const ringY = new THREE.Mesh(ringYGeometry, rotateYMaterial);
|
|
167
|
-
const ringYHoverGeometry = new THREE.TorusGeometry(
|
|
168
|
-
0.42,
|
|
169
|
-
0.015 * HANDLE_HOVER_THICKNESS,
|
|
170
|
-
8,
|
|
171
|
-
64,
|
|
172
|
-
Math.PI,
|
|
173
|
-
);
|
|
174
|
-
ringY.rotation.x = -Math.PI / 2;
|
|
175
|
-
geometries.add(ringXGeometry);
|
|
176
|
-
geometries.add(ringXHoverGeometry);
|
|
177
|
-
geometries.add(ringYGeometry);
|
|
178
|
-
geometries.add(ringYHoverGeometry);
|
|
179
|
-
|
|
180
|
-
const hitMaterial = new THREE.MeshBasicMaterial({
|
|
181
|
-
color: 0xffffff,
|
|
182
|
-
opacity: 0,
|
|
183
|
-
transparent: true,
|
|
184
|
-
depthWrite: false,
|
|
185
|
-
});
|
|
186
|
-
materials.add(hitMaterial);
|
|
187
|
-
const translateHitGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.95, 10);
|
|
188
|
-
const translateHit = new THREE.Mesh(translateHitGeometry, hitMaterial);
|
|
189
|
-
translateHit.rotation.x = Math.PI / 2;
|
|
190
|
-
translateHit.position.z = 0.38;
|
|
191
|
-
translateHit.userData.cutawayHandle = "translate";
|
|
192
|
-
const rotateXHitGeometry = new THREE.TorusGeometry(
|
|
193
|
-
0.42,
|
|
194
|
-
0.12,
|
|
195
|
-
8,
|
|
196
|
-
48,
|
|
197
|
-
Math.PI,
|
|
198
|
-
);
|
|
199
|
-
const rotateXHit = new THREE.Mesh(rotateXHitGeometry, hitMaterial);
|
|
200
|
-
rotateXHit.quaternion.copy(ringX.quaternion);
|
|
201
|
-
rotateXHit.userData.cutawayHandle = "rotate-x";
|
|
202
|
-
const rotateYHitGeometry = new THREE.TorusGeometry(
|
|
203
|
-
0.42,
|
|
204
|
-
0.12,
|
|
205
|
-
8,
|
|
206
|
-
48,
|
|
207
|
-
Math.PI,
|
|
208
|
-
);
|
|
209
|
-
const rotateYHit = new THREE.Mesh(rotateYHitGeometry, hitMaterial);
|
|
210
|
-
rotateYHit.quaternion.copy(ringY.quaternion);
|
|
211
|
-
rotateYHit.userData.cutawayHandle = "rotate-y";
|
|
212
|
-
geometries.add(translateHitGeometry);
|
|
213
|
-
geometries.add(rotateXHitGeometry);
|
|
214
|
-
geometries.add(rotateYHitGeometry);
|
|
215
|
-
|
|
216
|
-
translateVisualRoot.add(shaft, cone);
|
|
217
|
-
arcRoot.add(ringX, ringY, rotateXHit, rotateYHit);
|
|
218
|
-
handleRoot.add(translateVisualRoot, translateHit, arcRoot);
|
|
219
|
-
group.add(fill, border);
|
|
56
|
+
const sceneGraph = buildGizmoScene(THEMES.dark);
|
|
57
|
+
const {
|
|
58
|
+
group,
|
|
59
|
+
fill,
|
|
60
|
+
border,
|
|
61
|
+
handleRoot,
|
|
62
|
+
arcRoot,
|
|
63
|
+
handles,
|
|
64
|
+
handleVisuals,
|
|
65
|
+
handleAppearance,
|
|
66
|
+
} = sceneGraph;
|
|
220
67
|
scene.add(group);
|
|
221
68
|
overlayScene.add(handleRoot);
|
|
222
69
|
|
|
223
|
-
const handles = {
|
|
224
|
-
translate: translateHit,
|
|
225
|
-
rotateX: rotateXHit,
|
|
226
|
-
rotateY: rotateYHit,
|
|
227
|
-
};
|
|
228
|
-
const handleVisuals = {
|
|
229
|
-
translate: translateVisualRoot,
|
|
230
|
-
rotateX: ringX,
|
|
231
|
-
rotateY: ringY,
|
|
232
|
-
};
|
|
233
|
-
const handleAppearance = {
|
|
234
|
-
translate: {
|
|
235
|
-
visual: translateVisualRoot,
|
|
236
|
-
material: translateMaterial,
|
|
237
|
-
geometryPairs: [
|
|
238
|
-
{ mesh: shaft, normal: shaftGeometry, hovered: shaftHoverGeometry },
|
|
239
|
-
{ mesh: cone, normal: coneGeometry, hovered: coneHoverGeometry },
|
|
240
|
-
],
|
|
241
|
-
},
|
|
242
|
-
"rotate-x": {
|
|
243
|
-
visual: ringX,
|
|
244
|
-
material: rotateXMaterial,
|
|
245
|
-
geometryPairs: [
|
|
246
|
-
{ mesh: ringX, normal: ringXGeometry, hovered: ringXHoverGeometry },
|
|
247
|
-
],
|
|
248
|
-
},
|
|
249
|
-
"rotate-y": {
|
|
250
|
-
visual: ringY,
|
|
251
|
-
material: rotateYMaterial,
|
|
252
|
-
geometryPairs: [
|
|
253
|
-
{ mesh: ringY, normal: ringYGeometry, hovered: ringYHoverGeometry },
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
|
|
258
70
|
let disposed = false;
|
|
259
71
|
let poseSize = 1;
|
|
260
72
|
let flipped = false;
|
|
@@ -336,8 +148,8 @@ export function createCutawayGizmo({
|
|
|
336
148
|
const theme = THEMES[themeMode] ?? THEMES.dark;
|
|
337
149
|
fill.material.color.set(theme.fill);
|
|
338
150
|
fill.material.opacity = activeAppearance ? 0.18 : 0.055;
|
|
339
|
-
|
|
340
|
-
|
|
151
|
+
border.material.color.set(theme.border);
|
|
152
|
+
border.material.opacity = activeAppearance ? 1 : 0.72;
|
|
341
153
|
|
|
342
154
|
for (const [handle, { visual, material, geometryPairs }] of Object.entries(handleAppearance)) {
|
|
343
155
|
const hovered = handle === hoveredHandle;
|
|
@@ -396,6 +208,12 @@ export function createCutawayGizmo({
|
|
|
396
208
|
return camera.getWorldDirection(new THREE.Vector3()).normalize();
|
|
397
209
|
}
|
|
398
210
|
|
|
211
|
+
// |axis . view|: 1 when we are staring straight down the axis, 0 when the
|
|
212
|
+
// axis lies flat in the screen plane. Both drag families branch on it.
|
|
213
|
+
function axisViewAlignment(position, axis) {
|
|
214
|
+
return Math.abs(axis.dot(viewDirectionAt(position)));
|
|
215
|
+
}
|
|
216
|
+
|
|
399
217
|
function projectToClient(point) {
|
|
400
218
|
const rect = domElement.getBoundingClientRect();
|
|
401
219
|
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
@@ -419,13 +237,9 @@ export function createCutawayGizmo({
|
|
|
419
237
|
return new THREE.Vector2(-screenAxis.y, screenAxis.x);
|
|
420
238
|
}
|
|
421
239
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (!ray) return;
|
|
426
|
-
const handle = pick(event, ray);
|
|
427
|
-
if (!handle) return;
|
|
428
|
-
|
|
240
|
+
// The fields every drag carries, before the mode-specific ones are filled in
|
|
241
|
+
// by beginTranslateDrag / beginRotateDrag.
|
|
242
|
+
function baseDrag(event, handle) {
|
|
429
243
|
const startPosition = group.position.clone();
|
|
430
244
|
const startQuaternion = group.quaternion.clone();
|
|
431
245
|
const localAxis = handle === "rotate-x"
|
|
@@ -433,10 +247,7 @@ export function createCutawayGizmo({
|
|
|
433
247
|
: handle === "rotate-y"
|
|
434
248
|
? new THREE.Vector3(0, 1, 0)
|
|
435
249
|
: new THREE.Vector3(0, 0, 1);
|
|
436
|
-
|
|
437
|
-
const viewDirection = viewDirectionAt(startPosition);
|
|
438
|
-
const alignment = Math.abs(axis.dot(viewDirection));
|
|
439
|
-
const nextDrag = {
|
|
250
|
+
return {
|
|
440
251
|
pointerId: event.pointerId,
|
|
441
252
|
handle,
|
|
442
253
|
orbitEnabled: orbitControls?.enabled,
|
|
@@ -445,38 +256,65 @@ export function createCutawayGizmo({
|
|
|
445
256
|
startClientX: event.clientX,
|
|
446
257
|
startClientY: event.clientY,
|
|
447
258
|
unitsPerPixel: worldUnitsPerPixelAt(startPosition),
|
|
448
|
-
axis,
|
|
259
|
+
axis: localAxis.applyQuaternion(startQuaternion).normalize(),
|
|
449
260
|
mode: null,
|
|
450
261
|
startParameter: null,
|
|
451
262
|
rotationPlane: null,
|
|
452
263
|
startRadial: null,
|
|
453
264
|
screenRotationDirection: null,
|
|
454
265
|
};
|
|
266
|
+
}
|
|
455
267
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
268
|
+
// Sliding along the plane normal. Pointing the normal at the camera leaves
|
|
269
|
+
// the axis almost no screen travel to slide along - and the ray may miss it
|
|
270
|
+
// outright - so vertical pointer motion drives the offset instead.
|
|
271
|
+
function beginTranslateDrag(record, ray) {
|
|
272
|
+
const alignment = axisViewAlignment(record.startPosition, record.axis);
|
|
273
|
+
record.startParameter = axisParameterFromRay(ray, record.startPosition, record.axis);
|
|
274
|
+
record.mode = alignment > TRANSLATION_SCREEN_ALIGNMENT
|
|
275
|
+
|| record.startParameter == null
|
|
276
|
+
? "screen-translate"
|
|
277
|
+
: "axis-translate";
|
|
278
|
+
return record;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Spinning about a ring axis. Resolving the drag geometrically against the
|
|
282
|
+
// rotation plane only works when we are looking down that axis; edge-on, the
|
|
283
|
+
// plane is nearly parallel to the view and the intersection is useless, so
|
|
284
|
+
// the angle comes from pointer travel perpendicular to the on-screen axis.
|
|
285
|
+
// Returns null when neither route is usable, which aborts the press.
|
|
286
|
+
function beginRotateDrag(record, ray) {
|
|
287
|
+
const alignment = axisViewAlignment(record.startPosition, record.axis);
|
|
288
|
+
if (alignment >= ROTATION_SCREEN_ALIGNMENT) {
|
|
289
|
+
const plane = new THREE.Plane()
|
|
290
|
+
.setFromNormalAndCoplanarPoint(record.axis, record.startPosition);
|
|
291
|
+
const point = ray.intersectPlane(plane, new THREE.Vector3());
|
|
292
|
+
const radial = point?.sub(record.startPosition);
|
|
293
|
+
if (radial && radial.lengthSq() >= 1e-12) {
|
|
294
|
+
record.mode = "plane-rotate";
|
|
295
|
+
record.rotationPlane = plane;
|
|
296
|
+
record.startRadial = radial.normalize();
|
|
297
|
+
return record;
|
|
478
298
|
}
|
|
479
299
|
}
|
|
300
|
+
record.screenRotationDirection = screenRotationDirection(record.startPosition, record.axis);
|
|
301
|
+
if (!record.screenRotationDirection) return null;
|
|
302
|
+
record.mode = "screen-rotate";
|
|
303
|
+
return record;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function onPointerDown(event) {
|
|
307
|
+
if (disposed || drag || !group.visible || (event.button != null && event.button !== 0)) return;
|
|
308
|
+
const ray = rayFromEvent(event);
|
|
309
|
+
if (!ray) return;
|
|
310
|
+
const handle = pick(event, ray);
|
|
311
|
+
if (!handle) return;
|
|
312
|
+
|
|
313
|
+
const record = baseDrag(event, handle);
|
|
314
|
+
const nextDrag = handle === "translate"
|
|
315
|
+
? beginTranslateDrag(record, ray)
|
|
316
|
+
: beginRotateDrag(record, ray);
|
|
317
|
+
if (!nextDrag) return;
|
|
480
318
|
|
|
481
319
|
setHoveredHandle(handle);
|
|
482
320
|
onActivity();
|
|
@@ -680,8 +518,7 @@ export function createCutawayGizmo({
|
|
|
680
518
|
}
|
|
681
519
|
scene.remove(group);
|
|
682
520
|
overlayScene.remove(handleRoot);
|
|
683
|
-
|
|
684
|
-
for (const material of materials) material.dispose();
|
|
521
|
+
sceneGraph.dispose();
|
|
685
522
|
}
|
|
686
523
|
}
|
|
687
524
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Which view a part opens on. The rule is independent of display order: an author
|
|
2
|
+
// can flag a view `default: true` wherever it sits in the tab bar, and when nobody
|
|
3
|
+
// flags one, the biggest view wins — the one placing the most sub-parts at the
|
|
4
|
+
// part's defaults, which for a multi-view part is the assembly. Ties fall back to
|
|
5
|
+
// declaration order, so every part written before this existed resolves to the view
|
|
6
|
+
// it has always opened on.
|
|
7
|
+
//
|
|
8
|
+
// Pure: no DOM, no storage, no kernel, no build. The headless surfaces (measure /
|
|
9
|
+
// verify / renderViews) deliberately do NOT use this — they stay on
|
|
10
|
+
// Object.keys(part.views)[0], where a mechanically obvious rule matters more than a
|
|
11
|
+
// convenient one.
|
|
12
|
+
|
|
13
|
+
const isPlainObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
|
|
14
|
+
|
|
15
|
+
// A sub-part counts toward a view when it lists that view and is enabled at the
|
|
16
|
+
// part's defaults. A throwing `enabled` counts as present: the same instinct as
|
|
17
|
+
// mount's throwing-`derive` handling, and an unrelated predicate bug shouldn't
|
|
18
|
+
// silently demote a whole view.
|
|
19
|
+
function placedCount(part, view, defaults) {
|
|
20
|
+
const parts = isPlainObject(part?.parts) ? part.parts : {};
|
|
21
|
+
let n = 0;
|
|
22
|
+
for (const sp of Object.values(parts)) {
|
|
23
|
+
if (!Array.isArray(sp?.views) || !sp.views.includes(view)) continue;
|
|
24
|
+
if (typeof sp.enabled !== "function") { n++; continue; }
|
|
25
|
+
try { if (sp.enabled(defaults)) n++; } catch { n++; }
|
|
26
|
+
}
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveDefaultView(part) {
|
|
31
|
+
if (!isPlainObject(part?.views)) return null;
|
|
32
|
+
const keys = Object.keys(part.views);
|
|
33
|
+
if (keys.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
const flagged = keys.find((k) => part.views[k]?.default === true);
|
|
36
|
+
if (flagged) return flagged;
|
|
37
|
+
|
|
38
|
+
const defaults = isPlainObject(part?.defaults) ? part.defaults : {};
|
|
39
|
+
let best = keys[0];
|
|
40
|
+
let bestCount = placedCount(part, best, defaults);
|
|
41
|
+
for (const k of keys.slice(1)) {
|
|
42
|
+
const n = placedCount(part, k, defaults);
|
|
43
|
+
if (n > bestCount) { best = k; bestCount = n; } // strict > keeps a tie on the earlier key
|
|
44
|
+
}
|
|
45
|
+
return best;
|
|
46
|
+
}
|