@embedpdf/plugin-render 2.14.4 → 3.0.0-next.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/LICENSE +192 -21
- package/dist/index.cjs +709 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -0
- package/dist/index.d.ts +233 -1
- package/dist/index.js +703 -137
- package/dist/index.js.map +1 -1
- package/package.json +19 -58
- package/dist/lib/index.d.ts +0 -6
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/render-plugin.d.ts +0 -21
- package/dist/lib/types.d.ts +0 -46
- package/dist/preact/adapter.d.ts +0 -5
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -85
- package/dist/preact/index.js.map +0 -1
- package/dist/react/adapter.d.ts +0 -2
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -84
- package/dist/react/index.js.map +0 -1
- package/dist/shared/components/index.d.ts +0 -1
- package/dist/shared/components/render-layer.d.ts +0 -35
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/use-render.d.ts +0 -11
- package/dist/shared/index.d.ts +0 -3
- package/dist/shared-preact/components/index.d.ts +0 -1
- package/dist/shared-preact/components/render-layer.d.ts +0 -35
- package/dist/shared-preact/hooks/index.d.ts +0 -1
- package/dist/shared-preact/hooks/use-render.d.ts +0 -11
- package/dist/shared-preact/index.d.ts +0 -3
- package/dist/shared-react/components/index.d.ts +0 -1
- package/dist/shared-react/components/render-layer.d.ts +0 -35
- package/dist/shared-react/hooks/index.d.ts +0 -1
- package/dist/shared-react/hooks/use-render.d.ts +0 -11
- package/dist/shared-react/index.d.ts +0 -3
- package/dist/svelte/components/RenderLayer.svelte.d.ts +0 -24
- package/dist/svelte/components/index.d.ts +0 -1
- package/dist/svelte/hooks/index.d.ts +0 -1
- package/dist/svelte/hooks/use-render.d.ts +0 -11
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -3
- package/dist/svelte/index.js +0 -116
- package/dist/svelte/index.js.map +0 -1
- package/dist/vue/components/index.d.ts +0 -1
- package/dist/vue/components/render-layer.vue.d.ts +0 -21
- package/dist/vue/hooks/index.d.ts +0 -1
- package/dist/vue/hooks/use-render.d.ts +0 -3
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -3
- package/dist/vue/index.js +0 -112
- package/dist/vue/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,141 +1,707 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
// Active document operations
|
|
11
|
-
renderPage: (options) => this.renderPage(options),
|
|
12
|
-
renderPageRect: (options) => this.renderPageRect(options),
|
|
13
|
-
renderPageRaw: (options) => this.renderPageRaw(options),
|
|
14
|
-
renderPageRectRaw: (options) => this.renderPageRectRaw(options),
|
|
15
|
-
// Document-scoped operations
|
|
16
|
-
forDocument: (documentId) => this.createRenderScope(documentId)
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
// ─────────────────────────────────────────────────────────
|
|
20
|
-
// Document Scoping
|
|
21
|
-
// ─────────────────────────────────────────────────────────
|
|
22
|
-
createRenderScope(documentId) {
|
|
23
|
-
return {
|
|
24
|
-
renderPage: (options) => this.renderPage(options, documentId),
|
|
25
|
-
renderPageRect: (options) => this.renderPageRect(options, documentId),
|
|
26
|
-
renderPageRaw: (options) => this.renderPageRaw(options, documentId),
|
|
27
|
-
renderPageRectRaw: (options) => this.renderPageRectRaw(options, documentId)
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
// ─────────────────────────────────────────────────────────
|
|
31
|
-
// Core Operations
|
|
32
|
-
// ─────────────────────────────────────────────────────────
|
|
33
|
-
renderPage({ pageIndex, options }, documentId) {
|
|
34
|
-
const id = documentId ?? this.getActiveDocumentId();
|
|
35
|
-
const coreDoc = this.coreState.core.documents[id];
|
|
36
|
-
if (!(coreDoc == null ? void 0 : coreDoc.document)) {
|
|
37
|
-
throw new Error(`Document ${id} not loaded`);
|
|
38
|
-
}
|
|
39
|
-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
|
|
40
|
-
if (!page) {
|
|
41
|
-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
|
|
42
|
-
}
|
|
43
|
-
const mergedOptions = {
|
|
44
|
-
...options ?? {},
|
|
45
|
-
withForms: (options == null ? void 0 : options.withForms) ?? this.config.withForms ?? false,
|
|
46
|
-
withAnnotations: (options == null ? void 0 : options.withAnnotations) ?? this.config.withAnnotations ?? false,
|
|
47
|
-
imageType: (options == null ? void 0 : options.imageType) ?? this.config.defaultImageType ?? "image/png",
|
|
48
|
-
imageQuality: (options == null ? void 0 : options.imageQuality) ?? this.config.defaultImageQuality ?? 0.92
|
|
49
|
-
};
|
|
50
|
-
return this.engine.renderPage(coreDoc.document, page, mergedOptions);
|
|
51
|
-
}
|
|
52
|
-
renderPageRect({ pageIndex, rect, options }, documentId) {
|
|
53
|
-
const id = documentId ?? this.getActiveDocumentId();
|
|
54
|
-
const coreDoc = this.coreState.core.documents[id];
|
|
55
|
-
if (!(coreDoc == null ? void 0 : coreDoc.document)) {
|
|
56
|
-
throw new Error(`Document ${id} not loaded`);
|
|
57
|
-
}
|
|
58
|
-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
|
|
59
|
-
if (!page) {
|
|
60
|
-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
|
|
61
|
-
}
|
|
62
|
-
const mergedOptions = {
|
|
63
|
-
...options ?? {},
|
|
64
|
-
withForms: (options == null ? void 0 : options.withForms) ?? this.config.withForms ?? false,
|
|
65
|
-
withAnnotations: (options == null ? void 0 : options.withAnnotations) ?? this.config.withAnnotations ?? false,
|
|
66
|
-
imageType: (options == null ? void 0 : options.imageType) ?? this.config.defaultImageType ?? "image/png",
|
|
67
|
-
imageQuality: (options == null ? void 0 : options.imageQuality) ?? this.config.defaultImageQuality ?? 0.92
|
|
68
|
-
};
|
|
69
|
-
return this.engine.renderPageRect(coreDoc.document, page, rect, mergedOptions);
|
|
70
|
-
}
|
|
71
|
-
// ─────────────────────────────────────────────────────────
|
|
72
|
-
// Raw Rendering (returns ImageDataLike, skips encoding)
|
|
73
|
-
// ─────────────────────────────────────────────────────────
|
|
74
|
-
renderPageRaw({ pageIndex, options }, documentId) {
|
|
75
|
-
const id = documentId ?? this.getActiveDocumentId();
|
|
76
|
-
const coreDoc = this.coreState.core.documents[id];
|
|
77
|
-
if (!(coreDoc == null ? void 0 : coreDoc.document)) {
|
|
78
|
-
throw new Error(`Document ${id} not loaded`);
|
|
79
|
-
}
|
|
80
|
-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
|
|
81
|
-
if (!page) {
|
|
82
|
-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
|
|
83
|
-
}
|
|
84
|
-
const mergedOptions = {
|
|
85
|
-
...options ?? {},
|
|
86
|
-
withForms: (options == null ? void 0 : options.withForms) ?? this.config.withForms ?? false,
|
|
87
|
-
withAnnotations: (options == null ? void 0 : options.withAnnotations) ?? this.config.withAnnotations ?? false
|
|
88
|
-
};
|
|
89
|
-
return this.engine.renderPageRaw(coreDoc.document, page, mergedOptions);
|
|
90
|
-
}
|
|
91
|
-
renderPageRectRaw({ pageIndex, rect, options }, documentId) {
|
|
92
|
-
const id = documentId ?? this.getActiveDocumentId();
|
|
93
|
-
const coreDoc = this.coreState.core.documents[id];
|
|
94
|
-
if (!(coreDoc == null ? void 0 : coreDoc.document)) {
|
|
95
|
-
throw new Error(`Document ${id} not loaded`);
|
|
96
|
-
}
|
|
97
|
-
const page = coreDoc.document.pages.find((p) => p.index === pageIndex);
|
|
98
|
-
if (!page) {
|
|
99
|
-
throw new Error(`Page ${pageIndex} not found in document ${id}`);
|
|
100
|
-
}
|
|
101
|
-
const mergedOptions = {
|
|
102
|
-
...options ?? {},
|
|
103
|
-
withForms: (options == null ? void 0 : options.withForms) ?? this.config.withForms ?? false,
|
|
104
|
-
withAnnotations: (options == null ? void 0 : options.withAnnotations) ?? this.config.withAnnotations ?? false
|
|
105
|
-
};
|
|
106
|
-
return this.engine.renderPageRectRaw(coreDoc.document, page, rect, mergedOptions);
|
|
107
|
-
}
|
|
108
|
-
// ─────────────────────────────────────────────────────────
|
|
109
|
-
// Lifecycle
|
|
110
|
-
// ─────────────────────────────────────────────────────────
|
|
111
|
-
async initialize(_config) {
|
|
112
|
-
this.logger.info("RenderPlugin", "Initialize", "Render plugin initialized");
|
|
113
|
-
}
|
|
114
|
-
async destroy() {
|
|
115
|
-
super.destroy();
|
|
116
|
-
}
|
|
1
|
+
import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, definePlugin, snapFullPageViewport } from "@embedpdf/core";
|
|
2
|
+
import { intersectRects } from "@embedpdf/core-geometry";
|
|
3
|
+
//#region src/paint-plan.ts
|
|
4
|
+
const EMPTY_TILE_PLAN = {
|
|
5
|
+
engaged: false,
|
|
6
|
+
paint: [],
|
|
7
|
+
fetching: [],
|
|
8
|
+
stamp: "empty"
|
|
117
9
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
10
|
+
function resolveTiling(config) {
|
|
11
|
+
return {
|
|
12
|
+
tileSize: config?.tileSize ?? 512,
|
|
13
|
+
scales: config?.scales ?? [
|
|
14
|
+
1,
|
|
15
|
+
2,
|
|
16
|
+
4,
|
|
17
|
+
8,
|
|
18
|
+
16,
|
|
19
|
+
32
|
|
20
|
+
],
|
|
21
|
+
engageAt: config?.engageAt ?? 1.25,
|
|
22
|
+
prefetchMargin: config?.prefetch?.margin ?? .5,
|
|
23
|
+
velocityBias: config?.prefetch?.velocityBias ?? true,
|
|
24
|
+
settleMs: config?.settleMs ?? 150
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/raster-store.ts
|
|
29
|
+
/**
|
|
30
|
+
* Client-side mirror of the server's one-door read-through: per-key
|
|
31
|
+
* SINGLEFLIGHT + LRU over encoded page images.
|
|
32
|
+
*
|
|
33
|
+
* The key is the raster's canonical identity (conformed viewport +
|
|
34
|
+
* annotations flag + epoch — see `renderSourceKey`), so the lattice does the
|
|
35
|
+
* caching work: every ask inside one rung is the same key, and rung
|
|
36
|
+
* re-crossings (zoom 1280 → 2560 → back) resolve instantly from the LRU
|
|
37
|
+
* instead of re-rendering. Epoch bumps mint new keys; stale-epoch entries
|
|
38
|
+
* age out through the LRU (they are never re-requested).
|
|
39
|
+
*
|
|
40
|
+
* Consumer aborts are REFCOUNTED: a caller abandoning a fetch (camera moved,
|
|
41
|
+
* layer unmounted) detaches without killing it for other consumers; the
|
|
42
|
+
* underlying engine call aborts only when the LAST in-flight consumer leaves.
|
|
43
|
+
* Resolved entries are kept (evictable by LRU) — an abort after resolution
|
|
44
|
+
* is a no-op by design.
|
|
45
|
+
*/
|
|
46
|
+
var RasterStore = class {
|
|
47
|
+
constructor(maxEntries = 48) {
|
|
48
|
+
this.maxEntries = maxEntries;
|
|
49
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
50
|
+
this.tick = 0;
|
|
51
|
+
}
|
|
52
|
+
acquire(key, fetch, signal) {
|
|
53
|
+
const existing = this.entries.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
existing.used = ++this.tick;
|
|
56
|
+
return this.attach(key, existing, signal);
|
|
57
|
+
}
|
|
58
|
+
const abort = new AbortController();
|
|
59
|
+
const entry = {
|
|
60
|
+
promise: void 0,
|
|
61
|
+
refs: 0,
|
|
62
|
+
abort,
|
|
63
|
+
used: ++this.tick
|
|
64
|
+
};
|
|
65
|
+
entry.promise = fetch(abort.signal).then((handle) => {
|
|
66
|
+
entry.handle = handle;
|
|
67
|
+
return handle;
|
|
68
|
+
}, (err) => {
|
|
69
|
+
if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
70
|
+
throw err;
|
|
71
|
+
});
|
|
72
|
+
entry.promise.catch(() => {});
|
|
73
|
+
this.entries.set(key, entry);
|
|
74
|
+
this.evict();
|
|
75
|
+
return this.attach(key, entry, signal);
|
|
76
|
+
}
|
|
77
|
+
/** Resolved handle if present — synchronous peek for paint-time reuse. */
|
|
78
|
+
peek(key) {
|
|
79
|
+
const entry = this.entries.get(key);
|
|
80
|
+
if (entry?.handle) entry.used = ++this.tick;
|
|
81
|
+
return entry?.handle;
|
|
82
|
+
}
|
|
83
|
+
get size() {
|
|
84
|
+
return this.entries.size;
|
|
85
|
+
}
|
|
86
|
+
attach(key, entry, signal) {
|
|
87
|
+
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
88
|
+
entry.refs += 1;
|
|
89
|
+
if (!signal) {
|
|
90
|
+
const release = () => {
|
|
91
|
+
entry.refs -= 1;
|
|
92
|
+
};
|
|
93
|
+
entry.promise.then(release, release);
|
|
94
|
+
return entry.promise;
|
|
95
|
+
}
|
|
96
|
+
let settled = false;
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
entry.refs -= 1;
|
|
102
|
+
if (entry.refs <= 0 && entry.handle === void 0) {
|
|
103
|
+
entry.abort.abort(abortReason(signal));
|
|
104
|
+
if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
105
|
+
}
|
|
106
|
+
reject(abortReason(signal));
|
|
107
|
+
};
|
|
108
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
109
|
+
entry.promise.then((handle) => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
entry.refs -= 1;
|
|
113
|
+
signal.removeEventListener("abort", onAbort);
|
|
114
|
+
resolve(handle);
|
|
115
|
+
}, (err) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
settled = true;
|
|
118
|
+
entry.refs -= 1;
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
reject(err);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
evict() {
|
|
125
|
+
if (this.entries.size <= this.maxEntries) return;
|
|
126
|
+
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.entries.size <= this.maxEntries) return;
|
|
129
|
+
this.entries.delete(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
129
132
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
function abortReason(signal) {
|
|
134
|
+
return signal.reason ?? new DOMException("aborted", "AbortError");
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/tiles.ts
|
|
138
|
+
function tileGrid(page, scale, tileSize) {
|
|
139
|
+
return {
|
|
140
|
+
scale,
|
|
141
|
+
tileSize,
|
|
142
|
+
cols: Math.max(1, Math.ceil(page.width * scale / tileSize)),
|
|
143
|
+
rows: Math.max(1, Math.ceil(page.height * scale / tileSize))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** One tile's edge in page points at its level. */
|
|
147
|
+
const tileSpanPt = (grid) => grid.tileSize / grid.scale;
|
|
148
|
+
/**
|
|
149
|
+
* The tiles of `grid` intersecting a y-down page-point rect, clamped to
|
|
150
|
+
* the page. An empty intersection yields an empty list.
|
|
151
|
+
*/
|
|
152
|
+
function tilesInRect(grid, page, rect) {
|
|
153
|
+
const span = tileSpanPt(grid);
|
|
154
|
+
const x0 = Math.max(0, Math.floor(rect.x / span));
|
|
155
|
+
const y0 = Math.max(0, Math.floor(rect.y / span));
|
|
156
|
+
const x1 = Math.min(grid.cols - 1, Math.ceil((rect.x + rect.width) / span) - 1);
|
|
157
|
+
const y1 = Math.min(grid.rows - 1, Math.ceil((rect.y + rect.height) / span) - 1);
|
|
158
|
+
const out = [];
|
|
159
|
+
for (let iy = y0; iy <= y1; iy++) for (let ix = x0; ix <= x1; ix++) out.push({
|
|
160
|
+
ix,
|
|
161
|
+
iy
|
|
162
|
+
});
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
/** Paint rect: y-down page points, clamped to the page edge. */
|
|
166
|
+
function tilePaintRect(grid, page, c) {
|
|
167
|
+
const span = tileSpanPt(grid);
|
|
168
|
+
const x = c.ix * span;
|
|
169
|
+
const y = c.iy * span;
|
|
170
|
+
return {
|
|
171
|
+
x,
|
|
172
|
+
y,
|
|
173
|
+
width: Math.min(span, page.width - x),
|
|
174
|
+
height: Math.min(span, page.height - y)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Engine target rect: y-UP PDF user space (top > bottom). */
|
|
178
|
+
function tileEngineRect(grid, page, c) {
|
|
179
|
+
const paint = tilePaintRect(grid, page, c);
|
|
180
|
+
return {
|
|
181
|
+
left: paint.x,
|
|
182
|
+
right: paint.x + paint.width,
|
|
183
|
+
top: page.height - paint.y,
|
|
184
|
+
bottom: page.height - (paint.y + paint.height)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Does a set of PAINTED want-level tiles cover `region` (y-down points)?
|
|
189
|
+
* The retention release check: a retained source may leave the paint list
|
|
190
|
+
* once its visible footprint answers true here. Pure index arithmetic on
|
|
191
|
+
* the aligned grid — the whole reason the pyramid is aligned.
|
|
192
|
+
*/
|
|
193
|
+
function regionCovered(grid, page, region, isPainted) {
|
|
194
|
+
if (region.width <= 0 || region.height <= 0) return true;
|
|
195
|
+
for (const c of tilesInRect(grid, page, region)) if (!isPainted(c)) return false;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Inflate a visible rect by a prefetch margin (fractions of the rect's own
|
|
200
|
+
* size per side), optionally velocity-biased: the margin stretches toward
|
|
201
|
+
* the direction of travel and shrinks behind it. Pure — the producer's
|
|
202
|
+
* velocity is just data.
|
|
203
|
+
*/
|
|
204
|
+
function inflateRect(rect, margin, velocity) {
|
|
205
|
+
const mx = rect.width * margin;
|
|
206
|
+
const my = rect.height * margin;
|
|
207
|
+
let left = mx;
|
|
208
|
+
let right = mx;
|
|
209
|
+
let up = my;
|
|
210
|
+
let down = my;
|
|
211
|
+
if (velocity) {
|
|
212
|
+
if (velocity.dx > 0) {
|
|
213
|
+
right = mx * 2;
|
|
214
|
+
left = mx / 2;
|
|
215
|
+
} else if (velocity.dx < 0) {
|
|
216
|
+
left = mx * 2;
|
|
217
|
+
right = mx / 2;
|
|
218
|
+
}
|
|
219
|
+
if (velocity.dy > 0) {
|
|
220
|
+
down = my * 2;
|
|
221
|
+
up = my / 2;
|
|
222
|
+
} else if (velocity.dy < 0) {
|
|
223
|
+
up = my * 2;
|
|
224
|
+
down = my / 2;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
x: rect.x - left,
|
|
229
|
+
y: rect.y - up,
|
|
230
|
+
width: rect.width + left + right,
|
|
231
|
+
height: rect.height + up + down
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Snap UP through a pyramid's sorted scales; cap at the top. */
|
|
235
|
+
function snapToPyramid(scales, needed) {
|
|
236
|
+
const sorted = [...scales].sort((a, b) => a - b);
|
|
237
|
+
return sorted.find((s) => s >= needed) ?? sorted[sorted.length - 1];
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/tile-manager.ts
|
|
241
|
+
/**
|
|
242
|
+
* The tile retention state machine. One instance per
|
|
243
|
+
* plugin instance (per document), all levels and pages in one place, over
|
|
244
|
+
* the SAME RasterStore the base renders use.
|
|
245
|
+
*
|
|
246
|
+
* Invariant it enforces: every screen region paints the sharpest PAINTED
|
|
247
|
+
* pixels available; quality per region only goes up until the want set
|
|
248
|
+
* resolves. Mechanics:
|
|
249
|
+
* - want vs paint: `plan()` schedules fetches for the want set (P0
|
|
250
|
+
* visible first, P1 prefetch when P0 is fully resolved) and returns a
|
|
251
|
+
* paint list drawn ONLY from resolved entries — current level AND
|
|
252
|
+
* retained older generations.
|
|
253
|
+
* - release-on-occlusion: when a want-level tile reports PAINTED (the
|
|
254
|
+
* layer's img onload — the decode boundary), retained sources whose
|
|
255
|
+
* visible footprint is covered by painted want tiles leave the paint
|
|
256
|
+
* list. Their bytes stay in the RasterStore (demotion, not eviction) —
|
|
257
|
+
* zoom-back re-promotes from cache.
|
|
258
|
+
* - epoch exception: an invalidation bump means retained pixels are
|
|
259
|
+
* WRONG — everything of the old epoch drops immediately.
|
|
260
|
+
*
|
|
261
|
+
* `plan()` is called from selectors: it MUST NOT dispatch. Fetch kickoff
|
|
262
|
+
* is idempotent (the store singleflights); resolution handlers dispatch
|
|
263
|
+
* the wake-up (`onAdvance`) that makes subscribed layers recompute.
|
|
264
|
+
*/
|
|
265
|
+
var TileManager = class {
|
|
266
|
+
constructor(deps) {
|
|
267
|
+
this.deps = deps;
|
|
268
|
+
this.pages = /* @__PURE__ */ new Map();
|
|
269
|
+
}
|
|
270
|
+
plan(pon, demand, includeAnnotations) {
|
|
271
|
+
const { config } = this.deps;
|
|
272
|
+
const policy = this.deps.getPolicy();
|
|
273
|
+
const page = this.deps.getPageSize(pon);
|
|
274
|
+
if (!page) return EMPTY_TILE_PLAN;
|
|
275
|
+
const epoch = this.deps.getEpoch(pon, includeAnnotations);
|
|
276
|
+
const state = this.pageState(pon);
|
|
277
|
+
if (state.epoch !== epoch) {
|
|
278
|
+
this.abortAll(state);
|
|
279
|
+
state.entries.clear();
|
|
280
|
+
state.epoch = epoch;
|
|
281
|
+
state.wantScale = null;
|
|
282
|
+
state.planCache = null;
|
|
283
|
+
}
|
|
284
|
+
const baseSupplied = policy.kind === "lattice" ? widthOf(snapFullPageViewport(policy, {
|
|
285
|
+
kind: "width",
|
|
286
|
+
width: demand.desiredDeviceWidth
|
|
287
|
+
})) : demand.desiredDeviceWidth;
|
|
288
|
+
if (demand.desiredDeviceWidth / baseSupplied <= config.engageAt) {
|
|
289
|
+
if (state.entries.size || state.wantScale !== null) {
|
|
290
|
+
this.abortAll(state);
|
|
291
|
+
state.entries.clear();
|
|
292
|
+
state.wantScale = null;
|
|
293
|
+
state.planCache = null;
|
|
294
|
+
}
|
|
295
|
+
return EMPTY_TILE_PLAN;
|
|
296
|
+
}
|
|
297
|
+
const pyramid = policy.kind === "lattice" && policy.tiles ? policy.tiles.scales : config.scales;
|
|
298
|
+
const wantScale = snapToPyramid(pyramid, demand.desiredDeviceWidth / page.width);
|
|
299
|
+
const grid = tileGrid(page, wantScale, config.tileSize);
|
|
300
|
+
const visible = demand.visibleRect ?? {
|
|
301
|
+
x: 0,
|
|
302
|
+
y: 0,
|
|
303
|
+
width: page.width,
|
|
304
|
+
height: page.height
|
|
305
|
+
};
|
|
306
|
+
state.lastVisible = visible;
|
|
307
|
+
const p0 = tilesInRect(grid, page, visible);
|
|
308
|
+
const ring = inflateRect(visible, config.prefetchMargin, config.velocityBias ? demand.velocity : void 0);
|
|
309
|
+
const p0Keys = new Set(p0.map((c) => coordKey(c)));
|
|
310
|
+
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
|
+
const demandKey = `${wantScale}|${rectKey(visible)}|e${epoch}`;
|
|
313
|
+
if (state.planCache && state.planCache.demandKey === demandKey && state.planCache.version === state.version) return state.planCache.plan;
|
|
314
|
+
const paint = [];
|
|
315
|
+
const fetching = [];
|
|
316
|
+
for (const entry of state.entries.values()) {
|
|
317
|
+
const visIntersect = intersectRects(entry.rect, visible);
|
|
318
|
+
if (entry.handle) {
|
|
319
|
+
if (visIntersect.width > 0 && visIntersect.height > 0) paint.push({
|
|
320
|
+
key: entry.key,
|
|
321
|
+
scale: entry.scale,
|
|
322
|
+
rect: entry.rect,
|
|
323
|
+
z: zOf(pyramid, entry.scale),
|
|
324
|
+
handle: entry.handle
|
|
325
|
+
});
|
|
326
|
+
} else if (entry.scale === wantScale) fetching.push(entry.key);
|
|
327
|
+
}
|
|
328
|
+
paint.sort((a, b) => a.z - b.z || a.key.localeCompare(b.key));
|
|
329
|
+
const plan = {
|
|
330
|
+
engaged: true,
|
|
331
|
+
paint,
|
|
332
|
+
fetching: fetching.sort(),
|
|
333
|
+
stamp: `${demandKey}|v${state.version}`
|
|
334
|
+
};
|
|
335
|
+
state.planCache = {
|
|
336
|
+
demandKey,
|
|
337
|
+
version: state.version,
|
|
338
|
+
plan
|
|
339
|
+
};
|
|
340
|
+
return plan;
|
|
341
|
+
}
|
|
342
|
+
/** The layer's decode-boundary report: this key's <img> actually painted. */
|
|
343
|
+
sourcePainted(pon, key) {
|
|
344
|
+
const state = this.pages.get(pon);
|
|
345
|
+
const entry = state?.entries.get(key);
|
|
346
|
+
if (!state || !entry || entry.painted) return;
|
|
347
|
+
entry.painted = true;
|
|
348
|
+
this.releaseOccluded(state, entry);
|
|
349
|
+
state.version += 1;
|
|
350
|
+
state.planCache = null;
|
|
351
|
+
this.deps.onAdvance(pon);
|
|
352
|
+
}
|
|
353
|
+
/** A lens unmounted its TileLayer: stop fetching, drop bookkeeping.
|
|
354
|
+
* Resolved bytes stay in the RasterStore for a re-mount. */
|
|
355
|
+
releasePage(pon) {
|
|
356
|
+
const state = this.pages.get(pon);
|
|
357
|
+
if (!state) return;
|
|
358
|
+
this.abortAll(state);
|
|
359
|
+
this.pages.delete(pon);
|
|
360
|
+
}
|
|
361
|
+
pageState(pon) {
|
|
362
|
+
let state = this.pages.get(pon);
|
|
363
|
+
if (!state) {
|
|
364
|
+
state = {
|
|
365
|
+
epoch: -1,
|
|
366
|
+
wantScale: null,
|
|
367
|
+
entries: /* @__PURE__ */ new Map(),
|
|
368
|
+
version: 0,
|
|
369
|
+
planCache: null,
|
|
370
|
+
settleTimer: null,
|
|
371
|
+
pendingLevel: null,
|
|
372
|
+
lastVisible: null
|
|
373
|
+
};
|
|
374
|
+
this.pages.set(pon, state);
|
|
375
|
+
}
|
|
376
|
+
return state;
|
|
377
|
+
}
|
|
378
|
+
schedule(pon, state, page, grid, wantScale, includeAnnotations, epoch, p0, p1) {
|
|
379
|
+
const { config } = this.deps;
|
|
380
|
+
const firstEngage = state.wantScale === null;
|
|
381
|
+
const levelChanged = state.wantScale !== null && state.wantScale !== wantScale;
|
|
382
|
+
state.wantScale = wantScale;
|
|
383
|
+
const wanted = new Set([...p0, ...p1].map((c) => this.tileKey(pon, wantScale, c, includeAnnotations, epoch)));
|
|
384
|
+
for (const [key, entry] of state.entries) if (entry.handle === void 0 && !wanted.has(key)) {
|
|
385
|
+
entry.abort?.abort();
|
|
386
|
+
state.entries.delete(key);
|
|
387
|
+
}
|
|
388
|
+
const kickoff = () => {
|
|
389
|
+
if (this.ensureFetches(pon, state, page, grid, includeAnnotations, epoch, p0)) this.ensureFetches(pon, state, page, grid, includeAnnotations, epoch, p1);
|
|
390
|
+
};
|
|
391
|
+
if (levelChanged && config.settleMs > 0) {
|
|
392
|
+
state.pendingLevel = wantScale;
|
|
393
|
+
if (state.settleTimer !== null) clearTimeout(state.settleTimer);
|
|
394
|
+
state.settleTimer = setTimeout(() => {
|
|
395
|
+
state.settleTimer = null;
|
|
396
|
+
if (state.pendingLevel === wantScale) kickoff();
|
|
397
|
+
}, config.settleMs);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (state.settleTimer !== null && !levelChanged && !firstEngage) {
|
|
401
|
+
clearTimeout(state.settleTimer);
|
|
402
|
+
state.settleTimer = null;
|
|
403
|
+
state.pendingLevel = null;
|
|
404
|
+
}
|
|
405
|
+
kickoff();
|
|
406
|
+
}
|
|
407
|
+
/** Start missing fetches; true when every coord is already resolved. */
|
|
408
|
+
ensureFetches(pon, state, page, grid, includeAnnotations, epoch, coords) {
|
|
409
|
+
let allReady = true;
|
|
410
|
+
for (const coord of coords) {
|
|
411
|
+
const key = this.tileKey(pon, grid.scale, coord, includeAnnotations, epoch);
|
|
412
|
+
const existing = state.entries.get(key);
|
|
413
|
+
if (existing) {
|
|
414
|
+
if (existing.handle === void 0) allReady = false;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
allReady = false;
|
|
418
|
+
const abort = new AbortController();
|
|
419
|
+
const entry = {
|
|
420
|
+
key,
|
|
421
|
+
scale: grid.scale,
|
|
422
|
+
coord,
|
|
423
|
+
rect: tilePaintRect(grid, page, coord),
|
|
424
|
+
painted: false,
|
|
425
|
+
abort
|
|
426
|
+
};
|
|
427
|
+
state.entries.set(key, entry);
|
|
428
|
+
this.deps.store.acquire(key, (signal) => this.deps.fetchTile(pon, tileEngineRect(grid, page, coord), grid.scale, includeAnnotations, signal), abort.signal).then((handle) => {
|
|
429
|
+
if (state.entries.get(key) !== entry) return;
|
|
430
|
+
entry.handle = handle;
|
|
431
|
+
state.version += 1;
|
|
432
|
+
state.planCache = null;
|
|
433
|
+
this.deps.onAdvance(pon);
|
|
434
|
+
}, () => {
|
|
435
|
+
if (state.entries.get(key) === entry) state.entries.delete(key);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return allReady;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* The release rule: after `painted` lands on a want-level tile, retained
|
|
442
|
+
* sources (any other level) whose footprint ∩ painted-tile region is
|
|
443
|
+
* covered by PAINTED want-level tiles leave the paint list. Index
|
|
444
|
+
* arithmetic via the aligned grid — no rectangle geometry.
|
|
445
|
+
*/
|
|
446
|
+
releaseOccluded(state, painted) {
|
|
447
|
+
const wantScale = state.wantScale;
|
|
448
|
+
if (wantScale === null || painted.scale !== wantScale) return;
|
|
449
|
+
const page = this.deps.getPageSize(pagePonOf(painted.key));
|
|
450
|
+
if (!page) return;
|
|
451
|
+
const grid = tileGrid(page, wantScale, this.deps.config.tileSize);
|
|
452
|
+
const paintedAt = (c) => {
|
|
453
|
+
const key = this.tileKey(pagePonOf(painted.key), wantScale, c, annotationsOf(painted.key), state.epoch);
|
|
454
|
+
return state.entries.get(key)?.painted === true;
|
|
455
|
+
};
|
|
456
|
+
for (const [key, entry] of state.entries) {
|
|
457
|
+
if (entry.scale === wantScale || entry.handle === void 0) continue;
|
|
458
|
+
const overlap = intersectRects(entry.rect, painted.rect);
|
|
459
|
+
if (overlap.width <= 0 || overlap.height <= 0) continue;
|
|
460
|
+
if (regionCovered(grid, page, state.lastVisible ? intersectRects(entry.rect, state.lastVisible) : entry.rect, paintedAt)) state.entries.delete(key);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
abortAll(state) {
|
|
464
|
+
if (state.settleTimer !== null) {
|
|
465
|
+
clearTimeout(state.settleTimer);
|
|
466
|
+
state.settleTimer = null;
|
|
467
|
+
}
|
|
468
|
+
for (const entry of state.entries.values()) if (entry.handle === void 0) entry.abort?.abort();
|
|
469
|
+
}
|
|
470
|
+
tileKey(pon, scale, c, includeAnnotations, epoch) {
|
|
471
|
+
return `t:${pon}|s${scale}|${c.ix},${c.iy}|a${includeAnnotations ? 1 : 0}|e${epoch}`;
|
|
472
|
+
}
|
|
136
473
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
474
|
+
const widthOf = (v) => v.kind === "width" && v.width !== void 0 ? v.width : Number.POSITIVE_INFINITY;
|
|
475
|
+
const coordKey = (c) => `${c.ix},${c.iy}`;
|
|
476
|
+
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;
|
|
140
482
|
};
|
|
141
|
-
|
|
483
|
+
const pagePonOf = (key) => Number(key.slice(2, key.indexOf("|")));
|
|
484
|
+
const annotationsOf = (key) => key.includes("|a1|");
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/capability.ts
|
|
487
|
+
/**
|
|
488
|
+
* The render capability: the ONE policy consumer in the client stack
|
|
489
|
+
* (the three-layer rule). The engine never snaps; framework
|
|
490
|
+
* layers never see the policy; everything between — conforming a desired
|
|
491
|
+
* scale to the deployment lattice, collapsing same-rung asks in the raster
|
|
492
|
+
* store, exposing stable source keys — happens here.
|
|
493
|
+
*/
|
|
494
|
+
function createRenderCapability(ctx, options = {}) {
|
|
495
|
+
const store = new RasterStore();
|
|
496
|
+
const policy = () => ctx.document()?.renderPolicy ?? CONTINUOUS_RENDER_POLICY;
|
|
497
|
+
const pageOf = (pon) => (ctx.document()?.pages ?? []).find((p) => p.pageObjectNumber === pon);
|
|
498
|
+
const pageWidthOf = (pon) => {
|
|
499
|
+
const page = pageOf(pon);
|
|
500
|
+
if (!page) throw new Error(`render: unknown page object number ${pon}`);
|
|
501
|
+
return page.size.width;
|
|
502
|
+
};
|
|
503
|
+
const conformViewport = (pon, scale) => {
|
|
504
|
+
const p = policy();
|
|
505
|
+
if (p.kind !== "lattice") return {
|
|
506
|
+
kind: "scale",
|
|
507
|
+
scale
|
|
508
|
+
};
|
|
509
|
+
return snapFullPageViewport(p, {
|
|
510
|
+
kind: "scale",
|
|
511
|
+
scale
|
|
512
|
+
}, { pageWidth: pageWidthOf(pon) });
|
|
513
|
+
};
|
|
514
|
+
const viewportKey = (viewport) => viewport.kind === "width" ? `w${viewport.width}` : `s${viewport.scale ?? 1}`;
|
|
515
|
+
const epochOf = (pon, includeAnnotations) => {
|
|
516
|
+
const s = ctx.getState();
|
|
517
|
+
const content = s.contentEpochs[pon] ?? 0;
|
|
518
|
+
if (!includeAnnotations) return content;
|
|
519
|
+
return content + (s.annotatedEpochs[pon] ?? 0);
|
|
520
|
+
};
|
|
521
|
+
const tiles = new TileManager({
|
|
522
|
+
store,
|
|
523
|
+
config: resolveTiling(options.tiling),
|
|
524
|
+
getPolicy: policy,
|
|
525
|
+
getPageSize: (pon) => pageOf(pon)?.size,
|
|
526
|
+
getEpoch: epochOf,
|
|
527
|
+
fetchTile: (pon, rect, scale, includeAnnotations, signal) => {
|
|
528
|
+
const doc = ctx.doc;
|
|
529
|
+
if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
|
|
530
|
+
const task = doc.page(pon).render.image({
|
|
531
|
+
target: {
|
|
532
|
+
kind: "rect",
|
|
533
|
+
rect
|
|
534
|
+
},
|
|
535
|
+
viewport: {
|
|
536
|
+
kind: "scale",
|
|
537
|
+
scale
|
|
538
|
+
},
|
|
539
|
+
includeAnnotations
|
|
540
|
+
});
|
|
541
|
+
if (signal.aborted) task.abort(signal.reason);
|
|
542
|
+
else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
|
|
543
|
+
return task;
|
|
544
|
+
},
|
|
545
|
+
onAdvance: (pon) => ctx.dispatch({
|
|
546
|
+
type: "PAINT_ADVANCED",
|
|
547
|
+
pon
|
|
548
|
+
})
|
|
549
|
+
});
|
|
550
|
+
return {
|
|
551
|
+
renderPage(pon, { scale, includeAnnotations, signal }) {
|
|
552
|
+
const doc = ctx.doc;
|
|
553
|
+
if (!doc) return Promise.reject(/* @__PURE__ */ new Error("render: no document bound"));
|
|
554
|
+
const annotations = includeAnnotations ?? true;
|
|
555
|
+
const viewport = conformViewport(pon, scale);
|
|
556
|
+
const key = `${pon}|${viewportKey(viewport)}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}`;
|
|
557
|
+
return store.acquire(key, (storeSignal) => {
|
|
558
|
+
const task = doc.page(pon).render.image({
|
|
559
|
+
viewport,
|
|
560
|
+
includeAnnotations: annotations
|
|
561
|
+
});
|
|
562
|
+
if (storeSignal.aborted) task.abort(storeSignal.reason);
|
|
563
|
+
else storeSignal.addEventListener("abort", () => task.abort(storeSignal.reason), { once: true });
|
|
564
|
+
return task;
|
|
565
|
+
}, signal);
|
|
566
|
+
},
|
|
567
|
+
renderSourceKey(pon, { scale, includeAnnotations }) {
|
|
568
|
+
const annotations = includeAnnotations ?? true;
|
|
569
|
+
const viewport = conformViewport(pon, scale);
|
|
570
|
+
return `${pon}|${viewportKey(viewport)}|a${annotations ? 1 : 0}|e${epochOf(pon, annotations)}`;
|
|
571
|
+
},
|
|
572
|
+
conformViewport,
|
|
573
|
+
renderPolicy: policy,
|
|
574
|
+
tilePlan(pon, demand, opts) {
|
|
575
|
+
return tiles.plan(pon, demand, opts?.includeAnnotations ?? true);
|
|
576
|
+
},
|
|
577
|
+
tilePainted(pon, key) {
|
|
578
|
+
tiles.sourcePainted(pon, key);
|
|
579
|
+
},
|
|
580
|
+
releaseTiles(pon) {
|
|
581
|
+
tiles.releasePage(pon);
|
|
582
|
+
},
|
|
583
|
+
renderEpoch(pon, includeAnnotations = true) {
|
|
584
|
+
return epochOf(pon, includeAnnotations);
|
|
585
|
+
},
|
|
586
|
+
invalidate({ pons, scope = "content" } = {}) {
|
|
587
|
+
const target = pons ?? (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
|
|
588
|
+
if (target.length) ctx.dispatch({
|
|
589
|
+
type: "INVALIDATE",
|
|
590
|
+
scope,
|
|
591
|
+
pons: target
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/effects.ts
|
|
598
|
+
/**
|
|
599
|
+
* Which pages' ANNOTATED raster a confirmed mutation repaints — the whole
|
|
600
|
+
* event→scope map. Everything the engine emits today is appearance-scoped;
|
|
601
|
+
* when it grows content-mutation events (redaction.applied, content.edited),
|
|
602
|
+
* they get a case here dispatching scope 'content'. Over-invalidation is
|
|
603
|
+
* acceptable (a z-order move that changes nothing repaints one thumb);
|
|
604
|
+
* under-invalidation is the bug.
|
|
605
|
+
*/
|
|
606
|
+
function annotatedPons(event, allPons) {
|
|
607
|
+
switch (event.type) {
|
|
608
|
+
case "annotation.created":
|
|
609
|
+
case "annotation.updated":
|
|
610
|
+
case "annotation.deleted":
|
|
611
|
+
case "annotation.moved": return [event.pageObjectNumber];
|
|
612
|
+
case "form.valueChanged":
|
|
613
|
+
case "form.effectsApplied": return event.changedWidgets.map((w) => w.pageObjectNumber);
|
|
614
|
+
case "form.fieldDeleted": return event.removedWidgets.map((w) => w.pageObjectNumber);
|
|
615
|
+
case "form.fieldCreated":
|
|
616
|
+
case "form.fieldUpdated":
|
|
617
|
+
case "form.widgetAttached":
|
|
618
|
+
case "form.widgetDetached": return event.field.widgets.map((w) => w.pageObjectNumber);
|
|
619
|
+
case "form.imported":
|
|
620
|
+
case "form.repaired": return allPons();
|
|
621
|
+
default: return [];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function registerRenderEffects(ctx) {
|
|
625
|
+
const doc = ctx.doc;
|
|
626
|
+
if (!doc) return;
|
|
627
|
+
const allPons = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
|
|
628
|
+
const unsubscribe = doc.events.subscribe((event) => {
|
|
629
|
+
if (event.type === "redaction.applied") {
|
|
630
|
+
const pons = event.results.filter((r) => r.status === "applied").map((r) => r.pageObjectNumber);
|
|
631
|
+
if (pons.length) ctx.dispatch({
|
|
632
|
+
type: "INVALIDATE",
|
|
633
|
+
scope: "content",
|
|
634
|
+
pons
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const pons = annotatedPons(event, allPons);
|
|
639
|
+
if (pons.length) ctx.dispatch({
|
|
640
|
+
type: "INVALIDATE",
|
|
641
|
+
scope: "annotations",
|
|
642
|
+
pons
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
ctx.cleanup(unsubscribe);
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/reducer.ts
|
|
649
|
+
const initialRenderState = () => ({
|
|
650
|
+
contentEpochs: {},
|
|
651
|
+
annotatedEpochs: {},
|
|
652
|
+
paintVersions: {}
|
|
653
|
+
});
|
|
654
|
+
const bump = (epochs, pons) => {
|
|
655
|
+
const next = { ...epochs };
|
|
656
|
+
for (const pon of pons) next[pon] = (next[pon] ?? 0) + 1;
|
|
657
|
+
return next;
|
|
658
|
+
};
|
|
659
|
+
/** Pure. Two transitions: bump the touched pages' ledger for the fact's
|
|
660
|
+
* scope (one action per fact, so a batch result bumps each pon once — a
|
|
661
|
+
* 'content' fact bumps ONLY the content ledger; annotated readers sum both,
|
|
662
|
+
* which is how content invalidation reaches them too), and the tile
|
|
663
|
+
* wake-up counter. The render POLICY is deliberately NOT here — it's a
|
|
664
|
+
* document fact on `DocumentMeta`, materialized by the kernel at open. */
|
|
665
|
+
const renderReducer = (state, a) => {
|
|
666
|
+
if (a.type === "PAINT_ADVANCED") return {
|
|
667
|
+
...state,
|
|
668
|
+
paintVersions: bump(state.paintVersions, [a.pon])
|
|
669
|
+
};
|
|
670
|
+
if (a.type !== "INVALIDATE" || a.pons.length === 0) return state;
|
|
671
|
+
return a.scope === "content" ? {
|
|
672
|
+
...state,
|
|
673
|
+
contentEpochs: bump(state.contentEpochs, a.pons)
|
|
674
|
+
} : {
|
|
675
|
+
...state,
|
|
676
|
+
annotatedEpochs: bump(state.annotatedEpochs, a.pons)
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/types.ts
|
|
681
|
+
const RenderToken = createCapabilityToken("render");
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/render.plugin.ts
|
|
684
|
+
/**
|
|
685
|
+
* Document-scoped. The ONE policy consumer in the client stack: the doc-bind
|
|
686
|
+
* effect resolves the engine's advertised render
|
|
687
|
+
* policy; `renderPage` conforms desired scales to it and collapses
|
|
688
|
+
* same-rung asks in the raster store; the tile manager turns host-supplied
|
|
689
|
+
* demand into a retention-safe paint plan over the SAME store. State is the
|
|
690
|
+
* per-page ledger — raster versions (two doors: the document event stream's
|
|
691
|
+
* built-in map and the `invalidate` verb), the policy latch, and the tile
|
|
692
|
+
* wake-up counter. Layers key on `renderSourceKey`/`tilePlan` and refetch
|
|
693
|
+
* exactly when those change.
|
|
694
|
+
*/
|
|
695
|
+
const renderPlugin = (options = {}) => definePlugin({
|
|
696
|
+
id: "render",
|
|
697
|
+
scope: "document",
|
|
698
|
+
token: RenderToken,
|
|
699
|
+
initialState: initialRenderState,
|
|
700
|
+
reduce: renderReducer,
|
|
701
|
+
capability: (ctx) => createRenderCapability(ctx, options),
|
|
702
|
+
effects: registerRenderEffects
|
|
703
|
+
});
|
|
704
|
+
//#endregion
|
|
705
|
+
export { RenderToken, renderPlugin };
|
|
706
|
+
|
|
707
|
+
//# sourceMappingURL=index.js.map
|