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.cjs CHANGED
@@ -7,127 +7,52 @@ 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
+ }
10
16
  function pushUniquePoint(points, point) {
11
17
  if (points.length === 0 || !pointsEqual2D(points[points.length - 1], point)) points.push(point);
12
18
  }
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;
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);
131
56
  }
132
57
  function midpoint(a, b) {
133
58
  return [(a[0] + b[0]) * .5, (a[1] + b[1]) * .5];
@@ -185,37 +110,28 @@ function distance(a, b) {
185
110
  return Math.sqrt(dx * dx + dy * dy);
186
111
  }
187
112
  function sampleRing(ring, step, callback) {
188
- const len = ring.length;
189
- for (let index = 0; index < len; index++) {
113
+ for (let index = 0; index < ring.length; index += 1) {
190
114
  const a = ring[index];
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);
115
+ const b = ring[(index + 1) % ring.length];
116
+ const segmentLength = distance(a, b);
194
117
  const divisions = Math.max(1, Math.ceil(segmentLength / step));
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++) {
118
+ for (let offset = 0; offset < divisions; offset += 1) if (index > 0 || offset > 0) {
202
119
  const t = offset / divisions;
203
- callback([a[0] + dx * t, a[1] + dy * t]);
204
- }
120
+ callback([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
121
+ } else callback([a[0], a[1]]);
205
122
  }
206
123
  }
207
124
  function ringBounds(ring) {
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
- }
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
+ });
219
135
  return {
220
136
  x: minX,
221
137
  y: minY,
@@ -224,10 +140,7 @@ function ringBounds(ring) {
224
140
  };
225
141
  }
226
142
  function translateRing(ring, tx, 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;
143
+ return ring.map((point) => [point[0] + tx, point[1] + ty]);
231
144
  }
232
145
  function translateRect(rect, tx, ty) {
233
146
  return {
@@ -238,28 +151,20 @@ function translateRect(rect, tx, ty) {
238
151
  };
239
152
  }
240
153
  function copyRing(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;
154
+ return ring.map((point) => [point[0], point[1]]);
245
155
  }
246
156
  function combineRects(rects) {
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
- }
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
+ });
263
168
  return {
264
169
  x: minX,
265
170
  y: minY,
@@ -301,64 +206,43 @@ function ringPerimeter(ring) {
301
206
  for (let index = 0; index < ring.length; index += 1) total += distance(ring[index], ring[(index + 1) % ring.length]);
302
207
  return total;
303
208
  }
304
- function buildCumulativeDistances(ring) {
305
- const len = ring.length;
306
- const cumulative = new Float64Array(len + 1);
307
- let total = 0;
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);
209
+ function polylineLength(path) {
321
210
  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];
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);
342
216
  if (perimeter <= 1e-9) return [ring[0][0], ring[0][1]];
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) {
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) {
353
232
  if (distanceAlong <= 0) return [path[0][0], path[0][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];
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]];
362
246
  }
363
247
  function segmentIntersectsRing(a, b, ring, ignoredEdges, epsilon) {
364
248
  for (let index = 0; index < ring.length; index += 1) {
@@ -440,28 +324,23 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
440
324
  current = null;
441
325
  return;
442
326
  }
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
- });
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
+ }
454
335
  contours.push({
455
336
  id: contourId++,
456
- start: current.start,
337
+ start: cloneRawPoint(current.start),
457
338
  segments: current.segments
458
339
  });
459
340
  current = null;
