pa_font 0.2.7 → 0.2.8

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,127 +3,52 @@ 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
+ }
6
12
  function pushUniquePoint(points, point) {
7
13
  if (points.length === 0 || !pointsEqual2D(points[points.length - 1], point)) points.push(point);
8
14
  }
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;
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);
127
52
  }
128
53
  function midpoint(a, b) {
129
54
  return [(a[0] + b[0]) * .5, (a[1] + b[1]) * .5];
@@ -181,37 +106,28 @@ function distance(a, b) {
181
106
  return Math.sqrt(dx * dx + dy * dy);
182
107
  }
183
108
  function sampleRing(ring, step, callback) {
184
- const len = ring.length;
185
- for (let index = 0; index < len; index++) {
109
+ for (let index = 0; index < ring.length; index += 1) {
186
110
  const a = ring[index];
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);
111
+ const b = ring[(index + 1) % ring.length];
112
+ const segmentLength = distance(a, b);
190
113
  const divisions = Math.max(1, Math.ceil(segmentLength / step));
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++) {
114
+ for (let offset = 0; offset < divisions; offset += 1) if (index > 0 || offset > 0) {
198
115
  const t = offset / divisions;
199
- callback([a[0] + dx * t, a[1] + dy * t]);
200
- }
116
+ callback([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
117
+ } else callback([a[0], a[1]]);
201
118
  }
202
119
  }
203
120
  function ringBounds(ring) {
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
- }
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
+ });
215
131
  return {
216
132
  x: minX,
217
133
  y: minY,
@@ -220,10 +136,7 @@ function ringBounds(ring) {
220
136
  };
221
137
  }
222
138
  function translateRing(ring, tx, 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;
139
+ return ring.map((point) => [point[0] + tx, point[1] + ty]);
227
140
  }
228
141
  function translateRect(rect, tx, ty) {
229
142
  return {
@@ -234,28 +147,20 @@ function translateRect(rect, tx, ty) {
234
147
  };
235
148
  }
236
149
  function copyRing(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;
150
+ return ring.map((point) => [point[0], point[1]]);
241
151
  }
242
152
  function combineRects(rects) {
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
- }
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
+ });
259
164
  return {
260
165
  x: minX,
261
166
  y: minY,
@@ -297,64 +202,43 @@ function ringPerimeter(ring) {
297
202
  for (let index = 0; index < ring.length; index += 1) total += distance(ring[index], ring[(index + 1) % ring.length]);
298
203
  return total;
299
204
  }
300
- function buildCumulativeDistances(ring) {
301
- const len = ring.length;
302
- const cumulative = new Float64Array(len + 1);
303
- let total = 0;
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);
205
+ function polylineLength(path) {
317
206
  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];
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);
338
212
  if (perimeter <= 1e-9) return [ring[0][0], ring[0][1]];
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) {
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) {
349
228
  if (distanceAlong <= 0) return [path[0][0], path[0][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];
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]];
358
242
  }
359
243
  function segmentIntersectsRing(a, b, ring, ignoredEdges, epsilon) {
360
244
  for (let index = 0; index < ring.length; index += 1) {
@@ -436,28 +320,23 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
436
320
  current = null;
437
321
  return;
438
322
  }
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
- });
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
+ }
450
331
  contours.push({
451
332
  id: contourId++,
452
- start: current.start,
333
+ start: cloneRawPoint(current.start),
453
334
  segments: current.segments
454
335
  });
455
336
  current = null;
456
337
  };
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") {
338
+ commands.forEach((cmd) => {
339
+ if (cmd.type === "M") {
461
340
  finishCurrentContour();
462
341
  current = {
463
342
  start: {
@@ -470,49 +349,47 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
470
349
  },
471
350
  segments: []
472
351
  };
473
- continue;
352
+ return;
474
353
  }
475
- if (!current) continue;
476
- if (type === "L") {
354
+ if (!current) return;
355
+ if (cmd.type === "L") {
477
356
  const to = {
478
357
  x: cmd.x,
479
358
  y: cmd.y
480
359
  };
481
360
  current.segments.push({
482
361
  type: "L",
483
- from: current.cursor,
362
+ from: cloneRawPoint(current.cursor),
484
363
  to
485
364
  });
486
- current.cursor = to;
487
- continue;
365
+ current.cursor = cloneRawPoint(to);
366
+ return;
488
367
  }
489
- if (type === "Q") {
490
- const from = current.cursor;
368
+ if (cmd.type === "Q") {
491
369
  const to = {
492
370
  x: cmd.x,
493
371
  y: cmd.y
494
372
  };
495
373
  current.segments.push({
496
374
  type: "Q",
497
- from,
375
+ from: cloneRawPoint(current.cursor),
498
376
  c1: {
499
377
  x: cmd.x1,
500
378
  y: cmd.y1
501
379
  },
502
380
  to
503
381
  });
504
- current.cursor = to;
505
- continue;
382
+ current.cursor = cloneRawPoint(to);
383
+ return;
506
384
  }
507
- if (type === "C") {
508
- const from = current.cursor;
385
+ if (cmd.type === "C") {
509
386
  const to = {
510
387
  x: cmd.x,
511
388
  y: cmd.y
512
389
  };
513
390
  current.segments.push({
514
391
  type: "C",
515
- from,
392
+ from: cloneRawPoint(current.cursor),
516
393
  c1: {
517
394
  x: cmd.x1,
518
395
  y: cmd.y1
@@ -523,11 +400,11 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
523
400
  },
524
401
  to
525
402
  });
526
- current.cursor = to;
527
- continue;
403
+ current.cursor = cloneRawPoint(to);
404
+ return;
528
405
  }
529
- if (type === "Z") finishCurrentContour();
530
- }
406
+ if (cmd.type === "Z") finishCurrentContour();
407
+ });
531
408
  finishCurrentContour();
532
409
  return {
533
410
  glyphIndex: glyph.index,
@@ -551,25 +428,18 @@ function flattenGlyphTopology(topology, options) {
551
428
  }
552
429
  function flattenContour(contour, scale, tolerance) {
553
430
  const ring = [];
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;
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;
571
436
  }
572
- }
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
+ });
573
443
  if (ring.length > 1 && pointsEqual2D(ring[0], ring[ring.length - 1])) ring.pop();
