@preference-sl/pref-viewer 2.11.0-beta.0 → 2.11.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/babylonjs-controller.js +929 -0
- package/src/gltf-resolver.js +288 -0
- package/src/index.js +488 -1060
- package/src/panzoom-controller.js +494 -0
- package/src/pref-viewer-2d.js +441 -0
- package/src/pref-viewer-3d-data.js +178 -0
- package/src/pref-viewer-3d.js +457 -0
- package/src/pref-viewer-task.js +54 -0
- package/src/svg-resolver.js +281 -0
package/src/index.js
CHANGED
|
@@ -1,225 +1,144 @@
|
|
|
1
|
+
import { PrefViewer2D } from "./pref-viewer-2d.js";
|
|
2
|
+
import { PrefViewer3D } from "./pref-viewer-3d.js";
|
|
3
|
+
import {PrefViewerTask} from "./pref-viewer-task.js";
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
6
|
+
* PrefViewer - Custom Web Component for rendering and managing 2D and 3D product visualizations.
|
|
7
|
+
*
|
|
8
|
+
* Overview:
|
|
9
|
+
* - Encapsulates both 2D and 3D viewers using Babylon.js, supporting glTF/GLB models and environments.
|
|
10
|
+
* - Handles loading from remote URLs, Base64 data URIs, and IndexedDB sources.
|
|
11
|
+
* - Provides a unified API for loading models, scenes, drawings, materials, and configuration via attributes or methods.
|
|
12
|
+
* - Manages an internal task queue for sequential processing of viewer operations.
|
|
13
|
+
* - Emits custom events for loading, errors, and state changes to facilitate integration.
|
|
5
14
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* • Supports loading via remote URL (storage.url), Base64 data URI (storage.url), or from a Base64 stored entry in IndexedDB (storage.db, storage.table, storage.id).
|
|
11
|
-
* • The data for loading both models is provided via the 'config' (both model and scene), 'model' and 'scene' attributes. The 'config' attribute can carry more initial configurations than just the models.
|
|
12
|
-
* • Exposes methods for making changes to the scene that replicate what the attribute observables do: 'loadConfig', 'loadModel', 'loadScene'.
|
|
13
|
-
* • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
|
|
14
|
-
* • Emits 'model-loaded' and 'model-error' events for integration.
|
|
15
|
+
* Usage:
|
|
16
|
+
* - Use as a custom HTML element: <pref-viewer ...>
|
|
17
|
+
* - Configure via attributes (e.g., config, model, scene, materials, drawing, options).
|
|
18
|
+
* - Control visibility, downloads, and viewer mode via public methods.
|
|
15
19
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* style="width:800px; height:600px;">
|
|
23
|
-
* </pref-viewer>
|
|
24
|
-
* ```
|
|
20
|
+
* Public Methods:
|
|
21
|
+
* - loadConfig(config), loadModel(model), loadScene(scene), loadMaterials(materials), loadDrawing(drawing)
|
|
22
|
+
* - setOptions(options)
|
|
23
|
+
* - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
|
|
24
|
+
* - showModel(), hideModel(), showScene(), hideScene()
|
|
25
|
+
* - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ()
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* style="width:800px; height:600px;">
|
|
31
|
-
* </pref-viewer>
|
|
32
|
-
* ```
|
|
27
|
+
* Public Properties:
|
|
28
|
+
* - initialized: Indicates if the viewer is initialized.
|
|
29
|
+
* - loaded: Indicates if the viewer has finished loading.
|
|
30
|
+
* - loading: Indicates if the viewer is currently loading.
|
|
33
31
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
32
|
+
* Events:
|
|
33
|
+
* - "scene-loading": Dispatched when a loading operation starts.
|
|
34
|
+
* - "scene-loaded": Dispatched when a loading operation completes.
|
|
35
|
+
* - "scene-error": Dispatched when initialization fails.
|
|
36
|
+
*
|
|
37
|
+
* Notes:
|
|
38
|
+
* - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
|
|
39
|
+
* - Processes tasks sequentially to ensure consistent state.
|
|
40
|
+
* - Designed for extensibility and integration in product configurators and visualization tools.
|
|
41
41
|
*/
|
|
42
|
-
import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
|
|
43
|
-
import "@babylonjs/loaders";
|
|
44
|
-
import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
|
|
45
|
-
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
|
|
46
|
-
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
|
|
47
|
-
import { initDb, loadModel } from "./gltf-storage.js";
|
|
48
|
-
import { FileStorage } from "./file-storage.js";
|
|
49
|
-
|
|
50
|
-
class PrefViewerTask {
|
|
51
|
-
static Types = Object.freeze({
|
|
52
|
-
Config: "config",
|
|
53
|
-
Environment: "environment",
|
|
54
|
-
Materials: "materials",
|
|
55
|
-
Model: "model",
|
|
56
|
-
Options: "options",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* value: any payload for the task
|
|
61
|
-
* type: must match one of PrefViewerTask.Types values (case-insensitive)
|
|
62
|
-
*/
|
|
63
|
-
constructor(value, type) {
|
|
64
|
-
this.value = value;
|
|
65
|
-
|
|
66
|
-
const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
|
|
67
|
-
const allowed = Object.values(PrefViewerTask.Types);
|
|
68
|
-
if (!allowed.includes(t)) {
|
|
69
|
-
throw new TypeError(`PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`);
|
|
70
|
-
}
|
|
71
|
-
this.type = t;
|
|
72
|
-
|
|
73
|
-
Object.freeze(this);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
42
|
class PrefViewer extends HTMLElement {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
#fileStorage = new FileStorage("PrefViewer", "Files");
|
|
83
|
-
|
|
84
|
-
#data = {
|
|
85
|
-
containers: {
|
|
86
|
-
model: {
|
|
87
|
-
name: "model",
|
|
88
|
-
assetContainer: null,
|
|
89
|
-
show: true,
|
|
90
|
-
storage: null,
|
|
91
|
-
visible: false,
|
|
92
|
-
size: null,
|
|
93
|
-
timeStamp: null,
|
|
94
|
-
changed: { pending: false, success: false },
|
|
95
|
-
},
|
|
96
|
-
environment: {
|
|
97
|
-
name: "environment",
|
|
98
|
-
assetContainer: null,
|
|
99
|
-
show: true,
|
|
100
|
-
storage: null,
|
|
101
|
-
visible: false,
|
|
102
|
-
size: null,
|
|
103
|
-
timeStamp: null,
|
|
104
|
-
changed: { pending: false, success: false },
|
|
105
|
-
},
|
|
106
|
-
materials: {
|
|
107
|
-
name: "materials",
|
|
108
|
-
assetContainer: null,
|
|
109
|
-
storage: null,
|
|
110
|
-
show: true,
|
|
111
|
-
visible: false,
|
|
112
|
-
size: null,
|
|
113
|
-
timeStamp: null,
|
|
114
|
-
changed: { pending: false, success: false },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
options: {
|
|
118
|
-
camera: {
|
|
119
|
-
value: null,
|
|
120
|
-
locked: true,
|
|
121
|
-
changed: { pending: false, success: false },
|
|
122
|
-
},
|
|
123
|
-
materials: {
|
|
124
|
-
innerWall: {
|
|
125
|
-
value: null,
|
|
126
|
-
prefix: "innerWall",
|
|
127
|
-
changed: { pending: false, success: false },
|
|
128
|
-
},
|
|
129
|
-
outerWall: {
|
|
130
|
-
value: null,
|
|
131
|
-
prefix: "outerWall",
|
|
132
|
-
changed: { pending: false, success: false },
|
|
133
|
-
},
|
|
134
|
-
innerFloor: {
|
|
135
|
-
value: null,
|
|
136
|
-
prefix: "innerFloor",
|
|
137
|
-
changed: { pending: false, success: false },
|
|
138
|
-
},
|
|
139
|
-
outerFloor: {
|
|
140
|
-
value: null,
|
|
141
|
-
prefix: "outerFloor",
|
|
142
|
-
changed: { pending: false, success: false },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
};
|
|
43
|
+
#isInitialized = false;
|
|
44
|
+
#isLoaded = false;
|
|
45
|
+
#isLoading = false;
|
|
46
|
+
#mode = "3d";
|
|
147
47
|
|
|
148
|
-
|
|
149
|
-
#wrapper = null;
|
|
150
|
-
#canvas = null;
|
|
48
|
+
#taskQueue = [];
|
|
151
49
|
|
|
152
|
-
|
|
153
|
-
#
|
|
154
|
-
#scene = null;
|
|
155
|
-
#camera = null;
|
|
156
|
-
#hemiLight = null;
|
|
157
|
-
#dirLight = null;
|
|
158
|
-
#cameraLight = null;
|
|
159
|
-
#shadowGen = null;
|
|
160
|
-
#XRExperience = null;
|
|
50
|
+
#component2D = null;
|
|
51
|
+
#component3D = null;
|
|
161
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new PrefViewer instance and attaches a shadow DOM.
|
|
55
|
+
* Initializes internal state and component references.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
162
58
|
constructor() {
|
|
163
59
|
super();
|
|
164
60
|
this.attachShadow({ mode: "open" });
|
|
165
|
-
this.#createCanvas();
|
|
166
|
-
this.#wrapCanvas();
|
|
167
|
-
// Point to whichever version you packaged or want to use:
|
|
168
|
-
const DRACO_BASE = "https://www.gstatic.com/draco/versioned/decoders/1.5.7";
|
|
169
|
-
DracoCompression.Configuration.decoder = {
|
|
170
|
-
// loader for the “wrapper” that pulls in the real WASM
|
|
171
|
-
wasmUrl: `${DRACO_BASE}/draco_wasm_wrapper_gltf.js`,
|
|
172
|
-
// the raw WebAssembly binary
|
|
173
|
-
wasmBinaryUrl: `${DRACO_BASE}/draco_decoder_gltf.wasm`,
|
|
174
|
-
// JS fallback if WASM isn’t available
|
|
175
|
-
fallbackUrl: `${DRACO_BASE}/draco_decoder_gltf.js`,
|
|
176
|
-
};
|
|
177
61
|
}
|
|
178
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the list of attributes to observe for changes.
|
|
65
|
+
* @public
|
|
66
|
+
* @returns {string[]} Array of attribute names to observe.
|
|
67
|
+
*/
|
|
179
68
|
static get observedAttributes() {
|
|
180
|
-
return ["config", "model", "scene", "show-model", "show-scene"];
|
|
69
|
+
return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
|
|
181
70
|
}
|
|
182
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Observes changes to specific attributes and triggers corresponding actions.
|
|
74
|
+
* Loads configuration, drawing, model, scene, materials, or options when their attributes change.
|
|
75
|
+
* Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
|
|
76
|
+
* @public
|
|
77
|
+
* @param {string} name - The name of the changed attribute.
|
|
78
|
+
* @param {*} _old - The previous value of the attribute (unused).
|
|
79
|
+
* @param {*} value - The new value of the attribute.
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
183
82
|
attributeChangedCallback(name, _old, value) {
|
|
184
|
-
let data = null;
|
|
185
83
|
switch (name) {
|
|
186
84
|
case "config":
|
|
187
85
|
this.loadConfig(value);
|
|
188
86
|
break;
|
|
87
|
+
case "drawing":
|
|
88
|
+
this.loadDrawing(value);
|
|
89
|
+
break;
|
|
90
|
+
case "materials":
|
|
91
|
+
this.loadMaterials(value);
|
|
92
|
+
break;
|
|
93
|
+
case "mode":
|
|
94
|
+
if (_old === value || value.toLowerCase() === this.#mode) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.setMode(value.toLowerCase());
|
|
98
|
+
break;
|
|
189
99
|
case "model":
|
|
190
100
|
this.loadModel(value);
|
|
191
101
|
break;
|
|
192
102
|
case "scene":
|
|
193
103
|
this.loadScene(value);
|
|
194
104
|
break;
|
|
195
|
-
case "materials":
|
|
196
|
-
this.loadMaterials(value);
|
|
197
|
-
break;
|
|
198
105
|
case "options":
|
|
199
106
|
this.setOptions(value);
|
|
200
107
|
break;
|
|
201
108
|
case "show-model":
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
data ? this.showModel() : this.hideModel();
|
|
205
|
-
} else {
|
|
206
|
-
this.#data.containers.model.show = data;
|
|
109
|
+
if (_old === value) {
|
|
110
|
+
return;
|
|
207
111
|
}
|
|
112
|
+
const showModel = value.toLowerCase() === "true";
|
|
113
|
+
showModel ? this.showModel() : this.hideModel();
|
|
208
114
|
break;
|
|
209
115
|
case "show-scene":
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
data ? this.showScene() : this.hideScene();
|
|
213
|
-
} else {
|
|
214
|
-
this.#data.containers.environment.show = data;
|
|
116
|
+
if (_old === value) {
|
|
117
|
+
return;
|
|
215
118
|
}
|
|
119
|
+
const showScene = value.toLowerCase() === "true";
|
|
120
|
+
showScene ? this.showScene() : this.hideScene();
|
|
216
121
|
break;
|
|
217
122
|
}
|
|
218
123
|
}
|
|
219
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Called when the element is inserted into the DOM.
|
|
127
|
+
* Initializes the 3D and 2D viewer components and starts processing tasks.
|
|
128
|
+
* If the "config" attribute is missing, dispatches a "scene-error" event and stops initialization.
|
|
129
|
+
* @public
|
|
130
|
+
* @returns {void|boolean} Returns false if initialization fails; otherwise void.
|
|
131
|
+
*/
|
|
220
132
|
connectedCallback() {
|
|
133
|
+
this.#createComponent3D();
|
|
134
|
+
this.#createComponent2D();
|
|
135
|
+
|
|
136
|
+
if (!this.hasAttribute("mode")) {
|
|
137
|
+
this.setMode();
|
|
138
|
+
}
|
|
139
|
+
|
|
221
140
|
if (!this.hasAttribute("config")) {
|
|
222
|
-
const error = 'PrefViewer: provide "
|
|
141
|
+
const error = 'PrefViewer: provide "config" as a configuration object to initialize the viewer.';
|
|
223
142
|
console.error(error);
|
|
224
143
|
this.dispatchEvent(
|
|
225
144
|
new CustomEvent("scene-error", {
|
|
@@ -232,45 +151,98 @@ class PrefViewer extends HTMLElement {
|
|
|
232
151
|
return false;
|
|
233
152
|
}
|
|
234
153
|
|
|
235
|
-
this.#
|
|
236
|
-
this.initialized = true;
|
|
154
|
+
this.#isInitialized = true;
|
|
237
155
|
this.#processNextTask();
|
|
238
156
|
}
|
|
239
157
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Creates and appends the 2D viewer component to the shadow DOM.
|
|
160
|
+
* Sets the "visible" attribute to true by default.
|
|
161
|
+
* @private
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
#createComponent2D() {
|
|
165
|
+
this.#component2D = document.createElement("pref-viewer-2d");
|
|
166
|
+
this.#component2D.setAttribute("visible", "false");
|
|
167
|
+
this.shadowRoot.appendChild(this.#component2D);
|
|
243
168
|
}
|
|
244
169
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Creates and appends the 3D viewer component to the shadow DOM.
|
|
172
|
+
* Sets the "visible" attribute to true by default.
|
|
173
|
+
* @private
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
#createComponent3D() {
|
|
177
|
+
this.#component3D = document.createElement("pref-viewer-3d");
|
|
178
|
+
this.#component3D.setAttribute("visible", "false");
|
|
179
|
+
this.shadowRoot.appendChild(this.#component3D);
|
|
254
180
|
}
|
|
255
181
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Adds a new task to the internal queue for processing.
|
|
184
|
+
* If the viewer is initialized and not currently loading, immediately processes the next task.
|
|
185
|
+
* @private
|
|
186
|
+
* @param {*} value - The payload or data for the task.
|
|
187
|
+
* @param {string} type - The type of task (see PrefViewerTask.Types).
|
|
188
|
+
* @returns {void}
|
|
189
|
+
*/
|
|
190
|
+
#addTaskToQueue(value, type) {
|
|
191
|
+
this.#taskQueue.push(new PrefViewerTask(value, type));
|
|
192
|
+
if (this.#isInitialized && !this.#isLoading) {
|
|
193
|
+
this.#processNextTask();
|
|
194
|
+
}
|
|
265
195
|
}
|
|
266
196
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Processes the next task in the queue, if any.
|
|
199
|
+
* Dispatches the task to the appropriate handler based on its type.
|
|
200
|
+
* @private
|
|
201
|
+
* @returns {boolean|void} Returns false if the queue is empty; otherwise void.
|
|
202
|
+
*/
|
|
203
|
+
#processNextTask() {
|
|
204
|
+
if (!this.#taskQueue.length) {
|
|
205
|
+
return false;
|
|
272
206
|
}
|
|
273
|
-
this
|
|
207
|
+
const task = this.#taskQueue[0];
|
|
208
|
+
this.#taskQueue.shift();
|
|
209
|
+
switch (task.type) {
|
|
210
|
+
case PrefViewerTask.Types.Config:
|
|
211
|
+
this.#processConfig(task.value);
|
|
212
|
+
break;
|
|
213
|
+
case PrefViewerTask.Types.Drawing:
|
|
214
|
+
this.#processDrawing(task.value);
|
|
215
|
+
break;
|
|
216
|
+
case PrefViewerTask.Types.Environment:
|
|
217
|
+
this.#processEnvironment(task.value);
|
|
218
|
+
break;
|
|
219
|
+
case PrefViewerTask.Types.Materials:
|
|
220
|
+
this.#processMaterials(task.value);
|
|
221
|
+
break;
|
|
222
|
+
case PrefViewerTask.Types.Model:
|
|
223
|
+
this.#processModel(task.value);
|
|
224
|
+
break;
|
|
225
|
+
case PrefViewerTask.Types.Options:
|
|
226
|
+
this.#processOptions(task.value);
|
|
227
|
+
break;
|
|
228
|
+
case PrefViewerTask.Types.Visibility:
|
|
229
|
+
this.#processVisibility(task.value);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handles the start of a 3D loading operation.
|
|
236
|
+
* Updates loading state, sets attributes, and dispatches a "scene-loading" event.
|
|
237
|
+
* @private
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
#on3DLoading() {
|
|
241
|
+
this.#isLoaded = false;
|
|
242
|
+
this.#isLoading = true;
|
|
243
|
+
|
|
244
|
+
this.removeAttribute("loaded-3d");
|
|
245
|
+
this.setAttribute("loading-3d", "");
|
|
274
246
|
this.dispatchEvent(
|
|
275
247
|
new CustomEvent("scene-loading", {
|
|
276
248
|
bubbles: true,
|
|
@@ -278,35 +250,16 @@ class PrefViewer extends HTMLElement {
|
|
|
278
250
|
composed: true,
|
|
279
251
|
})
|
|
280
252
|
);
|
|
281
|
-
this.#engine.stopRenderLoop(this.#renderLoop);
|
|
282
253
|
}
|
|
283
254
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.pending,
|
|
293
|
-
options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.pending,
|
|
294
|
-
};
|
|
295
|
-
const loadedDetail = {
|
|
296
|
-
container_model: !!this.#data.containers.model.changed.success,
|
|
297
|
-
container_environment: !!this.#data.containers.environment.changed.success,
|
|
298
|
-
container_materials: !!this.#data.containers.materials.changed.success,
|
|
299
|
-
options_camera: !!this.#data.options.camera.changed.success,
|
|
300
|
-
options_innerWallMaterial: !!this.#data.options.materials.innerWall.changed.success,
|
|
301
|
-
options_outerWallMaterial: !!this.#data.options.materials.outerWall.changed.success,
|
|
302
|
-
options_innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed.success,
|
|
303
|
-
options_outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed.success,
|
|
304
|
-
};
|
|
305
|
-
const detail = {
|
|
306
|
-
tried: toLoadDetail,
|
|
307
|
-
success: loadedDetail,
|
|
308
|
-
};
|
|
309
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Handles the completion of a 3D loading operation.
|
|
257
|
+
* Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
|
|
258
|
+
* @private
|
|
259
|
+
* @param {object} [detail={}] - Optional details to include in the event.
|
|
260
|
+
* @returns {void}
|
|
261
|
+
*/
|
|
262
|
+
#on3DLoaded(detail = {}) {
|
|
310
263
|
this.dispatchEvent(
|
|
311
264
|
new CustomEvent("scene-loaded", {
|
|
312
265
|
bubbles: true,
|
|
@@ -316,857 +269,224 @@ class PrefViewer extends HTMLElement {
|
|
|
316
269
|
})
|
|
317
270
|
);
|
|
318
271
|
|
|
319
|
-
|
|
320
|
-
this
|
|
321
|
-
|
|
322
|
-
this.#resetChangedFlags();
|
|
323
|
-
|
|
324
|
-
if (this.hasAttribute("loading")) {
|
|
325
|
-
this.removeAttribute("loading");
|
|
326
|
-
}
|
|
327
|
-
this.setAttribute("loaded", "");
|
|
328
|
-
|
|
329
|
-
this.loaded = true;
|
|
330
|
-
this.loading = false;
|
|
331
|
-
|
|
332
|
-
this.#processNextTask();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Data
|
|
336
|
-
#checkCameraChanged(options) {
|
|
337
|
-
if (!options || !options.camera) {
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
const prev = this.#data.options.camera.value;
|
|
341
|
-
const changed = options.camera !== prev;
|
|
342
|
-
|
|
343
|
-
this.#data.options.camera.changed.pending = changed;
|
|
344
|
-
this.#data.options.camera.changed.success = false;
|
|
345
|
-
if (changed) {
|
|
346
|
-
this.#data.options.camera.changed.value = prev;
|
|
347
|
-
this.#data.options.camera.changed.locked = this.#data.options.camera.locked;
|
|
348
|
-
this.#data.options.camera.value = options.camera;
|
|
349
|
-
}
|
|
350
|
-
return changed;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
#checkMaterialsChanged(options) {
|
|
354
|
-
if (!options) {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
let someChanged = false;
|
|
358
|
-
Object.keys(this.#data.options.materials).forEach((material) => {
|
|
359
|
-
const key = `${material}Material`;
|
|
360
|
-
const state = this.#data.options.materials[material];
|
|
361
|
-
const prev = state.value;
|
|
362
|
-
const incoming = options[key];
|
|
363
|
-
const changed = !!incoming && incoming !== prev;
|
|
364
|
-
|
|
365
|
-
state.changed.pending = changed;
|
|
366
|
-
state.changed.success = false;
|
|
367
|
-
if (changed) {
|
|
368
|
-
state.changed.value = prev;
|
|
369
|
-
state.value = incoming;
|
|
370
|
-
}
|
|
371
|
-
someChanged = someChanged || changed;
|
|
372
|
-
});
|
|
373
|
-
return someChanged;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
#storeChangedFlagsForContainer(container, success) {
|
|
377
|
-
if (success) {
|
|
378
|
-
container.timeStamp = container.changed.timeStamp;
|
|
379
|
-
container.size = container.changed.size;
|
|
380
|
-
container.changed.success = true;
|
|
381
|
-
} else {
|
|
382
|
-
container.changed.success = false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
#resetChangedFlags() {
|
|
387
|
-
const reset = (node) => {
|
|
388
|
-
node.changed = { pending: false, success: false };
|
|
389
|
-
};
|
|
390
|
-
Object.values(this.#data.containers).forEach(reset);
|
|
391
|
-
Object.values(this.#data.options.materials).forEach(reset);
|
|
392
|
-
reset(this.#data.options.camera);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Babylon.js
|
|
396
|
-
async #initializeBabylon() {
|
|
397
|
-
this.#engine = new Engine(this.#canvas, true, { alpha: true });
|
|
398
|
-
this.#engine.disableUniformBuffers = true;
|
|
399
|
-
this.#scene = new Scene(this.#engine);
|
|
400
|
-
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
401
|
-
this.#createCamera();
|
|
402
|
-
this.#createLights();
|
|
403
|
-
this.#setupInteraction();
|
|
404
|
-
await this.#createXRExperience();
|
|
405
|
-
this.#engine.runRenderLoop(this.#renderLoop);
|
|
406
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// If this function is defined as '#renderLoop() {}' it is not executed in 'this.#engine.runRenderLoop(this.#renderLoop)'
|
|
410
|
-
#renderLoop = () => {
|
|
411
|
-
this.#scene && this.#scene.render();
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
#addStylesToARButton() {
|
|
415
|
-
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"}';
|
|
416
|
-
const style = document.createElement("style");
|
|
417
|
-
style.appendChild(document.createTextNode(css));
|
|
418
|
-
this.#wrapper.appendChild(style);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async #createXRExperience() {
|
|
422
|
-
if (this.#XRExperience) {
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const sessionMode = "immersive-ar";
|
|
427
|
-
const sessionSupported = await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
|
|
428
|
-
if (!sessionSupported) {
|
|
429
|
-
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
const ground = MeshBuilder.CreateGround("ground", { width: 1000, height: 1000 }, this.#scene);
|
|
435
|
-
ground.isVisible = false;
|
|
436
|
-
|
|
437
|
-
const options = {
|
|
438
|
-
floorMeshes: [ground],
|
|
439
|
-
uiOptions: {
|
|
440
|
-
sessionMode: sessionMode,
|
|
441
|
-
renderTarget: "xrLayer",
|
|
442
|
-
referenceSpaceType: "local",
|
|
443
|
-
},
|
|
444
|
-
optionalFeatures: true,
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
this.#XRExperience = await WebXRDefaultExperience.CreateAsync(this.#scene, options);
|
|
448
|
-
|
|
449
|
-
const featuresManager = this.#XRExperience.baseExperience.featuresManager;
|
|
450
|
-
featuresManager.enableFeature(WebXRFeatureName.TELEPORTATION, "stable", {
|
|
451
|
-
xrInput: this.#XRExperience.input,
|
|
452
|
-
floorMeshes: [ground],
|
|
453
|
-
timeToTeleport: 1500,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
this.#XRExperience.baseExperience.sessionManager.onXRReady.add(() => {
|
|
457
|
-
// Set the initial position of xrCamera: use nonVRCamera, which contains a copy of the original this.#scene.activeCamera before entering XR
|
|
458
|
-
this.#XRExperience.baseExperience.camera.setTransformationFromNonVRCamera(this.#XRExperience.baseExperience._nonVRCamera);
|
|
459
|
-
this.#XRExperience.baseExperience.camera.setTarget(Vector3.Zero());
|
|
460
|
-
this.#XRExperience.baseExperience.onInitialXRPoseSetObservable.notifyObservers(this.#XRExperience.baseExperience.camera);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
this.#addStylesToARButton();
|
|
464
|
-
} catch (error) {
|
|
465
|
-
console.warn("PrefViewer: failed to create WebXR experience", error);
|
|
466
|
-
this.#XRExperience = null;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
272
|
+
this.removeAttribute("loading-3d");
|
|
273
|
+
this.setAttribute("loaded-3d", "");
|
|
469
274
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
#createCamera() {
|
|
473
|
-
this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
|
|
474
|
-
this.#camera.upperBetaLimit = Math.PI * 0.48;
|
|
475
|
-
this.#camera.lowerBetaLimit = Math.PI * 0.25;
|
|
476
|
-
this.#camera.lowerRadiusLimit = 5;
|
|
477
|
-
this.#camera.upperRadiusLimit = 20;
|
|
478
|
-
this.#camera.metadata = { locked: false };
|
|
479
|
-
this.#camera.attachControl(this.#canvas, true);
|
|
480
|
-
this.#scene.activeCamera = this.#camera;
|
|
275
|
+
this.#isLoaded = true;
|
|
276
|
+
this.#isLoading = false;
|
|
481
277
|
}
|
|
482
278
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
this.#
|
|
492
|
-
this.#hemiLight.intensity = 0.6;
|
|
493
|
-
|
|
494
|
-
// 2) Directional light from the front-right, angled slightly down
|
|
495
|
-
this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
496
|
-
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
497
|
-
this.#dirLight.intensity = 0.6;
|
|
498
|
-
|
|
499
|
-
// // 3) Soft shadows
|
|
500
|
-
this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
|
|
501
|
-
this.#shadowGen.useBlurExponentialShadowMap = true;
|
|
502
|
-
this.#shadowGen.blurKernel = 16;
|
|
503
|
-
this.#shadowGen.darkness = 0.5;
|
|
504
|
-
|
|
505
|
-
// 4) Camera‐attached headlight
|
|
506
|
-
this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
|
|
507
|
-
this.#cameraLight.parent = this.#camera;
|
|
508
|
-
this.#cameraLight.intensity = 0.3;
|
|
509
|
-
}
|
|
279
|
+
/**
|
|
280
|
+
* Handles the start of a 2D loading operation.
|
|
281
|
+
* Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
|
|
282
|
+
* @private
|
|
283
|
+
* @returns {void}
|
|
284
|
+
*/
|
|
285
|
+
#on2DLoading() {
|
|
286
|
+
this.#isLoaded = false;
|
|
287
|
+
this.#isLoading = true;
|
|
510
288
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
hdrTexture.level = 2.0;
|
|
521
|
-
this.#scene.environmentTexture = hdrTexture;
|
|
289
|
+
this.removeAttribute("loaded-2d");
|
|
290
|
+
this.setAttribute("loading-2d", "");
|
|
291
|
+
this.dispatchEvent(
|
|
292
|
+
new CustomEvent("drawing-loading", {
|
|
293
|
+
bubbles: true,
|
|
294
|
+
cancelable: false,
|
|
295
|
+
composed: true,
|
|
296
|
+
})
|
|
297
|
+
);
|
|
522
298
|
}
|
|
523
299
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
triPlanarVoxelization: true,
|
|
539
|
-
shadowOpacity: 0.8,
|
|
540
|
-
},
|
|
541
|
-
[scene.activeCamera]
|
|
542
|
-
);
|
|
543
|
-
pipeline.allowDebugPasses = false;
|
|
544
|
-
pipeline.gbufferDebugEnabled = true;
|
|
545
|
-
pipeline.importanceSamplingDebugEnabled = false;
|
|
546
|
-
pipeline.voxelDebugEnabled = false;
|
|
547
|
-
pipeline.voxelDebugDisplayMip = 1;
|
|
548
|
-
pipeline.voxelDebugAxis = 2;
|
|
549
|
-
pipeline.voxelTracingDebugEnabled = false;
|
|
550
|
-
pipeline.spatialBlurPassDebugEnabled = false;
|
|
551
|
-
pipeline.accumulationPassDebugEnabled = false;
|
|
552
|
-
return pipeline;
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
|
|
300
|
+
/**
|
|
301
|
+
* Handles the completion of a 2D loading operation.
|
|
302
|
+
* Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
|
|
303
|
+
* @private
|
|
304
|
+
* @returns {void}
|
|
305
|
+
*/
|
|
306
|
+
#on2DLoaded() {
|
|
307
|
+
this.dispatchEvent(
|
|
308
|
+
new CustomEvent("drawing-loaded", {
|
|
309
|
+
bubbles: true,
|
|
310
|
+
cancelable: false,
|
|
311
|
+
composed: true,
|
|
312
|
+
})
|
|
313
|
+
);
|
|
556
314
|
|
|
557
|
-
this
|
|
558
|
-
|
|
559
|
-
return false;
|
|
560
|
-
}
|
|
561
|
-
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
562
|
-
iblShadowsPipeline.updateSceneBounds();
|
|
563
|
-
});
|
|
315
|
+
this.removeAttribute("loading-2d");
|
|
316
|
+
this.setAttribute("loaded-2d", "");
|
|
564
317
|
|
|
565
|
-
this.#
|
|
566
|
-
|
|
567
|
-
});
|
|
318
|
+
this.#isLoaded = true;
|
|
319
|
+
this.#isLoading = false;
|
|
568
320
|
}
|
|
569
321
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Processes a configuration object by loading it into the 3D component.
|
|
324
|
+
* Dispatches loading events and processes the next task when finished.
|
|
325
|
+
* @private
|
|
326
|
+
* @param {object} config - The configuration object to process.
|
|
327
|
+
* @returns {void}
|
|
328
|
+
*/
|
|
329
|
+
#processConfig(config) {
|
|
330
|
+
if (!this.#component3D) {
|
|
331
|
+
return;
|
|
574
332
|
}
|
|
575
333
|
|
|
576
|
-
this.#
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
mesh.receiveShadows = true;
|
|
581
|
-
if (!mesh.name === "hdri") {
|
|
582
|
-
this.#shadowGen.addShadowCaster(mesh, true);
|
|
583
|
-
}
|
|
334
|
+
this.#on3DLoading();
|
|
335
|
+
this.#component3D.load(config).then((detail) => {
|
|
336
|
+
this.#on3DLoaded(detail);
|
|
337
|
+
this.#processNextTask();
|
|
584
338
|
});
|
|
585
339
|
}
|
|
586
340
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Processes a drawing object by loading it into the 2D component.
|
|
343
|
+
* Processes the next task when finished.
|
|
344
|
+
* @private
|
|
345
|
+
* @param {object} drawing - The drawing object to process.
|
|
346
|
+
* @returns {void}
|
|
347
|
+
*/
|
|
348
|
+
#processDrawing(drawing) {
|
|
349
|
+
if (!this.#component2D) {
|
|
350
|
+
return;
|
|
596
351
|
}
|
|
597
|
-
}
|
|
598
352
|
|
|
599
|
-
|
|
600
|
-
this.#
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
//const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
605
|
-
//this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
|
|
606
|
-
if (!this.#scene.activeCamera.metadata?.locked) {
|
|
607
|
-
this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
|
|
608
|
-
}
|
|
609
|
-
event.preventDefault();
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
#disposeEngine() {
|
|
614
|
-
if (!this.#engine) return;
|
|
615
|
-
this.#engine.dispose();
|
|
616
|
-
this.#engine = this.#scene = this.#camera = null;
|
|
617
|
-
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
618
|
-
this.#shadowGen = null;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Utility methods for loading gltf/glb
|
|
622
|
-
async #getServerFileDataHeader(uri) {
|
|
623
|
-
return new Promise((resolve) => {
|
|
624
|
-
const xhr = new XMLHttpRequest();
|
|
625
|
-
xhr.open("HEAD", uri, true);
|
|
626
|
-
xhr.responseType = "blob";
|
|
627
|
-
xhr.onload = () => {
|
|
628
|
-
if (xhr.status === 200) {
|
|
629
|
-
const size = parseInt(xhr.getResponseHeader("Content-Length"));
|
|
630
|
-
const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
|
|
631
|
-
resolve([size, timeStamp]);
|
|
632
|
-
} else {
|
|
633
|
-
resolve([0, null]);
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
xhr.onerror = () => {
|
|
637
|
-
resolve([0, null]);
|
|
638
|
-
};
|
|
639
|
-
xhr.send();
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
#transformUrl(url) {
|
|
644
|
-
return new Promise((resolve) => {
|
|
645
|
-
resolve(url.replace(/^blob:[^/]+\//i, "").replace(/\\/g, "/"));
|
|
353
|
+
this.#on2DLoading();
|
|
354
|
+
this.#component2D.load(drawing).then(() => {
|
|
355
|
+
this.#on2DLoaded();
|
|
356
|
+
this.#processNextTask();
|
|
646
357
|
});
|
|
647
358
|
}
|
|
648
359
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return { blob, extension, size };
|
|
660
|
-
}
|
|
661
|
-
let isJson = false;
|
|
662
|
-
try {
|
|
663
|
-
JSON.parse(decoded);
|
|
664
|
-
isJson = true;
|
|
665
|
-
} catch { }
|
|
666
|
-
extension = isJson ? ".gltf" : ".glb";
|
|
667
|
-
const type = isJson ? "model/gltf+json" : "model/gltf-binary";
|
|
668
|
-
const array = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
|
|
669
|
-
blob = new Blob([array], { type });
|
|
670
|
-
return { blob, extension, size };
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async #initStorage(db, table) {
|
|
674
|
-
if (window.gltfDB && window.gltfDB.name === db && window.gltfDB.objectStoreNames.contains(table)) {
|
|
675
|
-
return true;
|
|
676
|
-
}
|
|
677
|
-
await initDb(db, table);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Methods for managing Asset Containers
|
|
681
|
-
#setVisibilityOfWallAndFloorInModel(show) {
|
|
682
|
-
if (!this.#data.containers.model.assetContainer || !this.#data.containers.model.visible) {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
show = show !== undefined ? show : this.#data.containers.environment.visible;
|
|
686
|
-
const prefixes = Object.values(this.#data.options.materials).map((material) => material.prefix);
|
|
687
|
-
this.#data.containers.model.assetContainer.meshes.filter((meshToFilter) => prefixes.some((prefix) => meshToFilter.name.startsWith(prefix))).forEach((mesh) => mesh.setEnabled(show));
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
#setOptionsMaterial(optionMaterial) {
|
|
691
|
-
if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const material = this.#data.containers.materials.assetContainer?.materials.find((mat) => mat.name === optionMaterial.value) || null;
|
|
696
|
-
if (!material) {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const containers = [];
|
|
701
|
-
if (this.#data.containers.model.assetContainer && (this.#data.containers.model.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
|
|
702
|
-
containers.push(this.#data.containers.model.assetContainer);
|
|
703
|
-
}
|
|
704
|
-
if (this.#data.containers.environment.assetContainer && (this.#data.containers.environment.changed.pending || this.#data.containers.materials.changed.pending || optionMaterial.changed.pending)) {
|
|
705
|
-
containers.push(this.#data.containers.environment.assetContainer);
|
|
706
|
-
}
|
|
707
|
-
if (containers.length === 0) {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
let someSetted = false;
|
|
712
|
-
containers.forEach((container) =>
|
|
713
|
-
container.meshes
|
|
714
|
-
.filter((meshToFilter) => meshToFilter.name.startsWith(optionMaterial.prefix))
|
|
715
|
-
.forEach((mesh) => {
|
|
716
|
-
mesh.material = material;
|
|
717
|
-
someSetted = true;
|
|
718
|
-
})
|
|
719
|
-
);
|
|
720
|
-
|
|
721
|
-
if (someSetted) {
|
|
722
|
-
optionMaterial.changed.success = true;
|
|
723
|
-
} else if (optionMaterial.changed.pending) {
|
|
724
|
-
optionMaterial.value = optionMaterial.changed.value;
|
|
725
|
-
optionMaterial.changed.success = false;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
return someSetted;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
#setOptionsMaterials() {
|
|
732
|
-
let someSetted = false;
|
|
733
|
-
Object.values(this.#data.options.materials).forEach((material) => {
|
|
734
|
-
let settedMaterial = this.#setOptionsMaterial(material);
|
|
735
|
-
someSetted = someSetted || settedMaterial;
|
|
736
|
-
});
|
|
737
|
-
return someSetted;
|
|
360
|
+
/**
|
|
361
|
+
* Processes an environment (scene) object by wrapping it in a config and loading it.
|
|
362
|
+
* @private
|
|
363
|
+
* @param {object} environment - The environment/scene object to process.
|
|
364
|
+
* @returns {void}
|
|
365
|
+
*/
|
|
366
|
+
#processEnvironment(environment) {
|
|
367
|
+
const config = {};
|
|
368
|
+
config.scene = environment;
|
|
369
|
+
this.#processConfig(config);
|
|
738
370
|
}
|
|
739
371
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (camera) {
|
|
751
|
-
camera.metadata = { locked: this.#data.options.camera.changed.locked };
|
|
752
|
-
this.#data.options.camera.value = this.#data.options.camera.changed.value;
|
|
753
|
-
this.#data.options.camera.locked = this.#data.options.camera.changed.locked;
|
|
754
|
-
this.#data.options.camera.changed.success = false;
|
|
755
|
-
} else {
|
|
756
|
-
camera = this.#camera;
|
|
757
|
-
this.#data.options.camera.value = null;
|
|
758
|
-
this.#data.options.camera.locked = this.#camera.metadata.locked;
|
|
759
|
-
this.#data.options.camera.changed.success = false;
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
camera.metadata = { locked: this.#data.options.camera.locked };
|
|
763
|
-
if (this.#data.options.camera.changed.pending) {
|
|
764
|
-
this.#data.options.camera.changed.success = true;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
if (!this.#data.options.camera.locked && this.#data.options.camera.value !== null) {
|
|
768
|
-
camera.attachControl(this.#canvas, true);
|
|
769
|
-
}
|
|
770
|
-
this.#scene.activeCamera = camera;
|
|
771
|
-
return true;
|
|
372
|
+
/**
|
|
373
|
+
* Processes a materials object by wrapping it in a config and loading it.
|
|
374
|
+
* @private
|
|
375
|
+
* @param {object} materials - The materials object to process.
|
|
376
|
+
* @returns {void}
|
|
377
|
+
*/
|
|
378
|
+
#processMaterials(materials) {
|
|
379
|
+
const config = {};
|
|
380
|
+
config.materials = materials;
|
|
381
|
+
this.#processConfig(config);
|
|
772
382
|
}
|
|
773
383
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Processes a model object by wrapping it in a config and loading it.
|
|
386
|
+
* @private
|
|
387
|
+
* @param {object} model - The model object to process.
|
|
388
|
+
* @returns {void}
|
|
389
|
+
*/
|
|
390
|
+
#processModel(model) {
|
|
391
|
+
const config = {};
|
|
392
|
+
config.model = model;
|
|
393
|
+
this.#processConfig(config);
|
|
782
394
|
}
|
|
783
395
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Processes viewer options by loading them into the 3D component.
|
|
398
|
+
* Dispatches loading events and processes the next task.
|
|
399
|
+
* @private
|
|
400
|
+
* @param {object} options - The options object to process.
|
|
401
|
+
* @returns {void}
|
|
402
|
+
*/
|
|
403
|
+
#processOptions(options) {
|
|
404
|
+
if (!this.#component3D) {
|
|
405
|
+
return;
|
|
787
406
|
}
|
|
788
407
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
#replaceContainer(container, newAssetContainer) {
|
|
795
|
-
if (container.assetContainer) {
|
|
796
|
-
this.#removeContainer(container);
|
|
797
|
-
container.assetContainer.dispose();
|
|
798
|
-
container.assetContainer = null;
|
|
799
|
-
}
|
|
800
|
-
this.#scene.getEngine().releaseEffects();
|
|
801
|
-
container.assetContainer = newAssetContainer;
|
|
802
|
-
this.#addContainer(container);
|
|
803
|
-
return true;
|
|
408
|
+
this.#on3DLoading();
|
|
409
|
+
const detail = this.#component3D.setOptions(options);
|
|
410
|
+
this.#on3DLoaded(detail);
|
|
411
|
+
this.#processNextTask();
|
|
804
412
|
}
|
|
805
413
|
|
|
806
414
|
/**
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
* @
|
|
810
|
-
* @
|
|
811
|
-
* @
|
|
812
|
-
* - When provided, assetContainerURL is used as the base path for other scene files (binary buffers and all images).
|
|
813
|
-
* If not provided (null/undefined), it is because it is the assetContainer of the model or materials whose URIs are absolute.
|
|
814
|
-
* - According to the glTF 2.0 spec, only items inside the "buffers" and "images" arrays may have a "uri" property.
|
|
815
|
-
* - Data URIs (embedded base64) are ignored and left unchanged.
|
|
816
|
-
* - Matching asset URIs are normalized (backslashes converted to forward slashes) and passed to the FileStorage layer
|
|
817
|
-
* to obtain a usable URL (object URL or cached URL).
|
|
818
|
-
* - The function performs replacements in parallel and waits for all lookups to complete.
|
|
819
|
-
* - The JSON is updated in-place with the resolved URLs.
|
|
820
|
-
* @see {@link https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#uris|glTF™ 2.0 Specification - URIs}
|
|
415
|
+
* Processes visibility configuration for the model and scene.
|
|
416
|
+
* Shows or hides the model and/or scene based on the config, then processes the next task.
|
|
417
|
+
* @private
|
|
418
|
+
* @param {object} config - The visibility configuration object.
|
|
419
|
+
* @returns {void}
|
|
821
420
|
*/
|
|
822
|
-
|
|
823
|
-
if (!
|
|
421
|
+
#processVisibility(config) {
|
|
422
|
+
if (!this.#component3D) {
|
|
824
423
|
return;
|
|
825
424
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const lastIndexOfSlash = assetContainerURL.lastIndexOf("/");
|
|
831
|
-
if (lastIndexOfSlash !== -1) {
|
|
832
|
-
sceneURLBase = assetContainerURL.substring(0, lastIndexOfSlash + 1);
|
|
833
|
-
}
|
|
425
|
+
const showModel = config.model?.visible;
|
|
426
|
+
const showScene = config.scene?.visible;
|
|
427
|
+
if (showModel !== undefined) {
|
|
428
|
+
showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
|
|
834
429
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Check whether a value is a syntactically absolute URL.
|
|
840
|
-
* @param {string} url Value to test.
|
|
841
|
-
* @returns {boolean} True when `url` is a string that can be parsed by the global URL constructor (i.e. a syntactically absolute URL); false otherwise.
|
|
842
|
-
* @description
|
|
843
|
-
* - Returns false for non-string inputs.
|
|
844
|
-
* - Uses the browser's URL parser, so protocol-relative URLs ("//host/...") and relative paths are considered non-absolute.
|
|
845
|
-
* - This is a syntactic check only — it does not perform network requests or validate reachability/CORS.
|
|
846
|
-
*/
|
|
847
|
-
var isURLAbsolute = function (url) {
|
|
848
|
-
if (typeof url !== "string") {
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
try {
|
|
852
|
-
new URL(url);
|
|
853
|
-
return true;
|
|
854
|
-
} catch {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
};
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Collect asset entries that have an external URI (non-data URI) and store a normalized absolute/relative-resolved URI for later replacement.
|
|
861
|
-
* @param {Object} asset glTF asset entry (an element of buffers[] or images[]).
|
|
862
|
-
* @param {number} index Index of the asset within its parent array.
|
|
863
|
-
* @param {Array} array Reference to the parent array (buffers or images).
|
|
864
|
-
* @returns {void} Side-effect: pushes a record into arrayOfAssetsWithURI when applicable { parent: <array>, index: <number>, uri: <string> }.
|
|
865
|
-
*/
|
|
866
|
-
var saveAssetData = function (asset, index, array) {
|
|
867
|
-
if (asset.uri && !asset.uri.startsWith("data:")) {
|
|
868
|
-
const assetData = {
|
|
869
|
-
parent: array,
|
|
870
|
-
index: index,
|
|
871
|
-
uri: `${!isURLAbsolute(asset.uri) && sceneURLBase ? sceneURLBase : ""}${asset.uri}`.replace(/\\\\|\\|\/\\/g, "/"),
|
|
872
|
-
};
|
|
873
|
-
arrayOfAssetsWithURI.push(assetData);
|
|
874
|
-
}
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
if (assetContainerJSON.buffers) {
|
|
878
|
-
assetContainerJSON.buffers.forEach((asset, index, array) => saveAssetData(asset, index, array));
|
|
430
|
+
if (showScene !== undefined) {
|
|
431
|
+
showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
|
|
879
432
|
}
|
|
880
|
-
|
|
881
|
-
assetContainerJSON.images.forEach((asset, index, array) => saveAssetData(asset, index, array));
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Replace parallel URIs so that if files are not stored yet, they are downloaded in parallel
|
|
885
|
-
const promisesArray = arrayOfAssetsWithURI.map(async (asset) => {
|
|
886
|
-
const uri = await this.#fileStorage.getURL(asset.uri);
|
|
887
|
-
if (uri) {
|
|
888
|
-
asset.parent[asset.index].uri = uri;
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
await Promise.all(promisesArray);
|
|
433
|
+
this.#processNextTask();
|
|
892
434
|
}
|
|
893
435
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
} else {
|
|
911
|
-
container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (!source) {
|
|
916
|
-
return false;
|
|
436
|
+
/**
|
|
437
|
+
* ---------------------------
|
|
438
|
+
* Public methods
|
|
439
|
+
* ---------------------------
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
|
|
444
|
+
* @public
|
|
445
|
+
* @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
|
|
446
|
+
* @returns {void}
|
|
447
|
+
*/
|
|
448
|
+
setMode(mode = this.#mode) {
|
|
449
|
+
if (mode !== "2d" && mode !== "3d") {
|
|
450
|
+
console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
|
|
451
|
+
return;
|
|
917
452
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (blob && extension) {
|
|
923
|
-
if ((container.name === "model" || container.name === "materials") && extension === ".gltf") {
|
|
924
|
-
const assetContainerJSON = JSON.parse(await blob.text());
|
|
925
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
926
|
-
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
927
|
-
} else {
|
|
928
|
-
file = new File([blob], `${container.name}${extension}`, {
|
|
929
|
-
type: blob.type,
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
if (!container.changed.pending) {
|
|
933
|
-
if (container.timeStamp === null && container.size === size) {
|
|
934
|
-
container.changed = { pending: false, success: false };
|
|
935
|
-
return false;
|
|
936
|
-
} else {
|
|
937
|
-
container.changed = { pending: true, size: size, success: false, timeStamp: null };
|
|
938
|
-
}
|
|
939
|
-
}
|
|
453
|
+
this.#mode = mode;
|
|
454
|
+
if (mode === "2d") {
|
|
455
|
+
this.#component3D?.hide();
|
|
456
|
+
this.#component2D?.show();
|
|
940
457
|
} else {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
let [fileSize, fileTimeStamp] = [await this.#fileStorage.getSize(source), await this.#fileStorage.getTimeStamp(source)];
|
|
944
|
-
if (container.size === fileSize && container.timeStamp === fileTimeStamp) {
|
|
945
|
-
container.changed = { pending: false, success: false };
|
|
946
|
-
return false;
|
|
947
|
-
} else {
|
|
948
|
-
container.changed = { pending: true, size: fileSize, success: false, timeStamp: fileTimeStamp };
|
|
949
|
-
if (extension === ".gltf") {
|
|
950
|
-
const assetContainerBlob = await this.#fileStorage.getBlob(source);
|
|
951
|
-
const assetContainerJSON = JSON.parse(await assetContainerBlob.text());
|
|
952
|
-
await this.#replaceSceneURIAsync(assetContainerJSON, source);
|
|
953
|
-
source = `data:${JSON.stringify(assetContainerJSON)}`;
|
|
954
|
-
} else {
|
|
955
|
-
source = await this.#fileStorage.getURL(source);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
|
|
961
|
-
let options = {
|
|
962
|
-
pluginExtension: extension,
|
|
963
|
-
pluginOptions: {
|
|
964
|
-
gltf: {
|
|
965
|
-
compileMaterials: true,
|
|
966
|
-
loadAllMaterials: true,
|
|
967
|
-
loadOnlyMaterials: container.name === "materials",
|
|
968
|
-
//preprocessUrlAsync: this.#transformUrl,
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
return LoadAssetContainerAsync(file || source, this.#scene, options);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
|
|
977
|
-
this.#engine.stopRenderLoop(this.#renderLoop);
|
|
978
|
-
|
|
979
|
-
const promiseArray = [];
|
|
980
|
-
promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
|
|
981
|
-
promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
|
|
982
|
-
promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
|
|
983
|
-
|
|
984
|
-
Promise.allSettled(promiseArray)
|
|
985
|
-
.then((values) => {
|
|
986
|
-
const modelContainer = values[0];
|
|
987
|
-
const environmentContainer = values[1];
|
|
988
|
-
const materialsContainer = values[2];
|
|
989
|
-
|
|
990
|
-
if (modelContainer.status === "fulfilled" && modelContainer.value) {
|
|
991
|
-
modelContainer.value.lights = [];
|
|
992
|
-
this.#replaceContainer(this.#data.containers.model, modelContainer.value);
|
|
993
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
|
|
994
|
-
} else {
|
|
995
|
-
if (this.#data.containers.model.assetContainer && this.#data.containers.model.show !== this.#data.containers.model.visible) {
|
|
996
|
-
this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
|
|
997
|
-
}
|
|
998
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
|
|
1002
|
-
this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
|
|
1003
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
|
|
1004
|
-
} else {
|
|
1005
|
-
if (this.#data.containers.environment.assetContainer && this.#data.containers.environment.show !== this.#data.containers.environment.visible) {
|
|
1006
|
-
this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
|
|
1007
|
-
}
|
|
1008
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
|
|
1012
|
-
this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
|
|
1013
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
|
|
1014
|
-
} else {
|
|
1015
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
this.#setOptionsMaterials();
|
|
1019
|
-
this.#setOptionsCamera();
|
|
1020
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
1021
|
-
})
|
|
1022
|
-
.catch((error) => {
|
|
1023
|
-
this.loaded = true;
|
|
1024
|
-
console.error("PrefViewer: failed to load model", error);
|
|
1025
|
-
this.dispatchEvent(
|
|
1026
|
-
new CustomEvent("scene-error", {
|
|
1027
|
-
bubbles: true,
|
|
1028
|
-
cancelable: false,
|
|
1029
|
-
composed: true,
|
|
1030
|
-
detail: { error: error },
|
|
1031
|
-
})
|
|
1032
|
-
);
|
|
1033
|
-
})
|
|
1034
|
-
.finally(async () => {
|
|
1035
|
-
this.#setMaxSimultaneousLights();
|
|
1036
|
-
this.#initShadows();
|
|
1037
|
-
await this.#setStatusLoaded();
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Tasks
|
|
1042
|
-
#addTaskToQueue(value, type) {
|
|
1043
|
-
this.#taskQueue.push(new PrefViewerTask(value, type));
|
|
1044
|
-
if (this.initialized && !this.loading) {
|
|
1045
|
-
this.#processNextTask();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
#processNextTask() {
|
|
1050
|
-
if (!this.#taskQueue.length) {
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
const task = this.#taskQueue[0];
|
|
1054
|
-
this.#taskQueue.shift();
|
|
1055
|
-
switch (task.type) {
|
|
1056
|
-
case PrefViewerTask.Types.Config:
|
|
1057
|
-
this.#processConfig(task.value);
|
|
1058
|
-
break;
|
|
1059
|
-
case PrefViewerTask.Types.Model:
|
|
1060
|
-
this.#processModel(task.value);
|
|
1061
|
-
break;
|
|
1062
|
-
case PrefViewerTask.Types.Environment:
|
|
1063
|
-
this.#processEnvironment(task.value);
|
|
1064
|
-
break;
|
|
1065
|
-
case PrefViewerTask.Types.Materials:
|
|
1066
|
-
this.#processMaterials(task.value);
|
|
1067
|
-
break;
|
|
1068
|
-
case PrefViewerTask.Types.Options:
|
|
1069
|
-
this.#processOptions(task.value);
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
#processConfig(config) {
|
|
1075
|
-
this.#setStatusLoading();
|
|
1076
|
-
|
|
1077
|
-
// Containers
|
|
1078
|
-
const loadModel = !!config.model?.storage;
|
|
1079
|
-
this.#data.containers.model.changed.pending = loadModel;
|
|
1080
|
-
this.#data.containers.model.changed.success = false;
|
|
1081
|
-
this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
|
|
1082
|
-
this.#data.containers.model.storage = loadModel ? config.model.storage : this.#data.containers.model.storage;
|
|
1083
|
-
this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
|
|
1084
|
-
|
|
1085
|
-
const loadEnvironment = !!config.scene?.storage;
|
|
1086
|
-
this.#data.containers.environment.changed.pending = loadEnvironment;
|
|
1087
|
-
this.#data.containers.environment.changed.success = false;
|
|
1088
|
-
this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
|
|
1089
|
-
this.#data.containers.environment.storage = loadEnvironment ? config.scene.storage : this.#data.containers.environment.storage;
|
|
1090
|
-
this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
|
|
1091
|
-
|
|
1092
|
-
const loadMaterials = !!config.materials?.storage;
|
|
1093
|
-
this.#data.containers.materials.changed.pending = loadMaterials;
|
|
1094
|
-
this.#data.containers.materials.changed.success = false;
|
|
1095
|
-
this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
|
|
1096
|
-
this.#data.containers.materials.storage = loadMaterials ? config.materials.storage : this.#data.containers.materials.storage;
|
|
1097
|
-
|
|
1098
|
-
// Options
|
|
1099
|
-
if (config.options) {
|
|
1100
|
-
this.#checkCameraChanged(config.options);
|
|
1101
|
-
this.#checkMaterialsChanged(config.options);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
this.#loadContainers(loadModel, loadEnvironment, loadMaterials);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
#processModel(model) {
|
|
1108
|
-
this.#setStatusLoading();
|
|
1109
|
-
|
|
1110
|
-
const loadModel = !!model.storage;
|
|
1111
|
-
this.#data.containers.model.changed.pending = loadModel;
|
|
1112
|
-
this.#data.containers.model.changed.success = false;
|
|
1113
|
-
this.#data.containers.model.changed.storage = this.#data.containers.model.storage;
|
|
1114
|
-
this.#data.containers.model.storage = loadModel ? model.storage : this.#data.containers.model.storage;
|
|
1115
|
-
this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
|
|
1116
|
-
|
|
1117
|
-
this.initialized && this.#loadContainers(loadModel, false, false);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
#processEnvironment(environment) {
|
|
1121
|
-
this.#setStatusLoading();
|
|
1122
|
-
|
|
1123
|
-
const loadEnvironment = !!environment.storage;
|
|
1124
|
-
this.#data.containers.environment.changed.pending = loadEnvironment;
|
|
1125
|
-
this.#data.containers.environment.changed.success = false;
|
|
1126
|
-
this.#data.containers.environment.changed.storage = this.#data.containers.environment.storage;
|
|
1127
|
-
this.#data.containers.environment.storage = loadEnvironment ? environment.storage : this.#data.containers.environment.storage;
|
|
1128
|
-
this.#data.containers.environment.show = environment.visible !== undefined ? environment.visible : this.#data.containers.environment.show;
|
|
1129
|
-
|
|
1130
|
-
this.#loadContainers(false, loadEnvironment, false);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
#processMaterials(materials) {
|
|
1134
|
-
this.#setStatusLoading();
|
|
1135
|
-
|
|
1136
|
-
const loadMaterials = !!materials.storage;
|
|
1137
|
-
this.#data.containers.materials.changed.pending = loadMaterials;
|
|
1138
|
-
this.#data.containers.materials.changed.success = false;
|
|
1139
|
-
this.#data.containers.materials.changed.storage = this.#data.containers.materials.storage;
|
|
1140
|
-
this.#data.containers.materials.storage = loadMaterials ? materials.storage : this.#data.containers.materials.storage;
|
|
1141
|
-
|
|
1142
|
-
this.#loadContainers(false, false, loadMaterials);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
async #processOptions(options) {
|
|
1146
|
-
this.#setStatusLoading();
|
|
1147
|
-
|
|
1148
|
-
let someSetted = false;
|
|
1149
|
-
if (this.#checkCameraChanged(options)) {
|
|
1150
|
-
someSetted = someSetted || this.#setOptionsCamera();
|
|
458
|
+
this.#component3D?.show();
|
|
459
|
+
this.#component2D?.hide();
|
|
1151
460
|
}
|
|
1152
|
-
if (this
|
|
1153
|
-
|
|
461
|
+
if (this.getAttribute("mode") !== mode) {
|
|
462
|
+
this.setAttribute("mode", mode);
|
|
1154
463
|
}
|
|
1155
|
-
|
|
1156
|
-
await this.#setStatusLoaded();
|
|
1157
|
-
|
|
1158
|
-
return someSetted;
|
|
1159
464
|
}
|
|
1160
465
|
|
|
1161
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Loads a configuration object or JSON string and adds it to the task queue.
|
|
468
|
+
* If the config contains a drawing, adds a drawing task as well.
|
|
469
|
+
* @public
|
|
470
|
+
* @param {object|string} config - Configuration object or JSON string.
|
|
471
|
+
* @returns {boolean|void} Returns false if config is invalid; otherwise void.
|
|
472
|
+
*/
|
|
1162
473
|
loadConfig(config) {
|
|
1163
474
|
config = typeof config === "string" ? JSON.parse(config) : config;
|
|
1164
475
|
if (!config) {
|
|
1165
476
|
return false;
|
|
1166
477
|
}
|
|
478
|
+
if (config.drawing) {
|
|
479
|
+
this.#addTaskToQueue(config.drawing, "drawing");
|
|
480
|
+
}
|
|
1167
481
|
this.#addTaskToQueue(config, "config");
|
|
1168
482
|
}
|
|
1169
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Loads a model object or JSON string and adds it to the task queue.
|
|
486
|
+
* @public
|
|
487
|
+
* @param {object|string} model - Model object or JSON string.
|
|
488
|
+
* @returns {boolean|void} Returns false if model is invalid; otherwise void.
|
|
489
|
+
*/
|
|
1170
490
|
loadModel(model) {
|
|
1171
491
|
model = typeof model === "string" ? JSON.parse(model) : model;
|
|
1172
492
|
if (!model) {
|
|
@@ -1175,6 +495,12 @@ class PrefViewer extends HTMLElement {
|
|
|
1175
495
|
this.#addTaskToQueue(model, "model");
|
|
1176
496
|
}
|
|
1177
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Loads a scene/environment object or JSON string and adds it to the task queue.
|
|
500
|
+
* @public
|
|
501
|
+
* @param {object|string} scene - Scene object or JSON string.
|
|
502
|
+
* @returns {boolean|void} Returns false if scene is invalid; otherwise void.
|
|
503
|
+
*/
|
|
1178
504
|
loadScene(scene) {
|
|
1179
505
|
scene = typeof scene === "string" ? JSON.parse(scene) : scene;
|
|
1180
506
|
if (!scene) {
|
|
@@ -1183,6 +509,12 @@ class PrefViewer extends HTMLElement {
|
|
|
1183
509
|
this.#addTaskToQueue(scene, "environment");
|
|
1184
510
|
}
|
|
1185
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Loads materials object or JSON string and adds it to the task queue.
|
|
514
|
+
* @public
|
|
515
|
+
* @param {object|string} materials - Materials object or JSON string.
|
|
516
|
+
* @returns {boolean|void} Returns false if materials are invalid; otherwise void.
|
|
517
|
+
*/
|
|
1186
518
|
loadMaterials(materials) {
|
|
1187
519
|
materials = typeof materials === "string" ? JSON.parse(materials) : materials;
|
|
1188
520
|
if (!materials) {
|
|
@@ -1191,6 +523,26 @@ class PrefViewer extends HTMLElement {
|
|
|
1191
523
|
this.#addTaskToQueue(materials, "materials");
|
|
1192
524
|
}
|
|
1193
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Loads a drawing object or JSON string and adds it to the task queue.
|
|
528
|
+
* @public
|
|
529
|
+
* @param {object|string} drawing - Drawing object or JSON string.
|
|
530
|
+
* @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
|
|
531
|
+
*/
|
|
532
|
+
loadDrawing(drawing) {
|
|
533
|
+
drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
|
|
534
|
+
if (!drawing) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
this.#addTaskToQueue(drawing, "drawing");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Sets viewer options from an object or JSON string and adds them to the task queue.
|
|
542
|
+
* @public
|
|
543
|
+
* @param {object|string} options - Options object or JSON string.
|
|
544
|
+
* @returns {boolean|void} Returns false if options are invalid; otherwise void.
|
|
545
|
+
*/
|
|
1194
546
|
setOptions(options) {
|
|
1195
547
|
options = typeof options === "string" ? JSON.parse(options) : options;
|
|
1196
548
|
if (!options) {
|
|
@@ -1199,55 +551,131 @@ class PrefViewer extends HTMLElement {
|
|
|
1199
551
|
this.#addTaskToQueue(options, "options");
|
|
1200
552
|
}
|
|
1201
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Shows the 3D model by setting its visibility to true.
|
|
556
|
+
* Adds a visibility task to the queue for processing.
|
|
557
|
+
* @public
|
|
558
|
+
* @returns {void}
|
|
559
|
+
*/
|
|
1202
560
|
showModel() {
|
|
1203
|
-
|
|
1204
|
-
this.#
|
|
561
|
+
const config = { model: { visible: true } };
|
|
562
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1205
563
|
}
|
|
1206
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Hides the 3D model by setting its visibility to false.
|
|
567
|
+
* Adds a visibility task to the queue for processing.
|
|
568
|
+
* @public
|
|
569
|
+
* @returns {void}
|
|
570
|
+
*/
|
|
1207
571
|
hideModel() {
|
|
1208
|
-
|
|
1209
|
-
this.#
|
|
572
|
+
const config = { model: { visible: false } };
|
|
573
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1210
574
|
}
|
|
1211
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Shows the scene/environment by setting its visibility to true.
|
|
578
|
+
* Adds a visibility task to the queue for processing.
|
|
579
|
+
* @public
|
|
580
|
+
* @returns {void}
|
|
581
|
+
*/
|
|
1212
582
|
showScene() {
|
|
1213
|
-
|
|
1214
|
-
this.#
|
|
1215
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
583
|
+
const config = { scene: { visible: true } };
|
|
584
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1216
585
|
}
|
|
1217
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Hides the scene/environment by setting its visibility to false.
|
|
589
|
+
* Adds a visibility task to the queue for processing.
|
|
590
|
+
* @public
|
|
591
|
+
* @returns {void}
|
|
592
|
+
*/
|
|
1218
593
|
hideScene() {
|
|
1219
|
-
|
|
1220
|
-
this.#
|
|
1221
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
594
|
+
const config = { scene: { visible: false } };
|
|
595
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1222
596
|
}
|
|
1223
597
|
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Initiates download of the current 3D model in GLB format.
|
|
601
|
+
* @public
|
|
602
|
+
* @returns {void}
|
|
603
|
+
*/
|
|
1224
604
|
downloadModelGLB() {
|
|
1225
|
-
|
|
1226
|
-
|
|
605
|
+
if (!this.#component3D) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
this.#component3D.downloadModelGLB();
|
|
1227
610
|
}
|
|
1228
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Initiates download of the current 3D model in USDZ format.
|
|
614
|
+
* @public
|
|
615
|
+
* @returns {void}
|
|
616
|
+
*/
|
|
1229
617
|
downloadModelUSDZ() {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
618
|
+
if (!this.#component3D) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
this.#component3D.downloadModelUSDZ();
|
|
1236
623
|
}
|
|
1237
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Initiates download of both the 3D model and scene in USDZ format.
|
|
627
|
+
* @public
|
|
628
|
+
* @returns {void}
|
|
629
|
+
*/
|
|
1238
630
|
downloadModelAndSceneUSDZ() {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
});
|
|
631
|
+
if (!this.#component3D) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
this.#component3D.downloadModelAndSceneUSDZ();
|
|
1245
636
|
}
|
|
1246
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Initiates download of both the 3D model and scene in GLB format.
|
|
640
|
+
* @public
|
|
641
|
+
* @returns {void}
|
|
642
|
+
*/
|
|
1247
643
|
downloadModelAndSceneGLB() {
|
|
1248
|
-
|
|
1249
|
-
|
|
644
|
+
if (!this.#component3D) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.#component3D.downloadModelAndSceneGLB();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Indicates whether the viewer has been initialized.
|
|
653
|
+
* @public
|
|
654
|
+
* @returns {boolean} True if initialized; otherwise, false.
|
|
655
|
+
*/
|
|
656
|
+
get initialized() {
|
|
657
|
+
return this.#isInitialized;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Indicates whether the viewer has finished loading.
|
|
662
|
+
* @public
|
|
663
|
+
* @returns {boolean} True if loaded; otherwise, false.
|
|
664
|
+
*/
|
|
665
|
+
get loaded() {
|
|
666
|
+
return this.#isLoaded;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Indicates whether the viewer is currently loading.
|
|
671
|
+
* @public
|
|
672
|
+
* @returns {boolean} True if loading; otherwise, false.
|
|
673
|
+
*/
|
|
674
|
+
get loading() {
|
|
675
|
+
return this.#isLoading;
|
|
1250
676
|
}
|
|
1251
677
|
}
|
|
1252
678
|
|
|
679
|
+
customElements.define("pref-viewer-2d", PrefViewer2D);
|
|
680
|
+
customElements.define("pref-viewer-3d", PrefViewer3D);
|
|
1253
681
|
customElements.define("pref-viewer", PrefViewer);
|