@embedpdf/plugin-render 3.0.0-next.5 → 3.0.0-next.6
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/dist/index.cjs +452 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -40
- package/dist/index.d.ts +127 -40
- package/dist/index.js +453 -136
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, definePlugin
|
|
1
|
+
import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, definePlugin } from "@embedpdf/core";
|
|
2
2
|
import { intersectRects } from "@embedpdf/core-geometry";
|
|
3
3
|
//#region src/paint-plan.ts
|
|
4
4
|
const EMPTY_TILE_PLAN = {
|
|
@@ -7,36 +7,61 @@ const EMPTY_TILE_PLAN = {
|
|
|
7
7
|
fetching: [],
|
|
8
8
|
stamp: "empty"
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
/** The ×2 client pyramid used for lattice deployments without a tiles block. */
|
|
11
|
+
const DEFAULT_TILE_PYRAMID = [
|
|
12
|
+
1,
|
|
13
|
+
2,
|
|
14
|
+
4,
|
|
15
|
+
8,
|
|
16
|
+
16,
|
|
17
|
+
32
|
|
18
|
+
];
|
|
19
|
+
function resolveRenderOptions(options) {
|
|
20
|
+
const tiles = options.tiles === false ? void 0 : options.tiles;
|
|
11
21
|
return {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
fullPage: {
|
|
23
|
+
maxWidth: options.fullPage?.maxWidth ?? 640,
|
|
24
|
+
maxWidthExplicit: options.fullPage?.maxWidth !== void 0,
|
|
25
|
+
quantize: options.fullPage?.quantize ?? "exact"
|
|
26
|
+
},
|
|
27
|
+
tiles: {
|
|
28
|
+
enabled: options.tiles !== false,
|
|
29
|
+
size: tiles?.size ?? 512,
|
|
30
|
+
quantize: tiles?.quantize ?? "exact",
|
|
31
|
+
maxScale: tiles?.maxScale ?? 128,
|
|
32
|
+
bleedPx: tiles?.bleed ?? 1,
|
|
33
|
+
engageAt: tiles?.engageAt,
|
|
34
|
+
prefetchMargin: tiles?.prefetch?.margin ?? .5,
|
|
35
|
+
velocityBias: tiles?.prefetch?.velocityBias ?? true,
|
|
36
|
+
settleMs: tiles?.settleMs ?? 150,
|
|
37
|
+
fadeMs: tiles?.fadeMs ?? 0,
|
|
38
|
+
fallbackPyramid: DEFAULT_TILE_PYRAMID
|
|
39
|
+
},
|
|
40
|
+
format: options.format,
|
|
41
|
+
quality: options.quality,
|
|
42
|
+
debug: options.debug ?? false
|
|
25
43
|
};
|
|
26
44
|
}
|
|
27
45
|
//#endregion
|
|
28
46
|
//#region src/raster-store.ts
|
|
29
47
|
/**
|
|
30
48
|
* Client-side mirror of the server's one-door read-through: per-key
|
|
31
|
-
* SINGLEFLIGHT + LRU over encoded page images.
|
|
49
|
+
* SINGLEFLIGHT + cost-budgeted LRU over encoded page images.
|
|
32
50
|
*
|
|
33
51
|
* The key is the raster's canonical identity (conformed viewport +
|
|
34
|
-
* annotations flag + epoch — see `renderSourceKey`), so the
|
|
35
|
-
* caching work: every ask
|
|
52
|
+
* annotations flag + epoch — see `renderSourceKey`), so the render points do
|
|
53
|
+
* the caching work: every ask at one point is the same key, and
|
|
36
54
|
* re-crossings (zoom 1280 → 2560 → back) resolve instantly from the LRU
|
|
37
55
|
* instead of re-rendering. Epoch bumps mint new keys; stale-epoch entries
|
|
38
56
|
* age out through the LRU (they are never re-requested).
|
|
39
57
|
*
|
|
58
|
+
* The budget is COST, approximately bytes: an entry costs its encoded byte
|
|
59
|
+
* length when the handle carries bytes (local), its pixel count as a
|
|
60
|
+
* decoded-order estimate otherwise (URL-backed cloud handles), and 1 when
|
|
61
|
+
* neither is known. Entry counts lie — a 512² webp tile and a BMP full page
|
|
62
|
+
* differ by 100× — and long deep-zoom sessions cycle hundreds of tile keys,
|
|
63
|
+
* so the budget is what actually bounds memory.
|
|
64
|
+
*
|
|
40
65
|
* Consumer aborts are REFCOUNTED: a caller abandoning a fetch (camera moved,
|
|
41
66
|
* layer unmounted) detaches without killing it for other consumers; the
|
|
42
67
|
* underlying engine call aborts only when the LAST in-flight consumer leaves.
|
|
@@ -44,10 +69,11 @@ function resolveTiling(config) {
|
|
|
44
69
|
* is a no-op by design.
|
|
45
70
|
*/
|
|
46
71
|
var RasterStore = class {
|
|
47
|
-
constructor(
|
|
48
|
-
this.
|
|
72
|
+
constructor(maxCost = 128 * 1024 * 1024) {
|
|
73
|
+
this.maxCost = maxCost;
|
|
49
74
|
this.entries = /* @__PURE__ */ new Map();
|
|
50
75
|
this.tick = 0;
|
|
76
|
+
this.total = 0;
|
|
51
77
|
}
|
|
52
78
|
acquire(key, fetch, signal) {
|
|
53
79
|
const existing = this.entries.get(key);
|
|
@@ -64,9 +90,12 @@ var RasterStore = class {
|
|
|
64
90
|
};
|
|
65
91
|
entry.promise = fetch(abort.signal).then((handle) => {
|
|
66
92
|
entry.handle = handle;
|
|
93
|
+
entry.cost = costOf(handle);
|
|
94
|
+
this.total += entry.cost;
|
|
95
|
+
this.evict();
|
|
67
96
|
return handle;
|
|
68
97
|
}, (err) => {
|
|
69
|
-
|
|
98
|
+
this.drop(key, entry);
|
|
70
99
|
throw err;
|
|
71
100
|
});
|
|
72
101
|
entry.promise.catch(() => {});
|
|
@@ -83,6 +112,10 @@ var RasterStore = class {
|
|
|
83
112
|
get size() {
|
|
84
113
|
return this.entries.size;
|
|
85
114
|
}
|
|
115
|
+
/** Total cost (~bytes) of resolved entries — the budgeted quantity. */
|
|
116
|
+
get costUsed() {
|
|
117
|
+
return this.total;
|
|
118
|
+
}
|
|
86
119
|
attach(key, entry, signal) {
|
|
87
120
|
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
88
121
|
entry.refs += 1;
|
|
@@ -101,7 +134,7 @@ var RasterStore = class {
|
|
|
101
134
|
entry.refs -= 1;
|
|
102
135
|
if (entry.refs <= 0 && entry.handle === void 0) {
|
|
103
136
|
entry.abort.abort(abortReason(signal));
|
|
104
|
-
|
|
137
|
+
this.drop(key, entry);
|
|
105
138
|
}
|
|
106
139
|
reject(abortReason(signal));
|
|
107
140
|
};
|
|
@@ -121,19 +154,72 @@ var RasterStore = class {
|
|
|
121
154
|
});
|
|
122
155
|
});
|
|
123
156
|
}
|
|
157
|
+
/** Remove an entry, keeping the cost ledger honest. */
|
|
158
|
+
drop(key, entry) {
|
|
159
|
+
if (this.entries.get(key) !== entry) return;
|
|
160
|
+
this.entries.delete(key);
|
|
161
|
+
if (entry.cost !== void 0) this.total -= entry.cost;
|
|
162
|
+
}
|
|
124
163
|
evict() {
|
|
125
|
-
if (this.
|
|
164
|
+
if (this.total <= this.maxCost) return;
|
|
126
165
|
const candidates = [...this.entries.entries()].filter(([, e]) => e.refs <= 0 && e.handle !== void 0).sort((a, b) => a[1].used - b[1].used);
|
|
127
|
-
for (const [key] of candidates) {
|
|
128
|
-
if (this.
|
|
129
|
-
this.
|
|
166
|
+
for (const [key, entry] of candidates) {
|
|
167
|
+
if (this.total <= this.maxCost) return;
|
|
168
|
+
this.drop(key, entry);
|
|
130
169
|
}
|
|
131
170
|
}
|
|
132
171
|
};
|
|
172
|
+
/** Approximate memory cost: encoded bytes when known, pixel count as a
|
|
173
|
+
* decoded-order estimate for URL handles, 1 when nothing is known. */
|
|
174
|
+
function costOf(handle) {
|
|
175
|
+
if (handle.source?.kind === "bytes") return handle.source.bytes.byteLength;
|
|
176
|
+
if (handle.width !== void 0 && handle.height !== void 0) return handle.width * handle.height;
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
133
179
|
function abortReason(signal) {
|
|
134
180
|
return signal.reason ?? new DOMException("aborted", "AbortError");
|
|
135
181
|
}
|
|
136
182
|
//#endregion
|
|
183
|
+
//#region src/strategy.ts
|
|
184
|
+
const ascending = (values) => [...values].sort((a, b) => a - b);
|
|
185
|
+
/** Rung list capped by the budget — always keeps at least the smallest rung. */
|
|
186
|
+
const capWidths = (widths, maxWidth) => {
|
|
187
|
+
const sorted = ascending(widths);
|
|
188
|
+
const kept = sorted.filter((w) => w <= maxWidth);
|
|
189
|
+
return kept.length ? kept : sorted.slice(0, 1);
|
|
190
|
+
};
|
|
191
|
+
function resolveStrategy(policy, options) {
|
|
192
|
+
const lattice = policy.kind === "lattice";
|
|
193
|
+
const widths = lattice ? options.fullPage.maxWidthExplicit ? capWidths(policy.fullPage.widths, options.fullPage.maxWidth) : ascending(policy.fullPage.widths) : options.fullPage.quantize === "exact" ? null : capWidths(options.fullPage.quantize, options.fullPage.maxWidth);
|
|
194
|
+
const clientPyramid = options.tiles.quantize === "exact" ? null : ascending(options.tiles.quantize);
|
|
195
|
+
const pyramid = lattice ? policy.tiles?.scales ? ascending(policy.tiles.scales) : clientPyramid ?? options.tiles.fallbackPyramid : clientPyramid;
|
|
196
|
+
const tileSize = lattice && policy.tiles?.tileSizes.length ? policy.tiles.tileSizes.includes(options.tiles.size) ? options.tiles.size : policy.tiles.tileSizes[0] : options.tiles.size;
|
|
197
|
+
const engageAt = options.tiles.engageAt ?? (widths === null ? 1 : 1.25);
|
|
198
|
+
const requested = options.format;
|
|
199
|
+
const format = requested === void 0 ? void 0 : !lattice || policy.formats.includes(requested) ? requested : policy.formats[0];
|
|
200
|
+
return {
|
|
201
|
+
widths,
|
|
202
|
+
maxWidth: options.fullPage.maxWidth,
|
|
203
|
+
pyramid,
|
|
204
|
+
tileSize,
|
|
205
|
+
engageAt,
|
|
206
|
+
tileMaxScale: options.tiles.maxScale,
|
|
207
|
+
format,
|
|
208
|
+
quality: options.quality
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The ONE base-sizing function — four callers: `renderPage`,
|
|
213
|
+
* `renderSourceKey`, `conformViewport`, and tile engagement. Exact mode
|
|
214
|
+
* returns the (settled) demand itself up to the budget; ladder mode snaps
|
|
215
|
+
* UP and caps at the top rung.
|
|
216
|
+
*/
|
|
217
|
+
function baseAskWidth(strategy, demandWidth) {
|
|
218
|
+
const wanted = Math.max(1, Math.round(demandWidth));
|
|
219
|
+
if (strategy.widths === null) return Math.min(wanted, strategy.maxWidth);
|
|
220
|
+
return strategy.widths.find((w) => w >= wanted) ?? strategy.widths[strategy.widths.length - 1];
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
137
223
|
//#region src/tiles.ts
|
|
138
224
|
function tileGrid(page, scale, tileSize) {
|
|
139
225
|
return {
|
|
@@ -174,14 +260,32 @@ function tilePaintRect(grid, page, c) {
|
|
|
174
260
|
height: Math.min(span, page.height - y)
|
|
175
261
|
};
|
|
176
262
|
}
|
|
177
|
-
/** Engine target rect
|
|
178
|
-
|
|
179
|
-
|
|
263
|
+
/** Engine target rect (y-UP PDF user space, top > bottom) for an arbitrary
|
|
264
|
+
* y-down page-point rect. */
|
|
265
|
+
function toEngineRect(page, rect) {
|
|
266
|
+
return {
|
|
267
|
+
left: rect.x,
|
|
268
|
+
right: rect.x + rect.width,
|
|
269
|
+
top: page.height - rect.y,
|
|
270
|
+
bottom: page.height - (rect.y + rect.height)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Expand a paint rect by `pt` page points per side, clamped to the page —
|
|
275
|
+
* the tile BLEED. Neighboring tiles rendered with bleed overlap by twice
|
|
276
|
+
* this amount, and the overlapping strips contain identical content (same
|
|
277
|
+
* page region, same scale), so every img edge composites over its
|
|
278
|
+
* neighbor's duplicated pixels instead of the backdrop: no AA hairlines,
|
|
279
|
+
* no bilinear edge smear, at any CSS stretch.
|
|
280
|
+
*/
|
|
281
|
+
function bleedRect(rect, pt, page) {
|
|
282
|
+
const x = Math.max(0, rect.x - pt);
|
|
283
|
+
const y = Math.max(0, rect.y - pt);
|
|
180
284
|
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
285
|
+
x,
|
|
286
|
+
y,
|
|
287
|
+
width: Math.min(page.width, rect.x + rect.width + pt) - x,
|
|
288
|
+
height: Math.min(page.height, rect.y + rect.height + pt) - y
|
|
185
289
|
};
|
|
186
290
|
}
|
|
187
291
|
/**
|
|
@@ -247,29 +351,52 @@ function snapToPyramid(scales, needed) {
|
|
|
247
351
|
* pixels available; quality per region only goes up until the want set
|
|
248
352
|
* resolves. Mechanics:
|
|
249
353
|
* - want vs paint: `plan()` schedules fetches for the want set (P0
|
|
250
|
-
* visible first
|
|
251
|
-
* paint list drawn ONLY from resolved entries —
|
|
252
|
-
* retained older generations.
|
|
253
|
-
* - release-on-occlusion: when a want-level tile reports PAINTED (the
|
|
254
|
-
* layer's
|
|
255
|
-
* visible footprint is
|
|
256
|
-
*
|
|
257
|
-
* zoom-back re-promotes
|
|
354
|
+
* visible first — center-out — then P1 prefetch when P0 is fully
|
|
355
|
+
* resolved) and returns a paint list drawn ONLY from resolved entries —
|
|
356
|
+
* current level AND retained older generations.
|
|
357
|
+
* - release-on-occlusion: when a want-level tile reports PAINTED (after the
|
|
358
|
+
* layer's first presentation opportunity), retained sources whose
|
|
359
|
+
* visible footprint is
|
|
360
|
+
* covered by painted want tiles leave the paint list. Their bytes stay
|
|
361
|
+
* in the RasterStore (demotion, not eviction) — zoom-back re-promotes
|
|
362
|
+
* from cache.
|
|
258
363
|
* - epoch exception: an invalidation bump means retained pixels are
|
|
259
364
|
* WRONG — everything of the old epoch drops immediately.
|
|
260
365
|
*
|
|
366
|
+
* Levels come from the resolved STRATEGY (policy ∧ options): a pyramid
|
|
367
|
+
* under a lattice (or the opt-in client ladder), the EXACT settled scale
|
|
368
|
+
* under exact mode — where the level identity is the demand's device width
|
|
369
|
+
* across the page, so keys stay integer and stable. The retention/coverage
|
|
370
|
+
* math is generic over any mix of retained scales.
|
|
371
|
+
*
|
|
261
372
|
* `plan()` is called from selectors: it MUST NOT dispatch. Fetch kickoff
|
|
262
373
|
* is idempotent (the store singleflights); resolution handlers dispatch
|
|
263
374
|
* the wake-up (`onAdvance`) that makes subscribed layers recompute.
|
|
264
375
|
*/
|
|
376
|
+
/** Backpressure: raw rasters in flight at once (render + encode transit). */
|
|
377
|
+
const MAX_IN_FLIGHT = 8;
|
|
378
|
+
/** Stage-less (no visibleRect) demand is capped to this many whole-page tiles. */
|
|
379
|
+
const STAGELESS_TILE_CAP = 64;
|
|
265
380
|
var TileManager = class {
|
|
266
381
|
constructor(deps) {
|
|
267
382
|
this.deps = deps;
|
|
268
383
|
this.pages = /* @__PURE__ */ new Map();
|
|
384
|
+
this.strategyMemo = null;
|
|
385
|
+
this.inFlight = 0;
|
|
386
|
+
this.warnedStageless = false;
|
|
269
387
|
}
|
|
270
|
-
|
|
271
|
-
const { config } = this.deps;
|
|
388
|
+
strategy() {
|
|
272
389
|
const policy = this.deps.getPolicy();
|
|
390
|
+
if (this.strategyMemo?.policy !== policy) this.strategyMemo = {
|
|
391
|
+
policy,
|
|
392
|
+
strategy: resolveStrategy(policy, this.deps.options)
|
|
393
|
+
};
|
|
394
|
+
return this.strategyMemo.strategy;
|
|
395
|
+
}
|
|
396
|
+
plan(pon, demand, includeAnnotations) {
|
|
397
|
+
const { options } = this.deps;
|
|
398
|
+
if (!options.tiles.enabled) return EMPTY_TILE_PLAN;
|
|
399
|
+
const strategy = this.strategy();
|
|
273
400
|
const page = this.deps.getPageSize(pon);
|
|
274
401
|
if (!page) return EMPTY_TILE_PLAN;
|
|
275
402
|
const epoch = this.deps.getEpoch(pon, includeAnnotations);
|
|
@@ -277,26 +404,53 @@ var TileManager = class {
|
|
|
277
404
|
if (state.epoch !== epoch) {
|
|
278
405
|
this.abortAll(state);
|
|
279
406
|
state.entries.clear();
|
|
407
|
+
state.failedKeys.clear();
|
|
280
408
|
state.epoch = epoch;
|
|
281
409
|
state.wantScale = null;
|
|
410
|
+
state.wantWidth = null;
|
|
282
411
|
state.planCache = null;
|
|
283
412
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
})) : demand.desiredDeviceWidth;
|
|
288
|
-
if (demand.desiredDeviceWidth / baseSupplied <= config.engageAt) {
|
|
413
|
+
const supplied = baseAskWidth(strategy, demand.desiredDeviceWidth);
|
|
414
|
+
const deficit = demand.desiredDeviceWidth / supplied;
|
|
415
|
+
if (deficit <= strategy.engageAt) {
|
|
289
416
|
if (state.entries.size || state.wantScale !== null) {
|
|
417
|
+
this.deps.debug?.(`disengage pon=${pon} (deficit ${deficit.toFixed(2)})`);
|
|
290
418
|
this.abortAll(state);
|
|
291
419
|
state.entries.clear();
|
|
420
|
+
state.failedKeys.clear();
|
|
292
421
|
state.wantScale = null;
|
|
422
|
+
state.wantWidth = null;
|
|
293
423
|
state.planCache = null;
|
|
294
424
|
}
|
|
295
425
|
return EMPTY_TILE_PLAN;
|
|
296
426
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
427
|
+
let wantWidth;
|
|
428
|
+
let wantScale;
|
|
429
|
+
if (strategy.pyramid) {
|
|
430
|
+
wantScale = snapToPyramid(strategy.pyramid, demand.desiredDeviceWidth / page.width);
|
|
431
|
+
wantWidth = Math.round(wantScale * page.width);
|
|
432
|
+
} else {
|
|
433
|
+
wantWidth = Math.min(Math.max(1, Math.round(demand.desiredDeviceWidth)), Math.round(strategy.tileMaxScale * page.width));
|
|
434
|
+
wantScale = wantWidth / page.width;
|
|
435
|
+
}
|
|
436
|
+
if (!demand.visibleRect) {
|
|
437
|
+
const maxStagelessWidth = Math.max(strategy.tileSize, Math.floor(strategy.tileSize * Math.sqrt(STAGELESS_TILE_CAP * page.width / page.height)));
|
|
438
|
+
if (wantWidth > maxStagelessWidth) {
|
|
439
|
+
if (!this.warnedStageless) {
|
|
440
|
+
this.warnedStageless = true;
|
|
441
|
+
console.warn(`[render] stage-less tile demand of ${Math.round(demand.desiredDeviceWidth)} device px would tile the whole page — clamped to ${maxStagelessWidth}. Supply a visibleRect (host the page in a Stage) for sharp deep zoom.`);
|
|
442
|
+
}
|
|
443
|
+
if (strategy.pyramid) {
|
|
444
|
+
const fitting = strategy.pyramid.filter((s) => Math.round(s * page.width) <= maxStagelessWidth);
|
|
445
|
+
wantScale = fitting.length ? fitting[fitting.length - 1] : strategy.pyramid[0];
|
|
446
|
+
wantWidth = Math.round(wantScale * page.width);
|
|
447
|
+
} else {
|
|
448
|
+
wantWidth = maxStagelessWidth;
|
|
449
|
+
wantScale = wantWidth / page.width;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const grid = tileGrid(page, wantScale, strategy.tileSize);
|
|
300
454
|
const visible = demand.visibleRect ?? {
|
|
301
455
|
x: 0,
|
|
302
456
|
y: 0,
|
|
@@ -305,26 +459,43 @@ var TileManager = class {
|
|
|
305
459
|
};
|
|
306
460
|
state.lastVisible = visible;
|
|
307
461
|
const p0 = tilesInRect(grid, page, visible);
|
|
308
|
-
const ring = inflateRect(visible,
|
|
462
|
+
const ring = inflateRect(visible, options.tiles.prefetchMargin, options.tiles.velocityBias ? demand.velocity : void 0);
|
|
309
463
|
const p0Keys = new Set(p0.map((c) => coordKey(c)));
|
|
310
464
|
const p1 = tilesInRect(grid, page, ring).filter((c) => !p0Keys.has(coordKey(c)));
|
|
311
|
-
this.schedule(pon, state, page, grid, wantScale, includeAnnotations, epoch, p0, p1);
|
|
312
|
-
|
|
465
|
+
this.schedule(pon, state, page, grid, wantScale, wantWidth, includeAnnotations, epoch, p0, p1, visible);
|
|
466
|
+
if (this.releaseCovered(pon, state, page, includeAnnotations)) {
|
|
467
|
+
state.version += 1;
|
|
468
|
+
state.planCache = null;
|
|
469
|
+
}
|
|
470
|
+
const demandKey = `w${wantWidth}|${rectKey(visible)}|e${epoch}`;
|
|
313
471
|
if (state.planCache && state.planCache.demandKey === demandKey && state.planCache.version === state.version) return state.planCache.plan;
|
|
314
472
|
const paint = [];
|
|
315
473
|
const fetching = [];
|
|
474
|
+
const stale = [];
|
|
475
|
+
const bleedPx = this.deps.options.tiles.bleedPx;
|
|
316
476
|
for (const entry of state.entries.values()) {
|
|
317
477
|
const visIntersect = intersectRects(entry.rect, visible);
|
|
318
|
-
if (entry.
|
|
319
|
-
if (visIntersect.width > 0 && visIntersect.height > 0)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
478
|
+
if (entry.resolved) {
|
|
479
|
+
if (visIntersect.width > 0 && visIntersect.height > 0) {
|
|
480
|
+
const handle = this.deps.store.peek(entry.key);
|
|
481
|
+
if (!handle) {
|
|
482
|
+
stale.push(entry.key);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
paint.push({
|
|
486
|
+
key: entry.key,
|
|
487
|
+
scale: entry.scale,
|
|
488
|
+
rect: bleedPx > 0 ? bleedRect(entry.rect, bleedPx / entry.scale, page) : entry.rect,
|
|
489
|
+
z: 0,
|
|
490
|
+
handle
|
|
491
|
+
});
|
|
492
|
+
}
|
|
326
493
|
} else if (entry.scale === wantScale) fetching.push(entry.key);
|
|
327
494
|
}
|
|
495
|
+
for (const key of stale) state.entries.delete(key);
|
|
496
|
+
const rank = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const s of [...new Set(paint.map((p) => p.scale))].sort((a, b) => a - b)) rank.set(s, rank.size);
|
|
498
|
+
for (const p of paint) p.z = rank.get(p.scale);
|
|
328
499
|
paint.sort((a, b) => a.z - b.z || a.key.localeCompare(b.key));
|
|
329
500
|
const plan = {
|
|
330
501
|
engaged: true,
|
|
@@ -339,18 +510,31 @@ var TileManager = class {
|
|
|
339
510
|
};
|
|
340
511
|
return plan;
|
|
341
512
|
}
|
|
342
|
-
/** The layer's
|
|
513
|
+
/** The layer's painted report: this key's pixels had a presentation opportunity. */
|
|
343
514
|
sourcePainted(pon, key) {
|
|
344
515
|
const state = this.pages.get(pon);
|
|
345
516
|
const entry = state?.entries.get(key);
|
|
346
517
|
if (!state || !entry || entry.painted) return;
|
|
347
518
|
entry.painted = true;
|
|
348
|
-
this.
|
|
519
|
+
const page = this.deps.getPageSize(pon);
|
|
520
|
+
if (page) this.releaseCovered(pon, state, page, annotationsOf(key));
|
|
349
521
|
state.version += 1;
|
|
350
522
|
state.planCache = null;
|
|
351
523
|
this.deps.onAdvance(pon);
|
|
352
524
|
}
|
|
353
|
-
/**
|
|
525
|
+
/**
|
|
526
|
+
* The inverse report: this key's <img> left the DOM (pan-away, plan drop),
|
|
527
|
+
* so its pixels are NOT currently compositable. Without this, a tile that
|
|
528
|
+
* unmounts and later remounts is still counted as painted while its new
|
|
529
|
+
* <img> re-decodes — and an adjacent fresh `sourcePainted` could release
|
|
530
|
+
* retained coarse coverage over a region that momentarily has no sharp
|
|
531
|
+
* pixels. Painted is a statement about the SCREEN, so it follows the DOM.
|
|
532
|
+
*/
|
|
533
|
+
sourceUnpainted(pon, key) {
|
|
534
|
+
const entry = this.pages.get(pon)?.entries.get(key);
|
|
535
|
+
if (entry) entry.painted = false;
|
|
536
|
+
}
|
|
537
|
+
/** A lens unmounted its tile plane: stop fetching, drop bookkeeping.
|
|
354
538
|
* Resolved bytes stay in the RasterStore for a re-mount. */
|
|
355
539
|
releasePage(pon) {
|
|
356
540
|
const state = this.pages.get(pon);
|
|
@@ -364,7 +548,9 @@ var TileManager = class {
|
|
|
364
548
|
state = {
|
|
365
549
|
epoch: -1,
|
|
366
550
|
wantScale: null,
|
|
551
|
+
wantWidth: null,
|
|
367
552
|
entries: /* @__PURE__ */ new Map(),
|
|
553
|
+
failedKeys: /* @__PURE__ */ new Set(),
|
|
368
554
|
version: 0,
|
|
369
555
|
planCache: null,
|
|
370
556
|
settleTimer: null,
|
|
@@ -375,26 +561,44 @@ var TileManager = class {
|
|
|
375
561
|
}
|
|
376
562
|
return state;
|
|
377
563
|
}
|
|
378
|
-
schedule(pon, state, page, grid, wantScale, includeAnnotations, epoch, p0, p1) {
|
|
379
|
-
const {
|
|
380
|
-
const firstEngage = state.
|
|
381
|
-
const levelChanged = state.
|
|
564
|
+
schedule(pon, state, page, grid, wantScale, wantWidth, includeAnnotations, epoch, p0, p1, visible) {
|
|
565
|
+
const { options } = this.deps;
|
|
566
|
+
const firstEngage = state.wantWidth === null;
|
|
567
|
+
const levelChanged = state.wantWidth !== null && state.wantWidth !== wantWidth;
|
|
568
|
+
if (levelChanged) state.failedKeys.clear();
|
|
382
569
|
state.wantScale = wantScale;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
570
|
+
state.wantWidth = wantWidth;
|
|
571
|
+
const wanted = new Set([...p0, ...p1].map((c) => this.tileKey(pon, wantWidth, c, includeAnnotations, epoch)));
|
|
572
|
+
for (const [key, entry] of state.entries) {
|
|
573
|
+
if (wanted.has(key)) continue;
|
|
574
|
+
if (!entry.resolved) {
|
|
575
|
+
entry.abort?.abort();
|
|
576
|
+
this.releaseSlot(entry);
|
|
577
|
+
state.entries.delete(key);
|
|
578
|
+
} else if (entry.scale === wantScale) state.entries.delete(key);
|
|
387
579
|
}
|
|
580
|
+
const cx = visible.x + visible.width / 2;
|
|
581
|
+
const cy = visible.y + visible.height / 2;
|
|
582
|
+
const span = grid.tileSize / grid.scale;
|
|
583
|
+
const orderedP0 = [...p0].sort((a, b) => {
|
|
584
|
+
const da = (a.ix + .5) * span - cx;
|
|
585
|
+
const db = (b.ix + .5) * span - cx;
|
|
586
|
+
const ea = (a.iy + .5) * span - cy;
|
|
587
|
+
const eb = (b.iy + .5) * span - cy;
|
|
588
|
+
return da * da + ea * ea - (db * db + eb * eb);
|
|
589
|
+
});
|
|
388
590
|
const kickoff = () => {
|
|
389
|
-
if (this.ensureFetches(pon, state, page, grid, includeAnnotations, epoch,
|
|
591
|
+
if (this.ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, orderedP0)) this.ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, p1);
|
|
390
592
|
};
|
|
391
|
-
if (levelChanged &&
|
|
392
|
-
|
|
593
|
+
if (levelChanged && options.tiles.settleMs > 0) {
|
|
594
|
+
this.deps.debug?.(`arm settle pon=${pon} level=${wantWidth}`);
|
|
595
|
+
state.pendingLevel = wantWidth;
|
|
393
596
|
if (state.settleTimer !== null) clearTimeout(state.settleTimer);
|
|
394
597
|
state.settleTimer = setTimeout(() => {
|
|
395
598
|
state.settleTimer = null;
|
|
396
|
-
|
|
397
|
-
|
|
599
|
+
this.deps.debug?.(`settle fired pon=${pon} level=${wantWidth} current=${state.pendingLevel === wantWidth}`);
|
|
600
|
+
if (state.pendingLevel === wantWidth) kickoff();
|
|
601
|
+
}, options.tiles.settleMs);
|
|
398
602
|
return;
|
|
399
603
|
}
|
|
400
604
|
if (state.settleTimer !== null && !levelChanged && !firstEngage) {
|
|
@@ -405,95 +609,177 @@ var TileManager = class {
|
|
|
405
609
|
kickoff();
|
|
406
610
|
}
|
|
407
611
|
/** Start missing fetches; true when every coord is already resolved. */
|
|
408
|
-
ensureFetches(pon, state, page, grid, includeAnnotations, epoch, coords) {
|
|
612
|
+
ensureFetches(pon, state, page, grid, wantWidth, includeAnnotations, epoch, coords) {
|
|
409
613
|
let allReady = true;
|
|
614
|
+
let started = 0;
|
|
615
|
+
const bleedPt = this.deps.options.tiles.bleedPx / grid.scale;
|
|
410
616
|
for (const coord of coords) {
|
|
411
|
-
const key = this.tileKey(pon,
|
|
617
|
+
const key = this.tileKey(pon, wantWidth, coord, includeAnnotations, epoch);
|
|
618
|
+
if (state.failedKeys.has(key)) continue;
|
|
412
619
|
const existing = state.entries.get(key);
|
|
413
620
|
if (existing) {
|
|
414
|
-
if (existing.
|
|
621
|
+
if (!existing.resolved) allReady = false;
|
|
415
622
|
continue;
|
|
416
623
|
}
|
|
417
624
|
allReady = false;
|
|
625
|
+
if (this.inFlight >= MAX_IN_FLIGHT) continue;
|
|
626
|
+
this.inFlight += 1;
|
|
627
|
+
started += 1;
|
|
418
628
|
const abort = new AbortController();
|
|
629
|
+
const logical = tilePaintRect(grid, page, coord);
|
|
419
630
|
const entry = {
|
|
420
631
|
key,
|
|
421
632
|
scale: grid.scale,
|
|
422
633
|
coord,
|
|
423
|
-
rect:
|
|
634
|
+
rect: logical,
|
|
635
|
+
resolved: false,
|
|
424
636
|
painted: false,
|
|
637
|
+
charged: true,
|
|
425
638
|
abort
|
|
426
639
|
};
|
|
427
640
|
state.entries.set(key, entry);
|
|
428
|
-
this.deps.store.acquire(key, (signal) => this.deps.fetchTile(pon,
|
|
641
|
+
this.deps.store.acquire(key, (signal) => this.deps.fetchTile(pon, toEngineRect(page, bleedPt > 0 ? bleedRect(logical, bleedPt, page) : logical), grid.scale, includeAnnotations, signal), abort.signal).then(() => {
|
|
642
|
+
this.releaseSlot(entry);
|
|
429
643
|
if (state.entries.get(key) !== entry) return;
|
|
430
|
-
entry.
|
|
644
|
+
entry.resolved = true;
|
|
431
645
|
state.version += 1;
|
|
432
646
|
state.planCache = null;
|
|
433
647
|
this.deps.onAdvance(pon);
|
|
434
|
-
}, () => {
|
|
435
|
-
|
|
648
|
+
}, (err) => {
|
|
649
|
+
this.releaseSlot(entry);
|
|
650
|
+
if (state.entries.get(key) !== entry) return;
|
|
651
|
+
state.entries.delete(key);
|
|
652
|
+
if (!abort.signal.aborted) {
|
|
653
|
+
state.failedKeys.add(key);
|
|
654
|
+
this.deps.debug?.(`tile failed ${key}: ${String(err)}`);
|
|
655
|
+
state.version += 1;
|
|
656
|
+
state.planCache = null;
|
|
657
|
+
this.deps.onAdvance(pon);
|
|
658
|
+
}
|
|
436
659
|
});
|
|
437
660
|
}
|
|
661
|
+
if (started > 0) this.deps.debug?.(`fetch pon=${pon} level=${wantWidth} +${started} tiles`);
|
|
438
662
|
return allReady;
|
|
439
663
|
}
|
|
440
664
|
/**
|
|
441
|
-
* The release rule
|
|
442
|
-
* sources (any
|
|
443
|
-
* covered by PAINTED want-level tiles leave the paint list.
|
|
444
|
-
*
|
|
665
|
+
* The release rule, evaluated as a PURE FUNCTION of the current painted
|
|
666
|
+
* set: retained sources (any non-want level) whose VISIBLE footprint is
|
|
667
|
+
* covered by PAINTED want-level tiles leave the paint list. Runs on every
|
|
668
|
+
* `plan()` and on every painted report — never only "against the tile
|
|
669
|
+
* that just painted": release must not depend on paint ORDER (center-out
|
|
670
|
+
* scheduling means the report that completes a retained tile's coverage
|
|
671
|
+
* routinely lands far away from it), and re-evaluating from current state
|
|
672
|
+
* also self-heals any painted-flag transition the reports raced past.
|
|
673
|
+
* Coverage is index arithmetic over the WANT grid — generic over whatever
|
|
674
|
+
* mix of retained scales exists (exact levels included).
|
|
445
675
|
*/
|
|
446
|
-
|
|
447
|
-
const wantScale = state
|
|
448
|
-
if (wantScale === null ||
|
|
449
|
-
const
|
|
450
|
-
if (!page) return;
|
|
451
|
-
const grid = tileGrid(page, wantScale, this.deps.config.tileSize);
|
|
676
|
+
releaseCovered(pon, state, page, includeAnnotations) {
|
|
677
|
+
const { wantScale, wantWidth } = state;
|
|
678
|
+
if (wantScale === null || wantWidth === null) return false;
|
|
679
|
+
const grid = tileGrid(page, wantScale, this.strategy().tileSize);
|
|
452
680
|
const paintedAt = (c) => {
|
|
453
|
-
const key = this.tileKey(
|
|
681
|
+
const key = this.tileKey(pon, wantWidth, c, includeAnnotations, state.epoch);
|
|
454
682
|
return state.entries.get(key)?.painted === true;
|
|
455
683
|
};
|
|
684
|
+
let released = false;
|
|
456
685
|
for (const [key, entry] of state.entries) {
|
|
457
|
-
if (entry.scale === wantScale || entry.
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
|
|
686
|
+
if (entry.scale === wantScale || !entry.resolved) continue;
|
|
687
|
+
const region = state.lastVisible ? intersectRects(entry.rect, state.lastVisible) : entry.rect;
|
|
688
|
+
if (regionCovered(grid, page, region, paintedAt)) {
|
|
689
|
+
state.entries.delete(key);
|
|
690
|
+
released = true;
|
|
691
|
+
} else if (this.deps.debug) {
|
|
692
|
+
const missing = tilesInRect(grid, page, region).filter((c) => !paintedAt(c));
|
|
693
|
+
const detail = missing.slice(0, 3).map((c) => {
|
|
694
|
+
const k = this.tileKey(pon, wantWidth, c, includeAnnotations, state.epoch);
|
|
695
|
+
const e = state.entries.get(k);
|
|
696
|
+
const st = !e ? "NO-ENTRY" : !e.resolved ? "PENDING" : "RESOLVED-unpainted";
|
|
697
|
+
return `${c.ix},${c.iy}=${st}`;
|
|
698
|
+
});
|
|
699
|
+
this.deps.debug(`retained ${key} blocked by: ${detail.join(" ")} (${missing.length} missing)`);
|
|
700
|
+
}
|
|
461
701
|
}
|
|
702
|
+
return released;
|
|
703
|
+
}
|
|
704
|
+
/** Test/diagnostic introspection: bookkeeping size for one page. */
|
|
705
|
+
stats(pon) {
|
|
706
|
+
return {
|
|
707
|
+
entries: this.pages.get(pon)?.entries.size ?? 0,
|
|
708
|
+
inFlight: this.inFlight
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/** Idempotent transit-slot release — the ONE place inFlight decrements. */
|
|
712
|
+
releaseSlot(entry) {
|
|
713
|
+
if (!entry.charged) return;
|
|
714
|
+
entry.charged = false;
|
|
715
|
+
this.inFlight -= 1;
|
|
462
716
|
}
|
|
463
717
|
abortAll(state) {
|
|
464
718
|
if (state.settleTimer !== null) {
|
|
465
719
|
clearTimeout(state.settleTimer);
|
|
466
720
|
state.settleTimer = null;
|
|
467
721
|
}
|
|
468
|
-
for (const entry of state.entries.values()) if (entry.
|
|
722
|
+
for (const entry of state.entries.values()) if (!entry.resolved) {
|
|
723
|
+
entry.abort?.abort();
|
|
724
|
+
this.releaseSlot(entry);
|
|
725
|
+
}
|
|
469
726
|
}
|
|
470
|
-
|
|
471
|
-
|
|
727
|
+
/** Level identity is the integer device width across the page — integer
|
|
728
|
+
* and stable for pyramid rungs and exact levels alike. The key also
|
|
729
|
+
* carries everything that determines the fetched BITMAP for a coord
|
|
730
|
+
* (tile size, bleed, encode format): a raster's identity must include
|
|
731
|
+
* its geometry, or a cached handle from one configuration could be
|
|
732
|
+
* stretched into another's rect. Format genuinely varies at runtime —
|
|
733
|
+
* the cloud policy arrives async after open and can change the resolved
|
|
734
|
+
* format under a live store. */
|
|
735
|
+
tileKey(pon, levelWidth, c, includeAnnotations, epoch) {
|
|
736
|
+
const { tiles } = this.deps.options;
|
|
737
|
+
const format = this.strategy().format;
|
|
738
|
+
const geometry = `g${tiles.size}.${tiles.bleedPx}${format ? `.${format}` : ""}`;
|
|
739
|
+
return `t:${pon}|w${levelWidth}|${c.ix},${c.iy}|a${includeAnnotations ? 1 : 0}|e${epoch}|${geometry}`;
|
|
472
740
|
}
|
|
473
741
|
};
|
|
474
|
-
const widthOf = (v) => v.kind === "width" && v.width !== void 0 ? v.width : Number.POSITIVE_INFINITY;
|
|
475
742
|
const coordKey = (c) => `${c.ix},${c.iy}`;
|
|
476
743
|
const rectKey = (r) => `${Math.round(r.x)},${Math.round(r.y)},${Math.round(r.width)},${Math.round(r.height)}`;
|
|
477
|
-
/** Stacking: position in the ascending pyramid (coarse under fine). */
|
|
478
|
-
const zOf = (pyramid, scale) => {
|
|
479
|
-
const sorted = [...pyramid].sort((a, b) => a - b);
|
|
480
|
-
const idx = sorted.indexOf(scale);
|
|
481
|
-
return idx === -1 ? sorted.findIndex((s) => s > scale) : idx;
|
|
482
|
-
};
|
|
483
|
-
const pagePonOf = (key) => Number(key.slice(2, key.indexOf("|")));
|
|
484
744
|
const annotationsOf = (key) => key.includes("|a1|");
|
|
485
745
|
//#endregion
|
|
486
746
|
//#region src/capability.ts
|
|
487
747
|
/**
|
|
488
|
-
* The render capability: the ONE
|
|
489
|
-
* (the
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* store, exposing stable source
|
|
748
|
+
* The render capability: the ONE place STRATEGY (plugin options — what the
|
|
749
|
+
* viewer spends) composes with POLICY (the engine fact — what the deployment
|
|
750
|
+
* serves). The engine never snaps; framework layers never see either;
|
|
751
|
+
* everything between — conforming demand to the resolved render points,
|
|
752
|
+
* collapsing same-key asks in the raster store, exposing stable source
|
|
753
|
+
* keys — happens here.
|
|
493
754
|
*/
|
|
494
755
|
function createRenderCapability(ctx, options = {}) {
|
|
756
|
+
const resolved = resolveRenderOptions(options);
|
|
495
757
|
const store = new RasterStore();
|
|
496
758
|
const policy = () => ctx.document()?.renderPolicy ?? CONTINUOUS_RENDER_POLICY;
|
|
759
|
+
let strategyMemo = null;
|
|
760
|
+
const strategy = () => {
|
|
761
|
+
const p = policy();
|
|
762
|
+
if (strategyMemo?.policy !== p) {
|
|
763
|
+
strategyMemo = {
|
|
764
|
+
policy: p,
|
|
765
|
+
strategy: resolveStrategy(p, resolved)
|
|
766
|
+
};
|
|
767
|
+
warnDroppedFormat(strategyMemo.strategy);
|
|
768
|
+
}
|
|
769
|
+
return strategyMemo.strategy;
|
|
770
|
+
};
|
|
771
|
+
let warnedFormat = false;
|
|
772
|
+
const warnDroppedFormat = (s) => {
|
|
773
|
+
if (warnedFormat || resolved.format === void 0 || s.format === resolved.format) return;
|
|
774
|
+
warnedFormat = true;
|
|
775
|
+
console.warn(`[render] format '${resolved.format}' is not in the deployment's formats — using '${s.format}'.`);
|
|
776
|
+
};
|
|
777
|
+
let warnedBudget = false;
|
|
778
|
+
const warnPastBudgetNoTiles = (demandWidth, supplied) => {
|
|
779
|
+
if (warnedBudget || resolved.tiles.enabled || demandWidth <= supplied * 2) return;
|
|
780
|
+
warnedBudget = true;
|
|
781
|
+
console.warn(`[render] demand is ${(demandWidth / supplied).toFixed(1)}× over the base budget and the tile plane is disabled (tiles: false) — the page rests blurry. Raise fullPage.maxWidth or re-enable tiles.`);
|
|
782
|
+
};
|
|
497
783
|
const pageOf = (pon) => (ctx.document()?.pages ?? []).find((p) => p.pageObjectNumber === pon);
|
|
498
784
|
const pageWidthOf = (pon) => {
|
|
499
785
|
const page = pageOf(pon);
|
|
@@ -501,32 +787,52 @@ function createRenderCapability(ctx, options = {}) {
|
|
|
501
787
|
return page.size.width;
|
|
502
788
|
};
|
|
503
789
|
const conformViewport = (pon, scale) => {
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
790
|
+
const s = strategy();
|
|
791
|
+
const demandWidth = scale * pageWidthOf(pon);
|
|
792
|
+
const width = baseAskWidth(s, demandWidth);
|
|
793
|
+
warnPastBudgetNoTiles(demandWidth, width);
|
|
794
|
+
return {
|
|
795
|
+
kind: "width",
|
|
796
|
+
width
|
|
508
797
|
};
|
|
509
|
-
return snapFullPageViewport(p, {
|
|
510
|
-
kind: "scale",
|
|
511
|
-
scale
|
|
512
|
-
}, { pageWidth: pageWidthOf(pon) });
|
|
513
798
|
};
|
|
514
|
-
const viewportKey = (viewport) => viewport.kind === "width" ? `w${viewport.width}` : `s${viewport.scale ?? 1}`;
|
|
515
799
|
const epochOf = (pon, includeAnnotations) => {
|
|
516
800
|
const s = ctx.getState();
|
|
517
801
|
const content = s.contentEpochs[pon] ?? 0;
|
|
518
802
|
if (!includeAnnotations) return content;
|
|
519
803
|
return content + (s.annotatedEpochs[pon] ?? 0);
|
|
520
804
|
};
|
|
805
|
+
const baseKey = (pon, viewport, annotations) => {
|
|
806
|
+
const format = strategy().format;
|
|
807
|
+
return `${pon}|w${viewport.kind === "width" ? viewport.width : 0}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}${format ? `|f${format}` : ""}`;
|
|
808
|
+
};
|
|
809
|
+
const pendingAdvance = /* @__PURE__ */ new Set();
|
|
810
|
+
let advanceScheduled = false;
|
|
811
|
+
const raf = typeof requestAnimationFrame === "function" ? (cb) => requestAnimationFrame(() => cb()) : (cb) => void setTimeout(cb, 16);
|
|
812
|
+
const wake = (pon) => {
|
|
813
|
+
pendingAdvance.add(pon);
|
|
814
|
+
if (advanceScheduled) return;
|
|
815
|
+
advanceScheduled = true;
|
|
816
|
+
raf(() => {
|
|
817
|
+
advanceScheduled = false;
|
|
818
|
+
const pons = [...pendingAdvance];
|
|
819
|
+
pendingAdvance.clear();
|
|
820
|
+
for (const p of pons) ctx.dispatch({
|
|
821
|
+
type: "PAINT_ADVANCED",
|
|
822
|
+
pon: p
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
};
|
|
521
826
|
const tiles = new TileManager({
|
|
522
827
|
store,
|
|
523
|
-
|
|
828
|
+
options: resolved,
|
|
524
829
|
getPolicy: policy,
|
|
525
830
|
getPageSize: (pon) => pageOf(pon)?.size,
|
|
526
831
|
getEpoch: epochOf,
|
|
527
832
|
fetchTile: (pon, rect, scale, includeAnnotations, signal) => {
|
|
528
833
|
const doc = ctx.doc;
|
|
529
834
|
if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
|
|
835
|
+
const s = strategy();
|
|
530
836
|
const task = doc.page(pon).render.image({
|
|
531
837
|
target: {
|
|
532
838
|
kind: "rect",
|
|
@@ -536,16 +842,16 @@ function createRenderCapability(ctx, options = {}) {
|
|
|
536
842
|
kind: "scale",
|
|
537
843
|
scale
|
|
538
844
|
},
|
|
539
|
-
includeAnnotations
|
|
845
|
+
includeAnnotations,
|
|
846
|
+
...s.format !== void 0 ? { format: s.format } : {},
|
|
847
|
+
...s.quality !== void 0 ? { quality: s.quality } : {}
|
|
540
848
|
});
|
|
541
849
|
if (signal.aborted) task.abort(signal.reason);
|
|
542
850
|
else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
|
|
543
851
|
return task;
|
|
544
852
|
},
|
|
545
|
-
onAdvance:
|
|
546
|
-
|
|
547
|
-
pon
|
|
548
|
-
})
|
|
853
|
+
onAdvance: wake,
|
|
854
|
+
...resolved.debug ? { debug: (msg) => console.debug(`[render] ${msg}`) } : {}
|
|
549
855
|
});
|
|
550
856
|
return {
|
|
551
857
|
renderPage(pon, { scale, includeAnnotations, signal }) {
|
|
@@ -553,11 +859,14 @@ function createRenderCapability(ctx, options = {}) {
|
|
|
553
859
|
if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
|
|
554
860
|
const annotations = includeAnnotations ?? true;
|
|
555
861
|
const viewport = conformViewport(pon, scale);
|
|
556
|
-
const key =
|
|
862
|
+
const key = baseKey(pon, viewport, annotations);
|
|
557
863
|
return store.acquire(key, (storeSignal) => {
|
|
864
|
+
const s = strategy();
|
|
558
865
|
const task = doc.page(pon).render.image({
|
|
559
866
|
viewport,
|
|
560
|
-
includeAnnotations: annotations
|
|
867
|
+
includeAnnotations: annotations,
|
|
868
|
+
...s.format !== void 0 ? { format: s.format } : {},
|
|
869
|
+
...s.quality !== void 0 ? { quality: s.quality } : {}
|
|
561
870
|
});
|
|
562
871
|
if (storeSignal.aborted) task.abort(storeSignal.reason);
|
|
563
872
|
else storeSignal.addEventListener("abort", () => task.abort(storeSignal.reason), { once: true });
|
|
@@ -566,10 +875,15 @@ function createRenderCapability(ctx, options = {}) {
|
|
|
566
875
|
},
|
|
567
876
|
renderSourceKey(pon, { scale, includeAnnotations }) {
|
|
568
877
|
const annotations = includeAnnotations ?? true;
|
|
569
|
-
|
|
570
|
-
return `${pon}|${viewportKey(viewport)}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}`;
|
|
878
|
+
return baseKey(pon, conformViewport(pon, scale), annotations);
|
|
571
879
|
},
|
|
572
880
|
conformViewport,
|
|
881
|
+
paintSettings() {
|
|
882
|
+
return {
|
|
883
|
+
fadeMs: resolved.tiles.fadeMs,
|
|
884
|
+
tiles: resolved.tiles.enabled
|
|
885
|
+
};
|
|
886
|
+
},
|
|
573
887
|
renderPolicy: policy,
|
|
574
888
|
tilePlan(pon, demand, opts) {
|
|
575
889
|
return tiles.plan(pon, demand, opts?.includeAnnotations ?? true);
|
|
@@ -577,6 +891,9 @@ function createRenderCapability(ctx, options = {}) {
|
|
|
577
891
|
tilePainted(pon, key) {
|
|
578
892
|
tiles.sourcePainted(pon, key);
|
|
579
893
|
},
|
|
894
|
+
tileUnpainted(pon, key) {
|
|
895
|
+
tiles.sourceUnpainted(pon, key);
|
|
896
|
+
},
|
|
580
897
|
releaseTiles(pon) {
|
|
581
898
|
tiles.releasePage(pon);
|
|
582
899
|
},
|