460
341
  };
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") {
342
+ commands.forEach((cmd) => {
343
+ if (cmd.type === "M") {
465
344
  finishCurrentContour();
466
345
  current = {
467
346
  start: {
@@ -474,49 +353,47 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
474
353
  },
475
354
  segments: []
476
355
  };
477
- continue;
356
+ return;
478
357
  }
479
- if (!current) continue;
480
- if (type === "L") {
358
+ if (!current) return;
359
+ if (cmd.type === "L") {
481
360
  const to = {
482
361
  x: cmd.x,
483
362
  y: cmd.y
484
363
  };
485
364
  current.segments.push({
486
365
  type: "L",
487
- from: current.cursor,
366
+ from: cloneRawPoint(current.cursor),
488
367
  to
489
368
  });
490
- current.cursor = to;
491
- continue;
369
+ current.cursor = cloneRawPoint(to);
370
+ return;
492
371
  }
493
- if (type === "Q") {
494
- const from = current.cursor;
372
+ if (cmd.type === "Q") {
495
373
  const to = {
496
374
  x: cmd.x,
497
375
  y: cmd.y
498
376
  };
499
377
  current.segments.push({
500
378
  type: "Q",
501
- from,
379
+ from: cloneRawPoint(current.cursor),
502
380
  c1: {
503
381
  x: cmd.x1,
504
382
  y: cmd.y1
505
383
  },
506
384
  to
507
385
  });
508
- current.cursor = to;
509
- continue;
386
+ current.cursor = cloneRawPoint(to);
387
+ return;
510
388
  }
511
- if (type === "C") {
512
- const from = current.cursor;
389
+ if (cmd.type === "C") {
513
390
  const to = {
514
391
  x: cmd.x,
515
392
  y: cmd.y
516
393
  };
517
394
  current.segments.push({
518
395
  type: "C",
519
- from,
396
+ from: cloneRawPoint(current.cursor),
520
397
  c1: {
521
398
  x: cmd.x1,
522
399
  y: cmd.y1
@@ -527,11 +404,11 @@ function buildGlyphTopology(glyph, fallbackUnitsPerEm) {
527
404
  },
528
405
  to
529
406
  });
530
- current.cursor = to;
531
- continue;
407
+ current.cursor = cloneRawPoint(to);
408
+ return;
532
409
  }
533
- if (type === "Z") finishCurrentContour();
534
- }
410
+ if (cmd.type === "Z") finishCurrentContour();
411
+ });
535
412
  finishCurrentContour();
536
413
  return {
537
414
  glyphIndex: glyph.index,
@@ -555,25 +432,18 @@ function flattenGlyphTopology(topology, options) {
555
432
  }
556
433
  function flattenContour(contour, scale, tolerance) {
557
434
  const ring = [];
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;
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;
575
440
  }
576
- }
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
+ });
577
447
  if (ring.length > 1 && pointsEqual2D(ring[0], ring[ring.length - 1])) ring.pop();
578
448
  if (ring.length < 3) return null;
579
449
  const bbox = ringBounds(ring);
@@ -586,45 +456,37 @@ function flattenContour(contour, scale, tolerance) {
586
456
  };
587
457
  }
588
458
  function classifyContours(contours) {
589
- const len = contours.length;
590
- const result = new Array(len);
591
- for (let i = 0; i < len; i++) result[i] = {
592
- ...contours[i],
459
+ const result = contours.map((contour) => ({
460
+ ...contour,
593
461
  parentId: null,
594
462
  depth: 0,
595
463
  role: "outer"
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);
464
+ }));
465
+ result.forEach((child) => {
601
466
  let parent = null;
602
467
  let parentArea = Number.POSITIVE_INFINITY;
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
- }
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
+ });
614
479
  if (parent) child.parentId = parent.id;
615
- }
480
+ });
616
481
  const byId = new Map(result.map((contour) => [contour.id, contour]));
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
- }
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
+ });
628
490
  return result;
629
491
  }
630
492
  function buildParts(contours) {
@@ -639,26 +501,16 @@ function buildParts(contours) {
639
501
  });
640
502
  }
641
503
  function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
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] = {
504
+ return {
505
+ parts: geometry.parts.map((part, partIndex) => ({
651
506
  outer: translateRing(part.outer, tx, ty),
652
507
  holes: part.holes.map((hole) => translateRing(hole, tx, ty)),
653
508
  bbox: translateRect(part.bbox, tx, ty),
654
509
  area: part.area,
655
510
  glyphPosition,
656
- partIndex: i
657
- };
658
- }
659
- for (let i = 0; i < contoursLen; i++) {
660
- const contour = srcContours[i];
661
- contours[i] = {
511
+ partIndex
512
+ })),
513
+ contours: geometry.contours.map((contour) => ({
662
514
  id: `${glyphPosition}:${contour.id}`,
663
515
  sourceId: contour.id,
664
516
  glyphPosition,
@@ -668,11 +520,7 @@ function translateGlyphGeometry(geometry, tx, ty, glyphPosition) {
668
520
  area: contour.area,
669
521
  bbox: translateRect(contour.bbox, tx, ty),
670
522
  ring: translateRing(contour.ring, tx, ty)
671
- };
672
- }
673
- return {
674
- parts,
675
- contours,
523
+ })),
676
524
  bbox: translateRect(geometry.bbox, tx, ty)
677
525
  };
678
526
  }
