Skip to content

Commit ca8dabc

Browse files
committed
paint-boolean-ops: format with Prettier 3.2.5 (matching CI)
1 parent aec01e3 commit ca8dabc

2 files changed

Lines changed: 121 additions & 76 deletions

File tree

addons/paint-boolean-ops/text-to-path.js

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,13 @@ export default class TextToPath {
4343
static #DEBUG = false;
4444

4545
static #rasterScale(bounds, nLines) {
46-
const lineHeight = bounds.height / Math.max(nLines, 1);
47-
const maxDim = Math.max(bounds.width, bounds.height);
46+
const lineHeight = bounds.height / Math.max(nLines, 1);
47+
const maxDim = Math.max(bounds.width, bounds.height);
4848
// Take the BETTER (max) of the two quality targets:
4949
// • overall: 2400px on the longest side — protects single-line / small text
5050
// • per-line: 800px per line height — protects wide multi-line paragraphs
5151
// Then clamp to the hard canvas cap.
52-
const scaleQuality = Math.max(
53-
this.#TARGET_PX / maxDim,
54-
this.#TARGET_PX_PER_LINE / lineHeight,
55-
);
52+
const scaleQuality = Math.max(this.#TARGET_PX / maxDim, this.#TARGET_PX_PER_LINE / lineHeight);
5653
return Math.min(scaleQuality, this.#MAX_CANVAS_PX / maxDim);
5754
}
5855

@@ -61,23 +58,44 @@ export default class TextToPath {
6158
// Edges: 0 = top, 1 = right, 2 = bottom, 3 = left
6259
// Value: [edgeA, edgeB] pair, "S5"/"S10" for saddle cases, or null.
6360
static #MS = [
64-
null, [3,2], [2,1], [3,1], // 0–3
65-
[0,1], "S5", [0,2], [3,0], // 4–7
66-
[0,3], [0,2], "S10", [0,1], // 8–11
67-
[1,3], [1,2], [2,3], null, // 12–15
61+
null,
62+
[3, 2],
63+
[2, 1],
64+
[3, 1], // 0–3
65+
[0, 1],
66+
"S5",
67+
[0, 2],
68+
[3, 0], // 4–7
69+
[0, 3],
70+
[0, 2],
71+
"S10",
72+
[0, 1], // 8–11
73+
[1, 3],
74+
[1, 2],
75+
[2, 3],
76+
null, // 12–15
6877
];
6978

7079
// Each saddle case produces two independent segments.
7180
// S5 = 0101: TR+BL inside → [top↔right] and [bottom↔left]
7281
// S10 = 1010: TL+BR inside → [top↔left] and [bottom↔right]
73-
static #SAD = { S5: [[0,1],[2,3]], S10: [[0,3],[2,1]] };
82+
static #SAD = {
83+
S5: [
84+
[0, 1],
85+
[2, 3],
86+
],
87+
S10: [
88+
[0, 3],
89+
[2, 1],
90+
],
91+
};
7492

7593
// Per exit-edge: [Δrow, Δcol, entry-edge-in-next-cell]
7694
static #MOV = [
77-
[-1, 0, 2], // 0 TOP → move up, enter via BOTTOM
78-
[ 0, 1, 3], // 1 RIGHT → move right, enter via LEFT
79-
[ 1, 0, 0], // 2 BOTTOM → move down, enter via TOP
80-
[ 0, -1, 1], // 3 LEFT → move left, enter via RIGHT
95+
[-1, 0, 2], // 0 TOP → move up, enter via BOTTOM
96+
[0, 1, 3], // 1 RIGHT → move right, enter via LEFT
97+
[1, 0, 0], // 2 BOTTOM → move down, enter via TOP
98+
[0, -1, 1], // 3 LEFT → move left, enter via RIGHT
8199
];
82100

