pa_font 0.2.4 → 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
  }
@@ -692,26 +844,31 @@ var PAShape = class {
692
844
  function createTextShape(layout, opts, fontInstance) {
693
845
  const parts = [];
694
846
  const contours = [];
695
- const glyphs = [];
696
- const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
847
+ const layoutGlyphs = layout.glyphs;
848
+ const glyphCount = layoutGlyphs.length;
849
+ const glyphs = new Array(glyphCount);
850
+ const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
697
851
  const variantOptions = {
698
852
  openWidth: normalizePositive(opts.openWidth, 0),
699
853
  step: normalizePositive(opts.step, 0)
700
854
  };
701
- layout.glyphs.forEach((item, glyphPosition) => {
855
+ for (let glyphPosition = 0; glyphPosition < glyphCount; glyphPosition++) {
856
+ const item = layoutGlyphs[glyphPosition];
702
857
  const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
703
- parts.push(...translated.parts);
704
- contours.push(...translated.contours);
705
- glyphs.push({
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] = {
706
863
  position: glyphPosition,
707
864
  glyphIndex: item.glyph.index,
708
865
  x: item.x,
709
866
  y: item.y,
710
867
  size: item.size,
711
868
  bbox: translated.bbox,
712
- partCount: translated.parts.length
713
- });
714
- });
869
+ partCount: tParts.length
870
+ };
871
+ }
715
872
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
716
873
  return new PAShape({
717
874
  text: layout.text,
@@ -805,8 +962,8 @@ function createDerivedShape(shape, parts) {
805
962
  }
806
963
  function createRegionCollection(shape) {
807
964
  return shape.parts.map((part) => ({
808
- outer: copyRing(part.outer),
809
- holes: part.holes.map((hole) => copyRing(hole)),
965
+ outer: copyRing$1(part.outer),
966
+ holes: part.holes.map((hole) => copyRing$1(hole)),
810
967
  bbox: { ...part.bbox }
811
968
  }));
812
969
  }
@@ -816,8 +973,8 @@ function copyParts(parts) {
816
973
  function copyPart(part) {
817
974
  return {
818
975
  ...part,
819
- outer: copyRing(part.outer),
820
- holes: part.holes.map((hole) => copyRing(hole)),
976
+ outer: copyRing$1(part.outer),
977
+ holes: part.holes.map((hole) => copyRing$1(hole)),
821
978
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
822
979
  bbox: { ...part.bbox }
823
980
  };
@@ -826,7 +983,7 @@ function copyContours(contours) {
826
983
  return contours.map((contour) => ({
827
984
  ...contour,
828
985
  bbox: { ...contour.bbox },
829
- ring: copyRing(contour.ring)
986
+ ring: copyRing$1(contour.ring)
830
987
  }));
831
988
  }
832
989
  function groupPartsByGlyphPosition(parts) {
@@ -942,76 +1099,108 @@ function resolveGeometryEpsilon(width) {
942
1099
  }
943
1100
  function openPartWithSlit(part, width, epsilon) {
944
1101
  if (!part.holes || part.holes.length === 0) return copyPart(part);
945
- let outer = copyRing(part.outer);
946
- 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));
947
1104
  const anchors = [];
1105
+ const blockers = [];
948
1106
  while (holes.length > 0) {
949
1107
  let selectedHoleIndex = -1;
950
1108
  let selectedBridge = null;
951
- holes.forEach((hole, holeIndex) => {
952
- const blockers = holes.filter((_, index) => index !== holeIndex);
953
- const candidate = findBestHoleBridge(outer, hole, blockers, epsilon);
954
- 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) {
955
1115
  selectedBridge = candidate;
956
- selectedHoleIndex = holeIndex;
1116
+ selectedHoleIndex = hi;
1117
+ selectedDist = candidate.distance;
957
1118
  }
958
- });
1119
+ }
959
1120
  if (!selectedBridge || selectedHoleIndex === -1) break;
960
1121
  const [hole] = holes.splice(selectedHoleIndex, 1);
961
1122
  const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
962
1123
  outer = merged.ring;
963
- anchors.push(...merged.anchors);
1124
+ const mergedAnchors = merged.anchors;
1125
+ for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
964
1126
  }
1127
+ let holeArea = 0;
1128
+ for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
965
1129
  return {
966
1130
  ...part,
967
1131
  outer,
968
1132
  holes,
969
1133
  anchors,
970
1134
  bbox: ringBounds(outer),
971
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1135
+ area: Math.abs(signedArea(outer)) - holeArea
972
1136
  };
973
1137
  }
974
1138
  function resamplePart(part, step) {
975
1139
  const outer = resampleRing(part.outer, step, part.anchors ?? []);
976
- 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
+ }
977
1147
  return {
978
1148
  ...part,
979
1149
  outer,
980
1150
  holes,
981
1151
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
982
1152
  bbox: ringBounds(outer),
983
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1153
+ area: Math.abs(signedArea(outer)) - holeArea
984
1154
  };
985
1155
  }
986
1156
  function findBestHoleBridge(outer, hole, blockers, epsilon) {
987
1157
  let best = null;
988
- outer.forEach((outerPoint, outerIndex) => {
989
- hole.forEach((holePoint, holeIndex) => {
990
- if (!isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon)) return;
991
- const candidate = {
992
- outerIndex,
993
- holeIndex,
994
- 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
995
1173
  };
996
- if (!best || candidate.distance < best.distance) best = candidate;
997
- });
998
- });
1174
+ bestDist = dist;
1175
+ }
1176
+ }
999
1177
  if (best) return best;
