@preference-sl/pref-viewer 2.17.0-beta.0 → 2.17.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 +48 -16
- package/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +12 -163
- package/src/babylonjs-animation-opening-menu.js +8 -1
- package/src/babylonjs-animation-opening.js +21 -1
- package/src/babylonjs-animation-overlay-controller.js +145 -0
- package/src/babylonjs-controller.js +332 -177
- package/src/babylonjs-selection-controller.js +215 -0
- package/src/babylonjs-view-cube-controller.js +169 -77
- package/src/file-storage.js +13 -0
- package/src/gltf-storage.js +97 -0
- package/src/localization/i18n.js +147 -74
- package/src/material-utils.js +67 -60
- package/src/panzoom-controller.js +14 -1
- package/src/pref-viewer-2d.js +16 -4
- package/src/pref-viewer-3d-data.js +166 -0
- package/src/pref-viewer-3d.js +11 -0
- package/src/pref-viewer-dialog.js +7 -1
- package/src/pref-viewer-menu-3d.js +55 -35
- package/src/pref-viewer.js +56 -20
- package/src/render-math.js +130 -117
- package/src/svg-resolver.js +5 -4
|
@@ -11,15 +11,12 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
11
|
import BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
12
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
13
|
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
14
|
+
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
14
15
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
15
|
-
import
|
|
16
|
+
import I18n from "./localization/i18n.js";
|
|
16
17
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
// Re-export so existing consumers (tests, other modules) can keep importing from this file.
|
|
21
|
-
export { getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
22
|
-
export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
18
|
+
import RenderMath from "./render-math.js";
|
|
19
|
+
import MaterialUtils from "./material-utils.js";
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -126,6 +123,7 @@ export default class BabylonJSController {
|
|
|
126
123
|
|
|
127
124
|
#gltfResolver = null; // GLTFResolver instance
|
|
128
125
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
126
|
+
#selectionController = null;
|
|
129
127
|
|
|
130
128
|
#renderPipelines = {
|
|
131
129
|
default: null,
|
|
@@ -136,6 +134,7 @@ export default class BabylonJSController {
|
|
|
136
134
|
#handlers = {
|
|
137
135
|
onAnimationGroupChanged: null,
|
|
138
136
|
onKeyUp: null,
|
|
137
|
+
onPointerCancel: null,
|
|
139
138
|
onPointerObservable: null,
|
|
140
139
|
onResize: null,
|
|
141
140
|
renderLoop: null,
|
|
@@ -151,6 +150,7 @@ export default class BabylonJSController {
|
|
|
151
150
|
lastMovePickX: NaN,
|
|
152
151
|
lastMovePickY: NaN,
|
|
153
152
|
lastPickedMeshId: null,
|
|
153
|
+
pointerDown: null,
|
|
154
154
|
},
|
|
155
155
|
// Render loop state balances performance with responsiveness.
|
|
156
156
|
render: {
|
|
@@ -219,6 +219,12 @@ export default class BabylonJSController {
|
|
|
219
219
|
pointerPicking: {
|
|
220
220
|
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
221
221
|
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
222
|
+
clickMaxDistancePx: 6,
|
|
223
|
+
},
|
|
224
|
+
selection: {
|
|
225
|
+
selectableColor: [1, 1, 0],
|
|
226
|
+
selectedColor: [1, 0, 0],
|
|
227
|
+
overlayAlpha: 0.5,
|
|
222
228
|
},
|
|
223
229
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
224
230
|
quality: {
|
|
@@ -320,6 +326,7 @@ export default class BabylonJSController {
|
|
|
320
326
|
* @private
|
|
321
327
|
* @param {WebGLRenderingContext} gl - WebGL context
|
|
322
328
|
* @param {string} rendererName - GPU renderer name
|
|
329
|
+
* @param {boolean} [conservative=false] - Uses a cautious fallback when the GPU renderer is unknown.
|
|
323
330
|
* @returns {string} Hardware tier
|
|
324
331
|
*/
|
|
325
332
|
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
@@ -429,6 +436,7 @@ export default class BabylonJSController {
|
|
|
429
436
|
* Applies quality settings based on detected hardware tier.
|
|
430
437
|
* @private
|
|
431
438
|
* @param {string} tier - Hardware tier: 'low', 'medium', or 'high'
|
|
439
|
+
* @returns {void}
|
|
432
440
|
*/
|
|
433
441
|
#applyQualitySettings(tier) {
|
|
434
442
|
const qualityPresets = {
|
|
@@ -538,6 +546,16 @@ export default class BabylonJSController {
|
|
|
538
546
|
};
|
|
539
547
|
});
|
|
540
548
|
this.#options = options;
|
|
549
|
+
const selectionConfig = BabylonJSController.config.selection;
|
|
550
|
+
this.#selectionController = new BabylonJSSelectionController(
|
|
551
|
+
() => this.#containers.model?.assetContainer?.meshes ?? [],
|
|
552
|
+
{
|
|
553
|
+
selectableColor: new Color3(...selectionConfig.selectableColor),
|
|
554
|
+
selectedColor: new Color3(...selectionConfig.selectedColor),
|
|
555
|
+
overlayAlpha: selectionConfig.overlayAlpha,
|
|
556
|
+
requestRender: () => this.#requestRender({ frames: 1 }),
|
|
557
|
+
},
|
|
558
|
+
);
|
|
541
559
|
this.#loadStoredRenderSettings();
|
|
542
560
|
this.#bindHandlers();
|
|
543
561
|
}
|
|
@@ -550,6 +568,7 @@ export default class BabylonJSController {
|
|
|
550
568
|
#bindHandlers() {
|
|
551
569
|
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
552
570
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
571
|
+
this.#handlers.onPointerCancel = this.#onPointerCancel.bind(this);
|
|
553
572
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
554
573
|
this.#handlers.onResize = this.#onResize.bind(this);
|
|
555
574
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
@@ -716,6 +735,11 @@ export default class BabylonJSController {
|
|
|
716
735
|
|
|
717
736
|
try {
|
|
718
737
|
await new Promise((resolve) => {
|
|
738
|
+
/**
|
|
739
|
+
* Polls BRDF readiness until ready, disposed or the preparation timeout elapses.
|
|
740
|
+
* @private
|
|
741
|
+
* @returns {void}
|
|
742
|
+
*/
|
|
719
743
|
const tick = () => {
|
|
720
744
|
if (!this.#scene || !this.#engine || brdfTexture.isDisposed?.()) {
|
|
721
745
|
resolve(false);
|
|
@@ -1395,7 +1419,7 @@ export default class BabylonJSController {
|
|
|
1395
1419
|
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
1396
1420
|
* Only applies if the scene has an environment texture set.
|
|
1397
1421
|
* @private
|
|
1398
|
-
* @returns {Promise<
|
|
1422
|
+
* @returns {Promise<boolean>} True when the IBL shadow pipeline is initialized; false when unavailable or setup fails.
|
|
1399
1423
|
*/
|
|
1400
1424
|
async #initializeIBLShadows() {
|
|
1401
1425
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
@@ -1524,7 +1548,7 @@ export default class BabylonJSController {
|
|
|
1524
1548
|
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
1525
1549
|
* Uses hardware-adaptive quality settings.
|
|
1526
1550
|
* @private
|
|
1527
|
-
* @returns {void}
|
|
1551
|
+
* @returns {Promise<void>}
|
|
1528
1552
|
*/
|
|
1529
1553
|
async #initializeDefaultLightShadows() {
|
|
1530
1554
|
if (!this.#dirLight) {
|
|
@@ -1550,7 +1574,7 @@ export default class BabylonJSController {
|
|
|
1550
1574
|
* Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
|
|
1551
1575
|
* then adds generators for every directional or spot light coming from the environment asset container.
|
|
1552
1576
|
* @private
|
|
1553
|
-
* @returns {void}
|
|
1577
|
+
* @returns {Promise<void>}
|
|
1554
1578
|
*/
|
|
1555
1579
|
async #initializeEnvironmentShadows() {
|
|
1556
1580
|
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
@@ -1599,7 +1623,7 @@ export default class BabylonJSController {
|
|
|
1599
1623
|
* Disposes every active shadow generator plus the IBL shadow pipeline to avoid stale casters across reloads.
|
|
1600
1624
|
* Clears the cached `#shadowGen` array so subsequent loads can rebuild fresh generators.
|
|
1601
1625
|
* @private
|
|
1602
|
-
* @returns {void}
|
|
1626
|
+
* @returns {Promise<void>}
|
|
1603
1627
|
*/
|
|
1604
1628
|
async #disableShadows() {
|
|
1605
1629
|
this.#shadowGen.forEach((shadowGenerator) => {
|
|
@@ -1612,10 +1636,10 @@ export default class BabylonJSController {
|
|
|
1612
1636
|
/**
|
|
1613
1637
|
* Initializes shadows for the Babylon.js scene.
|
|
1614
1638
|
* @private
|
|
1615
|
-
* @returns {void|
|
|
1639
|
+
* @returns {Promise<void|false>} Resolves after shadow setup, or false when shadows are disabled.
|
|
1616
1640
|
* @description
|
|
1617
|
-
*
|
|
1618
|
-
* Otherwise,
|
|
1641
|
+
* Uses IBL shadows only when IBL is enabled with a valid source and its shadow option is active.
|
|
1642
|
+
* Otherwise initializes default-light shadows, then adds shadow support for environment lights.
|
|
1619
1643
|
*/
|
|
1620
1644
|
async #initializeShadows() {
|
|
1621
1645
|
if (!this.#renderSettings.shadowsEnabled) {
|
|
@@ -1726,16 +1750,16 @@ export default class BabylonJSController {
|
|
|
1726
1750
|
}
|
|
1727
1751
|
|
|
1728
1752
|
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1729
|
-
const timeOfDay = clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1730
|
-
const profile = getLightingCycleProfile(timeOfDay);
|
|
1753
|
+
const timeOfDay = RenderMath.clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1754
|
+
const profile = RenderMath.getLightingCycleProfile(timeOfDay);
|
|
1731
1755
|
const daylight = profile.dayFactor;
|
|
1732
1756
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1733
1757
|
const twilight = 1 - peakLight;
|
|
1734
1758
|
|
|
1735
1759
|
if (this.#scene.imageProcessingConfiguration) {
|
|
1736
1760
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1737
|
-
imageProcessing.exposure = lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1738
|
-
imageProcessing.contrast = lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1761
|
+
imageProcessing.exposure = RenderMath.lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1762
|
+
imageProcessing.contrast = RenderMath.lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1739
1763
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1740
1764
|
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1741
1765
|
imageProcessing.vignetteEnabled = false;
|
|
@@ -1746,57 +1770,57 @@ export default class BabylonJSController {
|
|
|
1746
1770
|
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1747
1771
|
const bgDay = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1748
1772
|
this.#scene.clearColor = new Color4(
|
|
1749
|
-
lerp(0.000627, bgDay[0], daylight),
|
|
1750
|
-
lerp(0.001089, bgDay[1], daylight),
|
|
1751
|
-
lerp(0.004413, bgDay[2], daylight),
|
|
1773
|
+
RenderMath.lerp(0.000627, bgDay[0], daylight),
|
|
1774
|
+
RenderMath.lerp(0.001089, bgDay[1], daylight),
|
|
1775
|
+
RenderMath.lerp(0.004413, bgDay[2], daylight),
|
|
1752
1776
|
1,
|
|
1753
1777
|
);
|
|
1754
1778
|
|
|
1755
1779
|
if ("environmentIntensity" in this.#scene) {
|
|
1756
|
-
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1780
|
+
this.#scene.environmentIntensity = RenderMath.lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1757
1781
|
}
|
|
1758
1782
|
|
|
1759
1783
|
if (this.#hemiLight) {
|
|
1760
1784
|
this.#hemiLight.direction = new Vector3(
|
|
1761
|
-
lerp(-0.45, -0.8, peakLight),
|
|
1762
|
-
lerp(0.45, 0.95, peakLight),
|
|
1763
|
-
lerp(-0.75, -0.35, peakLight),
|
|
1785
|
+
RenderMath.lerp(-0.45, -0.8, peakLight),
|
|
1786
|
+
RenderMath.lerp(0.45, 0.95, peakLight),
|
|
1787
|
+
RenderMath.lerp(-0.75, -0.35, peakLight),
|
|
1764
1788
|
);
|
|
1765
|
-
this.#hemiLight.intensity = lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1789
|
+
this.#hemiLight.intensity = RenderMath.lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1766
1790
|
this.#hemiLight.diffuse = new Color3(
|
|
1767
|
-
lerp(0.42, 0.92, peakLight),
|
|
1768
|
-
lerp(0.48, 0.96, peakLight),
|
|
1769
|
-
lerp(0.62, 0.98, peakLight),
|
|
1791
|
+
RenderMath.lerp(0.42, 0.92, peakLight),
|
|
1792
|
+
RenderMath.lerp(0.48, 0.96, peakLight),
|
|
1793
|
+
RenderMath.lerp(0.62, 0.98, peakLight),
|
|
1770
1794
|
);
|
|
1771
1795
|
this.#hemiLight.groundColor = new Color3(
|
|
1772
|
-
lerp(0.08, 0.3, twilight),
|
|
1773
|
-
lerp(0.08, 0.28, twilight),
|
|
1774
|
-
lerp(0.1, 0.22, twilight),
|
|
1796
|
+
RenderMath.lerp(0.08, 0.3, twilight),
|
|
1797
|
+
RenderMath.lerp(0.08, 0.28, twilight),
|
|
1798
|
+
RenderMath.lerp(0.1, 0.22, twilight),
|
|
1775
1799
|
);
|
|
1776
1800
|
}
|
|
1777
1801
|
|
|
1778
1802
|
if (this.#dirLight) {
|
|
1779
1803
|
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1780
|
-
this.#dirLight.intensity = lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1804
|
+
this.#dirLight.intensity = RenderMath.lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1781
1805
|
this.#dirLight.diffuse = new Color3(
|
|
1782
|
-
lerp(0.28, 1.0, peakLight),
|
|
1783
|
-
lerp(0.3, 0.97, peakLight),
|
|
1784
|
-
lerp(0.34, 0.9, peakLight),
|
|
1806
|
+
RenderMath.lerp(0.28, 1.0, peakLight),
|
|
1807
|
+
RenderMath.lerp(0.3, 0.97, peakLight),
|
|
1808
|
+
RenderMath.lerp(0.34, 0.9, peakLight),
|
|
1785
1809
|
);
|
|
1786
1810
|
}
|
|
1787
1811
|
|
|
1788
1812
|
if (this.#moonLight) {
|
|
1789
1813
|
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1790
|
-
this.#moonLight.intensity = lerp(0.44, 0.03, peakLight);
|
|
1814
|
+
this.#moonLight.intensity = RenderMath.lerp(0.44, 0.03, peakLight);
|
|
1791
1815
|
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1792
1816
|
}
|
|
1793
1817
|
|
|
1794
1818
|
if (this.#cameraLight) {
|
|
1795
|
-
this.#cameraLight.intensity = lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1819
|
+
this.#cameraLight.intensity = RenderMath.lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1796
1820
|
this.#cameraLight.diffuse = new Color3(
|
|
1797
|
-
lerp(0.35, 1.0, peakLight),
|
|
1798
|
-
lerp(0.35, 0.97, peakLight),
|
|
1799
|
-
lerp(0.36, 0.92, peakLight),
|
|
1821
|
+
RenderMath.lerp(0.35, 1.0, peakLight),
|
|
1822
|
+
RenderMath.lerp(0.35, 0.97, peakLight),
|
|
1823
|
+
RenderMath.lerp(0.36, 0.92, peakLight),
|
|
1800
1824
|
);
|
|
1801
1825
|
}
|
|
1802
1826
|
|
|
@@ -1818,7 +1842,7 @@ export default class BabylonJSController {
|
|
|
1818
1842
|
|
|
1819
1843
|
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1820
1844
|
const baseScaling = BabylonJSController.config.quality.hardwareScaling || 1;
|
|
1821
|
-
const scalingLevel = getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1845
|
+
const scalingLevel = RenderMath.getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1822
1846
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1823
1847
|
}
|
|
1824
1848
|
|
|
@@ -1901,6 +1925,7 @@ export default class BabylonJSController {
|
|
|
1901
1925
|
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1902
1926
|
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1903
1927
|
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1928
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
1904
1929
|
}
|
|
1905
1930
|
|
|
1906
1931
|
/**
|
|
@@ -1948,6 +1973,7 @@ export default class BabylonJSController {
|
|
|
1948
1973
|
#enableInteraction() {
|
|
1949
1974
|
if (this.#canvas) {
|
|
1950
1975
|
this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
|
|
1976
|
+
this.#canvas.addEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
1951
1977
|
}
|
|
1952
1978
|
if (this.#scene) {
|
|
1953
1979
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
@@ -1966,6 +1992,7 @@ export default class BabylonJSController {
|
|
|
1966
1992
|
#disableInteraction() {
|
|
1967
1993
|
if (this.#canvas) {
|
|
1968
1994
|
this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
|
|
1995
|
+
this.#canvas.removeEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
1969
1996
|
}
|
|
1970
1997
|
if (this.#scene !== null) {
|
|
1971
1998
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
@@ -2188,13 +2215,13 @@ export default class BabylonJSController {
|
|
|
2188
2215
|
}
|
|
2189
2216
|
|
|
2190
2217
|
/**
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2218
|
+
* Handles pointer up events on the Babylon.js scene.
|
|
2219
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2220
|
+
* @private
|
|
2221
|
+
* @param {PointerEvent} event - The pointer up event.
|
|
2222
|
+
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
2223
|
+
* @returns {void}
|
|
2224
|
+
*/
|
|
2198
2225
|
#onPointerUp(event, pickInfo) {
|
|
2199
2226
|
// OPTIMIZATION: Mark activity for idle mode
|
|
2200
2227
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
@@ -2241,6 +2268,49 @@ export default class BabylonJSController {
|
|
|
2241
2268
|
}
|
|
2242
2269
|
}
|
|
2243
2270
|
|
|
2271
|
+
/**
|
|
2272
|
+
* Invalidates an incomplete click gesture when the browser cancels its pointer.
|
|
2273
|
+
* @private
|
|
2274
|
+
* @returns {void}
|
|
2275
|
+
*/
|
|
2276
|
+
#onPointerCancel() {
|
|
2277
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/**
|
|
2281
|
+
* Emits the internal 3D click contract for every model mesh crossed by the pointer ray.
|
|
2282
|
+
* @private
|
|
2283
|
+
* @returns {void}
|
|
2284
|
+
*/
|
|
2285
|
+
#emitModelMeshClick() {
|
|
2286
|
+
const modelMeshes = this.#containers.model?.assetContainer?.meshes ?? [];
|
|
2287
|
+
const acceptedMeshes = BabylonJSSelectionController.getModelMeshSet(modelMeshes);
|
|
2288
|
+
if (!this.#scene || acceptedMeshes.size === 0) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const ray = this.#scene.createPickingRay(
|
|
2292
|
+
this.#scene.pointerX,
|
|
2293
|
+
this.#scene.pointerY,
|
|
2294
|
+
Matrix.Identity(),
|
|
2295
|
+
this.#scene.activeCamera,
|
|
2296
|
+
false,
|
|
2297
|
+
);
|
|
2298
|
+
const picks = this.#scene.multiPickWithRay(ray, (mesh) => BabylonJSSelectionController.isPickableModelMesh(mesh, acceptedMeshes)) ?? [];
|
|
2299
|
+
const ids = BabylonJSSelectionController.collectPickedMeshIds(picks, modelMeshes);
|
|
2300
|
+
if (ids.length === 0) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
this.#getPrefViewer3DComponent();
|
|
2304
|
+
const CustomEventType = this.#prefViewer3D?.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
|
|
2305
|
+
if (this.#prefViewer3D && CustomEventType) {
|
|
2306
|
+
this.#prefViewer3D.dispatchEvent(new CustomEventType("pref-viewer-3d-element-click", {
|
|
2307
|
+
bubbles: true,
|
|
2308
|
+
composed: true,
|
|
2309
|
+
detail: { ids },
|
|
2310
|
+
}));
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2244
2314
|
/**
|
|
2245
2315
|
* Handles pointer events observed on the Babylon.js scene.
|
|
2246
2316
|
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
@@ -2250,6 +2320,11 @@ export default class BabylonJSController {
|
|
|
2250
2320
|
*/
|
|
2251
2321
|
#onPointerObservable(info) {
|
|
2252
2322
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
2323
|
+
const start = this.#state.pointerPicking.pointerDown;
|
|
2324
|
+
if (start && (info.event?.pointerId !== start.pointerId || (info.event?.buttons & 1) === 0
|
|
2325
|
+
|| Math.hypot(info.event?.clientX - start.x, info.event?.clientY - start.y) > BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2326
|
+
start.dragged = true;
|
|
2327
|
+
}
|
|
2253
2328
|
let pickInfo = null;
|
|
2254
2329
|
let lastPickedMeshId = null;
|
|
2255
2330
|
if (this.#shouldPickOnPointerMove()) {
|
|
@@ -2258,7 +2333,17 @@ export default class BabylonJSController {
|
|
|
2258
2333
|
}
|
|
2259
2334
|
this.#onPointerMove(info.event, pickInfo);
|
|
2260
2335
|
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
2336
|
+
} else if (info.type === PointerEventTypes.POINTERDOWN) {
|
|
2337
|
+
this.#state.pointerPicking.pointerDown = info.event?.button === 0 && !this.#state.pointerPicking.pointerDown
|
|
2338
|
+
? { x: info.event.clientX, y: info.event.clientY, button: info.event.button, pointerId: info.event.pointerId }
|
|
2339
|
+
: null;
|
|
2261
2340
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
2341
|
+
const pointerDown = this.#state.pointerPicking.pointerDown;
|
|
2342
|
+
this.#state.pointerPicking.pointerDown = null;
|
|
2343
|
+
const pointerUp = { x: info.event?.clientX, y: info.event?.clientY, button: info.event?.button, pointerId: info.event?.pointerId };
|
|
2344
|
+
if (BabylonJSSelectionController.isPrimaryClickGesture(pointerDown, pointerUp, BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2345
|
+
this.#emitModelMeshClick();
|
|
2346
|
+
}
|
|
2262
2347
|
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
2263
2348
|
if (pickInfo) {
|
|
2264
2349
|
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
@@ -2269,6 +2354,149 @@ export default class BabylonJSController {
|
|
|
2269
2354
|
}
|
|
2270
2355
|
}
|
|
2271
2356
|
|
|
2357
|
+
/**
|
|
2358
|
+
* Applies a pending engine resize, cancels cube navigation and wakes rendering.
|
|
2359
|
+
* @private
|
|
2360
|
+
* @param {number} elapsed - Time since the previous resize, retained for diagnostics.
|
|
2361
|
+
* @returns {void}
|
|
2362
|
+
*/
|
|
2363
|
+
#applyResize(elapsed) {
|
|
2364
|
+
this.#state.resize.timeoutId = null;
|
|
2365
|
+
this.#state.resize.isScheduled = false;
|
|
2366
|
+
if (!this.#engine) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2370
|
+
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2371
|
+
this.#applyShowroomCameraFraming();
|
|
2372
|
+
this.#engine.resize();
|
|
2373
|
+
const needsEnhancedBurst = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled || this.#renderSettings.shadowsEnabled;
|
|
2374
|
+
const frames = needsEnhancedBurst ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2375
|
+
this.#requestRender({ frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/**
|
|
2379
|
+
* Extracts the final segment of a slash-containing metadata path, returning false otherwise.
|
|
2380
|
+
* @private
|
|
2381
|
+
* @static
|
|
2382
|
+
* @param {unknown} data - Metadata path candidate.
|
|
2383
|
+
* @returns {string|false}
|
|
2384
|
+
*/
|
|
2385
|
+
static #getNodeName(data) {
|
|
2386
|
+
if (data && typeof data === "string" && data.includes("/")) {
|
|
2387
|
+
data = data.slice(data.lastIndexOf("/") + 1);
|
|
2388
|
+
return data;
|
|
2389
|
+
}
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* Converts a vector to serializable coordinates, returning null for an absent value.
|
|
2395
|
+
* @private
|
|
2396
|
+
* @static
|
|
2397
|
+
* @param {Vector3|null|undefined} value - Vector to summarize.
|
|
2398
|
+
* @returns {{x:number,y:number,z:number}|null}
|
|
2399
|
+
*/
|
|
2400
|
+
static #vectorToJSON(value) {
|
|
2401
|
+
return value ? { x: value.x, y: value.y, z: value.z } : null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Summarizes texture identity and readiness without including its contents.
|
|
2406
|
+
* @private
|
|
2407
|
+
* @static
|
|
2408
|
+
* @param {object|null|undefined} texture - Babylon texture to inspect.
|
|
2409
|
+
* @returns {object}
|
|
2410
|
+
*/
|
|
2411
|
+
static #textureToJSON(texture) {
|
|
2412
|
+
return {
|
|
2413
|
+
name: texture?.name ?? null,
|
|
2414
|
+
url: texture?.url ?? null,
|
|
2415
|
+
isReady: texture?.isReady?.() ?? null,
|
|
2416
|
+
hasTexture: Boolean(texture),
|
|
2417
|
+
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
/**
|
|
2422
|
+
* Summarizes material readiness, related geometry and texture diagnostics, capturing readiness errors.
|
|
2423
|
+
* @private
|
|
2424
|
+
* @static
|
|
2425
|
+
* @param {Material} material - Material to inspect.
|
|
2426
|
+
* @param {Array<object>} sceneMeshes - Current scene mesh snapshot used to resolve material owners.
|
|
2427
|
+
* @returns {object}
|
|
2428
|
+
*/
|
|
2429
|
+
static #summarizeMaterial(material, sceneMeshes) {
|
|
2430
|
+
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
2431
|
+
const sampleMesh = relatedMeshes[0] ?? null;
|
|
2432
|
+
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
2433
|
+
let isReadyWithoutMesh = null;
|
|
2434
|
+
let isReadyWithMesh = null;
|
|
2435
|
+
try {
|
|
2436
|
+
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
2439
|
+
}
|
|
2440
|
+
try {
|
|
2441
|
+
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return {
|
|
2447
|
+
name: material?.name ?? null,
|
|
2448
|
+
id: material?.id ?? null,
|
|
2449
|
+
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
2450
|
+
isReadyWithoutMesh,
|
|
2451
|
+
isReadyWithMesh,
|
|
2452
|
+
relatedMeshes: relatedMeshes.length,
|
|
2453
|
+
sampleMesh: sampleMesh ? {
|
|
2454
|
+
name: sampleMesh.name,
|
|
2455
|
+
id: sampleMesh.id,
|
|
2456
|
+
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
2457
|
+
isVisible: sampleMesh.isVisible ?? null,
|
|
2458
|
+
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
2459
|
+
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
2460
|
+
} : null,
|
|
2461
|
+
textures: activeTextures.map(BabylonJSController.#textureToJSON),
|
|
2462
|
+
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(BabylonJSController.#textureToJSON),
|
|
2463
|
+
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
2464
|
+
transparencyMode: material?.transparencyMode ?? null,
|
|
2465
|
+
alpha: material?.alpha ?? null,
|
|
2466
|
+
backFaceCulling: material?.backFaceCulling ?? null,
|
|
2467
|
+
disableLighting: material?.disableLighting ?? null,
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/**
|
|
2472
|
+
* Summarizes container state and counts of its assets.
|
|
2473
|
+
* @private
|
|
2474
|
+
* @static
|
|
2475
|
+
* @param {object|null|undefined} container - Viewer container and its state.
|
|
2476
|
+
* @returns {object}
|
|
2477
|
+
*/
|
|
2478
|
+
static #summarizeContainer(container) {
|
|
2479
|
+
return {
|
|
2480
|
+
show: container?.state?.show ?? null,
|
|
2481
|
+
visible: container?.state?.visible ?? null,
|
|
2482
|
+
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
2483
|
+
isPending: container?.state?.isPending ?? null,
|
|
2484
|
+
isSuccess: container?.state?.isSuccess ?? null,
|
|
2485
|
+
hasAssetContainer: Boolean(container?.assetContainer),
|
|
2486
|
+
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
2487
|
+
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
2488
|
+
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
2489
|
+
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
2490
|
+
name: item.name,
|
|
2491
|
+
id: item.id,
|
|
2492
|
+
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
2493
|
+
position: BabylonJSController.#vectorToJSON(item.position),
|
|
2494
|
+
target: BabylonJSController.#vectorToJSON(item.target),
|
|
2495
|
+
})) ?? [],
|
|
2496
|
+
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2272
2500
|
/**
|
|
2273
2501
|
* Handles canvas resize notifications.
|
|
2274
2502
|
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
@@ -2282,23 +2510,10 @@ export default class BabylonJSController {
|
|
|
2282
2510
|
}
|
|
2283
2511
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2284
2512
|
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
2285
|
-
|
|
2286
|
-
this.#state.resize.timeoutId = null;
|
|
2287
|
-
this.#state.resize.isScheduled = false;
|
|
2288
|
-
if (!this.#engine) {
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2292
|
-
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2293
|
-
this.#applyShowroomCameraFraming();
|
|
2294
|
-
this.#engine.resize();
|
|
2295
|
-
const needsEnhancedBurst = this.#renderSettings.antiAliasingEnabled || this.#renderSettings.ambientOcclusionEnabled || this.#renderSettings.iblEnabled || this.#renderSettings.shadowsEnabled;
|
|
2296
|
-
const frames = needsEnhancedBurst ? BabylonJSController.config.render.burstFramesEnhanced : BabylonJSController.config.render.burstFramesBase;
|
|
2297
|
-
this.#requestRender({ frames, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2298
|
-
};
|
|
2513
|
+
|
|
2299
2514
|
|
|
2300
2515
|
if (elapsed >= BabylonJSController.config.resize.throttleMs) {
|
|
2301
|
-
applyResize();
|
|
2516
|
+
this.#applyResize(elapsed);
|
|
2302
2517
|
return;
|
|
2303
2518
|
}
|
|
2304
2519
|
|
|
@@ -2309,7 +2524,7 @@ export default class BabylonJSController {
|
|
|
2309
2524
|
this.#state.resize.isScheduled = true;
|
|
2310
2525
|
const waitMs = Math.max(0, BabylonJSController.config.resize.throttleMs - elapsed);
|
|
2311
2526
|
this.#state.resize.timeoutId = setTimeout(() => {
|
|
2312
|
-
applyResize();
|
|
2527
|
+
this.#applyResize(elapsed);
|
|
2313
2528
|
}, waitMs);
|
|
2314
2529
|
}
|
|
2315
2530
|
|
|
@@ -2396,6 +2611,7 @@ export default class BabylonJSController {
|
|
|
2396
2611
|
* Cancels pending cube navigation and applies configured camera selection and locking.
|
|
2397
2612
|
* Configures unlocked orbital cameras for proportional native wheel input, rebinds the model zoom guard
|
|
2398
2613
|
* and synchronizes the cube after selecting the active camera.
|
|
2614
|
+
* A missing named camera falls back to the unlocked default camera, rather than retaining an imported view.
|
|
2399
2615
|
* @private
|
|
2400
2616
|
* @returns {boolean} True if camera options were set successfully, false otherwise.
|
|
2401
2617
|
*/
|
|
@@ -2418,31 +2634,22 @@ export default class BabylonJSController {
|
|
|
2418
2634
|
}
|
|
2419
2635
|
|
|
2420
2636
|
if (!camera) {
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
// If the current camera (different from the default) was found, use it
|
|
2427
|
-
camera.metadata = { locked: cameraState.locked };
|
|
2428
|
-
cameraState.setSuccess(false);
|
|
2429
|
-
} else {
|
|
2430
|
-
// If no camera was found, use the default camera
|
|
2431
|
-
camera = this.#camera;
|
|
2432
|
-
cameraState.value = null;
|
|
2433
|
-
cameraState.locked = this.#camera.metadata.locked;
|
|
2434
|
-
cameraState.setSuccess(false);
|
|
2435
|
-
}
|
|
2637
|
+
camera = this.#camera;
|
|
2638
|
+
camera.metadata = { ...camera.metadata, locked: false };
|
|
2639
|
+
cameraState.value = null;
|
|
2640
|
+
cameraState.locked = false;
|
|
2641
|
+
cameraState.setSuccess(false);
|
|
2436
2642
|
} else {
|
|
2437
2643
|
// If the requested camera was found, use it
|
|
2438
2644
|
if (cameraName !== null) {
|
|
2439
2645
|
camera.metadata = { locked: cameraState.isPending ? cameraState.update.locked : cameraState.locked };
|
|
2440
2646
|
} else {
|
|
2441
|
-
//
|
|
2647
|
+
// Selecting the default camera always restores interactive navigation.
|
|
2648
|
+
camera.metadata = { ...camera.metadata, locked: false };
|
|
2442
2649
|
if (cameraState.isPending) {
|
|
2443
|
-
cameraState.update.locked =
|
|
2650
|
+
cameraState.update.locked = false;
|
|
2444
2651
|
} else {
|
|
2445
|
-
cameraState.locked =
|
|
2652
|
+
cameraState.locked = false;
|
|
2446
2653
|
}
|
|
2447
2654
|
}
|
|
2448
2655
|
if (cameraState.isPending) {
|
|
@@ -2469,7 +2676,7 @@ export default class BabylonJSController {
|
|
|
2469
2676
|
* Applies pending image-based lighting (IBL) option updates.
|
|
2470
2677
|
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
2471
2678
|
* @private
|
|
2472
|
-
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
2679
|
+
* @returns {Promise<boolean>} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
2473
2680
|
* @description
|
|
2474
2681
|
* Delegates to `#createLights()`, which executes the full IBL URL-to-texture lifecycle:
|
|
2475
2682
|
* `cachedUrl -> HDRCubeTexture -> #hdrTexture clone -> consumeCachedUrl(true)`.
|
|
@@ -2618,6 +2825,7 @@ export default class BabylonJSController {
|
|
|
2618
2825
|
* Stops every animation group on the provided asset container to guarantee new loads start from a clean state.
|
|
2619
2826
|
* @private
|
|
2620
2827
|
* @param {AssetContainer} assetContainer - Container whose animation groups should be halted.
|
|
2828
|
+
* @returns {void}
|
|
2621
2829
|
*/
|
|
2622
2830
|
#assetContainer_stopAnimations(assetContainer) {
|
|
2623
2831
|
if (!assetContainer.animationGroups || assetContainer.animationGroups.length === 0) {
|
|
@@ -2679,7 +2887,7 @@ export default class BabylonJSController {
|
|
|
2679
2887
|
mat.subSurface.isTranslucencyEnabled = false;
|
|
2680
2888
|
mat.alpha = Math.max(0.28, 1.0 - intensity * 0.65);
|
|
2681
2889
|
mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
|
2682
|
-
markPrefViewerSimplifiedTransmissionMaterial(mat);
|
|
2890
|
+
MaterialUtils.markPrefViewerSimplifiedTransmissionMaterial(mat);
|
|
2683
2891
|
}
|
|
2684
2892
|
|
|
2685
2893
|
/**
|
|
@@ -2717,7 +2925,7 @@ export default class BabylonJSController {
|
|
|
2717
2925
|
/**
|
|
2718
2926
|
* Stops the Babylon.js render loop for the current scene.
|
|
2719
2927
|
* @private
|
|
2720
|
-
* @returns {void}
|
|
2928
|
+
* @returns {Promise<void>}
|
|
2721
2929
|
*/
|
|
2722
2930
|
async #stopRender() {
|
|
2723
2931
|
this.#stopEngineRenderLoop();
|
|
@@ -2789,6 +2997,9 @@ export default class BabylonJSController {
|
|
|
2789
2997
|
compileMaterials: true,
|
|
2790
2998
|
loadAllMaterials: true,
|
|
2791
2999
|
loadOnlyMaterials: container.state.name === "materials",
|
|
3000
|
+
// InstancedMesh overlays render through their source mesh. Regular model
|
|
3001
|
+
// meshes keep shared materials while allowing independent semantic colors.
|
|
3002
|
+
createInstances: container.state.name !== "model",
|
|
2792
3003
|
extensionOptions: {
|
|
2793
3004
|
ExtrasAsMetadata: { enabled: true },
|
|
2794
3005
|
},
|
|
@@ -2822,6 +3033,7 @@ export default class BabylonJSController {
|
|
|
2822
3033
|
*/
|
|
2823
3034
|
async #loadContainers(force = false) {
|
|
2824
3035
|
this.#viewCube?.cancel();
|
|
3036
|
+
this.#resetPointerPickingState();
|
|
2825
3037
|
this.#detachAnimationChangedListener();
|
|
2826
3038
|
await this.#stopRender();
|
|
2827
3039
|
|
|
@@ -2891,6 +3103,7 @@ export default class BabylonJSController {
|
|
|
2891
3103
|
this.#setMaxSimultaneousLights();
|
|
2892
3104
|
this.#enhanceTextureQuality();
|
|
2893
3105
|
this.#syncAnimationControllerAfterModelLoad();
|
|
3106
|
+
this.#selectionController?.refresh();
|
|
2894
3107
|
|
|
2895
3108
|
await this.#startRender();
|
|
2896
3109
|
});
|
|
@@ -2922,7 +3135,7 @@ export default class BabylonJSController {
|
|
|
2922
3135
|
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
2923
3136
|
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
2924
3137
|
* @private
|
|
2925
|
-
* @returns {void}
|
|
3138
|
+
* @returns {Promise<void>}
|
|
2926
3139
|
*/
|
|
2927
3140
|
async #loadCameraDependentEffects() {
|
|
2928
3141
|
await this.#initializeVisualImprovements();
|
|
@@ -2934,7 +3147,7 @@ export default class BabylonJSController {
|
|
|
2934
3147
|
* Shuts down every post-process tied to the active camera before stopping the render loop.
|
|
2935
3148
|
* Ensures AA, SSAO, and shadow resources detach cleanly so reloads rebuild from scratch.
|
|
2936
3149
|
* @private
|
|
2937
|
-
* @returns {void}
|
|
3150
|
+
* @returns {Promise<void>}
|
|
2938
3151
|
*/
|
|
2939
3152
|
async #unloadCameraDependentEffects() {
|
|
2940
3153
|
await this.#disableVisualImprovements();
|
|
@@ -2975,19 +3188,13 @@ export default class BabylonJSController {
|
|
|
2975
3188
|
return;
|
|
2976
3189
|
}
|
|
2977
3190
|
|
|
2978
|
-
|
|
2979
|
-
if (data && typeof data === "string" && data.includes("/")) {
|
|
2980
|
-
data = data.slice(data.lastIndexOf("/") + 1);
|
|
2981
|
-
return data;
|
|
2982
|
-
}
|
|
2983
|
-
return false;
|
|
2984
|
-
};
|
|
3191
|
+
|
|
2985
3192
|
|
|
2986
3193
|
let applyNewTranslation = false;
|
|
2987
3194
|
let applyOldTranslation = false;
|
|
2988
3195
|
let undoOldTranslation = false;
|
|
2989
|
-
const newTranslationNodeName = getNodeName(newMetadata?.customProperties?.innerFloorPath);
|
|
2990
|
-
const oldTranslationNodeName = getNodeName(oldMetadata?.customProperties?.innerFloorPath);
|
|
3196
|
+
const newTranslationNodeName = BabylonJSController.#getNodeName(newMetadata?.customProperties?.innerFloorPath);
|
|
3197
|
+
const oldTranslationNodeName = BabylonJSController.#getNodeName(oldMetadata?.customProperties?.innerFloorPath);
|
|
2991
3198
|
|
|
2992
3199
|
if (this.#containers.environment.state.isSuccess) {
|
|
2993
3200
|
// If environment is being reloaded:
|
|
@@ -3100,7 +3307,7 @@ export default class BabylonJSController {
|
|
|
3100
3307
|
}
|
|
3101
3308
|
|
|
3102
3309
|
const locale = this.#prefViewer.culture;
|
|
3103
|
-
const texts = translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3310
|
+
const texts = I18n.translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3104
3311
|
const header = texts.title || "Download 3D Scene";
|
|
3105
3312
|
const content = `
|
|
3106
3313
|
<form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
|
|
@@ -3262,6 +3469,7 @@ export default class BabylonJSController {
|
|
|
3262
3469
|
|
|
3263
3470
|
this.#disablingPromises.general = (async () => {
|
|
3264
3471
|
this.#disableInteraction();
|
|
3472
|
+
this.#selectionController?.dispose();
|
|
3265
3473
|
this.#disposeAnimationController();
|
|
3266
3474
|
this.#disposeGLTFResolver();
|
|
3267
3475
|
try {
|
|
@@ -3312,6 +3520,16 @@ export default class BabylonJSController {
|
|
|
3312
3520
|
this.#viewCube?.sync();
|
|
3313
3521
|
}
|
|
3314
3522
|
|
|
3523
|
+
/**
|
|
3524
|
+
* Applies the host-owned semantic selection state to the current model meshes.
|
|
3525
|
+
* @public
|
|
3526
|
+
* @param {object} state - Canonical PrefViewer selection snapshot.
|
|
3527
|
+
* @returns {void}
|
|
3528
|
+
*/
|
|
3529
|
+
applySelectionState(state = {}) {
|
|
3530
|
+
this.#selectionController?.applySelectionState(state);
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3315
3533
|
/**
|
|
3316
3534
|
* Downloads the current scene, model, or environment as a GLB file.
|
|
3317
3535
|
* @public
|
|
@@ -3431,7 +3649,7 @@ export default class BabylonJSController {
|
|
|
3431
3649
|
* @returns {object} Debug information for canvas, engine, scene, containers, render loop and settings.
|
|
3432
3650
|
*/
|
|
3433
3651
|
getDebugInfo() {
|
|
3434
|
-
|
|
3652
|
+
|
|
3435
3653
|
const rect = this.#canvas?.getBoundingClientRect?.();
|
|
3436
3654
|
const camera = this.#scene?.activeCamera ?? null;
|
|
3437
3655
|
const sceneMeshes = this.#scene?.meshes ?? [];
|
|
@@ -3441,62 +3659,16 @@ export default class BabylonJSController {
|
|
|
3441
3659
|
const modelAssetContainer = this.#containers?.model?.assetContainer ?? null;
|
|
3442
3660
|
const modelMeshes = modelAssetContainer?.meshes ?? [];
|
|
3443
3661
|
const enabledModelMeshes = modelMeshes.filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false);
|
|
3444
|
-
const textureToJSON = (texture) => ({
|
|
3445
|
-
name: texture?.name ?? null,
|
|
3446
|
-
url: texture?.url ?? null,
|
|
3447
|
-
isReady: texture?.isReady?.() ?? null,
|
|
3448
|
-
hasTexture: Boolean(texture),
|
|
3449
|
-
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
3450
|
-
});
|
|
3451
|
-
const summarizeMaterial = (material) => {
|
|
3452
|
-
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
3453
|
-
const sampleMesh = relatedMeshes[0] ?? null;
|
|
3454
|
-
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
3455
|
-
let isReadyWithoutMesh = null;
|
|
3456
|
-
let isReadyWithMesh = null;
|
|
3457
|
-
try {
|
|
3458
|
-
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
3459
|
-
} catch (error) {
|
|
3460
|
-
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
3461
|
-
}
|
|
3462
|
-
try {
|
|
3463
|
-
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
3464
|
-
} catch (error) {
|
|
3465
|
-
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
3466
|
-
}
|
|
3467
3662
|
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
id: material?.id ?? null,
|
|
3471
|
-
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
3472
|
-
isReadyWithoutMesh,
|
|
3473
|
-
isReadyWithMesh,
|
|
3474
|
-
relatedMeshes: relatedMeshes.length,
|
|
3475
|
-
sampleMesh: sampleMesh ? {
|
|
3476
|
-
name: sampleMesh.name,
|
|
3477
|
-
id: sampleMesh.id,
|
|
3478
|
-
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
3479
|
-
isVisible: sampleMesh.isVisible ?? null,
|
|
3480
|
-
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
3481
|
-
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
3482
|
-
} : null,
|
|
3483
|
-
textures: activeTextures.map(textureToJSON),
|
|
3484
|
-
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(textureToJSON),
|
|
3485
|
-
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
3486
|
-
transparencyMode: material?.transparencyMode ?? null,
|
|
3487
|
-
alpha: material?.alpha ?? null,
|
|
3488
|
-
backFaceCulling: material?.backFaceCulling ?? null,
|
|
3489
|
-
disableLighting: material?.disableLighting ?? null,
|
|
3490
|
-
};
|
|
3491
|
-
};
|
|
3492
|
-
const materialDiagnostics = sceneMaterials.slice(0, 25).map(summarizeMaterial);
|
|
3663
|
+
|
|
3664
|
+
const materialDiagnostics = sceneMaterials.slice(0, 25).map((material) => BabylonJSController.#summarizeMaterial(material, sceneMeshes));
|
|
3493
3665
|
const notReadyMaterials = materialDiagnostics
|
|
3494
3666
|
.filter((material) => material.isReadyWithoutMesh === false || material.isReadyWithMesh === false || material.notReadyTextures.length > 0)
|
|
3495
3667
|
.slice(0, 25);
|
|
3496
3668
|
const notReadyTextures = sceneTextures
|
|
3497
3669
|
.filter((texture) => texture?.isReady?.() === false)
|
|
3498
3670
|
.slice(0, 25)
|
|
3499
|
-
.map(textureToJSON);
|
|
3671
|
+
.map(BabylonJSController.#textureToJSON);
|
|
3500
3672
|
const notReadyMeshes = sceneMeshes
|
|
3501
3673
|
.filter((mesh) => typeof mesh.isReady === "function" && !mesh.isReady())
|
|
3502
3674
|
.slice(0, 25)
|
|
@@ -3510,25 +3682,7 @@ export default class BabylonJSController {
|
|
|
3510
3682
|
const modelBounds = enabledModelMeshes.length && this.#scene?.getWorldExtends
|
|
3511
3683
|
? this.#scene.getWorldExtends((mesh) => enabledModelMeshes.includes(mesh))
|
|
3512
3684
|
: null;
|
|
3513
|
-
|
|
3514
|
-
show: container?.state?.show ?? null,
|
|
3515
|
-
visible: container?.state?.visible ?? null,
|
|
3516
|
-
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
3517
|
-
isPending: container?.state?.isPending ?? null,
|
|
3518
|
-
isSuccess: container?.state?.isSuccess ?? null,
|
|
3519
|
-
hasAssetContainer: Boolean(container?.assetContainer),
|
|
3520
|
-
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
3521
|
-
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
3522
|
-
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
3523
|
-
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
3524
|
-
name: item.name,
|
|
3525
|
-
id: item.id,
|
|
3526
|
-
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
3527
|
-
position: vectorToJSON(item.position),
|
|
3528
|
-
target: vectorToJSON(item.target),
|
|
3529
|
-
})) ?? [],
|
|
3530
|
-
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
3531
|
-
});
|
|
3685
|
+
|
|
3532
3686
|
|
|
3533
3687
|
return {
|
|
3534
3688
|
canvas: {
|
|
@@ -3560,8 +3714,8 @@ export default class BabylonJSController {
|
|
|
3560
3714
|
name: camera.name,
|
|
3561
3715
|
id: camera.id,
|
|
3562
3716
|
className: camera.getClassName?.() ?? camera.constructor?.name ?? null,
|
|
3563
|
-
position: vectorToJSON(camera.position),
|
|
3564
|
-
target: vectorToJSON(camera.target),
|
|
3717
|
+
position: BabylonJSController.#vectorToJSON(camera.position),
|
|
3718
|
+
target: BabylonJSController.#vectorToJSON(camera.target),
|
|
3565
3719
|
radius: camera.radius ?? null,
|
|
3566
3720
|
alpha: camera.alpha ?? null,
|
|
3567
3721
|
beta: camera.beta ?? null,
|
|
@@ -3582,13 +3736,13 @@ export default class BabylonJSController {
|
|
|
3582
3736
|
notReadyMeshes,
|
|
3583
3737
|
},
|
|
3584
3738
|
containers: {
|
|
3585
|
-
model: summarizeContainer(this.#containers.model),
|
|
3586
|
-
environment: summarizeContainer(this.#containers.environment),
|
|
3587
|
-
materials: summarizeContainer(this.#containers.materials),
|
|
3739
|
+
model: BabylonJSController.#summarizeContainer(this.#containers.model),
|
|
3740
|
+
environment: BabylonJSController.#summarizeContainer(this.#containers.environment),
|
|
3741
|
+
materials: BabylonJSController.#summarizeContainer(this.#containers.materials),
|
|
3588
3742
|
},
|
|
3589
3743
|
modelBounds: modelBounds ? {
|
|
3590
|
-
min: vectorToJSON(modelBounds.min),
|
|
3591
|
-
max: vectorToJSON(modelBounds.max),
|
|
3744
|
+
min: BabylonJSController.#vectorToJSON(modelBounds.min),
|
|
3745
|
+
max: BabylonJSController.#vectorToJSON(modelBounds.max),
|
|
3592
3746
|
} : null,
|
|
3593
3747
|
render: { ...this.#state.render },
|
|
3594
3748
|
settings: { ...this.#renderSettings },
|
|
@@ -3604,7 +3758,8 @@ export default class BabylonJSController {
|
|
|
3604
3758
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
3605
3759
|
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
3606
3760
|
* @public
|
|
3607
|
-
* @
|
|
3761
|
+
* @param {boolean} [force=false] - Bypasses asset cache metadata checks when reloading.
|
|
3762
|
+
* @returns {Promise<{success:boolean,error:*}>} Result of loading and preparing the asset containers.
|
|
3608
3763
|
*/
|
|
3609
3764
|
async load(force = false) {
|
|
3610
3765
|
return await this.#loadContainers(force);
|
|
@@ -3640,7 +3795,7 @@ export default class BabylonJSController {
|
|
|
3640
3795
|
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
3641
3796
|
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
3642
3797
|
* @public
|
|
3643
|
-
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
3798
|
+
* @returns {Promise<boolean>} True if IBL options were set successfully, false otherwise.
|
|
3644
3799
|
*/
|
|
3645
3800
|
async setIBLOptions() {
|
|
3646
3801
|
await this.#stopRender();
|
|
@@ -3689,7 +3844,7 @@ export default class BabylonJSController {
|
|
|
3689
3844
|
}
|
|
3690
3845
|
|
|
3691
3846
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3692
|
-
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3847
|
+
const normalizedTime = RenderMath.clamp01(config.lightingTimeOfDay);
|
|
3693
3848
|
if (this.#renderSettings.lightingTimeOfDay !== normalizedTime) {
|
|
3694
3849
|
this.#renderSettings.lightingTimeOfDay = normalizedTime;
|
|
3695
3850
|
shouldApplyLighting = true;
|