83101
/**
@@ -123,8 +141,10 @@ export default class TextToPath {
123141

124142
// Coordinate mapping: pixel (px, py) → paper (ox + px/sx, oy + py/sy)
125143
// The canvas spans [bounds.x, bounds.x + bounds.width] × [...height]
126-
const ox = bounds.x, oy = bounds.y;
127-
const sx = cw / bounds.width, sy = ch / bounds.height;
144+
const ox = bounds.x,
145+
oy = bounds.y;
146+
const sx = cw / bounds.width,
147+
sy = ch / bounds.height;
128148

129149
// 2. Binary grid: 1 = inside (alpha above threshold) ─────────────────
130150
// Optional: pre-blur alpha with a separable 3×3 Gaussian (σ≈0.7px) before
@@ -149,7 +169,8 @@ export default class TextToPath {
149169
// Vertical pass: [1,2,1]/4 → combined σ≈0.7px
150170
blurredA = new Float32Array(cw * ch);
151171
for (let r = 0; r < ch; r++) {
152-
const rt = Math.max(r - 1, 0), rb = Math.min(r + 1, ch - 1);
172+
const rt = Math.max(r - 1, 0),
173+
rb = Math.min(r + 1, ch - 1);
153174
for (let c = 0; c < cw; c++) {
154175
blurredA[r * cw + c] = (tmpA[rt * cw + c] + 2 * tmpA[r * cw + c] + tmpA[rb * cw + c]) * 0.25;
155176
}
@@ -226,9 +247,12 @@ export default class TextToPath {
226247
const p0 = pts[(i - CORNER_SPAN + n) % n];
227248
const p1 = pts[i];
228249
const p2 = pts[(i + CORNER_SPAN) % n];
229-
const inDx = p1[0] - p0[0], inDy = p1[1] - p0[1];
230-
const outDx = p2[0] - p1[0], outDy = p2[1] - p1[1];
231-
const li = Math.hypot(inDx, inDy), lo = Math.hypot(outDx, outDy);
250+
const inDx = p1[0] - p0[0],
251+
inDy = p1[1] - p0[1];
252+
const outDx = p2[0] - p1[0],
253+
outDy = p2[1] - p1[1];
254+
const li = Math.hypot(inDx, inDy),
255+
lo = Math.hypot(outDx, outDy);
232256
if (li < 0.5 || lo < 0.5) continue;
233257
cornerDot[i] = (inDx * outDx + inDy * outDy) / (li * lo);
234258
}
@@ -256,7 +280,9 @@ export default class TextToPath {
256280
// Also check wrap-around: if last and first candidate are close
257281
if (cj === candidates.length && ci > 0) {
258282
const wrapGap = (candidates[0] + n - candidates[candidates.length - 1]) % n;
259-
if (wrapGap <= clusterGap) { break; } // all one cluster — no corners
283+
if (wrapGap <= clusterGap) {
284+
break;
285+
} // all one cluster — no corners
260286
}
261287
// Pick sharpest in this cluster
262288
let best = candidates[ci];
@@ -278,10 +304,12 @@ export default class TextToPath {
278304
}
279305
const denoised = pts.map((p, i) => {
280306
if (isProtected[i]) return p; // preserve original position near corners
281-
let ax = 0, ay = 0;
307+
let ax = 0,
308+
ay = 0;
282309
for (let j = -half; j <= half; j++) {
283310
const [x, y] = pts[(i + j + n) % n];
284-
ax += x; ay += y;
311+
ax += x;
312+
ay += y;
285313
}
286314
return [ax / SMOOTH_WIN, ay / SMOOTH_WIN];
287315
});
@@ -293,9 +321,14 @@ export default class TextToPath {
293321
if (this.#DEBUG) {
294322
// Compact corner summary: world-space positions so we can match against SVG coordinates.
295323
const contourIdx = contours.indexOf(pts);
296-
const cornerRows = cornerIdxs.map(ci => {
324+
const cornerRows = cornerIdxs.map((ci) => {
297325
const [rx, ry] = pts[ci];
298-
return { idx: ci, wx: (ox + rx / sx).toFixed(3), wy: (oy + ry / sy).toFixed(3), dot: cornerDot[ci].toFixed(3) };
326+
return {
327+
idx: ci,
328+
wx: (ox + rx / sx).toFixed(3),
329+
wy: (oy + ry / sy).toFixed(3),
330+
dot: cornerDot[ci].toFixed(3),
331+
};
299332
});
300333
console.log(`[TextToPath] contour ${contourIdx}: ${cornerRows.length} corners (world-space):`);
301334
console.table(cornerRows);
@@ -322,21 +355,23 @@ export default class TextToPath {
322355
// and the dilation zone) and measure direction over WINDOW indices.
323356
// SAMPLE must exceed half (the dilation half-width) — otherwise we
324357
// would sample unsmoothed protected points and get noisy directions.
325-
const SAMPLE = half + 4; // half + a few extra pts clear of dilation
358+
const SAMPLE = half + 4; // half + a few extra pts clear of dilation
326359
const WINDOW = 4;
327360
const correctedCorner = cornerIdxs.map((ics) => {
328-
const outPts = [], inPts = [];
329-
for (let d = SAMPLE; d < SAMPLE + WINDOW; d++)
330-
outPts.push(denoised[(ics + d + n) % n]);
331-
for (let d = -(SAMPLE + WINDOW); d < -SAMPLE; d++)
332-
inPts.push(denoised[(ics + d + n) % n]);
361+
const outPts = [],
362+
inPts = [];
363+
for (let d = SAMPLE; d < SAMPLE + WINDOW; d++) outPts.push(denoised[(ics + d + n) % n]);
364+
for (let d = -(SAMPLE + WINDOW); d < -SAMPLE; d++) inPts.push(denoised[(ics + d + n) % n]);
333365

334366
// Direction vectors (endpoint→endpoint over the window)
335-
const odx = outPts[WINDOW-1][0] - outPts[0][0], ody = outPts[WINDOW-1][1] - outPts[0][1];
336-
const idx = inPts[WINDOW-1][0] - inPts[0][0], idy = inPts[WINDOW-1][1] - inPts[0][1];
367+
const odx = outPts[WINDOW - 1][0] - outPts[0][0],
368+
ody = outPts[WINDOW - 1][1] - outPts[0][1];
369+
const idx = inPts[WINDOW - 1][0] - inPts[0][0],
370+
idy = inPts[WINDOW - 1][1] - inPts[0][1];
337371
const cross = idx * ody - idy * odx;
338372
if (Math.abs(cross) < 0.01) return null; // parallel → keep original
339-
const dx = outPts[0][0] - inPts[0][0], dy = outPts[0][1] - inPts[0][1];
373+
const dx = outPts[0][0] - inPts[0][0],
374+
dy = outPts[0][1] - inPts[0][1];
340375
const t = (dx * ody - dy * odx) / cross;
341376
return [inPts[0][0] + idx * t, inPts[0][1] + idy * t];
342377
});
@@ -350,8 +385,8 @@ export default class TextToPath {
350385
const CORNER_SKIP_PX = 1.5;
351386
const arcPaths = cornerIdxs.map((iStart, ci) => {
352387
const iEnd = cornerIdxs[(ci + 1) % nc3];
353-
const cs = correctedCorner[ci] ?? denoised[iStart];
354-
const ce = correctedCorner[(ci+1)%nc3] ?? denoised[iEnd];
388+
const cs = correctedCorner[ci] ?? denoised[iStart];
389+
const ce = correctedCorner[(ci + 1) % nc3] ?? denoised[iEnd];
355390

356391
const arcPts = [cs];
357392
let k = (iStart + 1) % n;
@@ -373,14 +408,14 @@ export default class TextToPath {
373408
// If all interior points are within tolerance of the start→end line,
374409
// this arc is straight — bypass simplify() entirely (yields exactly
375410
// 2 segments, zero handles, guaranteed straight line after offset).
376-
const [ax, ay] = cs, [bx, by] = ce;
411+
const [ax, ay] = cs,
412+
[bx, by] = ce;
377413
const abLen = Math.hypot(bx - ax, by - ay);
378414
let maxDev = 0;
379415
if (abLen > 0.01) {
380416
for (let j = 1; j < arcPts.length - 1; j++) {
381417
const [px, py] = arcPts[j];
382-
maxDev = Math.max(maxDev,
383-
Math.abs((px - ax) * (by - ay) - (py - ay) * (bx - ax)) / abLen);
418+
maxDev = Math.max(maxDev, Math.abs((px - ax) * (by - ay) - (py - ay) * (bx - ax)) / abLen);
384419
}
385420
}
386421
let arc;
@@ -407,24 +442,25 @@ export default class TextToPath {
407442
// to arcSegs[last-1].handleOut approaching the next cusp.
408443
for (let s = 1; s < last; s++) {
409444
const seg = arcSegs[s];
410-
const hi = s === 1 ? new paper.Point(0, 0) : seg.handleIn.clone();
411-
const ho = s === last-1 ? new paper.Point(0, 0) : seg.handleOut.clone();
445+
const hi = s === 1 ? new paper.Point(0, 0) : seg.handleIn.clone();
446+
const ho = s === last - 1 ? new paper.Point(0, 0) : seg.handleOut.clone();
412447
path.add(new paper.Segment(seg.point.clone(), hi, ho));
413448
}
414449

415450
if (this.#DEBUG) {
416451
// Show what simplify() produced for this arc before we override handles.
417452
console.groupCollapsed(
418-
` arc ${ci} (corner idx=${cornerIdxs[ci]}${cornerIdxs[(ci+1)%nc3]}): ` +
419-
`${arcPaths[ci].segments.length} raw simplify segs → ${last} used`
453+
` arc ${ci} (corner idx=${cornerIdxs[ci]}${cornerIdxs[(ci + 1) % nc3]}): ` +
454+
`${arcPaths[ci].segments.length} raw simplify segs → ${last} used`
420455
);
421456
arcPaths[ci].segments.forEach((seg, s) => {
422-
const hi = seg.handleIn, ho = seg.handleOut;
457+
const hi = seg.handleIn,
458+
ho = seg.handleOut;
423459
console.log(
424460
` seg[${s}] pt=(${seg.point.x.toFixed(2)},${seg.point.y.toFixed(2)})` +
425-
` hIn=(${hi.x.toFixed(2)},${hi.y.toFixed(2)})` +
426-
` hOut=(${ho.x.toFixed(2)},${ho.y.toFixed(2)})` +
427-
(s===0 || s===last ? " ← endpoint (handles zeroed)" : "")
461+
` hIn=(${hi.x.toFixed(2)},${hi.y.toFixed(2)})` +
462+
` hOut=(${ho.x.toFixed(2)},${ho.y.toFixed(2)})` +
463+
(s === 0 || s === last ? " ← endpoint (handles zeroed)" : "")
428464
);
429465
});
430466
console.groupEnd();
@@ -442,14 +478,14 @@ export default class TextToPath {
442478
} else {
443479
const simp = this.#rdp(denoised, this.#EPS);
444480
if (simp.length < 3) continue;
445-
path = new paper.Path(
446-
simp.map(([px, py]) => new paper.Point(ox + px / sx, oy + py / sy))
447-
);
481+
path = new paper.Path(simp.map(([px, py]) => new paper.Point(ox + px / sx, oy + py / sy)));
448482
path.closed = true;
449483
}
450484

451-
if (path.segments.length >= 3) { paths.push(path); cornerCounts.push(nCorners); }
452-
else path.remove();
485+
if (path.segments.length >= 3) {
486+
paths.push(path);
487+
cornerCounts.push(nCorners);
488+
} else path.remove();
453489

454490
if (this.#DEBUG && path.segments.length >= 3) {
455491
// Final assembled path: print every segment's handle lengths.
@@ -460,8 +496,8 @@ export default class TextToPath {
460496
s,
461497
x: seg.point.x.toFixed(3),
462498
y: seg.point.y.toFixed(3),
463-
"hIn": seg.handleIn.length.toFixed(4),
464-
"hOut": seg.handleOut.length.toFixed(4),
499+
hIn: seg.handleIn.length.toFixed(4),
500+
hOut: seg.handleOut.length.toFixed(4),
465501
}));
466502
console.log(`[TextToPath] contour ${contours.indexOf(pts)} FINAL path (${segs.length} segs, world-space):`);
467503
console.table(segRows);
@@ -489,10 +525,10 @@ export default class TextToPath {
489525

490526
// 4-bit case for cell (r, c): corners TL, TR, BR, BL
491527
const cellCase = (r, c) =>
492-
(grid[ r * w + c ] << 3) |
493-
(grid[ r * w + (c+1)] << 2) |
494-
(grid[(r + 1) * w + (c+1)] << 1) |
495-
grid[(r + 1) * w + c ];
528+
(grid[r * w + c] << 3) |
529+
(grid[r * w + (c + 1)] << 2) |
530+
(grid[(r + 1) * w + (c + 1)] << 1) |
531+
grid[(r + 1) * w + c];
496532

497533
// Segment list for a cell case
498534
const segsOf = (ci) => {
@@ -504,10 +540,10 @@ export default class TextToPath {
504540

505541
// Floating-point edge midpoint in raster-pixel space
506542
const edgePt = (r, c, e) => {
507-
if (e === 0) return [c + 0.5, r ]; // top
508-
if (e === 1) return [c + 1, r + 0.5]; // right
509-
if (e === 2) return [c + 0.5, r + 1 ]; // bottom
510-
/* e===3 */ return [c, r + 0.5]; // left
543+
if (e === 0) return [c + 0.5, r]; // top
544+
if (e === 1) return [c + 1, r + 0.5]; // right
545+
if (e === 2) return [c + 0.5, r + 1]; // bottom
546+
/* e===3 */ return [c, r + 0.5]; // left
511547
};
512548

