@preference-sl/pref-viewer 2.16.2 → 2.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,6 +130,12 @@ export default class FileStorage {
130
130
  #maxQuotaEvictionRetries = 5; // max retries after quota error
131
131
  #lastCleanupAt = 0; // timestamp of last automatic cleanup run
132
132
 
133
+ /**
134
+ * Configures the file-cache database and object store; opening is deferred until storage is used.
135
+ * @public
136
+ * @param {string} [dbName="FilesDB"] - IndexedDB cache database name.
137
+ * @param {string} [osName="FilesObjectStore"] - Object store for cached files.
138
+ */
133
139
  constructor(dbName = "FilesDB", osName = "FilesObjectStore") {
134
140
  this.dbName = dbName; // database name
135
141
  this.osName = osName; // object store name used for file cache
@@ -447,6 +453,7 @@ export default class FileStorage {
447
453
  * @private
448
454
  * @param {IDBDatabase} db IndexedDB database instance
449
455
  * @param {String} osName Object store name
456
+ * @returns {void}
450
457
  */
451
458
  #createObjectStore(db, osName) {
452
459
  if (!db.objectStoreNames.contains(osName)) {
@@ -466,6 +473,12 @@ export default class FileStorage {
466
473
  const self = this;
467
474
  try {
468
475
  return await openDB(this.dbName, this.#dbVersion, {
476
+ /**
477
+ * Creates the configured cache object store during an IndexedDB schema upgrade.
478
+ * @private
479
+ * @param {IDBDatabase} db - Database being upgraded.
480
+ * @returns {void}
481
+ */
469
482
  upgrade(db) {
470
483
  self.#createObjectStore(db, self.osName);
471
484
  },
@@ -7,6 +7,12 @@ PC.db = PC.db ?? null;
7
7
  PC.dbName = PC.dbName ?? null;
8
8
  PC.storeName = PC.storeName ?? null;
9
9
 
10
+ /**
11
+ * Returns the active IndexedDB connection, throwing if initDb has not initialized it.
12
+ * @private
13
+ * @returns {IDBDatabase}
14
+ * @throws {Error} If initDb has not established a database connection.
15
+ */
10
16
  function _getDbOrThrow() {
11
17
  const db = PC.db;
12
18
  if (!db) throw new Error("Database not initialized. Call initDb(...) first.");
@@ -14,6 +20,13 @@ function _getDbOrThrow() {
14
20
  }
15
21
 
16
22
  // --- internal helpers -------------------------------------------------------
23
+ /**
24
+ * Opens database version 5 and creates the model store and expiration index during upgrade.
25
+ * @private
26
+ * @param {string} dbName - IndexedDB database name.
27
+ * @param {string} storeName - Object store name.
28
+ * @returns {Promise<IDBDatabase>}
29
+ */
17
30
  function _openEnsuringStore(dbName, storeName) {
18
31
  return new Promise((resolve, reject) => {
19
32
  const open = indexedDB.open(dbName, 5);
@@ -41,6 +54,12 @@ function _openEnsuringStore(dbName, storeName) {
41
54
  });
42
55
  }
43
56
 
57
+ /**
58
+ * Identifies missing-store or closing-connection errors that allow one database reinitialization.
59
+ * @private
60
+ * @param {unknown} err - Error raised by an IndexedDB operation.
61
+ * @returns {boolean}
62
+ */
44
63
  function _isRecoverableDbError(err) {
45
64
  const msg = String(err?.message ?? err ?? "");
46
65
  if (!msg) return false;
@@ -50,6 +69,12 @@ function _isRecoverableDbError(err) {
50
69
  msg.includes("NotFoundError");
51
70
  }
52
71
 
72
+ /**
73
+ * Executes a database operation and retries once after reopening a recoverable connection.
74
+ * @private
75
+ * @param {function(IDBDatabase): Promise<*>} work - Operation using the active database.
76
+ * @returns {Promise<*>}
77
+ */
53
78
  async function _withDbRetry(work) {
54
79
  try {
55
80
  return await work(_getDbOrThrow());
@@ -65,6 +90,13 @@ async function _withDbRetry(work) {
65
90
  // --- public API -------------------------------------------------------------
66
91
 
67
92
  // Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
93
+ /**
94
+ * Opens the model database, replaces the previous connection and closes on version changes.
95
+ * @public
96
+ * @param {string} dbName - IndexedDB database name.
97
+ * @param {string} storeName - Object store to create on upgrade.
98
+ * @returns {Promise<void>}
99
+ */
68
100
  export async function initDb(dbName, storeName) {
69
101
  PC.dbName = dbName;
70
102
  PC.storeName = storeName;
@@ -83,6 +115,14 @@ export async function initDb(dbName, storeName) {
83
115
  }
84
116
 
85
117
  // Guardar modelo
118
+ /**
119
+ * Parses a JSON model record and stores it with its base64 data length as the size.
120
+ * @public
121
+ * @param {string} modelDataStr - JSON record containing id, data and optional metadata or timestamps.
122
+ * @param {string} storeName - Object store receiving the record.
123
+ * @returns {Promise<void>}
124
+ * @throws {Error} Rejects when JSON parsing or the IndexedDB request fails.
125
+ */
86
126
  export async function saveModel(modelDataStr, storeName) {
87
127
  return _withDbRetry((db) => new Promise((resolve, reject) => {
88
128
  let modelData;
@@ -105,6 +145,13 @@ export async function saveModel(modelDataStr, storeName) {
105
145
  }
106
146
 
107
147
  // Cargar modelo
148
+ /**
149
+ * Reads a complete model record by its IndexedDB key, returning null when absent.
150
+ * @public
151
+ * @param {IDBValidKey} modelId - Stored model key.
152
+ * @param {string} storeName - Object store containing the record.
153
+ * @returns {Promise<object|null>}
154
+ */
108
155
  export function loadModel(modelId, storeName) {
109
156
  return _withDbRetry((db) => new Promise((resolve, reject) => {
110
157
  const tx = db.transaction([storeName], "readonly");
@@ -117,6 +164,14 @@ export function loadModel(modelId, storeName) {
117
164
  }
118
165
 
119
166
  // Descargar archivo desde base64
167
+ /**
168
+ * Triggers a browser download using a base64 data URI and removes the temporary link.
169
+ * @public
170
+ * @param {string} fileName - Suggested download filename.
171
+ * @param {string} bytesBase64 - Base64-encoded file contents.
172
+ * @param {string} mimeType - MIME type used in the data URI.
173
+ * @returns {void}
174
+ */
120
175
  export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
121
176
  const link = document.createElement("a");
122
177
  link.download = fileName;
@@ -127,6 +182,12 @@ export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
127
182
  }
128
183
 
129
184
  // Obtener todos los modelos (solo metadata)
185
+ /**
186
+ * Returns JSON metadata for all models, excluding their stored data payloads.
187
+ * @public
188
+ * @param {string} storeName - Object store to read.
189
+ * @returns {Promise<string>}
190
+ */
130
191
  export async function getAllModels(storeName) {
131
192
  return _withDbRetry((db) => new Promise((resolve, reject) => {
132
193
  const tx = db.transaction([storeName], "readonly");
@@ -149,6 +210,13 @@ export async function getAllModels(storeName) {
149
210
  }
150
211
 
151
212
  // Eliminar modelo por id
213
+ /**
214
+ * Deletes the record with the requested key.
215
+ * @public
216
+ * @param {IDBValidKey} modelId - Stored model key.
217
+ * @param {string} storeName - Object store to update.
218
+ * @returns {Promise<void>}
219
+ */
152
220
  export async function deleteModel(modelId, storeName) {
153
221
  return _withDbRetry((db) => new Promise((resolve, reject) => {
154
222
  const tx = db.transaction([storeName], "readwrite");
@@ -161,6 +229,12 @@ export async function deleteModel(modelId, storeName) {
161
229
  }
162
230
 
163
231
  // Limpiar toda la store
232
+ /**
233
+ * Deletes every model record in the requested object store.
234
+ * @public
235
+ * @param {string} storeName - Object store to clear.
236
+ * @returns {Promise<void>}
237
+ */
164
238
  export async function clearAll(storeName) {
165
239
  return _withDbRetry((db) => new Promise((resolve, reject) => {
166
240
  const tx = db.transaction([storeName], "readwrite");
@@ -173,6 +247,12 @@ export async function clearAll(storeName) {
173
247
  }
174
248
 
175
249
  // Borrar modelos expirados usando el índice "expirationTimeStamp"
250
+ /**
251
+ * Deletes records whose expirationTimeStamp is at or before the current time and waits for transaction completion.
252
+ * @public
253
+ * @param {string} storeName - Object store with an expirationTimeStamp index.
254
+ * @returns {Promise<void>}
255
+ */
176
256
  export async function cleanExpiredModels(storeName) {
177
257
  return _withDbRetry((db) => new Promise((resolve, reject) => {
178
258
  const tx = db.transaction([storeName], "readwrite");
@@ -198,6 +278,11 @@ export async function cleanExpiredModels(storeName) {
198
278
  }
199
279
 
200
280
  // Utilidades opcionales y visibles (por si las quieres usar en consola)
281
+ /**
282
+ * Closes the active database connection and clears the shared handle.
283
+ * @public
284
+ * @returns {void}
285
+ */
201
286
  export function closeDb() {
202
287
  if (PC.db) {
203
288
  try { PC.db.close(); } catch { }
@@ -205,6 +290,12 @@ export function closeDb() {
205
290
  }
206
291
  }
207
292
 
293
+ /**
294
+ * Closes the matching active connection and deletes the database, rejecting blocked or failed requests.
295
+ * @public
296
+ * @param {string} dbName - IndexedDB database to delete.
297
+ * @returns {Promise<void>}
298
+ */
208
299
  export function deleteDatabase(dbName) {
209
300
  return new Promise((resolve, reject) => {
210
301
  // Close current handle if points to this DB
@@ -220,6 +311,12 @@ export function deleteDatabase(dbName) {
220
311
  }
221
312
 
222
313
  // Attach a frozen, public API (no private state)
314
+ /**
315
+ * Attaches the frozen storage API to the shared PrefConfigurator namespace.
316
+ * @private
317
+ * @param {typeof globalThis} global - Global object exposing the initialized namespace.
318
+ * @returns {void}
319
+ */
223
320
  (function attachPublicAPI(global) {
224
321
  const storage = {
225
322
  initDb,
@@ -1,94 +1,167 @@
1
1
  import translations from "./translations.js";
2
2
 
3
- export const DEFAULT_LOCALE = "en-EN";
3
+ /**
4
+ * Shared localization service with encapsulated locale and subscriber state.
5
+ * Static methods preserve the single locale shared by all viewer components.
6
+ * @class I18n
7
+ */
8
+ export default class I18n {
9
+ static DEFAULT_LOCALE = "en-EN";
10
+ static SUPPORTED_LOCALES = Object.keys(translations);
11
+ static #listeners = new Set();
12
+ static #currentLocale = I18n.DEFAULT_LOCALE;
4
13
 
5
- export const SUPPORTED_LOCALES = Object.keys(translations);
6
- const listeners = new Set();
7
- let currentLocale = DEFAULT_LOCALE;
8
-
9
- const findLocaleKey = (localeId) => {
10
- if (typeof localeId !== "string") {
11
- return DEFAULT_LOCALE;
14
+ /**
15
+ * Matches a supported locale without case sensitivity and falls back to DEFAULT_LOCALE.
16
+ * @private
17
+ * @static
18
+ * @param {unknown} localeId - Requested locale identifier.
19
+ * @returns {string}
20
+ */
21
+ static #findLocaleKey(localeId) {
22
+ if (typeof localeId !== "string") {
23
+ return I18n.DEFAULT_LOCALE;
24
+ }
25
+ const normalized = localeId.trim().toLowerCase();
26
+ const match = I18n.SUPPORTED_LOCALES.find((locale) => locale.toLowerCase() === normalized);
27
+ return match ?? I18n.DEFAULT_LOCALE;
12
28
  }
13
- const normalized = localeId.trim().toLowerCase();
14
- const match = SUPPORTED_LOCALES.find((locale) => locale.toLowerCase() === normalized);
15
- return match ?? DEFAULT_LOCALE;
16
- };
17
-
18
- const getBundle = (localeId = currentLocale) => {
19
- const key = findLocaleKey(localeId);
20
- return translations[key] ?? translations[DEFAULT_LOCALE];
21
- };
22
-
23
- const resolveValue = (bundle, parts) => parts.reduce((acc, part) => (
24
- acc && Object.prototype.hasOwnProperty.call(acc, part) ? acc[part] : undefined
25
- ), bundle);
26
29
 
27
- export const availableLocales = () => [...SUPPORTED_LOCALES];
30
+ /**
31
+ * Returns the requested translation bundle or the default bundle.
32
+ * @private
33
+ * @static
34
+ * @param {string} [localeId=currentLocale] - Locale to resolve.
35
+ * @returns {object}
36
+ */
37
+ static #getBundle(localeId = I18n.#currentLocale) {
38
+ const key = I18n.#findLocaleKey(localeId);
39
+ return translations[key] ?? translations[I18n.DEFAULT_LOCALE];
40
+ }
28
41
 
29
- export const getLocale = () => currentLocale;
42
+ /**
43
+ * Traverses own translation properties, returning undefined when a path is absent.
44
+ * @private
45
+ * @static
46
+ * @param {object} bundle - Translation bundle.
47
+ * @param {string[]} parts - Nested translation-key segments.
48
+ * @returns {*}
49
+ */
50
+ static #resolveValue(bundle, parts) {
51
+ return parts.reduce((acc, part) => (
52
+ acc && Object.prototype.hasOwnProperty.call(acc, part) ? acc[part] : undefined
53
+ ), bundle);
54
+ }
30
55
 
31
- export const resolveLocale = (localeId) => findLocaleKey(localeId);
56
+ /**
57
+ * Returns a copy of the supported locale identifiers.
58
+ * @public
59
+ * @static
60
+ * @returns {string[]}
61
+ */
62
+ static availableLocales() {
63
+ return [...I18n.SUPPORTED_LOCALES];
64
+ }
32
65
 
33
- export const setLocale = (localeId) => {
34
- const resolved = findLocaleKey(localeId);
35
- if (resolved === currentLocale) {
36
- return currentLocale;
66
+ /**
67
+ * Returns the active shared locale.
68
+ * @public
69
+ * @static
70
+ * @returns {string}
71
+ */
72
+ static getLocale() {
73
+ return I18n.#currentLocale;
37
74
  }
38
- currentLocale = resolved;
39
- listeners.forEach((listener) => {
40
- try {
41
- listener(currentLocale);
42
- } catch (error) {
43
- console.warn("PrefViewer: i18n listener failed", error);
44
- }
45
- });
46
- return currentLocale;
47
- };
48
75
 
49
- export const onLocaleChange = (listener) => {
50
- if (typeof listener !== "function") {
51
- return () => {};
76
+ /**
77
+ * Resolves a requested locale to a supported identifier or the default locale.
78
+ * @public
79
+ * @static
80
+ * @param {unknown} localeId - Requested locale.
81
+ * @returns {string}
82
+ */
83
+ static resolveLocale(localeId) {
84
+ return I18n.#findLocaleKey(localeId);
52
85
  }
53
- listeners.add(listener);
54
- return () => listeners.delete(listener);
55
- };
56
86
 
57
- export const translate = (key, options = {}) => {
58
- if (!key) {
59
- return "";
87
+ /**
88
+ * Updates the shared locale and notifies subscribers only when it changes; subscriber errors are logged.
89
+ * @public
90
+ * @static
91
+ * @param {unknown} localeId - Requested locale.
92
+ * @returns {string}
93
+ */
94
+ static setLocale(localeId) {
95
+ const resolved = I18n.#findLocaleKey(localeId);
96
+ if (resolved === I18n.#currentLocale) {
97
+ return I18n.#currentLocale;
98
+ }
99
+ I18n.#currentLocale = resolved;
100
+ I18n.#listeners.forEach((listener) => {
101
+ try {
102
+ listener(I18n.#currentLocale);
103
+ } catch (error) {
104
+ console.warn("PrefViewer: i18n listener failed", error);
105
+ }
106
+ });
107
+ return I18n.#currentLocale;
60
108
  }
61
- const { locale, fallback } = options;
62
- const parts = Array.isArray(key) ? key : key.split(".");
63
- const bundle = getBundle(locale);
64
- let value = resolveValue(bundle, parts);
65
- if (value !== undefined) {
66
- return value;
109
+
110
+ /**
111
+ * Registers a locale-change subscriber and returns its unsubscribe function.
112
+ * @public
113
+ * @static
114
+ * @param {function(string): void} listener - Callback receiving the resolved locale.
115
+ * @returns {function(): void}
116
+ */
117
+ static onLocaleChange(listener) {
118
+ if (typeof listener !== "function") {
119
+ return () => {};
120
+ }
121
+ I18n.#listeners.add(listener);
122
+ return () => I18n.#listeners.delete(listener);
67
123
  }
68
- if (bundle !== translations[DEFAULT_LOCALE]) {
69
- value = resolveValue(translations[DEFAULT_LOCALE], parts);
124
+
125
+ /**
126
+ * Resolves a dotted or segmented key, then the default locale, then the supplied fallback or an empty string.
127
+ * @public
128
+ * @static
129
+ * @param {string|string[]} key - Translation key or nested path.
130
+ * @param {{locale?:string,fallback?:*}} [options={}] - Locale override and missing-key fallback.
131
+ * @returns {*}
132
+ */
133
+ static translate(key, options = {}) {
134
+ if (!key) {
135
+ return "";
136
+ }
137
+ const { locale, fallback } = options;
138
+ const parts = Array.isArray(key) ? key : key.split(".");
139
+ const bundle = I18n.#getBundle(locale);
140
+ let value = I18n.#resolveValue(bundle, parts);
70
141
  if (value !== undefined) {
71
142
  return value;
72
143
  }
144
+ if (bundle !== translations[I18n.DEFAULT_LOCALE]) {
145
+ value = I18n.#resolveValue(translations[I18n.DEFAULT_LOCALE], parts);
146
+ if (value !== undefined) {
147
+ return value;
148
+ }
149
+ }
150
+ return fallback ?? "";
73
151
  }
74
- return fallback ?? "";
75
- };
76
152
 
77
- export const getNamespace = (namespace, options = {}) => {
78
- if (!namespace) {
79
- return getBundle(options.locale);
153
+ /**
154
+ * Returns an entire locale bundle or resolves a namespace through the translation fallback rules.
155
+ * @public
156
+ * @static
157
+ * @param {string} namespace - Nested namespace; an empty value returns the bundle.
158
+ * @param {{locale?:string,fallback?:*}} [options={}] - Locale override and fallback.
159
+ * @returns {*}
160
+ */
161
+ static getNamespace(namespace, options = {}) {
162
+ if (!namespace) {
163
+ return I18n.#getBundle(options.locale);
164
+ }
165
+ return I18n.translate(namespace, options);
80
166
  }
81
- return translate(namespace, options);
82
- };
83
-
84
- export default {
85
- availableLocales,
86
- DEFAULT_LOCALE,
87
- getLocale,
88
- getNamespace,
89
- onLocaleChange,
90
- resolveLocale,
91
- setLocale,
92
- SUPPORTED_LOCALES,
93
- translate,
94
- };
167
+ }
@@ -1,6 +1,15 @@
1
1
  export const translations = {
2
2
  "en-EN": {
3
3
  prefViewer: {
4
+ viewCube: {
5
+ title: "View orientation",
6
+ front: "Inner",
7
+ back: "Outer",
8
+ top: "Top",
9
+ bottom: "Bottom",
10
+ left: "Left",
11
+ right: "Right"
12
+ },
4
13
  menu3d: {
5
14
  title: "3D render settings",
6
15
  subtitle: "Modify the visual effects.",
@@ -62,6 +71,15 @@ export const translations = {
62
71
  },
63
72
  "es-ES": {
64
73
  prefViewer: {
74
+ viewCube: {
75
+ title: "Orientación de vista",
76
+ front: "Interior",
77
+ back: "Exterior",
78
+ top: "Arriba",
79
+ bottom: "Abajo",
80
+ left: "Izquierda",
81
+ right: "Derecha"
82
+ },
65
83
  menu3d: {
66
84
  title: "Ajustes de render 3D",
67
85
  subtitle: "Modifica los efectos visuales.",
@@ -1,72 +1,79 @@
1
1
  import { PBRMaterial } from "@babylonjs/core";
2
2
 
3
3
  /**
4
- * Utilities for classifying and tagging PBR materials by their transparency/transmission mode.
5
- * Used by BabylonJSController at load time and by pref-viewer-3d for render-order decisions.
4
+ * Classifies and tags materials for transmission and transparent render ordering.
5
+ * @class MaterialUtils
6
6
  */
7
+ export default class MaterialUtils {
8
+ static #SIMPLIFIED_TRANSMISSION_KEY = "prefViewerSimplifiedTransmission";
7
9
 
8
- const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
10
+ /**
11
+ * Tags a PBR material as having been converted from physical KHR_materials_transmission to simple
12
+ * alpha blending. The tag is stored in `mat.metadata` under an internal key so that downstream
13
+ * render-order passes can still identify these materials as transparent candidates even after
14
+ * their `subSurface.isRefractionEnabled` flag has been cleared.
15
+ *
16
+ * Returns the same material instance (or the original value when `mat` is falsy) so calls can be
17
+ * chained.
18
+ * @public
19
+ * @static
20
+ * @param {import("@babylonjs/core").PBRMaterial|null|undefined} mat - Material to tag.
21
+ * @returns {import("@babylonjs/core").PBRMaterial|null|undefined} The same `mat` value that was passed in.
22
+ */
23
+ static markPrefViewerSimplifiedTransmissionMaterial(mat) {
24
+ if (!mat) {
25
+ return mat;
26
+ }
9
27
 
10
- /**
11
- * Tags a PBR material as having been converted from physical KHR_materials_transmission to simple
12
- * alpha blending. The tag is stored in `mat.metadata` under an internal key so that downstream
13
- * render-order passes can still identify these materials as transparent candidates even after
14
- * their `subSurface.isRefractionEnabled` flag has been cleared.
15
- *
16
- * Returns the same material instance (or the original value when `mat` is falsy) so calls can be
17
- * chained.
18
- * @param {import("@babylonjs/core").PBRMaterial|null|undefined} mat - Material to tag.
19
- * @returns {import("@babylonjs/core").PBRMaterial|null|undefined} The same `mat` value that was passed in.
20
- */
21
- export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
22
- if (!mat) {
28
+ mat.metadata = {
29
+ ...(mat.metadata ?? {}),
30
+ [MaterialUtils.#SIMPLIFIED_TRANSMISSION_KEY]: true,
31
+ };
23
32
  return mat;
24
33
  }
25
-
26
- mat.metadata = {
27
- ...(mat.metadata ?? {}),
28
- [PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
29
- };
30
- return mat;
31
- }
32
34
 
33
- /**
34
- * Returns true when the material uses Babylon.js physical glass rendering — i.e. either
35
- * KHR_materials_transmission (refraction) or KHR_materials_translucency is active.
36
- *
37
- * This is intentionally narrower than `isPrefViewerTransparentRenderOrderMaterial`: it only
38
- * matches full physical transmission, not plain alpha blending or simplified environment glass.
39
- * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
40
- * @returns {boolean}
41
- */
42
- export function isPrefViewerPhysicalGlassMaterial(mat) {
43
- return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
44
- }
45
-
46
- /**
47
- * Returns true when a material should be placed in the transparent rendering group (renderingGroupId=1).
48
- *
49
- * A material qualifies when any of the following is true:
50
- * - It is a physical glass material (KHR_materials_transmission / translucency).
51
- * - It has been tagged as simplified environment transmission via `markPrefViewerSimplifiedTransmissionMaterial`.
52
- * - Its effective alpha is below 1, its `transparencyMode` is alpha-blend or alpha-test-and-blend,
53
- * or it self-reports as needing blending/testing through the Babylon.js material API.
54
- * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
55
- * @returns {boolean}
56
- */
57
- export function isPrefViewerTransparentRenderOrderMaterial(mat) {
58
- if (!mat) {
59
- return false;
35
+ /**
36
+ * Returns true when the material uses Babylon.js physical glass rendering — i.e. either
37
+ * KHR_materials_transmission (refraction) or KHR_materials_translucency is active.
38
+ *
39
+ * This is intentionally narrower than `isPrefViewerTransparentRenderOrderMaterial`: it only
40
+ * matches full physical transmission, not plain alpha blending or simplified environment glass.
41
+ * @public
42
+ * @static
43
+ * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
44
+ * @returns {boolean}
45
+ */
46
+ static isPrefViewerPhysicalGlassMaterial(mat) {
47
+ return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
60
48
  }
61
- const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
62
- const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
63
- const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
64
- const isAlphaBlended =
65
- alpha < 1 ||
66
- mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
67
- mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
68
- mat.needAlphaBlending?.() === true ||
69
- mat.needAlphaTesting?.() === true;
70
49
 
71
- return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
50
+ /**
51
+ * Returns true when a material should be placed in the transparent rendering group (renderingGroupId=1).
52
+ *
53
+ * A material qualifies when any of the following is true:
54
+ * - It is a physical glass material (KHR_materials_transmission / translucency).
55
+ * - It has been tagged as simplified environment transmission via `markPrefViewerSimplifiedTransmissionMaterial`.
56
+ * - Its effective alpha is below 1, its `transparencyMode` is alpha-blend or alpha-test-and-blend,
57
+ * or it self-reports as needing blending/testing through the Babylon.js material API.
58
+ * @public
59
+ * @static
60
+ * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
61
+ * @returns {boolean}
62
+ */
63
+ static isPrefViewerTransparentRenderOrderMaterial(mat) {
64
+ if (!mat) {
65
+ return false;
66
+ }
67
+ const isTransmissive = MaterialUtils.isPrefViewerPhysicalGlassMaterial(mat);
68
+ const isSimplifiedTransmission = mat.metadata?.[MaterialUtils.#SIMPLIFIED_TRANSMISSION_KEY] === true;
69
+ const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
70
+ const isAlphaBlended =
71
+ alpha < 1 ||
72
+ mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
73
+ mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
74
+ mat.needAlphaBlending?.() === true ||
75
+ mat.needAlphaTesting?.() === true;
76
+
77
+ return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
78
+ }
72
79
  }
@@ -105,6 +105,14 @@ export default class PanzoomController {
105
105
  wheel: null,
106
106
  };
107
107
 
108
+ /**
109
+ * Stores the SVG wrapper, Panzoom options and callbacks, and binds reusable input handlers.
110
+ * @public
111
+ * @param {HTMLElement|SVGElement} wrapper - Element transformed by Panzoom.
112
+ * @param {object} [options] - Panzoom configuration.
113
+ * @param {function(object): void} [changedCallback] - Receives the current viewport state.
114
+ * @param {function(Event): void} [clickCallback] - Receives the original event for a recognized click.
115
+ */
108
116
  constructor(wrapper = null, options = {}, changedCallback = null, clickCallback = null) {
109
117
  this.#wrapper = wrapper;
110
118
  Object.assign(this.#options, options);
@@ -161,7 +169,12 @@ export default class PanzoomController {
161
169
  }
162
170
  }
163
171
 
164
- /** Reset transient pointer data after a completed or interrupted gesture. */
172
+ /**
173
+ * Reset transient pointer data after a completed or interrupted gesture.
174
+ * This includes clearing the last pointer down position and resetting counters.
175
+ * @private
176
+ * @returns {void}
177
+ */
165
178
  #resetPointerEvents() {
166
179
  this.#pointerEvents.lastPointerDownPosition = null;
167
180
  this.#pointerEvents.partialCounter = 0;