@preference-sl/pref-viewer 2.18.0 → 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +507 -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 +7 -4
- package/src/gltf-resolver.js +13 -8
- 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 +280 -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,4 +1,5 @@
|
|
|
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";
|
|
2
3
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
4
|
import "@babylonjs/loaders";
|
|
4
5
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -11,13 +12,15 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
12
|
import BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
13
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
14
|
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
15
|
+
import BabylonJSOrbitalPointersInput from "./babylonjs-orbital-pointers-input.js";
|
|
14
16
|
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
15
17
|
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
16
18
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
17
19
|
import I18n from "./localization/i18n.js";
|
|
18
20
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
19
|
-
import
|
|
21
|
+
import BabylonJSRenderMath from "./babylonjs-render-math.js";
|
|
20
22
|
import MaterialUtils from "./material-utils.js";
|
|
23
|
+
import QuickLookAction from "./quick-look-action.js";
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -35,8 +38,12 @@ import MaterialUtils from "./material-utils.js";
|
|
|
35
38
|
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
36
39
|
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
37
40
|
* cost on dense scenes.
|
|
41
|
+
* - Configures distance-proportional orbital wheel/pinch zoom, with focal-enabled touch pinch following the two-finger midpoint.
|
|
38
42
|
* - 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
|
|
43
|
+
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized, density-aware download dialogs.
|
|
44
|
+
* - Shares cube-and-scan-frame AR artwork between native XR and Quick Look controls.
|
|
45
|
+
* - Owns metric WebXR floor placement or an eligible Safari Quick Look action. Prepared native content tracks
|
|
46
|
+
* completed model/material state; obsolete asynchronous work cannot publish into a replacement lifecycle.
|
|
40
47
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
41
48
|
*
|
|
42
49
|
* Runtime Flow
|
|
@@ -55,7 +62,7 @@ import MaterialUtils from "./material-utils.js";
|
|
|
55
62
|
*
|
|
56
63
|
* Public API Highlights
|
|
57
64
|
* - constructor(canvas, containers, options, viewCubeConfig?)
|
|
58
|
-
* - setViewCubeEnabled(enabled) / refreshViewCube()
|
|
65
|
+
* - setViewCubeEnabled(enabled) / setViewCubeRenderer(renderer) / refreshViewCube()
|
|
59
66
|
* - enable() / disable()
|
|
60
67
|
* - load(force?) / reloadWithCurrentSettings()
|
|
61
68
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
@@ -82,13 +89,20 @@ import MaterialUtils from "./material-utils.js";
|
|
|
82
89
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
83
90
|
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
84
91
|
* input avoids interaction picks; post-input orbital zoom protection separately probes only model meshes.
|
|
85
|
-
* Right-click POINTERUP
|
|
92
|
+
* Right-click POINTERUP and a stationary touch hold perform on-demand animation-menu picking.
|
|
93
|
+
* Touch holds consume release selection and are cancelled by navigation, interruption or teardown.
|
|
94
|
+
* Touch feedback lasts only while a hold is pending; touch-generated hover cannot retain animation highlights.
|
|
86
95
|
* - Runtime bookkeeping is split into mutable `#state` and shared tuning settings under `BabylonJSController.config` to keep behavior changes
|
|
87
96
|
* explicit and reduce cross-field drift.
|
|
88
97
|
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
89
98
|
* and any queued resize callback is canceled during teardown.
|
|
90
99
|
* - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
|
|
91
100
|
* during XR exit and engine disposal.
|
|
101
|
+
* - AR routing is selected internally from confirmed WebXR capability and eligible Safari browser signals.
|
|
102
|
+
* - XR lifecycle: entry wakes an idle engine and keeps its frame pump running through session transitions;
|
|
103
|
+
* otherwise Babylon cannot request native frames or deliver floor hit tests to placement.
|
|
104
|
+
* Transient AR gesture feedback is excluded from all scene/model download formats.
|
|
105
|
+
* Optional native plane detection exposes contours for initial footprint fitting when the runtime supports it.
|
|
92
106
|
*/
|
|
93
107
|
export default class BabylonJSController {
|
|
94
108
|
// Canvas HTML element
|
|
@@ -108,6 +122,11 @@ export default class BabylonJSController {
|
|
|
108
122
|
#cameraLight = null;
|
|
109
123
|
#shadowGen = [];
|
|
110
124
|
#XRExperience = null;
|
|
125
|
+
#arPlacement = null;
|
|
126
|
+
#xrGeneration = 0;
|
|
127
|
+
#quickLookAction = null;
|
|
128
|
+
#quickLookLoads = 0;
|
|
129
|
+
#quickLookValid = { model: false, materials: true };
|
|
111
130
|
#canvasResizeObserver = null;
|
|
112
131
|
|
|
113
132
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
@@ -152,6 +171,9 @@ export default class BabylonJSController {
|
|
|
152
171
|
lastMovePickY: NaN,
|
|
153
172
|
lastPickedMeshId: null,
|
|
154
173
|
pointerDown: null,
|
|
174
|
+
touchPointers: new Set(),
|
|
175
|
+
longPressTimeout: null,
|
|
176
|
+
touchHighlightActive: false,
|
|
155
177
|
},
|
|
156
178
|
// Render loop state balances performance with responsiveness.
|
|
157
179
|
render: {
|
|
@@ -166,6 +188,7 @@ export default class BabylonJSController {
|
|
|
166
188
|
resize: {
|
|
167
189
|
isScheduled: false,
|
|
168
190
|
timeoutId: null,
|
|
191
|
+
frameId: null,
|
|
169
192
|
lastAppliedAt: 0,
|
|
170
193
|
},
|
|
171
194
|
};
|
|
@@ -175,6 +198,7 @@ export default class BabylonJSController {
|
|
|
175
198
|
* All instances use this object, including the quality preset updated by hardware detection.
|
|
176
199
|
* render.defaultSettings supplies defaults for each instance's persisted render settings snapshot.
|
|
177
200
|
* Per-instance render and interaction bookkeeping remains in #state.
|
|
201
|
+
* camera wheel/pinch percentages tune separate native inputs; both scale zoom by the current orbital radius.
|
|
178
202
|
* @public
|
|
179
203
|
* @static
|
|
180
204
|
* @type {object}
|
|
@@ -182,6 +206,7 @@ export default class BabylonJSController {
|
|
|
182
206
|
static config = {
|
|
183
207
|
camera: {
|
|
184
208
|
wheelDeltaPercentage: 0.015,
|
|
209
|
+
pinchDeltaPercentage: 0.001, // Native pinch uses squared finger separation, unlike wheel deltas
|
|
185
210
|
fitMarginFactor: 0.15, // Total free viewport fraction: 0.15 leaves 85% for the model
|
|
186
211
|
maxRadiusFactor: 4, // Pull back past the whole model
|
|
187
212
|
minRadiusFactor: 0.25, // ViewCube minimum radius relative to transition distance
|
|
@@ -229,6 +254,7 @@ export default class BabylonJSController {
|
|
|
229
254
|
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
230
255
|
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
231
256
|
clickMaxDistancePx: 6,
|
|
257
|
+
longPressMs: 500,
|
|
232
258
|
},
|
|
233
259
|
selection: {
|
|
234
260
|
selectableColor: [1, 1, 0],
|
|
@@ -531,13 +557,14 @@ export default class BabylonJSController {
|
|
|
531
557
|
* Constructs a new BabylonJSController instance.
|
|
532
558
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
533
559
|
* @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.
|
|
560
|
+
* @param {HTMLCanvasElement|null} [canvas=null] - The canvas element to render the scene on.
|
|
561
|
+
* @param {object} [containers={}] - An object containing container states for model, environment, materials, etc.
|
|
562
|
+
* @param {object} [options={}] - Configuration options for the Babylon.js scene and assets.
|
|
537
563
|
* @param {object} [viewCubeConfig={}] - Internal overlay integration options.
|
|
564
|
+
* @param {"svg"|"canvas"} [viewCubeConfig.renderer="canvas"] - ViewCube implementation; invalid values select Canvas.
|
|
538
565
|
* @param {boolean} [viewCubeConfig.enabled=true] - Desired activation; creation waits for a live scene and camera.
|
|
539
566
|
* @param {function(): string} [viewCubeConfig.getCulture] - Current UI culture for cube labels.
|
|
540
|
-
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured
|
|
567
|
+
* @param {function(number, number): void} [viewCubeConfig.onLayout] - Measured active ViewCube height and scene viewport height in CSS pixels; (0, 0) on disposal.
|
|
541
568
|
* @returns {BabylonJSController}
|
|
542
569
|
* @description
|
|
543
570
|
* - Assigns the provided canvas to the controller.
|
|
@@ -904,6 +931,7 @@ export default class BabylonJSController {
|
|
|
904
931
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
905
932
|
* Pending ViewCube transitions and active XR force continuous rendering; animation/camera motion extends the
|
|
906
933
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
934
|
+
* Entering/exiting XR also stays continuous: the native frame pump must survive asynchronous setup and teardown.
|
|
907
935
|
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
908
936
|
* @private
|
|
909
937
|
* @param {number} now - Current high-resolution timestamp.
|
|
@@ -913,8 +941,8 @@ export default class BabylonJSController {
|
|
|
913
941
|
if (this.#viewCube?.isAnimating) {
|
|
914
942
|
return true;
|
|
915
943
|
}
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
944
|
+
const xrState = this.#XRExperience?.baseExperience?.state;
|
|
945
|
+
if (xrState === WebXRState.ENTERING_XR || xrState === WebXRState.IN_XR || xrState === WebXRState.EXITING_XR) {
|
|
918
946
|
this.#state.render.isIdle = false;
|
|
919
947
|
this.#state.render.lastActivityAt = now;
|
|
920
948
|
return true;
|
|
@@ -998,12 +1026,23 @@ export default class BabylonJSController {
|
|
|
998
1026
|
}
|
|
999
1027
|
|
|
1000
1028
|
/**
|
|
1001
|
-
* Adds
|
|
1029
|
+
* Adds the supplied cube-and-scan-frame SVG and shared interaction styles to native XR and Quick Look buttons.
|
|
1030
|
+
* Preserves the supplied 24-unit geometry and renders its filled paths white against the toggle background.
|
|
1002
1031
|
* @private
|
|
1003
1032
|
* @returns {void}
|
|
1004
1033
|
*/
|
|
1005
1034
|
#addStylesToARButton() {
|
|
1006
|
-
const
|
|
1035
|
+
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"
|
|
1036
|
+
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
|
1037
|
+
<path fill="none" d="M0,0h24v24H0V0z"/>
|
|
1038
|
+
<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
|
|
1039
|
+
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
|
|
1040
|
+
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
|
|
1041
|
+
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
|
|
1042
|
+
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
|
|
1043
|
+
c0.25-0.13,0.5-0.19,0.75-0.19s0.5,0.06,0.75,0.19L18.23,7.59z"/>
|
|
1044
|
+
</svg>`;
|
|
1045
|
+
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
1046
|
let style = this.#canvas.parentElement.parentElement.querySelector("style");
|
|
1008
1047
|
if (!style) {
|
|
1009
1048
|
style = document.createElement("style");
|
|
@@ -1013,66 +1052,172 @@ export default class BabylonJSController {
|
|
|
1013
1052
|
}
|
|
1014
1053
|
|
|
1015
1054
|
/**
|
|
1016
|
-
*
|
|
1055
|
+
* Selects an AR candidate from a completed WebXR support query and browser signals.
|
|
1056
|
+
* A failed or pending query is unknown, not permission to use the native fallback.
|
|
1057
|
+
* Native content preparation/opening is a separate step from this decision.
|
|
1058
|
+
* @param {boolean|null} webXRSupported Resolved immersive-ar support, or null if unknown.
|
|
1059
|
+
* @param {object} [browser=globalThis.navigator] Navigator-like browser signals; no GPU-tier inference.
|
|
1017
1060
|
* @private
|
|
1018
|
-
* @returns {
|
|
1061
|
+
* @returns {"webxr"|"quick-look"|null} Candidate route, not confirmation of native opening.
|
|
1062
|
+
*/
|
|
1063
|
+
static #selectARRoute(webXRSupported, browser = globalThis.navigator) {
|
|
1064
|
+
if (webXRSupported === true) {
|
|
1065
|
+
return "webxr";
|
|
1066
|
+
}
|
|
1067
|
+
if (webXRSupported !== false || !browser) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const userAgent = browser.userAgent ?? "";
|
|
1072
|
+
const isIOS = /iPhone|iPad|iPod/.test(userAgent);
|
|
1073
|
+
// iPadOS can request a desktop user agent. MacIntel alone also matches Macs.
|
|
1074
|
+
const isTouchIPad = browser.platform === "MacIntel" && browser.maxTouchPoints > 1;
|
|
1075
|
+
const isSafari = /AppleWebKit\//.test(userAgent) && /Version\//.test(userAgent) && /Safari\//.test(userAgent)
|
|
1076
|
+
&& !/CriOS|FxiOS|EdgiOS|OPiOS|Chrome|Chromium|Android/.test(userAgent);
|
|
1077
|
+
return (isIOS || isTouchIPad) && isSafari ? "quick-look" : null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Selects and initializes supported WebXR AR or an eligible Safari Quick Look action.
|
|
1082
|
+
* @private
|
|
1083
|
+
* @returns {Promise<boolean>} True when an AR route is configured, false when unsupported, obsolete or setup fails.
|
|
1019
1084
|
* @description
|
|
1020
|
-
* Checks
|
|
1021
|
-
*
|
|
1085
|
+
* Checks AR support and requires native floor hit testing at real-world metre scale.
|
|
1086
|
+
* Requests optional plane detection so initial placement can use an observed floor contour when supplied.
|
|
1087
|
+
* Placement follows the tracked XR camera rather than inheriting the showroom camera's fit.
|
|
1022
1088
|
* Adds custom styles to the AR button for consistent appearance.
|
|
1023
|
-
*
|
|
1089
|
+
* Unsupported or failed XR setup leaves ordinary 3D usable. Obsolete setup owns its own cleanup.
|
|
1090
|
+
* Quick Look prepares current-model USDZ separately; configured routing does not prove native opening.
|
|
1091
|
+
* Native state changes wake the idle engine; transition states keep rendering alive before the first XR frame.
|
|
1024
1092
|
*/
|
|
1025
1093
|
async #createXRExperience() {
|
|
1026
1094
|
if (this.#XRExperience) {
|
|
1027
1095
|
return true;
|
|
1028
1096
|
}
|
|
1029
1097
|
|
|
1098
|
+
const scene = this.#scene;
|
|
1099
|
+
const generation = this.#xrGeneration;
|
|
1100
|
+
/** @returns {boolean} Whether this setup still owns the live scene generation. */
|
|
1101
|
+
const isCurrent = () => this.#scene === scene && this.#xrGeneration === generation && !scene.isDisposed;
|
|
1030
1102
|
const sessionMode = "immersive-ar";
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1103
|
+
let experience = null;
|
|
1104
|
+
let placement = null;
|
|
1037
1105
|
try {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1106
|
+
const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
|
|
1107
|
+
if (!isCurrent()) {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
const browser = this.#canvas?.ownerDocument?.defaultView?.navigator ?? globalThis.navigator;
|
|
1111
|
+
const route = BabylonJSController.#selectARRoute(sessionSupported, browser);
|
|
1112
|
+
if (route === "quick-look") {
|
|
1113
|
+
this.#addStylesToARButton();
|
|
1114
|
+
this.#quickLookAction = new QuickLookAction(this.#canvas.parentElement, () => {
|
|
1115
|
+
const component = this.#canvas.closest("pref-viewer-3d");
|
|
1116
|
+
const root = component?.getRootNode();
|
|
1117
|
+
const viewer = root?.host?.matches("pref-viewer") ? root : component?.closest("pref-viewer") ?? component;
|
|
1118
|
+
return this.#quickLookLoads === 0 && !this.#containers.model?.state?.isPending
|
|
1119
|
+
&& !this.#containers.materials?.state?.isPending
|
|
1120
|
+
&& component?.getAttribute("visible") !== "false" && !viewer?.querySelector("pref-viewer-dialog[open]");
|
|
1121
|
+
}, () => this.#viewCubeConfig.getCulture?.() ?? I18n.getLocale());
|
|
1122
|
+
this.#refreshQuickLook();
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
if (route !== "webxr") {
|
|
1126
|
+
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1040
1129
|
|
|
1041
1130
|
const options = {
|
|
1042
|
-
|
|
1131
|
+
disableTeleportation: true,
|
|
1132
|
+
disablePointerSelection: true,
|
|
1133
|
+
disableNearInteraction: true,
|
|
1134
|
+
disableHandTracking: true,
|
|
1135
|
+
ignoreNativeCameraTransformation: true,
|
|
1043
1136
|
uiOptions: {
|
|
1044
1137
|
sessionMode: sessionMode,
|
|
1045
1138
|
renderTarget: "xrLayer",
|
|
1046
1139
|
referenceSpaceType: "local",
|
|
1140
|
+
requiredFeatures: ["hit-test"],
|
|
1141
|
+
optionalFeatures: ["plane-detection"],
|
|
1047
1142
|
},
|
|
1048
|
-
optionalFeatures: true,
|
|
1049
1143
|
};
|
|
1050
1144
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
xrInput: this.#XRExperience.input,
|
|
1056
|
-
floorMeshes: [ground],
|
|
1057
|
-
timeToTeleport: 1500,
|
|
1058
|
-
});
|
|
1145
|
+
experience = await WebXRDefaultExperience.CreateAsync(scene, options);
|
|
1146
|
+
if (!isCurrent()) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1059
1149
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
this.#
|
|
1064
|
-
|
|
1150
|
+
placement = new BabylonJSARPlacement(scene, experience,
|
|
1151
|
+
() => this.#containers.model?.state?.isVisible && !this.#containers.model.state.isPending
|
|
1152
|
+
? this.#containers.model.assetContainer : null,
|
|
1153
|
+
() => this.#containers.environment?.assetContainer);
|
|
1154
|
+
|
|
1155
|
+
experience.baseExperience.onStateChangedObservable.add(() => {
|
|
1156
|
+
// Babylon schedules XR frames only while engine render callbacks are registered.
|
|
1157
|
+
// Clicking its external AR control need not have woken the idle canvas first.
|
|
1158
|
+
if (isCurrent() && this.#XRExperience === experience) {
|
|
1159
|
+
this.#requestRender({
|
|
1160
|
+
frames: 1
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1065
1163
|
});
|
|
1066
1164
|
|
|
1067
1165
|
this.#addStylesToARButton();
|
|
1166
|
+
this.#XRExperience = experience;
|
|
1167
|
+
this.#arPlacement = placement;
|
|
1168
|
+
return true;
|
|
1068
1169
|
} catch (error) {
|
|
1069
1170
|
console.warn("PrefViewer: failed to create WebXR experience", error);
|
|
1070
|
-
|
|
1171
|
+
return false;
|
|
1172
|
+
} finally {
|
|
1173
|
+
if (experience !== this.#XRExperience || !experience) {
|
|
1174
|
+
try {
|
|
1175
|
+
experience?.dispose();
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
console.warn("PrefViewer: failed to dispose obsolete WebXR experience", error);
|
|
1178
|
+
} finally {
|
|
1179
|
+
placement?.dispose();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Prepares Quick Look from completed, visible model/material state without changing download helpers.
|
|
1187
|
+
* Invalid or absent content invalidates the link. Only supported visible model meshes are serialized;
|
|
1188
|
+
* asynchronous export ownership, stale results and preparation errors are handled by QuickLookAction.
|
|
1189
|
+
* @private
|
|
1190
|
+
* @returns {void} Starts preparation without waiting for it or changing public task-queue ordering.
|
|
1191
|
+
*/
|
|
1192
|
+
#refreshQuickLook() {
|
|
1193
|
+
if (!this.#quickLookAction || this.#quickLookLoads > 0) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const model = this.#containers.model;
|
|
1197
|
+
const asset = model?.assetContainer;
|
|
1198
|
+
if (!this.#scene || !asset || !model.state.isVisible || !this.#quickLookValid.model || !this.#quickLookValid.materials) {
|
|
1199
|
+
this.#quickLookAction.invalidate();
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Selects current visible geometry supported by the installed USDZ material exporter.
|
|
1204
|
+
* @param {import("@babylonjs/core").AbstractMesh} mesh - Candidate model mesh.
|
|
1205
|
+
* @returns {boolean} Whether the mesh can be included in prepared native content.
|
|
1206
|
+
*/
|
|
1207
|
+
const eligible = (mesh) => !!mesh.geometry && mesh.getTotalVertices() > 0
|
|
1208
|
+
&& mesh.isVisible !== false && mesh.visibility > 0 && mesh.isEnabled()
|
|
1209
|
+
&& ["StandardMaterial", "PBRMaterial", "PBRMetallicRoughnessMaterial"].includes(mesh.material?.getClassName());
|
|
1210
|
+
if (!asset.meshes.some(eligible)) {
|
|
1211
|
+
this.#quickLookAction.invalidate();
|
|
1212
|
+
return;
|
|
1071
1213
|
}
|
|
1214
|
+
// AssetContainer contains only model meshes; applied materials and world transforms are retained.
|
|
1215
|
+
// No environment, XR ground, ViewCube or utility-scene overlays enter this export.
|
|
1216
|
+
void this.#quickLookAction.update(() => USDZExportAsync(asset, {}, eligible));
|
|
1072
1217
|
}
|
|
1073
1218
|
|
|
1074
1219
|
/**
|
|
1075
|
-
* Creates the default ArcRotateCamera with unrestricted beta, upright orbit and proportional
|
|
1220
|
+
* Creates the default ArcRotateCamera with unrestricted beta, upright orbit and proportional wheel/pinch zoom.
|
|
1076
1221
|
* Starts at the exact Inner orientation (+Z, alpha/beta pi/2), matching the ViewCube before loading.
|
|
1077
1222
|
* Starts without a fixed lower radius; the independent model zoom guard supplies the dynamic floor.
|
|
1078
1223
|
* Imported-camera configuration is applied separately and is not reset here.
|
|
@@ -1084,7 +1229,7 @@ export default class BabylonJSController {
|
|
|
1084
1229
|
this.#camera.upperBetaLimit = null;
|
|
1085
1230
|
this.#camera.lowerBetaLimit = null;
|
|
1086
1231
|
this.#camera.allowUpsideDown = false;
|
|
1087
|
-
this.#camera
|
|
1232
|
+
this.#configureOrbitalZoom(this.#camera);
|
|
1088
1233
|
this.#camera.zoomToMouseLocation = true;
|
|
1089
1234
|
this.#camera.lowerRadiusLimit = null;
|
|
1090
1235
|
this.#camera.upperRadiusLimit = 20;
|
|
@@ -1107,6 +1252,28 @@ export default class BabylonJSController {
|
|
|
1107
1252
|
BabylonJSCameraWheelMotion.focusOnSurface(camera, () => this.#containers.model?.state?.isVisible ? this.#containers.model.assetContainer?.meshes : null);
|
|
1108
1253
|
}
|
|
1109
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* Applies distance-proportional native wheel and pinch inputs to an unlocked orbital camera.
|
|
1257
|
+
* Pinch uses its own sensitivity because Babylon measures squared finger separation, not wheel pixels.
|
|
1258
|
+
* Disables natural pinch's direct-radius override so both inputs keep Babylon's inertia and zoom guard.
|
|
1259
|
+
* Shares surface-focused wheel zoom with midpoint-focused pinch when mouse-location zoom is enabled.
|
|
1260
|
+
* Installs accumulated-motion gesture discrimination so slow pinch cannot become premature touch panning.
|
|
1261
|
+
* Combined pan/zoom stays disabled; classification is owned by the pointers input until release or detachment.
|
|
1262
|
+
* @private
|
|
1263
|
+
* @param {ArcRotateCamera} camera - Orbital camera being initialized or selected for interactive navigation.
|
|
1264
|
+
* @returns {void}
|
|
1265
|
+
*/
|
|
1266
|
+
#configureOrbitalZoom(camera) {
|
|
1267
|
+
camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1268
|
+
camera.pinchDeltaPercentage = BabylonJSController.config.camera.pinchDeltaPercentage;
|
|
1269
|
+
camera.useNaturalPinchZoom = false;
|
|
1270
|
+
const pointers = BabylonJSOrbitalPointersInput.install(camera);
|
|
1271
|
+
if (pointers) {
|
|
1272
|
+
pointers.multiTouchPanAndZoom = false;
|
|
1273
|
+
pointers.multiTouchPanning = true;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1110
1277
|
/**
|
|
1111
1278
|
* Creates and configures the main lights for the Babylon.js scene.
|
|
1112
1279
|
* Initializes the environment texture if needed.
|
|
@@ -1780,16 +1947,16 @@ export default class BabylonJSController {
|
|
|
1780
1947
|
}
|
|
1781
1948
|
|
|
1782
1949
|
const showroom = BabylonJSController.config.look?.showroom || {};
|
|
1783
|
-
const timeOfDay =
|
|
1784
|
-
const profile =
|
|
1950
|
+
const timeOfDay = BabylonJSRenderMath.clamp01(this.#renderSettings.lightingTimeOfDay ? this.#renderSettings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1951
|
+
const profile = BabylonJSRenderMath.getLightingCycleProfile(timeOfDay);
|
|
1785
1952
|
const daylight = profile.dayFactor;
|
|
1786
1953
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1787
1954
|
const twilight = 1 - peakLight;
|
|
1788
1955
|
|
|
1789
1956
|
if (this.#scene.imageProcessingConfiguration) {
|
|
1790
1957
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1791
|
-
imageProcessing.exposure =
|
|
1792
|
-
imageProcessing.contrast =
|
|
1958
|
+
imageProcessing.exposure = BabylonJSRenderMath.lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1959
|
+
imageProcessing.contrast = BabylonJSRenderMath.lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1793
1960
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1794
1961
|
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1795
1962
|
imageProcessing.vignetteEnabled = false;
|
|
@@ -1800,57 +1967,57 @@ export default class BabylonJSController {
|
|
|
1800
1967
|
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1801
1968
|
const bgDay = showroom.backgroundDay ?? BabylonJSController.config.look.showroom.backgroundDay;
|
|
1802
1969
|
this.#scene.clearColor = new Color4(
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1970
|
+
BabylonJSRenderMath.lerp(0.000627, bgDay[0], daylight),
|
|
1971
|
+
BabylonJSRenderMath.lerp(0.001089, bgDay[1], daylight),
|
|
1972
|
+
BabylonJSRenderMath.lerp(0.004413, bgDay[2], daylight),
|
|
1806
1973
|
1,
|
|
1807
1974
|
);
|
|
1808
1975
|
|
|
1809
1976
|
if ("environmentIntensity" in this.#scene) {
|
|
1810
|
-
this.#scene.environmentIntensity =
|
|
1977
|
+
this.#scene.environmentIntensity = BabylonJSRenderMath.lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1811
1978
|
}
|
|
1812
1979
|
|
|
1813
1980
|
if (this.#hemiLight) {
|
|
1814
1981
|
this.#hemiLight.direction = new Vector3(
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1982
|
+
BabylonJSRenderMath.lerp(-0.45, -0.8, peakLight),
|
|
1983
|
+
BabylonJSRenderMath.lerp(0.45, 0.95, peakLight),
|
|
1984
|
+
BabylonJSRenderMath.lerp(-0.75, -0.35, peakLight),
|
|
1818
1985
|
);
|
|
1819
|
-
this.#hemiLight.intensity =
|
|
1986
|
+
this.#hemiLight.intensity = BabylonJSRenderMath.lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1820
1987
|
this.#hemiLight.diffuse = new Color3(
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1988
|
+
BabylonJSRenderMath.lerp(0.42, 0.92, peakLight),
|
|
1989
|
+
BabylonJSRenderMath.lerp(0.48, 0.96, peakLight),
|
|
1990
|
+
BabylonJSRenderMath.lerp(0.62, 0.98, peakLight),
|
|
1824
1991
|
);
|
|
1825
1992
|
this.#hemiLight.groundColor = new Color3(
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1993
|
+
BabylonJSRenderMath.lerp(0.08, 0.3, twilight),
|
|
1994
|
+
BabylonJSRenderMath.lerp(0.08, 0.28, twilight),
|
|
1995
|
+
BabylonJSRenderMath.lerp(0.1, 0.22, twilight),
|
|
1829
1996
|
);
|
|
1830
1997
|
}
|
|
1831
1998
|
|
|
1832
1999
|
if (this.#dirLight) {
|
|
1833
2000
|
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1834
|
-
this.#dirLight.intensity =
|
|
2001
|
+
this.#dirLight.intensity = BabylonJSRenderMath.lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1835
2002
|
this.#dirLight.diffuse = new Color3(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2003
|
+
BabylonJSRenderMath.lerp(0.28, 1.0, peakLight),
|
|
2004
|
+
BabylonJSRenderMath.lerp(0.3, 0.97, peakLight),
|
|
2005
|
+
BabylonJSRenderMath.lerp(0.34, 0.9, peakLight),
|
|
1839
2006
|
);
|
|
1840
2007
|
}
|
|
1841
2008
|
|
|
1842
2009
|
if (this.#moonLight) {
|
|
1843
2010
|
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1844
|
-
this.#moonLight.intensity =
|
|
2011
|
+
this.#moonLight.intensity = BabylonJSRenderMath.lerp(0.44, 0.03, peakLight);
|
|
1845
2012
|
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1846
2013
|
}
|
|
1847
2014
|
|
|
1848
2015
|
if (this.#cameraLight) {
|
|
1849
|
-
this.#cameraLight.intensity =
|
|
2016
|
+
this.#cameraLight.intensity = BabylonJSRenderMath.lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1850
2017
|
this.#cameraLight.diffuse = new Color3(
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2018
|
+
BabylonJSRenderMath.lerp(0.35, 1.0, peakLight),
|
|
2019
|
+
BabylonJSRenderMath.lerp(0.35, 0.97, peakLight),
|
|
2020
|
+
BabylonJSRenderMath.lerp(0.36, 0.92, peakLight),
|
|
1854
2021
|
);
|
|
1855
2022
|
}
|
|
1856
2023
|
|
|
@@ -1872,7 +2039,7 @@ export default class BabylonJSController {
|
|
|
1872
2039
|
|
|
1873
2040
|
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1874
2041
|
const baseScaling = BabylonJSController.config.quality.hardwareScaling || 1;
|
|
1875
|
-
const scalingLevel =
|
|
2042
|
+
const scalingLevel = BabylonJSRenderMath.getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1876
2043
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1877
2044
|
}
|
|
1878
2045
|
|
|
@@ -1934,7 +2101,7 @@ export default class BabylonJSController {
|
|
|
1934
2101
|
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
1935
2102
|
* field of view on ArcRotate cameras to avoid the background appearing undersized on wide layouts.
|
|
1936
2103
|
* @private
|
|
1937
|
-
* @param {Camera|null|undefined} camera - Active camera to adjust.
|
|
2104
|
+
* @param {Camera|null|undefined} [camera] - Active camera to adjust; defaults to the current scene camera.
|
|
1938
2105
|
* @returns {void}
|
|
1939
2106
|
*/
|
|
1940
2107
|
#applyShowroomCameraFraming(camera = this.#scene?.activeCamera) {
|
|
@@ -1946,11 +2113,13 @@ export default class BabylonJSController {
|
|
|
1946
2113
|
}
|
|
1947
2114
|
|
|
1948
2115
|
/**
|
|
1949
|
-
* Resets pointer-picking sampling state.
|
|
2116
|
+
* Resets pointer-picking sampling and active touch state, cancelling any pending animation-menu hold.
|
|
1950
2117
|
* @private
|
|
1951
2118
|
* @returns {void}
|
|
1952
2119
|
*/
|
|
1953
2120
|
#resetPointerPickingState() {
|
|
2121
|
+
this.#cancelTouchMenuHold();
|
|
2122
|
+
this.#state.pointerPicking.touchPointers.clear();
|
|
1954
2123
|
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1955
2124
|
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1956
2125
|
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
@@ -1996,7 +2165,7 @@ export default class BabylonJSController {
|
|
|
1996
2165
|
}
|
|
1997
2166
|
|
|
1998
2167
|
/**
|
|
1999
|
-
* Sets up
|
|
2168
|
+
* Sets up canvas/scene interactions and cancels incomplete gestures when the owning window loses focus.
|
|
2000
2169
|
* @private
|
|
2001
2170
|
* @returns {void}
|
|
2002
2171
|
*/
|
|
@@ -2004,6 +2173,7 @@ export default class BabylonJSController {
|
|
|
2004
2173
|
if (this.#canvas) {
|
|
2005
2174
|
this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
|
|
2006
2175
|
this.#canvas.addEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
2176
|
+
this.#canvas.ownerDocument?.defaultView?.addEventListener("blur", this.#handlers.onPointerCancel);
|
|
2007
2177
|
}
|
|
2008
2178
|
if (this.#scene) {
|
|
2009
2179
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
@@ -2015,7 +2185,7 @@ export default class BabylonJSController {
|
|
|
2015
2185
|
}
|
|
2016
2186
|
|
|
2017
2187
|
/**
|
|
2018
|
-
* Removes interaction
|
|
2188
|
+
* Removes canvas/scene/window interaction listeners and releases pending gesture/resize resources.
|
|
2019
2189
|
* @private
|
|
2020
2190
|
* @returns {void}
|
|
2021
2191
|
*/
|
|
@@ -2023,6 +2193,7 @@ export default class BabylonJSController {
|
|
|
2023
2193
|
if (this.#canvas) {
|
|
2024
2194
|
this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
|
|
2025
2195
|
this.#canvas.removeEventListener("pointercancel", this.#handlers.onPointerCancel);
|
|
2196
|
+
this.#canvas.ownerDocument?.defaultView?.removeEventListener("blur", this.#handlers.onPointerCancel);
|
|
2026
2197
|
}
|
|
2027
2198
|
if (this.#scene !== null) {
|
|
2028
2199
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
@@ -2035,11 +2206,12 @@ export default class BabylonJSController {
|
|
|
2035
2206
|
}
|
|
2036
2207
|
|
|
2037
2208
|
/**
|
|
2038
|
-
*
|
|
2209
|
+
* Cancels pending touch-menu activation and disposes the animation controller if it exists.
|
|
2039
2210
|
* @private
|
|
2040
2211
|
* @returns {void}
|
|
2041
2212
|
*/
|
|
2042
2213
|
#disposeAnimationController() {
|
|
2214
|
+
this.#cancelTouchMenuHold();
|
|
2043
2215
|
if (this.#babylonJSAnimationController) {
|
|
2044
2216
|
this.#babylonJSAnimationController.dispose();
|
|
2045
2217
|
this.#babylonJSAnimationController = null;
|
|
@@ -2063,6 +2235,7 @@ export default class BabylonJSController {
|
|
|
2063
2235
|
* If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
|
|
2064
2236
|
* tearing down the engine while the XR session is still shutting down.
|
|
2065
2237
|
* Concurrent calls share the same in-flight promise so disposal runs only once.
|
|
2238
|
+
* Restores metric-placement transforms and releases owned native hit-test/input observers before disposal.
|
|
2066
2239
|
* @private
|
|
2067
2240
|
* @returns {Promise<void>}
|
|
2068
2241
|
*/
|
|
@@ -2072,7 +2245,10 @@ export default class BabylonJSController {
|
|
|
2072
2245
|
}
|
|
2073
2246
|
|
|
2074
2247
|
const xrExperience = this.#XRExperience;
|
|
2248
|
+
const placement = this.#arPlacement;
|
|
2075
2249
|
if (!xrExperience) {
|
|
2250
|
+
placement?.dispose();
|
|
2251
|
+
this.#arPlacement = null;
|
|
2076
2252
|
return;
|
|
2077
2253
|
}
|
|
2078
2254
|
|
|
@@ -2084,6 +2260,7 @@ export default class BabylonJSController {
|
|
|
2084
2260
|
} catch (error) {
|
|
2085
2261
|
console.warn("PrefViewer: Error exiting XR experience:", error);
|
|
2086
2262
|
} finally {
|
|
2263
|
+
placement?.dispose();
|
|
2087
2264
|
try {
|
|
2088
2265
|
xrExperience.dispose();
|
|
2089
2266
|
} catch (error) {
|
|
@@ -2091,6 +2268,7 @@ export default class BabylonJSController {
|
|
|
2091
2268
|
}
|
|
2092
2269
|
if (this.#XRExperience === xrExperience) {
|
|
2093
2270
|
this.#XRExperience = null;
|
|
2271
|
+
this.#arPlacement = null;
|
|
2094
2272
|
}
|
|
2095
2273
|
this.#disablingPromises.xr = null;
|
|
2096
2274
|
}
|
|
@@ -2270,7 +2448,7 @@ export default class BabylonJSController {
|
|
|
2270
2448
|
}
|
|
2271
2449
|
|
|
2272
2450
|
/**
|
|
2273
|
-
* Handles
|
|
2451
|
+
* Handles camera activity and mouse/pen hover; touch-generated moves never enable persistent animation hover.
|
|
2274
2452
|
* OPTIMIZATION: Tracks activity for idle mode.
|
|
2275
2453
|
* @private
|
|
2276
2454
|
* @param {PointerEvent} event - The pointer move event.
|
|
@@ -2287,7 +2465,7 @@ export default class BabylonJSController {
|
|
|
2287
2465
|
if (camera && !camera.metadata?.locked) {
|
|
2288
2466
|
this.#requestRender({ frames: 1, continuousMs: BabylonJSController.config.render.interactionMs });
|
|
2289
2467
|
}
|
|
2290
|
-
if (this.#babylonJSAnimationController && pickInfo) {
|
|
2468
|
+
if (!this.#isTouchPointerEvent(event) && this.#babylonJSAnimationController && pickInfo) {
|
|
2291
2469
|
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2292
2470
|
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
2293
2471
|
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
@@ -2299,14 +2477,108 @@ export default class BabylonJSController {
|
|
|
2299
2477
|
}
|
|
2300
2478
|
|
|
2301
2479
|
/**
|
|
2302
|
-
* Invalidates
|
|
2480
|
+
* Invalidates incomplete clicks/holds and tracked touches on pointer cancellation or window focus loss.
|
|
2303
2481
|
* @private
|
|
2304
2482
|
* @returns {void}
|
|
2305
2483
|
*/
|
|
2306
2484
|
#onPointerCancel() {
|
|
2485
|
+
this.#cancelTouchMenuHold();
|
|
2486
|
+
this.#state.pointerPicking.touchPointers.clear();
|
|
2307
2487
|
this.#state.pointerPicking.pointerDown = null;
|
|
2308
2488
|
}
|
|
2309
2489
|
|
|
2490
|
+
/**
|
|
2491
|
+
* Clears the owned long-press timer and temporary touch highlight, preserving selection, camera controls and menu visibility.
|
|
2492
|
+
* @private
|
|
2493
|
+
* @returns {void}
|
|
2494
|
+
*/
|
|
2495
|
+
#cancelTouchMenuHold() {
|
|
2496
|
+
clearTimeout(this.#state.pointerPicking.longPressTimeout);
|
|
2497
|
+
this.#state.pointerPicking.longPressTimeout = null;
|
|
2498
|
+
if (this.#state.pointerPicking.touchHighlightActive) {
|
|
2499
|
+
this.#setTouchAnimationHighlight(null);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* Identifies direct touch input and compatibility mouse events explicitly marked as touch-generated.
|
|
2505
|
+
* @private
|
|
2506
|
+
* @param {PointerEvent|MouseEvent|null|undefined} event - Current browser pointer event.
|
|
2507
|
+
* @returns {boolean} True when the event must not drive persistent mouse hover.
|
|
2508
|
+
*/
|
|
2509
|
+
#isTouchPointerEvent(event) {
|
|
2510
|
+
return event?.pointerType === "touch" || event?.sourceCapabilities?.firesTouchEvents === true;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Applies opening-only feedback for a pending touch hold, or clears previous touch/mouse highlight.
|
|
2515
|
+
* Honors the configured highlight preference without changing it; invalidates the hover cache and wakes rendering on changes.
|
|
2516
|
+
* @private
|
|
2517
|
+
* @param {PickingInfo|null} pick - Opening candidate, or null to remove feedback.
|
|
2518
|
+
* @returns {void}
|
|
2519
|
+
*/
|
|
2520
|
+
#setTouchAnimationHighlight(pick) {
|
|
2521
|
+
const result = this.#babylonJSAnimationController?.highlightMeshes(this.#renderSettings.highlightEnabled ? pick : null, true);
|
|
2522
|
+
this.#state.pointerPicking.touchHighlightActive = result?.highlighted ?? false;
|
|
2523
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
2524
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
2525
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
2526
|
+
if (result?.changed) {
|
|
2527
|
+
this.#requestRender({
|
|
2528
|
+
frames: 1
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* Checks whether current content and UI can accept a touch opening-menu gesture or its temporary feedback.
|
|
2535
|
+
* @private
|
|
2536
|
+
* @returns {boolean} False for missing content, hidden/modal UI, disconnected canvas or an active XR transition/session.
|
|
2537
|
+
*/
|
|
2538
|
+
#canUseTouchAnimationMenu() {
|
|
2539
|
+
const component = this.#canvas?.closest("pref-viewer-3d");
|
|
2540
|
+
const viewer = this.#canvas?.closest("pref-viewer");
|
|
2541
|
+
const xrState = this.#XRExperience?.baseExperience?.state;
|
|
2542
|
+
return !!(this.#scene && this.#canvas?.isConnected && this.#babylonJSAnimationController?.hasAnimations()
|
|
2543
|
+
&& this.#canvas.ownerDocument.visibilityState !== "hidden" && component?.getAttribute("visible") !== "false"
|
|
2544
|
+
&& viewer?.getAttribute("mode") !== "2d" && !viewer?.querySelector("pref-viewer-dialog[open]")
|
|
2545
|
+
&& (xrState === undefined || xrState === WebXRState.NOT_IN_XR));
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* Schedules a single-touch context action using the existing click movement tolerance.
|
|
2550
|
+
* Highlights the opening candidate during the pending half-second only, if enabled; early release or cancellation clears it.
|
|
2551
|
+
* Rechecks current content, visible UI and XR eligibility before picking the original CSS touch position.
|
|
2552
|
+
* Recognized holds consume the click and its release; no camera input is prevented or captured here.
|
|
2553
|
+
* @private
|
|
2554
|
+
* @param {PointerEvent} event - Primary touch down on this viewer's canvas.
|
|
2555
|
+
* @returns {void} Owns one cancellable timer until activation, interruption or teardown.
|
|
2556
|
+
*/
|
|
2557
|
+
#scheduleTouchMenuHold(event) {
|
|
2558
|
+
this.#cancelTouchMenuHold();
|
|
2559
|
+
const state = this.#state.pointerPicking;
|
|
2560
|
+
const start = state.pointerDown;
|
|
2561
|
+
if (event?.pointerType !== "touch" || !start || state.touchPointers.size !== 1 || !this.#canUseTouchAnimationMenu()) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
const rect = this.#canvas.getBoundingClientRect();
|
|
2565
|
+
this.#setTouchAnimationHighlight(this.#scene.pick(start.x - rect.left, start.y - rect.top));
|
|
2566
|
+
state.longPressTimeout = setTimeout(() => {
|
|
2567
|
+
state.longPressTimeout = null;
|
|
2568
|
+
this.#cancelTouchMenuHold();
|
|
2569
|
+
if (state.pointerDown !== start || start.dragged || state.touchPointers.size !== 1 || !this.#canUseTouchAnimationMenu()) {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
start.longPressed = start.dragged = true;
|
|
2573
|
+
const rect = this.#canvas.getBoundingClientRect();
|
|
2574
|
+
const pick = this.#scene.pick(start.x - rect.left, start.y - rect.top);
|
|
2575
|
+
this.#babylonJSAnimationController.showMenu(pick);
|
|
2576
|
+
this.#requestRender({
|
|
2577
|
+
frames: 1
|
|
2578
|
+
});
|
|
2579
|
+
}, BabylonJSController.config.pointerPicking.longPressMs);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2310
2582
|
/**
|
|
2311
2583
|
* Emits the internal 3D click contract for every model mesh crossed by the pointer ray.
|
|
2312
2584
|
* @private
|
|
@@ -2344,6 +2616,8 @@ export default class BabylonJSController {
|
|
|
2344
2616
|
/**
|
|
2345
2617
|
* Handles pointer events observed on the Babylon.js scene.
|
|
2346
2618
|
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
2619
|
+
* Recognizes stationary primary touch holds independently of short-click selection; release preserves their menu.
|
|
2620
|
+
* Touch motion skips hover raycasts; touch down clears stale hover before starting temporary hold feedback.
|
|
2347
2621
|
* @private
|
|
2348
2622
|
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
2349
2623
|
* @returns {void}
|
|
@@ -2351,26 +2625,47 @@ export default class BabylonJSController {
|
|
|
2351
2625
|
#onPointerObservable(info) {
|
|
2352
2626
|
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
2353
2627
|
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)) {
|
|
2628
|
+
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
2629
|
start.dragged = true;
|
|
2630
|
+
this.#cancelTouchMenuHold();
|
|
2357
2631
|
}
|
|
2358
2632
|
let pickInfo = null;
|
|
2359
2633
|
let lastPickedMeshId = null;
|
|
2360
|
-
if (this.#shouldPickOnPointerMove()) {
|
|
2634
|
+
if (!this.#isTouchPointerEvent(info.event) && this.#shouldPickOnPointerMove()) {
|
|
2361
2635
|
pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
2362
2636
|
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
2363
2637
|
}
|
|
2364
2638
|
this.#onPointerMove(info.event, pickInfo);
|
|
2365
2639
|
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
2366
2640
|
} else if (info.type === PointerEventTypes.POINTERDOWN) {
|
|
2367
|
-
this.#
|
|
2368
|
-
|
|
2369
|
-
|
|
2641
|
+
this.#cancelTouchMenuHold();
|
|
2642
|
+
if (this.#isTouchPointerEvent(info.event)) {
|
|
2643
|
+
this.#setTouchAnimationHighlight(null);
|
|
2644
|
+
}
|
|
2645
|
+
if (info.event?.pointerType === "touch") {
|
|
2646
|
+
this.#state.pointerPicking.touchPointers.add(info.event.pointerId);
|
|
2647
|
+
}
|
|
2648
|
+
this.#state.pointerPicking.pointerDown = info.event?.button === 0 && info.event.isPrimary !== false && this.#state.pointerPicking.touchPointers.size <= 1 && !this.#state.pointerPicking.pointerDown ? {
|
|
2649
|
+
x: info.event.clientX,
|
|
2650
|
+
y: info.event.clientY,
|
|
2651
|
+
button: info.event.button,
|
|
2652
|
+
pointerId: info.event.pointerId
|
|
2653
|
+
} : null;
|
|
2654
|
+
this.#scheduleTouchMenuHold(info.event);
|
|
2370
2655
|
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
2656
|
+
this.#cancelTouchMenuHold();
|
|
2657
|
+
this.#state.pointerPicking.touchPointers.delete(info.event?.pointerId);
|
|
2371
2658
|
const pointerDown = this.#state.pointerPicking.pointerDown;
|
|
2372
2659
|
this.#state.pointerPicking.pointerDown = null;
|
|
2373
|
-
|
|
2660
|
+
if (pointerDown?.longPressed && pointerDown.pointerId === info.event?.pointerId) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
const pointerUp = {
|
|
2664
|
+
x: info.event?.clientX,
|
|
2665
|
+
y: info.event?.clientY,
|
|
2666
|
+
button: info.event?.button,
|
|
2667
|
+
pointerId: info.event?.pointerId
|
|
2668
|
+
};
|
|
2374
2669
|
if (BabylonJSSelectionController.isPrimaryClickGesture(pointerDown, pointerUp, BabylonJSController.config.pointerPicking.clickMaxDistancePx)) {
|
|
2375
2670
|
this.#emitModelMeshClick();
|
|
2376
2671
|
}
|
|
@@ -2386,6 +2681,7 @@ export default class BabylonJSController {
|
|
|
2386
2681
|
|
|
2387
2682
|
/**
|
|
2388
2683
|
* Applies a pending engine resize, cancels cube navigation and wakes rendering.
|
|
2684
|
+
* Skips zero-sized hidden canvases to prevent intrinsic-buffer/DPR growth; a visible resize retries later.
|
|
2389
2685
|
* @private
|
|
2390
2686
|
* @param {number} elapsed - Time since the previous resize, retained for diagnostics.
|
|
2391
2687
|
* @returns {void}
|
|
@@ -2396,6 +2692,12 @@ export default class BabylonJSController {
|
|
|
2396
2692
|
if (!this.#engine) {
|
|
2397
2693
|
return;
|
|
2398
2694
|
}
|
|
2695
|
+
// Babylon falls back to the intrinsic canvas size for hidden elements, then applies
|
|
2696
|
+
// DPR again. Repeated hidden resizes can exponentially grow GPU attachments on WebKit.
|
|
2697
|
+
// ResizeObserver will notify us again when the canvas has a visible CSS size.
|
|
2698
|
+
if (!this.#canvas.clientWidth || !this.#canvas.clientHeight) {
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2399
2701
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2400
2702
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2401
2703
|
this.#applyShowroomCameraFraming();
|
|
@@ -2531,6 +2833,7 @@ export default class BabylonJSController {
|
|
|
2531
2833
|
* Handles canvas resize notifications.
|
|
2532
2834
|
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
2533
2835
|
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
2836
|
+
* Coalesces notifications through a throttled timer and animation frame, outside ResizeObserver delivery.
|
|
2534
2837
|
* @private
|
|
2535
2838
|
* @returns {void}
|
|
2536
2839
|
*/
|
|
@@ -2542,28 +2845,33 @@ export default class BabylonJSController {
|
|
|
2542
2845
|
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
2543
2846
|
|
|
2544
2847
|
|
|
2545
|
-
if (elapsed >= BabylonJSController.config.resize.throttleMs) {
|
|
2546
|
-
this.#applyResize(elapsed);
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
2848
|
if (this.#state.resize.isScheduled) {
|
|
2551
2849
|
return;
|
|
2552
2850
|
}
|
|
2553
2851
|
|
|
2554
2852
|
this.#state.resize.isScheduled = true;
|
|
2853
|
+
// Mutating canvas buffers/layout inside ResizeObserver delivery causes WebKit's
|
|
2854
|
+
// "undelivered notifications" loop. Apply before the next paint instead.
|
|
2555
2855
|
const waitMs = Math.max(0, BabylonJSController.config.resize.throttleMs - elapsed);
|
|
2556
2856
|
this.#state.resize.timeoutId = setTimeout(() => {
|
|
2557
|
-
this.#
|
|
2857
|
+
this.#state.resize.timeoutId = null;
|
|
2858
|
+
this.#state.resize.frameId = requestAnimationFrame(() => {
|
|
2859
|
+
this.#state.resize.frameId = null;
|
|
2860
|
+
this.#applyResize(elapsed);
|
|
2861
|
+
});
|
|
2558
2862
|
}, waitMs);
|
|
2559
2863
|
}
|
|
2560
2864
|
|
|
2561
2865
|
/**
|
|
2562
|
-
* Clears
|
|
2866
|
+
* Clears both the queued throttle timer and animation-frame resize callback during teardown.
|
|
2563
2867
|
* @private
|
|
2564
2868
|
* @returns {void}
|
|
2565
2869
|
*/
|
|
2566
2870
|
#cancelScheduledResize() {
|
|
2871
|
+
if (this.#state.resize.frameId != null) {
|
|
2872
|
+
cancelAnimationFrame(this.#state.resize.frameId);
|
|
2873
|
+
}
|
|
2874
|
+
this.#state.resize.frameId = null;
|
|
2567
2875
|
if (this.#state.resize.timeoutId !== null) {
|
|
2568
2876
|
clearTimeout(this.#state.resize.timeoutId);
|
|
2569
2877
|
}
|
|
@@ -2639,7 +2947,7 @@ export default class BabylonJSController {
|
|
|
2639
2947
|
|
|
2640
2948
|
/**
|
|
2641
2949
|
* 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
|
|
2950
|
+
* Configures unlocked orbital cameras for proportional native wheel/pinch input, rebinds the model zoom guard
|
|
2643
2951
|
* and synchronizes the cube after selecting the active camera.
|
|
2644
2952
|
* A missing named camera falls back to the unlocked default camera, rather than retaining an imported view.
|
|
2645
2953
|
* @private
|
|
@@ -2694,7 +3002,7 @@ export default class BabylonJSController {
|
|
|
2694
3002
|
camera.detachControl();
|
|
2695
3003
|
if (!cameraState.locked) {
|
|
2696
3004
|
if (camera instanceof ArcRotateCamera) {
|
|
2697
|
-
camera
|
|
3005
|
+
this.#configureOrbitalZoom(camera);
|
|
2698
3006
|
}
|
|
2699
3007
|
camera.attachControl(this.#canvas, true);
|
|
2700
3008
|
}
|
|
@@ -3000,11 +3308,13 @@ export default class BabylonJSController {
|
|
|
3000
3308
|
* Loads a single asset container (model, environment, materials, etc.) based on the container state flags.
|
|
3001
3309
|
* Skips work when nothing is pending, otherwise resolves the GLTF source, refreshes cache metadata and streams it
|
|
3002
3310
|
* into the Babylon.js scene via `LoadAssetContainerAsync`.
|
|
3311
|
+
* Distinguishes unchanged content from a failed replacement for native AR readiness and revokes source URLs
|
|
3312
|
+
* after attempted imports. Resolver failures may reject; Babylon import errors are logged and returned as null.
|
|
3003
3313
|
* @private
|
|
3004
3314
|
* @param {object} container - Container descriptor that carries the GLTF storage pointer and current cache info.
|
|
3005
3315
|
* @param {boolean} [force=false] - When true, bypasses cached size/timestamp so the resolver re-downloads the asset.
|
|
3006
|
-
* @returns {Promise<[object, AssetContainer|
|
|
3007
|
-
*
|
|
3316
|
+
* @returns {Promise<[object, AssetContainer|false|null]>} Container paired with imported assets, false for skipped,
|
|
3317
|
+
* unchanged or unresolved content, or null after a caught import failure.
|
|
3008
3318
|
* @description
|
|
3009
3319
|
* 1. Validates that the container has pending data and initializes the shared `GLTFResolver` if needed.
|
|
3010
3320
|
* 2. Requests the source blob (respecting the cached size/timestamp unless `force` is set) and stores the new cache
|
|
@@ -3028,7 +3338,18 @@ export default class BabylonJSController {
|
|
|
3028
3338
|
const currentSize = force ? 0 : container.state.size;
|
|
3029
3339
|
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
3030
3340
|
|
|
3031
|
-
|
|
3341
|
+
const isQuickLookContent = container.state.name === "model" || container.state.name === "materials";
|
|
3342
|
+
if (isQuickLookContent) {
|
|
3343
|
+
this.#quickLookValid[container.state.name] = false;
|
|
3344
|
+
}
|
|
3345
|
+
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp, true);
|
|
3346
|
+
|
|
3347
|
+
if (sourceData?.unchanged) {
|
|
3348
|
+
if (isQuickLookContent) {
|
|
3349
|
+
this.#quickLookValid[container.state.name] = !!container.assetContainer;
|
|
3350
|
+
}
|
|
3351
|
+
return [container, false];
|
|
3352
|
+
}
|
|
3032
3353
|
|
|
3033
3354
|
if (!sourceData) {
|
|
3034
3355
|
return [container, false];
|
|
@@ -3070,6 +3391,7 @@ export default class BabylonJSController {
|
|
|
3070
3391
|
/**
|
|
3071
3392
|
* Cancels pending cube navigation, loads asset containers and adds replacements to the scene.
|
|
3072
3393
|
* Current model selection is resolved by the zoom guard on subsequent input updates.
|
|
3394
|
+
* Invalidates native content during overlapping loads, then prepares only completed current-model content.
|
|
3073
3395
|
* @private
|
|
3074
3396
|
* @param {boolean} [force=false] - Whether to bypass existing asset cache metadata.
|
|
3075
3397
|
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
@@ -3079,6 +3401,29 @@ export default class BabylonJSController {
|
|
|
3079
3401
|
* Returns an object with success status and error details.
|
|
3080
3402
|
*/
|
|
3081
3403
|
async #loadContainers(force = false) {
|
|
3404
|
+
this.#quickLookAction?.invalidate();
|
|
3405
|
+
this.#quickLookLoads += 1;
|
|
3406
|
+
try {
|
|
3407
|
+
const result = await this.#loadContainersInternal(force);
|
|
3408
|
+
if (!result.success) {
|
|
3409
|
+
this.#quickLookValid.materials = false;
|
|
3410
|
+
}
|
|
3411
|
+
return result;
|
|
3412
|
+
} finally {
|
|
3413
|
+
this.#quickLookLoads -= 1;
|
|
3414
|
+
this.#refreshQuickLook();
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* Loads pending containers in parallel, replaces scene assets and reapplies material/camera/light state.
|
|
3420
|
+
* Cancels cube/animation work, stops rendering during changes and restarts it after setup. Disposed scenes
|
|
3421
|
+
* discard late assets; successful model/material replacements update Quick Look content-validity state.
|
|
3422
|
+
* @private
|
|
3423
|
+
* @param {boolean} [force=false] - Whether to bypass cached source size/timestamp checks.
|
|
3424
|
+
* @returns {Promise<{success: boolean, error: *}>} Aggregate scene-load result and any captured setup error.
|
|
3425
|
+
*/
|
|
3426
|
+
async #loadContainersInternal(force = false) {
|
|
3082
3427
|
this.#viewCube?.cancel();
|
|
3083
3428
|
this.#resetPointerPickingState();
|
|
3084
3429
|
this.#detachAnimationChangedListener();
|
|
@@ -3125,6 +3470,9 @@ export default class BabylonJSController {
|
|
|
3125
3470
|
}
|
|
3126
3471
|
this.#replaceContainer(container, assetContainer);
|
|
3127
3472
|
container.state.setSuccess(true);
|
|
3473
|
+
if (container.state.name === "model" || container.state.name === "materials") {
|
|
3474
|
+
this.#quickLookValid[container.state.name] = true;
|
|
3475
|
+
}
|
|
3128
3476
|
} else {
|
|
3129
3477
|
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
3130
3478
|
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
@@ -3345,6 +3693,7 @@ export default class BabylonJSController {
|
|
|
3345
3693
|
|
|
3346
3694
|
/**
|
|
3347
3695
|
* Opens a modal dialog for downloading the 3D scene, model, or environment.
|
|
3696
|
+
* Uses shared dialog presentation and wrapping option groups; preserves export choices and routing.
|
|
3348
3697
|
* @private
|
|
3349
3698
|
* @returns {void}
|
|
3350
3699
|
*/
|
|
@@ -3358,15 +3707,15 @@ export default class BabylonJSController {
|
|
|
3358
3707
|
const texts = I18n.translate("prefViewer.downloadDialog", locale ? { locale } : undefined) || {};
|
|
3359
3708
|
const header = texts.title || "Download 3D Scene";
|
|
3360
3709
|
const content = `
|
|
3361
|
-
<form slot="content" id="download-dialog-form"
|
|
3710
|
+
<form slot="content" id="download-dialog-form" class="download-dialog-form">
|
|
3362
3711
|
<h4>${texts.sections?.content || "Content"}</h4>
|
|
3363
|
-
<div
|
|
3712
|
+
<div class="download-dialog-options">
|
|
3364
3713
|
<label><input type="radio" name="content" value="1"> ${texts.options?.model || "Model"}</label>
|
|
3365
3714
|
<label><input type="radio" name="content" value="2"> ${texts.options?.scene || "Scene"}</label>
|
|
3366
3715
|
<label><input type="radio" name="content" value="0" checked> ${texts.options?.both || "Both"}</label>
|
|
3367
3716
|
</div>
|
|
3368
3717
|
<h4>${texts.sections?.format || "Format"}</h4>
|
|
3369
|
-
<div
|
|
3718
|
+
<div class="download-dialog-options">
|
|
3370
3719
|
<label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
|
|
3371
3720
|
<label><input type="radio" name="format" value="glb"> GLB</label>
|
|
3372
3721
|
<label><input type="radio" name="format" value="usdz"> USDZ</label>
|
|
@@ -3416,7 +3765,7 @@ export default class BabylonJSController {
|
|
|
3416
3765
|
*/
|
|
3417
3766
|
|
|
3418
3767
|
/**
|
|
3419
|
-
* Creates the
|
|
3768
|
+
* Creates the selected SVG/Canvas cube once a live scene/camera exists and teardown is not in progress.
|
|
3420
3769
|
* Resolves camera/model state through callbacks; does not reload assets or camera options.
|
|
3421
3770
|
* @private
|
|
3422
3771
|
* @returns {void}
|
|
@@ -3435,7 +3784,7 @@ export default class BabylonJSController {
|
|
|
3435
3784
|
onLayout: this.#viewCubeConfig.onLayout,
|
|
3436
3785
|
// A cube drag is pointer interaction like a main-canvas drag: keep the loop continuous for the same window.
|
|
3437
3786
|
requestRender: (options) => this.#requestRender({ frames: 2, continuousMs: options?.interaction ? BabylonJSController.config.render.interactionMs : 0 }),
|
|
3438
|
-
});
|
|
3787
|
+
}, this.#viewCubeConfig.renderer);
|
|
3439
3788
|
}
|
|
3440
3789
|
|
|
3441
3790
|
/**
|
|
@@ -3444,10 +3793,15 @@ export default class BabylonJSController {
|
|
|
3444
3793
|
* Observes canvas resize events, binds model-only orbital zoom protection and adaptive default-camera near clipping,
|
|
3445
3794
|
* and lazily creates the requested cube. Imported camera clipping is preserved by the host policy.
|
|
3446
3795
|
* Aborts remaining initialization if the scene was replaced/disposed during texture readiness.
|
|
3796
|
+
* Waits for preceding teardown and guards XR route creation with the current scene generation.
|
|
3447
3797
|
* @public
|
|
3448
3798
|
* @returns {Promise<void>}
|
|
3449
3799
|
*/
|
|
3450
3800
|
async enable() {
|
|
3801
|
+
if (this.#disablingPromises.general) {
|
|
3802
|
+
await this.#disablingPromises.general;
|
|
3803
|
+
}
|
|
3804
|
+
const enabledGeneration = ++this.#xrGeneration;
|
|
3451
3805
|
this.#configureDracoCompression();
|
|
3452
3806
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
3453
3807
|
this.#engine.disableUniformBuffers = true;
|
|
@@ -3490,7 +3844,7 @@ export default class BabylonJSController {
|
|
|
3490
3844
|
this.#initializeViewCube();
|
|
3491
3845
|
const enabledScene = this.#scene;
|
|
3492
3846
|
await this.#ensureEnvironmentBRDFTextureReady();
|
|
3493
|
-
if (!this.#engine || this.#scene !== enabledScene) {
|
|
3847
|
+
if (!this.#engine || this.#scene !== enabledScene || this.#xrGeneration !== enabledGeneration) {
|
|
3494
3848
|
return;
|
|
3495
3849
|
}
|
|
3496
3850
|
this.#enableInteraction();
|
|
@@ -3503,10 +3857,16 @@ export default class BabylonJSController {
|
|
|
3503
3857
|
* floor and suspending cube creation until the next enable. Cleans up light, XR and resolver resources.
|
|
3504
3858
|
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
3505
3859
|
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
3860
|
+
* Immediately invalidates pending XR setup and disposes the Quick Look action before awaiting teardown.
|
|
3506
3861
|
* @public
|
|
3507
3862
|
* @returns {Promise<void>}
|
|
3508
3863
|
*/
|
|
3509
3864
|
async disable() {
|
|
3865
|
+
// Invalidate pending support/setup before the first teardown await, even while the scene is live.
|
|
3866
|
+
this.#xrGeneration += 1;
|
|
3867
|
+
this.#quickLookAction?.dispose();
|
|
3868
|
+
this.#quickLookAction = null;
|
|
3869
|
+
this.#quickLookValid = { model: false, materials: true };
|
|
3510
3870
|
BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
|
|
3511
3871
|
BabylonJSCameraWheelMotion.cancel(this.#camera);
|
|
3512
3872
|
BabylonJSCameraWheelMotion.releaseSurfaceFocus(this.#camera);
|
|
@@ -3564,18 +3924,30 @@ export default class BabylonJSController {
|
|
|
3564
3924
|
}
|
|
3565
3925
|
|
|
3566
3926
|
/**
|
|
3567
|
-
*
|
|
3927
|
+
* Selects the cube implementation while preserving the active scene, camera and asset state.
|
|
3928
|
+
* @param {"svg"|"canvas"} renderer - Per-viewer preference; invalid values use Canvas.
|
|
3929
|
+
* @returns {void} Disabled cubes retain preference without allocating a new overlay.
|
|
3930
|
+
*/
|
|
3931
|
+
setViewCubeRenderer(renderer) {
|
|
3932
|
+
this.#viewCubeConfig.renderer = BabylonJSViewCubeController.normalizeRenderer(renderer);
|
|
3933
|
+
this.#viewCube?.setRenderer(this.#viewCubeConfig.renderer);
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
/**
|
|
3937
|
+
* Resizes/synchronizes the existing cube after culture, camera or presentation changes without activating it.
|
|
3938
|
+
* Also refreshes the existing Quick Look action's localized label from the host culture.
|
|
3568
3939
|
* @public
|
|
3569
3940
|
* @returns {void}
|
|
3570
3941
|
*/
|
|
3571
3942
|
refreshViewCube() {
|
|
3572
|
-
this.#
|
|
3943
|
+
this.#quickLookAction?.refreshCulture();
|
|
3944
|
+
this.#viewCube?.resize();
|
|
3573
3945
|
}
|
|
3574
3946
|
|
|
3575
3947
|
/**
|
|
3576
3948
|
* Applies the host-owned semantic selection state to the current model meshes.
|
|
3577
3949
|
* @public
|
|
3578
|
-
* @param {object} state - Canonical PrefViewer selection snapshot.
|
|
3950
|
+
* @param {object} [state={}] - Canonical PrefViewer selection snapshot.
|
|
3579
3951
|
* @returns {void}
|
|
3580
3952
|
*/
|
|
3581
3953
|
applySelectionState(state = {}) {
|
|
@@ -3583,9 +3955,9 @@ export default class BabylonJSController {
|
|
|
3583
3955
|
}
|
|
3584
3956
|
|
|
3585
3957
|
/**
|
|
3586
|
-
* Downloads the current scene, model, or environment as a GLB file.
|
|
3958
|
+
* Downloads the current scene, model, or environment as a GLB file, excluding transient AR feedback.
|
|
3587
3959
|
* @public
|
|
3588
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
3960
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3589
3961
|
* @returns {void}
|
|
3590
3962
|
*/
|
|
3591
3963
|
downloadGLB(content = 0) {
|
|
@@ -3608,7 +3980,8 @@ export default class BabylonJSController {
|
|
|
3608
3980
|
break;
|
|
3609
3981
|
}
|
|
3610
3982
|
fileName = this.#addDateToName(fileName);
|
|
3611
|
-
GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true
|
|
3983
|
+
GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true,
|
|
3984
|
+
shouldExportNode: node => !node.metadata?.prefViewerARFeedback }).then((glb) => {
|
|
3612
3985
|
if (glb) {
|
|
3613
3986
|
glb.downloadFiles();
|
|
3614
3987
|
}
|
|
@@ -3616,9 +3989,9 @@ export default class BabylonJSController {
|
|
|
3616
3989
|
}
|
|
3617
3990
|
|
|
3618
3991
|
/**
|
|
3619
|
-
* Downloads the current scene, model, or environment as a glTF ZIP file.
|
|
3992
|
+
* Downloads the current scene, model, or environment as a glTF ZIP file, excluding transient AR feedback.
|
|
3620
3993
|
* @public
|
|
3621
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
3994
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3622
3995
|
* @returns {void}
|
|
3623
3996
|
*/
|
|
3624
3997
|
downloadGLTF(content = 0) {
|
|
@@ -3645,7 +4018,8 @@ export default class BabylonJSController {
|
|
|
3645
4018
|
break;
|
|
3646
4019
|
}
|
|
3647
4020
|
comment += "\nPrefViewer by Preference, S.L.\npreference.com\n";
|
|
3648
|
-
GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true
|
|
4021
|
+
GLTF2Export.GLTFAsync(container, fileName, { exportWithoutWaitingForScene: true,
|
|
4022
|
+
shouldExportNode: node => !node.metadata?.prefViewerARFeedback }).then((gltf) => {
|
|
3649
4023
|
if (gltf?.files) {
|
|
3650
4024
|
this.#downloadZip(gltf.files, fileName, comment, true);
|
|
3651
4025
|
}
|
|
@@ -3653,9 +4027,9 @@ export default class BabylonJSController {
|
|
|
3653
4027
|
}
|
|
3654
4028
|
|
|
3655
4029
|
/**
|
|
3656
|
-
* Downloads the current scene, model, or environment as a USDZ file.
|
|
4030
|
+
* Downloads the current scene, model, or environment as a USDZ file, excluding transient AR feedback.
|
|
3657
4031
|
* @public
|
|
3658
|
-
* @param {number} content - 0 for full scene, 1 for model, 2 for environment.
|
|
4032
|
+
* @param {number} [content=0] - 0 for full scene, 1 for model, 2 for environment.
|
|
3659
4033
|
* @returns {void}
|
|
3660
4034
|
*/
|
|
3661
4035
|
downloadUSDZ(content = 0) {
|
|
@@ -3678,7 +4052,7 @@ export default class BabylonJSController {
|
|
|
3678
4052
|
break;
|
|
3679
4053
|
}
|
|
3680
4054
|
fileName = this.#addDateToName(fileName);
|
|
3681
|
-
USDZExportAsync(container).then((response) => {
|
|
4055
|
+
USDZExportAsync(container, undefined, mesh => !mesh.metadata?.prefViewerARFeedback).then((response) => {
|
|
3682
4056
|
if (response) {
|
|
3683
4057
|
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
3684
4058
|
}
|
|
@@ -3698,7 +4072,8 @@ export default class BabylonJSController {
|
|
|
3698
4072
|
* Returns a diagnostic snapshot of the current Babylon.js runtime state.
|
|
3699
4073
|
* Intended for integration troubleshooting and should not mutate scene state.
|
|
3700
4074
|
* @public
|
|
3701
|
-
* @returns {object}
|
|
4075
|
+
* @returns {object} Canvas, engine, scene, container, render-loop and settings diagnostics, plus nullable
|
|
4076
|
+
* `ar` placement state, metric distance/position and drag/model availability when WebXR is configured.
|
|
3702
4077
|
*/
|
|
3703
4078
|
getDebugInfo() {
|
|
3704
4079
|
|
|
@@ -3737,6 +4112,7 @@ export default class BabylonJSController {
|
|
|
3737
4112
|
|
|
3738
4113
|
|
|
3739
4114
|
return {
|
|
4115
|
+
ar: this.#arPlacement?.getDebugInfo() ?? null,
|
|
3740
4116
|
canvas: {
|
|
3741
4117
|
clientWidth: this.#canvas?.clientWidth ?? null,
|
|
3742
4118
|
clientHeight: this.#canvas?.clientHeight ?? null,
|
|
@@ -3833,13 +4209,16 @@ export default class BabylonJSController {
|
|
|
3833
4209
|
/**
|
|
3834
4210
|
* Applies material options from the configuration to the relevant meshes.
|
|
3835
4211
|
* Stops and restarts the render loop to apply changes.
|
|
4212
|
+
* Invalidates native content during the update and requests fresh Quick Look preparation after render readiness.
|
|
3836
4213
|
* @public
|
|
3837
4214
|
* @returns {boolean} True if material options were set successfully, false otherwise.
|
|
3838
4215
|
*/
|
|
3839
4216
|
setMaterialOptions() {
|
|
4217
|
+
this.#quickLookAction?.invalidate();
|
|
3840
4218
|
this.#stopRender();
|
|
3841
4219
|
const materialsOptionsSetted = this.#setOptions_Materials();
|
|
3842
|
-
this.#startRender()
|
|
4220
|
+
void this.#startRender().then(() => this.#refreshQuickLook())
|
|
4221
|
+
.catch(error => console.warn("PrefViewer: failed to prepare material rendering", error));
|
|
3843
4222
|
return materialsOptionsSetted;
|
|
3844
4223
|
}
|
|
3845
4224
|
|
|
@@ -3864,7 +4243,7 @@ export default class BabylonJSController {
|
|
|
3864
4243
|
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3865
4244
|
* the active lighting time is reset to `BabylonJSController.config.render.defaultSettings.lightingTimeOfDay` (0.5).
|
|
3866
4245
|
* @public
|
|
3867
|
-
* @param {object} config - Immediate render/illumination configuration.
|
|
4246
|
+
* @param {object} [config={}] - Immediate render/illumination configuration.
|
|
3868
4247
|
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
3869
4248
|
* @param {boolean|string|null} [config.highlightEnabled] - Enables/disables hover highlight; string "false" is normalized to false, null is ignored.
|
|
3870
4249
|
* @param {string|null} [config.highlightColor] - Hex color used by hover highlight; null is ignored.
|
|
@@ -3896,7 +4275,7 @@ export default class BabylonJSController {
|
|
|
3896
4275
|
}
|
|
3897
4276
|
|
|
3898
4277
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3899
|
-
const normalizedTime =
|
|
4278
|
+
const normalizedTime = BabylonJSRenderMath.clamp01(config.lightingTimeOfDay);
|
|
3900
4279
|
if (this.#renderSettings.lightingTimeOfDay !== normalizedTime) {
|
|
3901
4280
|
this.#renderSettings.lightingTimeOfDay = normalizedTime;
|
|
3902
4281
|
shouldApplyLighting = true;
|
|
@@ -3947,6 +4326,8 @@ export default class BabylonJSController {
|
|
|
3947
4326
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
3948
4327
|
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
3949
4328
|
* Restarts the render loop to apply changes.
|
|
4329
|
+
* Refreshes Quick Look readiness after rendering so hidden model content cannot retain an active native link.
|
|
4330
|
+
* Render/preparation failures are logged asynchronously; this method does not wait for native preparation.
|
|
3950
4331
|
* @public
|
|
3951
4332
|
* @param {string} name - The name of the container to show or hide.
|
|
3952
4333
|
* @param {boolean} show - True to show the container, false to hide it.
|
|
@@ -3964,7 +4345,8 @@ export default class BabylonJSController {
|
|
|
3964
4345
|
this.#stopRender();
|
|
3965
4346
|
show ? this.#addContainer(container) : this.#removeContainer(container);
|
|
3966
4347
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
3967
|
-
this.#startRender()
|
|
4348
|
+
void this.#startRender().then(() => this.#refreshQuickLook())
|
|
4349
|
+
.catch(error => console.warn("PrefViewer: failed to prepare visibility rendering", error));
|
|
3968
4350
|
}
|
|
3969
4351
|
|
|
3970
4352
|
/**
|
|
@@ -3973,7 +4355,7 @@ export default class BabylonJSController {
|
|
|
3973
4355
|
* Immediate settings such as highlight, animation menu, and time-of-day lighting are handled by
|
|
3974
4356
|
* `setIlluminationConfig()` before this method receives the remaining reload settings.
|
|
3975
4357
|
* @public
|
|
3976
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of reload render toggles to apply.
|
|
4358
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} [settings={}] Partial set of reload render toggles to apply.
|
|
3977
4359
|
* @returns {{changed:boolean, settings:object}} Current full render settings snapshot and whether a reload is required.
|
|
3978
4360
|
* @description
|
|
3979
4361
|
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
@@ -4042,9 +4424,9 @@ export default class BabylonJSController {
|
|
|
4042
4424
|
}
|
|
4043
4425
|
|
|
4044
4426
|
/**
|
|
4045
|
-
* Configures
|
|
4427
|
+
* Configures animation highlights for mouse hover and pending touch holds.
|
|
4046
4428
|
* @private
|
|
4047
|
-
* @param {{enabled?: boolean, color?: string}} config - Configuration object.
|
|
4429
|
+
* @param {{enabled?: boolean, color?: string}} [config={}] - Configuration object.
|
|
4048
4430
|
* @returns {void}
|
|
4049
4431
|
*/
|
|
4050
4432
|
#setHighlightConfig(config = {}) {
|