@preference-sl/pref-viewer 2.11.0-beta.0 → 2.11.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/babylonjs-controller.js +932 -0
- package/src/file-storage.js +166 -39
- package/src/gltf-resolver.js +288 -0
- package/src/index.js +598 -1074
- package/src/panzoom-controller.js +494 -0
- package/src/pref-viewer-2d.js +459 -0
- package/src/pref-viewer-3d-data.js +178 -0
- package/src/pref-viewer-3d.js +635 -0
- package/src/pref-viewer-task.js +54 -0
- package/src/svg-resolver.js +281 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import isSvg from "is-svg";
|
|
2
|
+
import { initDb, loadModel } from "./gltf-storage.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SVGResolver - Utility class for resolving, decoding, and validating SVG resources from multiple sources.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Handles SVG input from:
|
|
9
|
+
* * IndexedDB records (storage.db, storage.table, storage.id)
|
|
10
|
+
* * remote URLs (storage.url)
|
|
11
|
+
* * data URIs (base64 or plain SVG) (storage.url)
|
|
12
|
+
* * direct SVG strings (storage.url)
|
|
13
|
+
* - Decodes base64 and validates SVG content.
|
|
14
|
+
* - Avoids unnecessary reloads by comparing size and Last-Modified timestamp.
|
|
15
|
+
* - Provides a unified API for consumers to obtain SVG content and metadata.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* - Instantiate with an initial SVG state object (required).
|
|
19
|
+
* Example: const resolver = new SVGResolver({ value: "", size: 0, timeStamp: null, update: true });
|
|
20
|
+
* - Call getSVG(svgConfig) to resolve and update the SVG state.
|
|
21
|
+
* Example: await resolver.getSVG({ storage: { url: "data:image/svg+xml;base64,..." } });
|
|
22
|
+
*
|
|
23
|
+
* Public methods:
|
|
24
|
+
* - getSVG(svgConfig, svg?): Resolves and prepares SVG from the given configuration, updating internal and optionally external state.
|
|
25
|
+
*
|
|
26
|
+
* Internal methods:
|
|
27
|
+
* - #initializeStorage(db, table): Ensures IndexedDB store is initialized.
|
|
28
|
+
* - #parseSource(source): Decodes and validates SVG from string or data URI.
|
|
29
|
+
* - #getServerFileMetaData(uri): Gets file size and last modified from server.
|
|
30
|
+
* - #getServerSVG(uri): Downloads SVG text from server.
|
|
31
|
+
*
|
|
32
|
+
* Events:
|
|
33
|
+
* - None (state is updated via returned object).
|
|
34
|
+
*
|
|
35
|
+
* Notes:
|
|
36
|
+
* - The internal SVG state is always kept up-to-date and can be referenced externally if passed in the constructor.
|
|
37
|
+
* - Designed for extensibility and integration with custom storage or fetch logic.
|
|
38
|
+
*/
|
|
39
|
+
export default class SVGResolver {
|
|
40
|
+
#svg = {
|
|
41
|
+
value: "",
|
|
42
|
+
size: 0,
|
|
43
|
+
timeStamp: null,
|
|
44
|
+
update: true,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new SVGResolver instance.
|
|
49
|
+
* @param {{value?:string, size?:number, timeStamp?:string|null, update?:boolean}=} svg - Optional initial SVG state to seed the resolver's internal cache.
|
|
50
|
+
* If omitted, the internal state defaults to: { value: "", size: 0, timeStamp: null, update: true }
|
|
51
|
+
* @description
|
|
52
|
+
* The 'svg' parameter allows you to preload the resolver with a known SVG state that will always be up-to-date since it's a referenced object.
|
|
53
|
+
* If this parameter isn't passed when creating the instance, it must be passed as a parameter in the 'getSVG' public method to keep it updated.
|
|
54
|
+
*/
|
|
55
|
+
constructor(svg) {
|
|
56
|
+
if (svg && typeof svg === "object") {
|
|
57
|
+
this.#svg = svg;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensure the IndexedDB object store for GLTF/SVG storage is initialized.
|
|
63
|
+
* @private
|
|
64
|
+
* @param {string} db - Database name.
|
|
65
|
+
* @param {string} table - Object store name.
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
* @description
|
|
68
|
+
* If a global PrefConfigurator.db already references the requested DB and table, no action is taken. Otherwise delegates to initDb to create/open the store.
|
|
69
|
+
*/
|
|
70
|
+
async #initializeStorage(db, table) {
|
|
71
|
+
if (typeof window !== "undefined" && window.PrefConfigurator.db && window.PrefConfigurator.db.name === db && window.PrefConfigurator.db.objectStoreNames.contains(table)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await initDb(db, table);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse an input that may be a plain SVG string or a data URI (possibly base64).
|
|
79
|
+
* @private
|
|
80
|
+
* @param {string} source - Data URI or raw string to decode/check.
|
|
81
|
+
* @returns {{value: string|null, size: number}} value when detected/decoded, and size (length of the text string).
|
|
82
|
+
*/
|
|
83
|
+
#parseSource(source) {
|
|
84
|
+
let value = null;
|
|
85
|
+
let size = 0;
|
|
86
|
+
|
|
87
|
+
if (typeof source !== "string") {
|
|
88
|
+
return [value, size];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isSvg(source)) {
|
|
92
|
+
value = source;
|
|
93
|
+
size = source.length;
|
|
94
|
+
return [value, size];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let raw;
|
|
98
|
+
if (source.startsWith("data:")) {
|
|
99
|
+
const commaIndex = source.indexOf(",");
|
|
100
|
+
raw = commaIndex !== -1 ? source.slice(commaIndex + 1) : source;
|
|
101
|
+
} else {
|
|
102
|
+
raw = source;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isSvg(raw)) {
|
|
106
|
+
size = raw.length;
|
|
107
|
+
value = raw;
|
|
108
|
+
return [value, size];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let decoded = "";
|
|
112
|
+
try {
|
|
113
|
+
decoded = atob(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return [value, size];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isSvg(decoded)) {
|
|
119
|
+
size = raw.length;
|
|
120
|
+
value = decoded;
|
|
121
|
+
return [value, size];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [value, size];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Perform a HEAD request to gather server-side file metadata (Content-Length and Last-Modified).
|
|
129
|
+
* @private
|
|
130
|
+
* @param {string} uri - Resource URI to probe.
|
|
131
|
+
* @returns {Promise<{size:number,timeStamp:string|null}>} Object with size (0 when unknown) and ISO timeStamp or null.
|
|
132
|
+
*/
|
|
133
|
+
async #getServerFileMetaData(uri) {
|
|
134
|
+
let data = {
|
|
135
|
+
size: 0,
|
|
136
|
+
timeStamp: null,
|
|
137
|
+
};
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const xhr = new XMLHttpRequest();
|
|
140
|
+
xhr.open("HEAD", uri, true);
|
|
141
|
+
xhr.responseType = "blob";
|
|
142
|
+
xhr.onload = () => {
|
|
143
|
+
if (xhr.status === 200) {
|
|
144
|
+
const lastModified = xhr.getResponseHeader("Last-Modified");
|
|
145
|
+
data.timeStamp = lastModified ? new Date(lastModified).toISOString() : null;
|
|
146
|
+
const contentLength = xhr.getResponseHeader("Content-Length");
|
|
147
|
+
data.size = contentLength ? parseInt(contentLength, 10) : 0;
|
|
148
|
+
resolve(data);
|
|
149
|
+
} else {
|
|
150
|
+
resolve(data);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
xhr.onerror = () => {
|
|
154
|
+
resolve(data);
|
|
155
|
+
};
|
|
156
|
+
xhr.send();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Download an SVG document as text from the server.
|
|
162
|
+
* @private
|
|
163
|
+
* @param {string} uri - Resource URI.
|
|
164
|
+
* @returns {Promise<string>} The SVG text or empty string on failure.
|
|
165
|
+
*/
|
|
166
|
+
async #getServerSVG(uri) {
|
|
167
|
+
let svg = "";
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const xhr = new XMLHttpRequest();
|
|
170
|
+
xhr.open("GET", uri, true);
|
|
171
|
+
xhr.responseType = "text";
|
|
172
|
+
xhr.onload = () => {
|
|
173
|
+
if (xhr.status === 200) {
|
|
174
|
+
svg = xhr.responseText;
|
|
175
|
+
resolve(svg);
|
|
176
|
+
} else {
|
|
177
|
+
resolve(svg);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
xhr.onerror = () => {
|
|
181
|
+
resolve(svg);
|
|
182
|
+
};
|
|
183
|
+
xhr.send();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve and prepare an SVG from multiple storage forms.
|
|
189
|
+
* @public
|
|
190
|
+
* @param {object|string} svgConfig - Configuration object. Expected shape for storage:
|
|
191
|
+
* { storage: { url: "<url>" } } // remote URL or data URI (base64 or plain)
|
|
192
|
+
* { storage: { db: "<db>", table: "<table>", id: "<id>" } } // IndexedDB record
|
|
193
|
+
* @param {object} [svg] - Optional external SVG state object to seed/update. If provided, the resolver
|
|
194
|
+
* updates this object in-place: { value: string, size: number, timeStamp: string|null, update: boolean }.
|
|
195
|
+
* @returns {Promise<false|{value:string,size:number,timeStamp:string|null,update:boolean}>}
|
|
196
|
+
* - Resolves to false when no update is required or on error.
|
|
197
|
+
* - Resolves to the internal SVG state object when an update was prepared.
|
|
198
|
+
* @description
|
|
199
|
+
* - Accepts SVG (base64 or plain string) from IndexedDB (storage.db/table/id), from a data URI (base64 or plain string) (storage.url),
|
|
200
|
+
* from a remote URL (storage.url) or from direct SVG string (storage.url).
|
|
201
|
+
* - Compares sizes and timestamps to avoid unnecessary reloads.
|
|
202
|
+
* - If the 'svg' parameter was provided when creating the instance, the referenced object will also be updated.
|
|
203
|
+
*/
|
|
204
|
+
async getSVG(svgConfig, svg) {
|
|
205
|
+
if (svg && typeof svg === "object") {
|
|
206
|
+
this.#svg = svg;
|
|
207
|
+
}
|
|
208
|
+
const storage = svgConfig?.storage;
|
|
209
|
+
if (!storage) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let source = storage.url || null;
|
|
214
|
+
let idbSize = 0;
|
|
215
|
+
let idbTimeStamp = null;
|
|
216
|
+
|
|
217
|
+
// SVG from an entry stored in IndexedDB (storage.db, storage.table, storage.id)
|
|
218
|
+
if (storage.db && storage.table && storage.id) {
|
|
219
|
+
await this.#initializeStorage(storage.db, storage.table);
|
|
220
|
+
const object = await loadModel(storage.id, storage.table);
|
|
221
|
+
source = object.data;
|
|
222
|
+
idbSize = object.size;
|
|
223
|
+
idbTimeStamp = object.timeStamp;
|
|
224
|
+
if (this.#svg.size === idbSize && this.#svg.timeStamp === idbTimeStamp) {
|
|
225
|
+
this.#svg.update = false;
|
|
226
|
+
return false;
|
|
227
|
+
} else {
|
|
228
|
+
this.#svg.update = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!source) {
|
|
233
|
+
this.#svg.update = false;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let [svgValue, svgSize] = this.#parseSource(source);
|
|
238
|
+
|
|
239
|
+
// SVG decoded successfully from Base64 or plain string, from object stored in IndexedDB (storage.db, storage.table, storage.id) or Base64 data URI (storage.url)
|
|
240
|
+
if (svgValue && svgSize) {
|
|
241
|
+
if (this.#svg.update) {
|
|
242
|
+
// From object stored in IndexedDB (storage.db, storage.table, storage.id)
|
|
243
|
+
this.#svg.value = svgValue;
|
|
244
|
+
this.#svg.size = idbSize;
|
|
245
|
+
this.#svg.timeStamp = idbTimeStamp;
|
|
246
|
+
return this.#svg;
|
|
247
|
+
} else {
|
|
248
|
+
// From Base64 data URI (storage.url)
|
|
249
|
+
if (this.#svg.size === svgSize && this.#svg.timeStamp === null) {
|
|
250
|
+
this.#svg.update = false;
|
|
251
|
+
return false;
|
|
252
|
+
} else {
|
|
253
|
+
this.#svg.size = svgSize;
|
|
254
|
+
this.#svg.timeStamp = null;
|
|
255
|
+
this.#svg.update = true;
|
|
256
|
+
this.#svg.value = svgValue;
|
|
257
|
+
return this.#svg;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// SVG from remote URL (storage.url)
|
|
262
|
+
let fileData = await this.#getServerFileMetaData(source);
|
|
263
|
+
if (this.#svg.size === fileData.size && this.#svg.timeStamp === fileData.timeStamp) {
|
|
264
|
+
this.#svg.update = false;
|
|
265
|
+
return false;
|
|
266
|
+
} else {
|
|
267
|
+
svgValue = await this.#getServerSVG(source);
|
|
268
|
+
if (isSvg(svgValue)) {
|
|
269
|
+
this.#svg.size = fileData.size;
|
|
270
|
+
this.#svg.timeStamp = fileData.timeStamp;
|
|
271
|
+
this.#svg.update = true;
|
|
272
|
+
this.#svg.value = svgValue;
|
|
273
|
+
return this.#svg;
|
|
274
|
+
} else {
|
|
275
|
+
this.#svg.update = false;
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|