partforge 0.71.0 → 0.73.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/docs/AUTHORING-PARTS.md +71 -3
- package/package.json +1 -1
- package/src/framework/animation-controls.js +171 -16
- package/src/framework/annotate/annotate-mode.js +31 -3
- package/src/framework/app.css +122 -4
- package/src/framework/camera-orbit.js +84 -0
- package/src/framework/camera-tween.js +22 -10
- package/src/framework/chrome.css +113 -3
- package/src/framework/cutaway-gizmo.js +6 -1
- package/src/framework/cutaway.js +9 -1
- package/src/framework/jobs.js +22 -5
- package/src/framework/measure/dim3-scene.js +15 -2
- package/src/framework/mount.js +79 -2
- package/src/framework/oracle/gates.js +50 -0
- package/src/framework/oracle/measure.js +21 -4
- package/src/framework/oracle/min-wall.js +17 -0
- package/src/framework/oracle/verify.js +63 -22
- package/src/framework/projection.js +19 -0
- package/src/framework/view-angles.js +69 -1
- package/src/framework/view-state.js +9 -0
- package/src/framework/viewcube/cube-canvas.js +410 -0
- package/src/framework/viewcube/cube-geom.js +367 -0
- package/src/framework/viewcube/viewcube-controls.js +157 -0
- package/src/framework/viewcube/viewcube-mode.js +201 -0
- package/src/framework/viewer.js +289 -23
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// The view cube's orchestrator — the only viewcube file that touches both the
|
|
2
|
+
// viewer and the DOM (the annotate-mode.js / measure-mode.js stance). Owns the
|
|
3
|
+
// frame subscription, the dirty check that keeps idle frames free, pointer
|
|
4
|
+
// input, and the drag/click split.
|
|
5
|
+
import { projectCube, hitRegion, CUBE_DOWN_BIAS_PX } from "./cube-geom.js";
|
|
6
|
+
import { createCubeCanvas, CUBE_SIZE, CUBE_SIZE_NARROW, CUBE_RENDER } from "./cube-canvas.js";
|
|
7
|
+
import { RAIL_NARROW_BREAKPOINT } from "../rail-state.js";
|
|
8
|
+
import { runCleanupSteps } from "../teardown.js";
|
|
9
|
+
|
|
10
|
+
// Past this many px of travel a press is an orbit, not a click. 4px is the
|
|
11
|
+
// usual "did they mean to drag" threshold and comfortably above the jitter a
|
|
12
|
+
// trackpad tap produces.
|
|
13
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
14
|
+
|
|
15
|
+
export function createViewcubeMode(viewer, {
|
|
16
|
+
host,
|
|
17
|
+
createCanvas = createCubeCanvas,
|
|
18
|
+
dragThreshold = DRAG_THRESHOLD_PX,
|
|
19
|
+
matchMedia = (typeof window !== "undefined" && typeof window.matchMedia === "function")
|
|
20
|
+
? window.matchMedia.bind(window)
|
|
21
|
+
: null,
|
|
22
|
+
} = {}) {
|
|
23
|
+
const wrap = document.createElement("div");
|
|
24
|
+
wrap.className = "pf-viewcube";
|
|
25
|
+
host.appendChild(wrap);
|
|
26
|
+
|
|
27
|
+
// Below the rail's narrow breakpoint the shell shows one pane at a time
|
|
28
|
+
// (rail.js) and the stage is a lot tighter, so the cube shrinks back to its
|
|
29
|
+
// previous size. This is a BREAKPOINT, not an element size, so it is a media
|
|
30
|
+
// query rather than a ResizeObserver — this renderer deliberately owns no
|
|
31
|
+
// observer of its own.
|
|
32
|
+
const narrowQuery = matchMedia ? matchMedia(`(max-width: ${RAIL_NARROW_BREAKPOINT}px)`) : null;
|
|
33
|
+
const sizeForViewport = () => (narrowQuery?.matches ? CUBE_SIZE_NARROW : CUBE_SIZE);
|
|
34
|
+
|
|
35
|
+
const canvas = createCanvas(wrap, { size: sizeForViewport() });
|
|
36
|
+
canvas.setTheme?.(viewer.getTheme?.() ?? "dark");
|
|
37
|
+
|
|
38
|
+
let hidden = false;
|
|
39
|
+
let hover = null;
|
|
40
|
+
let projected = null;
|
|
41
|
+
// The dirty check. An unchanged camera must cost nothing — no clear, no
|
|
42
|
+
// fills — because this runs inside the viewer's rAF callback alongside the
|
|
43
|
+
// cutaway's outline re-slice and the main render.
|
|
44
|
+
let lastKey = null;
|
|
45
|
+
|
|
46
|
+
const cameraKey = () => {
|
|
47
|
+
const cam = viewer.camera;
|
|
48
|
+
const q = cam.quaternion;
|
|
49
|
+
// Zoom is part of the key because an ortho dolly changes camera.zoom
|
|
50
|
+
// without touching the quaternion, and the cube's scale follows neither —
|
|
51
|
+
// but the projection SWAP repaints, and a zoom change is the cheapest
|
|
52
|
+
// signal that one happened.
|
|
53
|
+
return `${q.x.toFixed(6)},${q.y.toFixed(6)},${q.z.toFixed(6)},${q.w.toFixed(6)},${cam.isOrthographicCamera ? cam.zoom : 0}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function redraw() {
|
|
57
|
+
if (hidden) return;
|
|
58
|
+
const cam = viewer.camera;
|
|
59
|
+
const q = cam.quaternion;
|
|
60
|
+
// outerPad reserves screen-pixel room for everything cube-canvas.js draws
|
|
61
|
+
// past the cube's model geometry: the fixed-size arrowhead, the gap
|
|
62
|
+
// beyond it, and the axis label glyph (drawn centred on its anchor, so it
|
|
63
|
+
// still sticks out a few px past that point — one font-size's worth of
|
|
64
|
+
// slack comfortably covers a single uppercase character at this size).
|
|
65
|
+
const outerPad = CUBE_RENDER.headLengthPx + CUBE_RENDER.labelGapPx + CUBE_RENDER.labelPx;
|
|
66
|
+
// downBias spends part of that same reservation on ONE side, so the cube
|
|
67
|
+
// sits lower in its box and reads closer to the viewbar (the other half of
|
|
68
|
+
// that change is chrome.css's stack offset). Passed rather than defaulted
|
|
69
|
+
// inside projectCube because the two knobs are one budget: the bias is only
|
|
70
|
+
// safe against the pad this call just reserved. Hit-testing rides along for
|
|
71
|
+
// free — it reads the same projection.
|
|
72
|
+
projected = projectCube([q.x, q.y, q.z, q.w], {
|
|
73
|
+
size: canvas.size,
|
|
74
|
+
outerPad,
|
|
75
|
+
downBias: CUBE_DOWN_BIAS_PX,
|
|
76
|
+
});
|
|
77
|
+
canvas.draw(projected, { hover });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onFrame() {
|
|
81
|
+
if (hidden) return;
|
|
82
|
+
const key = cameraKey();
|
|
83
|
+
if (key === lastKey) return;
|
|
84
|
+
lastKey = key;
|
|
85
|
+
redraw();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- pointer ---------------------------------------------------------------
|
|
89
|
+
let press = null; // { x, y, dragging, id }
|
|
90
|
+
|
|
91
|
+
const localPoint = (event) => {
|
|
92
|
+
const rect = canvas.element.getBoundingClientRect();
|
|
93
|
+
return [event.clientX - rect.left, event.clientY - rect.top];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onPointerDown = (event) => {
|
|
97
|
+
if (event.isPrimary === false || hidden) return;
|
|
98
|
+
press = { x: event.clientX, y: event.clientY, dragging: false, id: event.pointerId };
|
|
99
|
+
canvas.element.setPointerCapture?.(event.pointerId);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onPointerMove = (event) => {
|
|
103
|
+
if (event.isPrimary === false || hidden) return;
|
|
104
|
+
if (press) {
|
|
105
|
+
const dx = event.clientX - press.x;
|
|
106
|
+
const dy = event.clientY - press.y;
|
|
107
|
+
if (!press.dragging && Math.hypot(dx, dy) < dragThreshold) return;
|
|
108
|
+
press.dragging = true;
|
|
109
|
+
press.x = event.clientX;
|
|
110
|
+
press.y = event.clientY;
|
|
111
|
+
// Hover is meaningless mid-drag and would flicker as the cube spins.
|
|
112
|
+
if (hover !== null) hover = null;
|
|
113
|
+
viewer.orbitBy(dx, dy);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const next = hitRegion(...localPoint(event), projected);
|
|
117
|
+
if (next === hover) return;
|
|
118
|
+
hover = next;
|
|
119
|
+
redraw();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const onPointerUp = (event) => {
|
|
123
|
+
if (!press || event.isPrimary === false) return;
|
|
124
|
+
const wasDrag = press.dragging;
|
|
125
|
+
canvas.element.releasePointerCapture?.(press.id);
|
|
126
|
+
press = null;
|
|
127
|
+
if (wasDrag) return;
|
|
128
|
+
const id = hitRegion(...localPoint(event), projected);
|
|
129
|
+
// refit: clicking a region is now the only reframe control the framework's
|
|
130
|
+
// own pages ship, so it has to actually refit — under orthographic a tween
|
|
131
|
+
// alone changes the angle and leaves the user's dolly in place. See
|
|
132
|
+
// viewer.js's tweenCameraTo.
|
|
133
|
+
if (id) viewer.tweenCameraTo(id, { duration: 0.6, refit: true });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const onPointerLeave = () => {
|
|
137
|
+
if (press || hover === null) return;
|
|
138
|
+
hover = null;
|
|
139
|
+
redraw();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
canvas.element.addEventListener("pointerdown", onPointerDown);
|
|
143
|
+
canvas.element.addEventListener("pointermove", onPointerMove);
|
|
144
|
+
canvas.element.addEventListener("pointerup", onPointerUp);
|
|
145
|
+
canvas.element.addEventListener("pointercancel", onPointerUp);
|
|
146
|
+
canvas.element.addEventListener("pointerleave", onPointerLeave);
|
|
147
|
+
|
|
148
|
+
const offFrame = viewer.onFrame(onFrame);
|
|
149
|
+
const offTheme = viewer.onThemeChange((mode) => {
|
|
150
|
+
canvas.setTheme(mode);
|
|
151
|
+
redraw();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const onNarrowChange = () => {
|
|
155
|
+
canvas.setSize(sizeForViewport());
|
|
156
|
+
// The projection itself is size-dependent (the scale), so the dirty-check
|
|
157
|
+
// key alone won't force a real reproject here — clear it and redraw.
|
|
158
|
+
lastKey = null;
|
|
159
|
+
redraw();
|
|
160
|
+
};
|
|
161
|
+
narrowQuery?.addEventListener?.("change", onNarrowChange);
|
|
162
|
+
|
|
163
|
+
lastKey = cameraKey();
|
|
164
|
+
redraw(); // never show a blank box before the first camera movement
|
|
165
|
+
|
|
166
|
+
function setHidden(flag) {
|
|
167
|
+
const next = !!flag;
|
|
168
|
+
if (next === hidden) return;
|
|
169
|
+
hidden = next;
|
|
170
|
+
wrap.hidden = hidden;
|
|
171
|
+
if (!hidden) {
|
|
172
|
+
// The camera almost certainly moved while we were away, and the dirty
|
|
173
|
+
// check would otherwise hold the stale drawing until it moves again.
|
|
174
|
+
lastKey = cameraKey();
|
|
175
|
+
redraw();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let detached = false;
|
|
180
|
+
return {
|
|
181
|
+
element: wrap,
|
|
182
|
+
setHidden,
|
|
183
|
+
isHidden: () => hidden,
|
|
184
|
+
detach() {
|
|
185
|
+
if (detached) return;
|
|
186
|
+
detached = true;
|
|
187
|
+
runCleanupSteps([
|
|
188
|
+
offFrame,
|
|
189
|
+
offTheme,
|
|
190
|
+
() => narrowQuery?.removeEventListener?.("change", onNarrowChange),
|
|
191
|
+
() => canvas.element.removeEventListener("pointerdown", onPointerDown),
|
|
192
|
+
() => canvas.element.removeEventListener("pointermove", onPointerMove),
|
|
193
|
+
() => canvas.element.removeEventListener("pointerup", onPointerUp),
|
|
194
|
+
() => canvas.element.removeEventListener("pointercancel", onPointerUp),
|
|
195
|
+
() => canvas.element.removeEventListener("pointerleave", onPointerLeave),
|
|
196
|
+
() => canvas.dispose(),
|
|
197
|
+
() => wrap.remove(),
|
|
198
|
+
], "viewcube mode cleanup failed");
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|