partforge 0.124.0 → 0.126.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 +5 -1
- package/docs/AUTHORING-PARTS.md +16 -6
- package/package.json +1 -1
- package/src/framework/app.css +4 -0
- package/src/framework/cutaway.js +33 -0
- package/src/framework/geometry/threemf.js +3 -2
- package/src/framework/lint/rules-materials.js +3 -3
- package/src/framework/materials/contact-shadow.js +28 -1
- package/src/framework/materials/presets.js +3 -6
- package/src/framework/materials/resolve.js +38 -14
- package/src/framework/mount.js +33 -10
- package/src/framework/selection/hover.js +10 -10
- package/src/framework/tooltip.js +5 -1
- package/src/framework/view-style-controls.js +11 -0
- package/src/framework/view-style-state.js +2 -2
- package/src/framework/viewer.js +111 -4
- package/src/parts/material-swatches.js +8 -1
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@ const runtime = mount(part, {
|
|
|
108
108
|
},
|
|
109
109
|
onBuild: ({ status, ms, error }) => {}, // per accepted build: "success" | "error"
|
|
110
110
|
onPick: ({ selection, label, prompt, token }) => {}, // programmatic click-to-select
|
|
111
|
+
pickHint: "Click to edit", // hover-tooltip nudge while onPick is set (null = none)
|
|
111
112
|
});
|
|
112
113
|
await runtime.ready; // first successful build (rejects on a first-build error)
|
|
113
114
|
runtime.setHostPane("rail"); // narrow layout only: show just the controls
|
|
@@ -221,7 +222,10 @@ full contract.
|
|
|
221
222
|
back to the sub-part label/name) for compact UI, `prompt` is the LLM-ready
|
|
222
223
|
sentence, `token` the compact form, `selection` the raw object. When `onPick` is
|
|
223
224
|
set, the `?pick` / `?pickserver` URL modes are ignored (one click listener ever
|
|
224
|
-
live); hover labels stay always-on
|
|
225
|
+
live); hover labels stay always-on, and gain a `pickHint` line under the name
|
|
226
|
+
("Click to edit" by default; pass your own wording, or `null` for none). Hover
|
|
227
|
+
never tints the surface under the pointer — a per-face highlight read as "only
|
|
228
|
+
this spot is clickable".
|
|
225
229
|
|
|
226
230
|
## Authoring guide
|
|
227
231
|
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -309,8 +309,14 @@ parts: {
|
|
|
309
309
|
meta: { title: "…", units: "mm", environment: "studio" },
|
|
310
310
|
```
|
|
311
311
|
|
|
312
|
-
- `material` — a preset id from the table below. Without one
|
|
313
|
-
the
|
|
312
|
+
- `material` — a preset id from the table below. Without one (or with one the
|
|
313
|
+
library does not know) the sub-part keeps the CAD view's look — its `color`,
|
|
314
|
+
else the viewer's blue-grey — and **realistic mode shows it as a PLA print**
|
|
315
|
+
(`pla-print`'s finish and layer lines) in that same colour. So an untouched
|
|
316
|
+
part looks printed in realistic mode; name a material when it is made some
|
|
317
|
+
other way. The PLA look is realistic-only: the CAD view, 3MF colours and
|
|
318
|
+
`declaresMaterials` are unaffected, and a part that names no material is not
|
|
319
|
+
treated as declaring one.
|
|
314
320
|
- `color` — the base colour (`0xRRGGBB`). With a preset it is the TINT. Presets
|
|
315
321
|
marked tintable are normally coloured this way (anodizing, plastic, paint);
|
|
316
322
|
the others have an intrinsic colour (brass) but still accept one.
|
|
@@ -2740,7 +2746,10 @@ pane's pixel size:
|
|
|
2740
2746
|
target are restored after the render. Measurement-mode dimensions render directly
|
|
2741
2747
|
in the scene, so a dimensioned capture needs no special handling — enable measure
|
|
2742
2748
|
mode (`runtime.measure.setEnabled(true)`) and call `captureCurrent()`; the dims are
|
|
2743
|
-
just part of the rendered frame.
|
|
2749
|
+
just part of the rendered frame. With the cutaway on, the capture shows the section
|
|
2750
|
+
the way the user sees it — the part clipped, its cut faces hatched — but never the
|
|
2751
|
+
cutaway's translucent plane or its handles, which are controls, not the part (the
|
|
2752
|
+
same holds for `captureViews` and the view style popover's thumbnails).
|
|
2744
2753
|
`recenter: true` centres the part: the capture becomes the largest centred
|
|
2745
2754
|
sub-window of the current framing that still holds every visible vertex (equal
|
|
2746
2755
|
margins on both axes, rendered at the full `size` resolution through a view
|
|
@@ -2815,8 +2824,8 @@ addition to) the generated view style button above:
|
|
|
2815
2824
|
revert too.
|
|
2816
2825
|
- `runtime.declaresMaterials` — `true` when any sub-part names a
|
|
2817
2826
|
`display.material`. A part with none still supports realistic mode (every
|
|
2818
|
-
sub-part
|
|
2819
|
-
|
|
2827
|
+
sub-part renders as a PLA print in its CAD colour), so use this to decide
|
|
2828
|
+
whether to surface your own realistic control at all, not whether it works.
|
|
2820
2829
|
Feature lines are not a preference: they draw whenever `runtime.renderMode`
|
|
2821
2830
|
reads `"cad"` and never while it reads `"realistic"` — there is no switch
|
|
2822
2831
|
to drive independently of it.
|
|
@@ -3302,7 +3311,8 @@ nothing either way and stays silent, matching `animation-track-rebuilds`'s own
|
|
|
3302
3311
|
trust handling.
|
|
3303
3312
|
|
|
3304
3313
|
**Appearance** (all warnings) — `unknown-material` (a `display.material` the
|
|
3305
|
-
library does not know; the viewer
|
|
3314
|
+
library does not know; the viewer draws it as if it named none — a PLA print
|
|
3315
|
+
in realistic mode), `unknown-environment`
|
|
3306
3316
|
(`meta.environment` not known; realistic mode uses `studio`),
|
|
3307
3317
|
`material-key-unknown` (a `display` key that is not colour, opacity, material
|
|
3308
3318
|
or one of the six overrides; ignored), `material-override-clamped` (an override
|
package/package.json
CHANGED
package/src/framework/app.css
CHANGED
|
@@ -1039,6 +1039,10 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
1039
1039
|
.pf-hover-tip .pf-hover-sub { color: var(--pf-muted); font-size: 11px; margin-left: 7px; }
|
|
1040
1040
|
#pf-hover-tip .pf-hover-sub:empty,
|
|
1041
1041
|
.pf-hover-tip .pf-hover-sub:empty { display: none; }
|
|
1042
|
+
#pf-hover-tip .pf-hover-hint,
|
|
1043
|
+
.pf-hover-tip .pf-hover-hint { color: var(--pf-muted); font-size: 10.5px; margin-top: 1px; }
|
|
1044
|
+
#pf-hover-tip .pf-hover-hint:empty,
|
|
1045
|
+
.pf-hover-tip .pf-hover-hint:empty { display: none; }
|
|
1042
1046
|
|
|
1043
1047
|
/* request-a-pick: agent prompt banner, floated top-centre well below the part tabs,
|
|
1044
1048
|
laid out like a chat message (avatar + text). Slides/fades in when shown; themed. */
|
package/src/framework/cutaway.js
CHANGED
|
@@ -144,6 +144,29 @@ export function createCutaway({
|
|
|
144
144
|
handleHoverSubscribers.clear();
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// "What the section looks like changed": the mode went on or off, or the
|
|
148
|
+
// plane moved (a drag, flip, reset or restore). For anything that caches a
|
|
149
|
+
// picture of the part — the view style popover's thumbnails — which would
|
|
150
|
+
// otherwise keep showing a section the user has since moved or closed.
|
|
151
|
+
// Fires often during a drag, so a listener must be cheap; no argument, since
|
|
152
|
+
// every caller only needs to know that it happened. A throwing listener is
|
|
153
|
+
// reported, never allowed to interrupt the controller work that fired it.
|
|
154
|
+
const changeListeners = new Set();
|
|
155
|
+
function notifyChange() {
|
|
156
|
+
for (const listener of [...changeListeners]) {
|
|
157
|
+
try {
|
|
158
|
+
listener();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
try { console.error("Cutaway change listener failed", error); } catch { /* reporting only */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function onChange(listener) {
|
|
165
|
+
if (disposed || typeof listener !== "function") return () => {};
|
|
166
|
+
changeListeners.add(listener);
|
|
167
|
+
return () => changeListeners.delete(listener);
|
|
168
|
+
}
|
|
169
|
+
|
|
147
170
|
function selected(name) {
|
|
148
171
|
return selectedNames == null || selectedNames.has(name);
|
|
149
172
|
}
|
|
@@ -213,6 +236,7 @@ export function createCutaway({
|
|
|
213
236
|
gizmo.setFlipped(flipped);
|
|
214
237
|
gizmo.setPose(pose);
|
|
215
238
|
if (activeAppearance) showActive();
|
|
239
|
+
notifyChange();
|
|
216
240
|
}
|
|
217
241
|
|
|
218
242
|
function onPoseChange(nextPose) {
|
|
@@ -314,6 +338,7 @@ export function createCutaway({
|
|
|
314
338
|
} finally {
|
|
315
339
|
disabling = false;
|
|
316
340
|
}
|
|
341
|
+
if (wasEnabled) notifyChange();
|
|
317
342
|
if (firstError) throw firstError;
|
|
318
343
|
return true;
|
|
319
344
|
}
|
|
@@ -340,6 +365,7 @@ export function createCutaway({
|
|
|
340
365
|
gizmo.setVisible(true);
|
|
341
366
|
gizmo.updateForCamera();
|
|
342
367
|
showActive();
|
|
368
|
+
notifyChange(); // applyPose above fired too, but before `enabled` meant on
|
|
343
369
|
return true;
|
|
344
370
|
}
|
|
345
371
|
|
|
@@ -360,6 +386,7 @@ export function createCutaway({
|
|
|
360
386
|
planeFromPose(plane, planeNormal, pose.position, pose.quaternion, flipped);
|
|
361
387
|
gizmo.setFlipped(flipped);
|
|
362
388
|
showActive();
|
|
389
|
+
notifyChange();
|
|
363
390
|
return true;
|
|
364
391
|
}
|
|
365
392
|
|
|
@@ -579,6 +606,7 @@ export function createCutaway({
|
|
|
579
606
|
publishHandleHover(null);
|
|
580
607
|
drainHandleHoverPublications();
|
|
581
608
|
clearHandleHoverSubscribers();
|
|
609
|
+
changeListeners.clear();
|
|
582
610
|
attempt(() => overlayScene.clear());
|
|
583
611
|
attempt(() => capGeometry.dispose());
|
|
584
612
|
if (firstError) throw firstError;
|
|
@@ -605,6 +633,11 @@ export function createCutaway({
|
|
|
605
633
|
updateForCamera,
|
|
606
634
|
renderOverlay,
|
|
607
635
|
onHandleHoverChange,
|
|
636
|
+
onChange,
|
|
637
|
+
// The gizmo's in-scene half (the ghost plane and its border). It is
|
|
638
|
+
// interactive UI drawn in the part's own scene, so the viewer keeps it out
|
|
639
|
+
// of every offscreen render (viewer.js's captureHidden).
|
|
640
|
+
captureExcluded: [gizmo.group],
|
|
608
641
|
setCamera(next) {
|
|
609
642
|
if (!next) return;
|
|
610
643
|
camera = next;
|
|
@@ -69,8 +69,9 @@ function weld(positions, indices) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Default for an uncoloured object in an export where some other object IS
|
|
72
|
-
// coloured: the viewer's own
|
|
73
|
-
// literal so this module keeps its
|
|
72
|
+
// coloured: the viewer's own no-material colour (resolve.js
|
|
73
|
+
// NO_MATERIAL_COLOR). Written as a literal so this module keeps its
|
|
74
|
+
// fflate-only import closure; viewer-materials.test.js pins the copies equal.
|
|
74
75
|
const DEFAULT_DISPLAY_COLOR = 0x9fb4cc;
|
|
75
76
|
// Base-material group id: outside the object id range (objects are 1..n).
|
|
76
77
|
const BASE_MATERIALS_ID = 1000;
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// block geometry, and the viewer already falls back (resolve.js). Pure — imports
|
|
4
4
|
// only the three-free materials data (test/lint-purity.test.js).
|
|
5
5
|
import { warn } from "./finding.js";
|
|
6
|
-
import { PRESETS
|
|
6
|
+
import { PRESETS } from "../materials/presets.js";
|
|
7
7
|
import { ENVIRONMENTS } from "../materials/environments.js";
|
|
8
8
|
import { resolveMaterial, resolveEnvironmentId, OVERRIDE_RANGES } from "../materials/resolve.js";
|
|
9
9
|
import { suggest } from "../geometry/op-options.js";
|
|
10
10
|
|
|
11
|
-
const listedPresets = () => Object.keys(PRESETS)
|
|
11
|
+
const listedPresets = () => Object.keys(PRESETS);
|
|
12
12
|
const subParts = (part) => Object.entries(part?.parts ?? {}).filter(([, sp]) => sp && typeof sp === "object");
|
|
13
13
|
const issuesOf = (part, kind) => subParts(part).flatMap(([name, sp]) =>
|
|
14
14
|
resolveMaterial(sp.display).issues.filter((i) => i.kind === kind).map((i) => ({ name, ...i })));
|
|
@@ -20,7 +20,7 @@ export const MATERIAL_RULES = [
|
|
|
20
20
|
const near = typeof value === "string" ? suggest(value, listedPresets()) : null;
|
|
21
21
|
return warn("unknown-material",
|
|
22
22
|
`sub-part "${name}" names material ${JSON.stringify(value)}, which is not in the library`,
|
|
23
|
-
`${near ? `Did you mean "${near}"? ` : ""}The viewer
|
|
23
|
+
`${near ? `Did you mean "${near}"? ` : ""}The viewer draws it as if it named no material instead (a PLA print in realistic mode). Known materials: ${listedPresets().join(", ")}.`,
|
|
24
24
|
`parts.${name}.display.material`);
|
|
25
25
|
}),
|
|
26
26
|
},
|
|
@@ -110,7 +110,12 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
110
110
|
// While a part moves (lowRes) only the soft layer is redrawn and the tight
|
|
111
111
|
// one is hidden: an outline left where the part WAS would be wrong, and the
|
|
112
112
|
// settle render that follows brings it back.
|
|
113
|
-
|
|
113
|
+
//
|
|
114
|
+
// `clippingPlanes` (world space) clip the casters the way the cutaway clips
|
|
115
|
+
// what is on screen. The depth pass draws every caster with the one override
|
|
116
|
+
// material, which carries none of the casters' own clipping planes, so
|
|
117
|
+
// without this the half the cutaway removed still cast its shadow.
|
|
118
|
+
function render(scene, casters, { lowRes = false, clippingPlanes = null } = {}) {
|
|
114
119
|
const bg = scene.background;
|
|
115
120
|
const overrideBefore = scene.overrideMaterial;
|
|
116
121
|
const clear = renderer.getClearAlpha();
|
|
@@ -123,6 +128,7 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
123
128
|
renderer.setClearAlpha(0);
|
|
124
129
|
for (const l of layers) {
|
|
125
130
|
if (lowRes && l.spec.name === "tight") continue;
|
|
131
|
+
setClipping(l.depthMaterial, clippingPlanes);
|
|
126
132
|
scene.overrideMaterial = l.depthMaterial;
|
|
127
133
|
renderer.setRenderTarget(l.rt);
|
|
128
134
|
renderer.render(scene, cam);
|
|
@@ -143,6 +149,27 @@ export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
// A new plane count needs a new program; the same planes moved do not (three
|
|
153
|
+
// reads their values every frame).
|
|
154
|
+
//
|
|
155
|
+
// A clipped pass also draws BOTH sides, depth-tested. The pass looks UP at
|
|
156
|
+
// the casters, and a cut that removes a part's underside (a kept side facing
|
|
157
|
+
// up, e.g. a top-view cut after Flip) leaves no downward-facing faces at the
|
|
158
|
+
// cut — from below only back faces remain, which FrontSide culls, and the
|
|
159
|
+
// shadow shrinks to a faint ring of side walls. The interior back faces fill
|
|
160
|
+
// the silhouette instead, and the depth test keeps the LOWEST surface under
|
|
161
|
+
// each texel (the one the ground's darkness is measured from) rather than
|
|
162
|
+
// whichever face drew last. Unclipped, the pass is exactly as before:
|
|
163
|
+
// front faces, no depth test.
|
|
164
|
+
function setClipping(material, planes) {
|
|
165
|
+
const next = planes?.length ? planes : null;
|
|
166
|
+
const side = next ? THREE.DoubleSide : THREE.FrontSide;
|
|
167
|
+
if ((material.clippingPlanes?.length ?? 0) !== (next?.length ?? 0) || material.side !== side) material.needsUpdate = true;
|
|
168
|
+
material.clippingPlanes = next;
|
|
169
|
+
material.side = side;
|
|
170
|
+
material.depthTest = material.depthWrite = !!next;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
return {
|
|
147
174
|
group,
|
|
148
175
|
render,
|
|
@@ -8,15 +8,12 @@
|
|
|
8
8
|
// converted to sRGB. Adding a preset is one record here plus a look at the
|
|
9
9
|
// contact sheet (materials.html) in every environment.
|
|
10
10
|
|
|
11
|
-
export const DEFAULT_PRESET_ID = "default";
|
|
12
|
-
|
|
13
11
|
const P = (id, label, category, use, fields) => ({ id, label, category, use, tintable: false, ...fields });
|
|
14
12
|
|
|
15
13
|
export const PRESETS = {
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
{ color: 0x9fb4cc, metalness: 0.25, roughness: 0.55 }),
|
|
14
|
+
// Every record here is a documented, nameable material. A sub-part that names
|
|
15
|
+
// none is drawn by resolve.js (the blue-grey CAD look; a PLA print in that
|
|
16
|
+
// colour in realistic mode) — there is no hidden "default" record any more.
|
|
20
17
|
|
|
21
18
|
"machined-aluminum": P("machined-aluminum", "Machined aluminium", "aluminium", "Bare CNC-milled aluminium, fine tool marks, satin sheen.",
|
|
22
19
|
{ color: 0xf5f6f6, metalness: 1, roughness: 0.32 }),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/framework/materials/resolve.js
|
|
2
2
|
// Pure resolution of a sub-part's `display` block against the library. Never
|
|
3
|
-
// throws and never rejects a value: an unknown preset falls back to the
|
|
3
|
+
// throws and never rejects a value: an unknown preset falls back to the
|
|
4
|
+
// no-material look (below),
|
|
4
5
|
// an out-of-range override clamps, an unknown key is ignored — and every one of
|
|
5
6
|
// those is returned as an `issue` so lint can warn about it. Appearance must
|
|
6
7
|
// never be able to fail a build.
|
|
7
|
-
import { PRESETS
|
|
8
|
+
import { PRESETS } from "./presets.js";
|
|
8
9
|
import { ENVIRONMENTS, DEFAULT_ENVIRONMENT_ID } from "./environments.js";
|
|
9
10
|
|
|
10
11
|
export const OVERRIDE_RANGES = {
|
|
@@ -20,16 +21,34 @@ export const DISPLAY_KEYS = ["color", "opacity", "material", ...Object.keys(OVER
|
|
|
20
21
|
|
|
21
22
|
const isColor = (v) => Number.isInteger(v) && v >= 0 && v <= 0xffffff;
|
|
22
23
|
|
|
24
|
+
// A sub-part with no usable material — none named, or one the library does not
|
|
25
|
+
// know. The CAD view draws it in the viewer's neutral blue-grey drafting look
|
|
26
|
+
// (the look every part had before the library existed; viewer.js's base
|
|
27
|
+
// material and threemf.js's uncoloured-object colour are the same literal).
|
|
28
|
+
// Realistic mode draws it as a PLA print — the pla-print finish, layer lines
|
|
29
|
+
// and all — in that same colour, or the part's own `color`, so a part is the
|
|
30
|
+
// same colour in both views, just printed. `declaresMaterials` still counts
|
|
31
|
+
// only a NAMED material: this default never makes a part "declare" one.
|
|
32
|
+
export const NO_MATERIAL_COLOR = 0x9fb4cc;
|
|
33
|
+
export const NO_MATERIAL_PRESET_ID = "pla-print";
|
|
34
|
+
const NO_MATERIAL_CAD = { color: NO_MATERIAL_COLOR, metalness: 0.25, roughness: 0.55 };
|
|
35
|
+
|
|
36
|
+
const knownMaterial = (m) => typeof m === "string" && Object.hasOwn(PRESETS, m);
|
|
37
|
+
|
|
23
38
|
export function resolveMaterial(display) {
|
|
24
39
|
const d = display && typeof display === "object" ? display : {};
|
|
25
40
|
const issues = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
if (d.material != null && !knownMaterial(d.material)) issues.push({ kind: "unknown-material", key: "material", value: d.material });
|
|
42
|
+
const preset = knownMaterial(d.material) ? PRESETS[d.material] : PRESETS[NO_MATERIAL_PRESET_ID];
|
|
43
|
+
const baseColor = knownMaterial(d.material) ? preset.color : NO_MATERIAL_COLOR;
|
|
44
|
+
return { preset, ...applyDisplay(d, preset, baseColor, issues) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A preset record's numbers with the display block's colour, opacity and clamped
|
|
48
|
+
// overrides applied. Pushes onto `issues`; returns them with the params.
|
|
49
|
+
function applyDisplay(d, preset, baseColor, issues) {
|
|
31
50
|
const params = {
|
|
32
|
-
color:
|
|
51
|
+
color: baseColor,
|
|
33
52
|
metalness: preset.metalness,
|
|
34
53
|
roughness: preset.roughness,
|
|
35
54
|
clearcoat: preset.clearcoat ?? 0,
|
|
@@ -63,7 +82,7 @@ export function resolveMaterial(display) {
|
|
|
63
82
|
}
|
|
64
83
|
// `tinted`: the author named a colour. A textured preset's colour map carries
|
|
65
84
|
// its own colour, so only an explicit tint multiplies it (physical.js).
|
|
66
|
-
return {
|
|
85
|
+
return { params, issues, tinted: isColor(d.color) };
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
// CAD-view appearance: the resolved colour on the CAD MeshStandardMaterial,
|
|
@@ -73,11 +92,15 @@ export function resolveMaterial(display) {
|
|
|
73
92
|
const MIN_CAD_LUMA = 60; // 0..255, Rec. 709
|
|
74
93
|
export function cadAppearance(display) {
|
|
75
94
|
const d = display && typeof display === "object" ? display : {};
|
|
76
|
-
const { params } = resolveMaterial(d);
|
|
77
95
|
if (d.material == null) {
|
|
78
|
-
//
|
|
79
|
-
|
|
96
|
+
// The drafting look, untouched by overrides — as it always was.
|
|
97
|
+
const { params } = resolveMaterial(d);
|
|
98
|
+
return { color: params.color, metalness: NO_MATERIAL_CAD.metalness, roughness: NO_MATERIAL_CAD.roughness, opacity: params.opacity };
|
|
80
99
|
}
|
|
100
|
+
// An unknown material flattens the drafting look rather than the realistic
|
|
101
|
+
// PLA default, exactly as it did when it fell back to the retired "default"
|
|
102
|
+
// preset.
|
|
103
|
+
const { params } = knownMaterial(d.material) ? resolveMaterial(d) : applyDisplay(d, NO_MATERIAL_CAD, NO_MATERIAL_COLOR, []);
|
|
81
104
|
return {
|
|
82
105
|
color: liftLuma(params.color, MIN_CAD_LUMA),
|
|
83
106
|
metalness: Math.min(params.metalness, 0.5),
|
|
@@ -97,8 +120,9 @@ function liftLuma(color, min) {
|
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
// The colour a 3MF object carries: the explicit tint, else the preset's own
|
|
100
|
-
// colour
|
|
101
|
-
//
|
|
123
|
+
// colour (the blue-grey for an unknown material, never the realistic PLA
|
|
124
|
+
// default's). Null when the sub-part says nothing about appearance, which is
|
|
125
|
+
// what keeps an appearance-free export byte-for-byte unchanged.
|
|
102
126
|
export function printColor(display) {
|
|
103
127
|
if (!display || typeof display !== "object") return null;
|
|
104
128
|
if (isColor(display.color)) return display.color;
|
package/src/framework/mount.js
CHANGED
|
@@ -419,7 +419,7 @@ function createCleanupStack() {
|
|
|
419
419
|
// Every `elements` entry defaults to the legacy global-ID lookup (below), resolved
|
|
420
420
|
// exactly once here — submodules take element refs and never query the document.
|
|
421
421
|
// `container`/`controls` remain as deprecated aliases for elements.viewer/.controls.
|
|
422
|
-
export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDownload, onViewChange, onParamsCommit, onAnnotationSend,
|
|
422
|
+
export function mount(part, { createWorker, elements = {}, onBuild, onPick, pickHint = "Click to edit", onDownload, onViewChange, onParamsCommit, onAnnotationSend,
|
|
423
423
|
fontCatalog,
|
|
424
424
|
imageCatalog,
|
|
425
425
|
onAssetUpload,
|
|
@@ -497,7 +497,10 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
497
497
|
onRailLayout: () => railChrome.layoutChanged?.(),
|
|
498
498
|
});
|
|
499
499
|
cleanup.defer(() => paneTabs.detach());
|
|
500
|
-
|
|
500
|
+
// Always-on hover inspection (no-op on touch-only devices). With onPick the
|
|
501
|
+
// host acts on a click, so the tooltip nudges toward it with `pickHint`
|
|
502
|
+
// (a host passes its own wording, or null for none).
|
|
503
|
+
const hover = attachHoverLabels(viewer, { part, tooltip, hint: onPick ? pickHint : null });
|
|
501
504
|
cleanup.defer(() => hover.detach());
|
|
502
505
|
const ui = createStatusUi({ ...els.status, exports: [els.exports.stl, els.exports.step, els.exports.threeMf] });
|
|
503
506
|
cleanup.defer(() => ui.setStatus(""));
|
|
@@ -943,22 +946,42 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
943
946
|
|
|
944
947
|
// Print frames for the layer-line pattern: the display → export map of the
|
|
945
948
|
// geometry just delivered, so layers run the way the part is printed rather
|
|
946
|
-
// than the way it is displayed.
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
949
|
+
// than the way it is displayed. Each frame describes the delivered mesh
|
|
950
|
+
// (which a later pose-only repair only moves), and only sub-parts whose
|
|
951
|
+
// material draws layer lines have one — which includes every
|
|
952
|
+
// sub-part naming no material, since realistic mode shows those as PLA
|
|
953
|
+
// (resolve.js). One without a place() is identity at once; one with it
|
|
954
|
+
// costs two geometry-free probe builds.
|
|
950
955
|
const layerLined = new Set(Object.keys(part.parts).filter((n) => {
|
|
951
956
|
try { return resolveMaterial(part.parts[n].display).params.pattern === "layer-lines"; } catch { return false; }
|
|
952
957
|
}));
|
|
958
|
+
// LAZY: a delivery only records the view and params it was built at; the
|
|
959
|
+
// viewer pulls the frames (computePrintFrames) when it is about to draw the
|
|
960
|
+
// realistic look — live, loading, or borrowed by a capture — so a CAD-only
|
|
961
|
+
// session never probes (viewer.js setPrintFrameSource). The snapshot keeps
|
|
962
|
+
// a late computation describing the mesh actually delivered.
|
|
953
963
|
const printFrames = {};
|
|
964
|
+
const undrawnFrames = new Map(); // sub-part -> { view, params } at its delivery
|
|
954
965
|
function recordPrintFrames(names) {
|
|
955
966
|
const wanted = names.filter((n) => layerLined.has(n));
|
|
956
967
|
if (!wanted.length) return;
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
viewer.setPrintFrames({ ...printFrames });
|
|
968
|
+
const at = { view: view(), params: { ...params } };
|
|
969
|
+
for (const n of wanted) undrawnFrames.set(n, at);
|
|
970
|
+
viewer.invalidatePrintFrames?.();
|
|
961
971
|
}
|
|
972
|
+
function computePrintFrames() {
|
|
973
|
+
const resolvedFor = new Map(); // one resolveParams per delivery, not per sub-part
|
|
974
|
+
for (const [n, at] of undrawnFrames) {
|
|
975
|
+
if (!resolvedFor.has(at)) {
|
|
976
|
+
try { resolvedFor.set(at, resolveParams(part, at.params)); } catch { resolvedFor.set(at, null); } // diagnosed by the build
|
|
977
|
+
}
|
|
978
|
+
const resolved = resolvedFor.get(at);
|
|
979
|
+
if (resolved) printFrames[n] = printFrameMatrix(part.parts[n], { view: at.view, ...resolved });
|
|
980
|
+
}
|
|
981
|
+
undrawnFrames.clear();
|
|
982
|
+
return { ...printFrames };
|
|
983
|
+
}
|
|
984
|
+
viewer.setPrintFrameSource?.(computePrintFrames);
|
|
962
985
|
|
|
963
986
|
// Sub-parts whose latest fresh delivery had zero triangles (see the `meshes` case).
|
|
964
987
|
const emptySubParts = new Set();
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Always-on hover inspection: a cursor-following tooltip naming the feature +
|
|
2
|
-
// sub-part under the pointer
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// sub-part under the pointer. Feature names come from Solid.label() in the
|
|
3
|
+
// part's build, carried per-triangle in the mesh payload
|
|
4
|
+
// (geometry.userData.featureIds/features).
|
|
5
|
+
//
|
|
6
|
+
// There is deliberately NO surface highlight: tinting just the feature under
|
|
7
|
+
// the pointer read as "only this face is clickable", and people stopped
|
|
8
|
+
// clicking elsewhere on the part. `hint` (e.g. "Click to edit") adds a line
|
|
9
|
+
// under the name when a click does something — mount passes it with onPick.
|
|
5
10
|
import { createTooltipPresenter } from "../tooltip.js";
|
|
6
11
|
import { raycastViewer } from "./raycast.js";
|
|
7
|
-
import { createFeatureHighlight } from "./feature-highlight.js";
|
|
8
12
|
import { runCleanupSteps } from "../teardown.js";
|
|
9
13
|
|
|
10
14
|
export function attachHoverLabels(
|
|
11
15
|
viewer,
|
|
12
|
-
{ part, schedule = (cb) => requestAnimationFrame(cb), tooltip } = {},
|
|
16
|
+
{ part, schedule = (cb) => requestAnimationFrame(cb), tooltip, hint = null } = {},
|
|
13
17
|
) {
|
|
14
18
|
// Hover is a mouse idiom — skip entirely on touch-only devices. The stub
|
|
15
19
|
// still answers the WHOLE interface: mount.js calls setSuppressed from the
|
|
@@ -25,8 +29,6 @@ export function attachHoverLabels(
|
|
|
25
29
|
let presentationToken;
|
|
26
30
|
let hasPresented = false;
|
|
27
31
|
|
|
28
|
-
const highlight = createFeatureHighlight(viewer);
|
|
29
|
-
|
|
30
32
|
const subLabel = (name) => part.parts[name]?.label ?? name;
|
|
31
33
|
|
|
32
34
|
function hide() {
|
|
@@ -35,7 +37,6 @@ export function attachHoverLabels(
|
|
|
35
37
|
tooltipPresenter.hide(presentationToken);
|
|
36
38
|
presentationToken = undefined;
|
|
37
39
|
}
|
|
38
|
-
highlight.clear();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function show(hit, x, y) {
|
|
@@ -45,7 +46,7 @@ export function attachHoverLabels(
|
|
|
45
46
|
} else {
|
|
46
47
|
content = { title: subLabel(hit.subPart), subtitle: "" };
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
if (hint) content.hint = hint;
|
|
49
50
|
if (hasPresented) {
|
|
50
51
|
hasPresented = false;
|
|
51
52
|
tooltipPresenter.hide(presentationToken);
|
|
@@ -119,7 +120,6 @@ export function attachHoverLabels(
|
|
|
119
120
|
() => viewer.domElement.removeEventListener("pointerup", onUp),
|
|
120
121
|
() => viewer.domElement.removeEventListener("pointerleave", onLeave),
|
|
121
122
|
hide,
|
|
122
|
-
() => highlight.dispose(),
|
|
123
123
|
() => { if (ownsTooltip) tooltipPresenter.dispose(); },
|
|
124
124
|
], "feature hover cleanup failed");
|
|
125
125
|
},
|
package/src/framework/tooltip.js
CHANGED
|
@@ -14,7 +14,10 @@ export function createTooltipPresenter({ id = "pf-hover-tip" } = {}) {
|
|
|
14
14
|
const title = document.createElement("b");
|
|
15
15
|
const subtitle = document.createElement("span");
|
|
16
16
|
subtitle.className = "pf-hover-sub";
|
|
17
|
-
|
|
17
|
+
// A call-to-action line under the name ("Click to edit"); empty = hidden.
|
|
18
|
+
const hint = document.createElement("div");
|
|
19
|
+
hint.className = "pf-hover-hint";
|
|
20
|
+
element.append(title, subtitle, hint);
|
|
18
21
|
document.body.appendChild(element);
|
|
19
22
|
let disposed = false;
|
|
20
23
|
const claims = [];
|
|
@@ -22,6 +25,7 @@ export function createTooltipPresenter({ id = "pf-hover-tip" } = {}) {
|
|
|
22
25
|
function setContent(content) {
|
|
23
26
|
title.textContent = content.title;
|
|
24
27
|
subtitle.textContent = content.subtitle ?? "";
|
|
28
|
+
hint.textContent = content.hint ?? "";
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function measureVisible(content, anchored) {
|
|
@@ -150,6 +150,7 @@ export function attachViewStyleControls(viewer, { stage, toolbar = null, anchor
|
|
|
150
150
|
if (!cache.isStale()) return;
|
|
151
151
|
cache.markFresh();
|
|
152
152
|
const token = ++loopToken;
|
|
153
|
+
let missed = false;
|
|
153
154
|
// One at a time: each realistic style may load an environment.
|
|
154
155
|
for (const s of STYLES) {
|
|
155
156
|
if (detached || token !== loopToken) return;
|
|
@@ -161,9 +162,15 @@ export function attachViewStyleControls(viewer, { stage, toolbar = null, anchor
|
|
|
161
162
|
let url = null;
|
|
162
163
|
try { url = await viewer.renderStyleThumbnail(s.id, { size: 256 }); } catch { url = null; }
|
|
163
164
|
if (detached || token !== loopToken) return;
|
|
165
|
+
// A failed or empty render (null) blanks its tile rather than leaving an
|
|
166
|
+
// older picture up, and leaves the cache stale so the next open tries
|
|
167
|
+
// again: caching it made one bad moment permanent until a part or theme
|
|
168
|
+
// change came along.
|
|
164
169
|
cache.set(s.id, url);
|
|
170
|
+
if (!url) missed = true;
|
|
165
171
|
paint(s.id);
|
|
166
172
|
}
|
|
173
|
+
if (missed && token === loopToken) cache.invalidate();
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
// --- open / close ------------------------------------------------------------
|
|
@@ -225,6 +232,10 @@ export function attachViewStyleControls(viewer, { stage, toolbar = null, anchor
|
|
|
225
232
|
viewer.onEnvironmentChange(() => render()),
|
|
226
233
|
viewer.onAssemblyChange?.(() => cache.invalidate()) ?? (() => {}),
|
|
227
234
|
viewer.onThemeChange?.(() => cache.invalidate()) ?? (() => {}),
|
|
235
|
+
// A thumbnail shows the section as it was when it was taken, so turning
|
|
236
|
+
// the cutaway on or off, or moving its plane, stales them all — without
|
|
237
|
+
// this a set taken with the cutaway on outlived the cutaway itself.
|
|
238
|
+
viewer.onCutawayChange?.(() => cache.invalidate()) ?? (() => {}),
|
|
228
239
|
];
|
|
229
240
|
render();
|
|
230
241
|
// The popover closes whenever the button's host is hidden: #viewbar hides
|
|
@@ -11,8 +11,8 @@ export const STYLES = [
|
|
|
11
11
|
|
|
12
12
|
export const styleFor = (renderMode, environmentId) => (renderMode === "realistic" ? environmentId : "cad");
|
|
13
13
|
|
|
14
|
-
// Thumbnails are rendered on OPEN and cached. `invalidate` (a part or
|
|
15
|
-
// change) only marks them stale — the next open re-renders. A render already
|
|
14
|
+
// Thumbnails are rendered on OPEN and cached. `invalidate` (a part, theme or
|
|
15
|
+
// cutaway change, or a render that came back empty) only marks them stale — the next open re-renders. A render already
|
|
16
16
|
// in flight keeps painting (view-style-controls.js says why).
|
|
17
17
|
export function createThumbnailCache() {
|
|
18
18
|
const images = new Map();
|
package/src/framework/viewer.js
CHANGED
|
@@ -562,6 +562,8 @@ export function createViewer(container, part) {
|
|
|
562
562
|
const textureLoader = new THREE.TextureLoader();
|
|
563
563
|
let pmrem = null;
|
|
564
564
|
let printFrames = {};
|
|
565
|
+
let printFrameSource = null; // () => frames; mount's lazy provider (setPrintFrameSource)
|
|
566
|
+
let printFramesStale = false; // the source has frames the materials have not seen
|
|
565
567
|
const modeListeners = new Set();
|
|
566
568
|
const envListeners = new Set();
|
|
567
569
|
const SHADOW_LOWRES_MS = 100; // at most one low-res shadow per this, while a part moves
|
|
@@ -750,11 +752,14 @@ export function createViewer(container, part) {
|
|
|
750
752
|
.map((n) => subMesh[n]);
|
|
751
753
|
// `force` renders even while parked: a capture is offscreen work a parked
|
|
752
754
|
// viewer still does, and it must not bake a stale (or absent) shadow.
|
|
755
|
+
// The cutaway's plane clips the shadow's casters too, so the half it cut
|
|
756
|
+
// away casts no shadow (cutaway.onChange below re-renders it as it moves).
|
|
757
|
+
const _shadowClipPlane = new THREE.Plane();
|
|
753
758
|
function renderShadow(opts, { force = false } = {}) {
|
|
754
759
|
if (!realisticRig || (!active && !force)) return;
|
|
755
760
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
761
|
+
const plane = cutaway.getPlane?.(_shadowClipPlane) ?? null;
|
|
762
|
+
realisticRig.shadow.render(scene, castersNow(), { ...opts, clippingPlanes: plane ? [plane] : null });
|
|
758
763
|
} catch (e) {
|
|
759
764
|
console.warn("partforge: contact shadow failed", e);
|
|
760
765
|
}
|
|
@@ -787,6 +792,10 @@ export function createViewer(container, part) {
|
|
|
787
792
|
renderShadow({ lowRes: true });
|
|
788
793
|
}
|
|
789
794
|
}
|
|
795
|
+
// A cutaway turned on or off, or its plane moved, changes what casts the
|
|
796
|
+
// contact shadow: low-res while the plane is dragged, full once it settles —
|
|
797
|
+
// the same schedule as a moving sub-part.
|
|
798
|
+
cutaway.onChange?.(() => { if (realisticRig) shadowMovedAt = performance.now(); });
|
|
790
799
|
|
|
791
800
|
const livePixelRatio = (mode) =>
|
|
792
801
|
Math.min(devicePixelRatio, mode === "realistic" && isCoarsePointer() ? 1.5 : 2);
|
|
@@ -805,6 +814,7 @@ export function createViewer(container, part) {
|
|
|
805
814
|
// ground only moves on showAssembly, never because a capture happened.
|
|
806
815
|
function enterRealistic(rig, { live = true, reground = true } = {}) {
|
|
807
816
|
try {
|
|
817
|
+
syncPrintFrames(); // before the swap: the first realistic frame has the export-pose layers
|
|
808
818
|
renderMode = "realistic";
|
|
809
819
|
// A regen can land between the proxy compile and here, while the mode
|
|
810
820
|
// was still CAD, so setSubGeometry skipped the UVs. Idempotent.
|
|
@@ -874,6 +884,7 @@ export function createViewer(container, part) {
|
|
|
874
884
|
// target bound — each only for the synchronous compile() inside
|
|
875
885
|
// compileAsync, so the view on screen is untouched while it waits.
|
|
876
886
|
function compileRealistic(rig, { forCapture = false } = {}) {
|
|
887
|
+
syncPrintFrames();
|
|
877
888
|
const proxy = new THREE.Scene();
|
|
878
889
|
proxy.environment = rig.envMap;
|
|
879
890
|
for (const n of names) {
|
|
@@ -974,6 +985,32 @@ export function createViewer(container, part) {
|
|
|
974
985
|
for (const [n, m] of physicalMats) m.userData.patternUniforms?.pfPrintFrame.value.fromArray(printFrames[n] ?? identity);
|
|
975
986
|
}
|
|
976
987
|
|
|
988
|
+
// The lazy form mount uses. Computing a frame runs the sub-part's pose probe
|
|
989
|
+
// (two geometry-free builds for a placed sub-part), and every sub-part with
|
|
990
|
+
// no material draws layer lines, so frames are PULLED only when something is
|
|
991
|
+
// about to draw the realistic look: the live view (or a switch loading),
|
|
992
|
+
// and any capture that borrows it — compileRealistic and enterRealistic are
|
|
993
|
+
// the two doors every such path goes through. A delivery just marks them
|
|
994
|
+
// stale (invalidatePrintFrames); in CAD nothing is computed.
|
|
995
|
+
function setPrintFrameSource(source) {
|
|
996
|
+
printFrameSource = typeof source === "function" ? source : null;
|
|
997
|
+
invalidatePrintFrames();
|
|
998
|
+
}
|
|
999
|
+
function invalidatePrintFrames() {
|
|
1000
|
+
printFramesStale = true;
|
|
1001
|
+
if (renderMode === "realistic" || realisticPending) syncPrintFrames();
|
|
1002
|
+
}
|
|
1003
|
+
function syncPrintFrames() {
|
|
1004
|
+
if (!printFrameSource || !printFramesStale) return;
|
|
1005
|
+
printFramesStale = false;
|
|
1006
|
+
let frames;
|
|
1007
|
+
try { frames = printFrameSource(); } catch (e) {
|
|
1008
|
+
console.warn("partforge: computing print frames failed", e);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
setPrintFrames(frames);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
977
1014
|
// Resolves once the current environment's rig is loaded (captures wait on it).
|
|
978
1015
|
const whenRealisticReady = () => rigFor(environmentId).then(() => {});
|
|
979
1016
|
|
|
@@ -1825,7 +1862,7 @@ export function createViewer(container, part) {
|
|
|
1825
1862
|
for (const dot of [...flashDots, ...captureHidden]) if (dot.visible) { dot.visible = false; reshowFlashDots.push(dot); }
|
|
1826
1863
|
try {
|
|
1827
1864
|
renderer.setRenderTarget(rt);
|
|
1828
|
-
|
|
1865
|
+
renderWithBackdrop(renderScene, cam);
|
|
1829
1866
|
// render() resolves the multisample renderbuffer into the target texture, so this
|
|
1830
1867
|
// reads antialiased pixels.
|
|
1831
1868
|
renderer.readRenderTargetPixels(rt, 0, 0, width, height, buf);
|
|
@@ -1860,6 +1897,14 @@ export function createViewer(container, part) {
|
|
|
1860
1897
|
// Pointer feedback excluded from EVERY offscreen render (renderOffscreen),
|
|
1861
1898
|
// showcase captures included — unlike canonicalCaptureHidden below.
|
|
1862
1899
|
const captureHidden = new Set();
|
|
1900
|
+
// The cutaway's ghost plane is its gizmo, not its section: the part as the
|
|
1901
|
+
// user sees it (clipped, with its hatched caps) goes into a capture, the
|
|
1902
|
+
// translucent plane in front of it does not. It sits between the camera and
|
|
1903
|
+
// everything the cut keeps, and a capture blends it in linear light, where
|
|
1904
|
+
// it lands several times stronger than on the canvas — the view style
|
|
1905
|
+
// popover's thumbnails came back with the part washed out behind a blue
|
|
1906
|
+
// sheet, a dark part all but gone.
|
|
1907
|
+
for (const obj of cutaway.captureExcluded ?? []) captureHidden.add(obj);
|
|
1863
1908
|
function registerCaptureHidden(obj) {
|
|
1864
1909
|
captureHidden.add(obj);
|
|
1865
1910
|
return () => captureHidden.delete(obj);
|
|
@@ -2144,6 +2189,64 @@ export function createViewer(container, part) {
|
|
|
2144
2189
|
}
|
|
2145
2190
|
}
|
|
2146
2191
|
|
|
2192
|
+
// --- the environment backdrop under an orthographic camera ---------------
|
|
2193
|
+
// three draws a texture `scene.background` (the realistic environment photo)
|
|
2194
|
+
// as a unit box around the camera, projected through the camera's own
|
|
2195
|
+
// projection (WebGLBackground + the backgroundCube shader). Under a
|
|
2196
|
+
// PerspectiveCamera that box fills the frame; under an OrthographicCamera it
|
|
2197
|
+
// projects to a one-unit square in a frustum tens of millimetres wide or
|
|
2198
|
+
// more — a speck behind the part — so an ortho face view (the view cube
|
|
2199
|
+
// settles into one) lost its whole backdrop to the clear colour, on the
|
|
2200
|
+
// canvas and in every capture of that framing. So under ortho the backdrop is
|
|
2201
|
+
// its own pass: three's own background draw (same PMREM, blur, intensity,
|
|
2202
|
+
// rotation) through a perspective camera with the ortho camera's orientation
|
|
2203
|
+
// and the live perspective camera's fov, then the scene through the ortho
|
|
2204
|
+
// camera on top, clearing depth and stencil but not colour. The backdrop is
|
|
2205
|
+
// exactly what perspective shows looking the same way. A colour background
|
|
2206
|
+
// (CAD) clears the same under either projection and keeps the one pass.
|
|
2207
|
+
// Residual: a transmissive material in the scene pass refracts the clear
|
|
2208
|
+
// colour rather than the environment (its transmission pass reads
|
|
2209
|
+
// scene.background, which is null for that pass) — under ortho only.
|
|
2210
|
+
const _backdropCam = new THREE.PerspectiveCamera(45, 1, 0.1, 10);
|
|
2211
|
+
const _backdropScene = new THREE.Scene();
|
|
2212
|
+
function renderWithBackdrop(renderScene, cam) {
|
|
2213
|
+
const background = renderScene.background;
|
|
2214
|
+
if (!cam.isOrthographicCamera || !background?.isTexture) {
|
|
2215
|
+
renderer.render(renderScene, cam);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
cam.updateMatrixWorld();
|
|
2219
|
+
cam.matrixWorld.decompose(_backdropCam.position, _backdropCam.quaternion, _backdropCam.scale);
|
|
2220
|
+
_backdropCam.up.copy(cam.up);
|
|
2221
|
+
_backdropCam.fov = camera.fov;
|
|
2222
|
+
_backdropCam.aspect = (cam.right - cam.left) / (cam.top - cam.bottom) || 1;
|
|
2223
|
+
// A recentred capture renders a sub-window of a larger frame: the backdrop
|
|
2224
|
+
// is cropped the same way, so it lines up with the scene pass.
|
|
2225
|
+
if (cam.view?.enabled) {
|
|
2226
|
+
const { fullWidth, fullHeight, offsetX, offsetY, width, height } = cam.view;
|
|
2227
|
+
_backdropCam.setViewOffset(fullWidth, fullHeight, offsetX, offsetY, width, height);
|
|
2228
|
+
} else {
|
|
2229
|
+
_backdropCam.clearViewOffset();
|
|
2230
|
+
}
|
|
2231
|
+
_backdropCam.updateProjectionMatrix();
|
|
2232
|
+
_backdropCam.updateMatrixWorld(true);
|
|
2233
|
+
_backdropScene.background = background;
|
|
2234
|
+
_backdropScene.backgroundBlurriness = renderScene.backgroundBlurriness;
|
|
2235
|
+
_backdropScene.backgroundIntensity = renderScene.backgroundIntensity;
|
|
2236
|
+
_backdropScene.backgroundRotation.copy(renderScene.backgroundRotation);
|
|
2237
|
+
const clearColorWas = renderer.autoClearColor;
|
|
2238
|
+
try {
|
|
2239
|
+
renderer.render(_backdropScene, _backdropCam);
|
|
2240
|
+
renderScene.background = null;
|
|
2241
|
+
renderer.autoClearColor = false;
|
|
2242
|
+
renderer.render(renderScene, cam);
|
|
2243
|
+
} finally {
|
|
2244
|
+
renderScene.background = background;
|
|
2245
|
+
renderer.autoClearColor = clearColorWas;
|
|
2246
|
+
_backdropScene.background = null; // hold no reference to a rig's texture
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2147
2250
|
// --- render loop ----------------------------------------------------------
|
|
2148
2251
|
// The tween is applied after controls.update() so the cue wins the frame, and
|
|
2149
2252
|
// the frame listeners run before render so a playback frame draws its own pose.
|
|
@@ -2181,7 +2284,7 @@ export function createViewer(container, part) {
|
|
|
2181
2284
|
const next = projectPoint([anchorDot.position.x, anchorDot.position.y, anchorDot.position.z]);
|
|
2182
2285
|
if (anchorMoved(lastAnchor, next)) publishAnchor(next);
|
|
2183
2286
|
}
|
|
2184
|
-
|
|
2287
|
+
renderWithBackdrop(scene, activeCamera);
|
|
2185
2288
|
cutaway.renderOverlay(renderer, activeCamera);
|
|
2186
2289
|
}
|
|
2187
2290
|
renderer.setAnimationLoop(renderFrame);
|
|
@@ -2543,6 +2646,8 @@ export function createViewer(container, part) {
|
|
|
2543
2646
|
registerCanonicalCaptureHidden,
|
|
2544
2647
|
registerCaptureHidden,
|
|
2545
2648
|
onCutawayHandleHover: cutaway.onHandleHoverChange,
|
|
2649
|
+
// The section changed: on, off, or the plane moved. Listener gets no args.
|
|
2650
|
+
onCutawayChange: cutaway.onChange,
|
|
2546
2651
|
setRenderMode,
|
|
2547
2652
|
getRenderMode: () => renderMode,
|
|
2548
2653
|
onRenderModeChange: (cb) => { modeListeners.add(cb); return () => modeListeners.delete(cb); },
|
|
@@ -2557,6 +2662,8 @@ export function createViewer(container, part) {
|
|
|
2557
2662
|
isRealisticPending: () => realisticPending,
|
|
2558
2663
|
onEnvironmentChange: (cb) => { envListeners.add(cb); return () => envListeners.delete(cb); },
|
|
2559
2664
|
setPrintFrames,
|
|
2665
|
+
setPrintFrameSource,
|
|
2666
|
+
invalidatePrintFrames,
|
|
2560
2667
|
whenRealisticReady,
|
|
2561
2668
|
dispose,
|
|
2562
2669
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// judged by eye in every environment (materials.html). Not a reference part.
|
|
4
4
|
import { PRESETS } from "../framework/materials/presets.js";
|
|
5
5
|
|
|
6
|
-
const ids = Object.keys(PRESETS)
|
|
6
|
+
const ids = Object.keys(PRESETS);
|
|
7
7
|
const COLS = 6, PITCH = 40;
|
|
8
8
|
|
|
9
9
|
const withHole = (k) =>
|
|
@@ -20,6 +20,13 @@ export default {
|
|
|
20
20
|
build: (k) => withHole(k).translate([(i % COLS) * PITCH, -Math.floor(i / COLS) * PITCH, 0]),
|
|
21
21
|
display: { material: id },
|
|
22
22
|
}])),
|
|
23
|
+
// No material at all: the blue-grey CAD look, a PLA print in that colour in
|
|
24
|
+
// realistic mode (resolve.js).
|
|
25
|
+
no_material: {
|
|
26
|
+
label: "No material",
|
|
27
|
+
views: ["all"],
|
|
28
|
+
build: (k) => withHole(k).translate([(ids.length % COLS) * PITCH, -Math.floor(ids.length / COLS) * PITCH, 0]),
|
|
29
|
+
},
|
|
23
30
|
upright_pla: {
|
|
24
31
|
label: "PLA, displayed upright, printed flat",
|
|
25
32
|
views: ["print"],
|