partforge 0.40.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 (72) hide show
  1. package/README.md +46 -6
  2. package/bin/cli.js +103 -27
  3. package/docs/AUTHORING-PARTS.md +143 -15
  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 +23 -42
  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 +113 -18
  31. package/src/{testing → framework/oracle}/build.js +1 -1
  32. package/src/framework/oracle/bvh.js +463 -0
  33. package/src/{testing → framework/oracle}/gaps.js +6 -3
  34. package/src/{testing → framework/oracle}/measure.js +34 -4
  35. package/src/framework/oracle/min-wall.js +98 -0
  36. package/src/{testing → framework/oracle}/verify.js +61 -5
  37. package/src/framework/param-deps.js +1 -1
  38. package/src/framework/part-model.js +48 -0
  39. package/src/framework/pick-request/client.js +11 -3
  40. package/src/framework/pick-request/endpoint.js +60 -0
  41. package/src/framework/pick-request/index.js +6 -0
  42. package/src/framework/pick-request/server.js +222 -34
  43. package/src/framework/pick-request/token-store.js +31 -0
  44. package/src/framework/pose-fast-path.js +12 -1
  45. package/src/framework/pose-probe-core.js +129 -0
  46. package/src/framework/pose-probe.js +7 -123
  47. package/src/framework/regen-loop.js +10 -3
  48. package/src/framework/safe-name.js +26 -0
  49. package/src/framework/verify-metrics.js +19 -6
  50. package/src/framework/view-state.js +25 -21
  51. package/src/framework/view-tabs.js +22 -7
  52. package/src/framework/viewer-controls.js +5 -26
  53. package/src/framework/viewer.js +126 -16
  54. package/src/hinged-box-worker.js +3 -0
  55. package/src/index.js +1 -1
  56. package/src/parts/hinged-box.js +94 -0
  57. package/src/testing/render.js +19 -8
  58. package/src/testing.js +15 -8
  59. package/types/derive.d.ts +14 -0
  60. package/types/geometry.d.ts +117 -0
  61. package/types/index.d.ts +240 -0
  62. package/types/kernel.d.ts +409 -0
  63. package/types/lint.d.ts +85 -0
  64. package/types/part.d.ts +381 -0
  65. package/types/testing.d.ts +362 -0
  66. package/types/worker.d.ts +21 -0
  67. package/src/testing/bvh.js +0 -273
  68. package/src/testing/min-wall.js +0 -38
  69. /package/src/{testing → framework/oracle}/assert-dsl.js +0 -0
  70. /package/src/{testing → framework/oracle}/cases.js +0 -0
  71. /package/src/{testing → framework/oracle}/dfm-profiles.js +0 -0
  72. /package/src/{testing → framework/oracle}/mesh.js +0 -0
@@ -161,6 +161,14 @@
161
161
  .pf-float-tabs { top: 12px; left: 50%; transform: translateX(-50%); }
162
162
  .pf-float-viewbar { bottom: 12px; right: 12px; }
163
163
 
164
+ /* --- animation transport bar (generated by animation-controls.js) ----------
165
+ PLACEMENT ONLY, per the rule above; appearance lives in app.css next to
166
+ #viewbar's, so a host that re-anchors this bar still inherits its chrome. */
167
+ .pf-anim-bar {
168
+ position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
169
+ z-index: 15; max-width: calc(100% - 24px);
170
+ }
171
+
164
172
  /* ---- the narrow-layout tab bar ------------------------------------------
165
173
  Created by mobile-tabs.js (no host markup declares it). Hidden by default:
166
174
  below the breakpoint the rail sits beside the viewer and needs no tab, so
@@ -229,6 +237,14 @@
229
237
  }
230
238
  .pf-rail-seam { display: none; }
231
239
  .pf-tabbar { display: flex; }
240
+ /* Lift the transport bar clear of the viewbar. On a phone the bar is nearly
241
+ the full stage width (max-width: 100% - 24px) while #viewbar sits at the
242
+ bottom-right of the same stage, at the same z-index — so at bottom: 14px
243
+ the transport bar paints straight over cutaway/reframe/theme and
244
+ makes them unclickable. The viewbar occupies 12px…56px from the bottom
245
+ (12px offset + 4px padding + 34px button + 4px padding + 2px border);
246
+ 64px clears it with an 8px gap. */
247
+ .pf-anim-bar { bottom: 64px; }
232
248
  }
233
249
 
234
250
 
@@ -48,10 +48,20 @@ export function clampToRange(raw, min, max) {
48
48
  }
49
49
 
50
50
  // --- info glyph + per-panel popover -----------------------------------------
51
+ // Popover top edge: below the glyph when it fits, flipped above when the
52
+ // viewport bottom would clip it (e.g. the animation transport bar's ⓘ, which
53
+ // sits at the bottom of the stage). Pure, for direct unit testing — happy-dom
54
+ // reports zero layout metrics, so the flip can't be exercised via the DOM.
55
+ export function popoverTop({ glyphTop, glyphBottom, popHeight, viewportHeight }) {
56
+ const below = glyphBottom + 6;
57
+ if (below + popHeight <= viewportHeight - 8) return below;
58
+ return Math.max(8, glyphTop - 6 - popHeight);
59
+ }
60
+
51
61
  // One popover element per panel, shared by all its glyphs (only one open at a
52
62
  // time). Document-level dismiss listeners are registered per panel and removed
53
63
  // by panel.dispose().
54
- function createInfoPopover() {
64
+ export function createInfoPopover() {
55
65
  const pop = el("div", "popover");
56
66
  pop.hidden = true;
57
67
  document.body.append(pop);
@@ -78,7 +88,7 @@ function createInfoPopover() {
78
88
  owner = glyph;
79
89
  glyph.setAttribute("aria-expanded", "true");
80
90
  const r = glyph.getBoundingClientRect();
81
- pop.style.top = `${r.bottom + 6}px`;
91
+ pop.style.top = `${popoverTop({ glyphTop: r.top, glyphBottom: r.bottom, popHeight: pop.offsetHeight, viewportHeight: window.innerHeight })}px`;
82
92
  pop.style.left = `${Math.max(8, r.left - 8)}px`;
83
93
  },
84
94
  dispose() {
@@ -91,7 +101,7 @@ function createInfoPopover() {
91
101
 
92
102
  // Append a focusable ⓘ glyph to `container` that toggles the panel's shared
93
103
  // popover with `description` (Markdown). No-op when description is empty.
94
- function attachInfo(container, description, info) {
104
+ export function attachInfo(container, description, info) {
95
105
  if (typeof description !== "string" || !description.trim()) return;
96
106
  const glyph = document.createElement("button");
97
107
  glyph.type = "button";
@@ -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
+ }