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