@embedpdf/core-geometry 3.0.0-next.13 → 3.0.0-next.15

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 CHANGED
@@ -11,11 +11,11 @@ function boundsOfRects(rects) {
11
11
  let y1 = Infinity;
12
12
  let x2 = -Infinity;
13
13
  let y2 = -Infinity;
14
- for (const r of rects) {
15
- x1 = Math.min(x1, r.x);
16
- y1 = Math.min(y1, r.y);
17
- x2 = Math.max(x2, r.x + r.width);
18
- y2 = Math.max(y2, r.y + r.height);
14
+ for (const rect of rects) {
15
+ x1 = Math.min(x1, rect.x);
16
+ y1 = Math.min(y1, rect.y);
17
+ x2 = Math.max(x2, rect.x + rect.width);
18
+ y2 = Math.max(y2, rect.y + rect.height);
19
19
  }
20
20
  return {
21
21
  x: x1,
@@ -30,14 +30,48 @@ function boundsOfRects(rects) {
30
30
  * without a null branch. Space-agnostic like {@link boundsOfRects}: view-px
31
31
  * viewport ∩ footprint, page-point visibility ∩ tile footprints, ….
32
32
  */
33
- function intersectRects(a, b) {
34
- const x = Math.max(a.x, b.x);
35
- const y = Math.max(a.y, b.y);
33
+ function intersectRects(left, right) {
34
+ const x = Math.max(left.x, right.x);
35
+ const y = Math.max(left.y, right.y);
36
36
  return {
37
37
  x,
38
38
  y,
39
- width: Math.max(0, Math.min(a.x + a.width, b.x + b.width) - x),
40
- height: Math.max(0, Math.min(a.y + a.height, b.y + b.height) - y)
39
+ width: Math.max(0, Math.min(left.x + left.width, right.x + right.width) - x),
40
+ height: Math.max(0, Math.min(left.y + left.height, right.y + right.height) - y)
41
+ };
42
+ }
43
+ /** The axis-aligned rect spanned by two corners, in any order — a drag box. */
44
+ function rectFromCorners(from, to) {
45
+ return {
46
+ x: Math.min(from.x, to.x),
47
+ y: Math.min(from.y, to.y),
48
+ width: Math.abs(to.x - from.x),
49
+ height: Math.abs(to.y - from.y)
50
+ };
51
+ }
52
+ /** Whether a rect contains a point, edges inclusive. Space-agnostic. */
53
+ function rectContains(rect, point) {
54
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
55
+ }
56
+ /** Positive-area overlap of two rects — touching edges do not count. */
57
+ function rectsOverlap(left, right) {
58
+ const overlap = intersectRects(left, right);
59
+ return overlap.width > 0 && overlap.height > 0;
60
+ }
61
+ /**
62
+ * The largest rect of `content`'s proportions inside `box`, centred in it:
63
+ * CSS `object-fit: contain`. `box` itself when `content` has no area.
64
+ */
65
+ function containedRect(content, box) {
66
+ if (content.width <= 0 || content.height <= 0) return box;
67
+ const scale = Math.min(box.width / content.width, box.height / content.height);
68
+ const width = content.width * scale;
69
+ const height = content.height * scale;
70
+ return {
71
+ x: box.x + (box.width - width) / 2,
72
+ y: box.y + (box.height - height) / 2,
73
+ width,
74
+ height
41
75
  };
42
76
  }
43
77
  const NO_FRAME = {
@@ -51,16 +85,16 @@ function isQuarterTurn(rotation) {
51
85
  return rotation === 90 || rotation === 270;
52
86
  }
53
87
  /**
54
- * Compose two quarter-turn rotations (mod 360). THE way the shell resolves a
55
- * page's TOTAL display rotation — `addRotations(page /Rotate, view rotation)`
88
+ * Compose two quarter-turn rotations (mod 360). The way the shell resolves a
89
+ * page's total display rotation — `addRotations(page /Rotate, view rotation)`
56
90
  * — so the wrap arithmetic is never hand-written (and drift) per call site.
57
91
  */
58
- function addRotations(a, b) {
59
- return (a + b) % 360;
92
+ function addRotations(left, right) {
93
+ return (left + right) % 360;
60
94
  }
61
95
  /**
62
96
  * Snap a view-px value to the device pixel grid: round to a whole device pixel,
63
- * back to view px. Used for a page's screen POSITION so a CSS-rotated page lands
97
+ * back to view px. Used for a page's screen position so a CSS-rotated page lands
64
98
  * on the grid (no sub-pixel anti-aliased fringe). The shell never hand-rounds.
65
99
  */
66
100
  function snapToDevice(value, dpr) {
@@ -77,13 +111,13 @@ function displaySize(content, rotation) {
77
111
  };
78
112
  }
79
113
  /**
80
- * The engine's `kind:'width'` height rule, replicated EXACTLY so the viewer can
114
+ * The engine's `kind:'width'` height rule, replicated exactly so the viewer can
81
115
  * predict the rendered bitmap's height up front — the page box is sized to it,
82
116
  * so the box matches the bitmap with no reflow and no 1px seam.
83
117
  *
84
118
  * Mirrors `PageRenderReader.resolveDeviceSize` (engine-services): we render the
85
- * page UN-rotated (CSS applies the rotation), so there is no width/height swap
86
- * here — `pageSize` is the page's own points. MUST stay in lockstep with that
119
+ * page un-rotated (CSS applies the rotation), so there is no width/height swap
120
+ * here — `pageSize` is the page's own points. Must stay in lockstep with that
87
121
  * function; the round-trip test in the render plugin asserts they agree.
88
122
  */
89
123
  function deviceHeightForWidth(pageSize, deviceWidth) {
@@ -91,7 +125,7 @@ function deviceHeightForWidth(pageSize, deviceWidth) {
91
125
  }
92
126
  /**
93
127
  * Build a page's transform from its intrinsic size + the platform/view scale.
94
- * `scale` is VIEW px per PDF point (the shell composes it: `viewUnitsPerPoint ×
128
+ * `scale` is view px per PDF point (the shell composes it: `viewUnitsPerPoint ×
95
129
  * contentScale × zoom`); `dpr` is device px per view px. Device dimensions snap
96
130
  * to the grid (crisp, no rotation fringe) and follow the engine's width rule
97
131
  * (box == bitmap, no reflow).
@@ -119,13 +153,13 @@ function pageTransform(input) {
119
153
  ];
120
154
  const viewMat = rotateScaleMatrix(viewScale, content.width, content.height, rotation);
121
155
  const invMat = invert(viewMat);
122
- const toPixels = (p) => applyPoint(contentMat, p);
156
+ const toPixels = (point) => applyPoint(contentMat, point);
123
157
  const contentInv = invert(contentMat);
124
- const fromPixels = (p) => applyPoint(contentInv, p);
125
- const contentToView = (p) => applyPoint(viewMat, p);
126
- const viewToContent = (p) => applyPoint(invMat, p);
127
- const contentToViewRect = (r) => applyRect(viewMat, r);
128
- const viewToContentRect = (r) => applyRect(invMat, r);
158
+ const fromPixels = (point) => applyPoint(contentInv, point);
159
+ const pageToView = (point) => applyPoint(viewMat, point);
160
+ const viewToPage = (point) => applyPoint(invMat, point);
161
+ const pageToViewRect = (rect) => applyRect(viewMat, rect);
162
+ const viewToPageRect = (rect) => applyRect(invMat, rect);
129
163
  const cssMatrix = matrixToCss(viewMat);
130
164
  return {
131
165
  rotation,
@@ -141,13 +175,22 @@ function pageTransform(input) {
141
175
  zoom,
142
176
  toPixels,
143
177
  fromPixels,
144
- contentToView,
145
- contentToViewRect,
146
- viewToContent,
147
- viewToContentRect,
178
+ pageToView,
179
+ pageToViewRect,
180
+ viewToPage,
181
+ viewToPageRect,
148
182
  cssMatrix
149
183
  };
150
184
  }
185
+ /**
186
+ * Whether two transforms place things on the page the same: the same scale, zoom and turn, and
187
+ * the same content box. A Stage builds a new transform on every frame, also for a plain scroll,
188
+ * which moves the page but changes nothing inside it; a layer that compares with this redraws
189
+ * only when what it draws in the page's pixels moved.
190
+ */
191
+ function samePagePlacement(left, right) {
192
+ return left.viewScale === right.viewScale && left.zoom === right.zoom && left.rotation === right.rotation && left.contentWidth === right.contentWidth && left.contentHeight === right.contentHeight;
193
+ }
151
194
  /** The identity transform within a single space. */
152
195
  function identity() {
153
196
  return [
@@ -160,13 +203,13 @@ function identity() {
160
203
  ];
161
204
  }
162
205
  /**
163
- * Compose two matrices: `compose(m, n)` applies `n` first, then `m` (matrix
164
- * product `m · n`). The shared middle space `B` must line up, so the type system
165
- * rejects composing mismatched hops.
206
+ * Compose two matrices: `compose(matrix, inner)` applies `inner` first, then
207
+ * `matrix` (matrix product `matrix · inner`). The shared middle space `B` must
208
+ * line up, so the type system rejects composing mismatched hops.
166
209
  */
167
- function compose(m, n) {
168
- const [a, b, c, d, e, f] = m;
169
- const [a2, b2, c2, d2, e2, f2] = n;
210
+ function compose(matrix, inner) {
211
+ const [a, b, c, d, e, f] = matrix;
212
+ const [a2, b2, c2, d2, e2, f2] = inner;
170
213
  return [
171
214
  a * a2 + c * b2,
172
215
  b * a2 + d * b2,
@@ -176,9 +219,9 @@ function compose(m, n) {
176
219
  b * e2 + d * f2 + f
177
220
  ];
178
221
  }
179
- /** Invert a transform — `invert(pdfToView)` is `viewToPdf`, i.e. hit-testing for free. */
180
- function invert(m) {
181
- const [a, b, c, d, e, f] = m;
222
+ /** Invert a transform — `invert(pageToView)` is `viewToPage`, i.e. hit-testing for free. */
223
+ function invert(matrix) {
224
+ const [a, b, c, d, e, f] = matrix;
182
225
  const det = a * d - b * c;
183
226
  if (det === 0) throw new Error("Mat2D.invert: non-invertible matrix (det = 0)");
184
227
  const ia = d / det;
@@ -195,35 +238,35 @@ function invert(m) {
195
238
  ];
196
239
  }
197
240
  /** Apply a transform to a point. */
198
- function applyPoint(m, p) {
199
- const [a, b, c, d, e, f] = m;
241
+ function applyPoint(matrix, point) {
242
+ const [a, b, c, d, e, f] = matrix;
200
243
  return {
201
- x: a * p.x + c * p.y + e,
202
- y: b * p.x + d * p.y + f
244
+ x: a * point.x + c * point.y + e,
245
+ y: b * point.x + d * point.y + f
203
246
  };
204
247
  }
205
248
  /**
206
249
  * Apply a transform to a rect, returning the axis-aligned bounding box of the
207
- * four transformed corners. EXACT for translate/scale/quarter-turn (every page
250
+ * four transformed corners. Exact for translate/scale/quarter-turn (every page
208
251
  * transform `pageGeometry` produces); lossy under skew — use {@link applyQuad}
209
252
  * for content that can be rotated off-axis.
210
253
  */
211
- function applyRect(m, r) {
212
- const c0 = applyPoint(m, {
213
- x: r.x,
214
- y: r.y
254
+ function applyRect(matrix, rect) {
255
+ const c0 = applyPoint(matrix, {
256
+ x: rect.x,
257
+ y: rect.y
215
258
  });
216
- const c1 = applyPoint(m, {
217
- x: r.x + r.width,
218
- y: r.y
259
+ const c1 = applyPoint(matrix, {
260
+ x: rect.x + rect.width,
261
+ y: rect.y
219
262
  });
220
- const c2 = applyPoint(m, {
221
- x: r.x,
222
- y: r.y + r.height
263
+ const c2 = applyPoint(matrix, {
264
+ x: rect.x,
265
+ y: rect.y + rect.height
223
266
  });
224
- const c3 = applyPoint(m, {
225
- x: r.x + r.width,
226
- y: r.y + r.height
267
+ const c3 = applyPoint(matrix, {
268
+ x: rect.x + rect.width,
269
+ y: rect.y + rect.height
227
270
  });
228
271
  const xs = [
229
272
  c0.x,
@@ -246,22 +289,22 @@ function applyRect(m, r) {
246
289
  height: Math.max(...ys) - minY
247
290
  };
248
291
  }
249
- /** Apply a transform to a quad, preserving orientation (maps the four points). */
250
- function applyQuad(m, q) {
292
+ /** Apply a transform to a quad: each corner moves and keeps its name. */
293
+ function applyQuad(matrix, quad) {
251
294
  return {
252
- p1: applyPoint(m, q.p1),
253
- p2: applyPoint(m, q.p2),
254
- p3: applyPoint(m, q.p3),
255
- p4: applyPoint(m, q.p4)
295
+ upperLeft: applyPoint(matrix, quad.upperLeft),
296
+ upperRight: applyPoint(matrix, quad.upperRight),
297
+ lowerLeft: applyPoint(matrix, quad.lowerLeft),
298
+ lowerRight: applyPoint(matrix, quad.lowerRight)
256
299
  };
257
300
  }
258
301
  /**
259
- * A `Mat2D` IS a CSS `matrix()` — the six numbers are identical. Drop the result
302
+ * A `Mat2D` is a CSS `matrix()` — the six numbers are identical. Drop the result
260
303
  * on a footprint-space layer (`transform`, `transform-origin: 0 0`) and place
261
304
  * children in the source space; the rotation + scale are handled for free.
262
305
  */
263
- function matrixToCss(m) {
264
- const [a, b, c, d, e, f] = m;
306
+ function matrixToCss(matrix) {
307
+ const [a, b, c, d, e, f] = matrix;
265
308
  return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
266
309
  }
267
310
  /** Translate within a space. */
@@ -286,40 +329,40 @@ function scale(sx, sy) {
286
329
  0
287
330
  ];
288
331
  }
289
- /** Rotate about the space origin (radians; clockwise in y-down content space). */
332
+ /** Rotate about the space origin (radians; clockwise in y-down page space). */
290
333
  function rotate(rad) {
291
- const c = Math.cos(rad);
292
- const s = Math.sin(rad);
334
+ const cosine = Math.cos(rad);
335
+ const sine = Math.sin(rad);
293
336
  return [
294
- c,
295
- s,
296
- -s,
297
- c,
337
+ cosine,
338
+ sine,
339
+ -sine,
340
+ cosine,
298
341
  0,
299
342
  0
300
343
  ];
301
344
  }
302
345
  /** Rotate about an arbitrary pivot: `T(c) · R(rad) · T(-c)`. */
303
- function rotateAbout(c, rad) {
304
- return compose(translate(c.x, c.y), compose(rotate(rad), translate(-c.x, -c.y)));
346
+ function rotateAbout(point, rad) {
347
+ return compose(translate(point.x, point.y), compose(rotate(rad), translate(-point.x, -point.y)));
305
348
  }
306
349
  /** Scale about an arbitrary anchor: `T(a) · S(sx, sy) · T(-a)`. */
307
- function scaleAbout(a, sx, sy) {
308
- return compose(translate(a.x, a.y), compose(scale(sx, sy), translate(-a.x, -a.y)));
350
+ function scaleAbout(point, sx, sy) {
351
+ return compose(translate(point.x, point.y), compose(scale(sx, sy), translate(-point.x, -point.y)));
309
352
  }
310
353
  /** The rotation angle (radians) encoded in a matrix — `atan2(b, a)`. Positive is
311
- * clockwise in y-down content space (the inverse of {@link rotate}). */
312
- function angleOf(m) {
313
- return Math.atan2(m[1], m[0]);
354
+ * clockwise in y-down page space (the inverse of {@link rotate}). */
355
+ function angleOf(matrix) {
356
+ return Math.atan2(matrix[1], matrix[0]);
314
357
  }
315
358
  /**
316
- * The page's scale + integer quarter-turn as a `Mat2D`: a content point maps to
317
- * its place in the rotated DISPLAY box. `scale` is output units per content
318
- * point; `boxW`/`boxH` are the UN-rotated content's extents in those same output
359
+ * The page's scale + integer quarter-turn as a `Mat2D`: a page point maps to
360
+ * its place in the rotated display box. `scale` is output units per content
361
+ * point; `boxW`/`boxH` are the un-rotated content's extents in those same output
319
362
  * units, and the offsets keep the footprint in the box's positive quadrant
320
363
  * (origin at the box top-left).
321
364
  *
322
- * THE single encoding of the quarter-turn — `pageTransform` (view px),
365
+ * The single encoding of the quarter-turn — `pageTransform` (view px),
323
366
  * `pageGeometry` (the `content→view` hop), and `pageToWorld` (world units) all
324
367
  * build on this one function, so the rotation can never drift between them.
325
368
  */
@@ -359,112 +402,75 @@ function rotateScaleMatrix(scale, boxW, boxH, rotation) {
359
402
  ];
360
403
  }
361
404
  }
362
- /**
363
- * PDF user space (y-up, origin = crop bottom-left) → content space (y-down,
364
- * origin = crop top-left, unscaled points). THE single encoding of the engine→
365
- * content y-flip + crop offset: `pageGeometry` composes its `pdfToContent` from
366
- * this, and pure annotation geometry (content-space editing) reuses it too, so
367
- * the rule can never be hand-written twice and drift.
368
- */
369
- function pdfToContentMatrix(crop) {
370
- return [
371
- 1,
372
- 0,
373
- 0,
374
- -1,
375
- -crop.left,
376
- crop.top
377
- ];
378
- }
379
- /**
380
- * Build a page's space matrices from its crop box, rotation, and `userUnit` plus
381
- * the viewer `zoom`. This is the ONE function that knows the y-flip, crop origin,
382
- * `userUnit`, and rotation; everything else composes from its output. The engine
383
- * never emits these — they bake in viewer state (zoom, later scroll/dpr) — so the
384
- * boundary stays clean: engine owns `Pdf*` truth, the viewer composes the rest.
385
- */
386
- function pageGeometry(input, zoom) {
387
- const { crop, rotation, userUnit } = input;
388
- const s = zoom * userUnit;
389
- const pdfToContent = pdfToContentMatrix(crop);
390
- const contentToView = rotateScaleMatrix(s, (crop.right - crop.left) * s, (crop.top - crop.bottom) * s, rotation);
391
- const pdfToView = compose(contentToView, pdfToContent);
405
+ /** An upright quad over a rect (y-down: upper = smaller y). */
406
+ function quadFromRect(rect) {
392
407
  return {
393
- pdfToContent,
394
- contentToView,
395
- pdfToView,
396
- viewToPdf: invert(pdfToView)
397
- };
398
- }
399
- /** Axis-aligned TextQuad over a rect (y-down: upper = smaller y). */
400
- function textQuadFromRect(r) {
401
- return {
402
- upperStart: {
403
- x: r.x,
404
- y: r.y
408
+ upperLeft: {
409
+ x: rect.x,
410
+ y: rect.y
405
411
  },
406
- upperEnd: {
407
- x: r.x + r.width,
408
- y: r.y
412
+ upperRight: {
413
+ x: rect.x + rect.width,
414
+ y: rect.y
409
415
  },
410
- lowerStart: {
411
- x: r.x,
412
- y: r.y + r.height
416
+ lowerLeft: {
417
+ x: rect.x,
418
+ y: rect.y + rect.height
413
419
  },
414
- lowerEnd: {
415
- x: r.x + r.width,
416
- y: r.y + r.height
420
+ lowerRight: {
421
+ x: rect.x + rect.width,
422
+ y: rect.y + rect.height
417
423
  }
418
424
  };
419
425
  }
420
- /** The four corners in slot order: upper-start, upper-end, lower-start, lower-end. */
421
- function textQuadPoints(t) {
426
+ /** The four corners: upper-left, upper-right, lower-left, lower-right. */
427
+ function quadCorners(quad) {
422
428
  return [
423
- t.upperStart,
424
- t.upperEnd,
425
- t.lowerStart,
426
- t.lowerEnd
429
+ quad.upperLeft,
430
+ quad.upperRight,
431
+ quad.lowerLeft,
432
+ quad.lowerRight
427
433
  ];
428
434
  }
429
- /** The corners as a polygon RING (US → UE → LE → LS) — for `<polygon>`/fills. */
430
- function textQuadRing(t) {
435
+ /** The corners as a polygon ring: upper-left, upper-right, lower-right, lower-left. */
436
+ function quadRing(quad) {
431
437
  return [
432
- t.upperStart,
433
- t.upperEnd,
434
- t.lowerEnd,
435
- t.lowerStart
438
+ quad.upperLeft,
439
+ quad.upperRight,
440
+ quad.lowerRight,
441
+ quad.lowerLeft
436
442
  ];
437
443
  }
438
444
  /**
439
- * One SIDE edge of the cell — the start (`US → LS`) or end (`UE → LE`) edge,
440
- * ASCENT CORNER FIRST. This is the segment a caret or a selection handle
445
+ * One side edge of the cell, the left (`UL → LL`) or right (`UR → LR`) one,
446
+ * ascent corner first. This is the segment a caret or a selection handle
441
447
  * occupies: its length is the glyph's ink height in the text's own frame (not
442
448
  * the AABB height, which grows with tilt), and its direction carries the
443
- * text's rotation. Which side is the selection's LEADING edge is a reading
449
+ * text's rotation. Which side is the selection's leading edge is a reading
444
450
  * -order question — decide it with `advance`, not with geometry.
445
451
  */
446
- function textQuadEdge(t, side) {
447
- return side === "start" ? [t.upperStart, t.lowerStart] : [t.upperEnd, t.lowerEnd];
452
+ function quadEdge(quad, side) {
453
+ return side === "left" ? [quad.upperLeft, quad.lowerLeft] : [quad.upperRight, quad.lowerRight];
448
454
  }
449
- /** Corner-wise equality — an AABB comparison would call a quad that ROTATED
455
+ /** Corner-wise equality — an AABB comparison would call a quad that rotated
450
456
  * in place "unchanged"; corners cannot. (Change-detection for quad consumers.) */
451
- function textQuadEquals(a, b) {
452
- const eq = (p, q) => p.x === q.x && p.y === q.y;
453
- return eq(a.upperStart, b.upperStart) && eq(a.upperEnd, b.upperEnd) && eq(a.lowerStart, b.lowerStart) && eq(a.lowerEnd, b.lowerEnd);
457
+ function quadEquals(left, right) {
458
+ const eq = (point, other) => point.x === other.x && point.y === other.y;
459
+ return eq(left.upperLeft, right.upperLeft) && eq(left.upperRight, right.upperRight) && eq(left.lowerLeft, right.lowerLeft) && eq(left.lowerRight, right.lowerRight);
454
460
  }
455
- /** Axis-aligned bounds of a TextQuad. */
456
- function textQuadBounds(t) {
461
+ /** Axis-aligned bounds of a quad. */
462
+ function quadBounds(quad) {
457
463
  const xs = [
458
- t.upperStart.x,
459
- t.upperEnd.x,
460
- t.lowerStart.x,
461
- t.lowerEnd.x
464
+ quad.upperLeft.x,
465
+ quad.upperRight.x,
466
+ quad.lowerLeft.x,
467
+ quad.lowerRight.x
462
468
  ];
463
469
  const ys = [
464
- t.upperStart.y,
465
- t.upperEnd.y,
466
- t.lowerStart.y,
467
- t.lowerEnd.y
470
+ quad.upperLeft.y,
471
+ quad.upperRight.y,
472
+ quad.lowerLeft.y,
473
+ quad.lowerRight.y
468
474
  ];
469
475
  const minX = Math.min(...xs);
470
476
  const minY = Math.min(...ys);
@@ -475,141 +481,6 @@ function textQuadBounds(t) {
475
481
  height: Math.max(...ys) - minY
476
482
  };
477
483
  }
478
- /** Map a TextQuad through an affine transform (corner semantics ride along). */
479
- function applyTextQuad(m, t) {
480
- return {
481
- upperStart: applyPoint(m, t.upperStart),
482
- upperEnd: applyPoint(m, t.upperEnd),
483
- lowerStart: applyPoint(m, t.lowerStart),
484
- lowerEnd: applyPoint(m, t.lowerEnd)
485
- };
486
- }
487
- /** Serialize to the positional zigzag convention (`p1..p4` = US, UE, LS, LE). */
488
- function positionalQuad(t) {
489
- return {
490
- p1: t.upperStart,
491
- p2: t.upperEnd,
492
- p3: t.lowerStart,
493
- p4: t.lowerEnd
494
- };
495
- }
496
- const QUAD_EPSILON = 1e-6;
497
- function finitePoint(p) {
498
- return Number.isFinite(p.x) && Number.isFinite(p.y);
499
- }
500
- /** Is `[us, ue, ls, le]` a well-formed zigzag reading of a quad? */
501
- function zigzagWellFormed([us, ue, ls, le]) {
502
- if (![
503
- us,
504
- ue,
505
- ls,
506
- le
507
- ].every(finitePoint)) return false;
508
- const upper = {
509
- x: ue.x - us.x,
510
- y: ue.y - us.y
511
- };
512
- const lower = {
513
- x: le.x - ls.x,
514
- y: le.y - ls.y
515
- };
516
- const startSide = {
517
- x: ls.x - us.x,
518
- y: ls.y - us.y
519
- };
520
- const endSide = {
521
- x: le.x - ue.x,
522
- y: le.y - ue.y
523
- };
524
- const lenU = Math.hypot(upper.x, upper.y);
525
- const lenL = Math.hypot(lower.x, lower.y);
526
- const lenS = Math.hypot(startSide.x, startSide.y);
527
- const lenE = Math.hypot(endSide.x, endSide.y);
528
- if (lenU <= QUAD_EPSILON || lenL <= QUAD_EPSILON || lenS <= QUAD_EPSILON || lenE <= QUAD_EPSILON) return false;
529
- if (upper.x * lower.x + upper.y * lower.y <= 0) return false;
530
- if (startSide.x * endSide.x + startSide.y * endSide.y <= 0) return false;
531
- const startArea = upper.x * startSide.y - upper.y * startSide.x;
532
- const endArea = lower.x * endSide.y - lower.y * endSide.x;
533
- if (Math.abs(startArea) <= QUAD_EPSILON * lenU * lenS) return false;
534
- if (Math.abs(endArea) <= QUAD_EPSILON * lenL * lenE) return false;
535
- return Math.sign(startArea) === Math.sign(endArea);
536
- }
537
- /**
538
- * Interpret an arbitrary positional quad (an imported `/QuadPoints` entry,
539
- * already in content space) as a TextQuad — a NORMALIZER, not a cast:
540
- *
541
- * 1. the de-facto zigzag order (US, UE, LS, LE) passes through untouched —
542
- * this covers every well-formed writer, rotated quads included;
543
- * 2. the common ring order (US, UE, LE, LS) is recognized and repaired;
544
- * 3. anything else gets a deterministic geometric labeling: corners sorted
545
- * into a ring around their centroid, "upper" = the edge whose midpoint
546
- * sits highest on screen (smallest y). The 180° "which side is up"
547
- * ambiguity is unresolvable from geometry alone — convention decides.
548
- *
549
- * Drawing code downstream (`markupScene`) reads corner SEMANTICS, so a
550
- * mislabeled import can shift an underline to the wrong edge but can never
551
- * crash or self-intersect.
552
- */
553
- function normalizeQuad(q) {
554
- if (zigzagWellFormed([
555
- q.p1,
556
- q.p2,
557
- q.p3,
558
- q.p4
559
- ])) return {
560
- upperStart: q.p1,
561
- upperEnd: q.p2,
562
- lowerStart: q.p3,
563
- lowerEnd: q.p4
564
- };
565
- if (zigzagWellFormed([
566
- q.p1,
567
- q.p2,
568
- q.p4,
569
- q.p3
570
- ])) return {
571
- upperStart: q.p1,
572
- upperEnd: q.p2,
573
- lowerStart: q.p4,
574
- lowerEnd: q.p3
575
- };
576
- const pts = [
577
- q.p1,
578
- q.p2,
579
- q.p3,
580
- q.p4
581
- ].map((p) => finitePoint(p) ? p : {
582
- x: 0,
583
- y: 0
584
- });
585
- const cx = (pts[0].x + pts[1].x + pts[2].x + pts[3].x) / 4;
586
- const cy = (pts[0].y + pts[1].y + pts[2].y + pts[3].y) / 4;
587
- const ringSorted = [...pts].sort((a, b) => Math.atan2(a.y - cy, a.x - cx) - Math.atan2(b.y - cy, b.x - cx));
588
- let upperEdge = 0;
589
- let bestY = Infinity;
590
- for (let i = 0; i < 4; i++) {
591
- const midY = (ringSorted[i].y + ringSorted[(i + 1) % 4].y) / 2;
592
- if (midY < bestY) {
593
- bestY = midY;
594
- upperEdge = i;
595
- }
596
- }
597
- const a = ringSorted[upperEdge];
598
- const b = ringSorted[(upperEdge + 1) % 4];
599
- const c = ringSorted[(upperEdge + 2) % 4];
600
- const d = ringSorted[(upperEdge + 3) % 4];
601
- return a.x <= b.x ? {
602
- upperStart: a,
603
- upperEnd: b,
604
- lowerStart: d,
605
- lowerEnd: c
606
- } : {
607
- upperStart: b,
608
- upperEnd: a,
609
- lowerStart: c,
610
- lowerEnd: d
611
- };
612
- }
613
484
  //#endregion
614
485
  exports.NO_FRAME = NO_FRAME;
615
486
  exports.addRotations = addRotations;
@@ -617,9 +488,9 @@ exports.angleOf = angleOf;
617
488
  exports.applyPoint = applyPoint;
618
489
  exports.applyQuad = applyQuad;
619
490
  exports.applyRect = applyRect;
620
- exports.applyTextQuad = applyTextQuad;
621
491
  exports.boundsOfRects = boundsOfRects;
622
492
  exports.compose = compose;
493
+ exports.containedRect = containedRect;
623
494
  exports.deviceHeightForWidth = deviceHeightForWidth;
624
495
  exports.displaySize = displaySize;
625
496
  exports.identity = identity;
@@ -627,23 +498,23 @@ exports.intersectRects = intersectRects;
627
498
  exports.invert = invert;
628
499
  exports.isQuarterTurn = isQuarterTurn;
629
500
  exports.matrixToCss = matrixToCss;
630
- exports.normalizeQuad = normalizeQuad;
631
- exports.pageGeometry = pageGeometry;
632
501
  exports.pageTransform = pageTransform;
633
- exports.pdfToContentMatrix = pdfToContentMatrix;
634
- exports.positionalQuad = positionalQuad;
502
+ exports.quadBounds = quadBounds;
503
+ exports.quadCorners = quadCorners;
504
+ exports.quadEdge = quadEdge;
505
+ exports.quadEquals = quadEquals;
506
+ exports.quadFromRect = quadFromRect;
507
+ exports.quadRing = quadRing;
508
+ exports.rectContains = rectContains;
509
+ exports.rectFromCorners = rectFromCorners;
510
+ exports.rectsOverlap = rectsOverlap;
635
511
  exports.rotate = rotate;
636
512
  exports.rotateAbout = rotateAbout;
637
513
  exports.rotateScaleMatrix = rotateScaleMatrix;
514
+ exports.samePagePlacement = samePagePlacement;
638
515
  exports.scale = scale;
639
516
  exports.scaleAbout = scaleAbout;
640
517
  exports.snapToDevice = snapToDevice;
641
- exports.textQuadBounds = textQuadBounds;
642
- exports.textQuadEdge = textQuadEdge;
643
- exports.textQuadEquals = textQuadEquals;
644
- exports.textQuadFromRect = textQuadFromRect;
645
- exports.textQuadPoints = textQuadPoints;
646
- exports.textQuadRing = textQuadRing;
647
518
  exports.translate = translate;
648
519
 
649
520
  //# sourceMappingURL=index.cjs.map