@pascal-app/viewer 0.9.1 → 1.0.0-beta.1
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 +20 -5
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +59 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +113 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +180 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +23 -12
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -24
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -13
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +46 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +142 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +16 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +158 -34
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +73 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +191 -4
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +61 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +12 -5
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +34 -9
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -45
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +7 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +31 -83
- package/dist/systems/slab/slab-system.test.js +54 -14
- package/dist/systems/stair/stair-system.js +7 -4
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +49 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +116 -0
- package/dist/systems/wall/wall-system.d.ts +4 -8
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +397 -177
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -131
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { DataTexture, LinearFilter, LinearMipmapLinearFilter, RGBAFormat, UnsignedByteType, } from 'three';
|
|
2
|
+
// Deep path, NOT the `Addons.js` aggregate: the aggregate re-exports
|
|
3
|
+
// LottieLoader/TTFLoader, whose CDN URL imports (`lottie-web`, `opentype.js`)
|
|
4
|
+
// crash bun's test runtime for every consumer of the viewer barrel.
|
|
5
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
6
|
+
/** Base width/height of a Basis-supercompressed KTX2 payload needing transcode
|
|
7
|
+
* (vkFormat 0 = ETC1S/UASTC) whose dimensions aren't block-aligned, or null
|
|
8
|
+
* for anything safe. Little-endian u32s: vkFormat at byte 12, pixelWidth at
|
|
9
|
+
* 20, pixelHeight at 24. */
|
|
10
|
+
function readMisalignedBasisSize(buffer) {
|
|
11
|
+
if (buffer.byteLength < 28)
|
|
12
|
+
return null;
|
|
13
|
+
const view = new DataView(buffer);
|
|
14
|
+
const vkFormat = view.getUint32(12, true);
|
|
15
|
+
if (vkFormat !== 0)
|
|
16
|
+
return null;
|
|
17
|
+
const width = view.getUint32(20, true);
|
|
18
|
+
const height = view.getUint32(24, true);
|
|
19
|
+
return width % 4 !== 0 || height % 4 !== 0 ? { width, height } : null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* KTX2Loader that survives block-misaligned textures. WebGPU rejects
|
|
23
|
+
* block-compressed textures whose base dimensions aren't multiples of 4;
|
|
24
|
+
* three's KTX2Loader only warns and transcodes anyway, and the resulting
|
|
25
|
+
* invalid texture poisons every render pass that binds it (endless
|
|
26
|
+
* "Invalid BindGroup … Invalid CommandBuffer" spam — baked GLBs carrying an
|
|
27
|
+
* odd-sized user-item texture triggered exactly this). Such payloads are
|
|
28
|
+
* routed to a fallback loader whose support flags report no compressed
|
|
29
|
+
* formats, which makes the transcoder emit plain RGBA32 — uncompressed
|
|
30
|
+
* textures have no alignment requirement.
|
|
31
|
+
*/
|
|
32
|
+
class AlignmentSafeKTX2Loader extends KTX2Loader {
|
|
33
|
+
rgbaFallback = null;
|
|
34
|
+
fallbackLoader() {
|
|
35
|
+
if (!this.rgbaFallback) {
|
|
36
|
+
this.rgbaFallback = new KTX2Loader(this.manager);
|
|
37
|
+
this.rgbaFallback.setTranscoderPath(this.transcoderPath);
|
|
38
|
+
this.rgbaFallback.workerConfig = {
|
|
39
|
+
astcSupported: false,
|
|
40
|
+
astcHDRSupported: false,
|
|
41
|
+
etc1Supported: false,
|
|
42
|
+
etc2Supported: false,
|
|
43
|
+
dxtSupported: false,
|
|
44
|
+
bptcSupported: false,
|
|
45
|
+
pvrtcSupported: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return this.rgbaFallback;
|
|
49
|
+
}
|
|
50
|
+
/** Both `load()` and GLTFLoader's KHR_texture_basisu path funnel through
|
|
51
|
+
* this internal — overriding it covers every entry point. */
|
|
52
|
+
async _createTexture(buffer, config) {
|
|
53
|
+
const misaligned = readMisalignedBasisSize(buffer);
|
|
54
|
+
if (misaligned) {
|
|
55
|
+
console.warn(`[viewer] KTX2 texture is ${misaligned.width}x${misaligned.height} (not multiple-of-4); ` +
|
|
56
|
+
'transcoding uncompressed so WebGPU can create it.');
|
|
57
|
+
const transcoded = (await this.fallbackLoader()._createTexture(buffer, config));
|
|
58
|
+
// The RGBA32 result still arrives as a CompressedTexture; three's WebGPU
|
|
59
|
+
// texture upload treats every CompressedTexture as block-compressed and
|
|
60
|
+
// crashes looking up a block descriptor for rgba8. Repackage the decoded
|
|
61
|
+
// pixels as a plain DataTexture instead.
|
|
62
|
+
const mip = transcoded.mipmaps?.[0];
|
|
63
|
+
if (!mip)
|
|
64
|
+
return transcoded;
|
|
65
|
+
const texture = new DataTexture(mip.data, mip.width, mip.height, RGBAFormat, UnsignedByteType);
|
|
66
|
+
texture.colorSpace = transcoded.colorSpace;
|
|
67
|
+
texture.generateMipmaps = true;
|
|
68
|
+
texture.minFilter = LinearMipmapLinearFilter;
|
|
69
|
+
texture.magFilter = LinearFilter;
|
|
70
|
+
texture.needsUpdate = true;
|
|
71
|
+
transcoded.dispose();
|
|
72
|
+
return texture;
|
|
73
|
+
}
|
|
74
|
+
return KTX2Loader.prototype._createTexture.call(this, buffer, config);
|
|
75
|
+
}
|
|
76
|
+
dispose() {
|
|
77
|
+
this.rgbaFallback?.dispose();
|
|
78
|
+
this.rgbaFallback = null;
|
|
79
|
+
super.dispose();
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Single shared KTX2 loader for the whole viewer — used both by the GLB loader
|
|
85
|
+
* (`use-gltf-ktx2`) and by catalog finish textures (`materials.ts`). KTX2 must
|
|
86
|
+
* be transcoded at load via the Basis WASM, and `detectSupport(renderer)` has to
|
|
87
|
+
* run once before any `.ktx2` is loaded so the loader picks a GPU format the
|
|
88
|
+
* device supports. `ensureKtx2Support` is idempotent per renderer and is called
|
|
89
|
+
* from the viewer root the moment the renderer is ready (even when no GLB is in
|
|
90
|
+
* the scene, so catalog `.ktx2` finishes still load).
|
|
91
|
+
*/
|
|
92
|
+
export const ktx2Loader = new AlignmentSafeKTX2Loader();
|
|
93
|
+
ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/');
|
|
94
|
+
const configuredRenderers = new WeakSet();
|
|
95
|
+
const warnedRenderers = new WeakSet();
|
|
96
|
+
let resolveKtx2Ready;
|
|
97
|
+
const ktx2ReadyPromise = new Promise((resolve) => {
|
|
98
|
+
resolveKtx2Ready = resolve;
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Resolves once `detectSupport` has succeeded for any renderer. `.ktx2` loads
|
|
102
|
+
* issued before that point would throw inside KTX2Loader ("Missing
|
|
103
|
+
* initialization with `.detectSupport( renderer )`"), so texture loaders await
|
|
104
|
+
* this instead of failing — covers materials created while the renderer is
|
|
105
|
+
* still initializing (e.g. a standalone capture canvas).
|
|
106
|
+
*/
|
|
107
|
+
export function whenKtx2Ready() {
|
|
108
|
+
return ktx2ReadyPromise;
|
|
109
|
+
}
|
|
110
|
+
/** Returns true once support has been detected for this renderer (KTX2 safe to load). */
|
|
111
|
+
export function ensureKtx2Support(renderer) {
|
|
112
|
+
const key = renderer;
|
|
113
|
+
if (!key)
|
|
114
|
+
return false;
|
|
115
|
+
if (configuredRenderers.has(key))
|
|
116
|
+
return true;
|
|
117
|
+
try {
|
|
118
|
+
;
|
|
119
|
+
ktx2Loader.detectSupport(renderer);
|
|
120
|
+
configuredRenderers.add(key);
|
|
121
|
+
resolveKtx2Ready();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Some WebGPU flows can transiently call this before backend init; don't
|
|
126
|
+
// crash the scene — a later call (or the next render) retries.
|
|
127
|
+
if (!warnedRenderers.has(key)) {
|
|
128
|
+
console.warn('[viewer] Skipping KTX2 support detection for now.', error);
|
|
129
|
+
warnedRenderers.add(key);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function configureKtx2Support(loader, renderer) {
|
|
135
|
+
if (!ensureKtx2Support(renderer))
|
|
136
|
+
return false;
|
|
137
|
+
loader.setKTX2Loader(ktx2Loader);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
export function isKtx2Url(url) {
|
|
141
|
+
return url.toLowerCase().endsWith('.ktx2');
|
|
142
|
+
}
|
package/dist/lib/layers.d.ts
CHANGED
|
@@ -20,4 +20,14 @@ export declare const ZONE_LAYER = 2;
|
|
|
20
20
|
* from thumbnails like the other editor-only layers.
|
|
21
21
|
*/
|
|
22
22
|
export declare const GRID_LAYER = 3;
|
|
23
|
+
/**
|
|
24
|
+
* Layer for geometry hidden from the color passes but still rendered into the
|
|
25
|
+
* shadow map ("shadow-caster-only"). Used when cutaway/solo views hide roofs
|
|
26
|
+
* or non-selected levels: the sun keeps casting their shadows, so interiors
|
|
27
|
+
* get window-shaped light patches instead of flooding with uniform sun.
|
|
28
|
+
* No camera or pass enables this layer — only each shadow-casting light's
|
|
29
|
+
* shadow camera does (see `lights.tsx`). Applied per-object (layers don't
|
|
30
|
+
* cascade) via `applyShadowOnly` / `clearShadowOnly` in `lib/shadow-only.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const SHADOW_ONLY_LAYER = 4;
|
|
23
33
|
//# sourceMappingURL=layers.d.ts.map
|
package/dist/lib/layers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,IAAI,CAAA"}
|
|
1
|
+
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../src/lib/layers.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,oEAAoE;AACpE,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAA"}
|
package/dist/lib/layers.js
CHANGED
|
@@ -20,3 +20,13 @@ export const ZONE_LAYER = 2;
|
|
|
20
20
|
* from thumbnails like the other editor-only layers.
|
|
21
21
|
*/
|
|
22
22
|
export const GRID_LAYER = 3;
|
|
23
|
+
/**
|
|
24
|
+
* Layer for geometry hidden from the color passes but still rendered into the
|
|
25
|
+
* shadow map ("shadow-caster-only"). Used when cutaway/solo views hide roofs
|
|
26
|
+
* or non-selected levels: the sun keeps casting their shadows, so interiors
|
|
27
|
+
* get window-shaped light patches instead of flooding with uniform sun.
|
|
28
|
+
* No camera or pass enables this layer — only each shadow-casting light's
|
|
29
|
+
* shadow camera does (see `lights.tsx`). Applied per-object (layers don't
|
|
30
|
+
* cascade) via `applyShadowOnly` / `clearShadowOnly` in `lib/shadow-only.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export const SHADOW_ONLY_LAYER = 4;
|
package/dist/lib/materials.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MaterialPresetPayload, type MaterialSchema, type SurfaceRole } from '@pascal-app/core';
|
|
1
|
+
import { type MaterialPresetPayload, type MaterialSchema, type SceneMaterial, type SceneMaterialId, type SurfaceRole } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import { MeshLambertNodeMaterial } from 'three/webgpu';
|
|
4
4
|
export type RenderShading = 'solid' | 'rendered';
|
|
@@ -10,10 +10,25 @@ export declare const BLUEPRINT_PALETTE: Record<SurfaceRole, string>;
|
|
|
10
10
|
export declare const PRESET_PALETTES: Record<ColorPreset, Record<SurfaceRole, string>>;
|
|
11
11
|
export declare function resolveSurfaceColor(role: SurfaceRole, preset: ColorPreset, sceneThemeId?: string): string;
|
|
12
12
|
export declare const glassMaterial: MeshLambertNodeMaterial;
|
|
13
|
+
export declare function resolveTextureRepeat(repeat: unknown, scale: unknown): [number, number];
|
|
14
|
+
export declare function getTextureKey(material?: MaterialSchema): string;
|
|
13
15
|
export declare function applyMaterialPresetToMaterials(materialInput: THREE.Material | THREE.Material[], preset: MaterialPresetPayload | null | undefined): void;
|
|
14
16
|
export declare function createMaterialFromPreset(preset: MaterialPresetPayload, shading?: RenderShading): THREE.Material;
|
|
15
17
|
export declare function createMaterialFromPresetRef(materialPreset?: string, shading?: RenderShading): THREE.Material | null;
|
|
16
18
|
export declare function createMaterial(material?: MaterialSchema, shading?: RenderShading): THREE.Material;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a MaterialRef ('library:<id>' | 'scene:<id>') to a three.js material.
|
|
21
|
+
* Returns null for an unknown / dangling ref so callers fall back to the
|
|
22
|
+
* slot's default (authored material, then themed default). Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveMaterialRef(ref: string | undefined, sceneMaterials: Record<SceneMaterialId, SceneMaterial> | undefined, shading?: RenderShading): THREE.Material | null;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a node kind's declared slot default — either a catalog `library:<id>`
|
|
27
|
+
* finish or a flat `#rrggbb` colour — to a renderable material. Shared by the
|
|
28
|
+
* procedural kinds whose colored-mode unpainted appearance comes from a
|
|
29
|
+
* declarative default (slab, wall).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveSlotDefaultMaterial(slotDefault: string, shading?: RenderShading, roughness?: number): THREE.Material;
|
|
17
32
|
export declare function createDefaultMaterial(color?: string, roughness?: number, shading?: RenderShading, side?: THREE.Side): THREE.Material;
|
|
18
33
|
export declare function createSurfaceRoleMaterial(role: SurfaceRole, preset: ColorPreset, side?: THREE.Side, sceneThemeId?: string): THREE.Material;
|
|
19
34
|
export declare function baseMaterial(shading?: RenderShading): THREE.Material;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/materials.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { getMaterialPresetByRef, resolveMaterial, } from '@pascal-app/core';
|
|
1
|
+
import { getMaterialPresetByRef, parseMaterialRef, resolveMaterial, } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
+
import { float, mix, positionViewDirection, transformedNormalView } from 'three/tsl';
|
|
3
4
|
import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
4
5
|
import { resolveCdnUrl } from './asset-url';
|
|
6
|
+
import { isKtx2Url, ktx2Loader, whenKtx2Ready } from './ktx2-loader';
|
|
5
7
|
import { getSceneTheme } from './scene-themes';
|
|
8
|
+
import { stampPascalTextureRef } from './texture-reference';
|
|
6
9
|
export const CLAY_PALETTE = {
|
|
7
10
|
wall: '#dcd6c7',
|
|
8
11
|
floor: '#cfc8b6',
|
|
@@ -12,14 +15,16 @@ export const CLAY_PALETTE = {
|
|
|
12
15
|
glazing: '#c8d4dc',
|
|
13
16
|
furnishing: '#d2ccbe',
|
|
14
17
|
};
|
|
18
|
+
// Albedos are clamped to ≈0.83 linear (max channel #eb): real white paint
|
|
19
|
+
// reflects ~80%, and pure-white albedo kills GI/shadow contrast.
|
|
15
20
|
export const WHITE_PALETTE = {
|
|
16
|
-
wall: '#
|
|
17
|
-
floor: '#
|
|
18
|
-
ceiling: '#
|
|
21
|
+
wall: '#ebeae6',
|
|
22
|
+
floor: '#e7e4dd',
|
|
23
|
+
ceiling: '#ebeae6',
|
|
19
24
|
roof: '#dedbd2',
|
|
20
|
-
joinery: '#
|
|
25
|
+
joinery: '#e5e2d9',
|
|
21
26
|
glazing: '#dbe8ee',
|
|
22
|
-
furnishing: '#
|
|
27
|
+
furnishing: '#e9e7e1',
|
|
23
28
|
};
|
|
24
29
|
export const MONO_PALETTE = {
|
|
25
30
|
wall: '#c8c8c8',
|
|
@@ -49,7 +54,7 @@ export function resolveSurfaceColor(role, preset, sceneThemeId) {
|
|
|
49
54
|
// The active scene theme may tint individual roles (e.g. Mediterranean's blue
|
|
50
55
|
// roof); fall back to the chosen colour preset's palette when it doesn't.
|
|
51
56
|
const tints = sceneThemeId ? getSceneTheme(sceneThemeId).clayTints : undefined;
|
|
52
|
-
return tints?.[role] ?? PRESET_PALETTES[preset][role];
|
|
57
|
+
return tints?.[role] ?? (PRESET_PALETTES[preset] ?? CLAY_PALETTE)[role];
|
|
53
58
|
}
|
|
54
59
|
// DoubleSide on any NodeMaterial inside the MRT scenePass (SSGI's output /
|
|
55
60
|
// diffuseColor / normal targets) causes WebGPU to create a render pipeline
|
|
@@ -62,10 +67,14 @@ export const glassMaterial = new MeshLambertNodeMaterial({
|
|
|
62
67
|
opacity: 0.35,
|
|
63
68
|
side: THREE.FrontSide,
|
|
64
69
|
});
|
|
70
|
+
glassMaterial.userData.__pascalCachedMaterial = true;
|
|
71
|
+
function resolveNodeMaterialSide(side) {
|
|
72
|
+
return side === THREE.DoubleSide ? THREE.FrontSide : side;
|
|
73
|
+
}
|
|
65
74
|
const sideMap = {
|
|
66
75
|
front: THREE.FrontSide,
|
|
67
76
|
back: THREE.BackSide,
|
|
68
|
-
double: THREE.
|
|
77
|
+
double: THREE.FrontSide,
|
|
69
78
|
};
|
|
70
79
|
const materialCache = new Map();
|
|
71
80
|
const defaultMaterialCache = new Map();
|
|
@@ -73,6 +82,13 @@ const surfaceRoleMaterialCache = new Map();
|
|
|
73
82
|
const textureCache = new Map();
|
|
74
83
|
const textureLoadPromises = new Map();
|
|
75
84
|
const textureLoader = new THREE.TextureLoader();
|
|
85
|
+
// `.ktx2` finish maps transcode through the shared KTX2 loader (support is
|
|
86
|
+
// detected once at viewer init); everything else loads as a normal image.
|
|
87
|
+
function pickTextureLoader(url) {
|
|
88
|
+
// KTX2Loader's load/loadAsync are call-compatible with TextureLoader (url →
|
|
89
|
+
// Texture / Promise<Texture>); cast for typing.
|
|
90
|
+
return isKtx2Url(url) ? ktx2Loader : textureLoader;
|
|
91
|
+
}
|
|
76
92
|
const wrapMap = {
|
|
77
93
|
Repeat: THREE.RepeatWrapping,
|
|
78
94
|
ClampToEdge: THREE.ClampToEdgeWrapping,
|
|
@@ -100,13 +116,29 @@ function getTextureChannel(slot) {
|
|
|
100
116
|
function getCacheKey(props, shading) {
|
|
101
117
|
return `${shading}-${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`;
|
|
102
118
|
}
|
|
103
|
-
function
|
|
119
|
+
function isFiniteNumber(value) {
|
|
120
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
121
|
+
}
|
|
122
|
+
export function resolveTextureRepeat(repeat, scale) {
|
|
123
|
+
const fallback = isFiniteNumber(scale) ? scale : 1;
|
|
124
|
+
if (Array.isArray(repeat) && isFiniteNumber(repeat[0]) && isFiniteNumber(repeat[1])) {
|
|
125
|
+
return [repeat[0], repeat[1]];
|
|
126
|
+
}
|
|
127
|
+
if (isFiniteNumber(repeat))
|
|
128
|
+
return [repeat, repeat];
|
|
129
|
+
if (repeat && typeof repeat === 'object' && 'x' in repeat && 'y' in repeat) {
|
|
130
|
+
const { x, y } = repeat;
|
|
131
|
+
if (isFiniteNumber(x) && isFiniteNumber(y))
|
|
132
|
+
return [x, y];
|
|
133
|
+
}
|
|
134
|
+
return [fallback, fallback];
|
|
135
|
+
}
|
|
136
|
+
export function getTextureKey(material) {
|
|
104
137
|
const texture = material?.texture;
|
|
105
138
|
if (!texture)
|
|
106
139
|
return 'none';
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
return `${texture.url}-${repeat}-${scale}`;
|
|
140
|
+
const [repeatX, repeatY] = resolveTextureRepeat(texture.repeat, texture.scale);
|
|
141
|
+
return `${texture.url}-${repeatX}x${repeatY}`;
|
|
110
142
|
}
|
|
111
143
|
function getTexture(material) {
|
|
112
144
|
const textureConfig = material?.texture;
|
|
@@ -116,14 +148,21 @@ function getTexture(material) {
|
|
|
116
148
|
const cached = textureCache.get(cacheKey);
|
|
117
149
|
if (cached)
|
|
118
150
|
return cached;
|
|
119
|
-
const
|
|
151
|
+
const resolvedUrl = /^(?:asset|blob|data):/.test(textureConfig.url)
|
|
152
|
+
? textureConfig.url
|
|
153
|
+
: (resolveCdnUrl(textureConfig.url) ?? textureConfig.url);
|
|
154
|
+
const texture = pickTextureLoader(resolvedUrl).load(resolvedUrl);
|
|
120
155
|
texture.wrapS = THREE.RepeatWrapping;
|
|
121
156
|
texture.wrapT = THREE.RepeatWrapping;
|
|
122
|
-
const repeatX = textureConfig.repeat
|
|
123
|
-
const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1;
|
|
157
|
+
const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale);
|
|
124
158
|
texture.repeat.set(repeatX, repeatY);
|
|
125
159
|
texture.updateMatrix();
|
|
126
160
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
161
|
+
stampPascalTextureRef(texture, {
|
|
162
|
+
kind: 'project-asset',
|
|
163
|
+
src: resolvedUrl,
|
|
164
|
+
slot: 'map',
|
|
165
|
+
});
|
|
127
166
|
textureCache.set(cacheKey, texture);
|
|
128
167
|
return texture;
|
|
129
168
|
}
|
|
@@ -162,8 +201,13 @@ function getPresetTexture(path, props, slot) {
|
|
|
162
201
|
const cached = textureCache.get(cacheKey);
|
|
163
202
|
if (cached)
|
|
164
203
|
return cached;
|
|
165
|
-
const texture =
|
|
204
|
+
const texture = pickTextureLoader(resolvedPath).load(resolvedPath);
|
|
166
205
|
applyTextureProperties(texture, props, slot);
|
|
206
|
+
stampPascalTextureRef(texture, {
|
|
207
|
+
kind: 'material',
|
|
208
|
+
src: resolvedPath,
|
|
209
|
+
slot: slot ?? 'map',
|
|
210
|
+
});
|
|
167
211
|
setTextureCacheKey(texture, cacheKey);
|
|
168
212
|
textureCache.set(cacheKey, texture);
|
|
169
213
|
return texture;
|
|
@@ -195,10 +239,21 @@ async function loadPresetTexture(path, props, slot) {
|
|
|
195
239
|
const existingPromise = textureLoadPromises.get(cacheKey);
|
|
196
240
|
if (existingPromise)
|
|
197
241
|
return existingPromise;
|
|
198
|
-
|
|
199
|
-
|
|
242
|
+
// `.ktx2` loads wait for `detectSupport` (KTX2Loader.load throws before it) —
|
|
243
|
+
// materials can be created while a capture canvas's renderer is still
|
|
244
|
+
// initializing, and failing here would cache the material permanently
|
|
245
|
+
// texture-less (white).
|
|
246
|
+
const load = isKtx2Url(resolvedPath)
|
|
247
|
+
? whenKtx2Ready().then(() => ktx2Loader.loadAsync(resolvedPath))
|
|
248
|
+
: textureLoader.loadAsync(resolvedPath);
|
|
249
|
+
const promise = load
|
|
200
250
|
.then((texture) => {
|
|
201
251
|
applyTextureProperties(texture, props, slot);
|
|
252
|
+
stampPascalTextureRef(texture, {
|
|
253
|
+
kind: 'material',
|
|
254
|
+
src: resolvedPath,
|
|
255
|
+
slot: slot ?? 'map',
|
|
256
|
+
});
|
|
202
257
|
setTextureCacheKey(texture, cacheKey);
|
|
203
258
|
textureCache.set(cacheKey, texture);
|
|
204
259
|
textureLoadPromises.delete(cacheKey);
|
|
@@ -215,7 +270,13 @@ async function loadPresetTexture(path, props, slot) {
|
|
|
215
270
|
function queueTextureAssignment(material, slot, path, props) {
|
|
216
271
|
const textureMaterial = material;
|
|
217
272
|
if (!path) {
|
|
218
|
-
textureMaterial[slot]
|
|
273
|
+
if (textureMaterial[slot] != null) {
|
|
274
|
+
// Rebuild the node graph: a cached WebGPU material keeps a TextureNode
|
|
275
|
+
// for the slot, whose per-frame material reference would pull the null
|
|
276
|
+
// and crash in TextureNode.update ("null (reading 'matrix')").
|
|
277
|
+
textureMaterial[slot] = null;
|
|
278
|
+
material.needsUpdate = true;
|
|
279
|
+
}
|
|
219
280
|
return;
|
|
220
281
|
}
|
|
221
282
|
const resolvedPath = resolveCdnUrl(path) ?? path;
|
|
@@ -230,7 +291,14 @@ function queueTextureAssignment(material, slot, path, props) {
|
|
|
230
291
|
material.needsUpdate = true;
|
|
231
292
|
return;
|
|
232
293
|
}
|
|
233
|
-
|
|
294
|
+
// Cold load: clear the slot for the fetch window, and rebuild the node
|
|
295
|
+
// graph if it previously held a texture — reused cached materials otherwise
|
|
296
|
+
// keep a TextureNode whose reference pulls the null and crashes the render
|
|
297
|
+
// pass. Cold loads are the norm for freshly generated library materials.
|
|
298
|
+
if (textureMaterial[slot] != null) {
|
|
299
|
+
textureMaterial[slot] = null;
|
|
300
|
+
material.needsUpdate = true;
|
|
301
|
+
}
|
|
234
302
|
loadPresetTexture(path, props, slot).then((texture) => {
|
|
235
303
|
if (!texture)
|
|
236
304
|
return;
|
|
@@ -257,15 +325,35 @@ function applyMaterialMapProperties(material, mapProperties) {
|
|
|
257
325
|
}
|
|
258
326
|
material.transparent = mapProperties.transparent;
|
|
259
327
|
material.opacity = mapProperties.opacity;
|
|
260
|
-
material.side =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: THREE.DoubleSide;
|
|
328
|
+
material.side = resolveNodeMaterialSide(mapProperties.side === 0
|
|
329
|
+
? THREE.FrontSide
|
|
330
|
+
: mapProperties.side === 1
|
|
331
|
+
? THREE.BackSide
|
|
332
|
+
: THREE.DoubleSide);
|
|
266
333
|
applyTexturePropertiesToMaterial(material, mapProperties);
|
|
267
334
|
material.needsUpdate = true;
|
|
268
335
|
}
|
|
336
|
+
// Glass-like transparency threshold: any standard material authored as
|
|
337
|
+
// `transparent` with opacity below this gets the fresnel treatment.
|
|
338
|
+
const GLASS_OPACITY_THRESHOLD = 0.6;
|
|
339
|
+
/**
|
|
340
|
+
* Fresnel-driven opacity for glass: nearly the authored opacity head-on,
|
|
341
|
+
* increasingly opaque (showing the environment reflection) at grazing angles.
|
|
342
|
+
* This is what makes glass read as a surface instead of a flat blue tint.
|
|
343
|
+
*/
|
|
344
|
+
function applyGlassFresnel(material) {
|
|
345
|
+
const facing = transformedNormalView.dot(positionViewDirection).clamp(0, 1);
|
|
346
|
+
const fresnel = facing.oneMinus().pow(3);
|
|
347
|
+
material.opacityNode = mix(float(material.opacity), float(0.92), fresnel);
|
|
348
|
+
material.envMapIntensity = 1.4;
|
|
349
|
+
}
|
|
350
|
+
function maybeApplyGlassFresnel(material) {
|
|
351
|
+
if (material instanceof MeshStandardNodeMaterial &&
|
|
352
|
+
material.transparent &&
|
|
353
|
+
material.opacity < GLASS_OPACITY_THRESHOLD) {
|
|
354
|
+
applyGlassFresnel(material);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
269
357
|
function applyMaterialPresetTextures(material, preset) {
|
|
270
358
|
const { maps, mapProperties } = preset;
|
|
271
359
|
queueTextureAssignment(material, 'map', maps.albedoMap, mapProperties);
|
|
@@ -302,6 +390,8 @@ export function createMaterialFromPreset(preset, shading = 'rendered') {
|
|
|
302
390
|
}
|
|
303
391
|
const material = shading === 'solid' ? new MeshLambertNodeMaterial() : new MeshStandardNodeMaterial();
|
|
304
392
|
applyMaterialPresetToMaterials(material, preset);
|
|
393
|
+
maybeApplyGlassFresnel(material);
|
|
394
|
+
material.userData.__pascalCachedMaterial = true;
|
|
305
395
|
materialCache.set(cacheKey, material);
|
|
306
396
|
return material;
|
|
307
397
|
}
|
|
@@ -333,21 +423,53 @@ export function createMaterial(material, shading = 'rendered') {
|
|
|
333
423
|
roughness: props.roughness,
|
|
334
424
|
metalness: props.metalness,
|
|
335
425
|
});
|
|
426
|
+
maybeApplyGlassFresnel(threeMaterial);
|
|
427
|
+
threeMaterial.userData.__pascalCachedMaterial = true;
|
|
336
428
|
materialCache.set(cacheKey, threeMaterial);
|
|
337
429
|
return threeMaterial;
|
|
338
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Resolve a MaterialRef ('library:<id>' | 'scene:<id>') to a three.js material.
|
|
433
|
+
* Returns null for an unknown / dangling ref so callers fall back to the
|
|
434
|
+
* slot's default (authored material, then themed default). Never throws.
|
|
435
|
+
*/
|
|
436
|
+
export function resolveMaterialRef(ref, sceneMaterials, shading = 'rendered') {
|
|
437
|
+
const parsed = parseMaterialRef(ref);
|
|
438
|
+
if (!parsed)
|
|
439
|
+
return null;
|
|
440
|
+
if (parsed.kind === 'library')
|
|
441
|
+
return createMaterialFromPresetRef(ref, shading);
|
|
442
|
+
const sceneMaterial = sceneMaterials?.[parsed.id];
|
|
443
|
+
if (!sceneMaterial)
|
|
444
|
+
return null;
|
|
445
|
+
return createMaterial(sceneMaterial.material, shading);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Resolve a node kind's declared slot default — either a catalog `library:<id>`
|
|
449
|
+
* finish or a flat `#rrggbb` colour — to a renderable material. Shared by the
|
|
450
|
+
* procedural kinds whose colored-mode unpainted appearance comes from a
|
|
451
|
+
* declarative default (slab, wall).
|
|
452
|
+
*/
|
|
453
|
+
export function resolveSlotDefaultMaterial(slotDefault, shading = 'rendered', roughness = 0.9) {
|
|
454
|
+
if (parseMaterialRef(slotDefault)?.kind === 'library') {
|
|
455
|
+
return (createMaterialFromPresetRef(slotDefault, shading) ??
|
|
456
|
+
createDefaultMaterial('#ffffff', roughness, shading));
|
|
457
|
+
}
|
|
458
|
+
return createDefaultMaterial(slotDefault, roughness, shading);
|
|
459
|
+
}
|
|
339
460
|
export function createDefaultMaterial(color = '#ffffff', roughness = 0.9, shading = 'rendered', side = THREE.FrontSide) {
|
|
461
|
+
const resolvedSide = resolveNodeMaterialSide(side);
|
|
340
462
|
if (shading === 'solid') {
|
|
341
463
|
return new MeshLambertNodeMaterial({
|
|
342
464
|
color,
|
|
343
|
-
side,
|
|
465
|
+
side: resolvedSide,
|
|
344
466
|
});
|
|
345
467
|
}
|
|
346
468
|
return new MeshStandardNodeMaterial({
|
|
347
469
|
color,
|
|
348
470
|
roughness,
|
|
349
471
|
metalness: 0,
|
|
350
|
-
side,
|
|
472
|
+
side: resolvedSide,
|
|
351
473
|
});
|
|
352
474
|
}
|
|
353
475
|
function cachedDefaultMaterial(key, color, roughness, shading, side = THREE.FrontSide) {
|
|
@@ -356,6 +478,7 @@ function cachedDefaultMaterial(key, color, roughness, shading, side = THREE.Fron
|
|
|
356
478
|
if (cached)
|
|
357
479
|
return cached;
|
|
358
480
|
const material = createDefaultMaterial(color, roughness, shading, side);
|
|
481
|
+
material.userData.__pascalCachedMaterial = true;
|
|
359
482
|
defaultMaterialCache.set(cacheKey, material);
|
|
360
483
|
return material;
|
|
361
484
|
}
|
|
@@ -368,7 +491,7 @@ export function createSurfaceRoleMaterial(role, preset, side = THREE.FrontSide,
|
|
|
368
491
|
// on both gable faces on the first frame). Callers that need both sides
|
|
369
492
|
// visible (e.g. dormer back gable) must rotate the host mesh 180° so the
|
|
370
493
|
// FrontSide faces the viewer.
|
|
371
|
-
const resolvedSide = role === 'glazing' ? THREE.FrontSide : side;
|
|
494
|
+
const resolvedSide = role === 'glazing' ? THREE.FrontSide : resolveNodeMaterialSide(side ?? THREE.FrontSide);
|
|
372
495
|
const cacheKey = `${role}-${preset}-${resolvedSide}-${sceneThemeId ?? 'base'}`;
|
|
373
496
|
const cached = surfaceRoleMaterialCache.get(cacheKey);
|
|
374
497
|
if (cached)
|
|
@@ -390,10 +513,10 @@ export function createSurfaceRoleMaterial(role, preset, side = THREE.FrontSide,
|
|
|
390
513
|
return material;
|
|
391
514
|
}
|
|
392
515
|
export function baseMaterial(shading = 'rendered') {
|
|
393
|
-
return cachedDefaultMaterial('base', '#
|
|
516
|
+
return cachedDefaultMaterial('base', '#e9e7e3', 0.5, shading);
|
|
394
517
|
}
|
|
395
518
|
export function DEFAULT_WALL_MATERIAL(shading = 'rendered') {
|
|
396
|
-
return cachedDefaultMaterial('wall', '#
|
|
519
|
+
return cachedDefaultMaterial('wall', '#e9e6e0', 0.9, shading);
|
|
397
520
|
}
|
|
398
521
|
export function DEFAULT_SLAB_MATERIAL(shading = 'rendered') {
|
|
399
522
|
return cachedDefaultMaterial('slab', '#e5e5e5', 0.8, shading);
|
|
@@ -424,20 +547,21 @@ export function DEFAULT_WINDOW_MATERIAL(shading = 'rendered') {
|
|
|
424
547
|
roughness: 0.1,
|
|
425
548
|
metalness: 0.1,
|
|
426
549
|
});
|
|
550
|
+
maybeApplyGlassFresnel(material);
|
|
427
551
|
defaultMaterialCache.set(cacheKey, material);
|
|
428
552
|
return material;
|
|
429
553
|
}
|
|
430
554
|
export function DEFAULT_CEILING_MATERIAL(shading = 'rendered') {
|
|
431
|
-
return cachedDefaultMaterial('ceiling', '#
|
|
555
|
+
return cachedDefaultMaterial('ceiling', '#ebebd3', 0.95, shading);
|
|
432
556
|
}
|
|
433
557
|
export function DEFAULT_ROOF_MATERIAL(shading = 'rendered') {
|
|
434
558
|
return cachedDefaultMaterial('roof', '#808080', 0.85, shading);
|
|
435
559
|
}
|
|
436
560
|
export function DEFAULT_SHELF_MATERIAL(shading = 'rendered') {
|
|
437
|
-
return cachedDefaultMaterial('shelf', '#
|
|
561
|
+
return cachedDefaultMaterial('shelf', '#e9e6e0', 0.9, shading);
|
|
438
562
|
}
|
|
439
563
|
export function DEFAULT_STAIR_MATERIAL(shading = 'rendered') {
|
|
440
|
-
return cachedDefaultMaterial('stair', '#
|
|
564
|
+
return cachedDefaultMaterial('stair', '#e9e6e0', 0.9, shading);
|
|
441
565
|
}
|
|
442
566
|
export function disposeMaterial(material) {
|
|
443
567
|
material.dispose();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materials.test.d.ts","sourceRoot":"","sources":["../../src/lib/materials.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
|
|
2
|
+
// depend on @types/bun so the import type is unresolved at compile time.
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { getTextureKey, resolveTextureRepeat } from './materials';
|
|
5
|
+
function materialWithRepeat(repeat) {
|
|
6
|
+
return {
|
|
7
|
+
texture: {
|
|
8
|
+
url: 'https://example.com/texture.png',
|
|
9
|
+
repeat,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('legacy texture repeat values', () => {
|
|
14
|
+
test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
|
|
15
|
+
expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3]);
|
|
16
|
+
expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2]);
|
|
17
|
+
expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3]);
|
|
18
|
+
});
|
|
19
|
+
test('falls back to scale for malformed repeats', () => {
|
|
20
|
+
expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4]);
|
|
21
|
+
});
|
|
22
|
+
test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
|
|
23
|
+
expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(getTextureKey(materialWithRepeat({ x: 4, y: 5 })));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merged-outline-node.test.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
|
|
2
|
+
// depend on @types/bun so the import type is unresolved at compile time.
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { Object3D, PerspectiveCamera, Scene } from 'three';
|
|
5
|
+
import { mergedOutline } from './merged-outline-node';
|
|
6
|
+
describe('merged outline rendering', () => {
|
|
7
|
+
test('keeps selected outlines active during camera interaction', () => {
|
|
8
|
+
const cameraInteractionActive = true;
|
|
9
|
+
const params = {
|
|
10
|
+
enabled: () => !cameraInteractionActive,
|
|
11
|
+
primaryObjects: [new Object3D()],
|
|
12
|
+
};
|
|
13
|
+
const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params);
|
|
14
|
+
const frame = {
|
|
15
|
+
get renderer() {
|
|
16
|
+
throw new Error('outline renderer was reached');
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached');
|
|
20
|
+
outline.dispose();
|
|
21
|
+
});
|
|
22
|
+
});
|