pa_font 0.2.3 → 0.2.5

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/paFont.js CHANGED
@@ -3,52 +3,127 @@ import { load, parse } from "opentype.js";
3
3
  function toScreenPoint(point, scale) {
4
4
  return [point.x * scale, -point.y * scale];
5
5
  }
6
- function cloneRawPoint(point) {
7
- return {
8
- x: point.x,
9
- y: point.y
10
- };
11
- }
12
6
  function pushUniquePoint(points, point) {
13
- if (points.length === 0 || !pointsEqual2D(points[points.length - 1], point)) points.push(point);
14
- }
15
- function flattenQuadratic(p0, p1, p2, tolerance, out, depth = 0) {
16
- if (depth >= 12 || quadFlatness(p0, p1, p2) <= tolerance) {
17
- pushUniquePoint(out, p2);
18
- return;
19
- }
20
- const p01 = midpoint(p0, p1);
21
- const p12 = midpoint(p1, p2);
22
- const p012 = midpoint(p01, p12);
23
- flattenQuadratic(p0, p01, p012, tolerance, out, depth + 1);
24
- flattenQuadratic(p012, p12, p2, tolerance, out, depth + 1);
25
- }
26
- function flattenCubic(p0, p1, p2, p3, tolerance, out, depth = 0) {
27
- if (depth >= 12 || cubicFlatness(p0, p1, p2, p3) <= tolerance) {
28
- pushUniquePoint(out, p3);
29
- return;
30
- }
31
- const p01 = midpoint(p0, p1);
32
- const p12 = midpoint(p1, p2);
33
- const p23 = midpoint(p2, p3);
34
- const p012 = midpoint(p01, p12);
35
- const p123 = midpoint(p12, p23);
36
- const p0123 = midpoint(p012, p123);
37
- flattenCubic(p0, p01, p012, p0123, tolerance, out, depth + 1);
38
- flattenCubic(p0123, p123, p23, p3, tolerance, out, depth + 1);
39
- }
40
- function quadFlatness(p0, p1, p2) {
41
- return pointToLineDistance(p1, p0, p2);
42
- }
43
- function cubicFlatness(p0, p1, p2, p3) {
44
- return Math.max(pointToLineDistance(p1, p0, p3), pointToLineDistance(p2, p0, p3));
45
- }
46
- function pointToLineDistance(point, lineStart, lineEnd) {
47
- const dx = lineEnd[0] - lineStart[0];
48
- const dy = lineEnd[1] - lineStart[1];
49
- const lengthSq = dx * dx + dy * dy;
50
- if (lengthSq === 0) return distance(point, lineStart);
51
- return Math.abs(dx * (lineStart[1] - point[1]) - (lineStart[0] - point[0]) * dy) / Math.sqrt(lengthSq);
7
+ if (points.length === 0 || !pointsEqual2D$1(points[points.length - 1], point)) points.push(point);
8
+ }
9
+ function flattenQuadratic(p0, p1, p2, tolerance, out) {
10
+ let sp = 0;
11
+ const stack = _quadStack;
12
+ stack[sp++] = p0[0];
13
+ stack[sp++] = p0[1];
14
+ stack[sp++] = p1[0];
15
+ stack[sp++] = p1[1];
16
+ stack[sp++] = p2[0];
17
+ stack[sp++] = p2[1];
18
+ stack[sp++] = 0;
19
+ while (sp > 0) {
20
+ const depth = stack[--sp];
21
+ const ax2 = stack[--sp];
22
+ const ay2 = stack[--sp];
23
+ const ax1 = stack[--sp];
24
+ const ay1 = stack[--sp];
25
+ const ax0 = stack[--sp];
26
+ const ay0 = stack[--sp];
27
+ if (depth >= 12 || _quadFlatnessInline(ax0, ay0, ax1, ay1, ax2, ay2) <= tolerance) {
28
+ pushUniquePoint(out, [ax2, ay2]);
29
+ continue;
30
+ }
31
+ const m01x = (ax0 + ax1) * .5, m01y = (ay0 + ay1) * .5;
32
+ const m12x = (ax1 + ax2) * .5, m12y = (ay1 + ay2) * .5;
33
+ const mx = (m01x + m12x) * .5, my = (m01y + m12y) * .5;
34
+ const d1 = depth + 1;
35
+ stack[sp++] = mx;
36
+ stack[sp++] = my;
37
+ stack[sp++] = m12x;
38
+ stack[sp++] = m12y;
39
+ stack[sp++] = ax2;
40
+ stack[sp++] = ay2;
41
+ stack[sp++] = d1;
42
+ stack[sp++] = ax0;
43
+ stack[sp++] = ay0;
44
+ stack[sp++] = m01x;
45
+ stack[sp++] = m01y;
46
+ stack[sp++] = mx;
47
+ stack[sp++] = my;
48
+ stack[sp++] = d1;
49
+ }
50
+ }
51
+ function flattenCubic(p0, p1, p2, p3, tolerance, out) {
52
+ let sp = 0;
53
+ const stack = _cubicStack;
54
+ stack[sp++] = p0[0];
55
+ stack[sp++] = p0[1];
56
+ stack[sp++] = p1[0];
57
+ stack[sp++] = p1[1];
58
+ stack[sp++] = p2[0];
59
+ stack[sp++] = p2[1];
60
+ stack[sp++] = p3[0];
61
+ stack[sp++] = p3[1];
62
+ stack[sp++] = 0;
63
+ while (sp > 0) {
64
+ const depth = stack[--sp];
65
+ const bx3 = stack[--sp];
66
+ const by3 = stack[--sp];
67
+ const bx2 = stack[--sp];
68
+ const by2 = stack[--sp];
69
+ const bx1 = stack[--sp];
70
+ const by1 = stack[--sp];
71
+ const bx0 = stack[--sp];
72
+ const by0 = stack[--sp];
73
+ if (depth >= 12 || _cubicFlatnessInline(bx0, by0, bx1, by1, bx2, by2, bx3, by3) <= tolerance) {
74
+ pushUniquePoint(out, [bx3, by3]);
75
+ continue;
76
+ }
77
+ const m01x = (bx0 + bx1) * .5, m01y = (by0 + by1) * .5;
78
+ const m12x = (bx1 + bx2) * .5, m12y = (by1 + by2) * .5;
79
+ const m23x = (bx2 + bx3) * .5, m23y = (by2 + by3) * .5;
80
+ const m012x = (m01x + m12x) * .5, m012y = (m01y + m12y) * .5;
81
+ const m123x = (m12x + m23x) * .5, m123y = (m12y + m23y) * .5;
82
+ const mx = (m012x + m123x) * .5, my = (m012y + m123y) * .5;
83
+ const d1 = depth + 1;
84
+ stack[sp++] = mx;
85
+ stack[sp++] = my;
86
+ stack[sp++] = m123x;
87
+ stack[sp++] = m123y;
88
+ stack[sp++] = m23x;
89
+ stack[sp++] = m23y;
90
+ stack[sp++] = bx3;
91
+ stack[sp++] = by3;
92
+ stack[sp++] = d1;
93
+ stack[sp++] = bx0;
94
+ stack[sp++] = by0;
95
+ stack[sp++] = m01x;
96
+ stack[sp++] = m01y;
97
+ stack[sp++] = m012x;
98
+ stack[sp++] = m012y;
99
+ stack[sp++] = mx;
100
+ stack[sp++] = my;
101
+ stack[sp++] = d1;
102
+ }
103
+ }
104
+ var _quadStack = new Float64Array(896);
105
+ var _cubicStack = new Float64Array(1152);
106
+ function _quadFlatnessInline(x0, y0, x1, y1, x2, y2) {
107
+ const dx = x2 - x0, dy = y2 - y0;
108
+ const lenSq = dx * dx + dy * dy;
109
+ if (lenSq === 0) {
110
+ const ex = x1 - x0, ey = y1 - y0;
111
+ return Math.sqrt(ex * ex + ey * ey);
112
+ }
113
+ return Math.abs(dx * (y0 - y1) - (x0 - x1) * dy) / Math.sqrt(lenSq);
114
+ }
115
+ function _cubicFlatnessInline(x0, y0, x1, y1, x2, y2, x3, y3) {
116
+ const dx = x3 - x0, dy = y3 - y0;
117
+ const lenSq = dx * dx + dy * dy;
118
+ if (lenSq === 0) {
119
+ const e1x = x1 - x0, e1y = y1 - y0;
120
+ const e2x = x2 - x0, e2y = y2 - y0;
121
+ return Math.max(Math.sqrt(e1x * e1x + e1y * e1y), Math.sqrt(e2x * e2x + e2y * e2y));
122
+ }
123
+ const invLen = 1 / Math.sqrt(lenSq);
124
+ const a1 = Math.abs(dx * (y0 - y1) - (x0 - x1) * dy);
125
+ const a2 = Math.abs(dx * (y0 - y2) - (x0 - x2) * dy);
126
+ return Math.max(a1, a2) * invLen;
52
127
  }
