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.
Files changed (70) hide show
  1. package/README.md +31 -10
  2. package/bin/cli.js +100 -27
  3. package/docs/AUTHORING-PARTS.md +126 -14
  4. package/docs/ERROR-PATTERNS.md +6 -0
  5. package/package.json +48 -7
  6. package/skills/partforge/SKILL.md +17 -3
  7. package/src/app-embed-test.js +1 -1
  8. package/src/app-hinged-box.js +12 -0
  9. package/src/framework/animation-controls.js +243 -0
  10. package/src/framework/animation.js +217 -0
  11. package/src/framework/app.css +32 -0
  12. package/src/framework/assembly.js +1 -1
  13. package/src/framework/backend-select.js +25 -0
  14. package/src/framework/camera-tween.js +58 -0
  15. package/src/framework/chrome.css +16 -0
  16. package/src/framework/controls.js +13 -3
  17. package/src/framework/cutaway-gizmo-scene.js +244 -0
  18. package/src/framework/cutaway-gizmo.js +80 -243
  19. package/src/framework/default-view.js +46 -0
  20. package/src/framework/download.js +7 -2
  21. package/src/framework/export-controller.js +13 -2
  22. package/src/framework/geometry/probe.js +3 -22
  23. package/src/framework/jobs.js +9 -40
  24. package/src/framework/lint/finding.js +4 -0
  25. package/src/framework/lint/index.js +7 -3
  26. package/src/framework/lint/rules-animations.js +404 -0
  27. package/src/framework/lint/rules-place.js +76 -0
  28. package/src/framework/lint/rules-shape.js +12 -0
  29. package/src/framework/lint/rules-verify.js +2 -2
  30. package/src/framework/mount.js +93 -18
  31. package/src/{testing → framework/oracle}/build.js +1 -1
  32. package/src/{testing → framework/oracle}/bvh.js +1 -1
  33. package/src/{testing → framework/oracle}/measure.js +1 -1
  34. package/src/{testing → framework/oracle}/min-wall.js +1 -1
  35. package/src/{testing → framework/oracle}/verify.js +3 -3
  36. package/src/framework/param-deps.js +1 -1
  37. package/src/framework/part-model.js +48 -0
  38. package/src/framework/pick-request/client.js +11 -3
  39. package/src/framework/pick-request/endpoint.js +60 -0
  40. package/src/framework/pick-request/index.js +6 -0
  41. package/src/framework/pick-request/server.js +222 -34
  42. package/src/framework/pick-request/token-store.js +31 -0
  43. package/src/framework/pose-fast-path.js +12 -1
  44. package/src/framework/pose-probe-core.js +129 -0
  45. package/src/framework/pose-probe.js +7 -123
  46. package/src/framework/regen-loop.js +10 -3
  47. package/src/framework/safe-name.js +26 -0
  48. package/src/framework/verify-metrics.js +4 -4
  49. package/src/framework/view-state.js +25 -21
  50. package/src/framework/view-tabs.js +22 -7
  51. package/src/framework/viewer-controls.js +5 -26
  52. package/src/framework/viewer.js +58 -17
  53. package/src/hinged-box-worker.js +3 -0
  54. package/src/index.js +1 -1
  55. package/src/parts/hinged-box.js +94 -0
  56. package/src/testing/render.js +19 -8
  57. package/src/testing.js +15 -8
  58. package/types/derive.d.ts +14 -0
  59. package/types/geometry.d.ts +117 -0
  60. package/types/index.d.ts +240 -0
  61. package/types/kernel.d.ts +409 -0
  62. package/types/lint.d.ts +85 -0
  63. package/types/part.d.ts +381 -0
  64. package/types/testing.d.ts +362 -0
  65. package/types/worker.d.ts +21 -0
  66. /package/src/{testing → framework/oracle}/assert-dsl.js +0 -0
  67. /package/src/{testing → framework/oracle}/cases.js +0 -0
  68. /package/src/{testing → framework/oracle}/dfm-profiles.js +0 -0
  69. /package/src/{testing → framework/oracle}/gaps.js +0 -0
  70. /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 { CUTAWAY_OVERLAY_RENDER_ORDER } from "./cutaway-render.js";
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 group = new THREE.Group();
54
- const geometries = new Set();
55
- const materials = new Set();
56
-
57
- const fill = new THREE.Mesh(
58
- new THREE.PlaneGeometry(1, 1),
59
- new THREE.MeshBasicMaterial({
60
- color: 0x65bff5,
61
- opacity: 0.18,
62
- transparent: true,
63
- depthTest: true,
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
- borderMaterial.color.set(theme.border);
340
- borderMaterial.opacity = activeAppearance ? 1 : 0.72;
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
- function onPointerDown(event) {
423
- if (disposed || drag || !group.visible || (event.button != null && event.button !== 0)) return;
424
- const ray = rayFromEvent(event);
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
- const axis = localAxis.applyQuaternion(startQuaternion).normalize();
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
- if (handle === "translate") {
457
- nextDrag.startParameter = axisParameterFromRay(ray, startPosition, axis);
458
- nextDrag.mode = alignment > TRANSLATION_SCREEN_ALIGNMENT
459
- || nextDrag.startParameter == null
460
- ? "screen-translate"
461
- : "axis-translate";
462
- } else {
463
- const useScreenRotation = alignment < ROTATION_SCREEN_ALIGNMENT;
464
- if (!useScreenRotation) {
465
- const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(axis, startPosition);
466
- const point = ray.intersectPlane(plane, new THREE.Vector3());
467
- const radial = point?.sub(startPosition);
468
- if (radial && radial.lengthSq() >= 1e-12) {
469
- nextDrag.mode = "plane-rotate";
470
- nextDrag.rotationPlane = plane;
471
- nextDrag.startRadial = radial.normalize();
472
- }
473
- }
474
- if (nextDrag.mode !== "plane-rotate") {
475
- nextDrag.screenRotationDirection = screenRotationDirection(startPosition, axis);
476
- if (!nextDrag.screenRotationDirection) return;
477
- nextDrag.mode = "screen-rotate";
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
- for (const geometry of geometries) geometry.dispose();
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
+ }