@preference-sl/pref-viewer 2.18.0 → 3.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 +267 -31
- package/index.d.ts +75 -6
- package/package.json +8 -2
- package/src/babylonjs-animation-controller.js +18 -10
- package/src/babylonjs-animation-opening-menu.js +80 -14
- package/src/babylonjs-animation-opening.js +3 -3
- package/src/babylonjs-ar-floor-bounds.js +169 -0
- package/src/babylonjs-ar-glass.js +128 -0
- package/src/babylonjs-ar-placement-feedback.js +449 -0
- package/src/babylonjs-ar-placement.js +1038 -0
- package/src/babylonjs-camera-wheel-motion.js +35 -2
- package/src/babylonjs-controller.js +542 -125
- package/src/babylonjs-orbital-pointers-input.js +186 -0
- package/src/{render-math.js → babylonjs-render-math.js} +11 -11
- package/src/babylonjs-selection-controller.js +4 -4
- package/src/babylonjs-view-cube-canvas-controller.js +1287 -0
- package/src/babylonjs-view-cube-canvas-orbit-math.js +220 -0
- package/src/babylonjs-view-cube-canvas-renderer.js +775 -0
- package/src/babylonjs-view-cube-controller.js +74 -1103
- package/src/babylonjs-view-cube-svg-controller.js +1012 -0
- package/src/file-storage.js +22 -4
- package/src/gltf-resolver.js +83 -13
- package/src/localization/translations.js +12 -0
- package/src/panzoom-controller.js +4 -4
- package/src/pref-viewer-2d.js +2 -2
- package/src/pref-viewer-3d.js +55 -9
- package/src/pref-viewer-controls-presentation.js +292 -0
- package/src/pref-viewer-dialog.js +86 -7
- package/src/pref-viewer-menu-3d.js +134 -17
- package/src/pref-viewer-style-controller.js +320 -0
- package/src/pref-viewer.js +133 -109
- package/src/quick-look-action.js +291 -0
- package/src/styles.js +702 -98
- package/src/babylonjs-view-cube-renderer.js +0 -548
- package/src/view-cube-orbit-math.js +0 -199
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, CubeTexture, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix,
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, CubeTexture, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
2
|
+
import BabylonJSARPlacement from "./babylonjs-ar-placement.js";
|
|
3
|
+
import BabylonJSARGlass from "./babylonjs-ar-glass.js";
|
|
2
4
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
5
|
import "@babylonjs/loaders";
|
|
4
6
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
5
7
|
import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
|
|
6
8
|
import JSZip from "jszip";
|
|
9
|
+
// Browser build of fflate: the package's default export map resolves to its Node flavour under
|
|
10
|
+
// esbuild's `platform: node`, which imports `createRequire` from "module" and breaks in browsers.
|
|
11
|
+
import * as fflate from "fflate/browser";
|
|
7
12
|
|
|
8
13
|
import GLTFResolver from "./gltf-resolver.js";
|
|
9
14
|
import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
@@ -11,13 +16,15 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
16
|
import BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
17
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
18
|
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
19
|
+
import BabylonJSOrbitalPointersInput from "./babylonjs-orbital-pointers-input.js";
|
|
14
20
|
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
15
21
|
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
16
22
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
17
23
|
import I18n from "./localization/i18n.js";
|
|
18
24
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
19
|
-
import
|
|
25
|
+
import BabylonJSRenderMath from "./babylonjs-render-math.js";
|
|
20
26
|
import MaterialUtils from "./material-utils.js";
|
|
27
|
+
import QuickLookAction from "./quick-look-action.js";
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -35,8 +42,12 @@ import MaterialUtils from "./material-utils.js";
|
|
|
35
42
|
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
36
43
|
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
37
44
|
* cost on dense scenes.
|
|
45
|
+
* - Configures distance-proportional orbital wheel/pinch zoom, with focal-enabled touch pinch following the two-finger midpoint.
|
|
38
46
|
* - Lazily owns the ViewCube overlay and binds a model-only orbital zoom guard independently of cube visibility.
|
|
39
|
-
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized
|
|
47
|
+
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized, density-aware download dialogs.
|
|
48
|
+
* - Shares cube-and-scan-frame AR artwork between native XR and Quick Look controls.
|
|
49
|
+
* - Owns metric WebXR floor placement or an eligible Safari Quick Look action. Prepared native content tracks
|
|
50
|
+
* completed model/material state; obsolete asynchronous work cannot publish into a replacement lifecycle.
|
|
40
51
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
41
52
|
*
|
|
42
53
|
* Runtime Flow
|
|
@@ -55,7 +66,7 @@ import MaterialUtils from "./material-utils.js";
|
|
|
55
66
|
*
|
|
56
67
|
* Public API Highlights
|
|
57
68
|
* - constructor(canvas, containers, options, viewCubeConfig?)
|
|
58
|
-
* - setViewCubeEnabled(enabled) / refreshViewCube()
|
|
69
|
+
* - setViewCubeEnabled(enabled) / setViewCubeRenderer(renderer) / refreshViewCube()
|
|
59
70
|
* - enable() / disable()
|
|
60
71
|
* - load(force?) / reloadWithCurrentSettings()
|
|
61
72
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
@@ -82,13 +93,20 @@ import MaterialUtils from "./material-utils.js";
|
|
|
82
93
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
83
94
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
84
95
|
* input avoids interaction picks; post-input orbital zoom protection separately probes only model meshes.
|
|
85
|
-
* Right-click POINTERUP
|
|
96
|
+
* Right-click POINTERUP and a stationary touch hold perform on-demand animation-menu picking.
|
|
97
|
+
* Touch holds consume release selection and are cancelled by navigation, interruption or teardown.
|
|
98
|
+
* Touch feedback lasts only while a hold is pending; touch-generated hover cannot retain animation highlights.
|
|
86
99
|
* - Runtime bookkeeping is split into mutable `#state` and shared tuning settings under `BabylonJSController.config` to keep behavior changes
|
|
87
100
|
* explicit and reduce cross-field drift.
|
|
88
101
|
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
89
102
|
* and any queued resize callback is canceled during teardown.
|
|
90
103
|
* - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
|
|
91
104
|
* during XR exit and engine disposal.
|
|
105
|
+
* - AR routing is selected internally from confirmed WebXR capability and eligible Safari browser signals.
|
|
106
|
+
* - XR lifecycle: entry wakes an idle engine and keeps its frame pump running through session transitions;
|
|
107
|
+
* otherwise Babylon cannot request native frames or deliver floor hit tests to placement.
|
|
108
|
+
* Transient AR gesture feedback is excluded from all scene/model download formats.
|
|
109
|
+
* Optional native plane detection exposes contours for initial footprint fitting when the runtime supports it.
|
|
92
110
|
*/
|
|
93
111
|
export default class BabylonJSController {
|
|
94
112
|
// Canvas HTML element
|
|
@@ -108,6 +126,12 @@ export default class BabylonJSController {
|
|
|
108
126
|
#cameraLight = null;
|
|
109
127
|
#shadowGen = [];
|
|
110
128
|
#XRExperience = null;
|
|
129
|
+
#arPlacement = null;
|
|
130
|
+
#xrGeneration = 0;
|
|
131
|
+
#quickLookAction = null;
|
|
132
|
+
#quickLookGlass = new BabylonJSARGlass();
|
|
133
|
+
#quickLookLoads = 0;
|
|
134
|
+
#quickLookValid = { model: false, materials: true };
|
|
111
135
|
#canvasResizeObserver = null;
|
|
112
136
|
|
|
113
137
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
@@ -152,6 +176,9 @@ export default class BabylonJSController {
|
|
|
152
176
|
lastMovePickY: NaN,
|
|
153
177
|
lastPickedMeshId: null,
|
|
154
178
|
pointerDown: null,
|
|
179
|
+
touchPointers: new Set(),
|
|
180
|
+
longPressTimeout: null,
|
|
181
|
+
touchHighlightActive: false,
|
|
155
182
|
},
|
|
156
183
|
// Render loop state balances performance with responsiveness.
|
|
157
184
|
render: {
|
|
@@ -166,6 +193,7 @@ export default class BabylonJSController {
|
|
|
166
193
|
resize: {
|
|
167
194
|
isScheduled: false,
|
|
168
195
|
timeoutId: null,
|
|
196
|
+
frameId: null,
|
|
169
197
|
lastAppliedAt: 0,
|
|
170
198
|
},
|
|
171
199
|
};
|
|
@@ -175,6 +203,7 @@ export default class BabylonJSController {
|
|
|
175
203
|
* All instances use this object, including the quality preset updated by hardware detection.
|
|
176
204
|
* render.defaultSettings supplies defaults for each instance's persisted render settings snapshot.
|
|
177
205
|
* Per-instance render and interaction bookkeeping remains in #state.
|
|
206
|
+
* camera wheel/pinch percentages tune separate native inputs; both scale zoom by the current orbital radius.
|
|
178
207
|
* @public
|
|
179
208
|
* @static
|
|
180
209
|
* @type {object}
|
|
@@ -182,6 +211,7 @@ export default class BabylonJSController {
|
|
|
182
211
|
static config = {
|
|
183
212
|
camera: {
|
|
184
213
|
wheelDeltaPercentage: 0.015,
|
|
214
|
+
pinchDeltaPercentage: 0.001, // Native pinch uses squared finger separation, unlike wheel deltas
|
|
185
215
|
fitMarginFactor: 0.15, // Total free viewport fraction: 0.15 leaves 85% for the model
|
|
186
216
|
maxRadiusFactor: 4, // Pull back past the whole model
|
|
187
217
|
minRadiusFactor: 0.25, // ViewCube minimum radius relative to transition distance
|
|
@@ -229,6 +259,7 @@ export default class BabylonJSController {
|
|
|
229
259
|
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
230
260
|
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
231
261
|
clickMaxDistancePx: 6,
|
|
262
|
+
longPressMs: 500,
|
|
232
263
|
},
|
|
233
264
|
selection: {
|
|
234
265
|
selectableColor: [1, 1, 0],
|
|
@@ -531,13 +562,14 @@ export default class BabylonJSController {
|
|
|
531
562
|
* Constructs a new BabylonJSController instance.
|
|
532
563
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
533
564
|
* @public
|
|
534
|
-
* @param {HTMLCanvasElement|null} canvas - The canvas element to render the scene on.
|
|
535
|
-
* @param {object} containers - An object containing container states for model, environment, materials, etc.
|
|
536
|
-
* @param {object} options - Configuration options for the Babylon.js scene and assets.
|
|
565
|
+
* @param {HTMLCanvasElement|null} [canvas=null] - The canvas element to render the scene on.
|
|
566
|
+
* @param {object} [containers={}] - An object containing container states for model, environment, materials, etc.
|
|
567
|
+
* @param {object} [options={}] - Configuration options for the Babylon.js scene and assets.
|
|
537
568
|
* @param {object} [viewCubeConfig={}] - Internal overlay integration options.
|
|
569
|
+
* @param {"svg"|"canvas"} [viewCubeConfig.renderer="canvas"] - ViewCube implementation; invalid values select Canvas.
|
|
538
570
|
* @param {boolean} [viewCubeConfig.enabled=true] - Desired activation; creation waits for a live scene and camera.
|
|
539
571
|
* @param {function(): string} [viewCubeConfig.getCulture] - Current UI culture for cube labels.
|
|
540
|
-
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured
|
|
572
|
+
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured active ViewCube height and scene viewport height in CSS pixels; (0, 0) on disposal.
|
|
541
573
|
* @returns {BabylonJSController}
|
|
542
574
|
* @description
|
|
543
575
|
* - Assigns the provided canvas to the controller.
|
|
@@ -904,6 +936,7 @@ export default class BabylonJSController {
|
|
|
904
936
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
905
937
|
* Pending ViewCube transitions and active XR force continuous rendering; animation/camera motion extends the
|
|
906
938
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
939
|
+
* Entering/exiting XR also stays continuous: the native frame pump must survive asynchronous setup and teardown.
|
|
907
940
|
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
908
941
|
* @private
|
|
909
942
|
* @param {number} now - Current high-resolution timestamp.
|
|
@@ -913,8 +946,8 @@ export default class BabylonJSController {
|
|
|
913
946
|
if (this.#viewCube?.isAnimating) {
|
|
914
947
|
return true;
|
|
915
948
|
}
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
949
|
+
const xrState = this.#XRExperience?.baseExperience?.state;
|
|
950
|
+
if (xrState === WebXRState.ENTERING_XR || xrState === WebXRState.IN_XR || xrState === WebXRState.EXITING_XR) {
|
|
918
951
|
this.#state.render.isIdle = false;
|
|
919
952
|
this.#state.render.lastActivityAt = now;
|
|
920
953
|
return true;
|
|
@@ -998,12 +1031,23 @@ export default class BabylonJSController {
|
|
|
998
1031
|
}
|
|
999
1032
|
|
|
1000
1033
|
/**
|
|
1001
|
-
* Adds
|
|
1034
|
+
* Adds the supplied cube-and-scan-frame SVG and shared interaction styles to native XR and Quick Look buttons.
|
|
1035
|
+
* Preserves the supplied 24-unit geometry and renders its filled paths white against the toggle background.
|
|
1002
1036
|
* @private
|
|
1003
1037
|
* @returns {void}
|
|
1004
1038
|
*/
|
|
1005
1039
|
#addStylesToARButton() {
|
|
1006
|
-
const
|
|
1040
|
+
const icon = `<svg fill="white" version="1.0" id="ar" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
1041
|
+
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
|
1042
|
+
<path fill="none" d="M0,0h24v24H0V0z"/>
|
|
1043
|
+
<path d="M17,22v-2h3v-3h2v3.5c0,0.39-0.16,0.74-0.46,1.04c-0.3,0.3-0.65,0.46-1.04,0.46H17 M7,22H3.5c-0.39,0-0.74-0.16-1.04-0.46
|
|
1044
|
+
C2.16,21.24,2,20.89,2,20.5V17h2v3h3V22 M17,2h3.5c0.39,0,0.74,0.16,1.04,0.46C21.84,2.76,22,3.11,22,3.5V7h-2V4h-3V2 M7,2v2H4v3H2
|
|
1045
|
+
V3.5c0-0.39,0.16-0.74,0.46-1.04S3.11,2,3.5,2H7 M13,17.25l4-2.3v-4.59l-4,2.3V17.25 M12,10.92l4-2.29l-4-2.35L8,8.63L12,10.92
|
|
1046
|
+
M7,14.95l4,2.3v-4.59l-4-2.3V14.95 M18.23,7.59C18.73,7.91,19,8.34,19,8.91v6.32c0,0.57-0.27,1-0.77,1.32l-5.48,3.18
|
|
1047
|
+
c-0.5,0.32-1,0.32-1.5,0l-5.48-3.18C5.27,16.23,5,15.8,5,15.23V8.91c0-0.57,0.27-1,0.77-1.32l5.48-3.18
|
|
1048
|
+
c0.25-0.13,0.5-0.19,0.75-0.19s0.5,0.06,0.75,0.19L18.23,7.59z"/>
|
|
1049
|
+
</svg>`;
|
|
1050
|
+
const css = `.babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(data:image/svg+xml;charset=UTF-8,${encodeURIComponent(icon)}); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }.babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: "EXIT"} .xr-error::after { content: "ERROR"}`;
|
|
1007
1051
|
let style = this.#canvas.parentElement.parentElement.querySelector("style");
|
|
1008
1052
|
if (!style) {
|
|
1009
1053
|
style = document.createElement("style");
|
|
@@ -1013,66 +1057,202 @@ export default class BabylonJSController {
|
|
|
1013
1057
|
}
|
|
1014
1058
|
|
|
1015
1059
|
/**
|
|
1016
|
-
*
|
|
1060
|
+
* Selects an AR candidate from a completed WebXR support query and browser signals.
|
|
1061
|
+
* A failed or pending query is unknown, not permission to use the native fallback.
|
|
1062
|
+
* Native content preparation/opening is a separate step from this decision.
|
|
1063
|
+
* @param {boolean|null} webXRSupported Resolved immersive-ar support, or null if unknown.
|
|
1064
|
+
* @param {object} [browser=globalThis.navigator] Navigator-like browser signals; no GPU-tier inference.
|
|
1017
1065
|
* @private
|
|
1018
|
-
* @returns {
|
|
1066
|
+
* @returns {"webxr"|"quick-look"|null} Candidate route, not confirmation of native opening.
|
|
1067
|
+
*/
|
|
1068
|
+
static #selectARRoute(webXRSupported, browser = globalThis.navigator) {
|
|
1069
|
+
if (webXRSupported === true) {
|
|
1070
|
+
return "webxr";
|
|
1071
|
+
}
|
|
1072
|
+
if (webXRSupported !== false || !browser) {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const userAgent = browser.userAgent ?? "";
|
|
1077
|
+
const isIOS = /iPhone|iPad|iPod/.test(userAgent);
|
|
1078
|
+
// iPadOS can request a desktop user agent. MacIntel alone also matches Macs.
|
|
1079
|
+
const isTouchIPad = browser.platform === "MacIntel" && browser.maxTouchPoints > 1;
|
|
1080
|
+
const isSafari = /AppleWebKit\//.test(userAgent) && /Version\//.test(userAgent) && /Safari\//.test(userAgent)
|
|
1081
|
+
&& !/CriOS|FxiOS|EdgiOS|OPiOS|Chrome|Chromium|Android/.test(userAgent);
|
|
1082
|
+
return (isIOS || isTouchIPad) && isSafari ? "quick-look" : null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Selects and initializes supported WebXR AR or an eligible Safari Quick Look action.
|
|
1087
|
+
* @private
|
|
1088
|
+
* @returns {Promise<boolean>} True when an AR route is configured, false when unsupported, obsolete or setup fails.
|
|
1019
1089
|
* @description
|
|
1020
|
-
* Checks
|
|
1021
|
-
*
|
|
1090
|
+
* Checks AR support and requires native floor hit testing at real-world metre scale.
|
|
1091
|
+
* Requests optional plane detection so initial placement can use an observed floor contour when supplied.
|
|
1092
|
+
* Placement follows the tracked XR camera rather than inheriting the showroom camera's fit.
|
|
1022
1093
|
* Adds custom styles to the AR button for consistent appearance.
|
|
1023
|
-
*
|
|
1094
|
+
* Unsupported or failed XR setup leaves ordinary 3D usable. Obsolete setup owns its own cleanup.
|
|
1095
|
+
* Quick Look prepares current-model USDZ separately; configured routing does not prove native opening.
|
|
1096
|
+
* Native state changes wake the idle engine; transition states keep rendering alive before the first XR frame.
|
|
1024
1097
|
*/
|
|
1025
1098
|
async #createXRExperience() {
|
|
1026
1099
|
if (this.#XRExperience) {
|
|
1027
1100
|
return true;
|
|
1028
1101
|
}
|
|
1029
1102
|
|
|
1103
|
+
const scene = this.#scene;
|
|
1104
|
+
const generation = this.#xrGeneration;
|
|
1105
|
+
/** @returns {boolean} Whether this setup still owns the live scene generation. */
|
|
1106
|
+
const isCurrent = () => this.#scene === scene && this.#xrGeneration === generation && !scene.isDisposed;
|
|
1030
1107
|
const sessionMode = "immersive-ar";
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1108
|
+
let experience = null;
|
|
1109
|
+
let placement = null;
|
|
1037
1110
|
try {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1111
|
+
const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
|
|
1112
|
+
if (!isCurrent()) {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
const browser = this.#canvas?.ownerDocument?.defaultView?.navigator ?? globalThis.navigator;
|
|
1116
|
+
const route = BabylonJSController.#selectARRoute(sessionSupported, browser);
|
|
1117
|
+
if (route === "quick-look") {
|
|
1118
|
+
this.#addStylesToARButton();
|
|
1119
|
+
this.#quickLookAction = new QuickLookAction(this.#canvas.parentElement, () => {
|
|
1120
|
+
const component = this.#canvas.closest("pref-viewer-3d");
|
|
1121
|
+
const root = component?.getRootNode();
|
|
1122
|
+
const viewer = root?.host?.matches("pref-viewer") ? root : component?.closest("pref-viewer") ?? component;
|
|
1123
|
+
return this.#quickLookLoads === 0 && !this.#containers.model?.state?.isPending
|
|
1124
|
+
&& !this.#containers.materials?.state?.isPending
|
|
1125
|
+
&& component?.getAttribute("visible") !== "false" && !viewer?.querySelector("pref-viewer-dialog[open]");
|
|
1126
|
+
}, () => this.#viewCubeConfig.getCulture?.() ?? I18n.getLocale());
|
|
1127
|
+
this.#refreshQuickLook();
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
if (route !== "webxr") {
|
|
1131
|
+
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1040
1134
|
|
|
1041
1135
|
const options = {
|
|
1042
|
-
|
|
1136
|
+
disableTeleportation: true,
|
|
1137
|
+
disablePointerSelection: true,
|
|
1138
|
+
disableNearInteraction: true,
|
|
1139
|
+
disableHandTracking: true,
|
|
1140
|
+
ignoreNativeCameraTransformation: true,
|
|
1043
1141
|
uiOptions: {
|
|
1044
1142
|
sessionMode: sessionMode,
|
|
1045
1143
|
renderTarget: "xrLayer",
|
|
1046
1144
|
referenceSpaceType: "local",
|
|
1145
|
+
requiredFeatures: ["hit-test"],
|
|
1146
|
+
optionalFeatures: ["plane-detection"],
|
|
1047
1147
|
},
|
|
1048
|
-
optionalFeatures: true,
|
|
1049
1148
|
};
|
|
1050
1149
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
xrInput: this.#XRExperience.input,
|
|
1056
|
-
floorMeshes: [ground],
|
|
1057
|
-
timeToTeleport: 1500,
|
|
1058
|
-
});
|
|
1150
|
+
experience = await WebXRDefaultExperience.CreateAsync(scene, options);
|
|
1151
|
+
if (!isCurrent()) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1059
1154
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
this.#
|
|
1064
|
-
|
|
1155
|
+
placement = new BabylonJSARPlacement(scene, experience,
|
|
1156
|
+
() => this.#containers.model?.state?.isVisible && !this.#containers.model.state.isPending
|
|
1157
|
+
? this.#containers.model.assetContainer : null,
|
|
1158
|
+
() => this.#containers.environment?.assetContainer);
|
|
1159
|
+
|
|
1160
|
+
experience.baseExperience.onStateChangedObservable.add(() => {
|
|
1161
|
+
// Babylon schedules XR frames only while engine render callbacks are registered.
|
|
1162
|
+
// Clicking its external AR control need not have woken the idle canvas first.
|
|
1163
|
+
if (isCurrent() && this.#XRExperience === experience) {
|
|
1164
|
+
this.#requestRender({
|
|
1165
|
+
frames: 1
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1065
1168
|
});
|
|
1066
1169
|
|
|
1067
1170
|
this.#addStylesToARButton();
|
|
1171
|
+
this.#XRExperience = experience;
|
|
1172
|
+
this.#arPlacement = placement;
|
|
1173
|
+
return true;
|
|
1068
1174
|
} catch (error) {
|
|
1069
1175
|
console.warn("PrefViewer: failed to create WebXR experience", error);
|
|
1070
|
-
|
|
1176
|
+
return false;
|
|
1177
|
+
} finally {
|
|
1178
|
+
if (experience !== this.#XRExperience || !experience) {
|
|
1179
|
+
try {
|
|
1180
|
+
experience?.dispose();
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
console.warn("PrefViewer: failed to dispose obsolete WebXR experience", error);
|
|
1183
|
+
} finally {
|
|
1184
|
+
placement?.dispose();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1071
1187
|
}
|
|
1072
1188
|
}
|
|
1073
1189
|
|
|
1074
1190
|
/**
|
|
1075
|
-
*
|
|
1191
|
+
* Prepares Quick Look from completed, visible model/material state without changing download helpers.
|
|
1192
|
+
* Invalid or absent content invalidates the link. Only supported visible model meshes are serialized;
|
|
1193
|
+
* asynchronous export ownership, stale results and preparation errors are handled by QuickLookAction.
|
|
1194
|
+
* @private
|
|
1195
|
+
* @returns {void} Starts preparation without waiting for it or changing public task-queue ordering.
|
|
1196
|
+
*/
|
|
1197
|
+
#refreshQuickLook() {
|
|
1198
|
+
if (!this.#quickLookAction || this.#quickLookLoads > 0) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const model = this.#containers.model;
|
|
1202
|
+
const asset = model?.assetContainer;
|
|
1203
|
+
if (!this.#scene || !asset || !model.state.isVisible || !this.#quickLookValid.model || !this.#quickLookValid.materials) {
|
|
1204
|
+
this.#quickLookAction.invalidate();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Selects current visible geometry supported by the installed USDZ material exporter.
|
|
1209
|
+
* @param {import("@babylonjs/core").AbstractMesh} mesh - Candidate model mesh.
|
|
1210
|
+
* @returns {boolean} Whether the mesh can be included in prepared native content.
|
|
1211
|
+
*/
|
|
1212
|
+
const eligible = (mesh) => !!mesh.geometry && mesh.getTotalVertices() > 0
|
|
1213
|
+
&& mesh.isVisible !== false && mesh.visibility > 0 && mesh.isEnabled()
|
|
1214
|
+
&& ["StandardMaterial", "PBRMaterial", "PBRMetallicRoughnessMaterial"].includes(mesh.material?.getClassName());
|
|
1215
|
+
if (!asset.meshes.some(eligible)) {
|
|
1216
|
+
this.#quickLookAction.invalidate();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// AssetContainer contains only model meshes; applied materials and world transforms are retained.
|
|
1220
|
+
// No environment, XR ground, ViewCube or utility-scene overlays enter this export.
|
|
1221
|
+
void this.#quickLookAction.update(() => this.#exportQuickLookUSDZ(asset, eligible));
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Exposes the bundled fflate module as the global the Babylon USDZ exporter checks for, so it neither
|
|
1226
|
+
* fetches the library from a CDN nor awaits before its synchronous material pass.
|
|
1227
|
+
* @private
|
|
1228
|
+
* @returns {void} Leaves an existing global untouched.
|
|
1229
|
+
*/
|
|
1230
|
+
#ensureUSDZDependencies() {
|
|
1231
|
+
globalThis.fflate ??= fflate;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Serializes the model to USDZ with physical (transmission) glass converted to alpha opacity, because the
|
|
1236
|
+
* exporter ignores transmission and would write the on-screen opaque alpha. The exporter reads every
|
|
1237
|
+
* material synchronously before its first await, so the adapter is restored right after the call returns
|
|
1238
|
+
* and the live render loop never draws the adapted glass.
|
|
1239
|
+
* @private
|
|
1240
|
+
* @param {import("@babylonjs/core").AssetContainer} asset - Model container to serialize.
|
|
1241
|
+
* @param {function(import("@babylonjs/core").AbstractMesh): boolean} eligible - Mesh filter for the export.
|
|
1242
|
+
* @returns {Promise<Uint8Array>} The serialized USDZ bytes.
|
|
1243
|
+
*/
|
|
1244
|
+
#exportQuickLookUSDZ(asset, eligible) {
|
|
1245
|
+
this.#ensureUSDZDependencies();
|
|
1246
|
+
this.#quickLookGlass.sync(asset);
|
|
1247
|
+
try {
|
|
1248
|
+
return USDZExportAsync(asset, {}, eligible);
|
|
1249
|
+
} finally {
|
|
1250
|
+
this.#quickLookGlass.restore();
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Creates the default ArcRotateCamera with unrestricted beta, upright orbit and proportional wheel/pinch zoom.
|
|
1076
1256
|
* Starts at the exact Inner orientation (+Z, alpha/beta pi/2), matching the ViewCube before loading.
|
|
1077
1257
|
* Starts without a fixed lower radius; the independent model zoom guard supplies the dynamic floor.
|
|
1078
1258
|
* Imported-camera configuration is applied separately and is not reset here.
|
|
@@ -1084,7 +1264,7 @@ export default class BabylonJSController {
|
|
|
1084
1264
|
this.#camera.upperBetaLimit = null;
|
|
1085
1265
|
this.#camera.lowerBetaLimit = null;
|
|
1086
1266
|
this.#camera.allowUpsideDown = false;
|
|
1087
|
-
this.#camera
|
|
1267
|
+
this.#configureOrbitalZoom(this.#camera);
|
|
1088
1268
|
this.#camera.zoomToMouseLocation = true;
|
|
1089
1269
|
this.#camera.lowerRadiusLimit = null;
|
|
1090
1270
|
this.#camera.upperRadiusLimit = 20;
|
|
@@ -1107,6 +1287,28 @@ export default class BabylonJSController {
|
|
|
1107
1287
|
BabylonJSCameraWheelMotion.focusOnSurface(camera, () => this.#containers.model?.state?.isVisible ? this.#containers.model.assetContainer?.meshes : null);
|
|
1108
1288
|
}
|
|
1109
1289
|
|
|
1290
|
+
/**
|
|
1291
|
+
* Applies distance-proportional native wheel and pinch inputs to an unlocked orbital camera.
|
|
1292
|
+
* Pinch uses its own sensitivity because Babylon measures squared finger separation, not wheel pixels.
|
|
1293
|
+
* Disables natural pinch's direct-radius override so both inputs keep Babylon's inertia and zoom guard.
|
|
1294
|
+
* Shares surface-focused wheel zoom with midpoint-focused pinch when mouse-location zoom is enabled.
|
|
1295
|
+
* Installs accumulated-motion gesture discrimination so slow pinch cannot become premature touch panning.
|
|
1296
|
+
* Combined pan/zoom stays disabled; classification is owned by the pointers input until release or detachment.
|
|
1297
|
+
* @private
|
|
1298
|
+
* @param {ArcRotateCamera} camera - Orbital camera being initialized or selected for interactive navigation.
|
|
1299
|
+
* @returns {void}
|
|
1300
|
+
*/
|
|
1301
|
+
#configureOrbitalZoom(camera) {
|
|
1302
|
+
camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1303
|
+
camera.pinchDeltaPercentage = BabylonJSController.config.camera.pinchDeltaPercentage;
|
|
1304
|
+
camera.useNaturalPinchZoom = false;
|
|
1305
|
+
const pointers = BabylonJSOrbitalPointersInput.install(camera);
|
|
1306
|
+
if (pointers) {
|
|
1307
|
+
pointers.multiTouchPanAndZoom = false;
|
|
1308
|
+
pointers.multiTouchPanning = true;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1110
1312
|
/**
|
|
1111
1313
|
* Creates and configures the main lights for the Babylon.js scene.
|
|
1112
1314
|
* Initializes the environment texture if needed.
|
|
@@ -1780,16 +1982,16 @@ export default class BabylonJSController {
|
|
|
1780
1982
|
}
|
|
1781
1983
|
|
|
1782
1984
|
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1783
|
-
const timeOfDay =
|
|
1784
|
-
const profile =
|
|
1985
|
+
const timeOfDay = BabylonJSRenderMath.clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1986
|
+
const profile = BabylonJSRenderMath.getLightingCycleProfile(timeOfDay);
|
|
1785
1987
|
const daylight = profile.dayFactor;
|
|
1786
1988
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1787
1989
|
const twilight = 1 - peakLight;
|
|
1788
1990
|
|
|
1789
1991
|
if (this.#scene.imageProcessingConfiguration) {
|
|
1790
1992
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1791
|
-
imageProcessing.exposure =
|
|
1792
|
-
imageProcessing.contrast =
|
|
1993
|
+
imageProcessing.exposure = BabylonJSRenderMath.lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1994
|
+
imageProcessing.contrast = BabylonJSRenderMath.lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1793
1995
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1794
1996
|
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1795
1997
|
imageProcessing.vignetteEnabled = false;
|
|
@@ -1800,57 +2002,57 @@ export default class BabylonJSController {
|
|
|
1800
2002
|
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1801
2003
|
const bgDay = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1802
2004
|
this.#scene.clearColor = new Color4(
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2005
|
+
BabylonJSRenderMath.lerp(0.000627, bgDay[0], daylight),
|
|
2006
|
+
BabylonJSRenderMath.lerp(0.001089, bgDay[1], daylight),
|
|
2007
|
+
BabylonJSRenderMath.lerp(0.004413, bgDay[2], daylight),
|
|
1806
2008
|
1,
|
|
1807
2009
|
);
|
|
1808
2010
|
|
|
1809
2011
|
if ("environmentIntensity" in this.#scene) {
|
|
1810
|
-
this.#scene.environmentIntensity =
|
|
2012
|
+
this.#scene.environmentIntensity = BabylonJSRenderMath.lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1811
2013
|
}
|
|
1812
2014
|
|
|
1813
2015
|
if (this.#hemiLight) {
|
|
1814
2016
|
this.#hemiLight.direction = new Vector3(
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2017
|
+
BabylonJSRenderMath.lerp(-0.45, -0.8, peakLight),
|
|
2018
|
+
BabylonJSRenderMath.lerp(0.45, 0.95, peakLight),
|
|
2019
|
+
BabylonJSRenderMath.lerp(-0.75, -0.35, peakLight),
|
|
1818
2020
|
);
|
|
1819
|
-
this.#hemiLight.intensity =
|
|
2021
|
+
this.#hemiLight.intensity = BabylonJSRenderMath.lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1820
2022
|
this.#hemiLight.diffuse = new Color3(
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2023
|
+
BabylonJSRenderMath.lerp(0.42, 0.92, peakLight),
|
|
2024
|
+
BabylonJSRenderMath.lerp(0.48, 0.96, peakLight),
|
|
2025
|
+
BabylonJSRenderMath.lerp(0.62, 0.98, peakLight),
|
|
1824
2026
|
);
|
|
1825
2027
|
this.#hemiLight.groundColor = new Color3(
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2028
|
+
BabylonJSRenderMath.lerp(0.08, 0.3, twilight),
|
|
2029
|
+
BabylonJSRenderMath.lerp(0.08, 0.28, twilight),
|
|
2030
|
+
BabylonJSRenderMath.lerp(0.1, 0.22, twilight),
|
|
1829
2031
|
);
|
|
1830
2032
|
}
|
|
1831
2033
|
|
|
1832
2034
|
if (this.#dirLight) {
|
|
1833
2035
|
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1834
|
-
this.#dirLight.intensity =
|
|
2036
|
+
this.#dirLight.intensity = BabylonJSRenderMath.lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1835
2037
|
this.#dirLight.diffuse = new Color3(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2038
|
+
BabylonJSRenderMath.lerp(0.28, 1.0, peakLight),
|
|
2039
|
+
BabylonJSRenderMath.lerp(0.3, 0.97, peakLight),
|
|
2040
|
+
BabylonJSRenderMath.lerp(0.34, 0.9, peakLight),
|
|
1839
2041
|
);
|
|
1840
2042
|
}
|
|
1841
2043
|
|
|
1842
2044
|
if (this.#moonLight) {
|
|
1843
2045
|
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1844
|
-
this.#moonLight.intensity =
|
|
2046
|
+
this.#moonLight.intensity = BabylonJSRenderMath.lerp(0.44, 0.03, peakLight);
|
|
1845
2047
|
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1846
2048
|
}
|
|
1847
2049
|
|
|
1848
2050
|
if (this.#cameraLight) {
|
|
1849
|
-
this.#cameraLight.intensity =
|
|
2051
|
+
this.#cameraLight.intensity = BabylonJSRenderMath.lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1850
2052
|
this.#cameraLight.diffuse = new Color3(
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2053
|
+
BabylonJSRenderMath.lerp(0.35, 1.0, peakLight),
|
|
2054
|
+
BabylonJSRenderMath.lerp(0.35, 0.97, peakLight),
|
|
2055
|
+
BabylonJSRenderMath.lerp(0.36, 0.92, peakLight),
|
|
1854
2056
|
);
|
|
1855
2057
|
}
|
|
1856
2058
|
|
|
@@ -1872,7 +2074,7 @@ export default class BabylonJSController {
|
|
|
1872
2074
|
|
|
1873
2075
|
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1874
2076
|
const baseScaling = BabylonJSController.config.quality.hardwareScaling || 1;
|
|
1875
|
-
const scalingLevel =
|
|
2077
|
+
const scalingLevel = BabylonJSRenderMath.getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1876
2078
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1877
2079
|
}
|
|
1878
2080
|
|
|
@@ -1934,7 +2136,7 @@ export default class BabylonJSController {
|
|
|
1934
2136
|
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
1935
2137
|
* field of view on ArcRotate cameras to avoid the background appearing undersized on wide layouts.
|
|
1936
2138
|
* @private
|
|
1937
|
-
* @param {Camera|null|undefined} camera - Active camera to adjust.
|
|
2139
|
+
* @param {Camera|null|undefined} [camera] - Active camera to adjust; defaults to the current scene camera.
|
|
1938
2140
|
* @returns {void}
|
|
1939
2141
|
*/
|
|
1940
2142
|
#applyShowroomCameraFraming(camera = this.#scene?.activeCamera) {
|
|
@@ -1946,11 +2148,13 @@ export default class BabylonJSController {
|
|
|
1946
2148
|
}
|
|
1947
2149
|
|
|
1948
2150
|
/**
|
|
1949
|
-
* Resets pointer-picking sampling state.
|
|
2151
|
+
* Resets pointer-picking sampling and active touch state, cancelling any pending animation-menu hold.
|
|
1950
2152
|
* @private
|
|
1951
2153
|
* @returns {void}
|
|
1952
2154
|
*/
|
|
1953
2155
|
#resetPointerPickingState() {
|
|
2156
|
+
this.#cancelTouchMenuHold();
|
|
2157
|
+
this.#state.pointerPicking.touchPointers.clear();
|
|
1954
2158
|
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1955
2159
|
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1956
2160
|
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
@@ -1996,7 +2200,7 @@ export default class BabylonJSController {
|
|
|
1996
2200
|
}
|
|
1997
2201
|
|
|
1998
2202
|
/**
|
|
1999
|
-
* Sets up
|
|
2203
|
+
* Sets up canvas/scene interactions and cancels incomplete gestures when the owning window loses focus.
|
|
2000
2204
|
* @private
|
|
2001
2205
|
* @returns {void}
|
|
2002
2206
|
*/
|
|
@@ -2004,6 +2208,7 @@ export default class BabylonJSController {
|
|
|
2004
2208
|
if (this.#canvas) {
|
|
2005
2209
|
this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
|
|
2006
2210
|
this.#canvas.addEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
2211
|
+
this.#canvas.ownerDocument?.defaultView?.addEventListener("blur", this.#handlers.onPointerCancel);
|
|
2007
2212
|
}
|
|
2008
2213
|
if (this.#scene) {
|
|
2009
2214
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
@@ -2015,7 +2220,7 @@ export default class BabylonJSController {
|
|
|
2015
2220
|
}
|
|
2016
2221
|
|
|
2017
2222
|
/**
|
|
2018
|
-
* Removes interaction
|
|
2223
|
+
* Removes canvas/scene/window interaction listeners and releases pending gesture/resize resources.
|
|
2019
2224
|
* @private
|
|
2020
2225
|
* @returns {void}
|
|
2021
2226
|
*/
|
|
@@ -2023,6 +2228,7 @@ export default class BabylonJSController {
|
|
|
2023
2228
|
if (this.#canvas) {
|
|
2024
2229
|
this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
|
|
2025
2230
|
this.#canvas.removeEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
2231
|
+
this.#canvas.ownerDocument?.defaultView?.removeEventListener("blur", this.#handlers.onPointerCancel);
|
|
2026
2232
|
}
|
|
2027
2233
|
if (this.#scene !== null) {
|
|
2028
2234
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
@@ -2035,11 +2241,12 @@ export default class BabylonJSController {
|
|
|
2035
2241
|
}
|
|
2036
2242
|
|
|
2037
2243
|
/**
|
|
2038
|
-
*
|
|
2244
|
+
* Cancels pending touch-menu activation and disposes the animation controller if it exists.
|
|
2039
2245
|
* @private
|
|
2040
2246
|
* @returns {void}
|
|
2041
2247
|
*/
|
|
2042
2248
|
#disposeAnimationController() {
|
|
2249
|
+
this.#cancelTouchMenuHold();
|
|
2043
2250
|
if (this.#babylonJSAnimationController) {
|
|
2044
2251
|
this.#babylonJSAnimationController.dispose();
|
|
2045
2252
|
this.#babylonJSAnimationController = null;
|
|
@@ -2063,6 +2270,7 @@ export default class BabylonJSController {
|
|
|
2063
2270
|
* If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
|
|
2064
2271
|
* tearing down the engine while the XR session is still shutting down.
|
|
2065
2272
|
* Concurrent calls share the same in-flight promise so disposal runs only once.
|
|
2273
|
+
* Restores metric-placement transforms and releases owned native hit-test/input observers before disposal.
|
|
2066
2274
|
* @private
|
|
2067
2275
|
* @returns {Promise<void>}
|
|
2068
2276
|
*/
|
|
@@ -2072,7 +2280,10 @@ export default class BabylonJSController {
|
|
|
2072
2280
|
}
|
|
2073
2281
|
|
|
2074
2282
|
const xrExperience = this.#XRExperience;
|
|
2283
|
+
const placement = this.#arPlacement;
|
|
2075
2284
|
if (!xrExperience) {
|
|
2285
|
+
placement?.dispose();
|
|
2286
|
+
this.#arPlacement = null;
|
|
2076
2287
|
return;
|
|
2077
2288
|
}
|
|
2078
2289
|
|
|
@@ -2084,6 +2295,7 @@ export default class BabylonJSController {
|
|
|
2084
2295
|
} catch (error) {
|
|
2085
2296
|
console.warn("PrefViewer: Error exiting XR experience:", error);
|
|
2086
2297
|
} finally {
|
|
2298
|
+
placement?.dispose();
|
|
2087
2299
|
try {
|
|
2088
2300
|
xrExperience.dispose();
|
|
2089
2301
|
} catch (error) {
|
|
@@ -2091,6 +2303,7 @@ export default class BabylonJSController {
|
|
|
2091
2303
|
}
|
|
2092
2304
|
if (this.#XRExperience === xrExperience) {
|
|
2093
2305
|
this.#XRExperience = null;
|
|
2306
|
+
this.#arPlacement = null;
|
|
2094
2307
|
}
|
|
2095
2308
|
this.#disablingPromises.xr = null;
|
|
2096
2309
|
}
|
|
@@ -2270,7 +2483,7 @@ export default class BabylonJSController {
|
|
|
2270
2483
|
}
|
|
2271
2484
|
|
|
2272
2485
|
/**
|
|
2273
|
-
* Handles
|
|
2486
|
+
* Handles camera activity and mouse/pen hover; touch-generated moves never enable persistent animation hover.
|
|
2274
2487
|
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2275
2488
|
* @private
|
|
2276
2489
|
* @param {PointerEvent} event - The pointer move event.
|
|
@@ -2287,7 +2500,7 @@ export default class BabylonJSController {
|
|
|
2287
2500
|
if (camera && !camera.metadata?.locked) {
|
|
2288
2501
|
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2289
2502
|
}
|
|
2290
|
-
if (this.#babylonJSAnimationController && pickInfo) {
|
|
2503
|
+
if (!this.#isTouchPointerEvent(event) && this.#babylonJSAnimationController && pickInfo) {
|
|
2291
2504
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2292
2505
|
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
2293
2506
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
@@ -2299,14 +2512,108 @@ export default class BabylonJSController {
|
|
|
2299
2512
|
}
|
|
2300
2513
|
|
|
2301
2514
|
/**
|
|
2302
|
-
* Invalidates
|
|
2515
|
+
* Invalidates incomplete clicks/holds and tracked touches on pointer cancellation or window focus loss.
|
|
2303
2516
|
* @private
|
|
2304
2517
|
* @returns {void}
|
|
2305
2518
|
*/
|
|
2306
2519
|
#onPointerCancel() {
|
|
2520
|
+
this.#cancelTouchMenuHold();
|
|
2521
|
+
this.#state.pointerPicking.touchPointers.clear();
|
|
2307
2522
|
this.#state.pointerPicking.pointerDown = null;
|
|
2308
2523
|
}
|
|
2309
2524
|
|
|
2525
|
+
/**
|
|
2526
|
+
* Clears the owned long-press timer and temporary touch highlight, preserving selection, camera controls and menu visibility.
|
|
2527
|
+
* @private
|
|
2528
|
+
* @returns {void}
|
|
2529
|
+
*/
|
|
2530
|
+
#cancelTouchMenuHold() {
|
|
2531
|
+
clearTimeout(this.#state.pointerPicking.longPressTimeout);
|
|
2532
|
+
this.#state.pointerPicking.longPressTimeout = null;
|
|
2533
|
+
if (this.#state.pointerPicking.touchHighlightActive) {
|
|
2534
|
+
this.#setTouchAnimationHighlight(null);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* Identifies direct touch input and compatibility mouse events explicitly marked as touch-generated.
|
|
2540
|
+
* @private
|
|
2541
|
+
* @param {PointerEvent|MouseEvent|null|undefined} event - Current browser pointer event.
|
|
2542
|
+
* @returns {boolean} True when the event must not drive persistent mouse hover.
|
|
2543
|
+
*/
|
|
2544
|
+
#isTouchPointerEvent(event) {
|
|
2545
|
+
return event?.pointerType === "touch" || event?.sourceCapabilities?.firesTouchEvents === true;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* Applies opening-only feedback for a pending touch hold, or clears previous touch/mouse highlight.
|
|
2550
|
+
* Honors the configured highlight preference without changing it; invalidates the hover cache and wakes rendering on changes.
|
|
2551
|
+
* @private
|
|
2552
|
+
* @param {PickingInfo|null} pick - Opening candidate, or null to remove feedback.
|
|
2553
|
+
* @returns {void}
|
|
2554
|
+
*/
|
|
2555
|
+
#setTouchAnimationHighlight(pick) {
|
|
2556
|
+
const result = this.#babylonJSAnimationController?.highlightMeshes(this.#renderSettings.highlightEnabled ? pick : null, true);
|
|
2557
|
+
this.#state.pointerPicking.touchHighlightActive = result?.highlighted ?? false;
|
|
2558
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
2559
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
2560
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
2561
|
+
if (result?.changed) {
|
|
2562
|
+
this.#requestRender({
|
|
2563
|
+
frames: 1
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/**
|
|
2569
|
+
* Checks whether current content and UI can accept a touch opening-menu gesture or its temporary feedback.
|
|
2570
|
+
* @private
|
|
2571
|
+
* @returns {boolean} False for missing content, hidden/modal UI, disconnected canvas or an active XR transition/session.
|
|
2572
|
+
*/
|
|
2573
|
+
#canUseTouchAnimationMenu() {
|
|
2574
|
+
const component = this.#canvas?.closest("pref-viewer-3d");
|
|
2575
|
+
const viewer = this.#canvas?.closest("pref-viewer");
|
|
2576
|
+
const xrState = this.#XRExperience?.baseExperience?.state;
|
|
2577
|
+
return !!(this.#scene && this.#canvas?.isConnected && this.#babylonJSAnimationController?.hasAnimations()
|
|
2578
|
+
&& this.#canvas.ownerDocument.visibilityState !== "hidden" && component?.getAttribute("visible") !== "false"
|
|
2579
|
+
&& viewer?.getAttribute("mode") !== "2d" && !viewer?.querySelector("pref-viewer-dialog[open]")
|
|
2580
|
+
&& (xrState === undefined || xrState === WebXRState.NOT_IN_XR));
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* Schedules a single-touch context action using the existing click movement tolerance.
|
|
2585
|
+
* Highlights the opening candidate during the pending half-second only, if enabled; early release or cancellation clears it.
|
|
2586
|
+
* Rechecks current content, visible UI and XR eligibility before picking the original CSS touch position.
|
|
2587
|
+
* Recognized holds consume the click and its release; no camera input is prevented or captured here.
|
|
2588
|
+
* @private
|
|
2589
|
+
* @param {PointerEvent} event - Primary touch down on this viewer's canvas.
|
|
2590
|
+
* @returns {void} Owns one cancellable timer until activation, interruption or teardown.
|
|
2591
|
+
*/
|
|
2592
|
+
#scheduleTouchMenuHold(event) {
|
|
2593
|
+
this.#cancelTouchMenuHold();
|
|
2594
|
+
const state = this.#state.pointerPicking;
|
|
2595
|
+
const start = state.pointerDown;
|
|
2596
|
+
if (event?.pointerType !== "touch" || !start || state.touchPointers.size !== 1 || !this.#canUseTouchAnimationMenu()) {
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const rect = this.#canvas.getBoundingClientRect();
|
|
2600
|
+
this.#setTouchAnimationHighlight(this.#scene.pick(start.x - rect.left, start.y - rect.top));
|
|
2601
|
+
state.longPressTimeout = setTimeout(() => {
|
|
2602
|
+
state.longPressTimeout = null;
|
|
2603
|
+
this.#cancelTouchMenuHold();
|
|
2604
|
+
if (state.pointerDown !== start || start.dragged || state.touchPointers.size !== 1 || !this.#canUseTouchAnimationMenu()) {
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
start.longPressed = start.dragged = true;
|
|
2608
|
+
const rect = this.#canvas.getBoundingClientRect();
|
|
2609
|
+
const pick = this.#scene.pick(start.x - rect.left, start.y - rect.top);
|
|
2610
|
+
this.#babylonJSAnimationController.showMenu(pick);
|
|
2611
|
+
this.#requestRender({
|
|
2612
|
+
frames: 1
|
|
2613
|
+
});
|
|
2614
|
+
}, BabylonJSController.config.pointerPicking.longPressMs);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2310
2617
|
/**
|
|
2311
2618
|
* Emits the internal 3D click contract for every model mesh crossed by the pointer ray.
|
|
2312
2619
|
* @private
|
|
@@ -2344,6 +2651,8 @@ export default class BabylonJSController {
|
|
|
2344
2651
|
/**
|
|
2345
2652
|
* Handles pointer events observed on the Babylon.js scene.
|
|
2346
2653
|
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
2654
|
+
* Recognizes stationary primary touch holds independently of short-click selection; release preserves their menu.
|
|
2655
|
+
* Touch motion skips hover raycasts; touch down clears stale hover before starting temporary hold feedback.
|
|
2347
2656
|
* @private
|
|
2348
2657
|
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
2349
2658
|
* @returns {void}
|
|
@@ -2351,26 +2660,47 @@ export default class BabylonJSController {
|
|
|
2351
2660
|
#onPointerObservable(info) {
|
|
2352
2661
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
2353
2662
|
const start = this.#state.pointerPicking.pointerDown;
|
|
2354
|
-
if (start && (info.event?.pointerId !== start.pointerId || (info.event?.buttons & 1) === 0
|
|
2355
|
-
|| Math.hypot(info.event?.clientX - start.x, info.event?.clientY - start.y) > BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2663
|
+
if (start && (info.event?.pointerId !== start.pointerId || (info.event?.buttons & 1) === 0 || Math.hypot(info.event?.clientX - start.x, info.event?.clientY - start.y) > BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2356
2664
|
start.dragged = true;
|
|
2665
|
+
this.#cancelTouchMenuHold();
|
|
2357
2666
|
}
|
|
2358
2667
|
let pickInfo = null;
|
|
2359
2668
|
let lastPickedMeshId = null;
|
|
2360
|
-
if (this.#shouldPickOnPointerMove()) {
|
|
2669
|
+
if (!this.#isTouchPointerEvent(info.event) && this.#shouldPickOnPointerMove()) {
|
|
2361
2670
|
pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
2362
2671
|
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2363
2672
|
}
|
|
2364
2673
|
this.#onPointerMove(info.event, pickInfo);
|
|
2365
2674
|
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
2366
2675
|
} else if (info.type === PointerEventTypes.POINTERDOWN) {
|
|
2367
|
-
this.#
|
|
2368
|
-
|
|
2369
|
-
|
|
2676
|
+
this.#cancelTouchMenuHold();
|
|
2677
|
+
if (this.#isTouchPointerEvent(info.event)) {
|
|
2678
|
+
this.#setTouchAnimationHighlight(null);
|
|
2679
|
+
}
|
|
2680
|
+
if (info.event?.pointerType === "touch") {
|
|
2681
|
+
this.#state.pointerPicking.touchPointers.add(info.event.pointerId);
|
|
2682
|
+
}
|
|
2683
|
+
this.#state.pointerPicking.pointerDown = info.event?.button === 0 && info.event.isPrimary !== false && this.#state.pointerPicking.touchPointers.size <= 1 && !this.#state.pointerPicking.pointerDown ? {
|
|
2684
|
+
x: info.event.clientX,
|
|
2685
|
+
y: info.event.clientY,
|
|
2686
|
+
button: info.event.button,
|
|
2687
|
+
pointerId: info.event.pointerId
|
|
2688
|
+
} : null;
|
|
2689
|
+
this.#scheduleTouchMenuHold(info.event);
|
|
2370
2690
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
2691
|
+
this.#cancelTouchMenuHold();
|
|
2692
|
+
this.#state.pointerPicking.touchPointers.delete(info.event?.pointerId);
|
|
2371
2693
|
const pointerDown = this.#state.pointerPicking.pointerDown;
|
|
2372
2694
|
this.#state.pointerPicking.pointerDown = null;
|
|
2373
|
-
|
|
2695
|
+
if (pointerDown?.longPressed && pointerDown.pointerId === info.event?.pointerId) {
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
const pointerUp = {
|
|
2699
|
+
x: info.event?.clientX,
|
|
2700
|
+
y: info.event?.clientY,
|
|
2701
|
+
button: info.event?.button,
|
|
2702
|
+
pointerId: info.event?.pointerId
|
|
2703
|
+
};
|
|
2374
2704
|
if (BabylonJSSelectionController.isPrimaryClickGesture(pointerDown, pointerUp, BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2375
2705
|
this.#emitModelMeshClick();
|
|
2376
2706
|
}
|
|
@@ -2386,6 +2716,7 @@ export default class BabylonJSController {
|
|
|
2386
2716
|
|
|
2387
2717
|
/**
|
|
2388
2718
|
* Applies a pending engine resize, cancels cube navigation and wakes rendering.
|
|
2719
|
+
* Skips zero-sized hidden canvases to prevent intrinsic-buffer/DPR growth; a visible resize retries later.
|
|
2389
2720
|
* @private
|
|
2390
2721
|
* @param {number} elapsed - Time since the previous resize, retained for diagnostics.
|
|
2391
2722
|
* @returns {void}
|
|
@@ -2396,6 +2727,12 @@ export default class BabylonJSController {
|
|
|
2396
2727
|
if (!this.#engine) {
|
|
2397
2728
|
return;
|
|
2398
2729
|
}
|
|
2730
|
+
// Babylon falls back to the intrinsic canvas size for hidden elements, then applies
|
|
2731
|
+
// DPR again. Repeated hidden resizes can exponentially grow GPU attachments on WebKit.
|
|
2732
|
+
// ResizeObserver will notify us again when the canvas has a visible CSS size.
|
|
2733
|
+
if (!this.#canvas.clientWidth || !this.#canvas.clientHeight) {
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2399
2736
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2400
2737
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2401
2738
|
this.#applyShowroomCameraFraming();
|
|
@@ -2531,6 +2868,7 @@ export default class BabylonJSController {
|
|
|
2531
2868
|
* Handles canvas resize notifications.
|
|
2532
2869
|
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
2533
2870
|
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
2871
|
+
* Coalesces notifications through a throttled timer and animation frame, outside ResizeObserver delivery.
|
|
2534
2872
|
* @private
|
|
2535
2873
|
* @returns {void}
|
|
2536
2874
|
*/
|
|
@@ -2542,28 +2880,33 @@ export default class BabylonJSController {
|
|
|
2542
2880
|
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
2543
2881
|
|
|
2544
2882
|
|
|
2545
|
-
if (elapsed >= BabylonJSController.config.resize.throttleMs) {
|
|
2546
|
-
this.#applyResize(elapsed);
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
2883
|
if (this.#state.resize.isScheduled) {
|
|
2551
2884
|
return;
|
|
2552
2885
|
}
|
|
2553
2886
|
|
|
2554
2887
|
this.#state.resize.isScheduled = true;
|
|
2888
|
+
// Mutating canvas buffers/layout inside ResizeObserver delivery causes WebKit's
|
|
2889
|
+
// "undelivered notifications" loop. Apply before the next paint instead.
|
|
2555
2890
|
const waitMs = Math.max(0, BabylonJSController.config.resize.throttleMs - elapsed);
|
|
2556
2891
|
this.#state.resize.timeoutId = setTimeout(() => {
|
|
2557
|
-
this.#
|
|
2892
|
+
this.#state.resize.timeoutId = null;
|
|
2893
|
+
this.#state.resize.frameId = requestAnimationFrame(() => {
|
|
2894
|
+
this.#state.resize.frameId = null;
|
|
2895
|
+
this.#applyResize(elapsed);
|
|
2896
|
+
});
|
|
2558
2897
|
}, waitMs);
|
|
2559
2898
|
}
|
|
2560
2899
|
|
|
2561
2900
|
/**
|
|
2562
|
-
* Clears
|
|
2901
|
+
* Clears both the queued throttle timer and animation-frame resize callback during teardown.
|
|
2563
2902
|
* @private
|
|
2564
2903
|
* @returns {void}
|
|
2565
2904
|
*/
|
|
2566
2905
|
#cancelScheduledResize() {
|
|
2906
|
+
if (this.#state.resize.frameId != null) {
|
|
2907
|
+
cancelAnimationFrame(this.#state.resize.frameId);
|
|
2908
|
+
}
|
|
2909
|
+
this.#state.resize.frameId = null;
|
|
2567
2910
|
if (this.#state.resize.timeoutId !== null) {
|
|
2568
2911
|
clearTimeout(this.#state.resize.timeoutId);
|
|
2569
2912
|
}
|
|
@@ -2639,7 +2982,7 @@ export default class BabylonJSController {
|
|
|
2639
2982
|
|
|
2640
2983
|
/**
|
|
2641
2984
|
* Cancels pending cube navigation and applies configured camera selection and locking.
|
|
2642
|
-
* Configures unlocked orbital cameras for proportional native wheel input, rebinds the model zoom guard
|
|
2985
|
+
* Configures unlocked orbital cameras for proportional native wheel/pinch input, rebinds the model zoom guard
|
|
2643
2986
|
* and synchronizes the cube after selecting the active camera.
|
|
2644
2987
|
* A missing named camera falls back to the unlocked default camera, rather than retaining an imported view.
|
|
2645
2988
|
* @private
|
|
@@ -2694,7 +3037,7 @@ export default class BabylonJSController {
|
|
|
2694
3037
|
camera.detachControl();
|
|
2695
3038
|
if (!cameraState.locked) {
|
|
2696
3039
|
if (camera instanceof ArcRotateCamera) {
|
|
2697
|
-
camera
|
|
3040
|
+
this.#configureOrbitalZoom(camera);
|
|
2698
3041
|
}
|
|
2699
3042
|
camera.attachControl(this.#canvas, true);
|
|
2700
3043
|
}
|
|
@@ -3000,11 +3343,13 @@ export default class BabylonJSController {
|
|
|
3000
3343
|
* Loads a single asset container (model, environment, materials, etc.) based on the container state flags.
|
|
3001
3344
|
* Skips work when nothing is pending, otherwise resolves the GLTF source, refreshes cache metadata and streams it
|
|
3002
3345
|
* into the Babylon.js scene via `LoadAssetContainerAsync`.
|
|
3346
|
+
* Distinguishes unchanged content from a failed replacement for native AR readiness and revokes source URLs
|
|
3347
|
+
* after attempted imports. Resolver failures may reject; Babylon import errors are logged and returned as null.
|
|
3003
3348
|
* @private
|
|
3004
3349
|
* @param {object} container - Container descriptor that carries the GLTF storage pointer and current cache info.
|
|
3005
3350
|
* @param {boolean} [force=false] - When true, bypasses cached size/timestamp so the resolver re-downloads the asset.
|
|
3006
|
-
* @returns {Promise<[object, AssetContainer|
|
|
3007
|
-
*
|
|
3351
|
+
* @returns {Promise<[object, AssetContainer|false|null]>} Container paired with imported assets, false for skipped,
|
|
3352
|
+
* unchanged or unresolved content, or null after a caught import failure.
|
|
3008
3353
|
* @description
|
|
3009
3354
|
* 1. Validates that the container has pending data and initializes the shared `GLTFResolver` if needed.
|
|
3010
3355
|
* 2. Requests the source blob (respecting the cached size/timestamp unless `force` is set) and stores the new cache
|
|
@@ -3028,7 +3373,18 @@ export default class BabylonJSController {
|
|
|
3028
3373
|
const currentSize = force ? 0 : container.state.size;
|
|
3029
3374
|
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
3030
3375
|
|
|
3031
|
-
|
|
3376
|
+
const isQuickLookContent = container.state.name === "model" || container.state.name === "materials";
|
|
3377
|
+
if (isQuickLookContent) {
|
|
3378
|
+
this.#quickLookValid[container.state.name] = false;
|
|
3379
|
+
}
|
|
3380
|
+
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp, true);
|
|
3381
|
+
|
|
3382
|
+
if (sourceData?.unchanged) {
|
|
3383
|
+
if (isQuickLookContent) {
|
|
3384
|
+
this.#quickLookValid[container.state.name] = !!container.assetContainer;
|
|
3385
|
+
}
|
|
3386
|
+
return [container, false];
|
|
3387
|
+
}
|
|
3032
3388
|
|
|
3033
3389
|
if (!sourceData) {
|
|
3034
3390
|
return [container, false];
|
|
@@ -3070,6 +3426,7 @@ export default class BabylonJSController {
|
|
|
3070
3426
|
/**
|
|
3071
3427
|
* Cancels pending cube navigation, loads asset containers and adds replacements to the scene.
|
|
3072
3428
|
* Current model selection is resolved by the zoom guard on subsequent input updates.
|
|
3429
|
+
* Invalidates native content during overlapping loads, then prepares only completed current-model content.
|
|
3073
3430
|
* @private
|
|
3074
3431
|
* @param {boolean} [force=false] - Whether to bypass existing asset cache metadata.
|
|
3075
3432
|
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
@@ -3079,6 +3436,29 @@ export default class BabylonJSController {
|
|
|
3079
3436
|
* Returns an object with success status and error details.
|
|
3080
3437
|
*/
|
|
3081
3438
|
async #loadContainers(force = false) {
|
|
3439
|
+
this.#quickLookAction?.invalidate();
|
|
3440
|
+
this.#quickLookLoads += 1;
|
|
3441
|
+
try {
|
|
3442
|
+
const result = await this.#loadContainersInternal(force);
|
|
3443
|
+
if (!result.success) {
|
|
3444
|
+
this.#quickLookValid.materials = false;
|
|
3445
|
+
}
|
|
3446
|
+
return result;
|
|
3447
|
+
} finally {
|
|
3448
|
+
this.#quickLookLoads -= 1;
|
|
3449
|
+
this.#refreshQuickLook();
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
/**
|
|
3454
|
+
* Loads pending containers in parallel, replaces scene assets and reapplies material/camera/light state.
|
|
3455
|
+
* Cancels cube/animation work, stops rendering during changes and restarts it after setup. Disposed scenes
|
|
3456
|
+
* discard late assets; successful model/material replacements update Quick Look content-validity state.
|
|
3457
|
+
* @private
|
|
3458
|
+
* @param {boolean} [force=false] - Whether to bypass cached source size/timestamp checks.
|
|
3459
|
+
* @returns {Promise<{success: boolean, error: *}>} Aggregate scene-load result and any captured setup error.
|
|
3460
|
+
*/
|
|
3461
|
+
async #loadContainersInternal(force = false) {
|
|
3082
3462
|
this.#viewCube?.cancel();
|
|
3083
3463
|
this.#resetPointerPickingState();
|
|
3084
3464
|
this.#detachAnimationChangedListener();
|
|
@@ -3125,6 +3505,9 @@ export default class BabylonJSController {
|
|
|
3125
3505
|
}
|
|
3126
3506
|
this.#replaceContainer(container, assetContainer);
|
|
3127
3507
|
container.state.setSuccess(true);
|
|
3508
|
+
if (container.state.name === "model" || container.state.name === "materials") {
|
|
3509
|
+
this.#quickLookValid[container.state.name] = true;
|
|
3510
|
+
}
|
|
3128
3511
|
} else {
|
|
3129
3512
|
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
3130
3513
|
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
@@ -3345,6 +3728,7 @@ export default class BabylonJSController {
|
|
|
3345
3728
|
|
|
3346
3729
|
/**
|
|
3347
3730
|
* Opens a modal dialog for downloading the 3D scene, model, or environment.
|
|
3731
|
+
* Uses shared dialog presentation and wrapping option groups; preserves export choices and routing.
|
|
3348
3732
|
* @private
|
|
3349
3733
|
* @returns {void}
|
|
3350
3734
|
*/
|
|
@@ -3358,15 +3742,15 @@ export default class BabylonJSController {
|
|
|
3358
3742
|
const texts = I18n.translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3359
3743
|
const header = texts.title || "Download 3D Scene";
|
|
3360
3744
|
const content = `
|
|
3361
|
-
<form slot="content" id="download-dialog-form"
|
|
3745
|
+
<form slot="content" id="download-dialog-form" class="download-dialog-form">
|
|
3362
3746
|
<h4>${texts.sections?.content || "Content"}</h4>
|
|
3363
|
-
<div
|
|
3747
|
+
<div class="download-dialog-options">
|
|
3364
3748
|
<label><input type="radio" name="content" value="1"> ${texts.options?.model || "Model"}</label>
|
|
3365
3749
|
<label><input type="radio" name="content" value="2"> ${texts.options?.scene || "Scene"}</label>
|
|
3366
3750
|
<label><input type="radio" name="content" value="0" checked> ${texts.options?.both || "Both"}</label>
|
|
3367
3751
|
</div>
|
|
3368
3752
|
<h4>${texts.sections?.format || "Format"}</h4>
|
|
3369
|
-
<div
|
|
3753
|
+
<div class="download-dialog-options">
|
|
3370
3754
|
<label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
|
|
3371
3755
|
<label><input type="radio" name="format" value="glb"> GLB</label>
|
|
3372
3756
|
<label><input type="radio" name="format" value="usdz"> USDZ</label>
|
|
@@ -3416,7 +3800,7 @@ export default class BabylonJSController {
|
|
|
3416
3800
|
*/
|
|
3417
3801
|
|
|
3418
3802
|
/**
|
|
3419
|
-
* Creates the
|
|
3803
|
+
* Creates the selected SVG/Canvas cube once a live scene/camera exists and teardown is not in progress.
|
|
3420
3804
|
* Resolves camera/model state through callbacks; does not reload assets or camera options.
|
|
3421
3805
|
* @private
|
|
3422
3806
|
* @returns {void}
|
|
@@ -3435,7 +3819,7 @@ export default class BabylonJSController {
|
|
|
3435
3819
|
onLayout: this.#viewCubeConfig.onLayout,
|
|
3436
3820
|
// A cube drag is pointer interaction like a main-canvas drag: keep the loop continuous for the same window.
|
|
3437
3821
|
requestRender: (options) => this.#requestRender({ frames: 2, continuousMs: options?.interaction ? BabylonJSController.config.render.interactionMs : 0 }),
|
|
3438
|
-
});
|
|
3822
|
+
}, this.#viewCubeConfig.renderer);
|
|
3439
3823
|
}
|
|
3440
3824
|
|
|
3441
3825
|
/**
|
|
@@ -3444,10 +3828,15 @@ export default class BabylonJSController {
|
|
|
3444
3828
|
* Observes canvas resize events, binds model-only orbital zoom protection and adaptive default-camera near clipping,
|
|
3445
3829
|
* and lazily creates the requested cube. Imported camera clipping is preserved by the host policy.
|
|
3446
3830
|
* Aborts remaining initialization if the scene was replaced/disposed during texture readiness.
|
|
3831
|
+
* Waits for preceding teardown and guards XR route creation with the current scene generation.
|
|
3447
3832
|
* @public
|
|
3448
3833
|
* @returns {Promise<void>}
|
|
3449
3834
|
*/
|
|
3450
3835
|
async enable() {
|
|
3836
|
+
if (this.#disablingPromises.general) {
|
|
3837
|
+
await this.#disablingPromises.general;
|
|
3838
|
+
}
|
|
3839
|
+
const enabledGeneration = ++this.#xrGeneration;
|
|
3451
3840
|
this.#configureDracoCompression();
|
|
3452
3841
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
3453
3842
|
this.#engine.disableUniformBuffers = true;
|
|
@@ -3490,7 +3879,7 @@ export default class BabylonJSController {
|
|
|
3490
3879
|
this.#initializeViewCube();
|
|
3491
3880
|
const enabledScene = this.#scene;
|
|
3492
3881
|
await this.#ensureEnvironmentBRDFTextureReady();
|
|
3493
|
-
if (!this.#engine || this.#scene !== enabledScene) {
|
|
3882
|
+
if (!this.#engine || this.#scene !== enabledScene || this.#xrGeneration !== enabledGeneration) {
|
|
3494
3883
|
return;
|
|
3495
3884
|
}
|
|
3496
3885
|
this.#enableInteraction();
|
|
@@ -3503,10 +3892,16 @@ export default class BabylonJSController {
|
|
|
3503
3892
|
* floor and suspending cube creation until the next enable. Cleans up light, XR and resolver resources.
|
|
3504
3893
|
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
3505
3894
|
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
3895
|
+
* Immediately invalidates pending XR setup and disposes the Quick Look action before awaiting teardown.
|
|
3506
3896
|
* @public
|
|
3507
3897
|
* @returns {Promise<void>}
|
|
3508
3898
|
*/
|
|
3509
3899
|
async disable() {
|
|
3900
|
+
// Invalidate pending support/setup before the first teardown await, even while the scene is live.
|
|
3901
|
+
this.#xrGeneration += 1;
|
|
3902
|
+
this.#quickLookAction?.dispose();
|
|
3903
|
+
this.#quickLookAction = null;
|
|
3904
|
+
this.#quickLookValid = { model: false, materials: true };
|
|
3510
3905
|
BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
|
|
3511
3906
|
BabylonJSCameraWheelMotion.cancel(this.#camera);
|
|
3512
3907
|
BabylonJSCameraWheelMotion.releaseSurfaceFocus(this.#camera);
|
|
@@ -3564,18 +3959,30 @@ export default class BabylonJSController {
|
|
|
3564
3959
|
}
|
|
3565
3960
|
|
|
3566
3961
|
/**
|
|
3567
|
-
*
|
|
3962
|
+
* Selects the cube implementation while preserving the active scene, camera and asset state.
|
|
3963
|
+
* @param {"svg"|"canvas"} renderer - Per-viewer preference; invalid values use Canvas.
|
|
3964
|
+
* @returns {void} Disabled cubes retain preference without allocating a new overlay.
|
|
3965
|
+
*/
|
|
3966
|
+
setViewCubeRenderer(renderer) {
|
|
3967
|
+
this.#viewCubeConfig.renderer = BabylonJSViewCubeController.normalizeRenderer(renderer);
|
|
3968
|
+
this.#viewCube?.setRenderer(this.#viewCubeConfig.renderer);
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
/**
|
|
3972
|
+
* Resizes/synchronizes the existing cube after culture, camera or presentation changes without activating it.
|
|
3973
|
+
* Also refreshes the existing Quick Look action's localized label from the host culture.
|
|
3568
3974
|
* @public
|
|
3569
3975
|
* @returns {void}
|
|
3570
3976
|
*/
|
|
3571
3977
|
refreshViewCube() {
|
|
3572
|
-
this.#
|
|
3978
|
+
this.#quickLookAction?.refreshCulture();
|
|
3979
|
+
this.#viewCube?.resize();
|
|
3573
3980
|
}
|
|
3574
3981
|
|
|
3575
3982
|
/**
|
|
3576
3983
|
* Applies the host-owned semantic selection state to the current model meshes.
|
|
3577
3984
|
* @public
|
|
3578
|
-
* @param {object} state - Canonical PrefViewer selection snapshot.
|
|
3985
|
+
* @param {object} [state={}] - Canonical PrefViewer selection snapshot.
|
|
3579
3986
|
* @returns {void}
|
|
3580
3987
|
*/
|
|
3581
3988
|
applySelectionState(state = {}) {
|
|
@@ -3583,9 +3990,9 @@ export default class BabylonJSController {
|
|
|
3583
3990
|
}
|
|
3584
3991
|
|
|
3585
3992
|
/**
|
|
3586
|
-
* Downloads the current scene, model, or environment as a GLB file.
|
|
3993
|
+
* Downloads the current scene, model, or environment as a GLB file, excluding transient AR feedback.
|
|
3587
3994
|
* @public
|
|
3588
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
3995
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3589
3996
|
* @returns {void}
|
|
3590
3997
|
*/
|
|
3591
3998
|
downloadGLB(content = 0) {
|
|
@@ -3608,7 +4015,8 @@ export default class BabylonJSController {
|
|
|
3608
4015
|
break;
|
|
3609
4016
|
}
|
|
3610
4017
|
fileName = this.#addDateToName(fileName);
|
|
3611
|
-
GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true
|
|
4018
|
+
GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true,
|
|
4019
|
+
shouldExportNode: node => !node.metadata?.prefViewerARFeedback }).then((glb) => {
|
|
3612
4020
|
if (glb) {
|
|
3613
4021
|
glb.downloadFiles();
|
|
3614
4022
|
}
|
|
@@ -3616,9 +4024,9 @@ export default class BabylonJSController {
|
|
|
3616
4024
|
}
|
|
3617
4025
|
|
|
3618
4026
|
/**
|
|
3619
|
-
* Downloads the current scene, model, or environment as a glTF ZIP file.
|
|
4027
|
+
* Downloads the current scene, model, or environment as a glTF ZIP file, excluding transient AR feedback.
|
|
3620
4028
|
* @public
|
|
3621
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
4029
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3622
4030
|
* @returns {void}
|
|
3623
4031
|
*/
|
|
3624
4032
|
downloadGLTF(content = 0) {
|
|
@@ -3645,7 +4053,8 @@ export default class BabylonJSController {
|
|
|
3645
4053
|
break;
|
|
3646
4054
|
}
|
|
3647
4055
|
comment += "\nPrefViewer by Preference, S.L.\npreference.com\n";
|
|
3648
|
-
GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true
|
|
4056
|
+
GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true,
|
|
4057
|
+
shouldExportNode: node => !node.metadata?.prefViewerARFeedback }).then((gltf) => {
|
|
3649
4058
|
if (gltf?.files) {
|
|
3650
4059
|
this.#downloadZip(gltf.files, fileName, comment, true);
|
|
3651
4060
|
}
|
|
@@ -3653,9 +4062,9 @@ export default class BabylonJSController {
|
|
|
3653
4062
|
}
|
|
3654
4063
|
|
|
3655
4064
|
/**
|
|
3656
|
-
* Downloads the current scene, model, or environment as a USDZ file.
|
|
4065
|
+
* Downloads the current scene, model, or environment as a USDZ file, excluding transient AR feedback.
|
|
3657
4066
|
* @public
|
|
3658
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
4067
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3659
4068
|
* @returns {void}
|
|
3660
4069
|
*/
|
|
3661
4070
|
downloadUSDZ(content = 0) {
|
|
@@ -3678,7 +4087,7 @@ export default class BabylonJSController {
|
|
|
3678
4087
|
break;
|
|
3679
4088
|
}
|
|
3680
4089
|
fileName = this.#addDateToName(fileName);
|
|
3681
|
-
USDZExportAsync(container).then((response) => {
|
|
4090
|
+
USDZExportAsync(container, undefined, mesh => !mesh.metadata?.prefViewerARFeedback).then((response) => {
|
|
3682
4091
|
if (response) {
|
|
3683
4092
|
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
3684
4093
|
}
|
|
@@ -3698,7 +4107,8 @@ export default class BabylonJSController {
|
|
|
3698
4107
|
* Returns a diagnostic snapshot of the current Babylon.js runtime state.
|
|
3699
4108
|
* Intended for integration troubleshooting and should not mutate scene state.
|
|
3700
4109
|
* @public
|
|
3701
|
-
* @returns {object}
|
|
4110
|
+
* @returns {object} Canvas, engine, scene, container, render-loop and settings diagnostics, plus nullable
|
|
4111
|
+
* `ar` placement state, metric distance/position and drag/model availability when WebXR is configured.
|
|
3702
4112
|
*/
|
|
3703
4113
|
getDebugInfo() {
|
|
3704
4114
|
|
|
@@ -3737,6 +4147,7 @@ export default class BabylonJSController {
|
|
|
3737
4147
|
|
|
3738
4148
|
|
|
3739
4149
|
return {
|
|
4150
|
+
ar: this.#arPlacement?.getDebugInfo() ?? null,
|
|
3740
4151
|
canvas: {
|
|
3741
4152
|
clientWidth: this.#canvas?.clientWidth ?? null,
|
|
3742
4153
|
clientHeight: this.#canvas?.clientHeight ?? null,
|
|
@@ -3833,13 +4244,16 @@ export default class BabylonJSController {
|
|
|
3833
4244
|
/**
|
|
3834
4245
|
* Applies material options from the configuration to the relevant meshes.
|
|
3835
4246
|
* Stops and restarts the render loop to apply changes.
|
|
4247
|
+
* Invalidates native content during the update and requests fresh Quick Look preparation after render readiness.
|
|
3836
4248
|
* @public
|
|
3837
4249
|
* @returns {boolean} True if material options were set successfully, false otherwise.
|
|
3838
4250
|
*/
|
|
3839
4251
|
setMaterialOptions() {
|
|
4252
|
+
this.#quickLookAction?.invalidate();
|
|
3840
4253
|
this.#stopRender();
|
|
3841
4254
|
const materialsOptionsSetted = this.#setOptions_Materials();
|
|
3842
|
-
this.#startRender()
|
|
4255
|
+
void this.#startRender().then(() => this.#refreshQuickLook())
|
|
4256
|
+
.catch(error => console.warn("PrefViewer: failed to prepare material rendering", error));
|
|
3843
4257
|
return materialsOptionsSetted;
|
|
3844
4258
|
}
|
|
3845
4259
|
|
|
@@ -3864,7 +4278,7 @@ export default class BabylonJSController {
|
|
|
3864
4278
|
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3865
4279
|
* the active lighting time is reset to `BabylonJSController.config.render.defaultSettings.lightingTimeOfDay` (0.5).
|
|
3866
4280
|
* @public
|
|
3867
|
-
* @param {object} config - Immediate render/illumination configuration.
|
|
4281
|
+
* @param {object} [config={}] - Immediate render/illumination configuration.
|
|
3868
4282
|
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
3869
4283
|
* @param {boolean|string|null} [config.highlightEnabled] - Enables/disables hover highlight; string "false" is normalized to false, null is ignored.
|
|
3870
4284
|
* @param {string|null} [config.highlightColor] - Hex color used by hover highlight; null is ignored.
|
|
@@ -3896,7 +4310,7 @@ export default class BabylonJSController {
|
|
|
3896
4310
|
}
|
|
3897
4311
|
|
|
3898
4312
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3899
|
-
const normalizedTime =
|
|
4313
|
+
const normalizedTime = BabylonJSRenderMath.clamp01(config.lightingTimeOfDay);
|
|
3900
4314
|
if (this.#renderSettings.lightingTimeOfDay !== normalizedTime) {
|
|
3901
4315
|
this.#renderSettings.lightingTimeOfDay = normalizedTime;
|
|
3902
4316
|
shouldApplyLighting = true;
|
|
@@ -3947,6 +4361,8 @@ export default class BabylonJSController {
|
|
|
3947
4361
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
3948
4362
|
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
3949
4363
|
* Restarts the render loop to apply changes.
|
|
4364
|
+
* Refreshes Quick Look readiness after rendering so hidden model content cannot retain an active native link.
|
|
4365
|
+
* Render/preparation failures are logged asynchronously; this method does not wait for native preparation.
|
|
3950
4366
|
* @public
|
|
3951
4367
|
* @param {string} name - The name of the container to show or hide.
|
|
3952
4368
|
* @param {boolean} show - True to show the container, false to hide it.
|
|
@@ -3964,7 +4380,8 @@ export default class BabylonJSController {
|
|
|
3964
4380
|
this.#stopRender();
|
|
3965
4381
|
show ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3966
4382
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
3967
|
-
this.#startRender()
|
|
4383
|
+
void this.#startRender().then(() => this.#refreshQuickLook())
|
|
4384
|
+
.catch(error => console.warn("PrefViewer: failed to prepare visibility rendering", error));
|
|
3968
4385
|
}
|
|
3969
4386
|
|
|
3970
4387
|
/**
|
|
@@ -3973,7 +4390,7 @@ export default class BabylonJSController {
|
|
|
3973
4390
|
* Immediate settings such as highlight, animation menu, and time-of-day lighting are handled by
|
|
3974
4391
|
* `setIlluminationConfig()` before this method receives the remaining reload settings.
|
|
3975
4392
|
* @public
|
|
3976
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of reload render toggles to apply.
|
|
4393
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} [settings={}] Partial set of reload render toggles to apply.
|
|
3977
4394
|
* @returns {{changed:boolean, settings:object}} Current full render settings snapshot and whether a reload is required.
|
|
3978
4395
|
* @description
|
|
3979
4396
|
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
@@ -4042,9 +4459,9 @@ export default class BabylonJSController {
|
|
|
4042
4459
|
}
|
|
4043
4460
|
|
|
4044
4461
|
/**
|
|
4045
|
-
* Configures
|
|
4462
|
+
* Configures animation highlights for mouse hover and pending touch holds.
|
|
4046
4463
|
* @private
|
|
4047
|
-
* @param {{enabled?: boolean, color?: string}} config - Configuration object.
|
|
4464
|
+
* @param {{enabled?: boolean, color?: string}} [config={}] - Configuration object.
|
|
4048
4465
|
* @returns {void}
|
|
4049
4466
|
*/
|
|
4050
4467
|
#setHighlightConfig(config = {}) {
|