53
128
  function midpoint(a, b) {
54
129
  return [(a[0] + b[0]) * .5, (a[1] + b[1]) * .5];
@@ -106,28 +181,37 @@ function distance(a, b) {
106
181
  return Math.sqrt(dx * dx + dy * dy);
107
182
  }
108
183
  function sampleRing(ring, step, callback) {
109
- for (let index = 0; index < ring.length; index += 1) {
184
+ const len = ring.length;
185
+ for (let index = 0; index < len; index++) {
110
186
  const a = ring[index];
111
- const b = ring[(index + 1) % ring.length];
112
- const segmentLength = distance(a, b);
187
+ const b = ring[(index + 1) % len];
188
+ const dx = b[0] - a[0], dy = b[1] - a[1];
189
+ const segmentLength = Math.sqrt(dx * dx + dy * dy);
113
190
  const divisions = Math.max(1, Math.ceil(segmentLength / step));
114
- for (let offset = 0; offset < divisions; offset += 1) if (index > 0 || offset > 0) {
191
+ if (index === 0) {
192
+ callback([a[0], a[1]]);
193
+ for (let offset = 1; offset < divisions; offset++) {
194
+ const t = offset / divisions;
195
+ callback([a[0] + dx * t, a[1] + dy * t]);
196
+ }
197
+ } else for (let offset = 0; offset < divisions; offset++) {
115
198
  const t = offset / divisions;
116
- callback([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
117
- } else callback([a[0], a[1]]);
199
+ callback([a[0] + dx * t, a[1] + dy * t]);
200
+ }
118
201
  }
119
202
  }
120
203
  function ringBounds(ring) {
121
- let minX = Number.POSITIVE_INFINITY;
122
- let minY = Number.POSITIVE_INFINITY;
123
- let maxX = Number.NEGATIVE_INFINITY;
124
- let maxY = Number.NEGATIVE_INFINITY;
125
- ring.forEach((point) => {
126
- minX = Math.min(minX, point[0]);
127
- minY = Math.min(minY, point[1]);
128
- maxX = Math.max(maxX, point[0]);
129
- maxY = Math.max(maxY, point[1]);
130
- });
204
+ let minX = ring[0][0];
205
+ let minY = ring[0][1];
206
+ let maxX = minX;
207
+ let maxY = minY;
208
+ for (let i = 1, len = ring.length; i < len; i++) {
209
+ const x = ring[i][0], y = ring[i][1];
210
+ if (x < minX) minX = x;
211
+ else if (x > maxX) maxX = x;
212
+ if (y < minY) minY = y;
213
+ else if (y > maxY) maxY = y;
214
+ }
131
215
  return {
132
216
  x: minX,
133
217
  y: minY,
@@ -136,7 +220,10 @@ function ringBounds(ring) {
136
220
  };
137
221
  }
138
222
  function translateRing(ring, tx, ty) {
139
- return ring.map((point) => [point[0] + tx, point[1] + ty]);
223
+ const len = ring.length;
224
+ const result = new Array(len);
225
+ for (let i = 0; i < len; i++) result[i] = [ring[i][0] + tx, ring[i][1] + ty];
226
+ return result;
140
227
  }
141
228
  function translateRect(rect, tx, ty) {
142
229
  return {
@@ -146,21 +233,29 @@ function translateRect(rect, tx, ty) {
146
233
  h: rect.h
147
234
  };
148
235
  }
149
- function copyRing(ring) {
150
- return ring.map((point) => [point[0], point[1]]);
236
+ function copyRing$1(ring) {
237
+ const len = ring.length;
238
+ const result = new Array(len);
239
+ for (let i = 0; i < len; i++) result[i] = [ring[i][0], ring[i][1]];
240
+ return result;
151
241
  }
152
242
  function combineRects(rects) {
153
- if (rects.length === 0) return null;
154
- let minX = Number.POSITIVE_INFINITY;
155
- let minY = Number.POSITIVE_INFINITY;
156
- let maxX = Number.NEGATIVE_INFINITY;
157
- let maxY = Number.NEGATIVE_INFINITY;
158
- rects.forEach((rect) => {
159
- minX = Math.min(minX, rect.x);
160
- minY = Math.min(minY, rect.y);
161
- maxX = Math.max(maxX, rect.x + rect.w);
162
- maxY = Math.max(maxY, rect.y + rect.h);
163
- });
243
+ const len = rects.length;
244
+ if (len === 0) return null;
245
+ const first = rects[0];
246
+ let minX = first.x;
247
+ let minY = first.y;
248
+ let maxX = first.x + first.w;
249
+ let maxY = first.y + first.h;
250
+ for (let i = 1; i < len; i++) {
251
+ const r = rects[i];
252
+ const rx = r.x, ry = r.y;
253
+ const rx2 = rx + r.w, ry2 = ry + r.h;
254
+ if (rx < minX) minX = rx;
255
+ if (ry < minY) minY = ry;
256
+ if (rx2 > maxX) maxX = rx2;
257
+ if (ry2 > maxY) maxY = ry2;
258
+ }
164
259
  return {
165
260
  x: minX,
166
261
  y: minY,
@@ -191,7 +286,7 @@ function normalizePositive(value, fallback) {
191
286
  function samePoint(a, b) {
192
287
  return a.x === b.x && a.y === b.y;
193
288
  }
194
- function pointsEqual2D(a, b) {
289
+ function pointsEqual2D$1(a, b) {
195
290
  return a[0] === b[0] && a[1] === b[1];
196
291
  }
197
292
  function pointsAlmostEqual2D(a, b, epsilon = 1e-6) {
@@ -202,43 +297,64 @@ function ringPerimeter(ring) {
202
297
  for (let index = 0; index < ring.length; index += 1) total += distance(ring[index], ring[(index + 1) % ring.length]);
203
298
  return total;
204
299
  }
205
- function polylineLength(path) {
300
+ function buildCumulativeDistances(ring) {
301
+ const len = ring.length;
302
+ const cumulative = new Float64Array(len + 1);
206
303
  let total = 0;
207
- for (let index = 0; index < path.length - 1; index += 1) total += distance(path[index], path[index + 1]);
208
- return total;
209
- }
210
- function pointAtClosedPathDistance(ring, distanceAlong) {
211
- const perimeter = ringPerimeter(ring);
304
+ for (let i = 0; i < len; i++) {
305
+ cumulative[i] = total;
306
+ const next = (i + 1) % len;
307
+ const dx = ring[next][0] - ring[i][0];
308
+ const dy = ring[next][1] - ring[i][1];
309
+ total += Math.sqrt(dx * dx + dy * dy);
310
+ }
311
+ cumulative[len] = total;
312
+ return cumulative;
313
+ }
314
+ function buildOpenCumulativeDistances(path) {
315
+ const len = path.length;
316
+ const cumulative = new Float64Array(len);
317
+ let total = 0;
318
+ cumulative[0] = 0;
319
+ for (let i = 1; i < len; i++) {
320
+ const dx = path[i][0] - path[i - 1][0];
321
+ const dy = path[i][1] - path[i - 1][1];
322
+ total += Math.sqrt(dx * dx + dy * dy);
323
+ cumulative[i] = total;
324
+ }
325
+ return cumulative;
326
+ }
327
+ function binarySearchSegment(cumulative, d) {
328
+ let lo = 0, hi = cumulative.length - 2;
329
+ while (lo < hi) {
330
+ const mid = lo + hi + 1 >>> 1;
331
+ if (cumulative[mid] <= d) lo = mid;
332
+ else hi = mid - 1;
333
+ }
334
+ return lo;
335
+ }
336
+ function pointAtClosedPathDistanceFast(ring, cumulative, distanceAlong) {
337
+ const perimeter = cumulative[ring.length];
212
338
  if (perimeter <= 1e-9) return [ring[0][0], ring[0][1]];
213
- let remaining = mod(distanceAlong, perimeter);
214
- for (let index = 0; index < ring.length; index += 1) {
215
- const start = ring[index];
216
- const end = ring[(index + 1) % ring.length];
217
- const segmentLength = distance(start, end);
218
- if (segmentLength <= 1e-9) continue;
219
- if (remaining <= segmentLength) {
220
- const t = remaining / segmentLength;
221
- return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
222
- }
223
- remaining -= segmentLength;
224
- }
225
- return [ring[ring.length - 1][0], ring[ring.length - 1][1]];
226
- }
227
- function pointAtOpenPathDistance(path, distanceAlong) {
339
+ const d = mod(distanceAlong, perimeter);
340
+ const idx = binarySearchSegment(cumulative, d);
341
+ const segLen = cumulative[idx + 1] - cumulative[idx];
342
+ if (segLen <= 1e-9) return [ring[idx][0], ring[idx][1]];
343
+ const t = (d - cumulative[idx]) / segLen;
344
+ const start = ring[idx];
345
+ const end = ring[(idx + 1) % ring.length];
346
+ return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
347
+ }
348
+ function pointAtOpenPathDistanceFast(path, cumulative, distanceAlong) {
228
349
  if (distanceAlong <= 0) return [path[0][0], path[0][1]];
229
- let remaining = distanceAlong;
230
- for (let index = 0; index < path.length - 1; index += 1) {
231
- const start = path[index];
232
- const end = path[index + 1];
233
- const segmentLength = distance(start, end);
234
- if (segmentLength <= 1e-9) continue;
235
- if (remaining <= segmentLength) {
236
- const t = remaining / segmentLength;
237
- return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
238
- }
239
- remaining -= segmentLength;
240
- }
241
- return [path[path.length - 1][0], path[path.length - 1][1]];
350
+ if (distanceAlong >= cumulative[path.length - 1]) return [path[path.length - 1][0], path[path.length - 1][1]];
351
+ const idx = binarySearchSegment(cumulative, distanceAlong);
352
+ const segLen = cumulative[idx + 1] - cumulative[idx];
353
+ if (segLen <= 1e-9) return [path[idx][0], path[idx][1]];
354
+ const t = (distanceAlong - cumulative[idx]) / segLen;
355
+ const start = path[idx];
356
+ const end = path[idx + 1];
357
+ return [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t];
242
358
  }
243
359
  function segmentIntersectsRing(a, b, ring, ignoredEdges, epsilon) {
244
360
  for (let index = 0; index < ring.length; index += 1) {
@@ -320,23 +436,28 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
320
436
  current = null;
321
437
  return;
322
438
  }
323
- if (!samePoint(current.cursor, current.start)) {
324
- current.segments.push({
325
- type: "L",
326
- from: cloneRawPoint(current.cursor),
327
- to: cloneRawPoint(current.start)
328
- });
329
- current.cursor = cloneRawPoint(current.start);
330
- }
439
+ if (!samePoint(current.cursor, current.start)) current.segments.push({
440
+ type: "L",
441
+ from: {
442
+ x: current.cursor.x,
443
+ y: current.cursor.y
444
+ },
445
+ to: {
446
+ x: current.start.x,
447
+ y: current.start.y
448
+ }
449
+ });
331
450
  contours.push({
332
451
  id: contourId++,
333
- start: cloneRawPoint(current.start),
452
+ start: current.start,
334
453
  segments: current.segments
335
454
  });
336
455
  current = null;
337
456
  };
338
- commands.forEach((cmd) => {
339
- if (cmd.type === "M") {
457
+ for (let i = 0, len = commands.length; i < len; i++) {
458
+ const cmd = commands[i];
459
+ const type = cmd.type;
460
+ if (type === "M") {
340
461
  finishCurrentContour();
341
462
  current = {
342
463
  start: {
@@ -349,47 +470,49 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
349
470
  },
350
471
  segments: []
351
472
  };
352
- return;
473
+ continue;
353
474
  }
354
- if (!current) return;
355
- if (cmd.type === "L") {
475
+ if (!current) continue;
476
+ if (type === "L") {
356
477
  const to = {
357
478
  x: cmd.x,
358
479
  y: cmd.y
359
480
  };
360
481
  current.segments.push({
361
482
  type: "L",
362
- from: cloneRawPoint(current.cursor),
483
+ from: current.cursor,
363
484
  to
364
485
  });
365
- current.cursor = cloneRawPoint(to);
366
- return;
486
+ current.cursor = to;
487
+ continue;
367
488
  }
368
- if (cmd.type === "Q") {
489
+ if (type === "Q") {
490
+ const from = current.cursor;
369
491
  const to = {
370
492
  x: cmd.x,
371
493
  y: cmd.y
372
494
  };
373
495
  current.segments.push({
374
496
  type: "Q",
375
- from: cloneRawPoint(current.cursor),
497
+ from,
376
498
  c1: {
377
499
  x: cmd.x1,
378
500
  y: cmd.y1
379
501
  },
380
502
  to
381
503
  });
382
- current.cursor = cloneRawPoint(to);
383
- return;
504
+ current.cursor = to;
505
+ continue;
384
506
  }
385
- if (cmd.type === "C") {
507
+ if (type === "C") {
508
+ const from = current.cursor;
386
509
  const to = {
387
510
  x: cmd.x,
388
511
  y: cmd.y
389
512
  };
390
513
  current.segments.push({
391
514
  type: "C",
392
- from: cloneRawPoint(current.cursor),
515
+ from,
393
516
  c1: {
394
517
  x: cmd.x1,
395
518
  y: cmd.y1
@@ -400,11 +523,11 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
400
523
  },
401
524
  to
402
525
  });
403
- current.cursor = cloneRawPoint(to);
404
- return;
526
+ current.cursor = to;
527
+ continue;
405
528
  }
406
- if (cmd.type === "Z") finishCurrentContour();
407
- });
529
+ if (type === "Z") finishCurrentContour();
530
+ }
408
531
  finishCurrentContour();
409
532
  return {
410
533
  glyphIndex: glyph.index,
@@ -428,18 +551,25 @@ function flattenGlyphTopology(topology, options) {
428
551
  }
429
552
  function flattenContour(contour, scale, tolerance) {
430
553
  const ring = [];
431
- pushUniquePoint(ring, toScreenPoint(contour.start, scale));
432
- contour.segments.forEach((segment) => {
433
- if (segment.type === "L") {
434
- pushUniquePoint(ring, toScreenPoint(segment.to, scale));
435
- return;
554
+ let cursor = toScreenPoint(contour.start, scale);
555
+ pushUniquePoint(ring, cursor);
556
+ const segments = contour.segments;
557
+ for (let i = 0, len = segments.length; i < len; i++) {
558
+ const segment = segments[i];
559
+ const type = segment.type;
560
+ if (type === "L") {
561
+ cursor = toScreenPoint(segment.to, scale);
562
+ pushUniquePoint(ring, cursor);
563
+ } else if (type === "Q") {
564
+ const to = toScreenPoint(segment.to, scale);
565
+ flattenQuadratic(cursor, toScreenPoint(segment.c1, scale), to, tolerance, ring);
566
+ cursor = to;
567
+ } else if (type === "C") {
568
+ const to = toScreenPoint(segment.to, scale);
569
+ flattenCubic(cursor, toScreenPoint(segment.c1, scale), toScreenPoint(segment.c2, scale), to, tolerance, ring);
570
+ cursor = to;
436
571
  }
437
- if (segment.type === "Q") {
438
- flattenQuadratic(toScreenPoint(segment.from, scale), toScreenPoint(segment.c1, scale), toScreenPoint(segment.to, scale), tolerance, ring);
439
- return;
440
- }
441
- if (segment.type === "C") flattenCubic(toScreenPoint(segment.from, scale), toScreenPoint(segment.c1, scale), toScreenPoint(segment.c2, scale), toScreenPoint(segment.to, scale), tolerance, ring);
442
- });
572
+ }
443
573
  if (ring.length > 1 && pointsEqual2D(ring[0], ring[ring.length - 1])) ring.pop();
444
574
  if (ring.length < 3) return null;
445
575
  const bbox = ringBounds(ring);
@@ -452,37 +582,45 @@ function flattenContour(contour, scale, tolerance) {
452
582
  };
453
583
  }
454
584
  function classifyContours(contours) {
455
- const result = contours.map((contour) => ({
456
- ...contour,
585
+ const len = contours.length;
586
+ const result = new Array(len);
587
+ for (let i = 0; i < len; i++) result[i] = {
588
+ ...contours[i],
457
589
  parentId: null,
458
590
  depth: 0,
459
591
  role: "outer"
460
- }));
461
- result.forEach((child) => {
592
+ };
593
+ const sortedByArea = result.slice().sort((a, b) => Math.abs(b.area) - Math.abs(a.area));
594
+ for (let ci = 0; ci < len; ci++) {
595
+ const child = result[ci];
596
+ const childAbsArea = Math.abs(child.area);
462
597
  let parent = null;
463
598
  let parentArea = Number.POSITIVE_INFINITY;
464
- result.forEach((candidate) => {
465
- if (candidate.id === child.id) return;
466
- if (Math.abs(candidate.area) <= Math.abs(child.area)) return;
467
- if (!rectContainsRect(candidate.bbox, child.bbox)) return;
468
- if (!pointInRing(child.ring[0], candidate.ring)) return;
469
- const candidateArea = Math.abs(candidate.area);
470
- if (candidateArea < parentArea) {
471
- parent = candidate;
472
- parentArea = candidateArea;
473
- }
474
- });
599
+ for (let si = 0; si < len; si++) {
600
+ const candidate = sortedByArea[si];
601
+ const candidateAbsArea = Math.abs(candidate.area);
602
+ if (candidateAbsArea <= childAbsArea) break;
603
+ if (candidate.id === child.id) continue;
604
+ if (candidateAbsArea >= parentArea) continue;
605
+ if (!rectContainsRect(candidate.bbox, child.bbox)) continue;
606
+ if (!pointInRing(child.ring[0], candidate.ring)) continue;
607
+ parent = candidate;
608
+ parentArea = candidateAbsArea;
609
+ }
475
610
  if (parent) child.parentId = parent.id;
476
- });
611
+ }
477
612
  const byId = new Map(result.map((contour) => [contour.id, contour]));
478
- const resolveDepth = (contour) => {
479
- if (contour.parentId == null) return 0;
480
- return resolveDepth(byId.get(contour.parentId)) + 1;
481
- };
482
- result.forEach((contour) => {
483
- contour.depth = resolveDepth(contour);
484
- contour.role = contour.depth % 2 === 0 ? "outer" : "hole";
485
- });
613
+ for (let i = 0; i < len; i++) {
614
+ const contour = result[i];
615
+ let depth = 0;
616
+ let current = contour;
617
+ while (current.parentId != null) {
618
+ depth++;
619
+ current = byId.get(current.parentId);
620
+ }
621
+ contour.depth = depth;
622
+ contour.role = depth % 2 === 0 ? "outer" : "hole";
623
+ }
486
624
  return result;
487
625
  }
488
626
  function buildParts(contours) {
@@ -497,16 +635,26 @@ function buildParts(contours) {
497
635
  });
498
636
  }
499
637
  function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
500
- return {
501
- parts: geometry.parts.map((part, partIndex) => ({
638
+ const srcParts = geometry.parts;
639
+ const srcContours = geometry.contours;
640
+ const partsLen = srcParts.length;
641
+ const contoursLen = srcContours.length;
642
+ const parts = new Array(partsLen);
643
+ const contours = new Array(contoursLen);
644
+ for (let i = 0; i < partsLen; i++) {
645
+ const part = srcParts[i];
646
+ parts[i] = {
502
647
  outer: translateRing(part.outer, tx, ty),
503
648
  holes: part.holes.map((hole) => translateRing(hole, tx, ty)),
504
649
  bbox: translateRect(part.bbox, tx, ty),
505
650
  area: part.area,
506
651
  glyphPosition,
507
- partIndex
508
- })),
509
- contours: geometry.contours.map((contour) => ({
652
+ partIndex: i
653
+ };
654
+ }
655
+ for (let i = 0; i < contoursLen; i++) {
656
+ const contour = srcContours[i];
657
+ contours[i] = {
510
658
  id: `${glyphPosition}:${contour.id}`,
511
659
  sourceId: contour.id,
512
660
  glyphPosition,
@@ -516,7 +664,11 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
516
664
  area: contour.area,
517
665
  bbox: translateRect(contour.bbox, tx, ty),
518
666
  ring: translateRing(contour.ring, tx, ty)
519
- })),
667
+ };
668
+ }
669
+ return {
670
+ parts,
671
+ contours,
520
672
  bbox: translateRect(geometry.bbox, tx, ty)
521
673
  };
522
674
  }
@@ -556,13 +708,14 @@ function toArrayBuffer(value) {
556
708
  //#endregion
557
709
  //#region src/paFont/shape.js
558
710
  var PAShape = class {
559
- constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon }) {
711
+ constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon, rawData }) {
560
712
  this.text = text;
561
713
  this.parts = parts;
562
714
  this.bbox = bbox;
563
715
  this.metrics = metrics;
564
716
  this.edgeEpsilon = edgeEpsilon;
565
717
  this.raw = {
718
+ ...rawData ?? {},
566
719
  contours,
567
720
  glyphs
568
721
  };
@@ -595,12 +748,23 @@ var PAShape = class {
595
748
  toRegions(options = {}) {
596
749
  return createRegionCollection(this.toShape(options));
597
750
  }
751
+ toRegionViews(options = {}) {
752
+ return this.toShape(options)._getRegionViews();
753
+ }
598
754
  openHoles(width) {
599
755
  const slitWidth = normalizePositive(width, 0);
600
756
  if (slitWidth <= 0 || !this.parts.some((part) => part.holes.length > 0)) return this;
601
757
  const cacheKey = toCacheKey(slitWidth);
602
758
  const cached = this._cache.openHoles.get(cacheKey);
603
759
  if (cached) return cached;
760
+ const glyphVariant = createGlyphDerivedShape(this, {
761
+ openWidth: slitWidth,
762
+ step: 0
763
+ });
764
+ if (glyphVariant) {
765
+ this._cache.openHoles.set(cacheKey, glyphVariant);
766
+ return glyphVariant;
767
+ }
604
768
  const geometryEpsilon = resolveGeometryEpsilon(slitWidth);
605
769
  const shape = createDerivedShape(this, this.parts.map((part) => openPartWithSlit(part, slitWidth, geometryEpsilon)));
606
770
  this._cache.openHoles.set(cacheKey, shape);
@@ -612,6 +776,14 @@ var PAShape = class {
612
776
  const cacheKey = toCacheKey(spacing);
613
777
  const cached = this._cache.resample.get(cacheKey);
614
778
  if (cached) return cached;
779
+ const glyphVariant = createGlyphDerivedShape(this, {
780
+ openWidth: 0,
781
+ step: spacing
782
+ });
783
+ if (glyphVariant) {
784
+ this._cache.resample.set(cacheKey, glyphVariant);
785
+ return glyphVariant;
786
+ }
615
787
  const shape = createDerivedShape(this, this.parts.map((part) => resamplePart(part, spacing)));
616
788
  this._cache.resample.set(cacheKey, shape);
617
789
  return shape;
@@ -672,21 +844,31 @@ var PAShape = class {
672
844
  function createTextShape(layout, opts, fontInstance) {
673
845
  const parts = [];
674
846
  const contours = [];
675
- const glyphs = [];
676
- layout.glyphs.forEach((item, glyphPosition) => {
677
- const translated = translateGlyphGeometry(fontInstance._getFlattenedGlyph(item.glyph, opts), item.x, item.y, glyphPosition);
678
- parts.push(...translated.parts);
679
- contours.push(...translated.contours);
680
- glyphs.push({
847
+ const layoutGlyphs = layout.glyphs;
848
+ const glyphCount = layoutGlyphs.length;
849
+ const glyphs = new Array(glyphCount);
850
+ const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
851
+ const variantOptions = {
852
+ openWidth: normalizePositive(opts.openWidth, 0),
853
+ step: normalizePositive(opts.step, 0)
854
+ };
855
+ for (let glyphPosition = 0; glyphPosition < glyphCount; glyphPosition++) {
856
+ const item = layoutGlyphs[glyphPosition];
857
+ const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
858
+ const tParts = translated.parts;
859
+ const tContours = translated.contours;
860
+ for (let j = 0; j < tParts.length; j++) parts.push(tParts[j]);
861
+ for (let j = 0; j < tContours.length; j++) contours.push(tContours[j]);
862
+ glyphs[glyphPosition] = {
681
863
  position: glyphPosition,
682
864
  glyphIndex: item.glyph.index,
683
865
  x: item.x,
684
866
  y: item.y,
685
867
  size: item.size,
686
868
  bbox: translated.bbox,
687
- partCount: translated.parts.length
688
- });
689
- });
869
+ partCount: tParts.length
870
+ };
871
+ }
690
872
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
691
873
  return new PAShape({
692
874
  text: layout.text,
@@ -698,11 +880,21 @@ function createTextShape(layout, opts, fontInstance) {
698
880
  ...layout.metrics,
699
881
  bbox
700
882
  },
701
- edgeEpsilon: opts.edgeEpsilon
883
+ edgeEpsilon: opts.edgeEpsilon,
884
+ rawData: {
885
+ fontInstance,
886
+ sourceLayout: {
887
+ glyphs: sourceLayoutGlyphs,
888
+ metrics: { ...layout.metrics }
889
+ },
890
+ sourceOptions: extractShapeSourceOptions(opts),
891
+ variantOptions
892
+ }
702
893
  });
703
894
  }
704
895
  function createGlyphShape(shape, glyphPosition) {
705
896
  const glyphMeta = shape.raw.glyphs?.[glyphPosition] ?? null;
897
+ const sourceGlyph = shape.raw.sourceLayout?.glyphs?.[glyphPosition] ?? null;
706
898
  const parts = copyParts(shape.parts.filter((part) => part.glyphPosition === glyphPosition));
707
899
  const contours = copyContours((shape.raw.contours ?? []).filter((contour) => contour.glyphPosition === glyphPosition));
708
900
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? (glyphMeta?.bbox ? { ...glyphMeta.bbox } : emptyRect());
@@ -723,14 +915,28 @@ function createGlyphShape(shape, glyphPosition) {
723
915
  width: bbox.w,
724
916
  bbox
725
917
  },
726
- edgeEpsilon: shape.edgeEpsilon
918
+ edgeEpsilon: shape.edgeEpsilon,
919
+ rawData: sourceGlyph && shape.raw.fontInstance ? {
920
+ fontInstance: shape.raw.fontInstance,
921
+ sourceLayout: {
922
+ glyphs: [sourceGlyph],
923
+ metrics: {
924
+ x: glyphMeta?.x ?? shape.metrics?.x ?? 0,
925
+ y: glyphMeta?.y ?? shape.metrics?.y ?? 0,
926
+ size: glyphMeta?.size ?? shape.metrics?.size ?? 0,
927
+ width: glyphMeta?.bbox?.w ?? bbox.w
928
+ }
929
+ },
930
+ sourceOptions: shape.raw.sourceOptions ?? null,
931
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
932
+ } : void 0
727
933
  });
728
934
  }
729
935
  function createDerivedShape(shape, parts) {
730
- const copiedParts = copyParts(parts);
731
- const bbox = combineRects(copiedParts.map((part) => part.bbox)) ?? emptyRect();
936
+ const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
937
+ const partsByGlyphPosition = groupPartsByGlyphPosition(parts);
732
938
  const glyphs = (shape.raw.glyphs ?? []).map((glyph, glyphPosition) => {
733
- const glyphParts = copiedParts.filter((part) => part.glyphPosition === glyphPosition);
939
+ const glyphParts = partsByGlyphPosition.get(glyphPosition) ?? [];
734
940
  return {
735
941
  ...glyph,
736
942
  bbox: combineRects(glyphParts.map((part) => part.bbox)) ?? (glyph.bbox ? { ...glyph.bbox } : emptyRect()),
@@ -739,7 +945,7 @@ function createDerivedShape(shape, parts) {
739
945
  });
740
946
  return new PAShape({
741
947
  text: shape.text,
742
- parts: copiedParts,
948
+ parts,
743
949
  contours: [],
744
950
  glyphs,
745
951
  bbox,
@@ -747,13 +953,17 @@ function createDerivedShape(shape, parts) {
747
953
  ...shape.metrics,
748
954
  bbox
749
955
  },
750
- edgeEpsilon: shape.edgeEpsilon
956
+ edgeEpsilon: shape.edgeEpsilon,
957
+ rawData: {
958
+ ...shape.raw,
959
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
960
+ }
751
961
  });
752
962
  }
753
963
  function createRegionCollection(shape) {
754
964
  return shape.parts.map((part) => ({
755
- outer: copyRing(part.outer),
756
- holes: part.holes.map((hole) => copyRing(hole)),
965
+ outer: copyRing$1(part.outer),
966
+ holes: part.holes.map((hole) => copyRing$1(hole)),
757
967
  bbox: { ...part.bbox }
758
968
  }));
759
969
  }
@@ -763,8 +973,8 @@ function copyParts(parts) {
763
973
  function copyPart(part) {
764
974
  return {
765
975
  ...part,
766
- outer: copyRing(part.outer),
767
- holes: part.holes.map((hole) => copyRing(hole)),
976
+ outer: copyRing$1(part.outer),
977
+ holes: part.holes.map((hole) => copyRing$1(hole)),
768
978
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
769
979
  bbox: { ...part.bbox }
770
980
  };
@@ -773,9 +983,19 @@ function copyContours(contours) {
773
983
  return contours.map((contour) => ({
774
984
  ...contour,
775
985
  bbox: { ...contour.bbox },
776
- ring: copyRing(contour.ring)
986
+ ring: copyRing$1(contour.ring)
777
987
  }));
778
988
  }
989
+ function groupPartsByGlyphPosition(parts) {
990
+ const grouped = /* @__PURE__ */ new Map();
991
+ parts.forEach((part) => {
992
+ if (typeof part.glyphPosition !== "number") return;
993
+ const bucket = grouped.get(part.glyphPosition) ?? [];
994
+ bucket.push(part);
995
+ grouped.set(part.glyphPosition, bucket);
996
+ });
997
+ return grouped;
998
+ }
779
999
  function extractGlyphText(text, glyphPosition, glyphCount) {
780
1000
  if (glyphCount <= 1) return text;
781
1001
  return Array.from(text ?? "")[glyphPosition] ?? "";
@@ -804,6 +1024,19 @@ function normalizeShapeOptions(options = {}) {
804
1024
  openWidth: normalizePositive(options.openWidth, 0)
805
1025
  };
806
1026
  }
1027
+ function extractShapeSourceOptions(opts) {
1028
+ return {
1029
+ size: opts.size,
1030
+ flatten: opts.flatten,
1031
+ edgeEpsilon: opts.edgeEpsilon,
1032
+ kerning: opts.kerning,
1033
+ letterSpacing: opts.letterSpacing,
1034
+ tracking: opts.tracking,
1035
+ script: opts.script,
1036
+ language: opts.language,
1037
+ features: opts.features
1038
+ };
1039
+ }
807
1040
  function resolveShapeVariant(shape, options = {}) {
808
1041
  const normalized = normalizeShapeOptions(options);
809
1042
  if (normalized.step <= 0 && normalized.openWidth <= 0) return shape;
@@ -816,84 +1049,158 @@ function resolveShapeVariant(shape, options = {}) {
816
1049
  shape._cache.shapes.set(cacheKey, next);
817
1050
  return next;
818
1051
  }
1052
+ function deriveGlyphGeometryVariant(geometry, options = {}) {
1053
+ const normalized = normalizeShapeOptions(options);
1054
+ if (normalized.step <= 0 && normalized.openWidth <= 0) return geometry;
1055
+ let parts = geometry.parts;
1056
+ if (normalized.openWidth > 0) {
1057
+ const geometryEpsilon = resolveGeometryEpsilon(normalized.openWidth);
1058
+ parts = parts.map((part) => openPartWithSlit(part, normalized.openWidth, geometryEpsilon));
1059
+ }
1060
+ if (normalized.step > 0) parts = parts.map((part) => resamplePart(part, normalized.step));
1061
+ return {
1062
+ ...geometry,
1063
+ contours: [],
1064
+ parts,
1065
+ bbox: combineRects(parts.map((part) => part.bbox)) ?? emptyRect()
1066
+ };
1067
+ }
819
1068
  function toCacheKey(value) {
820
1069
  return normalizePositive(value, 0).toFixed(6);
821
1070
  }
1071
+ function zeroShapeVariant() {
1072
+ return {
1073
+ openWidth: 0,
1074
+ step: 0
1075
+ };
1076
+ }
1077
+ function createGlyphDerivedShape(shape, variant) {
1078
+ if (!canUseGlyphDerivedShape(shape)) return null;
1079
+ return createTextShape({
1080
+ text: shape.text,
1081
+ glyphs: shape.raw.sourceLayout.glyphs,
1082
+ metrics: shape.raw.sourceLayout.metrics
1083
+ }, {
1084
+ ...shape.raw.sourceOptions,
1085
+ ...mergeShapeVariantOptions(shape.raw.variantOptions, variant)
1086
+ }, shape.raw.fontInstance);
1087
+ }
1088
+ function canUseGlyphDerivedShape(shape) {
1089
+ return shape.raw?.fontInstance != null && shape.raw?.sourceOptions != null && shape.raw?.sourceLayout != null && Array.isArray(shape.raw.sourceLayout.glyphs);
1090
+ }
1091
+ function mergeShapeVariantOptions(previous, next) {
1092
+ return {
1093
+ openWidth: normalizePositive(next.openWidth, previous?.openWidth ?? 0),
1094
+ step: normalizePositive(next.step, previous?.step ?? 0)
1095
+ };
1096
+ }
822
1097
  function resolveGeometryEpsilon(width) {
823
1098
  return Math.max(1e-4, normalizePositive(width, 1) * .001);
824
1099
  }
825
1100
  function openPartWithSlit(part, width, epsilon) {
826
1101
  if (!part.holes || part.holes.length === 0) return copyPart(part);
827
- let outer = copyRing(part.outer);
828
- const holes = part.holes.map((hole) => copyRing(hole));
1102
+ let outer = copyRing$1(part.outer);
1103
+ const holes = part.holes.map((hole) => copyRing$1(hole));
829
1104
  const anchors = [];
1105
+ const blockers = [];
830
1106
  while (holes.length > 0) {
831
1107
  let selectedHoleIndex = -1;
832
1108
  let selectedBridge = null;
833
- holes.forEach((hole, holeIndex) => {
834
- const blockers = holes.filter((_, index) => index !== holeIndex);
835
- const candidate = findBestHoleBridge(outer, hole, blockers, epsilon);
836
- if (candidate && (!selectedBridge || candidate.distance < selectedBridge.distance)) {
1109
+ let selectedDist = Number.POSITIVE_INFINITY;
1110
+ for (let hi = 0; hi < holes.length; hi++) {
1111
+ blockers.length = 0;
1112
+ for (let bi = 0; bi < holes.length; bi++) if (bi !== hi) blockers.push(holes[bi]);
1113
+ const candidate = findBestHoleBridge(outer, holes[hi], blockers, epsilon);
1114
+ if (candidate && candidate.distance < selectedDist) {
837
1115
  selectedBridge = candidate;
838
- selectedHoleIndex = holeIndex;
1116
+ selectedHoleIndex = hi;
1117
+ selectedDist = candidate.distance;
839
1118
  }
840
- });
1119
+ }
841
1120
  if (!selectedBridge || selectedHoleIndex === -1) break;
842
1121
  const [hole] = holes.splice(selectedHoleIndex, 1);
843
1122
  const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
844
1123
  outer = merged.ring;
845
- anchors.push(...merged.anchors);
1124
+ const mergedAnchors = merged.anchors;
1125
+ for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
846
1126
  }
1127
+ let holeArea = 0;
1128
+ for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
847
1129
  return {
848
1130
  ...part,
849
1131
  outer,
850
1132
  holes,
851
1133
  anchors,
852
1134
  bbox: ringBounds(outer),
853
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1135
+ area: Math.abs(signedArea(outer)) - holeArea
854
1136
  };
855
1137
  }
856
1138
  function resamplePart(part, step) {
857
1139
  const outer = resampleRing(part.outer, step, part.anchors ?? []);
858
- const holes = part.holes.map((hole) => resampleRing(hole, step));
1140
+ const srcHoles = part.holes;
1141
+ const holes = new Array(srcHoles.length);
1142
+ let holeArea = 0;
1143
+ for (let i = 0; i < srcHoles.length; i++) {
1144
+ holes[i] = resampleRing(srcHoles[i], step);
1145
+ holeArea += Math.abs(signedArea(holes[i]));
1146
+ }
859
1147
  return {
860
1148
  ...part,
861
1149
  outer,
862
1150
  holes,
863
1151
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
864
1152
  bbox: ringBounds(outer),
865
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1153
+ area: Math.abs(signedArea(outer)) - holeArea
866
1154
  };
867
1155
  }
868
1156
  function findBestHoleBridge(outer, hole, blockers, epsilon) {
869
1157
  let best = null;
870
- outer.forEach((outerPoint, outerIndex) => {
871
- hole.forEach((holePoint, holeIndex) => {
872
- if (!isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon)) return;
873
- const candidate = {
874
- outerIndex,
875
- holeIndex,
876
- distance: distance(outerPoint, holePoint)
1158
+ let bestDist = Number.POSITIVE_INFINITY;
1159
+ const outerLen = outer.length;
1160
+ const holeLen = hole.length;
1161
+ for (let oi = 0; oi < outerLen; oi++) {
1162
+ const op = outer[oi];
1163
+ for (let hi = 0; hi < holeLen; hi++) {
1164
+ const hp = hole[hi];
1165
+ const dx = op[0] - hp[0], dy = op[1] - hp[1];
1166
+ const dist = Math.sqrt(dx * dx + dy * dy);
1167
+ if (dist >= bestDist) continue;
1168
+ if (!isVisibleBridge(outer, hole, blockers, op, hp, oi, hi, epsilon)) continue;
1169
+ best = {
1170
+ outerIndex: oi,
1171
+ holeIndex: hi,
1172
+ distance: dist
877
1173
  };
878
- if (!best || candidate.distance < best.distance) best = candidate;
879
- });
880
- });
1174
+ bestDist = dist;
1175
+ }
1176
+ }
881
1177
  if (best) return best;
882
1178
  return findNearestBridge(outer, hole);
883
1179
  }
884
1180
  function findNearestBridge(outer, hole) {
885
- let best = null;
886
- outer.forEach((outerPoint, outerIndex) => {
887
- hole.forEach((holePoint, holeIndex) => {
888
- const candidate = {
889
- outerIndex,
890
- holeIndex,
891
- distance: distance(outerPoint, holePoint)
892
- };
893
- if (!best || candidate.distance < best.distance) best = candidate;
894
- });
895
- });
896
- return best;
1181
+ let bestOi = 0, bestHi = 0;
1182
+ let bestDistSq = Number.POSITIVE_INFINITY;
1183
+ const outerLen = outer.length;
1184
+ const holeLen = hole.length;
1185
+ for (let oi = 0; oi < outerLen; oi++) {
1186
+ const op = outer[oi];
1187
+ const ox = op[0], oy = op[1];
1188
+ for (let hi = 0; hi < holeLen; hi++) {
1189
+ const hp = hole[hi];
1190
+ const dx = ox - hp[0], dy = oy - hp[1];
1191
+ const distSq = dx * dx + dy * dy;
1192
+ if (distSq < bestDistSq) {
1193
+ bestDistSq = distSq;
1194
+ bestOi = oi;
1195
+ bestHi = hi;
1196
+ }
1197
+ }
1198
+ }
1199
+ return {
1200
+ outerIndex: bestOi,
1201
+ holeIndex: bestHi,
1202
+ distance: Math.sqrt(bestDistSq)
1203
+ };
897
1204
  }
898
1205
  function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
899
1206
  if (distance(outerPoint, holePoint) <= epsilon) return false;
@@ -1020,7 +1327,7 @@ function normalizeCutLocation(name, ring, cut, epsilon) {
1020
1327
  };
1021
1328
  }
1022
1329
  function ringPath(ring, startIndex, endIndex) {
1023
- if (startIndex == null || endIndex == null) return copyRing(ring);
1330
+ if (startIndex == null || endIndex == null) return copyRing$1(ring);
1024
1331
  const path = [ring[startIndex]];
1025
1332
  let cursor = startIndex;
1026
1333
  while (cursor !== endIndex) {
@@ -1035,7 +1342,7 @@ function appendPath(target, path, epsilon) {
1035
1342
  });
1036
1343
  }
1037
1344
  function resampleRing(ring, step, anchors = []) {
1038
- if (ring.length < 3) return copyRing(ring);
1345
+ if (ring.length < 3) return copyRing$1(ring);
1039
1346
  const normalizedAnchors = normalizeAnchorPoints(ring, anchors);
1040
1347
  if (normalizedAnchors.length >= 2) return resampleRingWithAnchors(ring, step, normalizedAnchors);
1041
1348
  return resampleClosedPath(ring, step);
@@ -1056,30 +1363,32 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
1056
1363
  appendPath(result, resampleOpenPath(ringPath(ring, startIndex, endIndex), step), 1e-6);
1057
1364
  }
1058
1365
  if (result.length > 1 && pointsAlmostEqual2D(result[0], result[result.length - 1], 1e-6)) result.pop();
1059
- if (result.length < 3 || result.length >= ring.length) return copyRing(ring);
1366
+ if (result.length < 3 || result.length >= ring.length) return copyRing$1(ring);
1060
1367
  return result;
1061
1368
  }
1062
1369
  function resampleClosedPath(ring, step) {
1063
- const perimeter = ringPerimeter(ring);
1064
- if (perimeter <= 1e-9) return copyRing(ring);
1370
+ const cumulative = buildCumulativeDistances(ring);
1371
+ const perimeter = cumulative[ring.length];
1372
+ if (perimeter <= 1e-9) return copyRing$1(ring);
1065
1373
  const pointCount = Math.max(3, Math.round(perimeter / step));
1066
- if (pointCount >= ring.length) return copyRing(ring);
1374
+ if (pointCount >= ring.length) return copyRing$1(ring);
1067
1375
  const sampled = [];
1068
- for (let index = 0; index < pointCount; index += 1) {
1069
- const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
1376
+ for (let index = 0; index < pointCount; index++) {
1377
+ const point = pointAtClosedPathDistanceFast(ring, cumulative, perimeter * index / pointCount);
1070
1378
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1071
1379
  }
1072
- if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
1380
+ if (sampled.length < 3 || sampled.length >= ring.length) return copyRing$1(ring);
1073
1381
  return sampled;
1074
1382
  }
1075
1383
  function resampleOpenPath(path, step) {
1076
1384
  if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
1077
- const length = polylineLength(path);
1385
+ const cumulative = buildOpenCumulativeDistances(path);
1386
+ const length = cumulative[path.length - 1];
1078
1387
  if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
1079
1388
  const divisionCount = Math.max(1, Math.round(length / step));
1080
1389
  const sampled = [];
1081
- for (let index = 0; index <= divisionCount; index += 1) {
1082
- const point = pointAtOpenPathDistance(path, length * index / divisionCount);
1390
+ for (let index = 0; index <= divisionCount; index++) {
1391
+ const point = pointAtOpenPathDistanceFast(path, cumulative, length * index / divisionCount);
1083
1392
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1084
1393
  }
1085
1394
  return sampled;
@@ -3180,9 +3489,11 @@ var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
3180
3489
  var HUGE_LAYOUT_WIDTH = 1e9;
3181
3490
  var JUSTIFY_EPSILON = 1e-6;
3182
3491
  var QUOTE_RE = /"/g;
3492
+ var SHARED_MEASURE_CACHE_LIMIT = 2048;
3183
3493
  var sharedMeasureContext = null;
3184
3494
  var sharedWordSegmenter = null;
3185
3495
  var sharedGraphemeSegmenter = null;
3496
+ var sharedMeasureCaches = /* @__PURE__ */ new WeakMap();
3186
3497
  function layoutParagraph(fontInstance, text, options = {}, state = {}) {
3187
3498
  const normalized = normalizeParagraphOptions(fontInstance, options);
3188
3499
  const textValue = String(text ?? "");
@@ -3449,9 +3760,25 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3449
3760
  width: 0,
3450
3761
  hardBreak: false
3451
3762
  };
3452
- let nextText = trimTrailingWhitespace(line.text);
3453
- while (nextText.length > 0 && measureWidth(`${nextText}${suffixText}`) > maxWidth + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
3454
- const text = `${nextText}${suffixText}`;
3763
+ const trimmed = trimTrailingWhitespace(line.text);
3764
+ if (measureWidth(`${trimmed}${suffixText}`) <= maxWidth + JUSTIFY_EPSILON) {
3765
+ const text = `${trimmed}${suffixText}`;
3766
+ return {
3767
+ ...line,
3768
+ text,
3769
+ width: measureWidth(text),
3770
+ hardBreak: false
3771
+ };
3772
+ }
3773
+ const segmenter = getGraphemeSegmenter();
3774
+ const graphemes = Array.from(segmenter.segment(trimmed), (seg) => seg.segment);
3775
+ let lo = 0, hi = graphemes.length;
3776
+ while (lo < hi) {
3777
+ const mid = lo + hi + 1 >>> 1;
3778
+ if (measureWidth(graphemes.slice(0, mid).join("") + suffixText) <= maxWidth + JUSTIFY_EPSILON) lo = mid;
3779
+ else hi = mid - 1;
3780
+ }
3781
+ const text = (lo > 0 ? graphemes.slice(0, lo).join("") : "") + suffixText;
3455
3782
  return {
3456
3783
  ...line,
3457
3784
  text,
@@ -3862,15 +4189,24 @@ function createTextMeasurer(fontInstance, options) {
3862
4189
  const cache = /* @__PURE__ */ new Map();
3863
4190
  const openTypeMeasurer = createOpenTypeMeasurer(fontInstance, options);
3864
4191
  const context = getMeasureContext();
4192
+ const sharedCache = getSharedMeasureCache(fontInstance, "canvas");
4193
+ const sharedKeyPrefix = `${options.font}\u0000`;
3865
4194
  return (value) => {
3866
4195
  if (value.length === 0) return 0;
3867
4196
  if (cache.has(value)) return cache.get(value);
4197
+ const sharedKey = `${sharedKeyPrefix}${value}`;
4198
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
4199
+ if (sharedWidth != null) {
4200
+ cache.set(value, sharedWidth);
4201
+ return sharedWidth;
4202
+ }
3868
4203
  let width;
3869
4204
  if (context) {
3870
4205
  context.font = options.font;
3871
4206
  width = context.measureText(value).width;
3872
4207
  } else width = openTypeMeasurer(value);
3873
4208
  cache.set(value, width);
4209
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3874
4210
  return width;
3875
4211
  };
3876
4212
  }
@@ -3883,6 +4219,7 @@ function createLazyTextMeasurer(fontInstance, options) {
3883
4219
  }
3884
4220
  function createOpenTypeMeasurer(fontInstance, options) {
3885
4221
  const cache = /* @__PURE__ */ new Map();
4222
+ const sharedCache = getSharedMeasureCache(fontInstance, "openType");
3886
4223
  const widthOptions = {
3887
4224
  x: 0,
3888
4225
  y: 0,
@@ -3896,11 +4233,19 @@ function createOpenTypeMeasurer(fontInstance, options) {
3896
4233
  language: options.language,
3897
4234
  features: options.features
3898
4235
  };
4236
+ const sharedKeyPrefix = createOpenTypeMeasureKeyPrefix(widthOptions);
3899
4237
  return (value) => {
3900
4238
  if (value.length === 0) return 0;
3901
4239
  if (cache.has(value)) return cache.get(value);
4240
+ const sharedKey = `${sharedKeyPrefix}${value}`;
4241
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
4242
+ if (sharedWidth != null) {
4243
+ cache.set(value, sharedWidth);
4244
+ return sharedWidth;
4245
+ }
3902
4246
  const width = measureAdvanceWidth(fontInstance.font, value, widthOptions);
3903
4247
  cache.set(value, width);
4248
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3904
4249
  return width;
3905
4250
  };
3906
4251
  }
@@ -3916,6 +4261,49 @@ function getMeasureContext() {
3916
4261
  }
3917
4262
  return null;
3918
4263
  }
4264
+ function getSharedMeasureCache(fontInstance, bucket) {
4265
+ if (fontInstance == null || typeof fontInstance !== "object" && typeof fontInstance !== "function") return null;
4266
+ let caches = sharedMeasureCaches.get(fontInstance);
4267
+ if (!caches) {
4268
+ caches = {
4269
+ canvas: /* @__PURE__ */ new Map(),
4270
+ openType: /* @__PURE__ */ new Map()
4271
+ };
4272
+ sharedMeasureCaches.set(fontInstance, caches);
4273
+ }
4274
+ return caches[bucket];
4275
+ }
4276
+ function readSharedMeasureCache(cache, key) {
4277
+ if (!cache || !cache.has(key)) return null;
4278
+ const value = cache.get(key);
4279
+ cache.delete(key);
4280
+ cache.set(key, value);
4281
+ return value;
4282
+ }
4283
+ function writeSharedMeasureCache(cache, key, value) {
4284
+ if (!cache) return;
4285
+ if (cache.has(key)) cache.delete(key);
4286
+ cache.set(key, value);
4287
+ if (cache.size > SHARED_MEASURE_CACHE_LIMIT) {
4288
+ const oldestKey = cache.keys().next().value;
4289
+ cache.delete(oldestKey);
4290
+ }
4291
+ }
4292
+ function createOpenTypeMeasureKeyPrefix(options) {
4293
+ return [
4294
+ options.size,
4295
+ options.kerning ? 1 : 0,
4296
+ options.letterSpacing ?? "",
4297
+ options.tracking ?? "",
4298
+ options.script ?? "",
4299
+ options.language ?? "",
4300
+ serializeMeasureFeatures(options.features)
4301
+ ].join("|") + "\0";
4302
+ }
4303
+ function serializeMeasureFeatures(features) {
4304
+ if (!features || typeof features !== "object") return "";
4305
+ return Object.keys(features).sort().map((key) => `${key}:${features[key]}`).join(",");
4306
+ }
3919
4307
  function getWordSegmenter() {
3920
4308
  if (sharedWordSegmenter == null) sharedWordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
3921
4309
  return sharedWordSegmenter;
@@ -3927,12 +4315,6 @@ function getGraphemeSegmenter() {
3927
4315
  function trimTrailingWhitespace(value) {
3928
4316
  return value.replace(/\s+$/u, "");
3929
4317
  }
3930
- function trimLastGrapheme(value) {
3931
- const segmenter = getGraphemeSegmenter();
3932
- const graphemes = Array.from(segmenter.segment(value), (segment) => segment.segment);
3933
- graphemes.pop();
3934
- return graphemes.join("");
3935
- }
3936
4318
  function splitPreservingWhitespace(value) {
3937
4319
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
3938
4320
  }
@@ -4011,6 +4393,10 @@ var paParagraph = class paParagraph {
4011
4393
  const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegions()");
4012
4394
  return this._getBaseShape(layout).toRegions(shapeOptions);
4013
4395
  }
4396
+ toRegionViews(options = {}) {
4397
+ const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegionViews()");
4398
+ return this._getBaseShape(layout).toRegionViews(shapeOptions);
4399
+ }
4014
4400
  toPoints(options = {}) {
4015
4401
  const { layout = "current", ...pointOptions } = normalizeParagraphPointOptions(options);
4016
4402
  return this._getBaseShape(layout).toPoints(pointOptions);
@@ -4146,6 +4532,7 @@ var paFont = class paFont {
4146
4532
  this.canvasFamily = this.family;
4147
4533
  this._glyphTopologyCache = /* @__PURE__ */ new Map();
4148
4534
  this._glyphFlatCache = /* @__PURE__ */ new Map();
4535
+ this._glyphVariantCache = /* @__PURE__ */ new Map();
4149
4536
  }
4150
4537
  static async load(source, options = {}) {
4151
4538
  const opts = normalizeLoadOptions(options);
@@ -4223,10 +4610,27 @@ var paFont = class paFont {
4223
4610
  }
4224
4611
  return this._glyphFlatCache.get(key);
4225
4612
  }
4613
+ _getGlyphGeometryVariant(glyph, opts) {
4614
+ const openWidth = normalizeShapeVariantValue(opts.openWidth);
4615
+ const step = normalizeShapeVariantValue(opts.step);
4616
+ if (openWidth <= 0 && step <= 0) return this._getFlattenedGlyph(glyph, opts);
4617
+ const key = `${glyph.index}:${opts.size}:${opts.flatten}:${toShapeVariantKey(openWidth)}:${toShapeVariantKey(step)}`;
4618
+ if (!this._glyphVariantCache.has(key)) this._glyphVariantCache.set(key, deriveGlyphGeometryVariant(this._getFlattenedGlyph(glyph, opts), {
4619
+ openWidth,
4620
+ step
4621
+ }));
4622
+ return this._glyphVariantCache.get(key);
4623
+ }
4226
4624
  _layoutText(value, opts) {
4227
4625
  return layoutGlyphs(this.font, value, opts);
4228
4626
  }
4229
4627
  };
4628
+ function normalizeShapeVariantValue(value) {
4629
+ return Number.isFinite(value) && value > 0 ? Number(value) : 0;
4630
+ }
4631
+ function toShapeVariantKey(value) {
4632
+ return normalizeShapeVariantValue(value).toFixed(6);
4633
+ }
4230
4634
  async function fetchFontBytes(source) {
4231
4635
  const response = await fetch(source);
4232
4636
  if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);