@preference-sl/pref-viewer 2.11.0-beta.0 → 2.11.0-beta.10
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 +14 -8
- package/src/babylonjs-animation-controller.js +235 -0
- package/src/babylonjs-animation-opening-menu.js +360 -0
- package/src/babylonjs-animation-opening.js +496 -0
- package/src/babylonjs-controller.js +1186 -0
- package/src/css/pref-viewer-2d.css +39 -0
- package/src/css/pref-viewer-3d.css +28 -0
- package/src/css/pref-viewer-dialog.css +105 -0
- package/src/css/pref-viewer.css +11 -0
- package/src/file-storage.js +166 -39
- package/src/gltf-resolver.js +288 -0
- package/src/index.js +721 -1057
- package/src/panzoom-controller.js +494 -0
- package/src/pref-viewer-2d.js +460 -0
- package/src/pref-viewer-3d-data.js +178 -0
- package/src/pref-viewer-3d.js +700 -0
- package/src/pref-viewer-dialog.js +139 -0
- package/src/pref-viewer-task.js +54 -0
- package/src/svg-resolver.js +281 -0
package/src/index.js
CHANGED
|
@@ -1,1253 +1,917 @@
|
|
|
1
|
+
import { PrefViewer2D } from "./pref-viewer-2d.js";
|
|
2
|
+
import { PrefViewer3D } from "./pref-viewer-3d.js";
|
|
3
|
+
import { PrefViewerDialog } from "./pref-viewer-dialog.js";
|
|
4
|
+
import { PrefViewerTask } from "./pref-viewer-task.js";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
7
|
+
* PrefViewer - Custom Web Component for advanced 2D and 3D product visualization and configuration.
|
|
8
|
+
*
|
|
9
|
+
* Overview:
|
|
10
|
+
* - Encapsulates both 2D (SVG) and 3D (Babylon.js) viewers, supporting glTF/GLB models, environments, and drawings.
|
|
11
|
+
* - Loads assets from remote URLs, Base64 data URIs, and IndexedDB sources.
|
|
12
|
+
* - Provides a unified API for loading models, scenes, drawings, materials, and configuration via attributes or methods.
|
|
13
|
+
* - Manages an internal task queue for sequential processing of viewer operations.
|
|
14
|
+
* - Emits custom events for loading, errors, and state changes to facilitate integration.
|
|
15
|
+
* - Supports downloading models and scenes in GLB and USDZ formats.
|
|
16
|
+
* - Automatically updates the viewer when reactive attributes change.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* - Use as a custom HTML element: <pref-viewer ...>
|
|
20
|
+
* - Configure via attributes (config, model, scene, materials, drawing, options, mode).
|
|
21
|
+
* - Control viewer mode, visibility, and downloads via public methods.
|
|
22
|
+
*
|
|
23
|
+
* Reactive Attributes:
|
|
24
|
+
* - config: URL or Base64 for configuration file.
|
|
25
|
+
* - model: URL or Base64 for 3D model (glTF/GLB).
|
|
26
|
+
* - scene: URL or Base64 for environment/scene (glTF/GLB).
|
|
27
|
+
* - materials: URL or Base64 for materials definition.
|
|
28
|
+
* - drawing: URL or Base64 for SVG drawing.
|
|
29
|
+
* - options: JSON string for viewer options.
|
|
30
|
+
* - mode: Viewer mode ("2d" or "3d").
|
|
5
31
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
32
|
+
* Public Methods:
|
|
33
|
+
* - loadConfig(config): Loads a configuration object or JSON string.
|
|
34
|
+
* - loadModel(model): Loads a model object or JSON string.
|
|
35
|
+
* - loadScene(scene): Loads a scene/environment object or JSON string.
|
|
36
|
+
* - loadMaterials(materials): Loads materials object or JSON string.
|
|
37
|
+
* - loadDrawing(drawing): Loads a drawing object or JSON string.
|
|
38
|
+
* - setOptions(options): Sets viewer options from an object or JSON string.
|
|
39
|
+
* - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
|
|
40
|
+
* - showModel(): Shows the 3D model.
|
|
41
|
+
* - hideModel(): Hides the 3D model.
|
|
42
|
+
* - showScene(): Shows the 3D environment/scene.
|
|
43
|
+
* - hideScene(): Hides the 3D environment/scene.
|
|
44
|
+
* - zoomCenter(): Centers the 2D drawing view.
|
|
45
|
+
* - zoomExtentsAll(): Zooms the 2D drawing to fit all content.
|
|
46
|
+
* - zoomIn(): Zooms in on the 2D drawing.
|
|
47
|
+
* - zoomOut(): Zooms out of the 2D drawing.
|
|
48
|
+
* - downloadModelGLB(): Downloads the current 3D model as a GLB file.
|
|
49
|
+
* - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
|
|
50
|
+
* - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
|
|
51
|
+
* - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
|
|
52
|
+
* - downloadModelAndSceneGLTF(): Downloads both the model and scene as a glTF ZIP file.
|
|
53
|
+
* - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
|
|
54
|
+
* - downloadSceneGLB(): Downloads the environment as a GLB file.
|
|
55
|
+
* - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
|
|
56
|
+
* - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
|
|
57
|
+
* - openDialog(title, content, footer): Opens a modal dialog with the specified title, content, and footer.
|
|
58
|
+
* - closeDialog(): Closes the currently open dialog, if any, and removes it from the DOM.
|
|
15
59
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* style="width:800px; height:600px;">
|
|
23
|
-
* </pref-viewer>
|
|
24
|
-
* ```
|
|
60
|
+
* Public Properties:
|
|
61
|
+
* - isInitialized: Indicates if the viewer is initialized.
|
|
62
|
+
* - isLoaded: Indicates if the viewer has finished loading.
|
|
63
|
+
* - isLoading: Indicates if the viewer is currently loading.
|
|
64
|
+
* - isMode2D: Indicates if the viewer is in 2D mode.
|
|
65
|
+
* - isMode3D: Indicates if the viewer is in 3D mode.
|
|
25
66
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ```
|
|
67
|
+
* Events:
|
|
68
|
+
* - "scene-loading": Dispatched when a 3D loading operation starts.
|
|
69
|
+
* - "scene-loaded": Dispatched when a 3D loading operation completes.
|
|
70
|
+
* - "drawing-loading": Dispatched when a 2D drawing loading operation starts.
|
|
71
|
+
* - "drawing-loaded": Dispatched when a 2D drawing loading operation completes.
|
|
72
|
+
* - "drawing-zoom-changed": Dispatched when the 2D drawing zoom/pan state changes.
|
|
33
73
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* style="width:800px; height:600px;">
|
|
39
|
-
* </pref-viewer>
|
|
40
|
-
* ```
|
|
74
|
+
* Notes:
|
|
75
|
+
* - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
|
|
76
|
+
* - Processes tasks sequentially to ensure consistent state.
|
|
77
|
+
* - Designed for extensibility and integration in product configurators and visualization tools.
|
|
41
78
|
*/
|
|
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
79
|
class PrefViewer extends HTMLElement {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
#isInitialized = false;
|
|
81
|
+
#isLoaded = false;
|
|
82
|
+
#isLoading = false;
|
|
83
|
+
#mode = "3d";
|
|
84
|
+
|
|
81
85
|
#taskQueue = [];
|
|
82
|
-
|
|
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
|
|
86
|
+
|
|
149
87
|
#wrapper = null;
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
#engine = null;
|
|
154
|
-
#scene = null;
|
|
155
|
-
#camera = null;
|
|
156
|
-
#hemiLight = null;
|
|
157
|
-
#dirLight = null;
|
|
158
|
-
#cameraLight = null;
|
|
159
|
-
#shadowGen = null;
|
|
160
|
-
#XRExperience = null;
|
|
88
|
+
#component2D = null;
|
|
89
|
+
#component3D = null;
|
|
90
|
+
#dialog = null;
|
|
161
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Creates a new PrefViewer instance and attaches a shadow DOM.
|
|
94
|
+
* Initializes internal state and component references.
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
162
97
|
constructor() {
|
|
163
98
|
super();
|
|
164
99
|
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
100
|
}
|
|
178
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Returns the list of attributes to observe for changes.
|
|
104
|
+
* @public
|
|
105
|
+
* @returns {string[]} Array of attribute names to observe.
|
|
106
|
+
*/
|
|
179
107
|
static get observedAttributes() {
|
|
180
|
-
return ["config", "model", "scene", "show-model", "show-scene"];
|
|
108
|
+
return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
|
|
181
109
|
}
|
|
182
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Observes changes to specific attributes and triggers corresponding actions.
|
|
113
|
+
* Loads configuration, drawing, model, scene, materials, or options when their attributes change.
|
|
114
|
+
* Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
|
|
115
|
+
* @public
|
|
116
|
+
* @param {string} name - The name of the changed attribute.
|
|
117
|
+
* @param {*} _old - The previous value of the attribute (unused).
|
|
118
|
+
* @param {*} value - The new value of the attribute.
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
183
121
|
attributeChangedCallback(name, _old, value) {
|
|
184
|
-
let data = null;
|
|
185
122
|
switch (name) {
|
|
186
123
|
case "config":
|
|
187
124
|
this.loadConfig(value);
|
|
188
125
|
break;
|
|
126
|
+
case "drawing":
|
|
127
|
+
this.loadDrawing(value);
|
|
128
|
+
break;
|
|
129
|
+
case "materials":
|
|
130
|
+
this.loadMaterials(value);
|
|
131
|
+
break;
|
|
132
|
+
case "mode":
|
|
133
|
+
if (_old === value || value.toLowerCase() === this.#mode) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.setMode(value.toLowerCase());
|
|
137
|
+
break;
|
|
189
138
|
case "model":
|
|
190
139
|
this.loadModel(value);
|
|
191
140
|
break;
|
|
192
141
|
case "scene":
|
|
193
142
|
this.loadScene(value);
|
|
194
143
|
break;
|
|
195
|
-
case "materials":
|
|
196
|
-
this.loadMaterials(value);
|
|
197
|
-
break;
|
|
198
144
|
case "options":
|
|
199
145
|
this.setOptions(value);
|
|
200
146
|
break;
|
|
201
147
|
case "show-model":
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
data ? this.showModel() : this.hideModel();
|
|
205
|
-
} else {
|
|
206
|
-
this.#data.containers.model.show = data;
|
|
148
|
+
if (_old === value) {
|
|
149
|
+
return;
|
|
207
150
|
}
|
|
151
|
+
const showModel = value.toLowerCase() === "true";
|
|
152
|
+
showModel ? this.showModel() : this.hideModel();
|
|
208
153
|
break;
|
|
209
154
|
case "show-scene":
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
data ? this.showScene() : this.hideScene();
|
|
213
|
-
} else {
|
|
214
|
-
this.#data.containers.environment.show = data;
|
|
155
|
+
if (_old === value) {
|
|
156
|
+
return;
|
|
215
157
|
}
|
|
158
|
+
const showScene = value.toLowerCase() === "true";
|
|
159
|
+
showScene ? this.showScene() : this.hideScene();
|
|
216
160
|
break;
|
|
217
161
|
}
|
|
218
162
|
}
|
|
219
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Called when the element is inserted into the DOM.
|
|
166
|
+
* Initializes the 3D and 2D viewer components and starts processing tasks.
|
|
167
|
+
* @public
|
|
168
|
+
* @returns {void|boolean} Returns false if initialization fails; otherwise void.
|
|
169
|
+
*/
|
|
220
170
|
connectedCallback() {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.dispatchEvent(
|
|
225
|
-
new CustomEvent("scene-error", {
|
|
226
|
-
bubbles: true,
|
|
227
|
-
cancelable: false,
|
|
228
|
-
composed: true,
|
|
229
|
-
detail: { error: new Error(error) },
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
171
|
+
this.#wrapper = document.createElement("div");
|
|
172
|
+
this.#wrapper.classList.add("pref-viewer-wrapper");
|
|
173
|
+
this.shadowRoot.append(this.#wrapper);
|
|
234
174
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this
|
|
238
|
-
}
|
|
175
|
+
const style = document.createElement("style");
|
|
176
|
+
style.textContent = `@import "/dist/css/pref-viewer.css";`;
|
|
177
|
+
this.shadowRoot.append(style);
|
|
239
178
|
|
|
240
|
-
|
|
241
|
-
this.#
|
|
242
|
-
this.#canvasResizeObserver.disconnect();
|
|
243
|
-
}
|
|
179
|
+
this.#createComponent3D();
|
|
180
|
+
this.#createComponent2D();
|
|
244
181
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
Object.assign(this.#canvas.style, {
|
|
249
|
-
width: "100%",
|
|
250
|
-
height: "100%",
|
|
251
|
-
display: "block",
|
|
252
|
-
outline: "none",
|
|
253
|
-
});
|
|
254
|
-
}
|
|
182
|
+
if (!this.hasAttribute("mode")) {
|
|
183
|
+
this.setMode();
|
|
184
|
+
}
|
|
255
185
|
|
|
256
|
-
|
|
257
|
-
this.#
|
|
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);
|
|
186
|
+
this.#isInitialized = true;
|
|
187
|
+
this.#processNextTask();
|
|
265
188
|
}
|
|
266
189
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
this.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
composed: true,
|
|
279
|
-
})
|
|
280
|
-
);
|
|
281
|
-
this.#engine.stopRenderLoop(this.#renderLoop);
|
|
190
|
+
/**
|
|
191
|
+
* Creates and appends the 2D viewer component to the shadow DOM.
|
|
192
|
+
* Sets the "visible" attribute to true by default.
|
|
193
|
+
* @private
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
196
|
+
#createComponent2D() {
|
|
197
|
+
this.#component2D = document.createElement("pref-viewer-2d");
|
|
198
|
+
this.#component2D.setAttribute("visible", "false");
|
|
199
|
+
this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
|
|
200
|
+
this.#wrapper.appendChild(this.#component2D);
|
|
282
201
|
}
|
|
283
202
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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();
|
|
203
|
+
/**
|
|
204
|
+
* Creates and appends the 3D viewer component to the shadow DOM.
|
|
205
|
+
* Sets the "visible" attribute to true by default.
|
|
206
|
+
* @private
|
|
207
|
+
* @returns {void}
|
|
208
|
+
*/
|
|
209
|
+
#createComponent3D() {
|
|
210
|
+
this.#component3D = document.createElement("pref-viewer-3d");
|
|
211
|
+
this.#component3D.setAttribute("visible", "false");
|
|
212
|
+
this.#wrapper.appendChild(this.#component3D);
|
|
213
|
+
}
|
|
323
214
|
|
|
324
|
-
|
|
325
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Adds a new task to the internal queue for processing.
|
|
217
|
+
* If the viewer is initialized and not currently loading, immediately processes the next task.
|
|
218
|
+
* @private
|
|
219
|
+
* @param {*} value - The payload or data for the task.
|
|
220
|
+
* @param {string} type - The type of task (see PrefViewerTask.Types).
|
|
221
|
+
* @returns {void}
|
|
222
|
+
*/
|
|
223
|
+
#addTaskToQueue(value, type) {
|
|
224
|
+
this.#taskQueue.push(new PrefViewerTask(value, type));
|
|
225
|
+
if (this.#isInitialized && !this.#isLoading) {
|
|
226
|
+
this.#processNextTask();
|
|
326
227
|
}
|
|
327
|
-
this.setAttribute("loaded", "");
|
|
328
|
-
|
|
329
|
-
this.loaded = true;
|
|
330
|
-
this.loading = false;
|
|
331
|
-
|
|
332
|
-
this.#processNextTask();
|
|
333
228
|
}
|
|
334
229
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Processes the next task in the queue, if any.
|
|
232
|
+
* Dispatches the task to the appropriate handler based on its type.
|
|
233
|
+
* @private
|
|
234
|
+
* @returns {boolean|void} Returns false if the queue is empty; otherwise void.
|
|
235
|
+
*/
|
|
236
|
+
#processNextTask() {
|
|
237
|
+
if (!this.#taskQueue.length) {
|
|
338
238
|
return false;
|
|
339
239
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
240
|
+
const task = this.#taskQueue[0];
|
|
241
|
+
this.#taskQueue.shift();
|
|
242
|
+
switch (task.type) {
|
|
243
|
+
case PrefViewerTask.Types.Config:
|
|
244
|
+
this.#processConfig(task.value);
|
|
245
|
+
break;
|
|
246
|
+
case PrefViewerTask.Types.Drawing:
|
|
247
|
+
this.#processDrawing(task.value);
|
|
248
|
+
break;
|
|
249
|
+
case PrefViewerTask.Types.Environment:
|
|
250
|
+
this.#processEnvironment(task.value);
|
|
251
|
+
break;
|
|
252
|
+
case PrefViewerTask.Types.Materials:
|
|
253
|
+
this.#processMaterials(task.value);
|
|
254
|
+
break;
|
|
255
|
+
case PrefViewerTask.Types.Model:
|
|
256
|
+
this.#processModel(task.value);
|
|
257
|
+
break;
|
|
258
|
+
case PrefViewerTask.Types.Options:
|
|
259
|
+
this.#processOptions(task.value);
|
|
260
|
+
break;
|
|
261
|
+
case PrefViewerTask.Types.Visibility:
|
|
262
|
+
this.#processVisibility(task.value);
|
|
263
|
+
break;
|
|
349
264
|
}
|
|
350
|
-
return changed;
|
|
351
265
|
}
|
|
352
266
|
|
|
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
|
-
}
|
|
267
|
+
/**
|
|
268
|
+
* Handles the start of a 3D loading operation.
|
|
269
|
+
* Updates loading state, sets attributes, and dispatches a "scene-loading" event.
|
|
270
|
+
* @private
|
|
271
|
+
* @returns {void}
|
|
272
|
+
*/
|
|
273
|
+
#on3DLoading() {
|
|
274
|
+
this.#isLoaded = false;
|
|
275
|
+
this.#isLoading = true;
|
|
375
276
|
|
|
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
|
-
}
|
|
277
|
+
this.removeAttribute("loaded-3d");
|
|
278
|
+
this.setAttribute("loading-3d", "");
|
|
385
279
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
280
|
+
const customEventOptions = {
|
|
281
|
+
bubbles: true,
|
|
282
|
+
cancelable: false,
|
|
283
|
+
composed: true,
|
|
389
284
|
};
|
|
390
|
-
|
|
391
|
-
Object.values(this.#data.options.materials).forEach(reset);
|
|
392
|
-
reset(this.#data.options.camera);
|
|
285
|
+
this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
|
|
393
286
|
}
|
|
394
287
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.#
|
|
404
|
-
|
|
405
|
-
this.#engine.runRenderLoop(this.#renderLoop);
|
|
406
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
407
|
-
}
|
|
288
|
+
/**
|
|
289
|
+
* Handles the completion of a 3D loading operation.
|
|
290
|
+
* Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
|
|
291
|
+
* @private
|
|
292
|
+
* @param {object} [detail] - Optional details to include in the event.
|
|
293
|
+
* @returns {void}
|
|
294
|
+
*/
|
|
295
|
+
#on3DLoaded(detail) {
|
|
296
|
+
this.#isLoaded = true;
|
|
297
|
+
this.#isLoading = false;
|
|
408
298
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.#scene && this.#scene.render();
|
|
412
|
-
};
|
|
299
|
+
this.removeAttribute("loading-3d");
|
|
300
|
+
this.setAttribute("loaded-3d", "");
|
|
413
301
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
302
|
+
const customEventOptions = {
|
|
303
|
+
bubbles: true,
|
|
304
|
+
cancelable: true,
|
|
305
|
+
composed: true,
|
|
306
|
+
};
|
|
307
|
+
if (detail) {
|
|
308
|
+
customEventOptions.detail = detail;
|
|
309
|
+
}
|
|
310
|
+
this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
|
|
419
311
|
}
|
|
420
312
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Handles the start of a 2D loading operation.
|
|
315
|
+
* Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
|
|
316
|
+
* @private
|
|
317
|
+
* @returns {void}
|
|
318
|
+
*/
|
|
319
|
+
#on2DLoading() {
|
|
320
|
+
this.#isLoaded = false;
|
|
321
|
+
this.#isLoading = true;
|
|
425
322
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (!sessionSupported) {
|
|
429
|
-
console.info("PrefViewer: WebXR in mode AR is not supported");
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
323
|
+
this.removeAttribute("loaded-2d");
|
|
324
|
+
this.setAttribute("loading-2d", "");
|
|
432
325
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
}
|
|
326
|
+
const customEventOptions = {
|
|
327
|
+
bubbles: true,
|
|
328
|
+
cancelable: true,
|
|
329
|
+
composed: true,
|
|
330
|
+
};
|
|
331
|
+
this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
|
|
468
332
|
}
|
|
469
333
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.#
|
|
479
|
-
this.#
|
|
480
|
-
this.#scene.activeCamera = this.#camera;
|
|
481
|
-
}
|
|
334
|
+
/**
|
|
335
|
+
* Handles the completion of a 2D loading operation.
|
|
336
|
+
* Updates loading state, sets attributes, dispatches a "drawing-loaded" event, and processes the next task.
|
|
337
|
+
* @private
|
|
338
|
+
* @param {object} [detail] - Optional details to include in the event.
|
|
339
|
+
* @returns {void}
|
|
340
|
+
*/
|
|
341
|
+
#on2DLoaded(detail) {
|
|
342
|
+
this.#isLoaded = true;
|
|
343
|
+
this.#isLoading = false;
|
|
482
344
|
|
|
483
|
-
|
|
484
|
-
this
|
|
345
|
+
this.removeAttribute("loading-2d");
|
|
346
|
+
this.setAttribute("loaded-2d", "");
|
|
485
347
|
|
|
486
|
-
|
|
487
|
-
|
|
348
|
+
const customEventOptions = {
|
|
349
|
+
bubbles: true,
|
|
350
|
+
cancelable: true,
|
|
351
|
+
composed: true,
|
|
352
|
+
};
|
|
353
|
+
if (detail) {
|
|
354
|
+
customEventOptions.detail = detail;
|
|
488
355
|
}
|
|
489
|
-
|
|
490
|
-
// 1) Stronger ambient fill
|
|
491
|
-
this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
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;
|
|
356
|
+
this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
|
|
509
357
|
}
|
|
510
358
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Handles the "drawing-zoom-changed" event from the 2D viewer component.
|
|
361
|
+
* Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
|
|
362
|
+
* @private
|
|
363
|
+
* @param {CustomEvent} event - The original zoom change event from the 2D viewer.
|
|
364
|
+
* @returns {void}
|
|
365
|
+
*/
|
|
366
|
+
#on2DZoomChanged(event) {
|
|
367
|
+
event.stopPropagation();
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
const customEventOptions = {
|
|
370
|
+
bubbles: true,
|
|
371
|
+
cancelable: true,
|
|
372
|
+
composed: true,
|
|
373
|
+
detail: event.detail,
|
|
374
|
+
};
|
|
375
|
+
this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
|
|
522
376
|
}
|
|
523
377
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Processes a configuration object by loading it into the 3D component.
|
|
380
|
+
* Dispatches loading events and processes the next task when finished.
|
|
381
|
+
* @private
|
|
382
|
+
* @param {object} config - The configuration object to process.
|
|
383
|
+
* @returns {void}
|
|
384
|
+
*/
|
|
385
|
+
#processConfig(config) {
|
|
386
|
+
if (!this.#component3D) {
|
|
387
|
+
return;
|
|
527
388
|
}
|
|
528
389
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
{
|
|
534
|
-
resolutionExp: 7,
|
|
535
|
-
sampleDirections: 2,
|
|
536
|
-
ssShadowsEnabled: true,
|
|
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;
|
|
553
|
-
};
|
|
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);
|
|
390
|
+
this.#on3DLoading();
|
|
391
|
+
this.#component3D.load(config).then((detail) => {
|
|
392
|
+
this.#on3DLoaded(detail);
|
|
393
|
+
this.#processNextTask();
|
|
567
394
|
});
|
|
568
395
|
}
|
|
569
396
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Processes a drawing object by loading it into the 2D component.
|
|
399
|
+
* Processes the next task when finished.
|
|
400
|
+
* @private
|
|
401
|
+
* @param {object} drawing - The drawing object to process.
|
|
402
|
+
* @returns {void}
|
|
403
|
+
*/
|
|
404
|
+
#processDrawing(drawing) {
|
|
405
|
+
if (!this.#component2D) {
|
|
406
|
+
return;
|
|
574
407
|
}
|
|
575
408
|
|
|
576
|
-
this.#
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
mesh.receiveShadows = true;
|
|
581
|
-
if (!mesh.name === "hdri") {
|
|
582
|
-
this.#shadowGen.addShadowCaster(mesh, true);
|
|
583
|
-
}
|
|
409
|
+
this.#on2DLoading();
|
|
410
|
+
this.#component2D.load(drawing).then((detail) => {
|
|
411
|
+
this.#on2DLoaded(detail);
|
|
412
|
+
this.#processNextTask();
|
|
584
413
|
});
|
|
585
414
|
}
|
|
586
415
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Processes an environment (scene) object by wrapping it in a config and loading it.
|
|
418
|
+
* @private
|
|
419
|
+
* @param {object} environment - The environment/scene object to process.
|
|
420
|
+
* @returns {void}
|
|
421
|
+
*/
|
|
422
|
+
#processEnvironment(environment) {
|
|
423
|
+
const config = {};
|
|
424
|
+
config.scene = environment;
|
|
425
|
+
this.#processConfig(config);
|
|
597
426
|
}
|
|
598
427
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
event.preventDefault();
|
|
610
|
-
});
|
|
428
|
+
/**
|
|
429
|
+
* Processes a materials object by wrapping it in a config and loading it.
|
|
430
|
+
* @private
|
|
431
|
+
* @param {object} materials - The materials object to process.
|
|
432
|
+
* @returns {void}
|
|
433
|
+
*/
|
|
434
|
+
#processMaterials(materials) {
|
|
435
|
+
const config = {};
|
|
436
|
+
config.materials = materials;
|
|
437
|
+
this.#processConfig(config);
|
|
611
438
|
}
|
|
612
439
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Processes a model object by wrapping it in a config and loading it.
|
|
442
|
+
* @private
|
|
443
|
+
* @param {object} model - The model object to process.
|
|
444
|
+
* @returns {void}
|
|
445
|
+
*/
|
|
446
|
+
#processModel(model) {
|
|
447
|
+
const config = {};
|
|
448
|
+
config.model = model;
|
|
449
|
+
this.#processConfig(config);
|
|
619
450
|
}
|
|
620
451
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
} else {
|
|
633
|
-
resolve([0, null]);
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
xhr.onerror = () => {
|
|
637
|
-
resolve([0, null]);
|
|
638
|
-
};
|
|
639
|
-
xhr.send();
|
|
640
|
-
});
|
|
641
|
-
}
|
|
452
|
+
/**
|
|
453
|
+
* Processes viewer options by loading them into the 3D component.
|
|
454
|
+
* Dispatches loading events and processes the next task.
|
|
455
|
+
* @private
|
|
456
|
+
* @param {object} options - The options object to process.
|
|
457
|
+
* @returns {void}
|
|
458
|
+
*/
|
|
459
|
+
#processOptions(options) {
|
|
460
|
+
if (!this.#component3D) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
642
463
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
464
|
+
this.#on3DLoading();
|
|
465
|
+
const detail = this.#component3D.setOptions(options);
|
|
466
|
+
this.#on3DLoaded(detail);
|
|
467
|
+
this.#processNextTask();
|
|
647
468
|
}
|
|
648
469
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Processes visibility configuration for the model and scene.
|
|
472
|
+
* Shows or hides the model and/or scene based on the config, then processes the next task.
|
|
473
|
+
* @private
|
|
474
|
+
* @param {object} config - The visibility configuration object.
|
|
475
|
+
* @returns {void}
|
|
476
|
+
*/
|
|
477
|
+
#processVisibility(config) {
|
|
478
|
+
if (!this.#component3D) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const showModel = config.model?.visible;
|
|
482
|
+
const showScene = config.scene?.visible;
|
|
483
|
+
if (showModel !== undefined) {
|
|
484
|
+
showModel ? this.#component3D.showModel() : this.#component3D.hideModel();
|
|
660
485
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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 };
|
|
486
|
+
if (showScene !== undefined) {
|
|
487
|
+
showScene ? this.#component3D.showEnvironment() : this.#component3D.hideEnvironment();
|
|
488
|
+
}
|
|
489
|
+
this.#processNextTask();
|
|
671
490
|
}
|
|
672
491
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
492
|
+
/**
|
|
493
|
+
* ---------------------------
|
|
494
|
+
* Public methods
|
|
495
|
+
* ---------------------------
|
|
496
|
+
*/
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
|
|
500
|
+
* @public
|
|
501
|
+
* @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
|
|
502
|
+
* @returns {void}
|
|
503
|
+
*/
|
|
504
|
+
setMode(mode = this.#mode) {
|
|
505
|
+
if (mode !== "2d" && mode !== "3d") {
|
|
506
|
+
console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
|
|
507
|
+
mode = this.#mode;
|
|
508
|
+
}
|
|
509
|
+
this.#mode = mode;
|
|
510
|
+
if (mode === "2d") {
|
|
511
|
+
this.#component3D?.hide();
|
|
512
|
+
this.#component2D?.show();
|
|
513
|
+
} else {
|
|
514
|
+
this.#component2D?.hide();
|
|
515
|
+
this.#component3D?.show();
|
|
516
|
+
}
|
|
517
|
+
if (this.getAttribute("mode") !== mode) {
|
|
518
|
+
this.setAttribute("mode", mode);
|
|
519
|
+
if (this.#dialog) {
|
|
520
|
+
this.closeDialog();
|
|
521
|
+
}
|
|
676
522
|
}
|
|
677
|
-
await initDb(db, table);
|
|
678
523
|
}
|
|
679
524
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Loads a configuration object or JSON string and adds it to the task queue.
|
|
527
|
+
* If the config contains a drawing, adds a drawing task as well.
|
|
528
|
+
* @public
|
|
529
|
+
* @param {object|string} config - Configuration object or JSON string.
|
|
530
|
+
* @returns {boolean|void} Returns false if config is invalid; otherwise void.
|
|
531
|
+
*/
|
|
532
|
+
loadConfig(config) {
|
|
533
|
+
config = typeof config === "string" ? JSON.parse(config) : config;
|
|
534
|
+
if (!config) {
|
|
683
535
|
return false;
|
|
684
536
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
537
|
+
if (config.drawing) {
|
|
538
|
+
this.#addTaskToQueue(config.drawing, "drawing");
|
|
539
|
+
}
|
|
540
|
+
this.#addTaskToQueue(config, "config");
|
|
688
541
|
}
|
|
689
542
|
|
|
690
|
-
|
|
691
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Loads a model object or JSON string and adds it to the task queue.
|
|
545
|
+
* @public
|
|
546
|
+
* @param {object|string} model - Model object or JSON string.
|
|
547
|
+
* @returns {boolean|void} Returns false if model is invalid; otherwise void.
|
|
548
|
+
*/
|
|
549
|
+
loadModel(model) {
|
|
550
|
+
model = typeof model === "string" ? JSON.parse(model) : model;
|
|
551
|
+
if (!model) {
|
|
692
552
|
return false;
|
|
693
553
|
}
|
|
554
|
+
this.#addTaskToQueue(model, "model");
|
|
555
|
+
}
|
|
694
556
|
|
|
695
|
-
|
|
696
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Loads a scene/environment object or JSON string and adds it to the task queue.
|
|
559
|
+
* @public
|
|
560
|
+
* @param {object|string} scene - Scene object or JSON string.
|
|
561
|
+
* @returns {boolean|void} Returns false if scene is invalid; otherwise void.
|
|
562
|
+
*/
|
|
563
|
+
loadScene(scene) {
|
|
564
|
+
scene = typeof scene === "string" ? JSON.parse(scene) : scene;
|
|
565
|
+
if (!scene) {
|
|
697
566
|
return false;
|
|
698
567
|
}
|
|
568
|
+
this.#addTaskToQueue(scene, "environment");
|
|
569
|
+
}
|
|
699
570
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Loads materials object or JSON string and adds it to the task queue.
|
|
573
|
+
* @public
|
|
574
|
+
* @param {object|string} materials - Materials object or JSON string.
|
|
575
|
+
* @returns {boolean|void} Returns false if materials are invalid; otherwise void.
|
|
576
|
+
*/
|
|
577
|
+
loadMaterials(materials) {
|
|
578
|
+
materials = typeof materials === "string" ? JSON.parse(materials) : materials;
|
|
579
|
+
if (!materials) {
|
|
708
580
|
return false;
|
|
709
581
|
}
|
|
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;
|
|
582
|
+
this.#addTaskToQueue(materials, "materials");
|
|
738
583
|
}
|
|
739
584
|
|
|
740
|
-
|
|
741
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Loads a drawing object or JSON string and adds it to the task queue.
|
|
587
|
+
* @public
|
|
588
|
+
* @param {object|string} drawing - Drawing object or JSON string.
|
|
589
|
+
* @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
|
|
590
|
+
*/
|
|
591
|
+
loadDrawing(drawing) {
|
|
592
|
+
drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
|
|
593
|
+
if (!drawing) {
|
|
742
594
|
return false;
|
|
743
595
|
}
|
|
744
|
-
|
|
745
|
-
let camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.value) || null;
|
|
746
|
-
if (!camera) {
|
|
747
|
-
if (this.#data.options.camera.changed.value && this.#data.options.camera.changed.value !== this.#data.options.camera.value) {
|
|
748
|
-
camera = this.#data.containers.model.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || this.#data.containers.environment.assetContainer?.cameras.find((thisCamera) => thisCamera.name === this.#data.options.camera.changed.value) || null;
|
|
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;
|
|
596
|
+
this.#addTaskToQueue(drawing, "drawing");
|
|
772
597
|
}
|
|
773
598
|
|
|
774
|
-
|
|
775
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Sets viewer options from an object or JSON string and adds them to the task queue.
|
|
601
|
+
* @public
|
|
602
|
+
* @param {object|string} options - Options object or JSON string.
|
|
603
|
+
* @returns {boolean|void} Returns false if options are invalid; otherwise void.
|
|
604
|
+
*/
|
|
605
|
+
setOptions(options) {
|
|
606
|
+
options = typeof options === "string" ? JSON.parse(options) : options;
|
|
607
|
+
if (!options) {
|
|
776
608
|
return false;
|
|
777
609
|
}
|
|
610
|
+
this.#addTaskToQueue(options, "options");
|
|
611
|
+
}
|
|
778
612
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Shows the 3D model by setting its visibility to true.
|
|
615
|
+
* Adds a visibility task to the queue for processing.
|
|
616
|
+
* @public
|
|
617
|
+
* @returns {void}
|
|
618
|
+
*/
|
|
619
|
+
showModel() {
|
|
620
|
+
const config = { model: { visible: true } };
|
|
621
|
+
this.#addTaskToQueue(config, "visibility");
|
|
782
622
|
}
|
|
783
623
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Hides the 3D model by setting its visibility to false.
|
|
626
|
+
* Adds a visibility task to the queue for processing.
|
|
627
|
+
* @public
|
|
628
|
+
* @returns {void}
|
|
629
|
+
*/
|
|
630
|
+
hideModel() {
|
|
631
|
+
const config = { model: { visible: false } };
|
|
632
|
+
this.#addTaskToQueue(config, "visibility");
|
|
633
|
+
}
|
|
788
634
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
635
|
+
/**
|
|
636
|
+
* Shows the scene/environment by setting its visibility to true.
|
|
637
|
+
* Adds a visibility task to the queue for processing.
|
|
638
|
+
* @public
|
|
639
|
+
* @returns {void}
|
|
640
|
+
*/
|
|
641
|
+
showScene() {
|
|
642
|
+
const config = { scene: { visible: true } };
|
|
643
|
+
this.#addTaskToQueue(config, "visibility");
|
|
792
644
|
}
|
|
793
645
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
this.#
|
|
803
|
-
return true;
|
|
646
|
+
/**
|
|
647
|
+
* Hides the scene/environment by setting its visibility to false.
|
|
648
|
+
* Adds a visibility task to the queue for processing.
|
|
649
|
+
* @public
|
|
650
|
+
* @returns {void}
|
|
651
|
+
*/
|
|
652
|
+
hideScene() {
|
|
653
|
+
const config = { scene: { visible: false } };
|
|
654
|
+
this.#addTaskToQueue(config, "visibility");
|
|
804
655
|
}
|
|
805
656
|
|
|
806
657
|
/**
|
|
807
|
-
*
|
|
808
|
-
* @
|
|
809
|
-
* @
|
|
810
|
-
* @returns {Promise<void>} Resolves when all applicable URIs have been resolved/replaced.
|
|
658
|
+
* Centers the 2D drawing view in the viewer.
|
|
659
|
+
* @public
|
|
660
|
+
* @returns {void}
|
|
811
661
|
* @description
|
|
812
|
-
*
|
|
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}
|
|
662
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
821
663
|
*/
|
|
822
|
-
|
|
823
|
-
if (!
|
|
664
|
+
zoomCenter() {
|
|
665
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
824
666
|
return;
|
|
825
667
|
}
|
|
826
|
-
|
|
827
|
-
let sceneURLBase = assetContainerURL;
|
|
828
|
-
|
|
829
|
-
if (typeof assetContainerURL === "string") {
|
|
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));
|
|
879
|
-
}
|
|
880
|
-
if (assetContainerJSON.images) {
|
|
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);
|
|
668
|
+
this.#component2D.zoomCenter();
|
|
892
669
|
}
|
|
893
670
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
}
|
|
671
|
+
/**
|
|
672
|
+
* Zooms the 2D drawing to fit all content within the viewer.
|
|
673
|
+
* @public
|
|
674
|
+
* @returns {void}
|
|
675
|
+
* @description
|
|
676
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
677
|
+
*/
|
|
678
|
+
zoomExtentsAll() {
|
|
679
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
680
|
+
return;
|
|
913
681
|
}
|
|
682
|
+
this.#component2D.zoomExtentsAll();
|
|
683
|
+
}
|
|
914
684
|
|
|
915
|
-
|
|
916
|
-
|
|
685
|
+
/**
|
|
686
|
+
* Zooms in on the 2D drawing.
|
|
687
|
+
* @public
|
|
688
|
+
* @returns {void}
|
|
689
|
+
* @description
|
|
690
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
691
|
+
*/
|
|
692
|
+
zoomIn() {
|
|
693
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
694
|
+
return;
|
|
917
695
|
}
|
|
696
|
+
this.#component2D.zoomIn();
|
|
697
|
+
}
|
|
918
698
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
}
|
|
940
|
-
} else {
|
|
941
|
-
const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
942
|
-
extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
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
|
-
}
|
|
699
|
+
/**
|
|
700
|
+
* Zooms out of the 2D drawing.
|
|
701
|
+
* @public
|
|
702
|
+
* @returns {void}
|
|
703
|
+
* @description
|
|
704
|
+
* Only works when the viewer is in 2D mode. Pending implementation for 3D mode when active camera is not blocked.
|
|
705
|
+
*/
|
|
706
|
+
zoomOut() {
|
|
707
|
+
if (!this.#component2D || this.#mode !== "2d") {
|
|
708
|
+
return;
|
|
958
709
|
}
|
|
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);
|
|
710
|
+
this.#component2D.zoomOut();
|
|
974
711
|
}
|
|
975
712
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
}
|
|
713
|
+
/**
|
|
714
|
+
* Initiates download of the current 3D model in GLB format.
|
|
715
|
+
* @public
|
|
716
|
+
* @returns {void}
|
|
717
|
+
*/
|
|
718
|
+
downloadModelGLB() {
|
|
719
|
+
if (!this.#component3D) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
1017
722
|
|
|
1018
|
-
|
|
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
|
-
});
|
|
723
|
+
this.#component3D.downloadModelGLB();
|
|
1039
724
|
}
|
|
1040
725
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
726
|
+
/**
|
|
727
|
+
* Initiates download of the current 3D model in GLTF format.
|
|
728
|
+
* @public
|
|
729
|
+
* @returns {void}
|
|
730
|
+
*/
|
|
731
|
+
downloadModelGLTF() {
|
|
732
|
+
if (!this.#component3D) {
|
|
733
|
+
return;
|
|
1046
734
|
}
|
|
1047
|
-
}
|
|
1048
735
|
|
|
1049
|
-
|
|
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
|
-
}
|
|
736
|
+
this.#component3D.downloadModelGLTF();
|
|
1072
737
|
}
|
|
1073
738
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
this.#
|
|
1081
|
-
|
|
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);
|
|
739
|
+
/**
|
|
740
|
+
* Initiates download of the current 3D model in USDZ format.
|
|
741
|
+
* @public
|
|
742
|
+
* @returns {void}
|
|
743
|
+
*/
|
|
744
|
+
downloadModelUSDZ() {
|
|
745
|
+
if (!this.#component3D) {
|
|
746
|
+
return;
|
|
1102
747
|
}
|
|
1103
748
|
|
|
1104
|
-
this.#
|
|
749
|
+
this.#component3D.downloadModelUSDZ();
|
|
1105
750
|
}
|
|
1106
751
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
this.#
|
|
1114
|
-
|
|
1115
|
-
|
|
752
|
+
/**
|
|
753
|
+
* Initiates download of the current complete scene (3D model and environment) in GLB format.
|
|
754
|
+
* @public
|
|
755
|
+
* @returns {void}
|
|
756
|
+
*/
|
|
757
|
+
downloadModelAndSceneGLB() {
|
|
758
|
+
if (!this.#component3D) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
1116
761
|
|
|
1117
|
-
this.
|
|
762
|
+
this.#component3D.downloadModelAndSceneGLB();
|
|
1118
763
|
}
|
|
1119
764
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
this.#
|
|
1127
|
-
|
|
1128
|
-
|
|
765
|
+
/**
|
|
766
|
+
* Initiates download of the current complete scene (3D model and environment) in GLTF format.
|
|
767
|
+
* @public
|
|
768
|
+
* @returns {void}
|
|
769
|
+
*/
|
|
770
|
+
downloadModelAndSceneGLTF() {
|
|
771
|
+
if (!this.#component3D) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
1129
774
|
|
|
1130
|
-
this.#
|
|
775
|
+
this.#component3D.downloadModelAndSceneGLTF();
|
|
1131
776
|
}
|
|
1132
777
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
this.#
|
|
1140
|
-
|
|
778
|
+
/**
|
|
779
|
+
* Initiates download of the current complete scene (3D model and environment) in USDZ format.
|
|
780
|
+
* @public
|
|
781
|
+
* @returns {void}
|
|
782
|
+
*/
|
|
783
|
+
downloadModelAndSceneUSDZ() {
|
|
784
|
+
if (!this.#component3D) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
1141
787
|
|
|
1142
|
-
this.#
|
|
788
|
+
this.#component3D.downloadModelAndSceneUSDZ();
|
|
1143
789
|
}
|
|
1144
790
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
someSetted = someSetted || this.#setOptionsMaterials();
|
|
791
|
+
/**
|
|
792
|
+
* Initiates download of the current 3D environment in GLB format.
|
|
793
|
+
* @public
|
|
794
|
+
* @returns {void}
|
|
795
|
+
*/
|
|
796
|
+
downloadSceneGLB() {
|
|
797
|
+
if (!this.#component3D) {
|
|
798
|
+
return;
|
|
1154
799
|
}
|
|
1155
800
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
return someSetted;
|
|
801
|
+
this.#component3D.downloadSceneGLB();
|
|
1159
802
|
}
|
|
1160
803
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
804
|
+
/**
|
|
805
|
+
* Initiates download of the current 3D environment in GLTF format.
|
|
806
|
+
* @public
|
|
807
|
+
* @returns {void}
|
|
808
|
+
*/
|
|
809
|
+
downloadSceneGLTF() {
|
|
810
|
+
if (!this.#component3D) {
|
|
811
|
+
return;
|
|
1166
812
|
}
|
|
1167
|
-
this.#addTaskToQueue(config, "config");
|
|
1168
|
-
}
|
|
1169
813
|
|
|
1170
|
-
|
|
1171
|
-
model = typeof model === "string" ? JSON.parse(model) : model;
|
|
1172
|
-
if (!model) {
|
|
1173
|
-
return false;
|
|
1174
|
-
}
|
|
1175
|
-
this.#addTaskToQueue(model, "model");
|
|
814
|
+
this.#component3D.downloadSceneGLTF();
|
|
1176
815
|
}
|
|
1177
816
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Initiates download of the current 3D environment in USDZ format.
|
|
819
|
+
* @public
|
|
820
|
+
* @returns {void}
|
|
821
|
+
*/
|
|
822
|
+
downloadSceneUSDZ() {
|
|
823
|
+
if (!this.#component3D) {
|
|
824
|
+
return;
|
|
1182
825
|
}
|
|
1183
|
-
this.#
|
|
826
|
+
this.#component3D.downloadSceneUSDZ();
|
|
1184
827
|
}
|
|
1185
828
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
829
|
+
/**
|
|
830
|
+
* Opens a modal dialog with the specified title, content, and footer.
|
|
831
|
+
* @public
|
|
832
|
+
* @param {string} title - The dialog title to display in the header.
|
|
833
|
+
* @param {string} content - The HTML content to display in the dialog body.
|
|
834
|
+
* @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
|
|
835
|
+
* @returns {HTMLElement} The created dialog element.
|
|
836
|
+
* @description
|
|
837
|
+
* If a dialog is already open, it is closed before opening the new one.
|
|
838
|
+
* The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
|
|
839
|
+
*/
|
|
840
|
+
openDialog(title, content, footer) {
|
|
841
|
+
if (this.#dialog && this.#dialog.hasAttribute("open")) {
|
|
842
|
+
this.#dialog.close();
|
|
1190
843
|
}
|
|
1191
|
-
this.#
|
|
844
|
+
this.#dialog = document.createElement("pref-viewer-dialog");
|
|
845
|
+
this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
|
|
846
|
+
const opened = this.#dialog.open(title, content, footer);
|
|
847
|
+
return opened ? this.#dialog : null;
|
|
1192
848
|
}
|
|
1193
849
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Closes the currently open dialog, if any, and removes it from the DOM.
|
|
852
|
+
* @public
|
|
853
|
+
* @returns {void}
|
|
854
|
+
*/
|
|
855
|
+
closeDialog() {
|
|
856
|
+
if (this.#dialog) {
|
|
857
|
+
this.#dialog.close();
|
|
858
|
+
this.#dialog = null;
|
|
1198
859
|
}
|
|
1199
|
-
this.#addTaskToQueue(options, "options");
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
showModel() {
|
|
1203
|
-
this.#data.containers.model.show = true;
|
|
1204
|
-
this.#addContainer(this.#data.containers.model);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
hideModel() {
|
|
1208
|
-
this.#data.containers.model.show = false;
|
|
1209
|
-
this.#removeContainer(this.#data.containers.model);
|
|
1210
860
|
}
|
|
1211
861
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
862
|
+
/**
|
|
863
|
+
* ---------------------------
|
|
864
|
+
* Public properties
|
|
865
|
+
* ---------------------------
|
|
866
|
+
*/
|
|
1217
867
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Indicates whether the viewer has been initialized.
|
|
870
|
+
* @public
|
|
871
|
+
* @returns {boolean} True if initialized; otherwise, false.
|
|
872
|
+
*/
|
|
873
|
+
get isInitialized() {
|
|
874
|
+
return this.#isInitialized;
|
|
1222
875
|
}
|
|
1223
876
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
877
|
+
/**
|
|
878
|
+
* Indicates whether the viewer has finished loading.
|
|
879
|
+
* @public
|
|
880
|
+
* @returns {boolean} True if loaded; otherwise, false.
|
|
881
|
+
*/
|
|
882
|
+
get isLoaded() {
|
|
883
|
+
return this.#isLoaded;
|
|
1227
884
|
}
|
|
1228
885
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
886
|
+
/**
|
|
887
|
+
* Indicates whether the viewer is currently loading.
|
|
888
|
+
* @public
|
|
889
|
+
* @returns {boolean} True if loading; otherwise, false.
|
|
890
|
+
*/
|
|
891
|
+
get isLoading() {
|
|
892
|
+
return this.#isLoading;
|
|
1236
893
|
}
|
|
1237
894
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Indicates whether the viewer is currently in 2D mode.
|
|
897
|
+
* @public
|
|
898
|
+
* @returns {boolean} True if the viewer is in 2D mode; otherwise, false.
|
|
899
|
+
*/
|
|
900
|
+
get isMode2D() {
|
|
901
|
+
return this.#mode === "2d";
|
|
1245
902
|
}
|
|
1246
903
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
904
|
+
/**
|
|
905
|
+
* Indicates whether the viewer is currently in 3D mode.
|
|
906
|
+
* @public
|
|
907
|
+
* @returns {boolean} True if the viewer is in 3D mode; otherwise, false.
|
|
908
|
+
*/
|
|
909
|
+
get isMode3D() {
|
|
910
|
+
return this.#mode === "3d";
|
|
1250
911
|
}
|
|
1251
912
|
}
|
|
1252
913
|
|
|
914
|
+
customElements.define("pref-viewer-2d", PrefViewer2D);
|
|
915
|
+
customElements.define("pref-viewer-3d", PrefViewer3D);
|
|
916
|
+
customElements.define("pref-viewer-dialog", PrefViewerDialog);
|
|
1253
917
|
customElements.define("pref-viewer", PrefViewer);
|