@es3d/es3d 1.0.0 → 1.0.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/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
|
@@ -1,392 +1,392 @@
|
|
|
1
|
-
// Persistent geometry cache: the meshes a document's shapes produced, kept
|
|
2
|
-
// between page loads so that a repeat visit reads them instead of
|
|
3
|
-
// tessellating again.
|
|
4
|
-
//
|
|
5
|
-
// - One record per document (one read on load), in ES3D's own binary layout
|
|
6
|
-
// (mesh-bundle.js), named by content: one SHA-256 over every cached shape's
|
|
7
|
-
// identity (its parameters and the full text of its path or point data),
|
|
8
|
-
// the generator revision, the quality setting and what the engine's Math
|
|
9
|
-
// functions return, which the generators use. Change any of them and the
|
|
10
|
-
// name changes; nothing is ever invalidated by date. Web Crypto computes
|
|
11
|
-
// the digest where the page has it, sha256.js where it does not.
|
|
12
|
-
// - Stored in IndexedDB, which pages served over plain http (a local
|
|
13
|
-
// network) have too, unlike Cache Storage and crypto.subtle. A store is
|
|
14
|
-
// only a get/put interface, so other places work as well (memoryStore,
|
|
15
|
-
// or a file store in Node).
|
|
16
|
-
// - Bounded: least recently used records go first once the store holds more
|
|
17
|
-
// than `maxBytes` or `maxRecords`; records of another generator revision go
|
|
18
|
-
// before anything else.
|
|
19
|
-
// - Advisory: a missing, unreadable, slow or invalid record means the meshes
|
|
20
|
-
// are generated as before. Nothing it stores changes what is drawn: the
|
|
21
|
-
// restored meshes are bit-identical to the ones generation would produce.
|
|
22
|
-
//
|
|
23
|
-
// Authored <mesh> data is not cached: hashing it costs about what computing
|
|
24
|
-
// its normals does.
|
|
25
|
-
|
|
26
|
-
import { sha256 } from './sha256.js';
|
|
27
|
-
import { encodeBundle, decodeBundle } from './mesh-bundle.js';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Bump whenever any generator's output changes, even by one bit
|
|
31
|
-
* (test/geometry-cache.test.mjs fails until the recorded digest of every
|
|
32
|
-
* example's meshes matches this revision).
|
|
33
|
-
*/
|
|
34
|
-
export const GEOMETRY_REVISION = 1;
|
|
35
|
-
|
|
36
|
-
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
37
|
-
|
|
38
|
-
let fingerprint = null;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* What this engine's Math functions return for a fixed set of arguments
|
|
42
|
-
* (bytes). IEEE 754 does not pin them down, and the generators use them, so
|
|
43
|
-
* meshes made by one engine are only reused by an engine that agrees.
|
|
44
|
-
*/
|
|
45
|
-
export function mathFingerprint() {
|
|
46
|
-
if (fingerprint) return fingerprint;
|
|
47
|
-
const out = new Float64Array(9 * 24);
|
|
48
|
-
let k = 0;
|
|
49
|
-
for (let i = 0; i < 24; i++) {
|
|
50
|
-
const x = (i - 11.5) * 0.8284271247461902;
|
|
51
|
-
out[k++] = Math.sin(x);
|
|
52
|
-
out[k++] = Math.cos(x);
|
|
53
|
-
out[k++] = Math.tan(x);
|
|
54
|
-
out[k++] = Math.atan2(x, 1.7 - i * 0.11);
|
|
55
|
-
out[k++] = Math.acos(Math.max(-1, Math.min(1, x / 10)));
|
|
56
|
-
out[k++] = Math.hypot(x, i * 0.3);
|
|
57
|
-
out[k++] = Math.hypot(x, i * 0.3, 1 / (i + 1));
|
|
58
|
-
out[k++] = Math.cbrt(x);
|
|
59
|
-
out[k++] = Math.sqrt(i + 0.1);
|
|
60
|
-
}
|
|
61
|
-
fingerprint = new Uint8Array(out.buffer);
|
|
62
|
-
return fingerprint;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const subtle = typeof crypto !== 'undefined' && crypto.subtle && typeof crypto.subtle.digest === 'function' ? crypto.subtle : null;
|
|
66
|
-
const utf8 = new TextEncoder();
|
|
67
|
-
|
|
68
|
-
async function digest(bytes, native) {
|
|
69
|
-
if (native && subtle) {
|
|
70
|
-
try {
|
|
71
|
-
const d = new Uint8Array(await subtle.digest('SHA-256', bytes));
|
|
72
|
-
let hex = '';
|
|
73
|
-
for (const b of d) hex += (b < 16 ? '0' : '') + b.toString(16);
|
|
74
|
-
return hex;
|
|
75
|
-
} catch {
|
|
76
|
-
// fall through to the script version
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return sha256(bytes);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The cacheable entries of a cache, ordered by identity (so the order in
|
|
84
|
-
* which a long-lived cache first saw them does not matter), and the name of
|
|
85
|
-
* their record.
|
|
86
|
-
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
87
|
-
* @param {{native?: boolean}} [opts] native: false always uses sha256.js
|
|
88
|
-
*/
|
|
89
|
-
export async function geometryKey(cache, opts = {}) {
|
|
90
|
-
const entries = [];
|
|
91
|
-
for (const e of cache.entries.values()) if (e.identity) entries.push(e);
|
|
92
|
-
if (!entries.length) return { key: null, entries };
|
|
93
|
-
entries.sort((a, b) => (a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0));
|
|
94
|
-
// identities are length-prefixed (registry.js), so their concatenation is unambiguous
|
|
95
|
-
const text = utf8.encode(`es3d-geometry:${GEOMETRY_REVISION}:${cache.quality}:${entries.length}:${entries.map((e) => e.identity).join('')}`);
|
|
96
|
-
const math = mathFingerprint();
|
|
97
|
-
const bytes = new Uint8Array(text.length + math.length);
|
|
98
|
-
bytes.set(text);
|
|
99
|
-
bytes.set(math, text.length);
|
|
100
|
-
return { key: await digest(bytes, opts.native !== false), entries };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Levels worth storing: generated, not failed, and not a stand-in. */
|
|
104
|
-
function storedLevels(entry) {
|
|
105
|
-
const levels = [];
|
|
106
|
-
for (let l = 0; l < entry.lods; l++) {
|
|
107
|
-
const mesh = entry.meshes[l];
|
|
108
|
-
if (mesh && !(entry.failedLods & (1 << l))) levels.push({ lod: l, mesh, reports: entry.reports[l] });
|
|
109
|
-
}
|
|
110
|
-
return levels;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const withTimeout = (promise, ms) =>
|
|
114
|
-
ms > 0 && ms < Infinity ? Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(undefined), ms))]) : promise;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Fill a cache with the meshes stored for its current shapes. Call after
|
|
118
|
-
* compile() and before the first frame; levels that exist already are kept.
|
|
119
|
-
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
120
|
-
* @param {object|Promise<object>} store from openGeometryStore() (null: nothing to do)
|
|
121
|
-
* @param {{timeout?: number}} [opts] give up on a store slower than this (ms, default 250)
|
|
122
|
-
* @returns {Promise<{hit: boolean, key: string|null, levels: number, bytes: number, ms: number, reason?: string}>}
|
|
123
|
-
*/
|
|
124
|
-
export async function restoreGeometry(cache, store, opts = {}) {
|
|
125
|
-
const t0 = now();
|
|
126
|
-
const done = (r) => ({ hit: false, key: null, levels: 0, bytes: 0, ...r, ms: now() - t0 });
|
|
127
|
-
const s = await withTimeout(Promise.resolve(store), opts.timeout ?? 250);
|
|
128
|
-
if (!s) return done({ reason: s === undefined ? 'store timed out' : 'no store' });
|
|
129
|
-
const { key, entries } = await geometryKey(cache);
|
|
130
|
-
if (!key) return done({ reason: 'nothing cacheable' });
|
|
131
|
-
let buffer;
|
|
132
|
-
try {
|
|
133
|
-
buffer = await withTimeout(s.get(key), opts.timeout ?? 250);
|
|
134
|
-
} catch {
|
|
135
|
-
buffer = null;
|
|
136
|
-
}
|
|
137
|
-
if (buffer === undefined) return done({ key, reason: 'store timed out' });
|
|
138
|
-
if (!buffer) {
|
|
139
|
-
cache.persisted = null;
|
|
140
|
-
return done({ key, reason: 'miss' });
|
|
141
|
-
}
|
|
142
|
-
const bundle = decodeBundle(buffer);
|
|
143
|
-
if (!bundle || bundle.key !== key || bundle.entries.length !== entries.length) {
|
|
144
|
-
// unreadable or not what it claims to be: drop it, generate instead
|
|
145
|
-
Promise.resolve(s.delete(key)).catch(() => {});
|
|
146
|
-
return done({ key, reason: 'invalid record' });
|
|
147
|
-
}
|
|
148
|
-
// the key covers the identities in this order, so the lists line up
|
|
149
|
-
let levels = 0, held = 0;
|
|
150
|
-
for (let i = 0; i < entries.length; i++) {
|
|
151
|
-
const stored = bundle.entries[i];
|
|
152
|
-
held += stored.length;
|
|
153
|
-
for (const { lod, mesh, reports } of stored) if (cache.adopt(entries[i], lod, mesh, reports)) levels++;
|
|
154
|
-
}
|
|
155
|
-
// what the store holds: saving is worth it once the cache holds more
|
|
156
|
-
cache.persisted = `${key}:${held}`;
|
|
157
|
-
return { hit: true, key, levels, bytes: buffer.byteLength, ms: now() - t0 };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function countLevels(entries) {
|
|
161
|
-
let n = 0;
|
|
162
|
-
for (const e of entries) n += storedLevels(e).length;
|
|
163
|
-
return n;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Store the meshes a cache holds for its current shapes, if it holds any the
|
|
168
|
-
* store does not have yet. Call when the page is idle after drawing.
|
|
169
|
-
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
170
|
-
* @param {object|Promise<object>} store
|
|
171
|
-
* @returns {Promise<{saved: boolean, key: string|null, bytes: number, reason?: string}>}
|
|
172
|
-
*/
|
|
173
|
-
export async function saveGeometry(cache, store) {
|
|
174
|
-
const s = await store;
|
|
175
|
-
if (!s) return { saved: false, key: null, bytes: 0, reason: 'no store' };
|
|
176
|
-
const { key, entries } = await geometryKey(cache);
|
|
177
|
-
if (!key) return { saved: false, key: null, bytes: 0, reason: 'nothing cacheable' };
|
|
178
|
-
const signature = `${key}:${countLevels(entries)}`;
|
|
179
|
-
if (cache.persisted === signature) return { saved: false, key, bytes: 0, reason: 'up to date' };
|
|
180
|
-
const records = entries.map(storedLevels);
|
|
181
|
-
if (!records.some((levels) => levels.length)) return { saved: false, key, bytes: 0, reason: 'nothing generated' };
|
|
182
|
-
const buffer = encodeBundle(key, records);
|
|
183
|
-
if (s.maxBytes && buffer.byteLength > s.maxBytes / 4) return { saved: false, key, bytes: buffer.byteLength, reason: 'too large' };
|
|
184
|
-
try {
|
|
185
|
-
await s.put(key, buffer);
|
|
186
|
-
} catch (e) {
|
|
187
|
-
return { saved: false, key, bytes: buffer.byteLength, reason: `store failed: ${e && e.message ? e.message : e}` };
|
|
188
|
-
}
|
|
189
|
-
cache.persisted = signature;
|
|
190
|
-
return { saved: true, key, bytes: buffer.byteLength };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* For pages: keep a geometry cache and a store in step. Call restore() after
|
|
195
|
-
* compile() and changed() after drawing; meshes generated since the last save
|
|
196
|
-
* are saved once nothing new has been generated for `delay` ms and the page
|
|
197
|
-
* is idle.
|
|
198
|
-
* @param {object|Promise<object>|null} store null: both calls do nothing
|
|
199
|
-
* @param {{delay?: number}} [opts]
|
|
200
|
-
*/
|
|
201
|
-
export function persistGeometry(store, opts = {}) {
|
|
202
|
-
const delay = opts.delay ?? 1500;
|
|
203
|
-
const idle = typeof requestIdleCallback === 'function' ? (fn) => requestIdleCallback(fn, { timeout: 5000 }) : (fn) => setTimeout(fn, 0);
|
|
204
|
-
let timer = 0;
|
|
205
|
-
let pending = null;
|
|
206
|
-
let seen = -1;
|
|
207
|
-
return {
|
|
208
|
-
store,
|
|
209
|
-
restore(cache, restoreOpts) {
|
|
210
|
-
return store ? restoreGeometry(cache, store, restoreOpts) : Promise.resolve(null);
|
|
211
|
-
},
|
|
212
|
-
changed(cache) {
|
|
213
|
-
if (!store || (cache === pending && cache.stats.generated === seen)) return;
|
|
214
|
-
pending = cache;
|
|
215
|
-
seen = cache.stats.generated;
|
|
216
|
-
clearTimeout(timer);
|
|
217
|
-
timer = setTimeout(() => idle(() => {
|
|
218
|
-
const c = pending;
|
|
219
|
-
pending = null;
|
|
220
|
-
if (c) saveGeometry(c, store).catch(() => {});
|
|
221
|
-
}), delay);
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// --- stores ---------------------------------------------------------------------
|
|
227
|
-
|
|
228
|
-
const DEFAULTS = { maxBytes: 64 * 1024 * 1024, maxRecords: 256 };
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* A store in memory (tests, Node, one page's lifetime). Keeps copies, as a
|
|
232
|
-
* real store would.
|
|
233
|
-
*/
|
|
234
|
-
export function memoryStore(opts = {}) {
|
|
235
|
-
const { maxBytes, maxRecords } = { ...DEFAULTS, ...opts };
|
|
236
|
-
const records = new Map();
|
|
237
|
-
let clock = 0;
|
|
238
|
-
const prune = () => {
|
|
239
|
-
let total = 0;
|
|
240
|
-
for (const r of records.values()) total += r.data.byteLength;
|
|
241
|
-
const order = [...records].sort((a, b) => a[1].used - b[1].used);
|
|
242
|
-
while (order.length && (total > maxBytes || records.size > maxRecords)) {
|
|
243
|
-
const [k, r] = order.shift();
|
|
244
|
-
records.delete(k);
|
|
245
|
-
total -= r.data.byteLength;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
return {
|
|
249
|
-
kind: 'memory',
|
|
250
|
-
maxBytes,
|
|
251
|
-
async get(key) {
|
|
252
|
-
const r = records.get(key);
|
|
253
|
-
if (!r) return null;
|
|
254
|
-
r.used = ++clock;
|
|
255
|
-
return r.data.slice(0);
|
|
256
|
-
},
|
|
257
|
-
async put(key, data) {
|
|
258
|
-
records.set(key, { data: data.slice(0), used: ++clock });
|
|
259
|
-
prune();
|
|
260
|
-
},
|
|
261
|
-
async delete(key) {
|
|
262
|
-
records.delete(key);
|
|
263
|
-
},
|
|
264
|
-
async clear() {
|
|
265
|
-
records.clear();
|
|
266
|
-
},
|
|
267
|
-
get size() {
|
|
268
|
-
return records.size;
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const request = (r) =>
|
|
274
|
-
new Promise((resolve, reject) => {
|
|
275
|
-
r.onsuccess = () => resolve(r.result);
|
|
276
|
-
r.onerror = () => reject(r.error);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const transaction = (t) =>
|
|
280
|
-
new Promise((resolve, reject) => {
|
|
281
|
-
t.oncomplete = () => resolve();
|
|
282
|
-
t.onerror = () => reject(t.error);
|
|
283
|
-
t.onabort = () => reject(t.error || new Error('transaction aborted'));
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* A store in IndexedDB. Two object stores: `data` (record name -> bundle)
|
|
288
|
-
* and `meta` (record name -> {key, bytes, used, revision}, indexed by `used`),
|
|
289
|
-
* so pruning reads the small records only.
|
|
290
|
-
* @param {{name?: string, maxBytes?: number, maxRecords?: number, timeout?: number}} [opts]
|
|
291
|
-
* @returns {Promise<object|null>} null where IndexedDB is missing or refuses
|
|
292
|
-
*/
|
|
293
|
-
export async function indexedDBStore(opts = {}) {
|
|
294
|
-
const { name, maxBytes, maxRecords, timeout } = { name: 'es3d-geometry', timeout: 2000, ...DEFAULTS, ...opts };
|
|
295
|
-
const idb = typeof indexedDB !== 'undefined' ? indexedDB : null;
|
|
296
|
-
if (!idb) return null;
|
|
297
|
-
let db;
|
|
298
|
-
try {
|
|
299
|
-
db = await withTimeout(
|
|
300
|
-
new Promise((resolve, reject) => {
|
|
301
|
-
const r = idb.open(name, 1);
|
|
302
|
-
r.onupgradeneeded = () => {
|
|
303
|
-
const d = r.result;
|
|
304
|
-
if (!d.objectStoreNames.contains('data')) d.createObjectStore('data');
|
|
305
|
-
if (!d.objectStoreNames.contains('meta')) d.createObjectStore('meta', { keyPath: 'key' }).createIndex('used', 'used');
|
|
306
|
-
};
|
|
307
|
-
r.onsuccess = () => resolve(r.result);
|
|
308
|
-
r.onerror = () => reject(r.error);
|
|
309
|
-
r.onblocked = () => resolve(null);
|
|
310
|
-
}),
|
|
311
|
-
timeout,
|
|
312
|
-
);
|
|
313
|
-
} catch {
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
if (!db) return null;
|
|
317
|
-
// let another tab upgrade or delete the database
|
|
318
|
-
db.onversionchange = () => db.close();
|
|
319
|
-
const touch = (key) => {
|
|
320
|
-
try {
|
|
321
|
-
const t = db.transaction('meta', 'readwrite');
|
|
322
|
-
const meta = t.objectStore('meta');
|
|
323
|
-
const r = meta.get(key);
|
|
324
|
-
r.onsuccess = () => {
|
|
325
|
-
if (r.result) meta.put({ ...r.result, used: Date.now() });
|
|
326
|
-
};
|
|
327
|
-
} catch {
|
|
328
|
-
// closed or busy: the order of eviction is a hint only
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
return {
|
|
332
|
-
kind: 'indexeddb',
|
|
333
|
-
maxBytes,
|
|
334
|
-
async get(key) {
|
|
335
|
-
const t = db.transaction('data', 'readonly');
|
|
336
|
-
const data = await request(t.objectStore('data').get(key));
|
|
337
|
-
if (!(data instanceof ArrayBuffer)) return null;
|
|
338
|
-
(typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout)(() => touch(key));
|
|
339
|
-
return data;
|
|
340
|
-
},
|
|
341
|
-
async put(key, data) {
|
|
342
|
-
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
343
|
-
const store = t.objectStore('data');
|
|
344
|
-
const meta = t.objectStore('meta');
|
|
345
|
-
store.put(data, key);
|
|
346
|
-
meta.put({ key, bytes: data.byteLength, used: Date.now(), revision: GEOMETRY_REVISION });
|
|
347
|
-
// prune in the same transaction: other revisions first, then the least recently used
|
|
348
|
-
const all = await request(meta.getAll());
|
|
349
|
-
all.sort((a, b) => (a.revision === GEOMETRY_REVISION) - (b.revision === GEOMETRY_REVISION) || a.used - b.used);
|
|
350
|
-
let total = all.reduce((s, r) => s + r.bytes, 0);
|
|
351
|
-
let count = all.length;
|
|
352
|
-
for (const r of all) {
|
|
353
|
-
if (r.key === key) continue;
|
|
354
|
-
if (r.revision === GEOMETRY_REVISION && total <= maxBytes && count <= maxRecords) break;
|
|
355
|
-
store.delete(r.key);
|
|
356
|
-
meta.delete(r.key);
|
|
357
|
-
total -= r.bytes;
|
|
358
|
-
count--;
|
|
359
|
-
}
|
|
360
|
-
await transaction(t);
|
|
361
|
-
},
|
|
362
|
-
async delete(key) {
|
|
363
|
-
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
364
|
-
t.objectStore('data').delete(key);
|
|
365
|
-
t.objectStore('meta').delete(key);
|
|
366
|
-
await transaction(t);
|
|
367
|
-
},
|
|
368
|
-
async clear() {
|
|
369
|
-
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
370
|
-
t.objectStore('data').clear();
|
|
371
|
-
t.objectStore('meta').clear();
|
|
372
|
-
await transaction(t);
|
|
373
|
-
},
|
|
374
|
-
close() {
|
|
375
|
-
db.close();
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* The best store this environment offers (IndexedDB), or null. Open it early,
|
|
382
|
-
* in parallel with fetching the document; restoreGeometry() takes the promise.
|
|
383
|
-
* `persist: true` also asks the browser not to evict the origin's storage
|
|
384
|
-
* under pressure (navigator.storage.persist(); browsers may ask the user).
|
|
385
|
-
*/
|
|
386
|
-
export async function openGeometryStore(opts = {}) {
|
|
387
|
-
const store = await indexedDBStore(opts).catch(() => null);
|
|
388
|
-
if (store && opts.persist && typeof navigator !== 'undefined' && navigator.storage && navigator.storage.persist) {
|
|
389
|
-
navigator.storage.persist().catch(() => {});
|
|
390
|
-
}
|
|
391
|
-
return store;
|
|
392
|
-
}
|
|
1
|
+
// Persistent geometry cache: the meshes a document's shapes produced, kept
|
|
2
|
+
// between page loads so that a repeat visit reads them instead of
|
|
3
|
+
// tessellating again.
|
|
4
|
+
//
|
|
5
|
+
// - One record per document (one read on load), in ES3D's own binary layout
|
|
6
|
+
// (mesh-bundle.js), named by content: one SHA-256 over every cached shape's
|
|
7
|
+
// identity (its parameters and the full text of its path or point data),
|
|
8
|
+
// the generator revision, the quality setting and what the engine's Math
|
|
9
|
+
// functions return, which the generators use. Change any of them and the
|
|
10
|
+
// name changes; nothing is ever invalidated by date. Web Crypto computes
|
|
11
|
+
// the digest where the page has it, sha256.js where it does not.
|
|
12
|
+
// - Stored in IndexedDB, which pages served over plain http (a local
|
|
13
|
+
// network) have too, unlike Cache Storage and crypto.subtle. A store is
|
|
14
|
+
// only a get/put interface, so other places work as well (memoryStore,
|
|
15
|
+
// or a file store in Node).
|
|
16
|
+
// - Bounded: least recently used records go first once the store holds more
|
|
17
|
+
// than `maxBytes` or `maxRecords`; records of another generator revision go
|
|
18
|
+
// before anything else.
|
|
19
|
+
// - Advisory: a missing, unreadable, slow or invalid record means the meshes
|
|
20
|
+
// are generated as before. Nothing it stores changes what is drawn: the
|
|
21
|
+
// restored meshes are bit-identical to the ones generation would produce.
|
|
22
|
+
//
|
|
23
|
+
// Authored <mesh> data is not cached: hashing it costs about what computing
|
|
24
|
+
// its normals does.
|
|
25
|
+
|
|
26
|
+
import { sha256 } from './sha256.js';
|
|
27
|
+
import { encodeBundle, decodeBundle } from './mesh-bundle.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bump whenever any generator's output changes, even by one bit
|
|
31
|
+
* (test/geometry-cache.test.mjs fails until the recorded digest of every
|
|
32
|
+
* example's meshes matches this revision).
|
|
33
|
+
*/
|
|
34
|
+
export const GEOMETRY_REVISION = 1;
|
|
35
|
+
|
|
36
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
37
|
+
|
|
38
|
+
let fingerprint = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What this engine's Math functions return for a fixed set of arguments
|
|
42
|
+
* (bytes). IEEE 754 does not pin them down, and the generators use them, so
|
|
43
|
+
* meshes made by one engine are only reused by an engine that agrees.
|
|
44
|
+
*/
|
|
45
|
+
export function mathFingerprint() {
|
|
46
|
+
if (fingerprint) return fingerprint;
|
|
47
|
+
const out = new Float64Array(9 * 24);
|
|
48
|
+
let k = 0;
|
|
49
|
+
for (let i = 0; i < 24; i++) {
|
|
50
|
+
const x = (i - 11.5) * 0.8284271247461902;
|
|
51
|
+
out[k++] = Math.sin(x);
|
|
52
|
+
out[k++] = Math.cos(x);
|
|
53
|
+
out[k++] = Math.tan(x);
|
|
54
|
+
out[k++] = Math.atan2(x, 1.7 - i * 0.11);
|
|
55
|
+
out[k++] = Math.acos(Math.max(-1, Math.min(1, x / 10)));
|
|
56
|
+
out[k++] = Math.hypot(x, i * 0.3);
|
|
57
|
+
out[k++] = Math.hypot(x, i * 0.3, 1 / (i + 1));
|
|
58
|
+
out[k++] = Math.cbrt(x);
|
|
59
|
+
out[k++] = Math.sqrt(i + 0.1);
|
|
60
|
+
}
|
|
61
|
+
fingerprint = new Uint8Array(out.buffer);
|
|
62
|
+
return fingerprint;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const subtle = typeof crypto !== 'undefined' && crypto.subtle && typeof crypto.subtle.digest === 'function' ? crypto.subtle : null;
|
|
66
|
+
const utf8 = new TextEncoder();
|
|
67
|
+
|
|
68
|
+
async function digest(bytes, native) {
|
|
69
|
+
if (native && subtle) {
|
|
70
|
+
try {
|
|
71
|
+
const d = new Uint8Array(await subtle.digest('SHA-256', bytes));
|
|
72
|
+
let hex = '';
|
|
73
|
+
for (const b of d) hex += (b < 16 ? '0' : '') + b.toString(16);
|
|
74
|
+
return hex;
|
|
75
|
+
} catch {
|
|
76
|
+
// fall through to the script version
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return sha256(bytes);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The cacheable entries of a cache, ordered by identity (so the order in
|
|
84
|
+
* which a long-lived cache first saw them does not matter), and the name of
|
|
85
|
+
* their record.
|
|
86
|
+
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
87
|
+
* @param {{native?: boolean}} [opts] native: false always uses sha256.js
|
|
88
|
+
*/
|
|
89
|
+
export async function geometryKey(cache, opts = {}) {
|
|
90
|
+
const entries = [];
|
|
91
|
+
for (const e of cache.entries.values()) if (e.identity) entries.push(e);
|
|
92
|
+
if (!entries.length) return { key: null, entries };
|
|
93
|
+
entries.sort((a, b) => (a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0));
|
|
94
|
+
// identities are length-prefixed (registry.js), so their concatenation is unambiguous
|
|
95
|
+
const text = utf8.encode(`es3d-geometry:${GEOMETRY_REVISION}:${cache.quality}:${entries.length}:${entries.map((e) => e.identity).join('')}`);
|
|
96
|
+
const math = mathFingerprint();
|
|
97
|
+
const bytes = new Uint8Array(text.length + math.length);
|
|
98
|
+
bytes.set(text);
|
|
99
|
+
bytes.set(math, text.length);
|
|
100
|
+
return { key: await digest(bytes, opts.native !== false), entries };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Levels worth storing: generated, not failed, and not a stand-in. */
|
|
104
|
+
function storedLevels(entry) {
|
|
105
|
+
const levels = [];
|
|
106
|
+
for (let l = 0; l < entry.lods; l++) {
|
|
107
|
+
const mesh = entry.meshes[l];
|
|
108
|
+
if (mesh && !(entry.failedLods & (1 << l))) levels.push({ lod: l, mesh, reports: entry.reports[l] });
|
|
109
|
+
}
|
|
110
|
+
return levels;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const withTimeout = (promise, ms) =>
|
|
114
|
+
ms > 0 && ms < Infinity ? Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(undefined), ms))]) : promise;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Fill a cache with the meshes stored for its current shapes. Call after
|
|
118
|
+
* compile() and before the first frame; levels that exist already are kept.
|
|
119
|
+
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
120
|
+
* @param {object|Promise<object>} store from openGeometryStore() (null: nothing to do)
|
|
121
|
+
* @param {{timeout?: number}} [opts] give up on a store slower than this (ms, default 250)
|
|
122
|
+
* @returns {Promise<{hit: boolean, key: string|null, levels: number, bytes: number, ms: number, reason?: string}>}
|
|
123
|
+
*/
|
|
124
|
+
export async function restoreGeometry(cache, store, opts = {}) {
|
|
125
|
+
const t0 = now();
|
|
126
|
+
const done = (r) => ({ hit: false, key: null, levels: 0, bytes: 0, ...r, ms: now() - t0 });
|
|
127
|
+
const s = await withTimeout(Promise.resolve(store), opts.timeout ?? 250);
|
|
128
|
+
if (!s) return done({ reason: s === undefined ? 'store timed out' : 'no store' });
|
|
129
|
+
const { key, entries } = await geometryKey(cache);
|
|
130
|
+
if (!key) return done({ reason: 'nothing cacheable' });
|
|
131
|
+
let buffer;
|
|
132
|
+
try {
|
|
133
|
+
buffer = await withTimeout(s.get(key), opts.timeout ?? 250);
|
|
134
|
+
} catch {
|
|
135
|
+
buffer = null;
|
|
136
|
+
}
|
|
137
|
+
if (buffer === undefined) return done({ key, reason: 'store timed out' });
|
|
138
|
+
if (!buffer) {
|
|
139
|
+
cache.persisted = null;
|
|
140
|
+
return done({ key, reason: 'miss' });
|
|
141
|
+
}
|
|
142
|
+
const bundle = decodeBundle(buffer);
|
|
143
|
+
if (!bundle || bundle.key !== key || bundle.entries.length !== entries.length) {
|
|
144
|
+
// unreadable or not what it claims to be: drop it, generate instead
|
|
145
|
+
Promise.resolve(s.delete(key)).catch(() => {});
|
|
146
|
+
return done({ key, reason: 'invalid record' });
|
|
147
|
+
}
|
|
148
|
+
// the key covers the identities in this order, so the lists line up
|
|
149
|
+
let levels = 0, held = 0;
|
|
150
|
+
for (let i = 0; i < entries.length; i++) {
|
|
151
|
+
const stored = bundle.entries[i];
|
|
152
|
+
held += stored.length;
|
|
153
|
+
for (const { lod, mesh, reports } of stored) if (cache.adopt(entries[i], lod, mesh, reports)) levels++;
|
|
154
|
+
}
|
|
155
|
+
// what the store holds: saving is worth it once the cache holds more
|
|
156
|
+
cache.persisted = `${key}:${held}`;
|
|
157
|
+
return { hit: true, key, levels, bytes: buffer.byteLength, ms: now() - t0 };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function countLevels(entries) {
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (const e of entries) n += storedLevels(e).length;
|
|
163
|
+
return n;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Store the meshes a cache holds for its current shapes, if it holds any the
|
|
168
|
+
* store does not have yet. Call when the page is idle after drawing.
|
|
169
|
+
* @param {import('../geometry/geometry-cache.js').GeometryCache} cache
|
|
170
|
+
* @param {object|Promise<object>} store
|
|
171
|
+
* @returns {Promise<{saved: boolean, key: string|null, bytes: number, reason?: string}>}
|
|
172
|
+
*/
|
|
173
|
+
export async function saveGeometry(cache, store) {
|
|
174
|
+
const s = await store;
|
|
175
|
+
if (!s) return { saved: false, key: null, bytes: 0, reason: 'no store' };
|
|
176
|
+
const { key, entries } = await geometryKey(cache);
|
|
177
|
+
if (!key) return { saved: false, key: null, bytes: 0, reason: 'nothing cacheable' };
|
|
178
|
+
const signature = `${key}:${countLevels(entries)}`;
|
|
179
|
+
if (cache.persisted === signature) return { saved: false, key, bytes: 0, reason: 'up to date' };
|
|
180
|
+
const records = entries.map(storedLevels);
|
|
181
|
+
if (!records.some((levels) => levels.length)) return { saved: false, key, bytes: 0, reason: 'nothing generated' };
|
|
182
|
+
const buffer = encodeBundle(key, records);
|
|
183
|
+
if (s.maxBytes && buffer.byteLength > s.maxBytes / 4) return { saved: false, key, bytes: buffer.byteLength, reason: 'too large' };
|
|
184
|
+
try {
|
|
185
|
+
await s.put(key, buffer);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return { saved: false, key, bytes: buffer.byteLength, reason: `store failed: ${e && e.message ? e.message : e}` };
|
|
188
|
+
}
|
|
189
|
+
cache.persisted = signature;
|
|
190
|
+
return { saved: true, key, bytes: buffer.byteLength };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* For pages: keep a geometry cache and a store in step. Call restore() after
|
|
195
|
+
* compile() and changed() after drawing; meshes generated since the last save
|
|
196
|
+
* are saved once nothing new has been generated for `delay` ms and the page
|
|
197
|
+
* is idle.
|
|
198
|
+
* @param {object|Promise<object>|null} store null: both calls do nothing
|
|
199
|
+
* @param {{delay?: number}} [opts]
|
|
200
|
+
*/
|
|
201
|
+
export function persistGeometry(store, opts = {}) {
|
|
202
|
+
const delay = opts.delay ?? 1500;
|
|
203
|
+
const idle = typeof requestIdleCallback === 'function' ? (fn) => requestIdleCallback(fn, { timeout: 5000 }) : (fn) => setTimeout(fn, 0);
|
|
204
|
+
let timer = 0;
|
|
205
|
+
let pending = null;
|
|
206
|
+
let seen = -1;
|
|
207
|
+
return {
|
|
208
|
+
store,
|
|
209
|
+
restore(cache, restoreOpts) {
|
|
210
|
+
return store ? restoreGeometry(cache, store, restoreOpts) : Promise.resolve(null);
|
|
211
|
+
},
|
|
212
|
+
changed(cache) {
|
|
213
|
+
if (!store || (cache === pending && cache.stats.generated === seen)) return;
|
|
214
|
+
pending = cache;
|
|
215
|
+
seen = cache.stats.generated;
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
timer = setTimeout(() => idle(() => {
|
|
218
|
+
const c = pending;
|
|
219
|
+
pending = null;
|
|
220
|
+
if (c) saveGeometry(c, store).catch(() => {});
|
|
221
|
+
}), delay);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- stores ---------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const DEFAULTS = { maxBytes: 64 * 1024 * 1024, maxRecords: 256 };
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A store in memory (tests, Node, one page's lifetime). Keeps copies, as a
|
|
232
|
+
* real store would.
|
|
233
|
+
*/
|
|
234
|
+
export function memoryStore(opts = {}) {
|
|
235
|
+
const { maxBytes, maxRecords } = { ...DEFAULTS, ...opts };
|
|
236
|
+
const records = new Map();
|
|
237
|
+
let clock = 0;
|
|
238
|
+
const prune = () => {
|
|
239
|
+
let total = 0;
|
|
240
|
+
for (const r of records.values()) total += r.data.byteLength;
|
|
241
|
+
const order = [...records].sort((a, b) => a[1].used - b[1].used);
|
|
242
|
+
while (order.length && (total > maxBytes || records.size > maxRecords)) {
|
|
243
|
+
const [k, r] = order.shift();
|
|
244
|
+
records.delete(k);
|
|
245
|
+
total -= r.data.byteLength;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
kind: 'memory',
|
|
250
|
+
maxBytes,
|
|
251
|
+
async get(key) {
|
|
252
|
+
const r = records.get(key);
|
|
253
|
+
if (!r) return null;
|
|
254
|
+
r.used = ++clock;
|
|
255
|
+
return r.data.slice(0);
|
|
256
|
+
},
|
|
257
|
+
async put(key, data) {
|
|
258
|
+
records.set(key, { data: data.slice(0), used: ++clock });
|
|
259
|
+
prune();
|
|
260
|
+
},
|
|
261
|
+
async delete(key) {
|
|
262
|
+
records.delete(key);
|
|
263
|
+
},
|
|
264
|
+
async clear() {
|
|
265
|
+
records.clear();
|
|
266
|
+
},
|
|
267
|
+
get size() {
|
|
268
|
+
return records.size;
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const request = (r) =>
|
|
274
|
+
new Promise((resolve, reject) => {
|
|
275
|
+
r.onsuccess = () => resolve(r.result);
|
|
276
|
+
r.onerror = () => reject(r.error);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const transaction = (t) =>
|
|
280
|
+
new Promise((resolve, reject) => {
|
|
281
|
+
t.oncomplete = () => resolve();
|
|
282
|
+
t.onerror = () => reject(t.error);
|
|
283
|
+
t.onabort = () => reject(t.error || new Error('transaction aborted'));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A store in IndexedDB. Two object stores: `data` (record name -> bundle)
|
|
288
|
+
* and `meta` (record name -> {key, bytes, used, revision}, indexed by `used`),
|
|
289
|
+
* so pruning reads the small records only.
|
|
290
|
+
* @param {{name?: string, maxBytes?: number, maxRecords?: number, timeout?: number}} [opts]
|
|
291
|
+
* @returns {Promise<object|null>} null where IndexedDB is missing or refuses
|
|
292
|
+
*/
|
|
293
|
+
export async function indexedDBStore(opts = {}) {
|
|
294
|
+
const { name, maxBytes, maxRecords, timeout } = { name: 'es3d-geometry', timeout: 2000, ...DEFAULTS, ...opts };
|
|
295
|
+
const idb = typeof indexedDB !== 'undefined' ? indexedDB : null;
|
|
296
|
+
if (!idb) return null;
|
|
297
|
+
let db;
|
|
298
|
+
try {
|
|
299
|
+
db = await withTimeout(
|
|
300
|
+
new Promise((resolve, reject) => {
|
|
301
|
+
const r = idb.open(name, 1);
|
|
302
|
+
r.onupgradeneeded = () => {
|
|
303
|
+
const d = r.result;
|
|
304
|
+
if (!d.objectStoreNames.contains('data')) d.createObjectStore('data');
|
|
305
|
+
if (!d.objectStoreNames.contains('meta')) d.createObjectStore('meta', { keyPath: 'key' }).createIndex('used', 'used');
|
|
306
|
+
};
|
|
307
|
+
r.onsuccess = () => resolve(r.result);
|
|
308
|
+
r.onerror = () => reject(r.error);
|
|
309
|
+
r.onblocked = () => resolve(null);
|
|
310
|
+
}),
|
|
311
|
+
timeout,
|
|
312
|
+
);
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (!db) return null;
|
|
317
|
+
// let another tab upgrade or delete the database
|
|
318
|
+
db.onversionchange = () => db.close();
|
|
319
|
+
const touch = (key) => {
|
|
320
|
+
try {
|
|
321
|
+
const t = db.transaction('meta', 'readwrite');
|
|
322
|
+
const meta = t.objectStore('meta');
|
|
323
|
+
const r = meta.get(key);
|
|
324
|
+
r.onsuccess = () => {
|
|
325
|
+
if (r.result) meta.put({ ...r.result, used: Date.now() });
|
|
326
|
+
};
|
|
327
|
+
} catch {
|
|
328
|
+
// closed or busy: the order of eviction is a hint only
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
kind: 'indexeddb',
|
|
333
|
+
maxBytes,
|
|
334
|
+
async get(key) {
|
|
335
|
+
const t = db.transaction('data', 'readonly');
|
|
336
|
+
const data = await request(t.objectStore('data').get(key));
|
|
337
|
+
if (!(data instanceof ArrayBuffer)) return null;
|
|
338
|
+
(typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout)(() => touch(key));
|
|
339
|
+
return data;
|
|
340
|
+
},
|
|
341
|
+
async put(key, data) {
|
|
342
|
+
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
343
|
+
const store = t.objectStore('data');
|
|
344
|
+
const meta = t.objectStore('meta');
|
|
345
|
+
store.put(data, key);
|
|
346
|
+
meta.put({ key, bytes: data.byteLength, used: Date.now(), revision: GEOMETRY_REVISION });
|
|
347
|
+
// prune in the same transaction: other revisions first, then the least recently used
|
|
348
|
+
const all = await request(meta.getAll());
|
|
349
|
+
all.sort((a, b) => (a.revision === GEOMETRY_REVISION) - (b.revision === GEOMETRY_REVISION) || a.used - b.used);
|
|
350
|
+
let total = all.reduce((s, r) => s + r.bytes, 0);
|
|
351
|
+
let count = all.length;
|
|
352
|
+
for (const r of all) {
|
|
353
|
+
if (r.key === key) continue;
|
|
354
|
+
if (r.revision === GEOMETRY_REVISION && total <= maxBytes && count <= maxRecords) break;
|
|
355
|
+
store.delete(r.key);
|
|
356
|
+
meta.delete(r.key);
|
|
357
|
+
total -= r.bytes;
|
|
358
|
+
count--;
|
|
359
|
+
}
|
|
360
|
+
await transaction(t);
|
|
361
|
+
},
|
|
362
|
+
async delete(key) {
|
|
363
|
+
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
364
|
+
t.objectStore('data').delete(key);
|
|
365
|
+
t.objectStore('meta').delete(key);
|
|
366
|
+
await transaction(t);
|
|
367
|
+
},
|
|
368
|
+
async clear() {
|
|
369
|
+
const t = db.transaction(['data', 'meta'], 'readwrite');
|
|
370
|
+
t.objectStore('data').clear();
|
|
371
|
+
t.objectStore('meta').clear();
|
|
372
|
+
await transaction(t);
|
|
373
|
+
},
|
|
374
|
+
close() {
|
|
375
|
+
db.close();
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The best store this environment offers (IndexedDB), or null. Open it early,
|
|
382
|
+
* in parallel with fetching the document; restoreGeometry() takes the promise.
|
|
383
|
+
* `persist: true` also asks the browser not to evict the origin's storage
|
|
384
|
+
* under pressure (navigator.storage.persist(); browsers may ask the user).
|
|
385
|
+
*/
|
|
386
|
+
export async function openGeometryStore(opts = {}) {
|
|
387
|
+
const store = await indexedDBStore(opts).catch(() => null);
|
|
388
|
+
if (store && opts.persist && typeof navigator !== 'undefined' && navigator.storage && navigator.storage.persist) {
|
|
389
|
+
navigator.storage.persist().catch(() => {});
|
|
390
|
+
}
|
|
391
|
+
return store;
|
|
392
|
+
}
|