574
444
  if (ring.length < 3) return null;
575
445
  const bbox = ringBounds(ring);
@@ -582,45 +452,37 @@ function flattenContour(contour, scale, tolerance) {
582
452
  };
583
453
  }
584
454
  function classifyContours(contours) {
585
- const len = contours.length;
586
- const result = new Array(len);
587
- for (let i = 0; i < len; i++) result[i] = {
588
- ...contours[i],
455
+ const result = contours.map((contour) => ({
456
+ ...contour,
589
457
  parentId: null,
590
458
  depth: 0,
591
459
  role: "outer"
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);
460
+ }));
461
+ result.forEach((child) => {
597
462
  let parent = null;
598
463
  let parentArea = Number.POSITIVE_INFINITY;
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
- }
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
+ });
610
475
  if (parent) child.parentId = parent.id;
611
- }
476
+ });
612
477
  const byId = new Map(result.map((contour) => [contour.id, contour]));
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
- }
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
+ });
624
486
  return result;
625
487
  }
626
488
  function buildParts(contours) {
@@ -635,26 +497,16 @@ function buildParts(contours) {
635
497
  });
636
498
  }
637
499
  function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
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] = {
500
+ return {
501
+ parts: geometry.parts.map((part, partIndex) => ({
647
502
  outer: translateRing(part.outer, tx, ty),
648
503
  holes: part.holes.map((hole) => translateRing(hole, tx, ty)),
649
504
  bbox: translateRect(part.bbox, tx, ty),
650
505
  area: part.area,
651
506
  glyphPosition,
652
- partIndex: i
653
- };
654
- }
655
- for (let i = 0; i < contoursLen; i++) {
656
- const contour = srcContours[i];
657
- contours[i] = {
507
+ partIndex
508
+ })),
509
+ contours: geometry.contours.map((contour) => ({
658
510
  id: `${glyphPosition}:${contour.id}`,
659
511
  sourceId: contour.id,
660
512
  glyphPosition,
@@ -664,11 +516,7 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
664
516
  area: contour.area,
665
517
  bbox: translateRect(contour.bbox, tx, ty),
666
518
  ring: translateRing(contour.ring, tx, ty)
667
- };
668
- }
669
- return {
670
- parts,
671
- contours,
519
+ })),
672
520
  bbox: translateRect(geometry.bbox, tx, ty)
673
521
  };
674
522
  }
@@ -844,31 +692,26 @@ var PAShape = class {
844
692
  function createTextShape(layout, opts, fontInstance) {
845
693
  const parts = [];
846
694
  const contours = [];
847
- const layoutGlyphs = layout.glyphs;
848
- const glyphCount = layoutGlyphs.length;
849
- const glyphs = new Array(glyphCount);
850
- const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
695
+ const glyphs = [];
696
+ const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
851
697
  const variantOptions = {
852
698
  openWidth: normalizePositive(opts.openWidth, 0),
853
699
  step: normalizePositive(opts.step, 0)
854
700
  };
855
- for (let glyphPosition = 0; glyphPosition < glyphCount; glyphPosition++) {
856
- const item = layoutGlyphs[glyphPosition];
701
+ layout.glyphs.forEach((item, glyphPosition) => {
857
702
  const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
858
- const tParts = translated.parts;
859
- const tContours = translated.contours;
860
- for (let j = 0; j < tParts.length; j++) parts.push(tParts[j]);
861
- for (let j = 0; j < tContours.length; j++) contours.push(tContours[j]);
862
- glyphs[glyphPosition] = {
703
+ parts.push(...translated.parts);
704
+ contours.push(...translated.contours);
705
+ glyphs.push({
863
706
  position: glyphPosition,
864
707
  glyphIndex: item.glyph.index,
865
708
  x: item.x,
866
709
  y: item.y,
867
710
  size: item.size,
868
711
  bbox: translated.bbox,
869
- partCount: tParts.length
870
- };
871
- }
712
+ partCount: translated.parts.length
713
+ });
714
+ });
872
715
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
873
716
  return new PAShape({
874
717
  text: layout.text,
@@ -1102,105 +945,73 @@ function openPartWithSlit(part, width, epsilon) {
1102
945
  let outer = copyRing(part.outer);
1103
946
  const holes = part.holes.map((hole) => copyRing(hole));
1104
947
  const anchors = [];
1105
- const blockers = [];
1106
948
  while (holes.length > 0) {
1107
949
  let selectedHoleIndex = -1;
1108
950
  let selectedBridge = null;
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) {
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)) {
1115
955
  selectedBridge = candidate;
1116
- selectedHoleIndex = hi;
1117
- selectedDist = candidate.distance;
956
+ selectedHoleIndex = holeIndex;
1118
957
  }
1119
- }
958
+ });
1120
959
  if (!selectedBridge || selectedHoleIndex === -1) break;
1121
960
  const [hole] = holes.splice(selectedHoleIndex, 1);
1122
961
  const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
1123
962
  outer = merged.ring;
1124
- const mergedAnchors = merged.anchors;
1125
- for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
963
+ anchors.push(...merged.anchors);
1126
964
  }