513549
for (let r0 = 0; r0 < gh; r0++) {
@@ -523,23 +559,28 @@ export default class TextToPath {
523559
// edge), so the first exit will naturally be segs[si][0].
524560
const [startE0, startE1] = segs[si];
525561
const pts = [];
526-
let r = r0, c = c0, entry = startE1;
562+
let r = r0,
563+
c = c0,
564+
entry = startE1;
527565
const MAX = (gw * gh + 4) * 2; // safety upper bound
528566

529567
for (let iter = 0; iter < MAX; iter++) {
530568
const vi = r * gw + c;
531569
const cs = segsOf(cellCase(r, c));
532570

533571
// Find the segment that contains the entry edge
534-
let seg = null, bit = 0;
572+
let seg = null,
573+
bit = 0;
535574
for (let bi = 0; bi < cs.length; bi++) {
536575
if (cs[bi][0] === entry || cs[bi][1] === entry) {
537-
seg = cs[bi]; bit = bi; break;
576+
seg = cs[bi];
577+
bit = bi;
578+
break;
538579
}
539580
}
540581
if (!seg) break; // degenerate / disconnected
541582

542-
vis[vi] |= (1 << bit);
583+
vis[vi] |= 1 << bit;
543584

544585
// Exit via the other edge of this segment
545586
const exit = seg[0] === entry ? seg[1] : seg[0];
@@ -554,16 +595,18 @@ export default class TextToPath {
554595
if ((entry + exit) % 2 === 1) {
555596
// The shared corner's X is c+1 if either edge is the right edge (1).
556597
// The shared corner's Y is r+1 if either edge is the bottom edge (2).
557-
const cx = (entry === 1 || exit === 1) ? c + 1 : c;
558-
const cy = (entry === 2 || exit === 2) ? r + 1 : r;
598+
const cx = entry === 1 || exit === 1 ? c + 1 : c;
599+
const cy = entry === 2 || exit === 2 ? r + 1 : r;
559600
pts.push([cx, cy]);
560601
}
561602

562603
pts.push(edgePt(r, c, exit));
563604

564605
// Step to the adjacent cell
565606
const [dr, dc, nextEntry] = this.#MOV[exit];
566-
r += dr; c += dc; entry = nextEntry;
607+
r += dr;
608+
c += dc;
609+
entry = nextEntry;
567610

568611
// Off-grid → open contour, stop here
569612
if (r < 0 || r >= gh || c < 0 || c >= gw) break;
@@ -588,14 +631,18 @@ export default class TextToPath {
588631
const [x1, y1] = pts[0];
589632
const [x2, y2] = pts[pts.length - 1];
590633
const len = Math.hypot(x2 - x1, y2 - y1);
591-
let maxD = 0, maxI = 0;
634+
let maxD = 0,
635+
maxI = 0;
592636
for (let i = 1; i < pts.length - 1; i++) {
593637
const [px, py] = pts[i];
594638
const d =
595639
len < 1e-10
596640
? Math.hypot(px - x1, py - y1)
597641
: Math.abs((y2 - y1) * px - (x2 - x1) * py + x2 * y1 - y2 * x1) / len;
598-
if (d > maxD) { maxD = d; maxI = i; }
642+
if (d > maxD) {
643+
maxD = d;
644+
maxI = i;
645+
}
599646
}
600647
if (maxD > eps) {
601648
const L = this.#rdp(pts.slice(0, maxI + 1), eps);

0 commit comments

Comments
 (0)