1000
1178
  return findNearestBridge(outer, hole);
1001
1179
  }
1002
1180
  function findNearestBridge(outer, hole) {
1003
- let best = null;
1004
- outer.forEach((outerPoint, outerIndex) => {
1005
- hole.forEach((holePoint, holeIndex) => {
1006
- const candidate = {
1007
- outerIndex,
1008
- holeIndex,
1009
- distance: distance(outerPoint, holePoint)
1010
- };
1011
- if (!best || candidate.distance < best.distance) best = candidate;
1012
- });
1013
- });
1014
- 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
+ };
1015
1204
  }
1016
1205
  function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
1017
1206
  if (distance(outerPoint, holePoint) <= epsilon) return false;
@@ -1138,7 +1327,7 @@ function normalizeCutLocation(name, ring, cut, epsilon) {
1138
1327
  };
1139
1328
  }
1140
1329
  function ringPath(ring, startIndex, endIndex) {
1141
- if (startIndex == null || endIndex == null) return copyRing(ring);
1330
+ if (startIndex == null || endIndex == null) return copyRing$1(ring);
1142
1331
  const path = [ring[startIndex]];
1143
1332
  let cursor = startIndex;
1144
1333
  while (cursor !== endIndex) {
@@ -1153,7 +1342,7 @@ function appendPath(target, path, epsilon) {
1153
1342
  });
1154
1343
  }
1155
1344
  function resampleRing(ring, step, anchors = []) {
1156
- if (ring.length < 3) return copyRing(ring);
1345
+ if (ring.length < 3) return copyRing$1(ring);
1157
1346
  const normalizedAnchors = normalizeAnchorPoints(ring, anchors);
1158
1347
  if (normalizedAnchors.length >= 2) return resampleRingWithAnchors(ring, step, normalizedAnchors);
1159
1348
  return resampleClosedPath(ring, step);
@@ -1174,30 +1363,32 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
1174
1363
  appendPath(result, resampleOpenPath(ringPath(ring, startIndex, endIndex), step), 1e-6);
1175
1364
  }
1176
1365
  if (result.length > 1 && pointsAlmostEqual2D(result[0], result[result.length - 1], 1e-6)) result.pop();
1177
- if (result.length < 3 || result.length >= ring.length) return copyRing(ring);
1366
+ if (result.length < 3 || result.length >= ring.length) return copyRing$1(ring);
1178
1367
  return result;
1179
1368
  }
1180
1369
  function resampleClosedPath(ring, step) {
1181
- const perimeter = ringPerimeter(ring);
1182
- 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);
1183
1373
  const pointCount = Math.max(3, Math.round(perimeter / step));
1184
- if (pointCount >= ring.length) return copyRing(ring);
1374
+ if (pointCount >= ring.length) return copyRing$1(ring);
1185
1375
  const sampled = [];
1186
- for (let index = 0; index < pointCount; index += 1) {
1187
- const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
1376
+ for (let index = 0; index < pointCount; index++) {
1377
+ const point = pointAtClosedPathDistanceFast(ring, cumulative, perimeter * index / pointCount);
1188
1378
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1189
1379
  }
1190
- if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
1380
+ if (sampled.length < 3 || sampled.length >= ring.length) return copyRing$1(ring);
1191
1381
  return sampled;
1192
1382
  }
1193
1383
  function resampleOpenPath(path, step) {
1194
1384
  if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
1195
- const length = polylineLength(path);
1385
+ const cumulative = buildOpenCumulativeDistances(path);
1386
+ const length = cumulative[path.length - 1];
1196
1387
  if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
1197
1388
  const divisionCount = Math.max(1, Math.round(length / step));
1198
1389
  const sampled = [];
1199
- for (let index = 0; index <= divisionCount; index += 1) {
1200
- const point = pointAtOpenPathDistance(path, length * index / divisionCount);
1390
+ for (let index = 0; index <= divisionCount; index++) {
1391
+ const point = pointAtOpenPathDistanceFast(path, cumulative, length * index / divisionCount);
1201
1392
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1202
1393
  }
1203
1394
  return sampled;
@@ -3569,9 +3760,25 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3569
3760
  width: 0,
3570
3761
  hardBreak: false
3571
3762
  };
3572
- let nextText = trimTrailingWhitespace(line.text);
3573
- while (nextText.length > 0 && measureWidth(`${nextText}${suffixText}`) > maxWidth + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
3574
- 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;
3575
3782
  return {
3576
3783
  ...line,
3577
3784
  text,
@@ -4108,12 +4315,6 @@ function getGraphemeSegmenter() {
4108
4315
  function trimTrailingWhitespace(value) {
4109
4316
  return value.replace(/\s+$/u, "");
4110
4317
  }
4111
- function trimLastGrapheme(value) {
4112
- const segmenter = getGraphemeSegmenter();
4113
- const graphemes = Array.from(segmenter.segment(value), (segment) => segment.segment);
4114
- graphemes.pop();
4115
- return graphemes.join("");
4116
- }
4117
4318
  function splitPreservingWhitespace(value) {
4118
4319
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
4119
4320
  }