partforge 0.52.0 → 0.54.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 +20 -5
- package/package.json +1 -1
- package/src/framework/app.css +27 -2
- package/src/framework/cutaway-controls.js +4 -27
- package/src/framework/cutaway-gizmo-scene.js +3 -1
- package/src/framework/measure/dim3-place.js +455 -0
- package/src/framework/measure/dim3-scene.js +438 -0
- package/src/framework/measure/feature-dims.js +258 -0
- package/src/framework/measure/measure-controls.js +110 -0
- package/src/framework/measure/measure-mode.js +546 -0
- package/src/framework/measure/param-link.js +32 -0
- package/src/framework/measure/pins.js +36 -0
- package/src/framework/mount.js +61 -6
- package/src/framework/panel/render.js +41 -0
- package/src/framework/selection/drag-tracker.js +39 -0
- package/src/framework/selection/feature-highlight.js +99 -0
- package/src/framework/selection/hover.js +14 -90
- package/src/framework/selection/index.js +1 -0
- package/src/framework/selection/pick.js +11 -37
- package/src/framework/teardown.js +29 -0
- package/src/framework/viewer.js +65 -5
- package/types/index.d.ts +16 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Shared teardown helpers: run a list of cleanup steps in isolation (one
|
|
2
|
+
// step's throw must never skip the others), and capture/restore a DOM
|
|
3
|
+
// element's attributes across an attach/detach cycle. Lifted out of
|
|
4
|
+
// cutaway-controls.js (the original) so measure-controls.js and
|
|
5
|
+
// selection/hover.js — and anything else that wraps host DOM — share one
|
|
6
|
+
// implementation instead of drifting copies.
|
|
7
|
+
|
|
8
|
+
export function runCleanupSteps(steps, message) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
for (const step of steps) {
|
|
11
|
+
try { step(); } catch (error) { errors.push(error); }
|
|
12
|
+
}
|
|
13
|
+
if (errors.length === 1) throw errors[0];
|
|
14
|
+
if (errors.length > 1) throw new AggregateError(errors, message);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function captureAttributes(element, names) {
|
|
18
|
+
return new Map(names.map((name) => [name, {
|
|
19
|
+
present: element.hasAttribute(name),
|
|
20
|
+
value: element.getAttribute(name),
|
|
21
|
+
}]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function restoreAttributes(element, attributes) {
|
|
25
|
+
for (const [name, { present, value }] of attributes) {
|
|
26
|
+
if (present) element.setAttribute(name, value);
|
|
27
|
+
else element.removeAttribute(name);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/framework/viewer.js
CHANGED
|
@@ -39,13 +39,15 @@ export function srgbEncodeInPlace(data) {
|
|
|
39
39
|
// `renderer.renderOffscreen(pose)` does the GL work (temp camera → offscreen
|
|
40
40
|
// target → readback → JPEG data URL); injected so this is unit-testable without
|
|
41
41
|
// a GL context. The grid is hidden for the whole synchronous pass and restored.
|
|
42
|
-
export function captureViewsFromScene(viewNames, { renderer, liveCamera, grid, bounds }) {
|
|
42
|
+
export function captureViewsFromScene(viewNames, { renderer, liveCamera, grid, bounds, hidden = [] }) {
|
|
43
43
|
const views = (viewNames?.length ? viewNames : ["iso", "front", "top"])
|
|
44
44
|
.filter((v) => CANONICAL_VIEWS.includes(v))
|
|
45
45
|
.slice(0, CANONICAL_VIEWS.length);
|
|
46
46
|
const before = liveCamera.position.clone();
|
|
47
47
|
const gridWasVisible = grid?.visible;
|
|
48
48
|
if (grid) grid.visible = false;
|
|
49
|
+
const hiddenWas = hidden.map((o) => o.visible);
|
|
50
|
+
for (const o of hidden) o.visible = false;
|
|
49
51
|
try {
|
|
50
52
|
return views.map((view) => ({
|
|
51
53
|
view,
|
|
@@ -53,10 +55,41 @@ export function captureViewsFromScene(viewNames, { renderer, liveCamera, grid, b
|
|
|
53
55
|
}));
|
|
54
56
|
} finally {
|
|
55
57
|
if (grid) grid.visible = gridWasVisible;
|
|
58
|
+
hidden.forEach((o, i) => { o.visible = hiddenWas[i]; });
|
|
56
59
|
liveCamera.position.copy(before); // belt-and-suspenders: never leak camera state
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
// The off-loop thumbnail capture (renderMeshPayloads, behind the handle's
|
|
64
|
+
// captureView) renders a THROWAWAY scene, so it gets no background from the
|
|
65
|
+
// live scene's theme — and before this constant existed it set none at all,
|
|
66
|
+
// which meant every thumbnail came back on the renderer's default opaque
|
|
67
|
+
// black, in light mode as much as dark. One deliberately theme-INDEPENDENT
|
|
68
|
+
// colour is the right answer rather than either THEME entry below: a thumbnail
|
|
69
|
+
// is baked at capture time and displayed later under host chrome this renderer
|
|
70
|
+
// cannot know (partforge-cloud's card grid draws them on both). Near the
|
|
71
|
+
// perceptual midpoint of THEME.light.bg / THEME.dark.bg, so it commits to
|
|
72
|
+
// neither, and clear of both the part material (0x9fb4cc, lighter) and the
|
|
73
|
+
// feature-edge lines (0x1c232d, much darker).
|
|
74
|
+
//
|
|
75
|
+
// The near-ZERO chroma is the part that looks arbitrary and isn't: the default
|
|
76
|
+
// part material is blue-grey, so a blue-grey background of the same value
|
|
77
|
+
// (0x6b7280 was the first try) competes with it and the shaded side of a part
|
|
78
|
+
// half-disappears into the plate. A neutral grey separates by hue as well as
|
|
79
|
+
// value. Judged on real captures of demo.js and hinged-box.js — if this is
|
|
80
|
+
// ever retuned, retune it the same way and not by eye on the hex.
|
|
81
|
+
export const THUMBNAIL_BG = 0x6e6e73;
|
|
82
|
+
|
|
83
|
+
// Resolve renderMeshPayloads' `background` option to what Scene.background
|
|
84
|
+
// wants. Exported for its own sake: renderMeshPayloads needs a GL context and
|
|
85
|
+
// so is untestable directly, and this is the whole of the decision. `null` is
|
|
86
|
+
// a real escape hatch — the pre-existing no-background behaviour, clearing to
|
|
87
|
+
// the renderer's clear colour — so it is passed through rather than treated as
|
|
88
|
+
// "unset"; only `undefined` (an absent option) takes the default.
|
|
89
|
+
export function thumbnailBackground(background = THUMBNAIL_BG) {
|
|
90
|
+
return background === null ? null : new THREE.Color(background);
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
// Render the LIVE camera's current framing offscreen, once, at a caller-chosen
|
|
61
94
|
// resolution — the showcase capture behind the runtime handle's captureCurrent.
|
|
62
95
|
// Same injected-renderer split as captureViewsFromScene so it runs without a GL
|
|
@@ -109,6 +142,10 @@ export function createViewer(container, part) {
|
|
|
109
142
|
};
|
|
110
143
|
scene.background = new THREE.Color(THEME.dark.bg);
|
|
111
144
|
|
|
145
|
+
let currentTheme = "dark";
|
|
146
|
+
const themeListeners = new Set();
|
|
147
|
+
function onThemeChange(cb) { themeListeners.add(cb); return () => themeListeners.delete(cb); }
|
|
148
|
+
|
|
112
149
|
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
|
|
113
150
|
camera.position.set(18, 12, 18);
|
|
114
151
|
|
|
@@ -506,6 +543,8 @@ export function createViewer(container, part) {
|
|
|
506
543
|
for (const m of fadeLineMats.values()) m.color.set(t.line); // clones follow the theme
|
|
507
544
|
cutaway.setTheme(mode, t.line);
|
|
508
545
|
reassertLiveFades(); // setTheme re-clones every section's materials and reassigns them
|
|
546
|
+
currentTheme = THEME[mode] ? mode : "dark";
|
|
547
|
+
for (const cb of [...themeListeners]) cb(currentTheme);
|
|
509
548
|
}
|
|
510
549
|
|
|
511
550
|
function hideAssembly() {
|
|
@@ -610,6 +649,15 @@ export function createViewer(container, part) {
|
|
|
610
649
|
return canvas.toDataURL("image/jpeg", quality);
|
|
611
650
|
}
|
|
612
651
|
|
|
652
|
+
// Objects excluded from CANONICAL captures only (agent renders must stay
|
|
653
|
+
// dimension-free); captureCurrent — the user-framed showcase capture —
|
|
654
|
+
// deliberately does NOT consult this set.
|
|
655
|
+
const canonicalCaptureHidden = new Set();
|
|
656
|
+
function registerCanonicalCaptureHidden(obj) {
|
|
657
|
+
canonicalCaptureHidden.add(obj);
|
|
658
|
+
return () => canonicalCaptureHidden.delete(obj);
|
|
659
|
+
}
|
|
660
|
+
|
|
613
661
|
// Render the canonical camera angles offscreen, framed to whatever is visible,
|
|
614
662
|
// without disturbing the user's live view. Returns [{ view, dataUrl }].
|
|
615
663
|
function captureCanonicalViews(viewNames) {
|
|
@@ -623,6 +671,7 @@ export function createViewer(container, part) {
|
|
|
623
671
|
renderer: { renderOffscreen },
|
|
624
672
|
liveCamera: camera,
|
|
625
673
|
grid,
|
|
674
|
+
hidden: [...canonicalCaptureHidden],
|
|
626
675
|
bounds: { center, radius },
|
|
627
676
|
});
|
|
628
677
|
}
|
|
@@ -646,12 +695,18 @@ export function createViewer(container, part) {
|
|
|
646
695
|
// Offscreen render of an arbitrary mesh set (a non-active view), for thumbnails.
|
|
647
696
|
// Assembles a THROWAWAY scene mirroring the live pivot convention, frames it from a
|
|
648
697
|
// canonical angle, renders through the parameterized renderOffscreen, and disposes
|
|
649
|
-
// everything. Never touches the live scene, camera, subMesh, or subCache.
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
|
|
698
|
+
// everything. Never touches the live scene, camera, subMesh, or subCache. The scene
|
|
699
|
+
// gets THUMBNAIL_BG unless `background` says otherwise (`null` = no background, the
|
|
700
|
+
// renderer's clear colour). `payloads` is the worker's [{name, positions, normals,
|
|
701
|
+
// indices, …}] array — placement is already baked into shared-frame coords, so
|
|
702
|
+
// meshes are NOT recentred.
|
|
703
|
+
function renderMeshPayloads(payloads, { angle = "iso", size = 640, quality = 0.8, background } = {}) {
|
|
653
704
|
if (disposed) return null; // same guard as captureCurrent/captureCanonicalViews — never touch a torn-down renderer
|
|
654
705
|
const tmpScene = new THREE.Scene();
|
|
706
|
+
// Deliberately the throwaway scene's own background, never the live one's:
|
|
707
|
+
// this must not follow the viewer theme (see THUMBNAIL_BG) and must not
|
|
708
|
+
// reach the live-scene captures, which correctly do follow it.
|
|
709
|
+
tmpScene.background = thumbnailBackground(background);
|
|
655
710
|
const tmpPivot = new THREE.Group();
|
|
656
711
|
tmpPivot.rotation.x = -Math.PI / 2; // model Z (CAD up) -> vertical, same as live pivot
|
|
657
712
|
tmpScene.add(tmpPivot);
|
|
@@ -841,6 +896,8 @@ export function createViewer(container, part) {
|
|
|
841
896
|
controls.removeEventListener("start", onControlsStart);
|
|
842
897
|
cameraStartListeners.clear();
|
|
843
898
|
frameListeners.clear();
|
|
899
|
+
themeListeners.clear();
|
|
900
|
+
canonicalCaptureHidden.clear();
|
|
844
901
|
camTween.cancel();
|
|
845
902
|
controls.dispose();
|
|
846
903
|
for (const t of flashTimers) clearTimeout(t);
|
|
@@ -894,6 +951,8 @@ export function createViewer(container, part) {
|
|
|
894
951
|
setActive,
|
|
895
952
|
onContextLost,
|
|
896
953
|
setTheme,
|
|
954
|
+
onThemeChange,
|
|
955
|
+
getTheme: () => currentTheme,
|
|
897
956
|
getCameraState,
|
|
898
957
|
setCameraState,
|
|
899
958
|
onCameraEnd,
|
|
@@ -910,6 +969,7 @@ export function createViewer(container, part) {
|
|
|
910
969
|
resetCutaway: cutaway.reset,
|
|
911
970
|
isWorldPointVisible: cutaway.isPointVisible,
|
|
912
971
|
registerCutawayMaterial: cutaway.registerClippableMaterial,
|
|
972
|
+
registerCanonicalCaptureHidden,
|
|
913
973
|
onCutawayHandleHover: cutaway.onHandleHoverChange,
|
|
914
974
|
dispose,
|
|
915
975
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface MountElements {
|
|
|
87
87
|
reframe?: HTMLElement | null;
|
|
88
88
|
theme?: HTMLElement | null;
|
|
89
89
|
cutaway?: HTMLElement | null;
|
|
90
|
+
measure?: HTMLElement | null;
|
|
90
91
|
railToggle?: HTMLElement | null;
|
|
91
92
|
};
|
|
92
93
|
}
|
|
@@ -142,6 +143,19 @@ export interface CaptureViewOptions {
|
|
|
142
143
|
angle?: CanonicalView | string;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Measurement mode runtime controls. Dimensioned captures come straight from
|
|
148
|
+
* `captureCurrent()` while the mode is enabled — in-scene dims render into
|
|
149
|
+
* the frame natively (canonical-view captures and thumbnails never include
|
|
150
|
+
* them).
|
|
151
|
+
*/
|
|
152
|
+
export interface MeasureRuntime {
|
|
153
|
+
isEnabled(): boolean;
|
|
154
|
+
setEnabled(on: boolean): void;
|
|
155
|
+
clearPins(): void;
|
|
156
|
+
pinCount(): number;
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
/** Where playback is: idle, swinging the camera to an intro cue, playing, or paused. */
|
|
146
160
|
export type AnimationStatus = "idle" | "intro" | "playing" | "paused";
|
|
147
161
|
|
|
@@ -253,6 +267,8 @@ export interface PartRuntime {
|
|
|
253
267
|
* active view has none, where `state().animation` reads `null`.
|
|
254
268
|
*/
|
|
255
269
|
animation: AnimationRuntime | null;
|
|
270
|
+
/** Measurement mode's runtime-controls API — mode on/off, unit, and pin state; dimensioned captures come from `captureCurrent()` while enabled. Always present (a no-op stand-in outside `makeHandle` tests). */
|
|
271
|
+
measure: MeasureRuntime;
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
/** Mount a full parametric-part app from a `PartDefinition`. */
|