pa_font 0.2.4 → 0.2.6

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
11
  if (points.length === 0 || !pointsEqual2D(points[points.length - 1], point)) points.push(point);
18
12
  }
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);
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,
@@ -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
  }
@@ -696,26 +848,31 @@ var PAShape = class {
696
848
  function createTextShape(layout, opts, fontInstance) {
697
849
  const parts = [];
698
850
  const contours = [];
699
- const glyphs = [];
700
- const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
851
+ const layoutGlyphs = layout.glyphs;
852
+ const glyphCount = layoutGlyphs.length;
853
+ const glyphs = new Array(glyphCount);
854
+ const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
701
855
  const variantOptions = {
702
856
  openWidth: normalizePositive(opts.openWidth, 0),
703
857
  step: normalizePositive(opts.step, 0)
704
858
  };
705
- layout.glyphs.forEach((item, glyphPosition) => {
859
+ for (let glyphPosition = 0; glyphPosition < glyphCount; glyphPosition++) {
860
+ const item = layoutGlyphs[glyphPosition];
706
861
  const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
707
- parts.push(...translated.parts);
708
- contours.push(...translated.contours);
709
- glyphs.push({
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] = {
710
867
  position: glyphPosition,
711
868
  glyphIndex: item.glyph.index,
712
869
  x: item.x,
713
870
  y: item.y,
714
871
  size: item.size,
715
872
  bbox: translated.bbox,
716
- partCount: translated.parts.length
717
- });
718
- });
873
+ partCount: tParts.length
874
+ };
875
+ }
719
876
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
720
877
  return new PAShape({
721
878
  text: layout.text,
@@ -809,8 +966,8 @@ function createDerivedShape(shape, parts) {
809
966
  }
810
967
  function createRegionCollection(shape) {
811
968
  return shape.parts.map((part) => ({
812
- outer: copyRing(part.outer),
813
- holes: part.holes.map((hole) => copyRing(hole)),
969
+ outer: copyRing$1(part.outer),
970
+ holes: part.holes.map((hole) => copyRing$1(hole)),
814
971
  bbox: { ...part.bbox }
815
972
  }));
816
973
  }
@@ -820,8 +977,8 @@ function copyParts(parts) {
820
977
  function copyPart(part) {
821
978
  return {
822
979
  ...part,
823
- outer: copyRing(part.outer),
824
- holes: part.holes.map((hole) => copyRing(hole)),
980
+ outer: copyRing$1(part.outer),
981
+ holes: part.holes.map((hole) => copyRing$1(hole)),
825
982
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
826
983
  bbox: { ...part.bbox }
827
984
  };
@@ -830,7 +987,7 @@ function copyContours(contours) {
830
987
  return contours.map((contour) => ({
831
988
  ...contour,
832
989
  bbox: { ...contour.bbox },
833
- ring: copyRing(contour.ring)
990
+ ring: copyRing$1(contour.ring)
834
991
  }));
835
992
  }
836
993
  function groupPartsByGlyphPosition(parts) {
@@ -946,76 +1103,108 @@ function resolveGeometryEpsilon(width) {
946
1103
  }
947
1104
  function openPartWithSlit(part, width, epsilon) {
948
1105
  if (!part.holes || part.holes.length === 0) return copyPart(part);
949
- let outer = copyRing(part.outer);
950
- 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));
951
1108
  const anchors = [];
1109
+ const blockers = [];
952
1110
  while (holes.length > 0) {
953
1111
  let selectedHoleIndex = -1;
954
1112
  let selectedBridge = null;
955
- holes.forEach((hole, holeIndex) => {
956
- const blockers = holes.filter((_, index) => index !== holeIndex);
957
- const candidate = findBestHoleBridge(outer, hole, blockers, epsilon);
958
- 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) {
959
1119
  selectedBridge = candidate;
960
- selectedHoleIndex = holeIndex;
1120
+ selectedHoleIndex = hi;
1121
+ selectedDist = candidate.distance;
961
1122
  }
962
- });
1123
+ }
963
1124
  if (!selectedBridge || selectedHoleIndex === -1) break;
964
1125
  const [hole] = holes.splice(selectedHoleIndex, 1);
965
1126
  const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
966
1127
  outer = merged.ring;
967
- anchors.push(...merged.anchors);
1128
+ const mergedAnchors = merged.anchors;
1129
+ for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
968
1130
  }
1131
+ let holeArea = 0;
1132
+ for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
969
1133
  return {
970
1134
  ...part,
971
1135
  outer,
972
1136
  holes,
973
1137
  anchors,
974
1138
  bbox: ringBounds(outer),
975
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1139
+ area: Math.abs(signedArea(outer)) - holeArea
976
1140
  };
977
1141
  }
978
1142
  function resamplePart(part, step) {
979
1143
  const outer = resampleRing(part.outer, step, part.anchors ?? []);
980
- 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
+ }
981
1151
  return {
982
1152
  ...part,
983
1153
  outer,
984
1154
  holes,
985
1155
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
986
1156
  bbox: ringBounds(outer),
987
- area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1157
+ area: Math.abs(signedArea(outer)) - holeArea
988
1158
  };
989
1159
  }
990
1160
  function findBestHoleBridge(outer, hole, blockers, epsilon) {
991
1161
  let best = null;
992
- outer.forEach((outerPoint, outerIndex) => {
993
- hole.forEach((holePoint, holeIndex) => {
994
- if (!isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon)) return;
995
- const candidate = {
996
- outerIndex,
997
- holeIndex,
998
- 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
999
1177
  };
1000
- if (!best || candidate.distance < best.distance) best = candidate;
1001
- });
1002
- });
1178
+ bestDist = dist;
1179
+ }
1180
+ }
1003
1181
  if (best) return best;