@@ -848,31 +696,26 @@ var PAShape = class {
848
696
  function createTextShape(layout, opts, fontInstance) {
849
697
  const parts = [];
850
698
  const contours = [];
851
- const layoutGlyphs = layout.glyphs;
852
- const glyphCount = layoutGlyphs.length;
853
- const glyphs = new Array(glyphCount);
854
- const sourceLayoutGlyphs = Array.isArray(layoutGlyphs) ? layoutGlyphs.slice() : [];
699
+ const glyphs = [];
700
+ const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
855
701
  const variantOptions = {
856
702
  openWidth: normalizePositive(opts.openWidth, 0),
857
703
  step: normalizePositive(opts.step, 0)
858
704
  };
859
- for (let glyphPosition = 0; glyphPosition < glyphCount; glyphPosition++) {
860
- const item = layoutGlyphs[glyphPosition];
705
+ layout.glyphs.forEach((item, glyphPosition) => {
861
706
  const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
862
- const tParts = translated.parts;
863
- const tContours = translated.contours;
864
- for (let j = 0; j < tParts.length; j++) parts.push(tParts[j]);
865
- for (let j = 0; j < tContours.length; j++) contours.push(tContours[j]);
866
- glyphs[glyphPosition] = {
707
+ parts.push(...translated.parts);
708
+ contours.push(...translated.contours);
709
+ glyphs.push({
867
710
  position: glyphPosition,
868
711
  glyphIndex: item.glyph.index,
869
712
  x: item.x,
870
713
  y: item.y,
871
714
  size: item.size,
872
715
  bbox: translated.bbox,
873
- partCount: tParts.length
874
- };
875
- }
716
+ partCount: translated.parts.length
717
+ });
718
+ });
876
719
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
877
720
  return new PAShape({
878
721
  text: layout.text,
@@ -1106,105 +949,73 @@ function openPartWithSlit(part, width, epsilon) {
1106
949
  let outer = copyRing(part.outer);
1107
950
  const holes = part.holes.map((hole) => copyRing(hole));
1108
951
  const anchors = [];
1109
- const blockers = [];
1110
952
  while (holes.length > 0) {
1111
953
  let selectedHoleIndex = -1;
1112
954
  let selectedBridge = null;
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) {
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)) {
1119
959
  selectedBridge = candidate;
1120
- selectedHoleIndex = hi;
1121
- selectedDist = candidate.distance;
960
+ selectedHoleIndex = holeIndex;
1122
961
  }
1123
- }
962
+ });
1124
963
  if (!selectedBridge || selectedHoleIndex === -1) break;
1125
964
  const [hole] = holes.splice(selectedHoleIndex, 1);
1126
965
  const merged = mergeHoleIntoOuter(outer, hole, selectedBridge, width, epsilon);
1127
966
  outer = merged.ring;
1128
- const mergedAnchors = merged.anchors;
1129
- for (let i = 0; i < mergedAnchors.length; i++) anchors.push(mergedAnchors[i]);
967
+ anchors.push(...merged.anchors);
1130
968
  }
1131
- let holeArea = 0;
1132
- for (let i = 0; i < holes.length; i++) holeArea += Math.abs(signedArea(holes[i]));
1133
969
  return {
1134
970
  ...part,
1135
971
  outer,
1136
972
  holes,
1137
973
  anchors,
1138
974
  bbox: ringBounds(outer),
1139
- area: Math.abs(signedArea(outer)) - holeArea
975
+ area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1140
976
  };
1141
977
  }