1127
- let holeArea = 0;
1128
- for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
1129
965
  return {
1130
966
  ...part,
1131
967
  outer,
1132
968
  holes,
1133
969
  anchors,
1134
970
  bbox: ringBounds(outer),
1135
- area: Math.abs(signedArea(outer)) - holeArea
971
+ area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1136
972
  };
1137
973
  }
1138
974
  function resamplePart(part, step) {
1139
975
  const outer = resampleRing(part.outer, step, part.anchors ?? []);
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
- }
976
+ const holes = part.holes.map((hole) => resampleRing(hole, step));
1147
977
  return {
1148
978
  ...part,
1149
979
  outer,
1150
980
  holes,
1151
981
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
1152
982
  bbox: ringBounds(outer),
1153
- area: Math.abs(signedArea(outer)) - holeArea
983
+ area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1154
984
  };
1155
985
  }
1156
986
  function findBestHoleBridge(outer, hole, blockers, epsilon) {
1157
987
  let best = null;
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
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)
1173
995
  };
1174
- bestDist = dist;
1175
- }
1176
- }
996
+ if (!best || candidate.distance < best.distance) best = candidate;
997
+ });
998
+ });
1177
999
  if (best) return best;
1178
1000
  return findNearestBridge(outer, hole);
1179
1001
  }
1180
1002
  function findNearestBridge(outer, hole) {
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
- };
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;
1204
1015
  }
1205
1016
  function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
1206
1017
  if (distance(outerPoint, holePoint) <= epsilon) return false;
@@ -1367,14 +1178,13 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
1367
1178
  return result;
1368
1179
  }
1369
1180
  function resampleClosedPath(ring, step) {
1370
- const cumulative = buildCumulativeDistances(ring);
1371
- const perimeter = cumulative[ring.length];
1181
+ const perimeter = ringPerimeter(ring);
1372
1182
  if (perimeter <= 1e-9) return copyRing(ring);
1373
1183
  const pointCount = Math.max(3, Math.round(perimeter / step));
1374
1184
  if (pointCount >= ring.length) return copyRing(ring);
1375
1185
  const sampled = [];
1376
- for (let index = 0; index < pointCount; index++) {
1377
- const point = pointAtClosedPathDistanceFast(ring, cumulative, perimeter * index / pointCount);
1186
+ for (let index = 0; index < pointCount; index += 1) {
1187
+ const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
1378
1188
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1379
1189
  }
1380
1190
  if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
@@ -1382,13 +1192,12 @@ function resampleClosedPath(ring, step) {
1382
1192
  }
1383
1193
  function resampleOpenPath(path, step) {
1384
1194
  if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
1385
- const cumulative = buildOpenCumulativeDistances(path);
1386
- const length = cumulative[path.length - 1];
1195
+ const length = polylineLength(path);
1387
1196
  if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
1388
1197
  const divisionCount = Math.max(1, Math.round(length / step));
1389
1198
  const sampled = [];
1390
- for (let index = 0; index <= divisionCount; index++) {
1391
- const point = pointAtOpenPathDistanceFast(path, cumulative, length * index / divisionCount);
1199
+ for (let index = 0; index <= divisionCount; index += 1) {
1200
+ const point = pointAtOpenPathDistance(path, length * index / divisionCount);
1392
1201
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1393
1202
  }
1394
1203
  return sampled;
@@ -3760,25 +3569,9 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3760
3569
  width: 0,
3761
3570
  hardBreak: false
3762
3571
  };
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;
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}`;
3782
3575
  return {
3783
3576
  ...line,
3784
3577
  text,
@@ -4315,6 +4108,12 @@ function getGraphemeSegmenter() {
4315
4108
  function trimTrailingWhitespace(value) {
4316
4109
  return value.replace(/\s+$/u, "");
4317
4110
  }
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
+ }
4318
4117
  function splitPreservingWhitespace(value) {
4319
4118
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
4320
4119
  }