1004
1182
  return findNearestBridge(outer, hole);
1005
1183
  }
1006
1184
  function findNearestBridge(outer, hole) {
1007
- let best = null;
1008
- outer.forEach((outerPoint, outerIndex) => {
1009
- hole.forEach((holePoint, holeIndex) => {
1010
- const candidate = {
1011
- outerIndex,
1012
- holeIndex,
1013
- distance: distance(outerPoint, holePoint)
1014
- };
1015
- if (!best || candidate.distance < best.distance) best = candidate;
1016
- });
1017
- });
1018
- 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
+ };
1019
1208
  }
1020
1209
  function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
1021
1210
  if (distance(outerPoint, holePoint) <= epsilon) return false;
@@ -1142,7 +1331,7 @@ function normalizeCutLocation(name, ring, cut, epsilon) {
1142
1331
  };
1143
1332
  }
1144
1333
  function ringPath(ring, startIndex, endIndex) {
1145
- if (startIndex == null || endIndex == null) return copyRing(ring);
1334
+ if (startIndex == null || endIndex == null) return copyRing$1(ring);
1146
1335
  const path = [ring[startIndex]];
1147
1336
  let cursor = startIndex;
1148
1337
  while (cursor !== endIndex) {
@@ -1157,7 +1346,7 @@ function appendPath(target, path, epsilon) {
1157
1346
  });
1158
1347
  }
1159
1348
  function resampleRing(ring, step, anchors = []) {
1160
- if (ring.length < 3) return copyRing(ring);
1349
+ if (ring.length < 3) return copyRing$1(ring);
1161
1350
  const normalizedAnchors = normalizeAnchorPoints(ring, anchors);
1162
1351
  if (normalizedAnchors.length >= 2) return resampleRingWithAnchors(ring, step, normalizedAnchors);
1163
1352
  return resampleClosedPath(ring, step);
@@ -1178,30 +1367,32 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
1178
1367
  appendPath(result, resampleOpenPath(ringPath(ring, startIndex, endIndex), step), 1e-6);
1179
1368
  }
1180
1369
  if (result.length > 1 && pointsAlmostEqual2D(result[0], result[result.length - 1], 1e-6)) result.pop();
1181
- if (result.length < 3 || result.length >= ring.length) return copyRing(ring);
1370
+ if (result.length < 3 || result.length >= ring.length) return copyRing$1(ring);
1182
1371
  return result;
1183
1372
  }
1184
1373
  function resampleClosedPath(ring, step) {
1185
- const perimeter = ringPerimeter(ring);
1186
- 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);
1187
1377
  const pointCount = Math.max(3, Math.round(perimeter / step));
1188
- if (pointCount >= ring.length) return copyRing(ring);
1378
+ if (pointCount >= ring.length) return copyRing$1(ring);
1189
1379
  const sampled = [];
1190
- for (let index = 0; index < pointCount; index += 1) {
1191
- const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
1380
+ for (let index = 0; index < pointCount; index++) {
1381
+ const point = pointAtClosedPathDistanceFast(ring, cumulative, perimeter * index / pointCount);
1192
1382
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1193
1383
  }
1194
- if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
1384
+ if (sampled.length < 3 || sampled.length >= ring.length) return copyRing$1(ring);
1195
1385
  return sampled;
1196
1386
  }
1197
1387
  function resampleOpenPath(path, step) {
1198
1388
  if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
1199
- const length = polylineLength(path);
1389
+ const cumulative = buildOpenCumulativeDistances(path);
1390
+ const length = cumulative[path.length - 1];
1200
1391
  if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
1201
1392
  const divisionCount = Math.max(1, Math.round(length / step));
1202
1393
  const sampled = [];
1203
- for (let index = 0; index <= divisionCount; index += 1) {
1204
- const point = pointAtOpenPathDistance(path, length * index / divisionCount);
1394
+ for (let index = 0; index <= divisionCount; index++) {
1395
+ const point = pointAtOpenPathDistanceFast(path, cumulative, length * index / divisionCount);
1205
1396
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1206
1397
  }
1207
1398
  return sampled;
@@ -3573,9 +3764,25 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3573
3764
  width: 0,
3574
3765
  hardBreak: false
3575
3766
  };
3576
- let nextText = trimTrailingWhitespace(line.text);
3577
- while (nextText.length > 0 && measureWidth(`${nextText}${suffixText}`) > maxWidth + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
3578
- 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;
3579
3786
  return {
3580
3787
  ...line,
3581
3788
  text,
@@ -4112,12 +4319,6 @@ function getGraphemeSegmenter() {
4112
4319
  function trimTrailingWhitespace(value) {
4113
4320
  return value.replace(/\s+$/u, "");
4114
4321
  }
4115
- function trimLastGrapheme(value) {
4116
- const segmenter = getGraphemeSegmenter();
4117
- const graphemes = Array.from(segmenter.segment(value), (segment) => segment.segment);
4118
- graphemes.pop();
4119
- return graphemes.join("");
4120
- }
4121
4322
  function splitPreservingWhitespace(value) {
4122
4323
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
4123
4324
  }