1142
978
  function resamplePart(part, step) {
1143
979
  const outer = resampleRing(part.outer, step, part.anchors ?? []);
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
- }
980
+ const holes = part.holes.map((hole) => resampleRing(hole, step));
1151
981
  return {
1152
982
  ...part,
1153
983
  outer,
1154
984
  holes,
1155
985
  anchors: part.anchors ? part.anchors.map((point) => [point[0], point[1]]) : void 0,
1156
986
  bbox: ringBounds(outer),
1157
- area: Math.abs(signedArea(outer)) - holeArea
987
+ area: Math.abs(signedArea(outer)) - holes.reduce((sum, hole) => sum + Math.abs(signedArea(hole)), 0)
1158
988
  };
1159
989
  }
1160
990
  function findBestHoleBridge(outer, hole, blockers, epsilon) {
1161
991
  let best = null;
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
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)
1177
999
  };
1178
- bestDist = dist;
1179
- }
1180
- }
1000
+ if (!best || candidate.distance < best.distance) best = candidate;
1001
+ });
1002
+ });
1181
1003
  if (best) return best;
1182
1004
  return findNearestBridge(outer, hole);
1183
1005
  }
1184
1006
  function findNearestBridge(outer, hole) {
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
- };
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;
1208
1019
  }
1209
1020
  function isVisibleBridge(outer, hole, blockers, outerPoint, holePoint, outerIndex, holeIndex, epsilon) {
1210
1021
  if (distance(outerPoint, holePoint) <= epsilon) return false;
@@ -1371,14 +1182,13 @@ function resampleRingWithAnchors(ring, step, anchorIndices) {
1371
1182
  return result;
1372
1183
  }
1373
1184
  function resampleClosedPath(ring, step) {
1374
- const cumulative = buildCumulativeDistances(ring);
1375
- const perimeter = cumulative[ring.length];
1185
+ const perimeter = ringPerimeter(ring);
1376
1186
  if (perimeter <= 1e-9) return copyRing(ring);
1377
1187
  const pointCount = Math.max(3, Math.round(perimeter / step));
1378
1188
  if (pointCount >= ring.length) return copyRing(ring);
1379
1189
  const sampled = [];
1380
- for (let index = 0; index < pointCount; index++) {
1381
- const point = pointAtClosedPathDistanceFast(ring, cumulative, perimeter * index / pointCount);
1190
+ for (let index = 0; index < pointCount; index += 1) {
1191
+ const point = pointAtClosedPathDistance(ring, perimeter * index / pointCount);
1382
1192
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1383
1193
  }
1384
1194
  if (sampled.length < 3 || sampled.length >= ring.length) return copyRing(ring);
@@ -1386,13 +1196,12 @@ function resampleClosedPath(ring, step) {
1386
1196
  }
1387
1197
  function resampleOpenPath(path, step) {
1388
1198
  if (path.length <= 2) return path.map((point) => [point[0], point[1]]);
1389
- const cumulative = buildOpenCumulativeDistances(path);
1390
- const length = cumulative[path.length - 1];
1199
+ const length = polylineLength(path);
1391
1200
  if (length <= step) return [[path[0][0], path[0][1]], [path[path.length - 1][0], path[path.length - 1][1]]];
1392
1201
  const divisionCount = Math.max(1, Math.round(length / step));
1393
1202
  const sampled = [];
1394
- for (let index = 0; index <= divisionCount; index++) {
1395
- const point = pointAtOpenPathDistanceFast(path, cumulative, length * index / divisionCount);
1203
+ for (let index = 0; index <= divisionCount; index += 1) {
1204
+ const point = pointAtOpenPathDistance(path, length * index / divisionCount);
1396
1205
  if (sampled.length === 0 || !pointsAlmostEqual2D(sampled[sampled.length - 1], point, 1e-6)) sampled.push(point);
1397
1206
  }
1398
1207
  return sampled;
@@ -3764,25 +3573,9 @@ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3764
3573
  width: 0,
3765
3574
  hardBreak: false
3766
3575
  };
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;
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}`;
3786
3579
  return {
3787
3580
  ...line,
3788
3581
  text,
@@ -4319,6 +4112,12 @@ function getGraphemeSegmenter() {
4319
4112
  function trimTrailingWhitespace(value) {
4320
4113
  return value.replace(/\s+$/u, "");
4321
4114
  }
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
+ }
4322
4121
  function splitPreservingWhitespace(value) {
4323
4122
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
4324
4123
  }