@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
package/src/index.js
CHANGED
|
@@ -1,1172 +1,515 @@
|
|
|
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 advanced 2D and 3D product visualization and configuration.
|
|
7
|
+
*
|
|
8
|
+
* Overview:
|
|
9
|
+
* - Encapsulates both 2D (SVG) and 3D (Babylon.js) viewers, supporting glTF/GLB models, environments, and drawings.
|
|
10
|
+
* - Loads assets 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.
|
|
14
|
+
* - Supports downloading models and scenes in GLB and USDZ formats.
|
|
15
|
+
* - Automatically updates the viewer when reactive attributes change.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* - Use as a custom HTML element: <pref-viewer ...>
|
|
19
|
+
* - Configure via attributes (config, model, scene, materials, drawing, options, mode).
|
|
20
|
+
* - Control viewer mode, visibility, and downloads via public methods.
|
|
21
|
+
*
|
|
22
|
+
* Reactive Attributes:
|
|
23
|
+
* - config: URL or Base64 for configuration file.
|
|
24
|
+
* - model: URL or Base64 for 3D model (glTF/GLB).
|
|
25
|
+
* - scene: URL or Base64 for environment/scene (glTF/GLB).
|
|
26
|
+
* - materials: URL or Base64 for materials definition.
|
|
27
|
+
* - drawing: URL or Base64 for SVG drawing.
|
|
28
|
+
* - options: JSON string for viewer options.
|
|
29
|
+
* - mode: Viewer mode ("2d" or "3d").
|
|
5
30
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* • Automatically handles scene creation (engine, camera, lighting) and resource cleanup.
|
|
14
|
-
* • Emits 'model-loaded' and 'model-error' events for integration.
|
|
31
|
+
* Public Methods:
|
|
32
|
+
* - loadConfig(config), loadModel(model), loadScene(scene), loadMaterials(materials), loadDrawing(drawing)
|
|
33
|
+
* - setOptions(options)
|
|
34
|
+
* - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
|
|
35
|
+
* - showModel(), hideModel(), showScene(), hideScene()
|
|
36
|
+
* - downloadModelGLB(), downloadModelUSDZ(), downloadModelAndSceneGLB(), downloadModelAndSceneUSDZ()
|
|
37
|
+
* - zoomCenter(), zoomExtentsAll(), zoomIn(), zoomOut()
|
|
15
38
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* style="width:800px; height:600px;">
|
|
23
|
-
* </pref-viewer>
|
|
24
|
-
* ```
|
|
39
|
+
* Public Properties:
|
|
40
|
+
* - isInitialized: Indicates if the viewer is initialized.
|
|
41
|
+
* - isLoaded: Indicates if the viewer has finished loading.
|
|
42
|
+
* - isLoading: Indicates if the viewer is currently loading.
|
|
43
|
+
* - isMode2D: Indicates if the viewer is in 2D mode.
|
|
44
|
+
* - isMode3D: Indicates if the viewer is in 3D mode.
|
|
25
45
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ```
|
|
46
|
+
* Events:
|
|
47
|
+
* - "scene-loading": Dispatched when a 3D loading operation starts.
|
|
48
|
+
* - "scene-loaded": Dispatched when a 3D loading operation completes.
|
|
49
|
+
* - "drawing-loading": Dispatched when a 2D drawing loading operation starts.
|
|
50
|
+
* - "drawing-loaded": Dispatched when a 2D drawing loading operation completes.
|
|
51
|
+
* - "drawing-zoom-changed": Dispatched when the 2D drawing zoom/pan state changes.
|
|
33
52
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* style="width:800px; height:600px;">
|
|
39
|
-
* </pref-viewer>
|
|
40
|
-
* ```
|
|
53
|
+
* Notes:
|
|
54
|
+
* - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
|
|
55
|
+
* - Processes tasks sequentially to ensure consistent state.
|
|
56
|
+
* - Designed for extensibility and integration in product configurators and visualization tools.
|
|
41
57
|
*/
|
|
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
58
|
class PrefViewer extends HTMLElement {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
#isInitialized = false;
|
|
60
|
+
#isLoaded = false;
|
|
61
|
+
#isLoading = false;
|
|
62
|
+
#mode = "3d";
|
|
63
|
+
|
|
81
64
|
#taskQueue = [];
|
|
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
|
-
};
|
|
147
|
-
|
|
148
|
-
// DOM elements
|
|
149
|
-
#wrapper = null;
|
|
150
|
-
#canvas = null;
|
|
151
|
-
|
|
152
|
-
// Babylon.js core objects
|
|
153
|
-
#engine = null;
|
|
154
|
-
#scene = null;
|
|
155
|
-
#camera = null;
|
|
156
|
-
#hemiLight = null;
|
|
157
|
-
#dirLight = null;
|
|
158
|
-
#cameraLight = null;
|
|
159
|
-
#shadowGen = null;
|
|
160
|
-
#XRExperience = null;
|
|
161
65
|
|
|
66
|
+
#component2D = null;
|
|
67
|
+
#component3D = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a new PrefViewer instance and attaches a shadow DOM.
|
|
71
|
+
* Initializes internal state and component references.
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
162
74
|
constructor() {
|
|
163
75
|
super();
|
|
164
76
|
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
77
|
}
|
|
178
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Returns the list of attributes to observe for changes.
|
|
81
|
+
* @public
|
|
82
|
+
* @returns {string[]} Array of attribute names to observe.
|
|
83
|
+
*/
|
|
179
84
|
static get observedAttributes() {
|
|
180
|
-
return ["config", "model", "scene", "show-model", "show-scene"];
|
|
85
|
+
return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
|
|
181
86
|
}
|
|
182
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Observes changes to specific attributes and triggers corresponding actions.
|
|
90
|
+
* Loads configuration, drawing, model, scene, materials, or options when their attributes change.
|
|
91
|
+
* Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
|
|
92
|
+
* @public
|
|
93
|
+
* @param {string} name - The name of the changed attribute.
|
|
94
|
+
* @param {*} _old - The previous value of the attribute (unused).
|
|
95
|
+
* @param {*} value - The new value of the attribute.
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
183
98
|
attributeChangedCallback(name, _old, value) {
|
|
184
|
-
let data = null;
|
|
185
99
|
switch (name) {
|
|
186
100
|
case "config":
|
|
187
101
|
this.loadConfig(value);
|
|
188
102
|
break;
|
|
103
|
+
case "drawing":
|
|
104
|
+
this.loadDrawing(value);
|
|
105
|
+
break;
|
|
106
|
+
case "materials":
|
|
107
|
+
this.loadMaterials(value);
|
|
108
|
+
break;
|
|
109
|
+
case "mode":
|
|
110
|
+
if (_old === value || value.toLowerCase() === this.#mode) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.setMode(value.toLowerCase());
|
|
114
|
+
break;
|
|
189
115
|
case "model":
|
|
190
116
|
this.loadModel(value);
|
|
191
117
|
break;
|
|
192
118
|
case "scene":
|
|
193
119
|
this.loadScene(value);
|
|
194
120
|
break;
|
|
195
|
-
case "materials":
|
|
196
|
-
this.loadMaterials(value);
|
|
197
|
-
break;
|
|
198
121
|
case "options":
|
|
199
122
|
this.setOptions(value);
|
|
200
123
|
break;
|
|
201
124
|
case "show-model":
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
data ? this.showModel() : this.hideModel();
|
|
205
|
-
} else {
|
|
206
|
-
this.#data.containers.model.show = data;
|
|
125
|
+
if (_old === value) {
|
|
126
|
+
return;
|
|
207
127
|
}
|
|
128
|
+
const showModel = value.toLowerCase() === "true";
|
|
129
|
+
showModel ? this.showModel() : this.hideModel();
|
|
208
130
|
break;
|
|
209
131
|
case "show-scene":
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
data ? this.showScene() : this.hideScene();
|
|
213
|
-
} else {
|
|
214
|
-
this.#data.containers.environment.show = data;
|
|
132
|
+
if (_old === value) {
|
|
133
|
+
return;
|
|
215
134
|
}
|
|
135
|
+
const showScene = value.toLowerCase() === "true";
|
|
136
|
+
showScene ? this.showScene() : this.hideScene();
|
|
216
137
|
break;
|
|
217
138
|
}
|
|
218
139
|
}
|
|
219
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Called when the element is inserted into the DOM.
|
|
143
|
+
* Initializes the 3D and 2D viewer components and starts processing tasks.
|
|
144
|
+
* @public
|
|
145
|
+
* @returns {void|boolean} Returns false if initialization fails; otherwise void.
|
|
146
|
+
*/
|
|
220
147
|
connectedCallback() {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
bubbles: true,
|
|
227
|
-
cancelable: false,
|
|
228
|
-
composed: true,
|
|
229
|
-
detail: { error: new Error(error) },
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
return false;
|
|
148
|
+
this.#createComponent3D();
|
|
149
|
+
this.#createComponent2D();
|
|
150
|
+
|
|
151
|
+
if (!this.hasAttribute("mode")) {
|
|
152
|
+
this.setMode();
|
|
233
153
|
}
|
|
234
154
|
|
|
235
|
-
this.#
|
|
236
|
-
this.initialized = true;
|
|
155
|
+
this.#isInitialized = true;
|
|
237
156
|
this.#processNextTask();
|
|
238
157
|
}
|
|
239
158
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#
|
|
247
|
-
this.#
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
display: "block",
|
|
252
|
-
outline: "none",
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
#wrapCanvas() {
|
|
257
|
-
this.#wrapper = document.createElement("div");
|
|
258
|
-
Object.assign(this.#wrapper.style, {
|
|
259
|
-
width: "100%",
|
|
260
|
-
height: "100%",
|
|
261
|
-
position: "relative",
|
|
262
|
-
});
|
|
263
|
-
this.#wrapper.appendChild(this.#canvas);
|
|
264
|
-
this.shadowRoot.append(this.#wrapper);
|
|
159
|
+
/**
|
|
160
|
+
* Creates and appends the 2D viewer component to the shadow DOM.
|
|
161
|
+
* Sets the "visible" attribute to true by default.
|
|
162
|
+
* @private
|
|
163
|
+
* @returns {void}
|
|
164
|
+
*/
|
|
165
|
+
#createComponent2D() {
|
|
166
|
+
this.#component2D = document.createElement("pref-viewer-2d");
|
|
167
|
+
this.#component2D.setAttribute("visible", "false");
|
|
168
|
+
this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
|
|
169
|
+
this.shadowRoot.appendChild(this.#component2D);
|
|
265
170
|
}
|
|
266
171
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
this.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
cancelable: false,
|
|
278
|
-
composed: true,
|
|
279
|
-
})
|
|
280
|
-
);
|
|
281
|
-
this.#engine.stopRenderLoop(this.#renderLoop);
|
|
172
|
+
/**
|
|
173
|
+
* Creates and appends the 3D viewer component to the shadow DOM.
|
|
174
|
+
* Sets the "visible" attribute to true by default.
|
|
175
|
+
* @private
|
|
176
|
+
* @returns {void}
|
|
177
|
+
*/
|
|
178
|
+
#createComponent3D() {
|
|
179
|
+
this.#component3D = document.createElement("pref-viewer-3d");
|
|
180
|
+
this.#component3D.setAttribute("visible", "false");
|
|
181
|
+
this.shadowRoot.appendChild(this.#component3D);
|
|
282
182
|
}
|
|
283
183
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
310
|
-
this.dispatchEvent(
|
|
311
|
-
new CustomEvent("scene-loaded", {
|
|
312
|
-
bubbles: true,
|
|
313
|
-
cancelable: false,
|
|
314
|
-
composed: true,
|
|
315
|
-
detail: detail,
|
|
316
|
-
})
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
await this.#scene.whenReadyAsync();
|
|
320
|
-
this.#engine.runRenderLoop(this.#renderLoop);
|
|
321
|
-
|
|
322
|
-
this.#resetChangedFlags();
|
|
323
|
-
|
|
324
|
-
if (this.hasAttribute("loading")) {
|
|
325
|
-
this.removeAttribute("loading");
|
|
184
|
+
/**
|
|
185
|
+
* Adds a new task to the internal queue for processing.
|
|
186
|
+
* If the viewer is initialized and not currently loading, immediately processes the next task.
|
|
187
|
+
* @private
|
|
188
|
+
* @param {*} value - The payload or data for the task.
|
|
189
|
+
* @param {string} type - The type of task (see PrefViewerTask.Types).
|
|
190
|
+
* @returns {void}
|
|
191
|
+
*/
|
|
192
|
+
#addTaskToQueue(value, type) {
|
|
193
|
+
this.#taskQueue.push(new PrefViewerTask(value, type));
|
|
194
|
+
if (this.#isInitialized && !this.#isLoading) {
|
|
195
|
+
this.#processNextTask();
|
|
326
196
|
}
|
|
327
|
-
this.setAttribute("loaded", "");
|
|
328
|
-
|
|
329
|
-
this.loaded = true;
|
|
330
|
-
this.loading = false;
|
|
331
|
-
|
|
332
|
-
this.#processNextTask();
|
|
333
197
|
}
|
|
334
198
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Processes the next task in the queue, if any.
|
|
201
|
+
* Dispatches the task to the appropriate handler based on its type.
|
|
202
|
+
* @private
|
|
203
|
+
* @returns {boolean|void} Returns false if the queue is empty; otherwise void.
|
|
204
|
+
*/
|
|
205
|
+
#processNextTask() {
|
|
206
|
+
if (!this.#taskQueue.length) {
|
|
338
207
|
return false;
|
|
339
208
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
209
|
+
const task = this.#taskQueue[0];
|
|
210
|
+
this.#taskQueue.shift();
|
|
211
|
+
switch (task.type) {
|
|
212
|
+
case PrefViewerTask.Types.Config:
|
|
213
|
+
this.#processConfig(task.value);
|
|
214
|
+
break;
|
|
215
|
+
case PrefViewerTask.Types.Drawing:
|
|
216
|
+
this.#processDrawing(task.value);
|
|
217
|
+
break;
|
|
218
|
+
case PrefViewerTask.Types.Environment:
|
|
219
|
+
this.#processEnvironment(task.value);
|
|
220
|
+
break;
|
|
221
|
+
case PrefViewerTask.Types.Materials:
|
|
222
|
+
this.#processMaterials(task.value);
|
|
223
|
+
break;
|
|
224
|
+
case PrefViewerTask.Types.Model:
|
|
225
|
+
this.#processModel(task.value);
|
|
226
|
+
break;
|
|
227
|
+
case PrefViewerTask.Types.Options:
|
|
228
|
+
this.#processOptions(task.value);
|
|
229
|
+
break;
|
|
230
|
+
case PrefViewerTask.Types.Visibility:
|
|
231
|
+
this.#processVisibility(task.value);
|
|
232
|
+
break;
|
|
349
233
|
}
|
|
350
|
-
return changed;
|
|
351
234
|
}
|
|
352
235
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
}
|
|
236
|
+
/**
|
|
237
|
+
* Handles the start of a 3D loading operation.
|
|
238
|
+
* Updates loading state, sets attributes, and dispatches a "scene-loading" event.
|
|
239
|
+
* @private
|
|
240
|
+
* @returns {void}
|
|
241
|
+
*/
|
|
242
|
+
#on3DLoading() {
|
|
243
|
+
this.#isLoaded = false;
|
|
244
|
+
this.#isLoading = true;
|
|
375
245
|
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
}
|
|
246
|
+
this.removeAttribute("loaded-3d");
|
|
247
|
+
this.setAttribute("loading-3d", "");
|
|
385
248
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
249
|
+
const customEventOptions = {
|
|
250
|
+
bubbles: true,
|
|
251
|
+
cancelable: false,
|
|
252
|
+
composed: true,
|
|
389
253
|
};
|
|
390
|
-
|
|
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);
|
|
254
|
+
this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
|
|
407
255
|
}
|
|
408
256
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Handles the completion of a 3D loading operation.
|
|
259
|
+
* Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
|
|
260
|
+
* @private
|
|
261
|
+
* @param {object} [detail] - Optional details to include in the event.
|
|
262
|
+
* @returns {void}
|
|
263
|
+
*/
|
|
264
|
+
#on3DLoaded(detail) {
|
|
265
|
+
this.#isLoaded = true;
|
|
266
|
+
this.#isLoading = false;
|
|
413
267
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const style = document.createElement("style");
|
|
417
|
-
style.appendChild(document.createTextNode(css));
|
|
418
|
-
this.#wrapper.appendChild(style);
|
|
419
|
-
}
|
|
268
|
+
this.removeAttribute("loading-3d");
|
|
269
|
+
this.setAttribute("loaded-3d", "");
|
|
420
270
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
271
|
+
const customEventOptions = {
|
|
272
|
+
bubbles: true,
|
|
273
|
+
cancelable: false,
|
|
274
|
+
composed: true,
|
|
275
|
+
};
|
|
276
|
+
if (detail) {
|
|
277
|
+
customEventOptions.detail = detail;
|
|
424
278
|
}
|
|
279
|
+
this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
|
|
280
|
+
}
|
|
425
281
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Handles the start of a 2D loading operation.
|
|
284
|
+
* Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
|
|
285
|
+
* @private
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
288
|
+
#on2DLoading() {
|
|
289
|
+
this.#isLoaded = false;
|
|
290
|
+
this.#isLoading = true;
|
|
432
291
|
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
}
|
|
292
|
+
this.removeAttribute("loaded-2d");
|
|
293
|
+
this.setAttribute("loading-2d", "");
|
|
469
294
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this
|
|
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;
|
|
295
|
+
const customEventOptions = {
|
|
296
|
+
bubbles: true,
|
|
297
|
+
cancelable: false,
|
|
298
|
+
composed: true,
|
|
299
|
+
};
|
|
300
|
+
this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
|
|
481
301
|
}
|
|
482
302
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Handles the completion of a 2D loading operation.
|
|
305
|
+
* Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
|
|
306
|
+
* @private
|
|
307
|
+
* @param {object} [detail] - Optional details to include in the event.
|
|
308
|
+
* @returns {void}
|
|
309
|
+
*/
|
|
310
|
+
#on2DLoaded(detail) {
|
|
311
|
+
this.#isLoaded = true;
|
|
312
|
+
this.#isLoading = false;
|
|
489
313
|
|
|
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
|
-
}
|
|
314
|
+
this.removeAttribute("loading-2d");
|
|
315
|
+
this.setAttribute("loaded-2d", "");
|
|
510
316
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
317
|
+
const customEventOptions = {
|
|
318
|
+
bubbles: true,
|
|
319
|
+
cancelable: false,
|
|
320
|
+
composed: true,
|
|
321
|
+
};
|
|
322
|
+
if (detail) {
|
|
323
|
+
customEventOptions.detail = detail;
|
|
515
324
|
}
|
|
516
|
-
|
|
517
|
-
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
|
|
518
|
-
hdrTexture.gammaSpace = true;
|
|
519
|
-
hdrTexture._noMipmap = false;
|
|
520
|
-
hdrTexture.level = 2.0;
|
|
521
|
-
this.#scene.environmentTexture = hdrTexture;
|
|
325
|
+
this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
|
|
522
326
|
}
|
|
523
327
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
shadowRemanence: 0.8,
|
|
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;
|
|
328
|
+
/**
|
|
329
|
+
* Handles the "drawing-zoom-changed" event from the 2D viewer component.
|
|
330
|
+
* Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
|
|
331
|
+
* @private
|
|
332
|
+
* @param {CustomEvent} event - The original zoom change event from the 2D viewer.
|
|
333
|
+
* @returns {void}
|
|
334
|
+
*/
|
|
335
|
+
#on2DZoomChanged(event) {
|
|
336
|
+
const customEventOptions = {
|
|
337
|
+
bubbles: true,
|
|
338
|
+
cancelable: false,
|
|
339
|
+
composed: true,
|
|
340
|
+
detail: event.detail,
|
|
553
341
|
};
|
|
554
|
-
|
|
555
|
-
let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
|
|
556
|
-
|
|
557
|
-
this.#scene.meshes.forEach((mesh) => {
|
|
558
|
-
if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
|
|
559
|
-
return false;
|
|
560
|
-
}
|
|
561
|
-
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
562
|
-
iblShadowsPipeline.updateSceneBounds();
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
this.#scene.materials.forEach((material) => {
|
|
566
|
-
iblShadowsPipeline.addShadowReceivingMaterial(material);
|
|
567
|
-
});
|
|
342
|
+
this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
|
|
568
343
|
}
|
|
569
344
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Processes a configuration object by loading it into the 3D component.
|
|
347
|
+
* Dispatches loading events and processes the next task when finished.
|
|
348
|
+
* @private
|
|
349
|
+
* @param {object} config - The configuration object to process.
|
|
350
|
+
* @returns {void}
|
|
351
|
+
*/
|
|
352
|
+
#processConfig(config) {
|
|
353
|
+
if (!this.#component3D) {
|
|
354
|
+
return;
|
|
574
355
|
}
|
|
575
356
|
|
|
576
|
-
this.#
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
mesh.receiveShadows = true;
|
|
581
|
-
if (!mesh.name === "hdri") {
|
|
582
|
-
this.#shadowGen.addShadowCaster(mesh, true);
|
|
583
|
-
}
|
|
357
|
+
this.#on3DLoading();
|
|
358
|
+
this.#component3D.load(config).then((detail) => {
|
|
359
|
+
this.#on3DLoaded(detail);
|
|
360
|
+
this.#processNextTask();
|
|
584
361
|
});
|
|
585
362
|
}
|
|
586
363
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Processes a drawing object by loading it into the 2D component.
|
|
366
|
+
* Processes the next task when finished.
|
|
367
|
+
* @private
|
|
368
|
+
* @param {object} drawing - The drawing object to process.
|
|
369
|
+
* @returns {void}
|
|
370
|
+
*/
|
|
371
|
+
#processDrawing(drawing) {
|
|
372
|
+
if (!this.#component2D) {
|
|
373
|
+
return;
|
|
596
374
|
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
#setupInteraction() {
|
|
600
|
-
this.#canvas.addEventListener("wheel", (event) => {
|
|
601
|
-
if (!this.#scene || !this.#camera) {
|
|
602
|
-
return false;
|
|
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
375
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
376
|
+
this.#on2DLoading();
|
|
377
|
+
this.#component2D.load(drawing).then((detail) => {
|
|
378
|
+
this.#on2DLoaded(detail);
|
|
379
|
+
this.#processNextTask();
|
|
646
380
|
});
|
|
647
381
|
}
|
|
648
382
|
|
|
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;
|
|
383
|
+
/**
|
|
384
|
+
* Processes an environment (scene) object by wrapping it in a config and loading it.
|
|
385
|
+
* @private
|
|
386
|
+
* @param {object} environment - The environment/scene object to process.
|
|
387
|
+
* @returns {void}
|
|
388
|
+
*/
|
|
389
|
+
#processEnvironment(environment) {
|
|
390
|
+
const config = {};
|
|
391
|
+
config.scene = environment;
|
|
392
|
+
this.#processConfig(config);
|
|
738
393
|
}
|
|
739
394
|
|
|
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;
|
|
395
|
+
/**
|
|
396
|
+
* Processes a materials object by wrapping it in a config and loading it.
|
|
397
|
+
* @private
|
|
398
|
+
* @param {object} materials - The materials object to process.
|
|
399
|
+
* @returns {void}
|
|
400
|
+
*/
|
|
401
|
+
#processMaterials(materials) {
|
|
402
|
+
const config = {};
|
|
403
|
+
config.materials = materials;
|
|
404
|
+
this.#processConfig(config);
|
|
772
405
|
}
|
|
773
406
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Processes a model object by wrapping it in a config and loading it.
|
|
409
|
+
* @private
|
|
410
|
+
* @param {object} model - The model object to process.
|
|
411
|
+
* @returns {void}
|
|
412
|
+
*/
|
|
413
|
+
#processModel(model) {
|
|
414
|
+
const config = {};
|
|
415
|
+
config.model = model;
|
|
416
|
+
this.#processConfig(config);
|
|
782
417
|
}
|
|
783
418
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Processes viewer options by loading them into the 3D component.
|
|
421
|
+
* Dispatches loading events and processes the next task.
|
|
422
|
+
* @private
|
|
423
|
+
* @param {object} options - The options object to process.
|
|
424
|
+
* @returns {void}
|
|
425
|
+
*/
|
|
426
|
+
#processOptions(options) {
|
|
427
|
+
if (!this.#component3D) {
|
|
428
|
+
return;
|
|
787
429
|
}
|
|
788
430
|
|
|
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;
|
|
431
|
+
this.#on3DLoading();
|
|
432
|
+
const detail = this.#component3D.setOptions(options);
|
|
433
|
+
this.#on3DLoaded(detail);
|
|
434
|
+
this.#processNextTask();
|
|
804
435
|
}
|
|
805
436
|
|
|
806
437
|
/**
|
|
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}
|
|
438
|
+
* Processes visibility configuration for the model and scene.
|
|
439
|
+
* Shows or hides the model and/or scene based on the config, then processes the next task.
|
|
440
|
+
* @private
|
|
441
|
+
* @param {object} config - The visibility configuration object.
|
|
442
|
+
* @returns {void}
|
|
821
443
|
*/
|
|
822
|
-
|
|
823
|
-
if (!
|
|
444
|
+
#processVisibility(config) {
|
|
445
|
+
if (!this.#component3D) {
|
|
824
446
|
return;
|
|
825
447
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const lastIndexOfSlash = assetContainerURL.lastIndexOf("/");
|
|
831
|
-
if (lastIndexOfSlash !== -1) {
|
|
832
|
-
sceneURLBase = assetContainerURL.substring(0, lastIndexOfSlash + 1);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const arrayOfAssetsWithURI = [];
|
|
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));
|
|
448
|
+
const showModel = config.model?.visible;
|
|
449
|
+
const showScene = config.scene?.visible;
|
|
450
|
+
if (showModel !== undefined) {
|
|
451
|
+
showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
|
|
879
452
|
}
|
|
880
|
-
if (
|
|
881
|
-
|
|
453
|
+
if (showScene !== undefined) {
|
|
454
|
+
showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
|
|
882
455
|
}
|
|
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);
|
|
456
|
+
this.#processNextTask();
|
|
892
457
|
}
|
|
893
458
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
let source = storage.url || null;
|
|
902
|
-
|
|
903
|
-
if (storage.db && storage.table && storage.id) {
|
|
904
|
-
await this.#initStorage(storage.db, storage.table);
|
|
905
|
-
const object = await loadModel(storage.id, storage.table);
|
|
906
|
-
source = object.data;
|
|
907
|
-
if (object.timeStamp === container.timeStamp) {
|
|
908
|
-
container.changed = { pending: false, success: false };
|
|
909
|
-
return false;
|
|
910
|
-
} else {
|
|
911
|
-
container.changed = { pending: true, size: object.size, success: false, timeStamp: object.timeStamp };
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (!source) {
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
459
|
+
/**
|
|
460
|
+
* ---------------------------
|
|
461
|
+
* Public methods
|
|
462
|
+
* ---------------------------
|
|
463
|
+
*/
|
|
918
464
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
}
|
|
465
|
+
/**
|
|
466
|
+
* Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
|
|
467
|
+
* @public
|
|
468
|
+
* @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
|
|
469
|
+
* @returns {void}
|
|
470
|
+
*/
|
|
471
|
+
setMode(mode = this.#mode) {
|
|
472
|
+
if (mode !== "2d" && mode !== "3d") {
|
|
473
|
+
console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
|
|
474
|
+
mode = this.#mode;
|
|
475
|
+
}
|
|
476
|
+
this.#mode = mode;
|
|
477
|
+
if (mode === "2d") {
|
|
478
|
+
this.#component3D?.hide();
|
|
479
|
+
this.#component2D?.show();
|
|
940
480
|
} 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
|
-
}
|
|
481
|
+
this.#component3D?.show();
|
|
482
|
+
this.#component2D?.hide();
|
|
958
483
|
}
|
|
959
|
-
|
|
960
|
-
|
|
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();
|
|
484
|
+
if (this.getAttribute("mode") !== mode) {
|
|
485
|
+
this.setAttribute("mode", mode);
|
|
1046
486
|
}
|
|
1047
487
|
}
|
|
1048
488
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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();
|
|
1151
|
-
}
|
|
1152
|
-
if (this.#checkMaterialsChanged(options)) {
|
|
1153
|
-
someSetted = someSetted || this.#setOptionsMaterials();
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
await this.#setStatusLoaded();
|
|
1157
|
-
|
|
1158
|
-
return someSetted;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Public Methods
|
|
489
|
+
/**
|
|
490
|
+
* Loads a configuration object or JSON string and adds it to the task queue.
|
|
491
|
+
* If the config contains a drawing, adds a drawing task as well.
|
|
492
|
+
* @public
|
|
493
|
+
* @param {object|string} config - Configuration object or JSON string.
|
|
494
|
+
* @returns {boolean|void} Returns false if config is invalid; otherwise void.
|
|
495
|
+
*/
|
|
1162
496
|
loadConfig(config) {
|
|
1163
497
|
config = typeof config === "string" ? JSON.parse(config) : config;
|
|
1164
498
|
if (!config) {
|
|
1165
499
|
return false;
|
|
1166
500
|
}
|
|
501
|
+
if (config.drawing) {
|
|
502
|
+
this.#addTaskToQueue(config.drawing, "drawing");
|
|
503
|
+
}
|
|
1167
504
|
this.#addTaskToQueue(config, "config");
|
|
1168
505
|
}
|
|
1169
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Loads a model object or JSON string and adds it to the task queue.
|
|
509
|
+
* @public
|
|
510
|
+
* @param {object|string} model - Model object or JSON string.
|
|
511
|
+
* @returns {boolean|void} Returns false if model is invalid; otherwise void.
|
|
512
|
+
*/
|
|
1170
513
|
loadModel(model) {
|
|
1171
514
|
model = typeof model === "string" ? JSON.parse(model) : model;
|
|
1172
515
|
if (!model) {
|
|
@@ -1175,6 +518,12 @@ class PrefViewer extends HTMLElement {
|
|
|
1175
518
|
this.#addTaskToQueue(model, "model");
|
|
1176
519
|
}
|
|
1177
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Loads a scene/environment object or JSON string and adds it to the task queue.
|
|
523
|
+
* @public
|
|
524
|
+
* @param {object|string} scene - Scene object or JSON string.
|
|
525
|
+
* @returns {boolean|void} Returns false if scene is invalid; otherwise void.
|
|
526
|
+
*/
|
|
1178
527
|
loadScene(scene) {
|
|
1179
528
|
scene = typeof scene === "string" ? JSON.parse(scene) : scene;
|
|
1180
529
|
if (!scene) {
|
|
@@ -1183,6 +532,12 @@ class PrefViewer extends HTMLElement {
|
|
|
1183
532
|
this.#addTaskToQueue(scene, "environment");
|
|
1184
533
|
}
|
|
1185
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Loads materials object or JSON string and adds it to the task queue.
|
|
537
|
+
* @public
|
|
538
|
+
* @param {object|string} materials - Materials object or JSON string.
|
|
539
|
+
* @returns {boolean|void} Returns false if materials are invalid; otherwise void.
|
|
540
|
+
*/
|
|
1186
541
|
loadMaterials(materials) {
|
|
1187
542
|
materials = typeof materials === "string" ? JSON.parse(materials) : materials;
|
|
1188
543
|
if (!materials) {
|
|
@@ -1191,6 +546,26 @@ class PrefViewer extends HTMLElement {
|
|
|
1191
546
|
this.#addTaskToQueue(materials, "materials");
|
|
1192
547
|
}
|
|
1193
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Loads a drawing object or JSON string and adds it to the task queue.
|
|
551
|
+
* @public
|
|
552
|
+
* @param {object|string} drawing - Drawing object or JSON string.
|
|
553
|
+
* @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
|
|
554
|
+
*/
|
|
555
|
+
loadDrawing(drawing) {
|
|
556
|
+
drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
|
|
557
|
+
if (!drawing) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
this.#addTaskToQueue(drawing, "drawing");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Sets viewer options from an object or JSON string and adds them to the task queue.
|
|
565
|
+
* @public
|
|
566
|
+
* @param {object|string} options - Options object or JSON string.
|
|
567
|
+
* @returns {boolean|void} Returns false if options are invalid; otherwise void.
|
|
568
|
+
*/
|
|
1194
569
|
setOptions(options) {
|
|
1195
570
|
options = typeof options === "string" ? JSON.parse(options) : options;
|
|
1196
571
|
if (!options) {
|
|
@@ -1199,55 +574,204 @@ class PrefViewer extends HTMLElement {
|
|
|
1199
574
|
this.#addTaskToQueue(options, "options");
|
|
1200
575
|
}
|
|
1201
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Shows the 3D model by setting its visibility to true.
|
|
579
|
+
* Adds a visibility task to the queue for processing.
|
|
580
|
+
* @public
|
|
581
|
+
* @returns {void}
|
|
582
|
+
*/
|
|
1202
583
|
showModel() {
|
|
1203
|
-
|
|
1204
|
-
this.#
|
|
584
|
+
const config = { model: { visible: true } };
|
|
585
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1205
586
|
}
|
|
1206
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Hides the 3D model by setting its visibility to false.
|
|
590
|
+
* Adds a visibility task to the queue for processing.
|
|
591
|
+
* @public
|
|
592
|
+
* @returns {void}
|
|
593
|
+
*/
|
|
1207
594
|
hideModel() {
|
|
1208
|
-
|
|
1209
|
-
this.#
|
|
595
|
+
const config = { model: { visible: false } };
|
|
596
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1210
597
|
}
|
|
1211
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Shows the scene/environment by setting its visibility to true.
|
|
601
|
+
* Adds a visibility task to the queue for processing.
|
|
602
|
+
* @public
|
|
603
|
+
* @returns {void}
|
|
604
|
+
*/
|
|
1212
605
|
showScene() {
|
|
1213
|
-
|
|
1214
|
-
this.#
|
|
1215
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
606
|
+
const config = { scene: { visible: true } };
|
|
607
|
+
this.#addTaskToQueue(config, "visibility");
|
|
1216
608
|
}
|
|
1217
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Hides the scene/environment by setting its visibility to false.
|
|
612
|
+
* Adds a visibility task to the queue for processing.
|
|
613
|
+
* @public
|
|
614
|
+
* @returns {void}
|
|
615
|
+
*/
|
|
1218
616
|
hideScene() {
|
|
1219
|
-
|
|
1220
|
-
this.#
|
|
1221
|
-
|
|
617
|
+
const config = { scene: { visible: false } };
|
|
618
|
+
this.#addTaskToQueue(config, "visibility");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Centers the 2D drawing view in the viewer.
|
|
623
|
+
* @public
|
|
624
|
+
* @returns {void}
|
|
625
|
+
* @description
|
|
626
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
627
|
+
*/
|
|
628
|
+
zoomCenter() {
|
|
629
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
this.#component2D.zoomCenter();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Zooms the 2D drawing to fit all content within the viewer.
|
|
637
|
+
* @public
|
|
638
|
+
* @returns {void}
|
|
639
|
+
* @description
|
|
640
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
641
|
+
*/
|
|
642
|
+
zoomExtentsAll() {
|
|
643
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.#component2D.zoomExtentsAll();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Zooms in on the 2D drawing.
|
|
651
|
+
* @public
|
|
652
|
+
* @returns {void}
|
|
653
|
+
* @description
|
|
654
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
655
|
+
*/
|
|
656
|
+
zoomIn() {
|
|
657
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
this.#component2D.zoomIn();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Zooms out of the 2D drawing.
|
|
665
|
+
* @public
|
|
666
|
+
* @returns {void}
|
|
667
|
+
* @description
|
|
668
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
669
|
+
*/
|
|
670
|
+
zoomOut() {
|
|
671
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.#component2D.zoomOut();
|
|
1222
675
|
}
|
|
1223
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Initiates download of the current 3D model in GLB format.
|
|
679
|
+
* @public
|
|
680
|
+
* @returns {void}
|
|
681
|
+
*/
|
|
1224
682
|
downloadModelGLB() {
|
|
1225
|
-
|
|
1226
|
-
|
|
683
|
+
if (!this.#component3D) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
this.#component3D.downloadModelGLB();
|
|
1227
688
|
}
|
|
1228
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Initiates download of the current 3D model in USDZ format.
|
|
692
|
+
* @public
|
|
693
|
+
* @returns {void}
|
|
694
|
+
*/
|
|
1229
695
|
downloadModelUSDZ() {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
696
|
+
if (!this.#component3D) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
this.#component3D.downloadModelUSDZ();
|
|
1236
701
|
}
|
|
1237
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Initiates download of both the 3D model and scene in USDZ format.
|
|
705
|
+
* @public
|
|
706
|
+
* @returns {void}
|
|
707
|
+
*/
|
|
1238
708
|
downloadModelAndSceneUSDZ() {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
});
|
|
709
|
+
if (!this.#component3D) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
this.#component3D.downloadModelAndSceneUSDZ();
|
|
1245
714
|
}
|
|
1246
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Initiates download of both the 3D model and scene in GLB format.
|
|
718
|
+
* @public
|
|
719
|
+
* @returns {void}
|
|
720
|
+
*/
|
|
1247
721
|
downloadModelAndSceneGLB() {
|
|
1248
|
-
|
|
1249
|
-
|
|
722
|
+
if (!this.#component3D) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.#component3D.downloadModelAndSceneGLB();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Indicates whether the viewer has been initialized.
|
|
731
|
+
* @public
|
|
732
|
+
* @returns {boolean} True if initialized; otherwise, false.
|
|
733
|
+
*/
|
|
734
|
+
get isInitialized() {
|
|
735
|
+
return this.#isInitialized;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Indicates whether the viewer has finished loading.
|
|
740
|
+
* @public
|
|
741
|
+
* @returns {boolean} True if loaded; otherwise, false.
|
|
742
|
+
*/
|
|
743
|
+
get isLoaded() {
|
|
744
|
+
return this.#isLoaded;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Indicates whether the viewer is currently loading.
|
|
749
|
+
* @public
|
|
750
|
+
* @returns {boolean} True if loading; otherwise, false.
|
|
751
|
+
*/
|
|
752
|
+
get isLoading() {
|
|
753
|
+
return this.#isLoading;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Indicates whether the viewer is currently in 2D mode.
|
|
758
|
+
* @public
|
|
759
|
+
* @returns {boolean} True if the viewer is in 2D mode; otherwise, false.
|
|
760
|
+
*/
|
|
761
|
+
get isMode2D() {
|
|
762
|
+
return this.#mode === "2d";
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Indicates whether the viewer is currently in 3D mode.
|
|
767
|
+
* @public
|
|
768
|
+
* @returns {boolean} True if the viewer is in 3D mode; otherwise, false.
|
|
769
|
+
*/
|
|
770
|
+
get isMode3D() {
|
|
771
|
+
return this.#mode === "3d";
|
|
1250
772
|
}
|
|
1251
773
|
}
|
|
1252
774
|
|
|
775
|
+
customElements.define("pref-viewer-2d", PrefViewer2D);
|
|
776
|
+
customElements.define("pref-viewer-3d", PrefViewer3D);
|
|
1253
777
|
customElements.define("pref-viewer", PrefViewer);
|