geometryview 0.1.2 → 0.3.0
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/README.md +3 -1
- package/package.json +1 -1
- package/src/mcp.mjs +1 -1
- package/src/native.mjs +147 -15
- package/src/tools.mjs +135 -5
- package/web/build/geometryview-thumbs-web.mjs +2 -0
- package/web/build/geometryview-thumbs-web.wasm +0 -0
- package/web/build/geometryview-web.mjs +1 -1
- package/web/build/geometryview-web.wasm +0 -0
- package/web/engine.js +4 -0
- package/web/gallery-core.js +359 -0
- package/web/gallery-library.js +262 -0
- package/web/gallery-toolbar.js +148 -0
- package/web/gallery-view.js +1018 -0
- package/web/gallery.css +94 -0
- package/web/gallery.js +563 -0
- package/web/icons.js +6 -0
- package/web/import-queue.js +168 -0
- package/web/import-worker.js +63 -9
- package/web/importers.js +140 -0
- package/web/index.html +1 -0
- package/web/kept.js +263 -0
- package/web/library-store.js +116 -3
- package/web/library.js +25 -1
- package/web/occt/geometryview-step-web.wasm +0 -0
- package/web/occt-threads/geometryview-step-web.wasm +0 -0
- package/web/pdf.js +54 -0
- package/web/thumbs-worker.js +86 -0
- package/web/thumbs.js +97 -0
- package/web/viewer.js +721 -25
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The order in which files are imported and drawn: the browser's copy of the Mac's library::Queue
|
|
2
|
+
// (Engine/library/queue.h). Both run the cases in Tests/data/gallery-queue.json, so a gallery
|
|
3
|
+
// fills in the same order everywhere. Pure policy: who runs a job, and how, is the caller's.
|
|
4
|
+
//
|
|
5
|
+
// What is wanted is replaced whole as the view moves: each job has a band (a file being opened, one
|
|
6
|
+
// in view, one near the view, or one prefetched while nothing else waits) and an order within its
|
|
7
|
+
// band. Jobs start best first while slots are free. A job no longer wanted is dropped before it
|
|
8
|
+
// starts; once running it runs on, since its result is kept, unless a job in view needs its slot.
|
|
9
|
+
// Only a file being opened or one in view ever cancels a running job, and only one in a lower band.
|
|
10
|
+
|
|
11
|
+
export const Bands = Object.freeze({ open: 0, visible: 1, near: 2, prefetch: 3 });
|
|
12
|
+
|
|
13
|
+
/** Files larger than this are never prefetched: they are imported only when in view. */
|
|
14
|
+
export const kMaxPrefetchBytes = 300_000_000;
|
|
15
|
+
|
|
16
|
+
const kStale = 4; // a running job no longer wanted ranks below every band
|
|
17
|
+
|
|
18
|
+
export class ImportQueue {
|
|
19
|
+
#slots;
|
|
20
|
+
#prefetch;
|
|
21
|
+
#jobs = new Map(); // id -> { band, order, bytes, position, wanted, running, cancelling }
|
|
22
|
+
#waiting = []; // ids, best first
|
|
23
|
+
#running = 0;
|
|
24
|
+
|
|
25
|
+
constructor({ slots = 1, prefetch = true } = {}) {
|
|
26
|
+
this.#slots = slots;
|
|
27
|
+
this.#prefetch = prefetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Replaces what is wanted: jobs as { id, band, order, bytes }, band a name from Bands. A waiting
|
|
32
|
+
* job not wanted is dropped; a running one runs on, stale. A job is started again only if it is
|
|
33
|
+
* wanted after it finished.
|
|
34
|
+
*/
|
|
35
|
+
want(jobs) {
|
|
36
|
+
for (const job of this.#jobs.values()) job.wanted = false;
|
|
37
|
+
jobs.forEach(({ id, band, order = 0, bytes = 0 }, position) => {
|
|
38
|
+
const rank = Bands[band];
|
|
39
|
+
if (rank === undefined) throw new Error(`No band named ${band}`);
|
|
40
|
+
const job = this.#jobs.get(id);
|
|
41
|
+
if (job) {
|
|
42
|
+
Object.assign(job, { band: rank, order, bytes, position, wanted: true });
|
|
43
|
+
} else {
|
|
44
|
+
this.#jobs.set(id, { band: rank, order, bytes, position, wanted: true, running: false, cancelling: false });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
for (const [id, job] of this.#jobs) {
|
|
48
|
+
if (!job.wanted && !job.running) this.#jobs.delete(id);
|
|
49
|
+
}
|
|
50
|
+
this.#sortWaiting();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Starts the best waiting job, if a slot is free and one may start, and returns its id, or null. */
|
|
54
|
+
next() {
|
|
55
|
+
if (this.#running >= this.#slots) return null;
|
|
56
|
+
for (let i = 0; i < this.#waiting.length; i++) {
|
|
57
|
+
const id = this.#waiting[i];
|
|
58
|
+
const job = this.#jobs.get(id);
|
|
59
|
+
if (job.band === Bands.prefetch) {
|
|
60
|
+
if (!this.#prefetch) break; // prefetched jobs come last
|
|
61
|
+
if (job.bytes > kMaxPrefetchBytes) continue;
|
|
62
|
+
}
|
|
63
|
+
this.#waiting.splice(i, 1);
|
|
64
|
+
job.running = true;
|
|
65
|
+
this.#running++;
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A running job completed, or failed: it is done with. */
|
|
72
|
+
finish(id) {
|
|
73
|
+
const job = this.#jobs.get(id);
|
|
74
|
+
if (!job?.running) return;
|
|
75
|
+
this.#running--;
|
|
76
|
+
this.#jobs.delete(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A running job stopped before completing, as when cancelled: if still wanted it waits again. */
|
|
80
|
+
stopped(id) {
|
|
81
|
+
const job = this.#jobs.get(id);
|
|
82
|
+
if (!job?.running) return;
|
|
83
|
+
this.#running--;
|
|
84
|
+
if (!job.wanted) {
|
|
85
|
+
this.#jobs.delete(id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
job.running = false;
|
|
89
|
+
job.cancelling = false;
|
|
90
|
+
this.#sortWaiting();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Running jobs to cancel now, worst first, so that waiting jobs in view can start. Each is
|
|
95
|
+
* returned once, and keeps its slot until it has stopped.
|
|
96
|
+
*/
|
|
97
|
+
toCancel() {
|
|
98
|
+
let available = Math.max(0, this.#slots - this.#running);
|
|
99
|
+
const candidates = [];
|
|
100
|
+
for (const [id, job] of this.#jobs) {
|
|
101
|
+
if (!job.running) continue;
|
|
102
|
+
if (job.cancelling) {
|
|
103
|
+
available++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
candidates.push({ id, rank: job.wanted ? job.band : kStale, order: job.order, position: job.position });
|
|
107
|
+
}
|
|
108
|
+
candidates.sort((a, b) => compare(b, a)); // worst first
|
|
109
|
+
const cancelled = [];
|
|
110
|
+
for (const id of this.#waiting) {
|
|
111
|
+
const band = this.#jobs.get(id).band;
|
|
112
|
+
if (band > Bands.visible) break;
|
|
113
|
+
if (available > 0) {
|
|
114
|
+
available--;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const worse = candidates.findIndex((c) => c.rank > band);
|
|
118
|
+
if (worse < 0) break; // later jobs are in the same band or lower, and can cancel no more
|
|
119
|
+
cancelled.push(candidates[worse].id);
|
|
120
|
+
candidates.splice(worse, 1);
|
|
121
|
+
}
|
|
122
|
+
for (const id of cancelled) this.#jobs.get(id).cancelling = true;
|
|
123
|
+
return cancelled;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Fewer slots, as memory runs short, start nothing new until running jobs fall below them. */
|
|
127
|
+
setSlots(slots) {
|
|
128
|
+
this.#slots = slots;
|
|
129
|
+
}
|
|
130
|
+
get slots() {
|
|
131
|
+
return this.#slots;
|
|
132
|
+
}
|
|
133
|
+
/** Whether prefetched jobs may start. */
|
|
134
|
+
setPrefetch(allowed) {
|
|
135
|
+
this.#prefetch = allowed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
waiting() {
|
|
139
|
+
return [...this.#waiting];
|
|
140
|
+
}
|
|
141
|
+
running() {
|
|
142
|
+
return [...this.#jobs]
|
|
143
|
+
.filter(([, job]) => job.running)
|
|
144
|
+
.sort(([a, x], [b, y]) => compare({ id: a, rank: x.band, ...x }, { id: b, rank: y.band, ...y }))
|
|
145
|
+
.map(([id]) => id);
|
|
146
|
+
}
|
|
147
|
+
get idle() {
|
|
148
|
+
return this.#waiting.length === 0 && this.#running === 0;
|
|
149
|
+
}
|
|
150
|
+
/** A job's band, by name, if the queue holds it. */
|
|
151
|
+
bandOf(id) {
|
|
152
|
+
const job = this.#jobs.get(id);
|
|
153
|
+
return job ? Object.keys(Bands).find((name) => Bands[name] === job.band) : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#sortWaiting() {
|
|
157
|
+
this.#waiting = [...this.#jobs]
|
|
158
|
+
.filter(([, job]) => !job.running)
|
|
159
|
+
.map(([id, job]) => ({ id, rank: job.band, order: job.order, position: job.position }))
|
|
160
|
+
.sort(compare)
|
|
161
|
+
.map((job) => job.id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Best first: band, then order, then where it was wanted, then id, as the Mac's queue keys jobs.
|
|
166
|
+
function compare(a, b) {
|
|
167
|
+
return a.rank - b.rank || a.order - b.order || a.position - b.position || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
168
|
+
}
|
package/web/import-worker.js
CHANGED
|
@@ -13,13 +13,25 @@
|
|
|
13
13
|
// A STEP file's source is kept beside its cache, so its definitions can be imported again, finer,
|
|
14
14
|
// when the view comes close: a refinement, whose segments the page adds to the open document.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
16
|
+
// A cache file is kept under its key (kept.js); a file the page can read again from a connected
|
|
17
|
+
// folder keeps no source.
|
|
18
|
+
//
|
|
19
|
+
// Messages in: { type: "import", file, kind: "step" | "stl", name, key, keepSource, workers,
|
|
20
|
+
// threads, progressive }
|
|
17
21
|
// { type: "refine", file, level, definitions, workers }
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// { type: "
|
|
22
|
+
// { type: "structure", file }: a STEP file's model alone, as one segment, for an
|
|
23
|
+
// estimated box while it waits to import; nothing is kept
|
|
24
|
+
// { type: "cancel" }: the import or refinement running stops after the slices out
|
|
25
|
+
// Messages out: { type: "progress", done, total } and, if progressive, refining or reading
|
|
26
|
+
// structure, { type: "segment", bytes } as each is written; then
|
|
27
|
+
// { type: "done", ... } or { type: "failed", message, cancelled }
|
|
21
28
|
|
|
22
29
|
import createStepImporter from "./occt/geometryview-step-web.mjs";
|
|
30
|
+
import { keepImport, keepSource as keepSourceByKey } from "./kept.js";
|
|
31
|
+
|
|
32
|
+
// Set by a cancel message, which arrives while an import awaits its slices.
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
const kCancelled = "The import was cancelled.";
|
|
23
35
|
|
|
24
36
|
// Some browsers refuse a worker that starts another worker by URL. Claude Desktop's built-in
|
|
25
37
|
// browser is one: the nested worker fails at once, with no message and no request sent. A module
|
|
@@ -130,6 +142,14 @@ function mesh(step, slots, progressive) {
|
|
|
130
142
|
let closed = false;
|
|
131
143
|
|
|
132
144
|
const pump = () => {
|
|
145
|
+
if (cancelled && !closed) {
|
|
146
|
+
// No more slices: once those out are answered, the import stops.
|
|
147
|
+
if (busy === 0) {
|
|
148
|
+
closed = true;
|
|
149
|
+
Promise.all(replacing).then(() => reject(new Error(kCancelled)));
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
133
153
|
while (!closed && idle.length > 0) {
|
|
134
154
|
const id = step._step_next();
|
|
135
155
|
if (id === -2) {
|
|
@@ -255,6 +275,7 @@ async function readInto(step, file) {
|
|
|
255
275
|
// Imports some of a STEP file's definitions again at a finer level, posting each segment as it is
|
|
256
276
|
// written; they hold geometry only, for the page to add to the document already open.
|
|
257
277
|
async function refine(data) {
|
|
278
|
+
cancelled = false;
|
|
258
279
|
try {
|
|
259
280
|
const step = await supervisor(false);
|
|
260
281
|
await readInto(step, data.file);
|
|
@@ -271,7 +292,27 @@ async function refine(data) {
|
|
|
271
292
|
self.postMessage({ type: "done", triangles: stats[11], faces: stats[10], failedFaces: stats[12],
|
|
272
293
|
crashedWorkers: stats[14], coarserFaces: stats[16] });
|
|
273
294
|
} catch (error) {
|
|
274
|
-
|
|
295
|
+
failed(error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function failed(error) {
|
|
300
|
+
const message = String(error.message ?? error);
|
|
301
|
+
self.postMessage({ type: "failed", message, cancelled: message === kCancelled });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Reads a STEP file's structure alone and posts its model, with each body's box estimated, as one
|
|
305
|
+
// segment: what a gallery lays a file out from while it waits to import. Nothing is kept, and no
|
|
306
|
+
// OCCT worker is needed.
|
|
307
|
+
async function structure(data) {
|
|
308
|
+
try {
|
|
309
|
+
const step = await supervisor(false);
|
|
310
|
+
await readInto(step, data.file);
|
|
311
|
+
if (step._step_begin() !== 0) throw new Error(step.UTF8ToString(step._step_error()));
|
|
312
|
+
postSegments(step);
|
|
313
|
+
self.postMessage({ type: "done", structure: true });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
failed(error);
|
|
275
316
|
}
|
|
276
317
|
}
|
|
277
318
|
|
|
@@ -289,11 +330,20 @@ async function readPieces(step, file) {
|
|
|
289
330
|
}
|
|
290
331
|
|
|
291
332
|
self.onmessage = async ({ data }) => {
|
|
333
|
+
if (data.type === "cancel") {
|
|
334
|
+
cancelled = true;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
292
337
|
if (data.type === "refine") {
|
|
293
338
|
await refine(data);
|
|
294
339
|
return;
|
|
295
340
|
}
|
|
341
|
+
if (data.type === "structure") {
|
|
342
|
+
await structure(data);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
296
345
|
if (data.type !== "import") return;
|
|
346
|
+
cancelled = false;
|
|
297
347
|
const started = performance.now();
|
|
298
348
|
try {
|
|
299
349
|
const step = await supervisor(data.threads && self.crossOriginIsolated);
|
|
@@ -319,11 +369,15 @@ self.onmessage = async ({ data }) => {
|
|
|
319
369
|
coarserFaces: stats[16], slices,
|
|
320
370
|
importMs: Math.round(performance.now() - started), cacheBytes: blob.length,
|
|
321
371
|
supervisorMB: Math.round(step.HEAPU8.length / 2 ** 20) };
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
const note = { name: data.name, size: data.file.size, lastModified: data.file.lastModified,
|
|
373
|
+
importedAt: Date.now(), ...result };
|
|
374
|
+
if (data.key) await keepImport(data.key, blob, note);
|
|
375
|
+
else await keep(data.name, blob, note);
|
|
324
376
|
self.postMessage({ type: "done", ...result });
|
|
325
|
-
if (data.kind === "step"
|
|
377
|
+
if (data.kind === "step" && data.keepSource !== false) {
|
|
378
|
+
await (data.key ? keepSourceByKey(data.key, data.file) : keepSource(data.name, data.file)).catch(() => {});
|
|
379
|
+
}
|
|
326
380
|
} catch (error) {
|
|
327
|
-
|
|
381
|
+
failed(error);
|
|
328
382
|
}
|
|
329
383
|
};
|
package/web/importers.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Runs the page's imports through ImportQueue, in two lanes: what a person opens, and what the
|
|
2
|
+
// gallery wants. What a person opens never waits behind the gallery: on a desktop the gallery has
|
|
3
|
+
// an importer of its own, which starts nothing new while anything is being opened; on a phone, one
|
|
4
|
+
// importer is shared, so the gallery's running job is cancelled for an open and waits again.
|
|
5
|
+
//
|
|
6
|
+
// The live lane's jobs are files being opened ("open"), in the order asked, and refinements of what
|
|
7
|
+
// is open ("visible"): a file being opened cancels a refinement running, which is not tried again;
|
|
8
|
+
// the engine asks anew. Who runs a job, and how, is the caller's: `run(signal)` returns a promise,
|
|
9
|
+
// and stops early, rejecting or not, once `signal` aborts.
|
|
10
|
+
|
|
11
|
+
import { ImportQueue } from "./import-queue.js";
|
|
12
|
+
|
|
13
|
+
export class Importers {
|
|
14
|
+
#live = new ImportQueue({ slots: 1 });
|
|
15
|
+
#liveWants = [];
|
|
16
|
+
#gallery;
|
|
17
|
+
#shared;
|
|
18
|
+
#jobs = new Map(); // id -> { lane, run, resolve, reject, controller }
|
|
19
|
+
#sequence = 0;
|
|
20
|
+
|
|
21
|
+
/** `shared`: one importer for both lanes, as on a phone, where prefetching is off too. */
|
|
22
|
+
constructor({ shared = false, gallerySlots = 1 } = {}) {
|
|
23
|
+
this.#shared = shared;
|
|
24
|
+
this.#gallery = new ImportQueue({ slots: gallerySlots, prefetch: !shared });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Runs a live job: a file being opened (band "open") or a refinement ("visible"). */
|
|
28
|
+
live(id, run, { band = "open" } = {}) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.#jobs.set(id, { lane: "live", run, resolve, reject, controller: null });
|
|
31
|
+
this.#liveWants.push({ id, band, order: this.#sequence++ });
|
|
32
|
+
this.#live.want(this.#liveWants);
|
|
33
|
+
this.#pump();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Stops a live job: dropped if it waits, aborted if it runs. */
|
|
38
|
+
cancel(id) {
|
|
39
|
+
const job = this.#jobs.get(id);
|
|
40
|
+
if (!job) return;
|
|
41
|
+
if (job.controller) {
|
|
42
|
+
job.controller.abort();
|
|
43
|
+
} else {
|
|
44
|
+
this.#dropLive(id);
|
|
45
|
+
this.#jobs.delete(id);
|
|
46
|
+
job.resolve({ type: "cancelled" });
|
|
47
|
+
this.#pump();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replaces what the gallery wants: [{ id, band, order, bytes, run }]. A job not wanted any more
|
|
53
|
+
* is dropped before it starts and runs on once started, unless a job in view needs its slot.
|
|
54
|
+
* Each job's result goes to `onDone(id, result)`; its failure to `onFailed(id, error)`.
|
|
55
|
+
*/
|
|
56
|
+
want(jobs, { onDone = () => {}, onFailed = () => {} } = {}) {
|
|
57
|
+
this.#onDone = onDone;
|
|
58
|
+
this.#onFailed = onFailed;
|
|
59
|
+
for (const job of jobs) {
|
|
60
|
+
const known = this.#jobs.get(job.id);
|
|
61
|
+
if (!known) this.#jobs.set(job.id, { lane: "gallery", run: job.run, controller: null });
|
|
62
|
+
else if (known.lane === "gallery") known.run = job.run;
|
|
63
|
+
}
|
|
64
|
+
this.#gallery.want(jobs.map(({ id, band, order, bytes }) => ({ id, band, order, bytes })));
|
|
65
|
+
for (const [id, job] of this.#jobs) {
|
|
66
|
+
if (job.lane === "gallery" && !job.controller && !this.#gallery.bandOf(id)) this.#jobs.delete(id);
|
|
67
|
+
}
|
|
68
|
+
this.#pump();
|
|
69
|
+
}
|
|
70
|
+
#onDone = () => {};
|
|
71
|
+
#onFailed = () => {};
|
|
72
|
+
|
|
73
|
+
setPrefetch(allowed) {
|
|
74
|
+
this.#gallery.setPrefetch(allowed && !this.#shared);
|
|
75
|
+
this.#pump();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether anything is being opened, waiting or running. */
|
|
79
|
+
get opening() {
|
|
80
|
+
return this.#live.waiting().length > 0 || this.#live.running().length > 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Ids running now, by lane, for checks. */
|
|
84
|
+
running() {
|
|
85
|
+
return { live: this.#live.running(), gallery: this.#gallery.running() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#pump() {
|
|
89
|
+
for (const id of this.#live.toCancel()) this.#jobs.get(id)?.controller?.abort();
|
|
90
|
+
for (let id = this.#live.next(); id !== null; id = this.#live.next()) this.#start("live", id);
|
|
91
|
+
|
|
92
|
+
if (this.opening) {
|
|
93
|
+
// One importer for both: the gallery's running job makes way, and waits again.
|
|
94
|
+
if (this.#shared) for (const id of this.#gallery.running()) this.#jobs.get(id)?.controller?.abort();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const id of this.#gallery.toCancel()) this.#jobs.get(id)?.controller?.abort();
|
|
98
|
+
for (let id = this.#gallery.next(); id !== null; id = this.#gallery.next()) this.#start("gallery", id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#dropLive(id) {
|
|
102
|
+
this.#liveWants = this.#liveWants.filter((want) => want.id !== id);
|
|
103
|
+
this.#live.want(this.#liveWants);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#start(lane, id) {
|
|
107
|
+
const job = this.#jobs.get(id);
|
|
108
|
+
const queue = lane === "live" ? this.#live : this.#gallery;
|
|
109
|
+
if (!job) {
|
|
110
|
+
queue.finish(id);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
job.controller = controller;
|
|
115
|
+
Promise.resolve()
|
|
116
|
+
.then(() => job.run(controller.signal))
|
|
117
|
+
.then((result) => ({ result }), (error) => ({ error }))
|
|
118
|
+
.then(({ result, error }) => {
|
|
119
|
+
job.controller = null;
|
|
120
|
+
const aborted = controller.signal.aborted;
|
|
121
|
+
if (lane === "live") {
|
|
122
|
+
queue.finish(id);
|
|
123
|
+
this.#dropLive(id);
|
|
124
|
+
this.#jobs.delete(id);
|
|
125
|
+
if (aborted) job.resolve({ type: "cancelled" });
|
|
126
|
+
else if (error) job.reject(error);
|
|
127
|
+
else job.resolve(result);
|
|
128
|
+
} else if (aborted) {
|
|
129
|
+
queue.stopped(id); // waits again, if still wanted
|
|
130
|
+
if (!queue.bandOf(id)) this.#jobs.delete(id);
|
|
131
|
+
} else {
|
|
132
|
+
queue.finish(id);
|
|
133
|
+
this.#jobs.delete(id);
|
|
134
|
+
if (error) this.#onFailed(id, error);
|
|
135
|
+
else this.#onDone(id, result);
|
|
136
|
+
}
|
|
137
|
+
this.#pump();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
package/web/index.html
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<link rel="icon" href="icons/icon-32.png" sizes="32x32" type="image/png">
|
|
9
9
|
<link rel="apple-touch-icon" href="icons/icon-180.png">
|
|
10
10
|
<link rel="stylesheet" href="viewer.css">
|
|
11
|
+
<link rel="stylesheet" href="gallery.css">
|
|
11
12
|
</head>
|
|
12
13
|
<body>
|
|
13
14
|
<canvas id="viewport"></canvas>
|