@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.
@@ -0,0 +1,635 @@
1
+ import { ContainerData, MaterialData, CameraData } from "./pref-viewer-3d-data.js";
2
+ import BabylonJSController from "./babylonjs-controller.js";
3
+
4
+ /**
5
+ * PrefViewer3D - Custom Web Component for interactive 3D visualization and configuration.
6
+ *
7
+ * Overview:
8
+ * - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
9
+ * - Manages internal state for containers (model, environment, materials) and options (camera, materials).
10
+ * - Handles asset loading, configuration, and option updates through attributes and public methods.
11
+ * - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
12
+ * - Emits custom events for loading, loaded, and option-setting states.
13
+ *
14
+ * Usage:
15
+ * - Use as a custom HTML element: <pref-viewer-3d ...>
16
+ * - Configure via attributes (config, options, show-model, show-scene, visible).
17
+ * - Control viewer state and assets via public methods.
18
+ *
19
+ * Observed Attributes:
20
+ * - show-model: Show or hide the 3D model ("true"/"false").
21
+ * - show-scene: Show or hide the 3D environment ("true"/"false").
22
+ * - visible: Show or hide the entire viewer ("true"/"false").
23
+ *
24
+ * Public Methods:
25
+ * - show(): Shows the 3D viewer component.
26
+ * - hide(): Hides the 3D viewer component.
27
+ * - load(config): Loads the provided configuration into the viewer.
28
+ * - setOptions(options): Sets viewer options such as camera and materials.
29
+ * - showModel(): Shows the 3D model.
30
+ * - hideModel(): Hides the 3D model.
31
+ * - showEnvironment(): Shows the 3D environment/scene.
32
+ * - hideEnvironment(): Hides the 3D environment/scene.
33
+ * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
34
+ * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
35
+ * - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
36
+ * - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
37
+ *
38
+ * Public Properties:
39
+ * - isInitialized: Indicates whether the component has completed initialization.
40
+ * - isLoaded: Indicates whether the GLTF/GLB content is loaded and ready.
41
+ * - isVisible: Indicates whether the component is currently visible.
42
+ *
43
+ * Events:
44
+ * - "scene-loading": Dispatched when a loading operation starts.
45
+ * - "scene-loaded": Dispatched when a loading operation completes.
46
+ * - "scene-setting-options": Dispatched when viewer options are being set.
47
+ * - "scene-set-options": Dispatched when viewer options have been set.
48
+ *
49
+ * Notes:
50
+ * - Internal state and heavy initialization are handled in connectedCallback.
51
+ * - Designed for extensibility and integration in product configurators and visualization tools.
52
+ * - All resource management and rendering operations are performed asynchronously for performance.
53
+ */
54
+ export class PrefViewer3D extends HTMLElement {
55
+ // State flags
56
+ #isInitialized = false;
57
+ #isLoaded = false;
58
+ #isVisible = false;
59
+
60
+ #wrapper = null;
61
+ #canvas = null;
62
+
63
+ #data = null; // Component data
64
+
65
+ #babylonJSController = null; // BabylonJSController instance
66
+
67
+ /**
68
+ * Create a new instance of the 3D viewer component.
69
+ * The constructor only calls super(); heavy initialization happens in connectedCallback.
70
+ */
71
+ constructor() {
72
+ super();
73
+ }
74
+
75
+ /**
76
+ * Attributes observed by the custom element.
77
+ * @returns {string[]} Array of attribute names that trigger attributeChangedCallback.
78
+ */
79
+ static get observedAttributes() {
80
+ return ["show-model", "show-scene", "visible"];
81
+ }
82
+
83
+ /**
84
+ * Handles changes to observed attributes and updates the component state accordingly.
85
+ * @param {string} name - The name of the changed attribute.
86
+ * @param {string|null} _old - The previous value of the attribute.
87
+ * @param {string|null} value - The new value of the attribute.
88
+ * @returns {void}
89
+ * @description
90
+ * - "show-model": shows or hides the model according to the attribute value ("true"/"false").
91
+ * - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
92
+ * - "visible": shows or hides the component according to the attribute value ("true"/"false").
93
+ */
94
+ attributeChangedCallback(name, _old, value) {
95
+ switch (name) {
96
+ case "show-model":
97
+ if (_old === value) {
98
+ return;
99
+ }
100
+ const showModel = value.toLowerCase?.() === "true";
101
+ showModel ? this.showModel() : this.hideModel();
102
+ break;
103
+ case "show-scene":
104
+ if (_old === value) {
105
+ return;
106
+ }
107
+ const showScene = value.toLowerCase?.() === "true";
108
+ showScene ? this.showEnvironment() : this.hideEnvironment();
109
+ break;
110
+ case "visible":
111
+ if (_old === value) {
112
+ return;
113
+ }
114
+ if (value === "true" && this.#isVisible === false) {
115
+ this.show();
116
+ } else if (value === "false" && this.#isVisible === true) {
117
+ this.hide();
118
+ }
119
+ break;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Called when the element is added to the DOM.
125
+ * Creates the DOM structure, sets initial visibility, initializes component data, and sets up the BabylonJSController.
126
+ * Performs heavy initialization tasks required for the 3D viewer.
127
+ * @returns {void}
128
+ */
129
+ connectedCallback() {
130
+ this.#createDOMElements();
131
+ this.#setInitialVisibility();
132
+ this.#initializeData();
133
+ this.#initializeBabylonJS();
134
+ }
135
+
136
+ /**
137
+ * Called when the element is removed from the DOM.
138
+ * Disables the BabylonJSController to clean up resources and event listeners associated with the 3D viewer.
139
+ * @returns {void}
140
+ */
141
+ disconnectedCallback() {
142
+ if (this.#babylonJSController) {
143
+ this.#babylonJSController.disable();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Create DOM structure and basic styles used by the component.
149
+ * @private
150
+ * @returns {void}
151
+ */
152
+ #createDOMElements() {
153
+ this.#wrapper = document.createElement("div");
154
+ this.append(this.#wrapper);
155
+ this.#canvas = document.createElement("canvas");
156
+ this.#wrapper.appendChild(this.#canvas);
157
+ const style = document.createElement("style");
158
+ style.textContent = `pref-viewer-3d[visible="true"] { display: block; } pref-viewer-3d[visible="false"] { display: none; } pref-viewer-3d, pref-viewer-3d > div, pref-viewer-3d > div > canvas { width: 100%; height: 100%; display: block; position: relative; outline: none; box-sizing: border-box; }`;
159
+ this.append(style);
160
+ }
161
+
162
+ /**
163
+ * Set initial visibility based on inline style and the visible attribute.
164
+ * @private
165
+ * @returns {void}
166
+ * @description If the "visible" attribute is not present, defaults to visible.
167
+ */
168
+ #setInitialVisibility() {
169
+ const visible = !this.hasAttribute("visible") || this.getAttribute("visible") === "true";
170
+ visible ? this.show() : this.hide();
171
+ }
172
+
173
+ /**
174
+ * Initializes the internal data structure for containers and options used by the 3D viewer.
175
+ * Sets up default states for model, environment, materials, camera, and material options.
176
+ * @private
177
+ * @returns {void}
178
+ */
179
+ #initializeData() {
180
+ this.#data = {
181
+ containers: {
182
+ model: new ContainerData("model"),
183
+ environment: new ContainerData("environment"),
184
+ materials: new ContainerData("materials"),
185
+ },
186
+ options: {
187
+ camera: new CameraData("activeCamera", null, true),
188
+ materials: {
189
+ innerWall: new MaterialData("innerWall", undefined, undefined, ["innerWall"]),
190
+ outerWall: new MaterialData("outerWall", undefined, undefined, ["outerWall"]),
191
+ innerFloor: new MaterialData("innerFloor", undefined, undefined, ["innerFloor"]),
192
+ outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
193
+ },
194
+ },
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Initializes the BabylonJSController and enables the Babylon.js engine for rendering.
200
+ * @private
201
+ * @returns {void}
202
+ */
203
+ #initializeBabylonJS() {
204
+ this.#babylonJSController = new BabylonJSController(this.#canvas, this.#data.containers, this.#data.options);
205
+ this.#babylonJSController.enable();
206
+ }
207
+
208
+ /**
209
+ * Resets update flags for all containers and material/camera options after loading or setting options.
210
+ * @private
211
+ * @returns {void}
212
+ */
213
+ #resetUpdateFlags() {
214
+ Object.values(this.#data.containers).forEach((container) => container.reset());
215
+ Object.values(this.#data.options.materials).forEach((material) => material.reset());
216
+ this.#data.options.camera.reset();
217
+ }
218
+
219
+ /**
220
+ * Checks if any container (model, environment, materials) needs to be updated based on the provided config.
221
+ * Sets pending state for containers if storage or visibility changes are detected.
222
+ * @private
223
+ * @param {object} config - Configuration object containing model, scene, and materials info.
224
+ * @returns {void}
225
+ */
226
+ #checkNeedToUpdateContainers(config) {
227
+ const modelState = this.#data.containers.model;
228
+ const modelHasStorage = !!config.model?.storage;
229
+ const modelNeedToChangeVisibility = config.model?.visible !== undefined && config.model?.visible !== modelState.show;
230
+ if (modelHasStorage) {
231
+ modelState.setPending(modelHasStorage ? config.model.storage : null, modelNeedToChangeVisibility ? config.model.visible : undefined);
232
+ } else if (modelNeedToChangeVisibility) {
233
+ modelState.show = config.model.visible;
234
+ }
235
+
236
+ const environmentState = this.#data.containers.environment;
237
+ const environmentHasStorage = !!config.scene?.storage;
238
+ const environmentNeedToChangeVisibility = config.scene?.visible !== undefined && config.scene?.visible !== environmentState.show;
239
+ if (environmentHasStorage) {
240
+ environmentState.setPending(environmentHasStorage ? config.scene.storage : null, environmentNeedToChangeVisibility ? config.scene.visible : undefined);
241
+ } else if (environmentNeedToChangeVisibility) {
242
+ environmentState.show = config.scene.visible;
243
+ }
244
+
245
+ // Materials container. Visibility is not established for this container because it does not contain geometry.
246
+ const materialsState = this.#data.containers.materials;
247
+ const materialsHasStorage = !!config.materials?.storage;
248
+ if (materialsHasStorage) {
249
+ materialsState.setPending(materialsHasStorage ? config.materials.storage : null, null);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Checks if the camera option needs to be updated based on the provided options.
255
+ * Sets pending state for the camera if a change is detected.
256
+ * @private
257
+ * @param {object} options - Options object containing camera info.
258
+ * @returns {boolean} True if camera needs update, false otherwise.
259
+ */
260
+ #checkNeedToUpdateCamera(options) {
261
+ if (!options || options.camera === undefined || options.camera === "") {
262
+ return false;
263
+ }
264
+ const cameraState = this.#data.options.camera;
265
+ const needUpdate = options.camera !== cameraState.value;
266
+ if (needUpdate) {
267
+ cameraState.setPending(options.camera);
268
+ }
269
+ return needUpdate;
270
+ }
271
+
272
+ /**
273
+ * Checks if any material option needs to be updated based on the provided options.
274
+ * Sets pending state for materials if changes are detected.
275
+ * @private
276
+ * @param {object} options - Options object containing material info.
277
+ * @returns {boolean} True if any material needs update, false otherwise.
278
+ */
279
+ #checkNeedToUpdateMaterials(options) {
280
+ if (!options) {
281
+ return false;
282
+ }
283
+ let someNeedUpdate = false;
284
+ Object.keys(this.#data.options.materials).forEach((material) => {
285
+ const key = `${material}Material`;
286
+ const materialState = this.#data.options.materials[material];
287
+ const previousValue = materialState.value;
288
+ const incomingValue = options[key];
289
+ const needUpdate = !!incomingValue && incomingValue !== previousValue;
290
+ if (needUpdate) {
291
+ materialState.setPending(incomingValue);
292
+ }
293
+ someNeedUpdate = someNeedUpdate || needUpdate;
294
+ });
295
+ return someNeedUpdate;
296
+ }
297
+
298
+ /**
299
+ * Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
300
+ * Used internally when a loading operation starts.
301
+ * @private
302
+ * @returns {void}
303
+ */
304
+ #onLoading() {
305
+ const customEventOptions = {
306
+ bubbles: true,
307
+ cancelable: false,
308
+ composed: true,
309
+ };
310
+ this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
311
+
312
+ this.removeAttribute("loaded");
313
+ this.setAttribute("loading", "");
314
+
315
+ if (this.#isLoaded === true) {
316
+ this.#isLoaded = false;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Dispatches a "prefviewer3d-loaded" event with details about what was loaded and updates state attributes.
322
+ * Used internally when a loading operation completes.
323
+ * @private
324
+ * @returns {object} Detail object describing what was tried and what succeeded.
325
+ */
326
+ #onLoaded() {
327
+ const toLoadDetail = {
328
+ container_model: this.#data.containers.model.isPending,
329
+ container_scene: this.#data.containers.environment.isPending,
330
+ container_materials: this.#data.containers.materials.isPending,
331
+ options_camera: this.#data.options.camera.isPending,
332
+ options_material_innerWall: this.#data.options.materials.innerWall.isPending,
333
+ options_material_outerWall: this.#data.options.materials.outerWall.isPending,
334
+ options_material_innerFloor: this.#data.options.materials.innerFloor.isPending,
335
+ options_material_outerFloor: this.#data.options.materials.outerFloor.isPending,
336
+ };
337
+ const loadedDetail = {
338
+ container_model: this.#data.containers.model.isSuccess,
339
+ container_scene: this.#data.containers.environment.isSuccess,
340
+ container_materials: this.#data.containers.materials.isSuccess,
341
+ options_camera: this.#data.options.camera.isSuccess,
342
+ options_material_innerWall: this.#data.options.materials.innerWall.isSuccess,
343
+ options_material_outerWall: this.#data.options.materials.outerWall.isSuccess,
344
+ options_material_innerFloor: this.#data.options.materials.innerFloor.isSuccess,
345
+ options_material_outerFloor: this.#data.options.materials.outerFloor.isSuccess,
346
+ };
347
+ const detail = {
348
+ tried: toLoadDetail,
349
+ success: loadedDetail,
350
+ };
351
+
352
+ const customEventOptions = {
353
+ bubbles: true,
354
+ cancelable: false,
355
+ composed: true,
356
+ detail: detail,
357
+ };
358
+ this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
359
+
360
+ this.removeAttribute("loading");
361
+ this.setAttribute("loaded", "");
362
+
363
+ this.#resetUpdateFlags();
364
+ this.#isLoaded = true;
365
+
366
+ return detail;
367
+ }
368
+
369
+ /**
370
+ * Dispatches a "prefviewer3d-setting-options" event and updates loading state attributes.
371
+ * Used internally when options are being set.
372
+ * @private
373
+ * @returns {void}
374
+ */
375
+ #onSettingOptions() {
376
+ const customEventOptions = {
377
+ bubbles: true,
378
+ cancelable: false,
379
+ composed: true,
380
+ };
381
+ this.dispatchEvent(new CustomEvent("scene-setting-options", customEventOptions));
382
+
383
+ this.removeAttribute("loaded");
384
+ this.setAttribute("loading", "");
385
+
386
+ if (this.#isLoaded === true) {
387
+ this.#isLoaded = false;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Dispatches a "prefviewer3d-loaded" event with details about what options were set and updates state attributes.
393
+ * Used internally when options have been set.
394
+ * @private
395
+ * @returns {object} Detail object describing what was tried and what succeeded.
396
+ */
397
+ #onSetOptions() {
398
+ const toSetDetail = {
399
+ camera: this.#data.options.camera.isPending,
400
+ innerWallMaterial: this.#data.options.materials.innerWall.isPending,
401
+ outerWallMaterial: this.#data.options.materials.outerWall.isPending,
402
+ innerFloorMaterial: this.#data.options.materials.innerFloor.isPending,
403
+ outerFloorMaterial: this.#data.options.materials.outerFloor.isPending,
404
+ };
405
+ const settedDetail = {
406
+ camera: this.#data.options.camera.isSuccess,
407
+ innerWallMaterial: this.#data.options.materials.innerWall.isSuccess,
408
+ outerWallMaterial: this.#data.options.materials.outerWall.isSuccess,
409
+ innerFloorMaterial: this.#data.options.materials.innerFloor.isSuccess,
410
+ outerFloorMaterial: this.#data.options.materials.outerFloor.isSuccess,
411
+ };
412
+ const detail = {
413
+ tried: toSetDetail,
414
+ success: settedDetail,
415
+ };
416
+
417
+ const customEventOptions = {
418
+ bubbles: true,
419
+ cancelable: false,
420
+ composed: true,
421
+ detail: detail,
422
+ };
423
+ this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions));
424
+
425
+ this.removeAttribute("loading");
426
+ this.setAttribute("loaded", "");
427
+
428
+ this.#resetUpdateFlags();
429
+ this.#isLoaded = true;
430
+
431
+ return detail;
432
+ }
433
+
434
+ /**
435
+ * ---------------------------
436
+ * Public methods
437
+ * ---------------------------
438
+ */
439
+
440
+ /**
441
+ * Hides the 3D viewer component by updating its visibility state and DOM attribute.
442
+ * @public
443
+ * @returns {void}
444
+ */
445
+ hide() {
446
+ this.#isVisible = false;
447
+ this.setAttribute("visible", "false");
448
+ }
449
+
450
+ /**
451
+ * Shows the 3D viewer component by updating its visibility state and DOM attribute.
452
+ * @public
453
+ * @returns {void}
454
+ */
455
+ show() {
456
+ this.#isVisible = true;
457
+ this.setAttribute("visible", "true");
458
+ }
459
+
460
+ /**
461
+ * Loads the provided configuration into the 3D viewer.
462
+ * Updates containers and options, triggers loading events, and returns the loading result.
463
+ * @public
464
+ * @param {object} config - Configuration object containing containers and options.
465
+ * @returns {Promise<object>} Resolves with an object containing success and error status and loading details.
466
+ */
467
+ async load(config) {
468
+ if (!this.#babylonJSController) {
469
+ return;
470
+ }
471
+
472
+ this.#onLoading();
473
+
474
+ // Containers
475
+ this.#checkNeedToUpdateContainers(config);
476
+
477
+ // Options
478
+ if (config.options) {
479
+ this.#checkNeedToUpdateCamera(config.options);
480
+ this.#checkNeedToUpdateMaterials(config.options);
481
+ }
482
+
483
+ const loadDetail = await this.#babylonJSController.load();
484
+
485
+ return { ...loadDetail, load: this.#onLoaded() };
486
+ }
487
+
488
+ /**
489
+ * Sets viewer options such as camera and materials.
490
+ * Updates internal states, triggers option setting events, and returns the result.
491
+ * @public
492
+ * @param {object} options - Options object containing camera and material settings.
493
+ * @returns {object} Object containing success status and details of set options.
494
+ */
495
+ setOptions(options) {
496
+ if (!this.#babylonJSController) {
497
+ return;
498
+ }
499
+
500
+ this.#onSettingOptions();
501
+
502
+ let someSetted = false;
503
+ if (this.#checkNeedToUpdateCamera(options)) {
504
+ someSetted = someSetted || this.#babylonJSController.setCameraOptions();
505
+ }
506
+ if (this.#checkNeedToUpdateMaterials(options)) {
507
+ someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
508
+ }
509
+ const detail = this.#onSetOptions();
510
+ return { success: someSetted, detail: detail };
511
+ }
512
+
513
+ /**
514
+ * Shows the 3D model within the viewer.
515
+ * @public
516
+ * @returns {void}
517
+ */
518
+ showModel() {
519
+ if (!this.#babylonJSController) {
520
+ return;
521
+ }
522
+ this.#babylonJSController.setContainerVisibility("model", true);
523
+ }
524
+
525
+ /**
526
+ * Hides the 3D model within the viewer.
527
+ * @public
528
+ * @returns {void}
529
+ */
530
+ hideModel() {
531
+ if (!this.#babylonJSController) {
532
+ return;
533
+ }
534
+ this.#babylonJSController.setContainerVisibility("model", false);
535
+ }
536
+
537
+ /**
538
+ * Shows the 3D environment/scene within the viewer.
539
+ * @public
540
+ * @returns {void}
541
+ */
542
+ showEnvironment() {
543
+ if (!this.#babylonJSController) {
544
+ return;
545
+ }
546
+ this.#babylonJSController.setContainerVisibility("environment", true);
547
+ }
548
+
549
+ /**
550
+ * Hides the 3D environment/scene within the viewer.
551
+ * @public
552
+ * @returns {void}
553
+ */
554
+ hideEnvironment() {
555
+ if (!this.#babylonJSController) {
556
+ return;
557
+ }
558
+ this.#babylonJSController.setContainerVisibility("environment", false);
559
+ }
560
+
561
+ /**
562
+ * Downloads the current 3D model as a GLB file.
563
+ * @public
564
+ * @returns {void}
565
+ */
566
+ downloadModelGLB() {
567
+ if (!this.#babylonJSController) {
568
+ return;
569
+ }
570
+ this.#babylonJSController.downloadModelGLB();
571
+ }
572
+
573
+ /**
574
+ * Downloads the current 3D model as a USDZ file.
575
+ * @public
576
+ * @returns {void}
577
+ */
578
+ downloadModelUSDZ() {
579
+ if (!this.#babylonJSController) {
580
+ return;
581
+ }
582
+ this.#babylonJSController.downloadModelUSDZ();
583
+ }
584
+
585
+ /**
586
+ * Downloads both the 3D model and scene as a USDZ file.
587
+ * @public
588
+ * @returns {void}
589
+ */
590
+ downloadModelAndSceneUSDZ() {
591
+ if (!this.#babylonJSController) {
592
+ return;
593
+ }
594
+ this.#babylonJSController.downloadModelAndSceneUSDZ();
595
+ }
596
+
597
+ /**
598
+ * Downloads both the 3D model and scene as a GLB file.
599
+ * @public
600
+ * @returns {void}
601
+ */
602
+ downloadModelAndSceneGLB() {
603
+ if (!this.#babylonJSController) {
604
+ return;
605
+ }
606
+ this.#babylonJSController.downloadModelAndSceneGLB();
607
+ }
608
+
609
+ /**
610
+ * Indicates whether the component has completed its initialization.
611
+ * @public
612
+ * @returns {boolean} True if the component is initialized; otherwise, false.
613
+ */
614
+ get isInitialized() {
615
+ return this.#isInitialized;
616
+ }
617
+
618
+ /**
619
+ * Indicates whether the GLTF/GLB content is loaded and ready.
620
+ * @public
621
+ * @returns {boolean} True if the GLTF/GLB is loaded; otherwise, false.
622
+ */
623
+ get isLoaded() {
624
+ return this.#isLoaded;
625
+ }
626
+
627
+ /**
628
+ * Indicates whether the component is currently visible.
629
+ * @public
630
+ * @returns {boolean} True if the component is visible; otherwise, false.
631
+ */
632
+ get isVisible() {
633
+ return this.#isVisible;
634
+ }
635
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * PrefViewerTask - Represents a single task or operation to be processed by the PrefViewer.
3
+ *
4
+ * Responsibilities:
5
+ * - Encapsulates the payload and type of a viewer task (e.g., loading a model, updating materials).
6
+ * - Validates the task type against a predefined set of allowed types.
7
+ * - Provides an immutable instance to ensure task integrity.
8
+ *
9
+ * Usage:
10
+ * - Instantiate with a value and a type:
11
+ * new PrefViewerTask(data, PrefViewerTask.Types.Model)
12
+ * - Access the payload via the `value` property and the normalized type via the `type` property.
13
+ *
14
+ * Static Properties:
15
+ * - Types: An object containing all allowed task types as string constants.
16
+ *
17
+ * Constructor:
18
+ * - Validates the type (case-insensitive) and throws a TypeError if invalid.
19
+ * - Freezes the instance to prevent further modification.
20
+ */
21
+ export class PrefViewerTask {
22
+ static Types = Object.freeze({
23
+ Config: "config",
24
+ Drawing: "drawing",
25
+ Environment: "environment",
26
+ Materials: "materials",
27
+ Model: "model",
28
+ Options: "options",
29
+ Visibility: "visibility",
30
+ });
31
+
32
+ /**
33
+ * Creates a new PrefViewerTask instance.
34
+ * Validates that the provided type matches one of the allowed task types (case-insensitive).
35
+ * If the type is invalid, throws a TypeError.
36
+ * The instance is frozen to prevent further modification.
37
+ *
38
+ * @param {*} value - The payload or data associated with the task.
39
+ * @param {string} type - The type of task; must match one of PrefViewerTask.Types values.
40
+ * @throws {TypeError} If the type is not valid.
41
+ */
42
+ constructor(value, type) {
43
+ this.value = value;
44
+
45
+ const t = typeof type === "string" ? type.toLowerCase() : String(type).toLowerCase();
46
+ const allowed = Object.values(PrefViewerTask.Types);
47
+ if (!allowed.includes(t)) {
48
+ throw new TypeError(`PrefViewerTask: invalid type "${type}". Allowed types: ${allowed.join(", ")}`);
49
+ }
50
+ this.type = t;
51
+
52
+ Object.freeze(this);
53
+ }
54
+ }