@embedpdf/plugin-selection 3.0.0-next.3 → 3.0.0-next.4
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 +104 -257
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -52
- package/dist/index.d.ts +70 -52
- package/dist/index.js +102 -254
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,251 +1,81 @@
|
|
|
1
1
|
import { createCapabilityToken, definePlugin } from "@embedpdf/core";
|
|
2
2
|
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
|
-
import {
|
|
3
|
+
import { applyPoint, invert, pageGeometry, textQuadBounds } from "@embedpdf/core-geometry";
|
|
4
|
+
import { buildPageTextLayout, expandTextRangeToLine, expandTextRangeToWord, textGlyphAt, textGlyphQuad, textSegmentsForRange } from "@embedpdf/engine-core/runtime";
|
|
4
5
|
//#region src/geometry.ts
|
|
5
6
|
/**
|
|
6
|
-
* The
|
|
7
|
-
* viewer content space (y-down), glyph hit-testing, word/line expansion, and the
|
|
8
|
-
* line-merge. No DOM, no engine, no React.
|
|
7
|
+
* The selection plugin's COORDINATE SEAM — and nothing else.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* The PDF↔content y-flip is the
|
|
16
|
-
* that math lives), never
|
|
9
|
+
* Segmentation, hit-testing, and word/line expansion live in the canonical
|
|
10
|
+
* engine-core text layout (`@embedpdf/engine-core` `text/layout`): a text
|
|
11
|
+
* range has exactly one segmentation whether it came from a drag or a
|
|
12
|
+
* search. This module only converts between the viewer's content space
|
|
13
|
+
* (y-down, crop-relative) and the engine's PDF space (y-up) — pointer
|
|
14
|
+
* points inward, canonical segments outward. The PDF↔content y-flip is the
|
|
15
|
+
* geometry package's `pageGeometry` (the one place that math lives), never
|
|
16
|
+
* hand-rolled here.
|
|
17
17
|
*/
|
|
18
|
-
const FLAG_EMPTY = 2;
|
|
19
|
-
const isBoundary = (flags) => (flags & 3) !== 0;
|
|
20
|
-
const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
|
|
21
|
-
const toContent = (m, b) => applyRect(m, {
|
|
22
|
-
x: b.left,
|
|
23
|
-
y: b.bottom,
|
|
24
|
-
width: b.right - b.left,
|
|
25
|
-
height: b.top - b.bottom
|
|
26
|
-
});
|
|
27
18
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* un-rotated
|
|
19
|
+
* Build a page's selection geometry. Page display rotation does NOT enter
|
|
20
|
+
* here — the overlay rides the page's CSS rotation, so content space stays
|
|
21
|
+
* un-rotated (the crop-relative y-flip is the only conversion; zoom = 1,
|
|
22
|
+
* the viewer's zoom is applied later by `PageTransform.pageToContent`).
|
|
31
23
|
*/
|
|
32
|
-
function
|
|
24
|
+
function buildSelectionPageGeometry(snapshot, crop, rotation, userUnit) {
|
|
33
25
|
const { pdfToContent } = pageGeometry({
|
|
34
26
|
crop,
|
|
35
27
|
rotation,
|
|
36
28
|
userUnit
|
|
37
29
|
}, 1);
|
|
38
|
-
const glyphs = [];
|
|
39
|
-
const runs = [];
|
|
40
|
-
for (const run of snapshot.runs) {
|
|
41
|
-
const start = glyphs.length;
|
|
42
|
-
for (const g of run.glyphs) glyphs.push({
|
|
43
|
-
loose: toContent(pdfToContent, g.looseBox),
|
|
44
|
-
tight: g.tightBox ? toContent(pdfToContent, g.tightBox) : void 0,
|
|
45
|
-
flags: g.flags
|
|
46
|
-
});
|
|
47
|
-
runs.push({
|
|
48
|
-
start,
|
|
49
|
-
count: run.glyphs.length,
|
|
50
|
-
rect: toContent(pdfToContent, run.rect),
|
|
51
|
-
fontSize: run.fontSize
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
30
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
31
|
+
layout: buildPageTextLayout(snapshot),
|
|
32
|
+
toContent: pdfToContent,
|
|
33
|
+
fromContent: invert(pdfToContent)
|
|
57
34
|
};
|
|
58
35
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (const g of text.glyphs) {
|
|
63
|
-
if (isEmpty(g.flags)) continue;
|
|
64
|
-
total += g.loose.height;
|
|
65
|
-
count++;
|
|
66
|
-
}
|
|
67
|
-
return count === 0 ? 0 : total / count;
|
|
36
|
+
/** A content-space pointer point in the layout's PDF space. */
|
|
37
|
+
function contentPointToPdf(geom, p) {
|
|
38
|
+
return applyPoint(geom.fromContent, p);
|
|
68
39
|
}
|
|
69
40
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
41
|
+
* A canonical PDF quad in content space. Engine quads carry frame-geometric
|
|
42
|
+
* slot semantics (`p1..p4` = upper-start, upper-end, lower-start,
|
|
43
|
+
* lower-end) — a trusted producer, so the y-flip maps corners straight onto
|
|
44
|
+
* their names; imported-annotation normalization does NOT apply here.
|
|
74
45
|
*/
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
if (toleranceFactor <= 0) return null;
|
|
85
|
-
const half = avgGlyphHeight(text) * toleranceFactor / 2;
|
|
86
|
-
let best = -1;
|
|
87
|
-
let bestDist = Infinity;
|
|
88
|
-
for (const run of text.runs) {
|
|
89
|
-
const r = run.rect;
|
|
90
|
-
if (p.y < r.y - half || p.y > r.y + r.height + half || p.x < r.x - half || p.x > r.x + r.width + half) continue;
|
|
91
|
-
for (let i = 0; i < run.count; i++) {
|
|
92
|
-
const g = text.glyphs[run.start + i];
|
|
93
|
-
if (isEmpty(g.flags)) continue;
|
|
94
|
-
const b = g.tight ?? g.loose;
|
|
95
|
-
if (p.x < b.x - half || p.x > b.x + b.width + half || p.y < b.y - half || p.y > b.y + b.height + half) continue;
|
|
96
|
-
const dx = Math.min(Math.abs(p.x - b.x), Math.abs(p.x - (b.x + b.width)));
|
|
97
|
-
const dy = Math.min(Math.abs(p.y - b.y), Math.abs(p.y - (b.y + b.height)));
|
|
98
|
-
if (dx + dy < bestDist) {
|
|
99
|
-
bestDist = dx + dy;
|
|
100
|
-
best = run.start + i;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return best >= 0 ? best : null;
|
|
105
|
-
}
|
|
106
|
-
/** Double-click: the word around `i` (walk to space/empty glyphs both ways). */
|
|
107
|
-
function expandToWord(text, i) {
|
|
108
|
-
const n = text.glyphs.length;
|
|
109
|
-
if (i < 0 || i >= n) return [i, i];
|
|
110
|
-
let from = i;
|
|
111
|
-
while (from > 0 && !isBoundary(text.glyphs[from - 1].flags)) from--;
|
|
112
|
-
let to = i;
|
|
113
|
-
while (to < n - 1 && !isBoundary(text.glyphs[to + 1].flags)) to++;
|
|
114
|
-
return [from, to];
|
|
115
|
-
}
|
|
116
|
-
/** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */
|
|
117
|
-
function expandToLine(text, i) {
|
|
118
|
-
const ri = text.runs.findIndex((r) => i >= r.start && i < r.start + r.count);
|
|
119
|
-
if (ri < 0) return [i, i];
|
|
120
|
-
const anchor = text.runs[ri];
|
|
121
|
-
const top = anchor.rect.y;
|
|
122
|
-
const bottom = anchor.rect.y + anchor.rect.height;
|
|
123
|
-
let from = anchor.start;
|
|
124
|
-
let to = anchor.start + anchor.count - 1;
|
|
125
|
-
for (let r = ri - 1; r >= 0; r--) {
|
|
126
|
-
const run = text.runs[r];
|
|
127
|
-
if (isZero(run.rect)) continue;
|
|
128
|
-
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
129
|
-
from = run.start;
|
|
130
|
-
}
|
|
131
|
-
for (let r = ri + 1; r < text.runs.length; r++) {
|
|
132
|
-
const run = text.runs[r];
|
|
133
|
-
if (isZero(run.rect)) continue;
|
|
134
|
-
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
135
|
-
to = run.start + run.count - 1;
|
|
136
|
-
}
|
|
137
|
-
return [from, to];
|
|
138
|
-
}
|
|
139
|
-
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
140
|
-
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
141
|
-
const VERTICAL_OVERLAP_THRESHOLD = .8;
|
|
142
|
-
const LINE_OVERLAP_THRESHOLD = .5;
|
|
143
|
-
function rectsForRange(text, from, to) {
|
|
144
|
-
const lo = Math.max(0, Math.min(from, to));
|
|
145
|
-
const hi = Math.min(text.glyphs.length - 1, Math.max(from, to));
|
|
146
|
-
const subRuns = [];
|
|
147
|
-
for (const run of text.runs) {
|
|
148
|
-
const runEnd = run.start + run.count - 1;
|
|
149
|
-
if (runEnd < lo || run.start > hi) continue;
|
|
150
|
-
const s = Math.max(lo, run.start);
|
|
151
|
-
const e = Math.min(hi, runEnd);
|
|
152
|
-
let minX = Infinity;
|
|
153
|
-
let maxX = -Infinity;
|
|
154
|
-
let minY = Infinity;
|
|
155
|
-
let maxY = -Infinity;
|
|
156
|
-
let charCount = 0;
|
|
157
|
-
let widthSum = 0;
|
|
158
|
-
let prevRight = -Infinity;
|
|
159
|
-
const flush = () => {
|
|
160
|
-
if (minX !== Infinity && charCount > 0) subRuns.push({
|
|
161
|
-
rect: {
|
|
162
|
-
x: minX,
|
|
163
|
-
y: minY,
|
|
164
|
-
width: maxX - minX,
|
|
165
|
-
height: maxY - minY
|
|
166
|
-
},
|
|
167
|
-
charCount,
|
|
168
|
-
fontSize: run.fontSize
|
|
169
|
-
});
|
|
170
|
-
minX = Infinity;
|
|
171
|
-
maxX = -Infinity;
|
|
172
|
-
minY = Infinity;
|
|
173
|
-
maxY = -Infinity;
|
|
174
|
-
charCount = 0;
|
|
175
|
-
widthSum = 0;
|
|
176
|
-
prevRight = -Infinity;
|
|
177
|
-
};
|
|
178
|
-
for (let gi = s; gi <= e; gi++) {
|
|
179
|
-
const g = text.glyphs[gi];
|
|
180
|
-
if (isEmpty(g.flags)) continue;
|
|
181
|
-
const b = g.loose;
|
|
182
|
-
if (charCount > 0 && prevRight > -Infinity) {
|
|
183
|
-
const avg = widthSum / charCount;
|
|
184
|
-
if (avg > 0 && Math.abs(b.x - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
|
|
185
|
-
}
|
|
186
|
-
minX = Math.min(minX, b.x);
|
|
187
|
-
maxX = Math.max(maxX, b.x + b.width);
|
|
188
|
-
minY = Math.min(minY, b.y);
|
|
189
|
-
maxY = Math.max(maxY, b.y + b.height);
|
|
190
|
-
charCount++;
|
|
191
|
-
widthSum += b.width;
|
|
192
|
-
prevRight = b.x + b.width;
|
|
193
|
-
}
|
|
194
|
-
flush();
|
|
195
|
-
}
|
|
196
|
-
return mergeAdjacentRects(subRuns);
|
|
197
|
-
}
|
|
198
|
-
function mergeAdjacentRects(runs) {
|
|
199
|
-
const out = [];
|
|
200
|
-
let prev = null;
|
|
201
|
-
let cur = null;
|
|
202
|
-
for (const run of runs) {
|
|
203
|
-
if (prev && cur && shouldMerge(prev, run)) cur = boundsOfRects([cur, run.rect]);
|
|
204
|
-
else {
|
|
205
|
-
if (cur) out.push(cur);
|
|
206
|
-
cur = run.rect;
|
|
207
|
-
}
|
|
208
|
-
prev = run;
|
|
209
|
-
}
|
|
210
|
-
if (cur && cur.width > 0 && cur.height > 0) out.push(cur);
|
|
211
|
-
return out;
|
|
212
|
-
}
|
|
213
|
-
function shouldMerge(a, b) {
|
|
214
|
-
if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
|
|
215
|
-
if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
|
|
216
|
-
}
|
|
217
|
-
if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;
|
|
218
|
-
const aw = a.rect.width / a.charCount;
|
|
219
|
-
const bw = b.rect.width / b.charCount;
|
|
220
|
-
const aL = a.rect.x - aw;
|
|
221
|
-
const aR = a.rect.x + a.rect.width + aw;
|
|
222
|
-
const bL = b.rect.x - bw;
|
|
223
|
-
return aL < b.rect.x + b.rect.width + bw && aR > bL;
|
|
224
|
-
}
|
|
225
|
-
function verticalOverlap(a, b) {
|
|
226
|
-
if (a.height <= 0 || b.height <= 0) return 0;
|
|
227
|
-
const u = Math.max(a.y + a.height, b.y + b.height) - Math.min(a.y, b.y);
|
|
228
|
-
if (u === a.height || u === b.height) return 1;
|
|
229
|
-
return Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y)) / u;
|
|
46
|
+
function toContentTextQuad(geom, quad) {
|
|
47
|
+
const pt = (p) => applyPoint(geom.toContent, p);
|
|
48
|
+
return {
|
|
49
|
+
upperStart: pt(quad.p1),
|
|
50
|
+
upperEnd: pt(quad.p2),
|
|
51
|
+
lowerStart: pt(quad.p3),
|
|
52
|
+
lowerEnd: pt(quad.p4)
|
|
53
|
+
};
|
|
230
54
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return
|
|
55
|
+
/** A canonical segment in content space (`rect` recomputed from the mapped
|
|
56
|
+
* quad so the pair can never drift). */
|
|
57
|
+
function toContentSegment(geom, segment) {
|
|
58
|
+
const quad = toContentTextQuad(geom, segment.quad);
|
|
59
|
+
return {
|
|
60
|
+
quad,
|
|
61
|
+
rect: textQuadBounds(quad),
|
|
62
|
+
advance: segment.advance
|
|
63
|
+
};
|
|
236
64
|
}
|
|
237
65
|
//#endregion
|
|
238
66
|
//#region src/capability.ts
|
|
239
|
-
const
|
|
67
|
+
const EMPTY_SEGMENTS = [];
|
|
240
68
|
/**
|
|
241
69
|
* The selection capability. The reducer state holds the selection range, the
|
|
242
|
-
* derived content-space
|
|
243
|
-
* non-serializable) per-page
|
|
70
|
+
* derived content-space segments (per page), and per-page loaded flags; the
|
|
71
|
+
* (large, non-serializable) per-page canonical layout is cached HERE in the
|
|
72
|
+
* closure. Segmentation itself is the engine-core text layout — selection
|
|
73
|
+
* owns gestures and state, never how glyphs become lines.
|
|
244
74
|
*
|
|
245
|
-
* Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph),
|
|
246
|
-
* a drag from page 2 into page 4 selects the tail of 2, all of 3, and the
|
|
247
|
-
* 4. `recompute` rebuilds the merged line
|
|
248
|
-
* and re-runs whenever a mid-span page finishes loading.
|
|
75
|
+
* Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph),
|
|
76
|
+
* so a drag from page 2 into page 4 selects the tail of 2, all of 3, and the
|
|
77
|
+
* head of 4. `recompute` rebuilds the merged line segments for every loaded
|
|
78
|
+
* page in the span and re-runs whenever a mid-span page finishes loading.
|
|
249
79
|
*/
|
|
250
80
|
function createSelectionCapability(ctx) {
|
|
251
81
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -255,6 +85,7 @@ function createSelectionCapability(ctx) {
|
|
|
255
85
|
const fireChange = () => changeCbs.forEach((cb) => cb());
|
|
256
86
|
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;
|
|
257
87
|
const ponAtIndex = (i) => ctx.document()?.pages[i]?.pageObjectNumber;
|
|
88
|
+
const glyphAt = (geom, point) => textGlyphAt(geom.layout, contentPointToPdf(geom, point));
|
|
258
89
|
function orderedEnds(sel) {
|
|
259
90
|
const ai = pageIndexOf(sel.anchor.pon);
|
|
260
91
|
const fi = pageIndexOf(sel.focus.pon);
|
|
@@ -269,18 +100,32 @@ function createSelectionCapability(ctx) {
|
|
|
269
100
|
};
|
|
270
101
|
}
|
|
271
102
|
function endpointFor(ptr, which) {
|
|
272
|
-
const
|
|
273
|
-
if (!
|
|
103
|
+
const segments = ctx.getState().segments[ptr.pon] ?? EMPTY_SEGMENTS;
|
|
104
|
+
if (!segments.length) return null;
|
|
105
|
+
const segment = which === "start" ? segments[0] : segments[segments.length - 1];
|
|
106
|
+
const geom = cache.get(ptr.pon);
|
|
107
|
+
const cell = geom ? textGlyphQuad(geom.layout, ptr.glyph) : null;
|
|
108
|
+
if (geom && cell) {
|
|
109
|
+
const glyphQuad = toContentTextQuad(geom, cell);
|
|
110
|
+
return {
|
|
111
|
+
pon: ptr.pon,
|
|
112
|
+
glyphQuad,
|
|
113
|
+
advance: segment.advance,
|
|
114
|
+
rect: textQuadBounds(glyphQuad)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
274
117
|
return {
|
|
275
118
|
pon: ptr.pon,
|
|
276
|
-
|
|
119
|
+
glyphQuad: segment.quad,
|
|
120
|
+
advance: segment.advance,
|
|
121
|
+
rect: segment.rect
|
|
277
122
|
};
|
|
278
123
|
}
|
|
279
124
|
function snapshot() {
|
|
280
|
-
const { selection,
|
|
281
|
-
const pages = Object.keys(
|
|
125
|
+
const { selection, segments } = ctx.getState();
|
|
126
|
+
const pages = Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0).map((pon) => ({
|
|
282
127
|
pon,
|
|
283
|
-
|
|
128
|
+
segments: segments[pon]
|
|
284
129
|
}));
|
|
285
130
|
if (!selection) return {
|
|
286
131
|
pages,
|
|
@@ -304,7 +149,7 @@ function createSelectionCapability(ctx) {
|
|
|
304
149
|
pending.add(pon);
|
|
305
150
|
doc.page(pon).geometry.read().then((snapshot) => {
|
|
306
151
|
pending.delete(pon);
|
|
307
|
-
cache.set(pon,
|
|
152
|
+
cache.set(pon, buildSelectionPageGeometry(snapshot, layout.boxes.crop, layout.rotation, layout.userUnit));
|
|
308
153
|
ctx.dispatch({
|
|
309
154
|
type: "PAGE_LOADED",
|
|
310
155
|
pon
|
|
@@ -320,30 +165,32 @@ function createSelectionCapability(ctx) {
|
|
|
320
165
|
const si = pageIndexOf(start.pon);
|
|
321
166
|
const ei = pageIndexOf(end.pon);
|
|
322
167
|
if (si < 0 || ei < 0) return;
|
|
323
|
-
const
|
|
168
|
+
const segments = {};
|
|
324
169
|
for (let i = si; i <= ei; i++) {
|
|
325
170
|
const pon = ponAtIndex(i);
|
|
326
171
|
if (pon == null) continue;
|
|
327
|
-
const
|
|
328
|
-
if (!
|
|
172
|
+
const geom = cache.get(pon);
|
|
173
|
+
if (!geom) {
|
|
329
174
|
ensurePage(pon);
|
|
330
175
|
continue;
|
|
331
176
|
}
|
|
332
|
-
|
|
177
|
+
const from = i === si ? start.glyph : 0;
|
|
178
|
+
const to = i === ei ? end.glyph : geom.layout.glyphs.length - 1;
|
|
179
|
+
segments[pon] = textSegmentsForRange(geom.layout, from, to - from + 1).map((s) => toContentSegment(geom, s));
|
|
333
180
|
}
|
|
334
181
|
ctx.dispatch({
|
|
335
182
|
type: "SET",
|
|
336
183
|
selection: sel,
|
|
337
|
-
|
|
184
|
+
segments
|
|
338
185
|
});
|
|
339
186
|
fireChange();
|
|
340
187
|
}
|
|
341
188
|
function selectSpan(pon, point, expand) {
|
|
342
|
-
const
|
|
343
|
-
if (!
|
|
344
|
-
const i = glyphAt(
|
|
189
|
+
const geom = cache.get(pon);
|
|
190
|
+
if (!geom) return;
|
|
191
|
+
const i = glyphAt(geom, point);
|
|
345
192
|
if (i == null) return;
|
|
346
|
-
const [from, to] = expand === "word" ?
|
|
193
|
+
const [from, to] = expand === "word" ? expandTextRangeToWord(geom.layout, i) : expandTextRangeToLine(geom.layout, i);
|
|
347
194
|
recompute({
|
|
348
195
|
anchor: {
|
|
349
196
|
pon,
|
|
@@ -359,13 +206,13 @@ function createSelectionCapability(ctx) {
|
|
|
359
206
|
ensurePage,
|
|
360
207
|
isLoaded: (pon) => !!ctx.getState().loaded[pon],
|
|
361
208
|
isOverText: (pon, point) => {
|
|
362
|
-
const
|
|
363
|
-
return
|
|
209
|
+
const geom = cache.get(pon);
|
|
210
|
+
return geom ? glyphAt(geom, point) != null : false;
|
|
364
211
|
},
|
|
365
212
|
beginAt: (pon, point) => {
|
|
366
|
-
const
|
|
367
|
-
if (!
|
|
368
|
-
const i = glyphAt(
|
|
213
|
+
const geom = cache.get(pon);
|
|
214
|
+
if (!geom) return false;
|
|
215
|
+
const i = glyphAt(geom, point);
|
|
369
216
|
if (i == null) return false;
|
|
370
217
|
recompute({
|
|
371
218
|
anchor: {
|
|
@@ -384,12 +231,12 @@ function createSelectionCapability(ctx) {
|
|
|
384
231
|
extendTo: (pon, point) => {
|
|
385
232
|
const cur = ctx.getState().selection;
|
|
386
233
|
if (!cur) return;
|
|
387
|
-
const
|
|
388
|
-
if (!
|
|
234
|
+
const geom = cache.get(pon);
|
|
235
|
+
if (!geom) {
|
|
389
236
|
ensurePage(pon);
|
|
390
237
|
return;
|
|
391
238
|
}
|
|
392
|
-
const i = glyphAt(
|
|
239
|
+
const i = glyphAt(geom, point);
|
|
393
240
|
if (i == null) return;
|
|
394
241
|
recompute({
|
|
395
242
|
anchor: cur.anchor,
|
|
@@ -405,11 +252,12 @@ function createSelectionCapability(ctx) {
|
|
|
405
252
|
fireChange();
|
|
406
253
|
},
|
|
407
254
|
snapshot,
|
|
408
|
-
|
|
255
|
+
segmentsForPage: (pon) => ctx.getState().segments[pon] ?? EMPTY_SEGMENTS,
|
|
256
|
+
rectsForPage: (pon) => (ctx.getState().segments[pon] ?? EMPTY_SEGMENTS).map((s) => s.rect),
|
|
409
257
|
hasSelection: () => ctx.getState().selection != null,
|
|
410
258
|
selectedPages: () => {
|
|
411
|
-
const {
|
|
412
|
-
return Object.keys(
|
|
259
|
+
const { segments } = ctx.getState();
|
|
260
|
+
return Object.keys(segments).map(Number).filter((pon) => (segments[pon]?.length ?? 0) > 0);
|
|
413
261
|
},
|
|
414
262
|
onChange: (cb) => {
|
|
415
263
|
changeCbs.add(cb);
|
|
@@ -503,7 +351,7 @@ function createTextSelectHandler(selection, interaction) {
|
|
|
503
351
|
//#region src/reducer.ts
|
|
504
352
|
const initialSelectionState = {
|
|
505
353
|
selection: null,
|
|
506
|
-
|
|
354
|
+
segments: {},
|
|
507
355
|
loaded: {},
|
|
508
356
|
highlightHidden: false
|
|
509
357
|
};
|
|
@@ -519,12 +367,12 @@ const selectionReducer = (state, action) => {
|
|
|
519
367
|
case "SET": return {
|
|
520
368
|
...state,
|
|
521
369
|
selection: action.selection,
|
|
522
|
-
|
|
370
|
+
segments: action.segments
|
|
523
371
|
};
|
|
524
|
-
case "CLEAR": return state.selection === null && Object.keys(state.
|
|
372
|
+
case "CLEAR": return state.selection === null && Object.keys(state.segments).length === 0 ? state : {
|
|
525
373
|
...state,
|
|
526
374
|
selection: null,
|
|
527
|
-
|
|
375
|
+
segments: {}
|
|
528
376
|
};
|
|
529
377
|
case "SET_HIGHLIGHT_HIDDEN": return state.highlightHidden === action.hidden ? state : {
|
|
530
378
|
...state,
|
|
@@ -559,6 +407,6 @@ const selectionPlugin = () => definePlugin({
|
|
|
559
407
|
}
|
|
560
408
|
});
|
|
561
409
|
//#endregion
|
|
562
|
-
export { SelectionToken,
|
|
410
|
+
export { SelectionToken, buildSelectionPageGeometry, contentPointToPdf, createSelectionCapability, createTextSelectHandler, initialSelectionState, selectionPlugin, selectionReducer, toContentSegment, toContentTextQuad };
|
|
563
411
|
|
|
564
412
|
//# sourceMappingURL=index.js.map
|