@preference-sl/pref-viewer 2.11.0-beta.0 → 2.11.0-beta.2
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/package.json +5 -3
- package/src/babylonjs-controller.js +932 -0
- package/src/file-storage.js +166 -39
- package/src/gltf-resolver.js +288 -0
- package/src/index.js +598 -1074
- package/src/panzoom-controller.js +494 -0
- package/src/pref-viewer-2d.js +459 -0
- package/src/pref-viewer-3d-data.js +178 -0
- package/src/pref-viewer-3d.js +635 -0
- package/src/pref-viewer-task.js +54 -0
- package/src/svg-resolver.js +281 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
|
|
2
|
+
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
|
|
3
|
+
import "@babylonjs/loaders";
|
|
4
|
+
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
|
|
5
|
+
import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
|
|
6
|
+
import GLTFResolver from "./gltf-resolver.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
|
|
13
|
+
* - Handles loading, replacing, and disposing of 3D models, environments, and materials.
|
|
14
|
+
* - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
|
|
15
|
+
* - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
|
|
16
|
+
* - Provides methods for downloading models and scenes in GLB and USDZ formats.
|
|
17
|
+
* - Manages camera and material options, container visibility, and user interactions.
|
|
18
|
+
* - Observes canvas resize events and updates the engine accordingly.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
|
|
22
|
+
* - Enable rendering: await controller.enable();
|
|
23
|
+
* - Load assets: await controller.load();
|
|
24
|
+
* - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
|
|
25
|
+
* - Control visibility: controller.setContainerVisibility(name, show);
|
|
26
|
+
* - Download assets: controller.downloadModelGLB(), controller.downloadModelUSDZ(), etc.
|
|
27
|
+
* - Disable rendering: controller.disable();
|
|
28
|
+
*
|
|
29
|
+
* Public Methods:
|
|
30
|
+
* - enable(): Initializes engine, scene, camera, lights, XR, and starts rendering.
|
|
31
|
+
* - disable(): Disposes engine and disconnects resize observer.
|
|
32
|
+
* - load(): Loads all asset containers and adds them to the scene.
|
|
33
|
+
* - setCameraOptions(): Applies camera options from configuration.
|
|
34
|
+
* - setMaterialOptions(): Applies material options from configuration.
|
|
35
|
+
* - setContainerVisibility(name, show): Shows or hides a container by name.
|
|
36
|
+
* - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ(): Downloads assets.
|
|
37
|
+
*
|
|
38
|
+
* Private Methods:
|
|
39
|
+
* - #configureDracoCompression(): Sets up Draco mesh compression.
|
|
40
|
+
* - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
|
|
41
|
+
* - #setupInteraction(): Sets up canvas interaction handlers.
|
|
42
|
+
* - #disposeEngine(): Disposes engine and resources.
|
|
43
|
+
* - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
|
|
44
|
+
* - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
|
|
45
|
+
* - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
|
|
46
|
+
* - #stopRender(), #startRender(): Render loop control.
|
|
47
|
+
* - #loadAssetContainer(), #loadContainers(): Asset loading.
|
|
48
|
+
* - #addStylesToARButton(): Styles AR button.
|
|
49
|
+
* - #createXRExperience(): Initializes WebXR AR experience.
|
|
50
|
+
*
|
|
51
|
+
* Notes:
|
|
52
|
+
* - Designed for integration with PrefViewer and GLTFResolver.
|
|
53
|
+
* - Supports advanced Babylon.js features for product visualization and configurators.
|
|
54
|
+
* - All resource management and rendering operations are performed asynchronously for performance.
|
|
55
|
+
*/
|
|
56
|
+
export default class BabylonJSController {
|
|
57
|
+
// Canvas HTML element
|
|
58
|
+
#canvas = null;
|
|
59
|
+
|
|
60
|
+
// References to parent custom elements
|
|
61
|
+
#prefViewer3D = undefined;
|
|
62
|
+
#prefViewer = undefined;
|
|
63
|
+
|
|
64
|
+
// Babylon.js core objects
|
|
65
|
+
#engine = null;
|
|
66
|
+
#scene = null;
|
|
67
|
+
#camera = null;
|
|
68
|
+
#hemiLight = null;
|
|
69
|
+
#dirLight = null;
|
|
70
|
+
#cameraLight = null;
|
|
71
|
+
#shadowGen = null;
|
|
72
|
+
#XRExperience = null;
|
|
73
|
+
#canvasResizeObserver = null;
|
|
74
|
+
|
|
75
|
+
#containers = {};
|
|
76
|
+
#options = {};
|
|
77
|
+
|
|
78
|
+
#gltfResolver = null; // GLTFResolver instance
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Constructs a new BabylonJSController instance.
|
|
82
|
+
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
83
|
+
* @public
|
|
84
|
+
* @param {HTMLCanvasElement|null} canvas - The canvas element to render the scene on.
|
|
85
|
+
* @param {object} containers - An object containing container states for model, environment, materials, etc.
|
|
86
|
+
* @param {object} options - Configuration options for the Babylon.js scene and assets.
|
|
87
|
+
* @returns {BabylonJSController}
|
|
88
|
+
* @description
|
|
89
|
+
* - Assigns the provided canvas to the controller.
|
|
90
|
+
* - Initializes each container with its state and sets assetContainer to null.
|
|
91
|
+
* - Stores the provided options for later use in scene setup and asset loading.
|
|
92
|
+
*/
|
|
93
|
+
constructor(canvas = null, containers = {}, options = {}) {
|
|
94
|
+
this.#canvas = canvas;
|
|
95
|
+
Object.keys(containers).forEach((key) => {
|
|
96
|
+
this.#containers[key] = {
|
|
97
|
+
assetContainer: null,
|
|
98
|
+
state: containers[key],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
this.#options = options;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Configures the Draco mesh compression decoder for Babylon.js.
|
|
106
|
+
* @private
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
#configureDracoCompression() {
|
|
110
|
+
// Point to whichever version you packaged or want to use:
|
|
111
|
+
const DRACO_BASE = "https://www.gstatic.com/draco/versioned/decoders/1.5.7";
|
|
112
|
+
DracoCompression.Configuration.decoder = {
|
|
113
|
+
// loader for the “wrapper” that pulls in the real WASM
|
|
114
|
+
wasmUrl: `${DRACO_BASE}/draco_wasm_wrapper_gltf.js`,
|
|
115
|
+
// the raw WebAssembly binary
|
|
116
|
+
wasmBinaryUrl: `${DRACO_BASE}/draco_decoder_gltf.wasm`,
|
|
117
|
+
// JS fallback if WASM isn’t available
|
|
118
|
+
fallbackUrl: `${DRACO_BASE}/draco_decoder_gltf.js`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render loop callback for Babylon.js.
|
|
124
|
+
* @private
|
|
125
|
+
* @returns {void}
|
|
126
|
+
* @description
|
|
127
|
+
* Continuously renders the current scene if it exists.
|
|
128
|
+
* Used by the engine's runRenderLoop method to update the view.
|
|
129
|
+
*/
|
|
130
|
+
#renderLoop() {
|
|
131
|
+
this.#scene && this.#scene.render();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Adds custom CSS styles to the Babylon.js AR button for consistent appearance and interaction.
|
|
136
|
+
* @private
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
#addStylesToARButton() {
|
|
140
|
+
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,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A); 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"}';
|
|
141
|
+
let style = this.#canvas.parentElement.parentElement.querySelector("style");
|
|
142
|
+
if (!style) {
|
|
143
|
+
style = document.createElement("style");
|
|
144
|
+
this.#canvas.parentElement.parentElement.appendChild(style);
|
|
145
|
+
}
|
|
146
|
+
style.appendChild(document.createTextNode(css));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates and initializes the Babylon.js WebXR experience for augmented reality (AR).
|
|
151
|
+
* @private
|
|
152
|
+
* @returns {Promise<boolean|void>} Resolves to true if XR experience is created, false if not supported, or void on error.
|
|
153
|
+
* @description
|
|
154
|
+
* Checks for AR session support, creates a hidden ground mesh, and configures session and UI options.
|
|
155
|
+
* Enables teleportation feature and sets the initial XR camera pose when the session starts.
|
|
156
|
+
* Adds custom styles to the AR button for consistent appearance.
|
|
157
|
+
* If AR is not supported or initialization fails, logs a warning and sets XRExperience to null.
|
|
158
|
+
*/
|
|
159
|
+
async #createXRExperience() {
|
|
160
|
+
if (this.#XRExperience) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sessionMode = "immersive-ar";
|
|
165
|
+
const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
|
|
166
|
+
if (!sessionSupported) {
|
|
167
|
+
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
|
|
173
|
+
ground.isVisible = false;
|
|
174
|
+
|
|
175
|
+
const options = {
|
|
176
|
+
floorMeshes: [ground],
|
|
177
|
+
uiOptions: {
|
|
178
|
+
sessionMode: sessionMode,
|
|
179
|
+
renderTarget: "xrLayer",
|
|
180
|
+
referenceSpaceType: "local",
|
|
181
|
+
},
|
|
182
|
+
optionalFeatures: true,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
|
|
186
|
+
|
|
187
|
+
const featuresManager = this.#XRExperience.baseExperience.featuresManager;
|
|
188
|
+
featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
|
|
189
|
+
xrInput: this.#XRExperience.input,
|
|
190
|
+
floorMeshes: [ground],
|
|
191
|
+
timeToTeleport: 1500,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
|
|
195
|
+
// Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
|
|
196
|
+
this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
|
|
197
|
+
this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
|
|
198
|
+
this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.#addStylesToARButton();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn("PrefViewer: failed to create WebXR experience", error);
|
|
204
|
+
this.#XRExperience = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates and configures the main ArcRotateCamera for the Babylon.js scene.
|
|
210
|
+
* @private
|
|
211
|
+
* @returns {void}
|
|
212
|
+
*/
|
|
213
|
+
#createCamera() {
|
|
214
|
+
this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
|
|
215
|
+
this.#camera.upperBetaLimit = Math.PI * 0.48;
|
|
216
|
+
this.#camera.lowerBetaLimit = Math.PI * 0.25;
|
|
217
|
+
this.#camera.lowerRadiusLimit = 5;
|
|
218
|
+
this.#camera.upperRadiusLimit = 20;
|
|
219
|
+
this.#camera.metadata = { locked: false };
|
|
220
|
+
this.#camera.attachControl(this.#canvas, true);
|
|
221
|
+
this.#scene.activeCamera = this.#camera;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Creates and configures the main lights for the Babylon.js scene.
|
|
226
|
+
* Initializes the environment texture if needed.
|
|
227
|
+
* Adds a hemispheric ambient light, a directional light for shadows, a shadow generator, and a camera-attached point light.
|
|
228
|
+
* Sets light intensities and shadow properties for realistic rendering.
|
|
229
|
+
* @private
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
#createLights() {
|
|
233
|
+
this.#initializeEnvironmentTexture();
|
|
234
|
+
|
|
235
|
+
if (this.#scene.environmentTexture) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 1) Stronger ambient fill
|
|
240
|
+
this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
241
|
+
this.#hemiLight.intensity = 0.6;
|
|
242
|
+
|
|
243
|
+
// 2) Directional light from the front-right, angled slightly down
|
|
244
|
+
this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
245
|
+
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
246
|
+
this.#dirLight.intensity = 0.6;
|
|
247
|
+
|
|
248
|
+
// // 3) Soft shadows
|
|
249
|
+
this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
|
|
250
|
+
this.#shadowGen.useBlurExponentialShadowMap = true;
|
|
251
|
+
this.#shadowGen.blurKernel = 16;
|
|
252
|
+
this.#shadowGen.darkness = 0.5;
|
|
253
|
+
|
|
254
|
+
// 4) Camera‐attached headlight
|
|
255
|
+
this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
|
|
256
|
+
this.#cameraLight.parent = this.#camera;
|
|
257
|
+
this.#cameraLight.intensity = 0.3;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Initializes the environment texture for the Babylon.js scene.
|
|
262
|
+
* Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
|
|
263
|
+
* Configures gamma space, mipmaps, and intensity level for realistic lighting.
|
|
264
|
+
* @private
|
|
265
|
+
* @returns {boolean}
|
|
266
|
+
*/
|
|
267
|
+
#initializeEnvironmentTexture() {
|
|
268
|
+
return false;
|
|
269
|
+
if (this.#scene.environmentTexture) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const hdrTextureURI = "../src/environments/noon_grass.hdr";
|
|
273
|
+
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
|
|
274
|
+
hdrTexture.gammaSpace = true;
|
|
275
|
+
hdrTexture._noMipmap = false;
|
|
276
|
+
hdrTexture.level = 2.0;
|
|
277
|
+
this.#scene.environmentTexture = hdrTexture;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Initializes the Image-Based Lighting (IBL) shadows for the Babylon.js scene.
|
|
282
|
+
* Creates an IBL shadow render pipeline and adds all relevant meshes and materials for shadow casting and receiving.
|
|
283
|
+
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
284
|
+
* Only applies if the scene has an environment texture set.
|
|
285
|
+
* @private
|
|
286
|
+
* @returns {void|false} Returns false if no environment texture is set; otherwise void.
|
|
287
|
+
*/
|
|
288
|
+
#initializeIBLShadows() {
|
|
289
|
+
if (!this.#scene.environmentTexture) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let createIBLShadowPipeline = function (scene) {
|
|
294
|
+
const pipeline = new IblShadowsRenderPipeline(
|
|
295
|
+
"iblShadowsPipeline",
|
|
296
|
+
scene,
|
|
297
|
+
{
|
|
298
|
+
resolutionExp: 7,
|
|
299
|
+
sampleDirections: 2,
|
|
300
|
+
ssShadowsEnabled: true,
|
|
301
|
+
shadowRemanence: 0.8,
|
|
302
|
+
triPlanarVoxelization: true,
|
|
303
|
+
shadowOpacity: 0.8,
|
|
304
|
+
},
|
|
305
|
+
[scene.activeCamera]
|
|
306
|
+
);
|
|
307
|
+
pipeline.allowDebugPasses = false;
|
|
308
|
+
pipeline.gbufferDebugEnabled = true;
|
|
309
|
+
pipeline.importanceSamplingDebugEnabled = false;
|
|
310
|
+
pipeline.voxelDebugEnabled = false;
|
|
311
|
+
pipeline.voxelDebugDisplayMip = 1;
|
|
312
|
+
pipeline.voxelDebugAxis = 2;
|
|
313
|
+
pipeline.voxelTracingDebugEnabled = false;
|
|
314
|
+
pipeline.spatialBlurPassDebugEnabled = false;
|
|
315
|
+
pipeline.accumulationPassDebugEnabled = false;
|
|
316
|
+
return pipeline;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
|
|
320
|
+
|
|
321
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
322
|
+
if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
326
|
+
iblShadowsPipeline.updateSceneBounds();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
this.#scene.materials.forEach((material) => {
|
|
330
|
+
iblShadowsPipeline.addShadowReceivingMaterial(material);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Initializes shadows for the Babylon.js scene.
|
|
336
|
+
* @private
|
|
337
|
+
* @returns {void|true} Returns true if IBL shadows are initialized, otherwise void.
|
|
338
|
+
* @description
|
|
339
|
+
* If no environment texture is set, initializes IBL shadows.
|
|
340
|
+
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
341
|
+
*/
|
|
342
|
+
#initializeShadows() {
|
|
343
|
+
if (!this.#scene.environmentTexture) {
|
|
344
|
+
this.#initializeIBLShadows();
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
349
|
+
if (mesh.id.startsWith("__root__")) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
mesh.receiveShadows = true;
|
|
353
|
+
if (!mesh.name === "hdri") {
|
|
354
|
+
this.#shadowGen.addShadowCaster(mesh, true);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Sets the maximum number of simultaneous lights for all materials in the scene.
|
|
361
|
+
* Counts enabled lights and updates the maxSimultaneousLights property for each material.
|
|
362
|
+
* @private
|
|
363
|
+
* @returns {void}
|
|
364
|
+
*/
|
|
365
|
+
#setMaxSimultaneousLights() {
|
|
366
|
+
let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
|
|
367
|
+
this.#scene.lights.forEach((light) => {
|
|
368
|
+
if (light.isEnabled()) {
|
|
369
|
+
++lightsNumber;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (this.#scene.materials) {
|
|
373
|
+
this.#scene.materials.forEach((material) => (material.maxSimultaneousLights = lightsNumber));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Sets up interaction handlers for the Babylon.js canvas.
|
|
379
|
+
* Adds a wheel event listener to control camera zoom based on mouse wheel input.
|
|
380
|
+
* Prevents zoom if the active camera is locked.
|
|
381
|
+
* @private
|
|
382
|
+
* @returns {void}
|
|
383
|
+
*/
|
|
384
|
+
#setupInteraction() {
|
|
385
|
+
this.#canvas.addEventListener("wheel", (event) => {
|
|
386
|
+
if (!this.#scene || !this.#camera) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
//const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
390
|
+
//this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
|
|
391
|
+
if (!this.#scene.activeCamera.metadata?.locked) {
|
|
392
|
+
this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
|
|
393
|
+
}
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Disposes the Babylon.js engine and releases all associated resources.
|
|
400
|
+
* @private
|
|
401
|
+
* @returns {void}
|
|
402
|
+
*/
|
|
403
|
+
#disposeEngine() {
|
|
404
|
+
if (!this.#engine) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.#engine.dispose();
|
|
408
|
+
this.#engine = this.#scene = this.#camera = null;
|
|
409
|
+
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
410
|
+
this.#shadowGen = null;
|
|
411
|
+
this.#XRExperience = null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Applies material options from the configuration to the relevant meshes.
|
|
416
|
+
* @private
|
|
417
|
+
* @param {object} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
|
|
418
|
+
* @returns {boolean} True if any mesh material was set, false otherwise.
|
|
419
|
+
*/
|
|
420
|
+
#setOptionsMaterial(optionMaterial) {
|
|
421
|
+
if (!optionMaterial || !(optionMaterial.value || (optionMaterial.isPending && optionMaterial.update.value)) || !(optionMaterial.nodePrefixes.length || optionMaterial.nodeNames.length)) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const materialContainer = this.#containers.materials;
|
|
426
|
+
const materialName = optionMaterial.isPending && optionMaterial.update.value ? optionMaterial.update.value : optionMaterial.value;
|
|
427
|
+
|
|
428
|
+
const material = materialContainer.assetContainer?.materials.find((mat) => mat.name === materialName) || null;
|
|
429
|
+
if (!material) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const assetContainersToProcess = [];
|
|
434
|
+
Object.values(this.#containers).forEach((container) => {
|
|
435
|
+
if (container.state.name === "materials") {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (container.assetContainer && (container.isPending || materialContainer.isPending || optionMaterial.isPending)) {
|
|
439
|
+
assetContainersToProcess.push(container.assetContainer);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
if (assetContainersToProcess.length === 0) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let someSetted = false;
|
|
447
|
+
assetContainersToProcess.forEach((assetContainer) =>
|
|
448
|
+
assetContainer.meshes
|
|
449
|
+
.filter((meshToFilter) => optionMaterial.nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || optionMaterial.nodeNames.includes(meshToFilter.name))
|
|
450
|
+
.forEach((mesh) => {
|
|
451
|
+
mesh.material = material;
|
|
452
|
+
someSetted = true;
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (someSetted) {
|
|
457
|
+
optionMaterial.setSuccess(true);
|
|
458
|
+
} else if (optionMaterial.isPending) {
|
|
459
|
+
optionMaterial.setSuccess(false);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return someSetted;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Applies all material options from the configuration to the relevant meshes.
|
|
467
|
+
* Iterates through each material option and applies it using #setOptionsMaterial.
|
|
468
|
+
* @private
|
|
469
|
+
* @returns {boolean} True if any material option was set, false otherwise.
|
|
470
|
+
*/
|
|
471
|
+
#setOptions_Materials() {
|
|
472
|
+
let someSetted = false;
|
|
473
|
+
Object.values(this.#options.materials).forEach((material) => {
|
|
474
|
+
let settedMaterial = this.#setOptionsMaterial(material);
|
|
475
|
+
someSetted = someSetted || settedMaterial;
|
|
476
|
+
});
|
|
477
|
+
return someSetted;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Applies camera options from the configuration to the active camera.
|
|
482
|
+
* @private
|
|
483
|
+
* @returns {boolean} True if camera options were set successfully, false otherwise.
|
|
484
|
+
*/
|
|
485
|
+
#setOptions_Camera() {
|
|
486
|
+
const cameraState = this.#options.camera;
|
|
487
|
+
const modelContainer = this.#containers.model;
|
|
488
|
+
const environmentContainer = this.#containers.environment;
|
|
489
|
+
|
|
490
|
+
if (!cameraState.isPending && !modelContainer.isPending && !environmentContainer.isPending) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const cameraName = cameraState.isPending ? cameraState.update.value : cameraState.value;
|
|
495
|
+
let camera = null;
|
|
496
|
+
if (cameraName !== null) {
|
|
497
|
+
camera = modelContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraName) || environmentContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraName) || null;
|
|
498
|
+
} else {
|
|
499
|
+
camera = this.#camera;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!camera) {
|
|
503
|
+
// If a new camera (different from the default) was tried and not found, search for the current one
|
|
504
|
+
if (cameraState.isPending && cameraState.update.value !== null && cameraState.value !== null && cameraState.update.value !== cameraState.value) {
|
|
505
|
+
camera = modelContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraState.value) || environmentContainer.assetContainer?.cameras.find((thisCamera) => thisCamera.name === cameraState.value) || null;
|
|
506
|
+
}
|
|
507
|
+
if (camera) {
|
|
508
|
+
// If the current camera (different from the default) was found, use it
|
|
509
|
+
camera.metadata = { locked: cameraState.locked };
|
|
510
|
+
cameraState.setSuccess(false);
|
|
511
|
+
} else {
|
|
512
|
+
// If no camera was found, use the default camera
|
|
513
|
+
camera = this.#camera;
|
|
514
|
+
cameraState.value = null;
|
|
515
|
+
cameraState.locked = this.#camera.metadata.locked;
|
|
516
|
+
cameraState.setSuccess(false);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
// If the requested camera was found, use it
|
|
520
|
+
if (cameraName !== null) {
|
|
521
|
+
camera.metadata = { locked: cameraState.isPending ? cameraState.update.locked : cameraState.locked };
|
|
522
|
+
} else {
|
|
523
|
+
// If it's the default camera, maintain its lock state
|
|
524
|
+
if (cameraState.isPending) {
|
|
525
|
+
cameraState.update.locked = this.#camera.metadata.locked;
|
|
526
|
+
} else {
|
|
527
|
+
cameraState.locked = this.#camera.metadata.locked;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (cameraState.isPending) {
|
|
531
|
+
cameraState.setSuccess(true);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!cameraState.locked && cameraState.value !== null) {
|
|
535
|
+
camera.attachControl(this.#canvas, true);
|
|
536
|
+
}
|
|
537
|
+
this.#scene.activeCamera = camera;
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Finds and returns the asset container object by its name.
|
|
543
|
+
* @private
|
|
544
|
+
* @param {string} name - The name of the container to find.
|
|
545
|
+
* @returns {object|null} The matching container object, or null if not found.
|
|
546
|
+
*/
|
|
547
|
+
#findContainerByName(name) {
|
|
548
|
+
return Object.values(this.#containers).find((container) => container.state.name === name) || null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Caches and retrieves the parent custom element "PREF-VIEWER-3D" for efficient access.
|
|
553
|
+
* @private
|
|
554
|
+
* @returns {void}
|
|
555
|
+
*/
|
|
556
|
+
#getPrefViewer3DComponent() {
|
|
557
|
+
if (this.#prefViewer3D === undefined) {
|
|
558
|
+
const grandParentElement = this.#canvas.parentElement.parentElement;
|
|
559
|
+
this.#prefViewer3D = grandParentElement && grandParentElement.nodeName === "PREF-VIEWER-3D" ? grandParentElement : null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Caches and retrieves the parent custom element "PREF-VIEWER" for efficient access.
|
|
565
|
+
* @private
|
|
566
|
+
* @returns {void}
|
|
567
|
+
*/
|
|
568
|
+
#getPrefViewerComponent() {
|
|
569
|
+
if (this.#prefViewer === undefined) {
|
|
570
|
+
const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
|
|
571
|
+
this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Updates the visibility attributes "show-model" or "show-scene" of parent custom elements ("PREF-VIEWER-3D" and "PREF-VIEWER") based on the container name and visibility state.
|
|
577
|
+
* @private
|
|
578
|
+
* @param {string} name - The name of the container ("model" or "environment").
|
|
579
|
+
* @param {boolean} isVisible - True to show the container, false to hide it.
|
|
580
|
+
* @returns {void}
|
|
581
|
+
*/
|
|
582
|
+
#updateVisibilityAttributeInComponentes(name, isVisible) {
|
|
583
|
+
// Cache references to parent custom elements
|
|
584
|
+
this.#getPrefViewer3DComponent();
|
|
585
|
+
this.#getPrefViewerComponent();
|
|
586
|
+
if (name === "model") {
|
|
587
|
+
this.#prefViewer3D?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
588
|
+
this.#prefViewer?.setAttribute("show-model", isVisible ? "true" : "false");
|
|
589
|
+
} else if (name === "environment") {
|
|
590
|
+
this.#prefViewer3D?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
591
|
+
this.#prefViewer?.setAttribute("show-scene", isVisible ? "true" : "false");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
|
|
597
|
+
* @private
|
|
598
|
+
* @param {object} container - The container object containing asset state and metadata.
|
|
599
|
+
* @returns {boolean} True if the container was added, false otherwise.
|
|
600
|
+
*/
|
|
601
|
+
#addContainer(container) {
|
|
602
|
+
if (!container.assetContainer || container.state.isVisible || !container.state.mustBeShown) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
this.#updateVisibilityAttributeInComponentes(container.state.name, true);
|
|
606
|
+
container.assetContainer.addAllToScene();
|
|
607
|
+
container.state.visible = true;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Removes the asset container from the Babylon.js scene if it is currently visible.
|
|
613
|
+
* @private
|
|
614
|
+
* @param {object} container - The container object containing asset state and metadata.
|
|
615
|
+
* @returns {boolean} True if the container was removed, false otherwise.
|
|
616
|
+
*/
|
|
617
|
+
#removeContainer(container) {
|
|
618
|
+
if (!container.assetContainer || !container.state.isVisible) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
this.#updateVisibilityAttributeInComponentes(container.state.name, false);
|
|
622
|
+
container.assetContainer.removeAllFromScene();
|
|
623
|
+
container.state.visible = false;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Replaces the asset container in the Babylon.js scene with a new one.
|
|
629
|
+
* @private
|
|
630
|
+
* @param {object} container - The container object containing asset state and metadata.
|
|
631
|
+
* @param {object} newAssetContainer - The new asset container to add to the scene.
|
|
632
|
+
* @returns {boolean} True if the container was replaced, false otherwise.
|
|
633
|
+
*/
|
|
634
|
+
#replaceContainer(container, newAssetContainer) {
|
|
635
|
+
if (container.assetContainer) {
|
|
636
|
+
this.#removeContainer(container);
|
|
637
|
+
container.assetContainer.dispose();
|
|
638
|
+
container.assetContainer = null;
|
|
639
|
+
}
|
|
640
|
+
this.#scene.getEngine().releaseEffects();
|
|
641
|
+
container.assetContainer = newAssetContainer;
|
|
642
|
+
this.#addContainer(container);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
|
|
647
|
+
* @private
|
|
648
|
+
* @param {boolean} [show] - Optional. True to show wall/floor meshes, false to hide. Defaults to environment visibility.
|
|
649
|
+
* @returns {void|false} Returns false if the model container is not available or not visible; otherwise void.
|
|
650
|
+
*/
|
|
651
|
+
#setVisibilityOfWallAndFloorInModel(show) {
|
|
652
|
+
if (!this.#containers.model.assetContainer || !this.#containers.model.state.isVisible) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
show = show !== undefined ? show : this.#containers.environment.state.isVisible;
|
|
656
|
+
const nodePrefixes = Object.values(this.#options.materials).flatMap((material) => material.nodePrefixes);
|
|
657
|
+
const nodeNames = Object.values(this.#options.materials).flatMap((material) => material.nodeNames);
|
|
658
|
+
this.#containers.model.assetContainer.meshes.filter((meshToFilter) => nodePrefixes.some((prefix) => meshToFilter.name.startsWith(prefix)) || nodeNames.includes(meshToFilter.name)).forEach((mesh) => mesh.setEnabled(show));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Stops the Babylon.js render loop for the current scene.
|
|
663
|
+
* @private
|
|
664
|
+
* @returns {void}
|
|
665
|
+
*/
|
|
666
|
+
#stopR;
|
|
667
|
+
#stopRender() {
|
|
668
|
+
this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Starts the Babylon.js render loop for the current scene.
|
|
672
|
+
* Waits until the scene is ready before beginning continuous rendering.
|
|
673
|
+
* @private
|
|
674
|
+
* @returns {Promise<void>}
|
|
675
|
+
*/
|
|
676
|
+
async #startRender() {
|
|
677
|
+
await this.#scene.whenReadyAsync();
|
|
678
|
+
this.#engine.runRenderLoop(this.#renderLoop.bind(this));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Loads an asset container (model, environment, materials, etc.) using the provided container state.
|
|
683
|
+
* @private
|
|
684
|
+
* @param {object} container - The container object containing asset state and metadata.
|
|
685
|
+
* @returns {Promise<[object, AssetContainer|boolean]>} Resolves to an array with the container and the loaded asset container, or false if loading fails.
|
|
686
|
+
* @description
|
|
687
|
+
* Resolves the asset source using GLTFResolver, prepares plugin options, and loads the asset into the Babylon.js scene.
|
|
688
|
+
* Updates the container's cache data and returns the container along with the loaded asset container or false if loading fails.
|
|
689
|
+
*/
|
|
690
|
+
async #loadAssetContainer(container) {
|
|
691
|
+
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
692
|
+
return [container, false];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (container.state.isPending === false) {
|
|
696
|
+
return [container, false];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!this.#gltfResolver) {
|
|
700
|
+
this.#gltfResolver = new GLTFResolver();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, container.state.size, container.state.timeStamp);
|
|
704
|
+
if (!sourceData) {
|
|
705
|
+
return [container, false];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp);
|
|
709
|
+
|
|
710
|
+
// https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
|
|
711
|
+
let options = {
|
|
712
|
+
pluginExtension: sourceData.extension,
|
|
713
|
+
pluginOptions: {
|
|
714
|
+
gltf: {
|
|
715
|
+
compileMaterials: true,
|
|
716
|
+
loadAllMaterials: true,
|
|
717
|
+
loadOnlyMaterials: container.state.name === "materials",
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
return [container, await LoadAssetContainerAsync(sourceData.source, this.#scene, options)];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
727
|
+
* @private
|
|
728
|
+
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
729
|
+
* @description
|
|
730
|
+
* Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
|
|
731
|
+
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
732
|
+
* Returns an object with success status and error details.
|
|
733
|
+
*/
|
|
734
|
+
async #loadContainers() {
|
|
735
|
+
this.#stopRender();
|
|
736
|
+
|
|
737
|
+
const promiseArray = [];
|
|
738
|
+
Object.values(this.#containers).forEach((container) => {
|
|
739
|
+
promiseArray.push(this.#loadAssetContainer(container));
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
let detail = {
|
|
743
|
+
success: false,
|
|
744
|
+
error: null,
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
await Promise.allSettled(promiseArray)
|
|
748
|
+
.then((values) => {
|
|
749
|
+
values.forEach((result) => {
|
|
750
|
+
const container = result.value ? result.value[0] : null;
|
|
751
|
+
const assetContainer = result.value ? result.value[1] : null;
|
|
752
|
+
if (result.status === "fulfilled" && assetContainer) {
|
|
753
|
+
if (container.state.name === "model") {
|
|
754
|
+
assetContainer.lights = [];
|
|
755
|
+
}
|
|
756
|
+
this.#replaceContainer(container, assetContainer);
|
|
757
|
+
container.state.setSuccess(true);
|
|
758
|
+
} else {
|
|
759
|
+
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
760
|
+
container.state.mustBeShown ? this.#addContainer(container) : this.#removeContainer(container);
|
|
761
|
+
}
|
|
762
|
+
container.state.setSuccess(false);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
this.#setOptions_Materials();
|
|
767
|
+
this.#setOptions_Camera();
|
|
768
|
+
this.#setVisibilityOfWallAndFloorInModel();
|
|
769
|
+
detail.success = true;
|
|
770
|
+
})
|
|
771
|
+
.catch((error) => {
|
|
772
|
+
this.loaded = true;
|
|
773
|
+
console.error("PrefViewer: failed to load model", error);
|
|
774
|
+
detail.success = false;
|
|
775
|
+
detail.error = error;
|
|
776
|
+
})
|
|
777
|
+
.finally(async () => {
|
|
778
|
+
this.#setMaxSimultaneousLights();
|
|
779
|
+
this.#initializeShadows();
|
|
780
|
+
this.#startRender();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
return detail;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* ---------------------------
|
|
788
|
+
* Public methods
|
|
789
|
+
* ---------------------------
|
|
790
|
+
*/
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Initializes the Babylon.js engine, scene, camera, lights, and interaction handlers.
|
|
794
|
+
* Configures Draco compression, sets up XR experience, and starts the render loop.
|
|
795
|
+
* Observes canvas resize events to update the engine.
|
|
796
|
+
* @public
|
|
797
|
+
* @returns {Promise<void>}
|
|
798
|
+
*/
|
|
799
|
+
async enable() {
|
|
800
|
+
this.#configureDracoCompression();
|
|
801
|
+
this.#engine = new Engine(this.#canvas, true, { alpha: true });
|
|
802
|
+
this.#engine.disableUniformBuffers = true;
|
|
803
|
+
this.#scene = new Scene(this.#engine);
|
|
804
|
+
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
805
|
+
this.#createCamera();
|
|
806
|
+
this.#createLights();
|
|
807
|
+
this.#setupInteraction();
|
|
808
|
+
await this.#createXRExperience();
|
|
809
|
+
this.#startRender();
|
|
810
|
+
this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
811
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
816
|
+
* Cleans up all scene, camera, light, and XR resources.
|
|
817
|
+
* @public
|
|
818
|
+
* @returns {void}
|
|
819
|
+
*/
|
|
820
|
+
disable() {
|
|
821
|
+
this.#disposeEngine();
|
|
822
|
+
this.#canvasResizeObserver.disconnect();
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
827
|
+
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
828
|
+
* @public
|
|
829
|
+
* @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
|
|
830
|
+
*/
|
|
831
|
+
async load() {
|
|
832
|
+
return await this.#loadContainers();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Applies camera options from the configuration to the active camera.
|
|
837
|
+
* Stops and restarts the render loop to apply changes.
|
|
838
|
+
* @public
|
|
839
|
+
* @returns {boolean} True if camera options were set successfully, false otherwise.
|
|
840
|
+
*/
|
|
841
|
+
setCameraOptions() {
|
|
842
|
+
this.#stopRender();
|
|
843
|
+
const cameraOptionsSetted = this.#setOptions_Camera();
|
|
844
|
+
this.#startRender();
|
|
845
|
+
return cameraOptionsSetted;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Applies material options from the configuration to the relevant meshes.
|
|
850
|
+
* Stops and restarts the render loop to apply changes.
|
|
851
|
+
* @public
|
|
852
|
+
* @returns {boolean} True if material options were set successfully, false otherwise.
|
|
853
|
+
*/
|
|
854
|
+
setMaterialOptions() {
|
|
855
|
+
this.#stopRender();
|
|
856
|
+
const materialsOptionsSetted = this.#setOptions_Materials();
|
|
857
|
+
this.#startRender();
|
|
858
|
+
return materialsOptionsSetted;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
863
|
+
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
864
|
+
* Restarts the render loop to apply changes.
|
|
865
|
+
* @public
|
|
866
|
+
* @param {string} name - The name of the container to show or hide.
|
|
867
|
+
* @param {boolean} show - True to show the container, false to hide it.
|
|
868
|
+
* @returns {void}
|
|
869
|
+
*/
|
|
870
|
+
setContainerVisibility(name, show) {
|
|
871
|
+
const container = this.#findContainerByName(name);
|
|
872
|
+
if (!container) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (container.state.show === show && container.state.visible === show) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
container.state.show = show;
|
|
879
|
+
this.#stopRender();
|
|
880
|
+
show ? this.#addContainer(container) : this.#removeContainer(container);
|
|
881
|
+
this.#setVisibilityOfWallAndFloorInModel();
|
|
882
|
+
this.#startRender();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Initiates download of the current model as a GLB file.
|
|
887
|
+
* @public
|
|
888
|
+
* @returns {void}
|
|
889
|
+
*/
|
|
890
|
+
downloadModelGLB() {
|
|
891
|
+
const fileName = "model";
|
|
892
|
+
GLTF2Export.GLBAsync(this.#containers.model.assetContainer, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Initiates download of the current model as a USDZ file.
|
|
897
|
+
* @public
|
|
898
|
+
* @returns {void}
|
|
899
|
+
*/
|
|
900
|
+
downloadModelUSDZ() {
|
|
901
|
+
const fileName = "model";
|
|
902
|
+
USDZExportAsync(this.#containers.model.assetContainer).then((response) => {
|
|
903
|
+
if (response) {
|
|
904
|
+
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Initiates download of the entire scene (model and environment) as a USDZ file.
|
|
911
|
+
* @public
|
|
912
|
+
* @returns {void}
|
|
913
|
+
*/
|
|
914
|
+
downloadModelAndSceneUSDZ() {
|
|
915
|
+
const fileName = "scene";
|
|
916
|
+
USDZExportAsync(this.#scene).then((response) => {
|
|
917
|
+
if (response) {
|
|
918
|
+
Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Initiates download of the entire scene (model and environment) as a GLB file.
|
|
925
|
+
* @public
|
|
926
|
+
* @returns {void}
|
|
927
|
+
*/
|
|
928
|
+
downloadModelAndSceneGLB() {
|
|
929
|
+
const fileName = "scene";
|
|
930
|
+
GLTF2Export.GLBAsync(this.#scene, fileName, { exportWithoutWaitingForScene: true }).then((glb) => glb.downloadFiles());
|
|
931
|
+
}
|
|
932
|
+
}
|