@pixerate/editor 0.36.0 → 0.38.0
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/THIRD_PARTY_NOTICES.md +17 -0
- package/dist/canvas/index.cjs +37 -25
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +3 -217
- package/dist/canvas/index.d.ts +3 -217
- package/dist/canvas/index.js +13 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-BGNPT5PE.cjs → chunk-7EDLVBOO.cjs} +671 -19
- package/dist/chunk-7EDLVBOO.cjs.map +1 -0
- package/dist/{chunk-EGPIVUEF.js → chunk-ZCCU3VNI.js} +668 -16
- package/dist/chunk-ZCCU3VNI.js.map +1 -0
- package/dist/pathfinder-Bd87_lsQ.d.cts +994 -0
- package/dist/pathfinder-fLmmFOJw.d.ts +994 -0
- package/dist/vector/index.cjs +1900 -297
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +620 -8
- package/dist/vector/index.d.ts +620 -8
- package/dist/vector/index.js +1696 -93
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/bezier-BvqVmWMg.d.ts +0 -547
- package/dist/bezier-D8bsO-L0.d.cts +0 -547
- package/dist/chunk-BGNPT5PE.cjs.map +0 -1
- package/dist/chunk-EGPIVUEF.js.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
HistoryManager
|
|
3
3
|
} from "../chunk-QVZZGT4X.js";
|
|
4
4
|
import {
|
|
5
|
+
PlanarMap,
|
|
5
6
|
bezierPathBounds,
|
|
6
7
|
bezierPathToSvg,
|
|
7
8
|
closeContour,
|
|
@@ -10,13 +11,17 @@ import {
|
|
|
10
11
|
distanceToBezierPath,
|
|
11
12
|
fitCubicWithTangents,
|
|
12
13
|
fitCubics,
|
|
14
|
+
flattenChain,
|
|
13
15
|
formatNumber,
|
|
14
16
|
gradientToSvgDef,
|
|
17
|
+
isStraightCubic,
|
|
15
18
|
normalizeBezierPath,
|
|
16
19
|
outlineStroke,
|
|
17
20
|
pathBoolean,
|
|
18
21
|
pathToContours,
|
|
22
|
+
pathfinder,
|
|
19
23
|
pointInBezierPath,
|
|
24
|
+
polygonBoolean,
|
|
20
25
|
segmentBounds,
|
|
21
26
|
segmentDerivative,
|
|
22
27
|
segmentIntersections,
|
|
@@ -31,13 +36,30 @@ import {
|
|
|
31
36
|
transformBezierPath,
|
|
32
37
|
trimSegments,
|
|
33
38
|
windingNumber
|
|
34
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-ZCCU3VNI.js";
|
|
35
40
|
|
|
36
41
|
// src/vector/types.ts
|
|
37
42
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
38
43
|
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
39
|
-
var VECTOR_TOOLS = [
|
|
44
|
+
var VECTOR_TOOLS = [
|
|
45
|
+
"select",
|
|
46
|
+
"direct",
|
|
47
|
+
"pen",
|
|
48
|
+
"pencil",
|
|
49
|
+
"rect",
|
|
50
|
+
"ellipse",
|
|
51
|
+
"polygon",
|
|
52
|
+
"star",
|
|
53
|
+
"line",
|
|
54
|
+
"scissors",
|
|
55
|
+
"knife",
|
|
56
|
+
"eraser",
|
|
57
|
+
"width",
|
|
58
|
+
"shapeBuilder",
|
|
59
|
+
"paintBucket"
|
|
60
|
+
];
|
|
40
61
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
62
|
+
var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
|
|
41
63
|
|
|
42
64
|
// src/vector/corners.ts
|
|
43
65
|
var EPS = 1e-9;
|
|
@@ -245,6 +267,535 @@ function setSubpathCorners(sp, radius, kind) {
|
|
|
245
267
|
return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
|
|
246
268
|
}
|
|
247
269
|
|
|
270
|
+
// src/vector/width.ts
|
|
271
|
+
var VECTOR_WIDTH_PROFILE_MAX_POINTS = 64;
|
|
272
|
+
var VECTOR_WIDTH_PROFILE_MAX_SCALE = 1e3;
|
|
273
|
+
var VECTOR_WIDTH_SAME_T = 1e-6;
|
|
274
|
+
var sym = (t, s) => ({ t, left: s, right: s });
|
|
275
|
+
var VECTOR_WIDTH_PRESETS = [
|
|
276
|
+
{ id: "uniform", label: "Uniform", points: [] },
|
|
277
|
+
{ id: "taperStart", label: "Taper start", points: [sym(0, 0), sym(1, 1)] },
|
|
278
|
+
{ id: "taperEnd", label: "Taper end", points: [sym(0, 1), sym(1, 0)] },
|
|
279
|
+
{ id: "taperBoth", label: "Taper both", points: [sym(0, 0), sym(0.5, 1), sym(1, 0)] },
|
|
280
|
+
// Full width at the ends, swelling to twice the width in the middle.
|
|
281
|
+
{ id: "bulge", label: "Bulge", points: [sym(0, 1), sym(0.5, 2), sym(1, 1)] },
|
|
282
|
+
// Full width at the ends, pinched to a quarter in the middle.
|
|
283
|
+
{ id: "pinch", label: "Pinch", points: [sym(0, 1), sym(0.5, 0.25), sym(1, 1)] },
|
|
284
|
+
// A round head a fifth of the way along, then a long taper to the end.
|
|
285
|
+
{ id: "teardrop", label: "Teardrop", points: [sym(0, 0), sym(0.2, 1), sym(1, 0)] },
|
|
286
|
+
// Two swells between narrow necks.
|
|
287
|
+
{ id: "wave", label: "Wave", points: [sym(0, 0.3), sym(0.25, 1), sym(0.5, 0.3), sym(0.75, 1), sym(1, 0.3)] },
|
|
288
|
+
// A pen stroke: a quick press, a slow lift.
|
|
289
|
+
{ id: "pressure", label: "Pressure", points: [sym(0, 0.15), sym(0.12, 0.85), sym(0.45, 1), sym(0.8, 0.7), sym(1, 0.05)] }
|
|
290
|
+
];
|
|
291
|
+
var VECTOR_WIDTH_PRESET_IDS = VECTOR_WIDTH_PRESETS.map((p) => p.id);
|
|
292
|
+
var clean = (v2, max) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(Math.max(v2, 0), max) : null;
|
|
293
|
+
function sanitizeWidthProfile(points) {
|
|
294
|
+
if (!Array.isArray(points)) return [];
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const p of points) {
|
|
297
|
+
if (!p || typeof p !== "object") continue;
|
|
298
|
+
const t = clean(p.t, 1);
|
|
299
|
+
const l = clean(p.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
300
|
+
const r = clean(p.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
301
|
+
if (t === null || l === null && r === null) continue;
|
|
302
|
+
out.push({ t, left: l ?? r, right: r ?? l });
|
|
303
|
+
if (out.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) break;
|
|
304
|
+
}
|
|
305
|
+
return out.map((p, i) => [p, i]).sort((a, b) => a[0].t - b[0].t || a[1] - b[1]).map(([p]) => p);
|
|
306
|
+
}
|
|
307
|
+
function isUniformWidthProfile(points) {
|
|
308
|
+
return !points?.length || points.every((p) => p.left === 1 && p.right === 1);
|
|
309
|
+
}
|
|
310
|
+
function secant(pts, i, k) {
|
|
311
|
+
const a = pts[i];
|
|
312
|
+
const b = pts[i + 1];
|
|
313
|
+
return a && b && b.t - a.t > 1e-12 ? (b[k] - a[k]) / (b.t - a.t) : 0;
|
|
314
|
+
}
|
|
315
|
+
function tangent(pts, i, k) {
|
|
316
|
+
const last = pts.length - 1;
|
|
317
|
+
if (i <= 0) return secant(pts, 0, k);
|
|
318
|
+
if (i >= last) return secant(pts, last - 1, k);
|
|
319
|
+
const d0 = secant(pts, i - 1, k);
|
|
320
|
+
const d1 = secant(pts, i, k);
|
|
321
|
+
return d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
|
|
322
|
+
}
|
|
323
|
+
function interp(pts, t, k) {
|
|
324
|
+
const first = pts[0];
|
|
325
|
+
const last = pts[pts.length - 1];
|
|
326
|
+
if (!(t > first.t)) return first[k];
|
|
327
|
+
if (t >= last.t) return last[k];
|
|
328
|
+
let i = 0;
|
|
329
|
+
while (i + 1 < pts.length && pts[i + 1].t <= t) i++;
|
|
330
|
+
const a = pts[i];
|
|
331
|
+
const b = pts[i + 1];
|
|
332
|
+
const h = b.t - a.t;
|
|
333
|
+
if (h <= 1e-12) return b[k];
|
|
334
|
+
const u = (t - a.t) / h;
|
|
335
|
+
const u2 = u * u;
|
|
336
|
+
const u3 = u2 * u;
|
|
337
|
+
const w = (2 * u3 - 3 * u2 + 1) * a[k] + (u3 - 2 * u2 + u) * h * tangent(pts, i, k) + (3 * u2 - 2 * u3) * b[k] + (u3 - u2) * h * tangent(pts, i + 1, k);
|
|
338
|
+
return Math.max(0, w);
|
|
339
|
+
}
|
|
340
|
+
function widthProfileAt(points, t) {
|
|
341
|
+
if (!points?.length) return { left: 1, right: 1 };
|
|
342
|
+
return { left: interp(points, t, "left"), right: interp(points, t, "right") };
|
|
343
|
+
}
|
|
344
|
+
function widthProfileMaxScale(points) {
|
|
345
|
+
if (!points?.length) return 1;
|
|
346
|
+
return points.reduce((m, p) => Math.max(m, p.left, p.right), 0);
|
|
347
|
+
}
|
|
348
|
+
function widthProfilePreset(id) {
|
|
349
|
+
const p = VECTOR_WIDTH_PRESETS.find((q) => q.id === id);
|
|
350
|
+
return p ? p.points.map((q) => ({ ...q })) : null;
|
|
351
|
+
}
|
|
352
|
+
function widthProfilePresetId(points) {
|
|
353
|
+
if (isUniformWidthProfile(points)) return "uniform";
|
|
354
|
+
const same = (a, b) => a.length === b.length && a.every((p, i) => Math.abs(p.t - b[i].t) < 1e-9 && Math.abs(p.left - b[i].left) < 1e-9 && Math.abs(p.right - b[i].right) < 1e-9);
|
|
355
|
+
return VECTOR_WIDTH_PRESETS.find((p) => p.points.length && same(points, p.points))?.id ?? "custom";
|
|
356
|
+
}
|
|
357
|
+
function reverseWidthProfile(points) {
|
|
358
|
+
return points.map((p) => ({ t: 1 - p.t, left: p.right, right: p.left })).reverse();
|
|
359
|
+
}
|
|
360
|
+
function mirrorWidthProfile(points) {
|
|
361
|
+
return points.map((p) => ({ t: p.t, left: p.right, right: p.left }));
|
|
362
|
+
}
|
|
363
|
+
function editable(points) {
|
|
364
|
+
return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
|
|
365
|
+
}
|
|
366
|
+
function insertWidthPoint(points, t, sides2 = {}, options = {}) {
|
|
367
|
+
const pts = editable(points);
|
|
368
|
+
const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
|
|
369
|
+
const here = widthProfileAt(points?.length ? points : null, tt);
|
|
370
|
+
const p = {
|
|
371
|
+
t: tt,
|
|
372
|
+
left: clean(sides2.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
|
|
373
|
+
right: clean(sides2.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
|
|
374
|
+
};
|
|
375
|
+
const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
|
|
376
|
+
if (same >= 0 && !options.step) {
|
|
377
|
+
pts[same] = { ...p, t: pts[same].t };
|
|
378
|
+
return { points: pts, index: same };
|
|
379
|
+
}
|
|
380
|
+
let i = 0;
|
|
381
|
+
while (i < pts.length && pts[i].t <= tt) i++;
|
|
382
|
+
if (pts.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { points: pts, index: Math.max(0, i - 1) };
|
|
383
|
+
pts.splice(i, 0, p);
|
|
384
|
+
return { points: pts, index: i };
|
|
385
|
+
}
|
|
386
|
+
function setWidthPoint(points, index, patch) {
|
|
387
|
+
const pts = points.map((p2) => ({ ...p2 }));
|
|
388
|
+
const p = pts[index];
|
|
389
|
+
if (!p) return pts;
|
|
390
|
+
if (typeof patch.t === "number" && Number.isFinite(patch.t)) {
|
|
391
|
+
const lo = index > 0 ? pts[index - 1].t : 0;
|
|
392
|
+
const hi = index < pts.length - 1 ? pts[index + 1].t : 1;
|
|
393
|
+
p.t = Math.min(Math.max(patch.t, lo), Math.max(hi, lo));
|
|
394
|
+
}
|
|
395
|
+
const l = clean(patch.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
396
|
+
const r = clean(patch.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
397
|
+
if (l !== null) p.left = l;
|
|
398
|
+
if (r !== null) p.right = r;
|
|
399
|
+
return pts;
|
|
400
|
+
}
|
|
401
|
+
function removeWidthPoints(points, indices) {
|
|
402
|
+
const drop = new Set(indices);
|
|
403
|
+
return points.filter((_, i) => !drop.has(i)).map((p) => ({ ...p }));
|
|
404
|
+
}
|
|
405
|
+
function ease(u, e) {
|
|
406
|
+
const x = Math.min(Math.max(u, 0), 1);
|
|
407
|
+
const round = Math.sqrt(Math.max(0, 1 - (1 - x) * (1 - x)));
|
|
408
|
+
return x + (round - x) * Math.min(Math.max(e, 0), 1);
|
|
409
|
+
}
|
|
410
|
+
function taperWaveFactor(t, taper, wave) {
|
|
411
|
+
let k = 1;
|
|
412
|
+
if (taper) {
|
|
413
|
+
let ls = Math.max(0, taper.startLength ?? 0);
|
|
414
|
+
let le = Math.max(0, taper.endLength ?? 0);
|
|
415
|
+
if (ls + le > 1) {
|
|
416
|
+
const f = 1 / (ls + le);
|
|
417
|
+
ls *= f;
|
|
418
|
+
le *= f;
|
|
419
|
+
}
|
|
420
|
+
const sw = Math.min(Math.max(taper.startWidth ?? 0, 0), 1);
|
|
421
|
+
const ew = Math.min(Math.max(taper.endWidth ?? 0, 0), 1);
|
|
422
|
+
if (ls > 0 && t < ls) k = Math.min(k, sw + (1 - sw) * ease(t / ls, taper.startEase ?? 0));
|
|
423
|
+
if (le > 0 && t > 1 - le) k = Math.min(k, ew + (1 - ew) * ease((1 - t) / le, taper.endEase ?? 0));
|
|
424
|
+
}
|
|
425
|
+
if (wave) {
|
|
426
|
+
const cycles = wave.cycles ?? 4;
|
|
427
|
+
const amount = Math.min(Math.max(wave.amount ?? 0.5, 0), 1);
|
|
428
|
+
if (cycles > 0 && amount > 0) {
|
|
429
|
+
const ph = 2 * Math.PI * t * cycles + (wave.phase ?? 0) * Math.PI / 180;
|
|
430
|
+
k *= 1 - amount * (0.5 - 0.5 * Math.cos(ph));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return k;
|
|
434
|
+
}
|
|
435
|
+
function generateWidthProfile(options) {
|
|
436
|
+
const cycles = options.wave ? Math.max(0, options.wave.cycles ?? 4) : 0;
|
|
437
|
+
const n = Math.max(8, Math.min(VECTOR_WIDTH_PROFILE_MAX_POINTS * 4, options.samples ?? Math.ceil(32 + cycles * 16)));
|
|
438
|
+
const raw = [];
|
|
439
|
+
for (let i = 0; i <= n; i++) raw.push([i / n, taperWaveFactor(i / n, options.taper, options.wave)]);
|
|
440
|
+
const ts = [options.taper?.startLength, options.taper?.endLength !== void 0 ? 1 - options.taper.endLength : void 0];
|
|
441
|
+
for (const t of ts) if (typeof t === "number" && t > 0 && t < 1) raw.push([t, taperWaveFactor(t, options.taper, options.wave)]);
|
|
442
|
+
raw.sort((a, b) => a[0] - b[0]);
|
|
443
|
+
const kept = simplifySamples(raw, options.tolerance ?? 0.01, VECTOR_WIDTH_PROFILE_MAX_POINTS);
|
|
444
|
+
return kept.map(([t, k]) => sym(t, k));
|
|
445
|
+
}
|
|
446
|
+
function simplifySamples(samples, tolerance, max) {
|
|
447
|
+
if (samples.length <= 2) return samples.slice();
|
|
448
|
+
const keep = new Array(samples.length).fill(false);
|
|
449
|
+
keep[0] = keep[samples.length - 1] = true;
|
|
450
|
+
const stack = [[0, samples.length - 1]];
|
|
451
|
+
while (stack.length) {
|
|
452
|
+
const [a, b] = stack.pop();
|
|
453
|
+
const [ta, va] = samples[a];
|
|
454
|
+
const [tb, vb] = samples[b];
|
|
455
|
+
let worst = -1;
|
|
456
|
+
let err = tolerance;
|
|
457
|
+
for (let i = a + 1; i < b; i++) {
|
|
458
|
+
const [t, v2] = samples[i];
|
|
459
|
+
const lin = tb - ta > 1e-12 ? va + (vb - va) * (t - ta) / (tb - ta) : va;
|
|
460
|
+
const e = Math.abs(v2 - lin);
|
|
461
|
+
if (e > err) {
|
|
462
|
+
err = e;
|
|
463
|
+
worst = i;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (worst >= 0) {
|
|
467
|
+
keep[worst] = true;
|
|
468
|
+
stack.push([a, worst], [worst, b]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
let out = samples.filter((_, i) => keep[i]);
|
|
472
|
+
while (out.length > max) out = out.filter((_, i) => i === 0 || i === out.length - 1 || i % 2 === 0);
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
var VECTOR_PRESSURE_NOMINAL = 0.5;
|
|
476
|
+
function pressureWidthProfile(samples, options = {}) {
|
|
477
|
+
const raw = samples.filter((s) => Number.isFinite(s.t) && Number.isFinite(s.pressure)).map((s) => [Math.min(Math.max(s.t, 0), 1), Math.max(0.05, Math.min(1, Math.max(0, s.pressure)) / VECTOR_PRESSURE_NOMINAL)]);
|
|
478
|
+
if (raw.length < 2) return [];
|
|
479
|
+
if (raw.every((s) => Math.abs(s[1] - raw[0][1]) < 1e-6)) return [];
|
|
480
|
+
return simplifySamples(raw, options.tolerance ?? 0.02, VECTOR_WIDTH_PROFILE_MAX_POINTS).map(([t, k]) => sym(t, Math.round(k * 1e3) / 1e3));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/vector/varstroke.ts
|
|
484
|
+
var SAMPLES_PER_SUBPATH = 128;
|
|
485
|
+
var MAX_DASHES = 5e3;
|
|
486
|
+
var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
|
|
487
|
+
var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
|
|
488
|
+
var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
|
|
489
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
490
|
+
var unitOf = (v2) => {
|
|
491
|
+
const l = Math.hypot(v2[0], v2[1]);
|
|
492
|
+
return l > 0 ? [v2[0] / l, v2[1] / l] : [1, 0];
|
|
493
|
+
};
|
|
494
|
+
var leftNormal = (d) => [d[1], -d[0]];
|
|
495
|
+
function flatten(segments, closed, tol) {
|
|
496
|
+
const pts = [];
|
|
497
|
+
const corner = [];
|
|
498
|
+
const push = (p, isCorner) => {
|
|
499
|
+
const last = pts[pts.length - 1];
|
|
500
|
+
if (last && dist(last, p) <= 1e-9) {
|
|
501
|
+
corner[corner.length - 1] ||= isCorner;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
pts.push(p);
|
|
505
|
+
corner.push(isCorner);
|
|
506
|
+
};
|
|
507
|
+
for (const seg of segments) {
|
|
508
|
+
const flat = flattenChain([seg], tol);
|
|
509
|
+
if (dist(seg[0], seg[seg.length - 1]) <= 1e-12 && flat.length > 1) flat.push(seg[seg.length - 1]);
|
|
510
|
+
flat.forEach((p, i) => push(p, i === 0 || i === flat.length - 1));
|
|
511
|
+
}
|
|
512
|
+
if (closed && pts.length > 2 && dist(pts[0], pts[pts.length - 1]) > 1e-9) push(pts[0], true);
|
|
513
|
+
if (pts.length < 2) return null;
|
|
514
|
+
let total = 0;
|
|
515
|
+
for (let i = 1; i < pts.length; i++) total += dist(pts[i - 1], pts[i]);
|
|
516
|
+
if (!(total > 0)) return null;
|
|
517
|
+
const step = Math.max(total / SAMPLES_PER_SUBPATH, tol);
|
|
518
|
+
const fine = [pts[0]];
|
|
519
|
+
const fineCorner = [corner[0]];
|
|
520
|
+
for (let i = 1; i < pts.length; i++) {
|
|
521
|
+
const n = Math.max(1, Math.ceil(dist(pts[i - 1], pts[i]) / step));
|
|
522
|
+
for (let k = 1; k <= n; k++) {
|
|
523
|
+
fine.push(k === n ? pts[i] : lerp(pts[i - 1], pts[i], k / n));
|
|
524
|
+
fineCorner.push(k === n ? corner[i] : false);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const s = [0];
|
|
528
|
+
for (let i = 1; i < fine.length; i++) s.push(s[i - 1] + dist(fine[i - 1], fine[i]));
|
|
529
|
+
return { pts: fine, s, corner: fineCorner, closed };
|
|
530
|
+
}
|
|
531
|
+
function dashIntervals(pattern, offset, len2) {
|
|
532
|
+
let pat = pattern.map((v2) => Number.isFinite(v2) ? Math.max(0, v2) : 0);
|
|
533
|
+
if (pat.length % 2 === 1) pat = [...pat, ...pat];
|
|
534
|
+
const period = pat.reduce((a, b) => a + b, 0);
|
|
535
|
+
if (pat.length < 2 || !(period > 1e-9) || !(len2 > 0)) return [[0, len2]];
|
|
536
|
+
let pos = ((Number.isFinite(offset) ? offset : 0) % period + period) % period;
|
|
537
|
+
let idx = 0;
|
|
538
|
+
while (pos >= pat[idx]) {
|
|
539
|
+
pos -= pat[idx];
|
|
540
|
+
idx = (idx + 1) % pat.length;
|
|
541
|
+
}
|
|
542
|
+
const out = [];
|
|
543
|
+
let s = 0;
|
|
544
|
+
let guard = 0;
|
|
545
|
+
while (s < len2 && guard++ < MAX_DASHES * pat.length) {
|
|
546
|
+
const e = Math.min(s + pat[idx] - pos, len2);
|
|
547
|
+
if (idx % 2 === 0 && e >= s) out.push([s, e]);
|
|
548
|
+
s += pat[idx] - pos;
|
|
549
|
+
pos = 0;
|
|
550
|
+
idx = (idx + 1) % pat.length;
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
function at(poly, t) {
|
|
555
|
+
let i = 1;
|
|
556
|
+
while (i < poly.pts.length - 1 && poly.s[i] <= t) i++;
|
|
557
|
+
const a = poly.s[i - 1];
|
|
558
|
+
const b = poly.s[i];
|
|
559
|
+
const u = b > a ? Math.min(Math.max((t - a) / (b - a), 0), 1) : 0;
|
|
560
|
+
return [lerp(poly.pts[i - 1], poly.pts[i], u), i];
|
|
561
|
+
}
|
|
562
|
+
function arcPoints(c, a0, sweep, r0, r1, tol) {
|
|
563
|
+
const r = Math.max(r0, r1);
|
|
564
|
+
if (!(r > tol * 0.5) || !Number.isFinite(sweep)) return [];
|
|
565
|
+
const stepAngle = 2 * Math.acos(Math.min(Math.max(1 - tol / r, -1), 1));
|
|
566
|
+
const n = Math.min(128, Math.max(2, Math.ceil(Math.abs(sweep) / Math.max(stepAngle, 1e-3))));
|
|
567
|
+
const out = [];
|
|
568
|
+
for (let k = 1; k < n; k++) {
|
|
569
|
+
const f = k / n;
|
|
570
|
+
const a = a0 + sweep * f;
|
|
571
|
+
const rr = r0 + (r1 - r0) * f;
|
|
572
|
+
out.push([c[0] + Math.cos(a) * rr, c[1] + Math.sin(a) * rr]);
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
function crossing(a0, a1, b0, b1) {
|
|
577
|
+
const r = sub2(a1, a0);
|
|
578
|
+
const q = sub2(b1, b0);
|
|
579
|
+
const den = r[0] * q[1] - r[1] * q[0];
|
|
580
|
+
if (Math.abs(den) < 1e-12) return null;
|
|
581
|
+
const w = sub2(b0, a0);
|
|
582
|
+
const t = (w[0] * q[1] - w[1] * q[0]) / den;
|
|
583
|
+
const u = (w[0] * r[1] - w[1] * r[0]) / den;
|
|
584
|
+
return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
|
|
585
|
+
}
|
|
586
|
+
function variableStrokeRings(path, options) {
|
|
587
|
+
const half = options.width / 2;
|
|
588
|
+
if (!(half > 0) || !Number.isFinite(half)) return [];
|
|
589
|
+
const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
|
|
590
|
+
const join = options.join ?? "miter";
|
|
591
|
+
const cap = options.cap ?? "butt";
|
|
592
|
+
const miterLimit = options.miterLimit && options.miterLimit >= 1 ? options.miterLimit : 4;
|
|
593
|
+
const rings = [];
|
|
594
|
+
const push = (out, p) => {
|
|
595
|
+
const last = out[out.length - 1];
|
|
596
|
+
if (!last || dist(last, p) > 1e-12) out.push(p);
|
|
597
|
+
};
|
|
598
|
+
for (const contour of pathToContours(path)) {
|
|
599
|
+
if (!contour.segments.length) continue;
|
|
600
|
+
const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
|
|
601
|
+
if (!poly) continue;
|
|
602
|
+
const len2 = poly.s[poly.s.length - 1];
|
|
603
|
+
const sides2 = (s) => {
|
|
604
|
+
const w = widthProfileAt(options.profile, s / len2);
|
|
605
|
+
return [w.left * half, w.right * half];
|
|
606
|
+
};
|
|
607
|
+
const intervals = options.dash?.some((v2) => v2 > 0) ? dashIntervals(options.dash, options.dashOffset ?? 0, len2) : [[0, len2]];
|
|
608
|
+
const whole = intervals.length === 1 && intervals[0][0] <= 0 && intervals[0][1] >= len2;
|
|
609
|
+
for (const [a, b] of intervals) {
|
|
610
|
+
const [pa, ia] = at(poly, a);
|
|
611
|
+
const [pb, ib] = at(poly, b);
|
|
612
|
+
const pts = [{ p: pa, s: a, corner: poly.corner[0] && a <= 0 }];
|
|
613
|
+
for (let i = ia; i < ib; i++) pts.push({ p: poly.pts[i], s: poly.s[i], corner: poly.corner[i] });
|
|
614
|
+
pts.push({ p: pb, s: b, corner: false });
|
|
615
|
+
const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
|
|
616
|
+
if (run.length < 2) {
|
|
617
|
+
const [l, r] = sides2(a);
|
|
618
|
+
const w2 = (l + r) / 2;
|
|
619
|
+
if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const closed = whole && poly.closed;
|
|
623
|
+
const m = run.length - 1;
|
|
624
|
+
const dirs = [];
|
|
625
|
+
for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
|
|
626
|
+
const normals = dirs.map(leftNormal);
|
|
627
|
+
const w = run.map((q) => sides2(q.s));
|
|
628
|
+
const side = (sigma) => {
|
|
629
|
+
const k = sigma > 0 ? 0 : 1;
|
|
630
|
+
const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
|
|
631
|
+
const E = (i) => along(run[i + 1].p, normals[i], sigma * w[i + 1][k]);
|
|
632
|
+
const out = [S(0)];
|
|
633
|
+
for (let i = 0; i < m; i++) {
|
|
634
|
+
const seam = i === m - 1;
|
|
635
|
+
if (seam && !closed) {
|
|
636
|
+
push(out, E(i));
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
const j = seam ? 0 : i + 1;
|
|
640
|
+
const p = run[i + 1].p;
|
|
641
|
+
const d0 = dirs[i];
|
|
642
|
+
const d1 = dirs[j];
|
|
643
|
+
const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
|
|
644
|
+
const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
|
|
645
|
+
const r0 = w[i + 1][k];
|
|
646
|
+
const r1 = w[j][k];
|
|
647
|
+
const e = E(i);
|
|
648
|
+
const st = S(j);
|
|
649
|
+
if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
|
|
650
|
+
push(out, e);
|
|
651
|
+
if (!seam) push(out, st);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const uturn = Math.abs(cross2) < 1e-12;
|
|
655
|
+
const outer = uturn ? sigma > 0 : cross2 * sigma > 0;
|
|
656
|
+
if (!outer) {
|
|
657
|
+
const x = seam ? null : crossing(out[out.length - 1], e, st, E(j));
|
|
658
|
+
if (x) push(out, x);
|
|
659
|
+
else {
|
|
660
|
+
push(out, e);
|
|
661
|
+
push(out, p);
|
|
662
|
+
if (!seam) push(out, st);
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
push(out, e);
|
|
667
|
+
const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
|
|
668
|
+
const r = (r0 + r1) / 2;
|
|
669
|
+
const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
|
|
670
|
+
if (r > 1e-9) {
|
|
671
|
+
if (kind === "round") {
|
|
672
|
+
if (r * (1 - Math.cos(Math.abs(theta) / 2)) > tol * 0.25) {
|
|
673
|
+
for (const q of arcPoints(p, Math.atan2(e[1] - p[1], e[0] - p[0]), theta, r0, r1, tol)) push(out, q);
|
|
674
|
+
}
|
|
675
|
+
} else if (kind === "miter") {
|
|
676
|
+
const halfCos = Math.cos(theta / 2);
|
|
677
|
+
const bis = [normals[i][0] + normals[j][0], normals[i][1] + normals[j][1]];
|
|
678
|
+
const bl = Math.hypot(bis[0], bis[1]);
|
|
679
|
+
if (halfCos > 1e-6 && 1 / halfCos <= miterLimit && bl > 1e-9) push(out, along(p, bis, sigma * r / halfCos / bl));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (!seam) push(out, st);
|
|
683
|
+
}
|
|
684
|
+
if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
|
|
685
|
+
return out;
|
|
686
|
+
};
|
|
687
|
+
const left = side(1);
|
|
688
|
+
const right = side(-1);
|
|
689
|
+
if (closed) {
|
|
690
|
+
rings.push(left, right.reverse());
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const capPoints = (atStart) => {
|
|
694
|
+
if (cap === "butt") return [];
|
|
695
|
+
const i = atStart ? 0 : m;
|
|
696
|
+
const d = atStart ? dirs[0] : dirs[m - 1];
|
|
697
|
+
const [l, r] = w[i];
|
|
698
|
+
if (!(l + r > 1e-9)) return [];
|
|
699
|
+
const p = run[i].p;
|
|
700
|
+
const n = leftNormal(d);
|
|
701
|
+
const L = along(p, n, l);
|
|
702
|
+
const R = along(p, n, -r);
|
|
703
|
+
if (cap === "round") {
|
|
704
|
+
const a0 = Math.atan2(d[1], d[0]) + (atStart ? Math.PI / 2 : -Math.PI / 2);
|
|
705
|
+
return arcPoints(p, a0, Math.PI, atStart ? r : l, atStart ? l : r, tol);
|
|
706
|
+
}
|
|
707
|
+
const ext = (l + r) / 2;
|
|
708
|
+
const o = atStart ? [-d[0], -d[1]] : d;
|
|
709
|
+
return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
|
|
710
|
+
};
|
|
711
|
+
const ring = [];
|
|
712
|
+
for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
|
|
713
|
+
if (ring.length > 2) rings.push(ring);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return rings;
|
|
717
|
+
}
|
|
718
|
+
function variableStrokeOutline(path, options) {
|
|
719
|
+
const pieces = variableStrokeRings(path, options);
|
|
720
|
+
if (!pieces.length) return [];
|
|
721
|
+
const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
|
|
722
|
+
let polygons;
|
|
723
|
+
try {
|
|
724
|
+
polygons = polygonBoolean("union", [{ rings: pieces, fillRule: "nonzero" }]);
|
|
725
|
+
} catch {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
const out = [];
|
|
729
|
+
for (const polygon2 of polygons) {
|
|
730
|
+
for (const ring of polygon2) {
|
|
731
|
+
if (ring.length < 3) continue;
|
|
732
|
+
const cubics = fitCubics(ring, tol, { closed: true, cornerAngleDeg: 25 });
|
|
733
|
+
if (!cubics.length) continue;
|
|
734
|
+
out.push({ type: "M", x: cubics[0].p0[0], y: cubics[0].p0[1] });
|
|
735
|
+
cubics.forEach((c, i) => {
|
|
736
|
+
if (isStraightCubic(c, tol * 0.02)) {
|
|
737
|
+
if (i < cubics.length - 1) out.push({ type: "L", x: c.p3[0], y: c.p3[1] });
|
|
738
|
+
} else out.push({ type: "C", x1: c.p1[0], y1: c.p1[1], x2: c.p2[0], y2: c.p2[1], x: c.p3[0], y: c.p3[1] });
|
|
739
|
+
});
|
|
740
|
+
out.push({ type: "Z" });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
function nearestStrokePosition(path, p, only) {
|
|
746
|
+
let best = null;
|
|
747
|
+
pathToContours(path).forEach((c, ci) => {
|
|
748
|
+
if (!c.segments.length || only !== void 0 && ci !== only) return;
|
|
749
|
+
const lengths = c.segments.map((s) => segmentLength(s));
|
|
750
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
751
|
+
if (!(total > 0)) return;
|
|
752
|
+
let acc = 0;
|
|
753
|
+
c.segments.forEach((seg, si) => {
|
|
754
|
+
const near2 = segmentNearest(seg, p);
|
|
755
|
+
if (!best || near2.distance < best.distance) {
|
|
756
|
+
const s = acc + segmentLength(seg, 0, near2.t);
|
|
757
|
+
const tan = segmentTangent(seg, near2.t);
|
|
758
|
+
const pt2 = seg.length === 2 ? lerp(seg[0], seg[1], near2.t) : evalSeg(seg, near2.t);
|
|
759
|
+
const n = leftNormal([tan[0], tan[1]]);
|
|
760
|
+
const side = (p[0] - pt2[0]) * n[0] + (p[1] - pt2[1]) * n[1] >= 0 ? "left" : "right";
|
|
761
|
+
best = { contour: ci, t: Math.min(Math.max(s / total, 0), 1), distance: near2.distance, side, point: pt2, tangent: [tan[0], tan[1]], length: total };
|
|
762
|
+
}
|
|
763
|
+
acc += lengths[si];
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
return best;
|
|
767
|
+
}
|
|
768
|
+
function evalSeg(seg, t) {
|
|
769
|
+
let pts = seg.map((q) => [q[0], q[1]]);
|
|
770
|
+
while (pts.length > 1) pts = pts.slice(1).map((q, i) => lerp(pts[i], q, t));
|
|
771
|
+
return pts[0];
|
|
772
|
+
}
|
|
773
|
+
function strokePositionPoint(path, index, t) {
|
|
774
|
+
const c = pathToContours(path)[index];
|
|
775
|
+
if (!c?.segments.length) return null;
|
|
776
|
+
const lengths = c.segments.map((s) => segmentLength(s));
|
|
777
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
778
|
+
let want = Math.min(Math.max(t, 0), 1) * total;
|
|
779
|
+
for (let i = 0; i < c.segments.length; i++) {
|
|
780
|
+
const seg = c.segments[i];
|
|
781
|
+
if (want <= lengths[i] + 1e-12 || i === c.segments.length - 1) {
|
|
782
|
+
let lo = 0;
|
|
783
|
+
let hi = 1;
|
|
784
|
+
const target = Math.min(want, lengths[i]);
|
|
785
|
+
for (let k = 0; k < 40; k++) {
|
|
786
|
+
const mid = (lo + hi) / 2;
|
|
787
|
+
if (segmentLength(seg, 0, mid) < target) lo = mid;
|
|
788
|
+
else hi = mid;
|
|
789
|
+
}
|
|
790
|
+
const u = (lo + hi) / 2;
|
|
791
|
+
const tan = segmentTangent(seg, u);
|
|
792
|
+
return { point: evalSeg(seg, u), tangent: [tan[0], tan[1]] };
|
|
793
|
+
}
|
|
794
|
+
want -= lengths[i];
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
248
799
|
// src/vector/stroke.ts
|
|
249
800
|
var KAPPA = 0.5522847498307936;
|
|
250
801
|
var NOTCH = 0.7;
|
|
@@ -256,8 +807,21 @@ function effectiveStrokeAlign(style, closedShape) {
|
|
|
256
807
|
const a = style.strokeAlign ?? "center";
|
|
257
808
|
return closedShape ? a : "center";
|
|
258
809
|
}
|
|
810
|
+
function strokeWidthProfileOf(style) {
|
|
811
|
+
if (!style.strokeWidthProfile?.length) return null;
|
|
812
|
+
const p = sanitizeWidthProfile(style.strokeWidthProfile);
|
|
813
|
+
return isUniformWidthProfile(p) ? null : p;
|
|
814
|
+
}
|
|
259
815
|
function hasStrokeExtras(style) {
|
|
260
|
-
return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow;
|
|
816
|
+
return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow || !!strokeWidthProfileOf(style);
|
|
817
|
+
}
|
|
818
|
+
function vectorStrokeLineArea(parts, style, tolerance) {
|
|
819
|
+
const profile = strokeWidthProfileOf(style);
|
|
820
|
+
const dash = style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {};
|
|
821
|
+
const join = style.strokeLinejoin ?? "miter";
|
|
822
|
+
const cap = style.strokeLinecap ?? "butt";
|
|
823
|
+
if (profile) return variableStrokeOutline(parts.line, { width: parts.drawWidth, profile, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
|
|
824
|
+
return outlineStroke(parts.line, { width: parts.drawWidth, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
|
|
261
825
|
}
|
|
262
826
|
var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
|
|
263
827
|
function arrowheadWeight(style, end) {
|
|
@@ -347,13 +911,13 @@ function endOf(segs, atStart) {
|
|
|
347
911
|
const last = segs[segs.length - 1];
|
|
348
912
|
return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
|
|
349
913
|
}
|
|
350
|
-
function pointAlong(segs, fromStart,
|
|
914
|
+
function pointAlong(segs, fromStart, dist4) {
|
|
351
915
|
const order = fromStart ? segs : [...segs].reverse();
|
|
352
916
|
let acc = 0;
|
|
353
917
|
for (const s of order) {
|
|
354
918
|
const l = segLen(s);
|
|
355
|
-
if (acc + l >=
|
|
356
|
-
const want = fromStart ?
|
|
919
|
+
if (acc + l >= dist4) {
|
|
920
|
+
const want = fromStart ? dist4 - acc : l - (dist4 - acc);
|
|
357
921
|
const pts = trimSegments([s], 0, want);
|
|
358
922
|
const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
|
|
359
923
|
return toVec(p);
|
|
@@ -410,12 +974,7 @@ function vectorStrokeParts(path, style) {
|
|
|
410
974
|
}
|
|
411
975
|
function vectorStrokeOutlinePath(path, style) {
|
|
412
976
|
const parts = vectorStrokeParts(path, style);
|
|
413
|
-
const stroke =
|
|
414
|
-
width: parts.drawWidth,
|
|
415
|
-
join: style.strokeLinejoin ?? "miter",
|
|
416
|
-
cap: style.strokeLinecap ?? "butt",
|
|
417
|
-
...style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {}
|
|
418
|
-
});
|
|
977
|
+
const stroke = vectorStrokeLineArea(parts, style);
|
|
419
978
|
const fill = { path, fillRule: style.fillRule ?? "nonzero" };
|
|
420
979
|
let area = stroke;
|
|
421
980
|
if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
|
|
@@ -431,15 +990,32 @@ function hitTestVectorStroke(path, p, style, tolerance = 0) {
|
|
|
431
990
|
if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
|
|
432
991
|
}
|
|
433
992
|
if (!parts.line.length) return false;
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
993
|
+
const profile = strokeWidthProfileOf(style);
|
|
994
|
+
let d;
|
|
995
|
+
let reach = width / 2;
|
|
996
|
+
if (profile) {
|
|
997
|
+
const near2 = nearestStrokePosition(parts.line, point);
|
|
998
|
+
if (!near2) return false;
|
|
999
|
+
d = near2.distance;
|
|
1000
|
+
const w = widthProfileAt(profile, near2.t);
|
|
1001
|
+
reach = width / 2 * (near2.side === "left" ? w.left : w.right);
|
|
1002
|
+
if (parts.align !== "center") reach *= 2;
|
|
1003
|
+
const atEnd = near2.t <= 1e-9 || near2.t >= 1 - 1e-9;
|
|
1004
|
+
if (atEnd && (style.strokeLinecap === "round" || style.strokeLinecap === "square") && d > reach) {
|
|
1005
|
+
reach = Math.max(reach, width / 2 * (w.left + w.right) / 2);
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
d = distanceToBezierPath(parts.line, point);
|
|
1009
|
+
if (parts.align !== "center") reach = width;
|
|
1010
|
+
}
|
|
1011
|
+
if (parts.align === "center") return d <= reach + tolerance;
|
|
1012
|
+
if (d > reach + tolerance) return false;
|
|
437
1013
|
if (d <= tolerance) return true;
|
|
438
1014
|
const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
|
|
439
1015
|
return parts.align === "inside" ? inside : !inside;
|
|
440
1016
|
}
|
|
441
1017
|
function vectorStrokeReach(style) {
|
|
442
|
-
const width = style.strokeWidth ?? 1;
|
|
1018
|
+
const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
|
|
443
1019
|
const align = style.strokeAlign ?? "center";
|
|
444
1020
|
let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
|
|
445
1021
|
if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
|
|
@@ -749,8 +1325,10 @@ function bakeIntoGeometry(node, m) {
|
|
|
749
1325
|
if (isIdentityMatrix(m)) return node;
|
|
750
1326
|
if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
|
|
751
1327
|
const path = node;
|
|
1328
|
+
const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
|
|
752
1329
|
return {
|
|
753
1330
|
...path,
|
|
1331
|
+
...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
|
|
754
1332
|
subpaths: path.subpaths.map((sp) => ({
|
|
755
1333
|
closed: sp.closed,
|
|
756
1334
|
anchors: sp.anchors.map((a) => {
|
|
@@ -915,8 +1493,8 @@ function ellipseSubpath(b) {
|
|
|
915
1493
|
]
|
|
916
1494
|
};
|
|
917
1495
|
}
|
|
918
|
-
function polygonSubpath(b,
|
|
919
|
-
const n = Math.max(3, Math.round(
|
|
1496
|
+
function polygonSubpath(b, sides2 = 6) {
|
|
1497
|
+
const n = Math.max(3, Math.round(sides2));
|
|
920
1498
|
const cx = b.x + b.width / 2;
|
|
921
1499
|
const cy = b.y + b.height / 2;
|
|
922
1500
|
const anchors = [];
|
|
@@ -980,11 +1558,20 @@ function vectorPaintToSvg(paint, gradientId) {
|
|
|
980
1558
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
981
1559
|
return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
|
|
982
1560
|
}
|
|
1561
|
+
var partsCache = /* @__PURE__ */ new WeakMap();
|
|
983
1562
|
function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
984
1563
|
if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
|
|
1564
|
+
const hit = partsCache.get(node);
|
|
1565
|
+
if (hit && hit.clipId === clipId && hit.precision === precision) return hit.parts;
|
|
1566
|
+
const parts = computeStrokeSvgParts(node, clipId, precision);
|
|
1567
|
+
partsCache.set(node, { clipId, precision, parts });
|
|
1568
|
+
return parts;
|
|
1569
|
+
}
|
|
1570
|
+
function computeStrokeSvgParts(node, clipId, precision) {
|
|
985
1571
|
const path = subpathsToBezier(node.subpaths);
|
|
986
1572
|
const parts = vectorStrokeParts(path, node);
|
|
987
|
-
|
|
1573
|
+
const profiled = !!strokeWidthProfileOf(node);
|
|
1574
|
+
if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
|
|
988
1575
|
let clip = null;
|
|
989
1576
|
if (parts.align !== "center") {
|
|
990
1577
|
const b = bezierPathBounds(path);
|
|
@@ -992,7 +1579,26 @@ function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
|
992
1579
|
const bounds = b ? { x: round2(b.minX - pad), y: round2(b.minY - pad), width: round2(b.maxX - b.minX + 2 * pad), height: round2(b.maxY - b.minY + 2 * pad) } : { x: 0, y: 0, width: 0, height: 0 };
|
|
993
1580
|
clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
|
|
994
1581
|
}
|
|
995
|
-
return {
|
|
1582
|
+
return {
|
|
1583
|
+
align: parts.align,
|
|
1584
|
+
d: bezierPathToSvg(parts.line, { precision }),
|
|
1585
|
+
width: parts.drawWidth,
|
|
1586
|
+
...profiled && (node.strokeWidth ?? 1) > 0 ? { outline: bezierPathToSvg(vectorStrokeLineArea(parts, node), { precision }) } : {},
|
|
1587
|
+
clip,
|
|
1588
|
+
heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision }))
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function widthProfileToAttribute(points, precision = 4) {
|
|
1592
|
+
return points.map((p) => [p.t, p.left, p.right].map((n) => formatNumber(n, precision)).join(",")).join(";");
|
|
1593
|
+
}
|
|
1594
|
+
function widthProfileFromAttribute(value) {
|
|
1595
|
+
if (!value) return [];
|
|
1596
|
+
return sanitizeWidthProfile(
|
|
1597
|
+
value.split(";").map((part) => {
|
|
1598
|
+
const [t, left, right] = part.split(",").map((n) => parseFloat(n));
|
|
1599
|
+
return { t, left, right: Number.isFinite(right) ? right : left };
|
|
1600
|
+
})
|
|
1601
|
+
);
|
|
996
1602
|
}
|
|
997
1603
|
var round2 = (n) => Math.round(n * 100) / 100;
|
|
998
1604
|
function gradientMarkup(def) {
|
|
@@ -1051,6 +1657,13 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1051
1657
|
if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
|
|
1052
1658
|
if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
|
|
1053
1659
|
if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
|
|
1660
|
+
const profile = strokeWidthProfileOf(node);
|
|
1661
|
+
if (profile) {
|
|
1662
|
+
data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
|
|
1663
|
+
if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
|
|
1664
|
+
if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
|
|
1665
|
+
if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
|
|
1666
|
+
}
|
|
1054
1667
|
lines.push(`${indent(depth)}<g${common(node)}${data}>`);
|
|
1055
1668
|
lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
|
|
1056
1669
|
if (options.strokes === "outline") {
|
|
@@ -1071,7 +1684,8 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1071
1684
|
clip = ` mask="url(#${id})"`;
|
|
1072
1685
|
}
|
|
1073
1686
|
}
|
|
1074
|
-
lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.
|
|
1687
|
+
if (parts.outline !== void 0) lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${parts.outline}" fill="${escapeAttr(stroke.value)}"${clip}/>`);
|
|
1688
|
+
else lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
|
|
1075
1689
|
for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1076
1690
|
}
|
|
1077
1691
|
lines.push(`${indent(depth)}</g>`);
|
|
@@ -1334,6 +1948,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1334
1948
|
return n > 0 ? n : void 0;
|
|
1335
1949
|
};
|
|
1336
1950
|
const align = a["data-vector-stroke-align"];
|
|
1951
|
+
const profile = widthProfileFromAttribute(a["data-vector-width-profile"]);
|
|
1952
|
+
const kept = (key, values) => values.includes(a[key]) ? a[key] : void 0;
|
|
1953
|
+
const keptCap = kept("data-vector-stroke-linecap", ["round", "square"]);
|
|
1954
|
+
const keptJoin = kept("data-vector-stroke-linejoin", ["round", "bevel"]);
|
|
1955
|
+
const keptDash = a["data-vector-stroke-dasharray"] ? numberList(a["data-vector-stroke-dasharray"]) : void 0;
|
|
1337
1956
|
const node = {
|
|
1338
1957
|
id: idFor(g),
|
|
1339
1958
|
type: "path",
|
|
@@ -1349,7 +1968,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1349
1968
|
...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
|
|
1350
1969
|
...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
|
|
1351
1970
|
...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
|
|
1352
|
-
...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {}
|
|
1971
|
+
...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
|
|
1972
|
+
...profile.length ? { strokeWidthProfile: profile } : {},
|
|
1973
|
+
...keptCap ? { strokeLinecap: keptCap } : {},
|
|
1974
|
+
...keptJoin ? { strokeLinejoin: keptJoin } : {},
|
|
1975
|
+
...keptDash?.length ? { strokeDasharray: keptDash } : {}
|
|
1353
1976
|
};
|
|
1354
1977
|
return placed(node, m);
|
|
1355
1978
|
};
|
|
@@ -1669,7 +2292,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1669
2292
|
*/
|
|
1670
2293
|
snapFrom(p, leave, tol) {
|
|
1671
2294
|
const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
|
|
1672
|
-
const
|
|
2295
|
+
const at2 = (q, kind, lines2 = []) => ({
|
|
1673
2296
|
point: q,
|
|
1674
2297
|
kind,
|
|
1675
2298
|
x: true,
|
|
@@ -1683,16 +2306,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1683
2306
|
return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
|
|
1684
2307
|
}
|
|
1685
2308
|
const point = this.nearestPoint(p, tol);
|
|
1686
|
-
if (point) return
|
|
2309
|
+
if (point) return at2(point.p, point.kind);
|
|
1687
2310
|
const near2 = this.segmentsNear(p, tol);
|
|
1688
|
-
const
|
|
1689
|
-
if (
|
|
2311
|
+
const crossing2 = this.crossing(near2, p, tol);
|
|
2312
|
+
if (crossing2) return at2(crossing2, "intersect");
|
|
1690
2313
|
if (leave && leave.curves !== false) {
|
|
1691
2314
|
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1692
|
-
if (foot) return
|
|
2315
|
+
if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1693
2316
|
}
|
|
1694
2317
|
const on = this.onSegment(near2);
|
|
1695
|
-
if (on) return
|
|
2318
|
+
if (on) return at2(on, "path");
|
|
1696
2319
|
if (leave?.angles?.length) {
|
|
1697
2320
|
const c = construction(leave, p, tol);
|
|
1698
2321
|
if (c) {
|
|
@@ -1739,7 +2362,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1739
2362
|
* the nearest target line crosses it, at most twice `tol` away: where it lands and the
|
|
1740
2363
|
* target point that line comes from.
|
|
1741
2364
|
*/
|
|
1742
|
-
slide(
|
|
2365
|
+
slide(at2, dir, q, tol, ray) {
|
|
1743
2366
|
const len2 = dir.x * dir.x + dir.y * dir.y;
|
|
1744
2367
|
if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
|
|
1745
2368
|
const reach = 2 * tol;
|
|
@@ -1747,10 +2370,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1747
2370
|
for (const vertical of [true, false]) {
|
|
1748
2371
|
const d = vertical ? dir.x : dir.y;
|
|
1749
2372
|
if (Math.abs(d) <= 1e-9) continue;
|
|
1750
|
-
const a = vertical ?
|
|
2373
|
+
const a = vertical ? at2.x : at2.y;
|
|
1751
2374
|
for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
|
|
1752
2375
|
const s = (l.v - a) / d;
|
|
1753
|
-
const hit = vertical ? { x: l.v, y:
|
|
2376
|
+
const hit = vertical ? { x: l.v, y: at2.y + dir.y * s } : { x: at2.x + dir.x * s, y: l.v };
|
|
1754
2377
|
const moved = distance(hit, q);
|
|
1755
2378
|
if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
|
|
1756
2379
|
}
|
|
@@ -1898,11 +2521,11 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1898
2521
|
for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
|
|
1899
2522
|
if (!pick) return null;
|
|
1900
2523
|
const { start, repeats } = pick;
|
|
1901
|
-
const
|
|
2524
|
+
const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
1902
2525
|
const end = start + len2;
|
|
1903
2526
|
const gaps = [];
|
|
1904
|
-
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b,
|
|
1905
|
-
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b,
|
|
2527
|
+
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at2)]);
|
|
2528
|
+
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at2)]);
|
|
1906
2529
|
if (repeats !== null) {
|
|
1907
2530
|
for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
|
|
1908
2531
|
}
|
|
@@ -2331,9 +2954,9 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
2331
2954
|
anchors[index] = moved;
|
|
2332
2955
|
return { anchors, closed: sp.closed };
|
|
2333
2956
|
}
|
|
2334
|
-
function cutSubpathAt(sp,
|
|
2957
|
+
function cutSubpathAt(sp, at2) {
|
|
2335
2958
|
const n = sp.anchors.length;
|
|
2336
|
-
const cuts = [...new Set(
|
|
2959
|
+
const cuts = [...new Set(at2)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
|
|
2337
2960
|
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
2338
2961
|
const first = cuts[0];
|
|
2339
2962
|
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
@@ -2364,7 +2987,7 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
|
2364
2987
|
return cutSubpathAt(subpath, [index]);
|
|
2365
2988
|
}
|
|
2366
2989
|
var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
2367
|
-
var
|
|
2990
|
+
var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
2368
2991
|
function reverse(sp) {
|
|
2369
2992
|
return {
|
|
2370
2993
|
closed: sp.closed,
|
|
@@ -2391,7 +3014,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
2391
3014
|
const sp = open[0];
|
|
2392
3015
|
const first = sp.anchors[0];
|
|
2393
3016
|
const last = sp.anchors[sp.anchors.length - 1];
|
|
2394
|
-
if (sp.anchors.length > 2 &&
|
|
3017
|
+
if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
|
|
2395
3018
|
sp.anchors.pop();
|
|
2396
3019
|
sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
|
|
2397
3020
|
} else {
|
|
@@ -2407,7 +3030,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
2407
3030
|
for (let j = i + 1; j < open.length; j++) {
|
|
2408
3031
|
for (const ei of [false, true]) {
|
|
2409
3032
|
for (const ej of [false, true]) {
|
|
2410
|
-
const d =
|
|
3033
|
+
const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
|
|
2411
3034
|
if (d < best.d) best = { d, i, j, ei, ej };
|
|
2412
3035
|
}
|
|
2413
3036
|
}
|
|
@@ -2447,8 +3070,8 @@ function smoothSubpath(sp, amount = 1, only) {
|
|
|
2447
3070
|
const p = orig[i];
|
|
2448
3071
|
const dir = unit2(next.x - prev.x, next.y - prev.y);
|
|
2449
3072
|
if (!dir) continue;
|
|
2450
|
-
const li =
|
|
2451
|
-
const lo =
|
|
3073
|
+
const li = dist2(p, prev) / 3;
|
|
3074
|
+
const lo = dist2(p, next) / 3;
|
|
2452
3075
|
const hin = p.in ?? p;
|
|
2453
3076
|
const hout = p.out ?? p;
|
|
2454
3077
|
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
@@ -2475,12 +3098,12 @@ function segsOf(sp) {
|
|
|
2475
3098
|
function segsToSubpath(segs, closed, types) {
|
|
2476
3099
|
if (!segs.length) return null;
|
|
2477
3100
|
const anchors = [];
|
|
2478
|
-
const
|
|
2479
|
-
anchors.push(
|
|
3101
|
+
const at2 = (p) => ({ x: p[0], y: p[1] });
|
|
3102
|
+
anchors.push(at2(segs[0].c[0]));
|
|
2480
3103
|
for (const s of segs) {
|
|
2481
3104
|
const end = s.c[s.c.length - 1];
|
|
2482
3105
|
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
2483
|
-
const next =
|
|
3106
|
+
const next = at2(end);
|
|
2484
3107
|
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
2485
3108
|
anchors.push(next);
|
|
2486
3109
|
}
|
|
@@ -2506,8 +3129,8 @@ function mergePair(a, b, tol) {
|
|
|
2506
3129
|
if (len2 < 1e-12) return null;
|
|
2507
3130
|
const mx = a3[0] - a0[0];
|
|
2508
3131
|
const my = a3[1] - a0[1];
|
|
2509
|
-
const
|
|
2510
|
-
return Math.abs((mx * cy - my * cx) / len2) <= tol &&
|
|
3132
|
+
const along2 = (mx * cx + my * cy) / len2;
|
|
3133
|
+
return Math.abs((mx * cy - my * cx) / len2) <= tol && along2 >= 0 && along2 <= len2 ? { c: [a0, b3], line: true } : null;
|
|
2511
3134
|
}
|
|
2512
3135
|
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
2513
3136
|
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
@@ -2553,13 +3176,13 @@ function loopSample(l) {
|
|
|
2553
3176
|
}
|
|
2554
3177
|
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
2555
3178
|
function groupFaces(loops) {
|
|
2556
|
-
const
|
|
2557
|
-
if (!
|
|
2558
|
-
const largest =
|
|
3179
|
+
const usable2 = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
3180
|
+
if (!usable2.length) return [];
|
|
3181
|
+
const largest = usable2.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
2559
3182
|
const sign = Math.sign(loopArea(largest));
|
|
2560
|
-
const outers =
|
|
3183
|
+
const outers = usable2.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
2561
3184
|
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
2562
|
-
for (const hole of
|
|
3185
|
+
for (const hole of usable2.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
2563
3186
|
const p = loopSample(hole);
|
|
2564
3187
|
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
2565
3188
|
if (parent) faces.get(parent).loops.push(hole);
|
|
@@ -2939,7 +3562,7 @@ function vectorInkShapeName(shape) {
|
|
|
2939
3562
|
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2940
3563
|
}
|
|
2941
3564
|
}
|
|
2942
|
-
var
|
|
3565
|
+
var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2943
3566
|
function bbox(pts) {
|
|
2944
3567
|
if (!pts.length) return [0, 0, 0, 0];
|
|
2945
3568
|
let [x0, y0] = pts[0];
|
|
@@ -2957,7 +3580,7 @@ function segDistance(a, b, p) {
|
|
|
2957
3580
|
const dy = b[1] - a[1];
|
|
2958
3581
|
const len2 = dx * dx + dy * dy;
|
|
2959
3582
|
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2960
|
-
return
|
|
3583
|
+
return dist3([a[0] + t * dx, a[1] + t * dy], p);
|
|
2961
3584
|
}
|
|
2962
3585
|
function polygonDistance(c, p) {
|
|
2963
3586
|
let best = Infinity;
|
|
@@ -3019,7 +3642,7 @@ function corners(p, diag) {
|
|
|
3019
3642
|
let far = 0;
|
|
3020
3643
|
let farD = 0;
|
|
3021
3644
|
p.forEach((q, i) => {
|
|
3022
|
-
const d =
|
|
3645
|
+
const d = dist3(start, q);
|
|
3023
3646
|
if (d > farD) {
|
|
3024
3647
|
farD = d;
|
|
3025
3648
|
far = i;
|
|
@@ -3046,7 +3669,7 @@ function corners(p, diag) {
|
|
|
3046
3669
|
}
|
|
3047
3670
|
let short = -1;
|
|
3048
3671
|
for (let j = 0; j < n; j++) {
|
|
3049
|
-
if (
|
|
3672
|
+
if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
3050
3673
|
short = j;
|
|
3051
3674
|
break;
|
|
3052
3675
|
}
|
|
@@ -3086,8 +3709,8 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3086
3709
|
const diag = Math.hypot(w, h);
|
|
3087
3710
|
if (size < minSize) return null;
|
|
3088
3711
|
let length2 = 0;
|
|
3089
|
-
for (let i = 1; i < p.length; i++) length2 +=
|
|
3090
|
-
const gap =
|
|
3712
|
+
for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
|
|
3713
|
+
const gap = dist3(first, last);
|
|
3091
3714
|
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
3092
3715
|
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
3093
3716
|
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
@@ -3111,9 +3734,188 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3111
3734
|
return roundish && rms < 0.1 ? ellipse() : null;
|
|
3112
3735
|
}
|
|
3113
3736
|
|
|
3737
|
+
// src/vector/builder.ts
|
|
3738
|
+
function vectorPathEntries(doc, roots, options = {}) {
|
|
3739
|
+
const out = [];
|
|
3740
|
+
const visit = (node, parent, inside) => {
|
|
3741
|
+
if (node.visible === false || node.locked && !options.locked) return;
|
|
3742
|
+
const picked = inside || roots === null || roots.includes(node.id);
|
|
3743
|
+
if (node.type === "group") {
|
|
3744
|
+
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
3745
|
+
for (const c of node.children) visit(c, m, picked);
|
|
3746
|
+
} else if (picked) out.push({ node, parent });
|
|
3747
|
+
};
|
|
3748
|
+
for (const n of doc.nodes) visit(n, IDENTITY_MATRIX, false);
|
|
3749
|
+
return out;
|
|
3750
|
+
}
|
|
3751
|
+
var usable = (sp) => sp.anchors.length > 1;
|
|
3752
|
+
function worldOf(entry, subpaths) {
|
|
3753
|
+
return vectorPathWorldBezier({ ...entry.node, subpaths }, entry.parent);
|
|
3754
|
+
}
|
|
3755
|
+
function subpathEnclosesArea(sp) {
|
|
3756
|
+
const contour = pathToContours(subpathsToBezier([{ ...sp, closed: true }]))[0];
|
|
3757
|
+
if (!contour) return false;
|
|
3758
|
+
const segs = closeContour(contour, true);
|
|
3759
|
+
const b = bezierPathBounds(contoursToPath([{ segments: segs, closed: true }]));
|
|
3760
|
+
if (!b) return false;
|
|
3761
|
+
const w = b.maxX - b.minX;
|
|
3762
|
+
const h = b.maxY - b.minY;
|
|
3763
|
+
return Math.abs(contourSignedArea(segs)) > 1e-9 * (w * w + h * h);
|
|
3764
|
+
}
|
|
3765
|
+
var paintKey = (paint) => typeof paint === "string" ? paint.trim().toLowerCase() : JSON.stringify(paint ?? null);
|
|
3766
|
+
function vectorPathfinderShape(entry, op) {
|
|
3767
|
+
const { node } = entry;
|
|
3768
|
+
const subpaths = node.subpaths.filter(usable);
|
|
3769
|
+
const closed = subpaths.map((s) => ({ ...s, closed: true }));
|
|
3770
|
+
const fillRule = node.fillRule ?? "nonzero";
|
|
3771
|
+
if (hasPaint(node.fill)) return { path: worldOf(entry, closed), fillRule, key: `fill:${paintKey(node.fill)}`, paint: "fill" };
|
|
3772
|
+
const open = subpaths.length > 0 && subpaths.every((s) => !s.closed);
|
|
3773
|
+
if (op === "divide" && open) return { path: worldOf(entry, subpaths), line: true, paint: "line" };
|
|
3774
|
+
if (hasPaint(node.stroke) && (node.strokeWidth ?? 1) > 0) {
|
|
3775
|
+
const path = vectorStrokeOutline({ ...node, subpaths }, entry.parent);
|
|
3776
|
+
return { path, fillRule: "nonzero", key: `fill:${paintKey(node.stroke)}`, paint: "stroke" };
|
|
3777
|
+
}
|
|
3778
|
+
return { path: worldOf(entry, closed), fillRule, key: "none", paint: "outline" };
|
|
3779
|
+
}
|
|
3780
|
+
var STROKE_KEYS = [
|
|
3781
|
+
"stroke",
|
|
3782
|
+
"strokeWidth",
|
|
3783
|
+
"strokeLinecap",
|
|
3784
|
+
"strokeLinejoin",
|
|
3785
|
+
"strokeDasharray",
|
|
3786
|
+
"strokeAlign",
|
|
3787
|
+
"startArrow",
|
|
3788
|
+
"endArrow",
|
|
3789
|
+
"arrowAlign",
|
|
3790
|
+
"startArrowScale",
|
|
3791
|
+
"endArrowScale",
|
|
3792
|
+
"strokeWidthProfile"
|
|
3793
|
+
];
|
|
3794
|
+
function vectorResultStyle(node, paint, edge = false) {
|
|
3795
|
+
const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
|
|
3796
|
+
const base = { ...rest, fillRule: "nonzero" };
|
|
3797
|
+
if (edge) {
|
|
3798
|
+
for (const k of STROKE_KEYS) delete base[k];
|
|
3799
|
+
const stroke = paint === "stroke" || !hasPaint(node.fill) ? node.stroke : node.fill;
|
|
3800
|
+
return { ...base, fill: null, stroke: stroke ?? "#000000", strokeWidth: node.strokeWidth ?? 1 };
|
|
3801
|
+
}
|
|
3802
|
+
if (paint === "stroke") {
|
|
3803
|
+
for (const k of STROKE_KEYS) delete base[k];
|
|
3804
|
+
return { ...base, fill: node.stroke ?? null, stroke: null };
|
|
3805
|
+
}
|
|
3806
|
+
return base;
|
|
3807
|
+
}
|
|
3808
|
+
function vectorShapeBuilderMap(entries) {
|
|
3809
|
+
const lines = [];
|
|
3810
|
+
const inputs = entries.map((entry) => {
|
|
3811
|
+
const filled = hasPaint(entry.node.fill);
|
|
3812
|
+
const subpaths = entry.node.subpaths.filter(usable).map((sp) => !sp.closed && filled && subpathEnclosesArea(sp) ? { ...sp, closed: true } : sp);
|
|
3813
|
+
lines.push(!subpaths.some((s) => s.closed));
|
|
3814
|
+
return { path: worldOf(entry, subpaths), fillRule: entry.node.fillRule ?? "nonzero" };
|
|
3815
|
+
});
|
|
3816
|
+
const map = PlanarMap.build(inputs);
|
|
3817
|
+
if (!map) return null;
|
|
3818
|
+
const pieces = map.chains().filter((c) => lines[c.source] && !c.closed).map((c) => {
|
|
3819
|
+
const path = contoursToPath([{ segments: c.segments, closed: false }]);
|
|
3820
|
+
return { path, segments: c.segments, source: c.source, bounds: bezierPathBounds(path) };
|
|
3821
|
+
});
|
|
3822
|
+
return { entries: [...entries], map, lines, pieces };
|
|
3823
|
+
}
|
|
3824
|
+
function vectorShapeBuilderHit(m, p, lineTolerance = null) {
|
|
3825
|
+
const point = [p.x, p.y];
|
|
3826
|
+
if (lineTolerance !== null && lineTolerance > 0) {
|
|
3827
|
+
let best = -1;
|
|
3828
|
+
let bestD = lineTolerance;
|
|
3829
|
+
m.pieces.forEach((piece, i) => {
|
|
3830
|
+
const b = piece.bounds;
|
|
3831
|
+
if (p.x < b.minX - bestD || p.x > b.maxX + bestD || p.y < b.minY - bestD || p.y > b.maxY + bestD) return;
|
|
3832
|
+
const d = distanceToBezierPath(piece.path, point);
|
|
3833
|
+
if (d <= bestD) {
|
|
3834
|
+
best = i;
|
|
3835
|
+
bestD = d;
|
|
3836
|
+
}
|
|
3837
|
+
});
|
|
3838
|
+
if (best >= 0) return { line: best };
|
|
3839
|
+
}
|
|
3840
|
+
const face = m.map.faceAt(point);
|
|
3841
|
+
return face >= 0 ? { face } : null;
|
|
3842
|
+
}
|
|
3843
|
+
function samplePolyline(points, step) {
|
|
3844
|
+
const out = [];
|
|
3845
|
+
const s = Math.max(step, 1e-3);
|
|
3846
|
+
points.forEach((p, i) => {
|
|
3847
|
+
if (i > 0) {
|
|
3848
|
+
const a = points[i - 1];
|
|
3849
|
+
const n = Math.min(4e3, Math.ceil(Math.hypot(p.x - a.x, p.y - a.y) / s));
|
|
3850
|
+
for (let k = 1; k < n; k++) out.push({ x: a.x + (p.x - a.x) * k / n, y: a.y + (p.y - a.y) * k / n });
|
|
3851
|
+
}
|
|
3852
|
+
out.push(p);
|
|
3853
|
+
});
|
|
3854
|
+
return out;
|
|
3855
|
+
}
|
|
3856
|
+
function addBuilderHit(touched, hit) {
|
|
3857
|
+
if (!hit) return false;
|
|
3858
|
+
const [list, i] = "face" in hit ? [touched.faces, hit.face] : [touched.lines, hit.line];
|
|
3859
|
+
if (list.includes(i)) return false;
|
|
3860
|
+
list.push(i);
|
|
3861
|
+
return true;
|
|
3862
|
+
}
|
|
3863
|
+
function sides(m, piece) {
|
|
3864
|
+
const seg = piece.segments[Math.floor(piece.segments.length / 2)];
|
|
3865
|
+
const mid = segmentPoint(seg, 0.5);
|
|
3866
|
+
const t = segmentTangent(seg, 0.5);
|
|
3867
|
+
const chord = Math.hypot(seg[seg.length - 1][0] - seg[0][0], seg[seg.length - 1][1] - seg[0][1]);
|
|
3868
|
+
const off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));
|
|
3869
|
+
return [m.map.faceAt([mid[0] - t[1] * off, mid[1] + t[0] * off]), m.map.faceAt([mid[0] + t[1] * off, mid[1] - t[0] * off])];
|
|
3870
|
+
}
|
|
3871
|
+
function planShapeBuilder(m, touched, erase) {
|
|
3872
|
+
const replace = /* @__PURE__ */ new Map();
|
|
3873
|
+
const picked = new Set(touched.faces);
|
|
3874
|
+
const faces = m.map.faces;
|
|
3875
|
+
const sourcesOf = faces.map((_, i) => m.map.faceSources(i));
|
|
3876
|
+
m.entries.forEach((_, i) => {
|
|
3877
|
+
if (m.lines[i]) {
|
|
3878
|
+
const own = m.pieces.flatMap((piece, k) => piece.source === i ? [k] : []);
|
|
3879
|
+
const keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));
|
|
3880
|
+
if (keep.length === own.length) return;
|
|
3881
|
+
replace.set(i, keep.length ? { kind: "pieces", paths: keep.map((k) => m.pieces[k].path) } : null);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
if (!touched.faces.some((f) => sourcesOf[f].includes(i))) return;
|
|
3885
|
+
const kept = faces.flatMap((_2, f) => sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []);
|
|
3886
|
+
const rest = kept.length ? m.map.merge(kept) : [];
|
|
3887
|
+
replace.set(i, rest.length ? { kind: "fill", path: rest } : null);
|
|
3888
|
+
});
|
|
3889
|
+
let merged = null;
|
|
3890
|
+
if (!erase && touched.faces.length) {
|
|
3891
|
+
const path = m.map.merge(touched.faces);
|
|
3892
|
+
const covered = touched.faces.map((f) => sourcesOf[f]).find((s) => s.length);
|
|
3893
|
+
const lastShape = m.lines.lastIndexOf(false);
|
|
3894
|
+
const style = covered ? covered[covered.length - 1] : lastShape >= 0 ? lastShape : m.entries.length - 1;
|
|
3895
|
+
if (path.length) merged = { path, style, currentFill: !covered };
|
|
3896
|
+
}
|
|
3897
|
+
return { replace, merged };
|
|
3898
|
+
}
|
|
3899
|
+
function vectorFillMap(doc, gap = 0) {
|
|
3900
|
+
const entries = vectorPathEntries(doc, null, { locked: true });
|
|
3901
|
+
return PlanarMap.build(
|
|
3902
|
+
entries.map((e) => ({ path: worldOf(e, e.node.subpaths.filter(usable)), fills: false })),
|
|
3903
|
+
{ gap, maxSegments: 2e4 }
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3114
3907
|
// src/vector/controller.ts
|
|
3115
3908
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
3116
|
-
var
|
|
3909
|
+
var VECTOR_FILL_GAP = 6;
|
|
3910
|
+
var DEFAULT_VECTOR_PATH_EDIT = {
|
|
3911
|
+
keepShapeOnDelete: true,
|
|
3912
|
+
curvatureMoves: false,
|
|
3913
|
+
pencilSmoothing: 0,
|
|
3914
|
+
recognizeShapes: false,
|
|
3915
|
+
eraserSize: 10,
|
|
3916
|
+
pencilPressure: "pen",
|
|
3917
|
+
fillGap: 0
|
|
3918
|
+
};
|
|
3117
3919
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3118
3920
|
var VECTOR_STYLE_KEYS = [
|
|
3119
3921
|
"fill",
|
|
@@ -3128,7 +3930,8 @@ var VECTOR_STYLE_KEYS = [
|
|
|
3128
3930
|
"endArrow",
|
|
3129
3931
|
"arrowAlign",
|
|
3130
3932
|
"startArrowScale",
|
|
3131
|
-
"endArrowScale"
|
|
3933
|
+
"endArrowScale",
|
|
3934
|
+
"strokeWidthProfile"
|
|
3132
3935
|
];
|
|
3133
3936
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
3134
3937
|
var DEFAULT_DEFAULTS = {
|
|
@@ -3190,9 +3993,9 @@ function getVisibleHandles(state) {
|
|
|
3190
3993
|
return out;
|
|
3191
3994
|
}
|
|
3192
3995
|
if (state.tool !== "direct") return out;
|
|
3193
|
-
const
|
|
3996
|
+
const editable2 = new Set(getEditablePathIds(state));
|
|
3194
3997
|
for (const ref of state.selectedAnchors) {
|
|
3195
|
-
if (!
|
|
3998
|
+
if (!editable2.has(ref.nodeId)) continue;
|
|
3196
3999
|
const node = findVectorNode(state.document.nodes, ref.nodeId);
|
|
3197
4000
|
const sp = node.subpaths[ref.subpath];
|
|
3198
4001
|
if (!sp) continue;
|
|
@@ -3229,6 +4032,56 @@ function getVectorCornerWidgets(state) {
|
|
|
3229
4032
|
}
|
|
3230
4033
|
return out;
|
|
3231
4034
|
}
|
|
4035
|
+
function vectorStrokeWidthGeometry(node) {
|
|
4036
|
+
if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;
|
|
4037
|
+
const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
|
|
4038
|
+
return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
|
|
4039
|
+
}
|
|
4040
|
+
function getVectorWidthHandles(state) {
|
|
4041
|
+
if (state.tool !== "width") return [];
|
|
4042
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4043
|
+
for (const id of state.selection) {
|
|
4044
|
+
const n = findVectorNode(state.document.nodes, id);
|
|
4045
|
+
if (n) for (const p of collectPathNodes(n)) ids.add(p.id);
|
|
4046
|
+
}
|
|
4047
|
+
if (state.hover?.nodeId) ids.add(state.hover.nodeId);
|
|
4048
|
+
if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
|
|
4049
|
+
if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
|
|
4050
|
+
const out = [];
|
|
4051
|
+
for (const id of ids) {
|
|
4052
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
4053
|
+
if (node?.type !== "path" || node.visible === false || !node.strokeWidthProfile?.length) continue;
|
|
4054
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
4055
|
+
if (!g) continue;
|
|
4056
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
4057
|
+
const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
|
|
4058
|
+
const contours = node.subpaths.length;
|
|
4059
|
+
for (let sp = 0; sp < contours; sp++) {
|
|
4060
|
+
profile.forEach((w, index) => {
|
|
4061
|
+
const at2 = strokePositionPoint(g.line, sp, w.t);
|
|
4062
|
+
if (!at2) return;
|
|
4063
|
+
const n = { x: at2.tangent[1], y: -at2.tangent[0] };
|
|
4064
|
+
const c = { x: at2.point[0], y: at2.point[1] };
|
|
4065
|
+
const p = applyMatrix(m, c);
|
|
4066
|
+
const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
|
|
4067
|
+
const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
|
|
4068
|
+
const sel = state.selectedWidthPoint;
|
|
4069
|
+
out.push({
|
|
4070
|
+
ref: { nodeId: id, index, subpath: sp },
|
|
4071
|
+
x: p.x,
|
|
4072
|
+
y: p.y,
|
|
4073
|
+
left: l,
|
|
4074
|
+
right: r,
|
|
4075
|
+
t: w.t,
|
|
4076
|
+
leftScale: w.left,
|
|
4077
|
+
rightScale: w.right,
|
|
4078
|
+
selected: !!sel && sel.nodeId === id && sel.index === index
|
|
4079
|
+
});
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
return out;
|
|
4084
|
+
}
|
|
3232
4085
|
function getAnchor(doc, ref) {
|
|
3233
4086
|
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3234
4087
|
return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
|
|
@@ -3299,9 +4152,28 @@ var TOOL_LABELS = {
|
|
|
3299
4152
|
line: "Line",
|
|
3300
4153
|
scissors: "Scissors",
|
|
3301
4154
|
knife: "Knife",
|
|
3302
|
-
eraser: "Eraser"
|
|
4155
|
+
eraser: "Eraser",
|
|
4156
|
+
width: "Width",
|
|
4157
|
+
shapeBuilder: "Shape Builder",
|
|
4158
|
+
paintBucket: "Paint Bucket"
|
|
4159
|
+
};
|
|
4160
|
+
var TOOL_KEYS = {
|
|
4161
|
+
v: "select",
|
|
4162
|
+
a: "direct",
|
|
4163
|
+
p: "pen",
|
|
4164
|
+
n: "pencil",
|
|
4165
|
+
r: "rect",
|
|
4166
|
+
o: "ellipse",
|
|
4167
|
+
y: "polygon",
|
|
4168
|
+
s: "star",
|
|
4169
|
+
l: "line",
|
|
4170
|
+
c: "scissors",
|
|
4171
|
+
k: "knife",
|
|
4172
|
+
e: "eraser",
|
|
4173
|
+
w: "width",
|
|
4174
|
+
m: "shapeBuilder",
|
|
4175
|
+
b: "paintBucket"
|
|
3303
4176
|
};
|
|
3304
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
3305
4177
|
function vectorToolLabel(tool) {
|
|
3306
4178
|
return TOOL_LABELS[tool];
|
|
3307
4179
|
}
|
|
@@ -3317,7 +4189,10 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
3317
4189
|
line: "L",
|
|
3318
4190
|
scissors: "C",
|
|
3319
4191
|
knife: "K",
|
|
3320
|
-
eraser: "E"
|
|
4192
|
+
eraser: "E",
|
|
4193
|
+
width: "W",
|
|
4194
|
+
shapeBuilder: "M",
|
|
4195
|
+
paintBucket: "B"
|
|
3321
4196
|
};
|
|
3322
4197
|
var VECTOR_TOOL_GLYPHS = {
|
|
3323
4198
|
select: "\u2196",
|
|
@@ -3331,7 +4206,10 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
3331
4206
|
line: "\u2571",
|
|
3332
4207
|
scissors: "\u2702",
|
|
3333
4208
|
knife: "\u27CB",
|
|
3334
|
-
eraser: "\u232B"
|
|
4209
|
+
eraser: "\u232B",
|
|
4210
|
+
width: "\u21F9",
|
|
4211
|
+
shapeBuilder: "\u2295",
|
|
4212
|
+
paintBucket: "\u25CD"
|
|
3335
4213
|
};
|
|
3336
4214
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
3337
4215
|
var BOOLEAN_OPS = {
|
|
@@ -3341,6 +4219,14 @@ var BOOLEAN_OPS = {
|
|
|
3341
4219
|
exclude: "xor"
|
|
3342
4220
|
};
|
|
3343
4221
|
var BOOLEAN_LABELS = { union: "Union", subtract: "Subtract", intersect: "Intersect", exclude: "Exclude" };
|
|
4222
|
+
var VECTOR_PATHFINDER_LABELS = {
|
|
4223
|
+
divide: "Divide",
|
|
4224
|
+
trim: "Trim",
|
|
4225
|
+
merge: "Merge",
|
|
4226
|
+
crop: "Crop",
|
|
4227
|
+
outline: "Outline",
|
|
4228
|
+
minusBack: "Minus Back"
|
|
4229
|
+
};
|
|
3344
4230
|
function mapTree(nodes, fn) {
|
|
3345
4231
|
let changed = false;
|
|
3346
4232
|
const out = [];
|
|
@@ -3385,6 +4271,7 @@ var VectorEditorController = class {
|
|
|
3385
4271
|
doc;
|
|
3386
4272
|
selection = [];
|
|
3387
4273
|
anchors = [];
|
|
4274
|
+
widthPoint = null;
|
|
3388
4275
|
editingId = null;
|
|
3389
4276
|
pen = null;
|
|
3390
4277
|
tool;
|
|
@@ -3404,6 +4291,13 @@ var VectorEditorController = class {
|
|
|
3404
4291
|
hoverTargets = null;
|
|
3405
4292
|
spaceHeld = false;
|
|
3406
4293
|
clipboard = null;
|
|
4294
|
+
/** Shape Builder map of the selection, kept while the document and the selection stay the same. */
|
|
4295
|
+
builderCache = null;
|
|
4296
|
+
/** Paint bucket map of the visible outlines. */
|
|
4297
|
+
fillCache = null;
|
|
4298
|
+
builderPreview = null;
|
|
4299
|
+
/** What the highlight shows, to skip notifying when the pointer stays over the same face. */
|
|
4300
|
+
builderHoverKey = null;
|
|
3407
4301
|
createId;
|
|
3408
4302
|
onChange;
|
|
3409
4303
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -3463,6 +4357,7 @@ var VectorEditorController = class {
|
|
|
3463
4357
|
tool: this.tool,
|
|
3464
4358
|
selection: this.selection,
|
|
3465
4359
|
selectedAnchors: this.anchors,
|
|
4360
|
+
selectedWidthPoint: this.widthPoint,
|
|
3466
4361
|
editingId: this.editingId,
|
|
3467
4362
|
selectionBounds: this.boundsOf(this.selection),
|
|
3468
4363
|
selectionFrame: this.frameOf(this.selectedRoots()),
|
|
@@ -3477,6 +4372,7 @@ var VectorEditorController = class {
|
|
|
3477
4372
|
interaction: this.interaction,
|
|
3478
4373
|
pen: this.pen,
|
|
3479
4374
|
pointer: this.pointer,
|
|
4375
|
+
builder: this.builderPreview,
|
|
3480
4376
|
canUndo: h.canUndo,
|
|
3481
4377
|
canRedo: h.canRedo,
|
|
3482
4378
|
undoLabel: h.undoName,
|
|
@@ -3531,6 +4427,15 @@ var VectorEditorController = class {
|
|
|
3531
4427
|
return h?.nodeId ? "move" : "default";
|
|
3532
4428
|
case "pen":
|
|
3533
4429
|
return h?.anchor ? "pointer" : "crosshair";
|
|
4430
|
+
case "width":
|
|
4431
|
+
if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
|
|
4432
|
+
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4433
|
+
return h?.strokeSpot ? "copy" : "default";
|
|
4434
|
+
case "shapeBuilder":
|
|
4435
|
+
if (!this.builderPreview?.faces.length && !this.builderPreview?.lines.length) return "crosshair";
|
|
4436
|
+
return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
|
|
4437
|
+
case "paintBucket":
|
|
4438
|
+
return this.builderPreview ? "cell" : "crosshair";
|
|
3534
4439
|
default:
|
|
3535
4440
|
return "crosshair";
|
|
3536
4441
|
}
|
|
@@ -3656,6 +4561,9 @@ var VectorEditorController = class {
|
|
|
3656
4561
|
this.tool = tool;
|
|
3657
4562
|
this.hover = null;
|
|
3658
4563
|
this.guides = null;
|
|
4564
|
+
this.widthPoint = null;
|
|
4565
|
+
this.builderPreview = null;
|
|
4566
|
+
this.builderHoverKey = null;
|
|
3659
4567
|
if (tool !== "direct") {
|
|
3660
4568
|
this.anchors = [];
|
|
3661
4569
|
this.editingId = null;
|
|
@@ -3887,6 +4795,7 @@ var VectorEditorController = class {
|
|
|
3887
4795
|
if (d.fillRule) out.fillRule = d.fillRule;
|
|
3888
4796
|
if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
|
|
3889
4797
|
for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
|
|
4798
|
+
if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
|
|
3890
4799
|
return out;
|
|
3891
4800
|
}
|
|
3892
4801
|
/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
|
|
@@ -3907,10 +4816,39 @@ var VectorEditorController = class {
|
|
|
3907
4816
|
}
|
|
3908
4817
|
if (!targets.size) return { ok: false, message: "Nothing is selected." };
|
|
3909
4818
|
this.change(options.label ?? "Change Style", () => {
|
|
3910
|
-
this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ?
|
|
4819
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? withStyle(n, style) : n));
|
|
3911
4820
|
});
|
|
3912
4821
|
return { ok: true };
|
|
3913
4822
|
}
|
|
4823
|
+
/**
|
|
4824
|
+
* Sets the variable-width profile of the selected (or given) paths: a preset id
|
|
4825
|
+
* (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for
|
|
4826
|
+
* an unknown preset.
|
|
4827
|
+
*/
|
|
4828
|
+
setWidthProfile(profile, options = {}) {
|
|
4829
|
+
let points;
|
|
4830
|
+
if (profile === null) points = [];
|
|
4831
|
+
else if (typeof profile === "string") {
|
|
4832
|
+
const preset = widthProfilePreset(profile);
|
|
4833
|
+
if (!preset) return { ok: false, message: `There is no width profile "${profile}".` };
|
|
4834
|
+
points = preset;
|
|
4835
|
+
} else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);
|
|
4836
|
+
else {
|
|
4837
|
+
const p = profile;
|
|
4838
|
+
points = p.taper || p.wave ? generateWidthProfile(p) : [];
|
|
4839
|
+
}
|
|
4840
|
+
this.widthPoint = null;
|
|
4841
|
+
return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
|
|
4842
|
+
}
|
|
4843
|
+
/** Removes one width point of a path's profile (the width tool's Delete). */
|
|
4844
|
+
removeWidthPoint(ref) {
|
|
4845
|
+
const node = this.getNode(ref.nodeId);
|
|
4846
|
+
if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
|
|
4847
|
+
const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
|
|
4848
|
+
this.widthPoint = null;
|
|
4849
|
+
this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
|
|
4850
|
+
return { ok: true };
|
|
4851
|
+
}
|
|
3914
4852
|
/**
|
|
3915
4853
|
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
3916
4854
|
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
@@ -3981,6 +4919,7 @@ var VectorEditorController = class {
|
|
|
3981
4919
|
arrowAlign: _al,
|
|
3982
4920
|
startArrowScale: _ss,
|
|
3983
4921
|
endArrowScale: _es,
|
|
4922
|
+
strokeWidthProfile: _wp,
|
|
3984
4923
|
...rest
|
|
3985
4924
|
} = cloneVectorNode(p);
|
|
3986
4925
|
const id = this.newId();
|
|
@@ -4282,7 +5221,7 @@ var VectorEditorController = class {
|
|
|
4282
5221
|
if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
|
|
4283
5222
|
const set = new Set(groups.map((g) => g.id));
|
|
4284
5223
|
this.change("Ungroup", () => {
|
|
4285
|
-
const
|
|
5224
|
+
const flatten2 = (list) => {
|
|
4286
5225
|
const out = [];
|
|
4287
5226
|
let changed = false;
|
|
4288
5227
|
for (const n of list) {
|
|
@@ -4294,14 +5233,14 @@ var VectorEditorController = class {
|
|
|
4294
5233
|
...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
|
|
4295
5234
|
);
|
|
4296
5235
|
} else if (n.type === "group") {
|
|
4297
|
-
const children =
|
|
5236
|
+
const children = flatten2(n.children);
|
|
4298
5237
|
if (children !== n.children) changed = true;
|
|
4299
5238
|
out.push(children !== n.children ? { ...n, children } : n);
|
|
4300
5239
|
} else out.push(n);
|
|
4301
5240
|
}
|
|
4302
5241
|
return changed ? out : list;
|
|
4303
5242
|
};
|
|
4304
|
-
this.setDoc(
|
|
5243
|
+
this.setDoc(flatten2(this.doc.nodes));
|
|
4305
5244
|
this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
|
|
4306
5245
|
});
|
|
4307
5246
|
return { ok: true };
|
|
@@ -4412,6 +5351,248 @@ var VectorEditorController = class {
|
|
|
4412
5351
|
return { ok: true };
|
|
4413
5352
|
}
|
|
4414
5353
|
// -------------------------------------------------------------------------
|
|
5354
|
+
// Pathfinder, Shape Builder and paint bucket
|
|
5355
|
+
// -------------------------------------------------------------------------
|
|
5356
|
+
/**
|
|
5357
|
+
* Pathfinder: Divide, Trim, Merge, Crop, Outline or Minus Back over the selected (or given)
|
|
5358
|
+
* paths, in document space (rotated nodes and groups included, live corners cut). Unfilled
|
|
5359
|
+
* stroked paths take part as the area their stroke paints (Divide: open ones cut instead).
|
|
5360
|
+
* The results replace the operands at the place of the top-most one, grouped when there are
|
|
5361
|
+
* several; each keeps the style of the operand whose area it is. Returns the new path ids,
|
|
5362
|
+
* or an empty list when nothing came out.
|
|
5363
|
+
*/
|
|
5364
|
+
pathfinder(op, options = {}) {
|
|
5365
|
+
const roots = options.ids ?? this.selectedRoots();
|
|
5366
|
+
const entries = vectorPathEntries(this.doc, roots);
|
|
5367
|
+
if (entries.length < (op === "divide" || op === "outline" ? 1 : 2)) return [];
|
|
5368
|
+
const shapes = entries.map((e) => vectorPathfinderShape(e, op));
|
|
5369
|
+
const result = pathfinder(op, shapes);
|
|
5370
|
+
if (result.error || !result.pieces.length) return [];
|
|
5371
|
+
const last = entries[entries.length - 1].node.id;
|
|
5372
|
+
const topRoot = [...roots].reverse().find((r) => {
|
|
5373
|
+
const n = this.getNode(r);
|
|
5374
|
+
return !!n && !!findVectorNode([n], last);
|
|
5375
|
+
}) ?? last;
|
|
5376
|
+
const nodes = [];
|
|
5377
|
+
for (const piece of result.pieces) {
|
|
5378
|
+
const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
|
|
5379
|
+
if (!subpaths.length) continue;
|
|
5380
|
+
const source = entries[piece.source].node;
|
|
5381
|
+
const style = vectorResultStyle(source, shapes[piece.source].paint, op === "outline");
|
|
5382
|
+
nodes.push({ ...style, type: "path", id: this.newId(), name: source.name ?? "Path", subpaths });
|
|
5383
|
+
}
|
|
5384
|
+
if (!nodes.length) return [];
|
|
5385
|
+
const label = VECTOR_PATHFINDER_LABELS[op];
|
|
5386
|
+
const container = nodes.length > 1 ? { id: this.newId(), type: "group", name: label, children: nodes } : nodes[0];
|
|
5387
|
+
const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
|
|
5388
|
+
this.change(label, () => {
|
|
5389
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === topRoot ? container : drop.has(n.id) ? null : n));
|
|
5390
|
+
this.selection = [container.id];
|
|
5391
|
+
this.anchors = [];
|
|
5392
|
+
this.editingId = null;
|
|
5393
|
+
});
|
|
5394
|
+
return nodes.map((n) => n.id);
|
|
5395
|
+
}
|
|
5396
|
+
/** The Shape Builder map of the selected (or given) paths, cached per document and selection. */
|
|
5397
|
+
shapeBuilderMap(ids) {
|
|
5398
|
+
const roots = ids ?? this.selectedRoots();
|
|
5399
|
+
const key = roots.join("\0");
|
|
5400
|
+
if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
|
|
5401
|
+
const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
|
|
5402
|
+
const map = entries.length ? vectorShapeBuilderMap(entries) : null;
|
|
5403
|
+
this.builderCache = { doc: this.doc, key, map };
|
|
5404
|
+
return map;
|
|
5405
|
+
}
|
|
5406
|
+
/** The faces the Shape Builder offers for the selected (or given) paths (document units). */
|
|
5407
|
+
getShapeBuilderFaces(options = {}) {
|
|
5408
|
+
const m = this.shapeBuilderMap(options.ids);
|
|
5409
|
+
if (!m) return [];
|
|
5410
|
+
return m.map.faces.map((f, index) => {
|
|
5411
|
+
const p = m.map.faceInterior(index);
|
|
5412
|
+
return {
|
|
5413
|
+
index,
|
|
5414
|
+
d: bezierPathToSvg(f.path, { precision: 3 }),
|
|
5415
|
+
area: f.area,
|
|
5416
|
+
sources: m.map.faceSources(index).map((i) => m.entries[i].node.id),
|
|
5417
|
+
point: p ? { x: p[0], y: p[1] } : null
|
|
5418
|
+
};
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
/** Pick distance of the Shape Builder's erase for line pieces (document units). */
|
|
5422
|
+
builderLineTolerance() {
|
|
5423
|
+
return Math.max(this.hitRadius, 4) / this.viewport.zoom;
|
|
5424
|
+
}
|
|
5425
|
+
/**
|
|
5426
|
+
* Shape Builder gesture along `points` (document units; one point is a click) over the faces
|
|
5427
|
+
* the selected (or given) paths make. Merges the faces touched into one shape styled like the
|
|
5428
|
+
* top-most shape over the first face (the current fill for a face only outlines enclose);
|
|
5429
|
+
* with `erase` deletes them, and the pieces of unfilled open paths within `tolerance`. Every
|
|
5430
|
+
* shape covering a touched face loses that area. One undo step.
|
|
5431
|
+
*/
|
|
5432
|
+
shapeBuilder(points, options = {}) {
|
|
5433
|
+
const none = (message) => ({ ok: false, message, merged: null, faces: 0, lines: 0 });
|
|
5434
|
+
if (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none("Give the points of the gesture.");
|
|
5435
|
+
const m = this.shapeBuilderMap(options.ids);
|
|
5436
|
+
if (!m) return none("Select paths that make faces.");
|
|
5437
|
+
const erase = !!options.erase;
|
|
5438
|
+
const tol = erase ? options.tolerance ?? this.builderLineTolerance() : null;
|
|
5439
|
+
const touched = { faces: [], lines: [] };
|
|
5440
|
+
const size = m.map.faces.reduce((s, f) => Math.max(s, f.bounds.maxX - f.bounds.minX, f.bounds.maxY - f.bounds.minY), 1);
|
|
5441
|
+
for (const p of samplePolyline(points, Math.max(size / 2e3, 0.25 / this.viewport.zoom))) addBuilderHit(touched, vectorShapeBuilderHit(m, p, tol));
|
|
5442
|
+
if (!touched.faces.length && !touched.lines.length) return none("There is no face under the pointer.");
|
|
5443
|
+
return this.applyShapeBuilder(m, touched, erase);
|
|
5444
|
+
}
|
|
5445
|
+
applyShapeBuilder(m, touched, erase) {
|
|
5446
|
+
const plan = planShapeBuilder(m, touched, erase);
|
|
5447
|
+
let mergedId = null;
|
|
5448
|
+
const created = /* @__PURE__ */ new Map();
|
|
5449
|
+
this.change(erase ? "Shape Builder Delete" : "Shape Builder", () => {
|
|
5450
|
+
let nodes = this.doc.nodes;
|
|
5451
|
+
m.entries.forEach((entry, i) => {
|
|
5452
|
+
const node = entry.node;
|
|
5453
|
+
const toLocal = (path) => transformBezierPath(path, invertMatrix(multiplyMatrices(nodeMatrix(node), entry.parent)));
|
|
5454
|
+
const list = [];
|
|
5455
|
+
const r = plan.replace.get(i);
|
|
5456
|
+
if (r === void 0) list.push(node);
|
|
5457
|
+
else if (r?.kind === "fill") list.push({ ...node, fillRule: "nonzero", subpaths: bezierToSubpaths(toLocal(r.path)) });
|
|
5458
|
+
else if (r?.kind === "pieces") {
|
|
5459
|
+
r.paths.forEach(
|
|
5460
|
+
(path, k) => list.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })
|
|
5461
|
+
);
|
|
5462
|
+
}
|
|
5463
|
+
if (plan.merged && plan.merged.style === i) {
|
|
5464
|
+
mergedId = this.newId();
|
|
5465
|
+
const merged = {
|
|
5466
|
+
...vectorResultStyle(node, "fill"),
|
|
5467
|
+
type: "path",
|
|
5468
|
+
id: mergedId,
|
|
5469
|
+
name: "Shape",
|
|
5470
|
+
...node.transform ? { transform: node.transform } : {},
|
|
5471
|
+
subpaths: bezierToSubpaths(toLocal(plan.merged.path))
|
|
5472
|
+
};
|
|
5473
|
+
if (plan.merged.currentFill || !hasPaint(merged.fill)) merged.fill = hasPaint(this.defaults.fill) ? this.defaults.fill : "#808080";
|
|
5474
|
+
list.push(merged);
|
|
5475
|
+
}
|
|
5476
|
+
if (r === void 0 && list.length === 1) return;
|
|
5477
|
+
created.set(node.id, list.map((n) => n.id));
|
|
5478
|
+
nodes = spliceTree(nodes, node.id, list);
|
|
5479
|
+
});
|
|
5480
|
+
this.setDoc(nodes);
|
|
5481
|
+
const selection = this.selection.flatMap((id) => created.get(id) ?? [id]);
|
|
5482
|
+
if (mergedId && !selection.includes(mergedId)) selection.push(mergedId);
|
|
5483
|
+
this.selection = selection.filter((id, k) => !!this.getNode(id) && selection.indexOf(id) === k);
|
|
5484
|
+
this.anchors = [];
|
|
5485
|
+
this.editingId = null;
|
|
5486
|
+
});
|
|
5487
|
+
this.builderCache = null;
|
|
5488
|
+
return { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length };
|
|
5489
|
+
}
|
|
5490
|
+
/** The paint bucket's map of the visible outlines, cached per document and gap. */
|
|
5491
|
+
fillMap(gap) {
|
|
5492
|
+
if (this.fillCache?.doc === this.doc && this.fillCache.gap === gap) return this.fillCache.map;
|
|
5493
|
+
const map = vectorFillMap(this.doc, gap);
|
|
5494
|
+
this.fillCache = { doc: this.doc, gap, map };
|
|
5495
|
+
return map;
|
|
5496
|
+
}
|
|
5497
|
+
/**
|
|
5498
|
+
* Paint bucket: the region the visible outlines enclose around `point` (document units),
|
|
5499
|
+
* crossings split and islands left as holes, becomes a new path filled with the current fill
|
|
5500
|
+
* default, placed just below the selection (or on top when nothing is selected). `gap`
|
|
5501
|
+
* (default `editing.fillGap`) closes gaps up to that size between loose ends. Returns the new
|
|
5502
|
+
* path's id, or null when nothing encloses the point.
|
|
5503
|
+
*/
|
|
5504
|
+
fillRegion(point, options = {}) {
|
|
5505
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
|
|
5506
|
+
const region = this.fillMap(options.gap ?? this.editing.fillGap)?.regionAt([point.x, point.y]);
|
|
5507
|
+
if (!region?.length) return null;
|
|
5508
|
+
let below = null;
|
|
5509
|
+
const roots = new Set(this.selectedRoots());
|
|
5510
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
5511
|
+
if (!below && roots.has(n.id)) below = n.id;
|
|
5512
|
+
});
|
|
5513
|
+
const subpaths = bezierToSubpaths(below ? this.intoParentSpace(below, region) : region);
|
|
5514
|
+
if (!subpaths.length) return null;
|
|
5515
|
+
const id = this.newId();
|
|
5516
|
+
const node = { id, type: "path", name: "Fill", ...this.styleFor(false), stroke: null, fillRule: "nonzero", subpaths };
|
|
5517
|
+
if (!hasPaint(node.fill)) node.fill = "#808080";
|
|
5518
|
+
const target = below;
|
|
5519
|
+
this.change("Fill Region", () => {
|
|
5520
|
+
if (target) {
|
|
5521
|
+
const current = this.getNode(target);
|
|
5522
|
+
this.setDoc(spliceTree(this.doc.nodes, target, [node, current]));
|
|
5523
|
+
} else this.setDoc([...this.doc.nodes, node]);
|
|
5524
|
+
if (!target) this.selection = [id];
|
|
5525
|
+
this.anchors = [];
|
|
5526
|
+
this.editingId = null;
|
|
5527
|
+
});
|
|
5528
|
+
return id;
|
|
5529
|
+
}
|
|
5530
|
+
/** Updates the Shape Builder / paint bucket highlight for the pointer at `p`; true when it changed. */
|
|
5531
|
+
hoverBuilder(p, alt) {
|
|
5532
|
+
if (this.tool === "paintBucket") {
|
|
5533
|
+
const region = this.fillMap(this.editing.fillGap)?.regionAt([p.x, p.y]) ?? null;
|
|
5534
|
+
const key2 = region ? bezierPathToSvg(region, { precision: 2 }) : null;
|
|
5535
|
+
if (key2 === this.builderHoverKey) return false;
|
|
5536
|
+
this.builderHoverKey = key2;
|
|
5537
|
+
this.builderPreview = region ? { mode: "fill", faces: [region], lines: [], trail: null } : null;
|
|
5538
|
+
return true;
|
|
5539
|
+
}
|
|
5540
|
+
const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
|
|
5541
|
+
const hit = m ? vectorShapeBuilderHit(m, p, alt ? this.builderLineTolerance() : null) : null;
|
|
5542
|
+
const key = hit ? `${alt ? "erase" : "merge"}:${JSON.stringify(hit)}` : null;
|
|
5543
|
+
if (key === this.builderHoverKey) return false;
|
|
5544
|
+
this.builderHoverKey = key;
|
|
5545
|
+
this.builderPreview = hit && m ? this.builderPreviewOf(m, "face" in hit ? { faces: [hit.face], lines: [] } : { faces: [], lines: [hit.line] }, alt, null) : null;
|
|
5546
|
+
return true;
|
|
5547
|
+
}
|
|
5548
|
+
builderPreviewOf(m, touched, erase, trail) {
|
|
5549
|
+
const faces = m.map.faces;
|
|
5550
|
+
return {
|
|
5551
|
+
mode: erase ? "erase" : "merge",
|
|
5552
|
+
faces: touched.faces.map((f) => faces[f].path),
|
|
5553
|
+
lines: touched.lines.map((k) => m.pieces[k].path),
|
|
5554
|
+
trail
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
downBuilder(p, input) {
|
|
5558
|
+
this.builderHoverKey = null;
|
|
5559
|
+
if (this.tool === "paintBucket") {
|
|
5560
|
+
this.fillRegion(p);
|
|
5561
|
+
this.builderPreview = null;
|
|
5562
|
+
this.hoverBuilder(p, false);
|
|
5563
|
+
this.notify();
|
|
5564
|
+
return;
|
|
5565
|
+
}
|
|
5566
|
+
const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
|
|
5567
|
+
if (!m) return;
|
|
5568
|
+
const erase = !!input.alt;
|
|
5569
|
+
const touched = { faces: [], lines: [] };
|
|
5570
|
+
addBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));
|
|
5571
|
+
this.drag = { kind: "builder", erase, points: [p], touched, map: m };
|
|
5572
|
+
this.interaction = { type: "builder", mode: erase ? "erase" : "merge", points: [p] };
|
|
5573
|
+
this.builderPreview = this.builderPreviewOf(m, touched, erase, [p]);
|
|
5574
|
+
this.notify();
|
|
5575
|
+
}
|
|
5576
|
+
moveBuilder(drag, p) {
|
|
5577
|
+
const last = drag.points[drag.points.length - 1];
|
|
5578
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1) return;
|
|
5579
|
+
const tol = drag.erase ? this.builderLineTolerance() : null;
|
|
5580
|
+
for (const q of samplePolyline([last, p], 2 / this.viewport.zoom)) addBuilderHit(drag.touched, vectorShapeBuilderHit(drag.map, q, tol));
|
|
5581
|
+
drag.points.push(p);
|
|
5582
|
+
this.interaction = { type: "builder", mode: drag.erase ? "erase" : "merge", points: drag.points.slice() };
|
|
5583
|
+
this.builderPreview = this.builderPreviewOf(drag.map, drag.touched, drag.erase, drag.points.slice());
|
|
5584
|
+
this.notify();
|
|
5585
|
+
}
|
|
5586
|
+
upBuilder(drag) {
|
|
5587
|
+
this.builderPreview = null;
|
|
5588
|
+
this.builderHoverKey = null;
|
|
5589
|
+
if (!drag.touched.faces.length && !drag.touched.lines.length) {
|
|
5590
|
+
this.notify();
|
|
5591
|
+
return;
|
|
5592
|
+
}
|
|
5593
|
+
this.applyShapeBuilder(drag.map, drag.touched, drag.erase);
|
|
5594
|
+
}
|
|
5595
|
+
// -------------------------------------------------------------------------
|
|
4415
5596
|
// Anchor editing
|
|
4416
5597
|
// -------------------------------------------------------------------------
|
|
4417
5598
|
updateAnchors(doc, refs, fn) {
|
|
@@ -4571,7 +5752,12 @@ var VectorEditorController = class {
|
|
|
4571
5752
|
const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
|
|
4572
5753
|
if (!set.size) return { ok: false, message: "Select a path." };
|
|
4573
5754
|
this.change("Reverse Path", () => {
|
|
4574
|
-
this.setDoc(
|
|
5755
|
+
this.setDoc(
|
|
5756
|
+
mapTree(
|
|
5757
|
+
this.doc.nodes,
|
|
5758
|
+
(n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {} } : n
|
|
5759
|
+
)
|
|
5760
|
+
);
|
|
4575
5761
|
this.anchors = [];
|
|
4576
5762
|
});
|
|
4577
5763
|
return { ok: true };
|
|
@@ -4652,9 +5838,9 @@ var VectorEditorController = class {
|
|
|
4652
5838
|
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
4653
5839
|
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
4654
5840
|
this.change("Average", () => {
|
|
4655
|
-
for (const { ref, at } of points) {
|
|
4656
|
-
const dx = axis === "horizontal" ? 0 : cx -
|
|
4657
|
-
const dy = axis === "vertical" ? 0 : cy -
|
|
5841
|
+
for (const { ref, at: at2 } of points) {
|
|
5842
|
+
const dx = axis === "horizontal" ? 0 : cx - at2.x;
|
|
5843
|
+
const dy = axis === "vertical" ? 0 : cy - at2.y;
|
|
4658
5844
|
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
4659
5845
|
}
|
|
4660
5846
|
});
|
|
@@ -4738,8 +5924,8 @@ var VectorEditorController = class {
|
|
|
4738
5924
|
const node = this.getNode(id);
|
|
4739
5925
|
if (node.locked) continue;
|
|
4740
5926
|
const ids = this.applyCuts(node, (sp, i) => {
|
|
4741
|
-
const
|
|
4742
|
-
const pieces =
|
|
5927
|
+
const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
5928
|
+
const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
|
|
4743
5929
|
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4744
5930
|
});
|
|
4745
5931
|
if (ids) created.push(...ids);
|
|
@@ -4943,8 +6129,8 @@ var VectorEditorController = class {
|
|
|
4943
6129
|
if (!frame) return null;
|
|
4944
6130
|
const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
|
|
4945
6131
|
const r = this.hitRadius + 2;
|
|
4946
|
-
const
|
|
4947
|
-
for (const h of
|
|
6132
|
+
const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
|
|
6133
|
+
for (const h of usable2) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
|
|
4948
6134
|
return null;
|
|
4949
6135
|
}
|
|
4950
6136
|
hitHandle(screen) {
|
|
@@ -5053,10 +6239,131 @@ var VectorEditorController = class {
|
|
|
5053
6239
|
const seg = this.nearestOutline(p);
|
|
5054
6240
|
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
5055
6241
|
}
|
|
6242
|
+
case "width": {
|
|
6243
|
+
const hit = this.hitWidthHandle(screen);
|
|
6244
|
+
if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
|
|
6245
|
+
const spot = this.strokeSpotAt(p);
|
|
6246
|
+
return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
|
|
6247
|
+
}
|
|
5056
6248
|
default:
|
|
5057
6249
|
return null;
|
|
5058
6250
|
}
|
|
5059
6251
|
}
|
|
6252
|
+
widthView() {
|
|
6253
|
+
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
|
|
6254
|
+
}
|
|
6255
|
+
/** The width point centre or side end under `screen` (width tool). */
|
|
6256
|
+
hitWidthHandle(screen) {
|
|
6257
|
+
let best = null;
|
|
6258
|
+
let bestD = Infinity;
|
|
6259
|
+
for (const h of getVectorWidthHandles(this.widthView())) {
|
|
6260
|
+
for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
|
|
6261
|
+
const s = this.documentToScreen(q);
|
|
6262
|
+
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
6263
|
+
if (d <= this.hitRadius && d < bestD - 1e-9) {
|
|
6264
|
+
best = { ref: { ...h.ref, part } };
|
|
6265
|
+
bestD = d;
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
return best;
|
|
6270
|
+
}
|
|
6271
|
+
/** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */
|
|
6272
|
+
strokeSpotAt(p) {
|
|
6273
|
+
let best = null;
|
|
6274
|
+
const tol = this.tolerance();
|
|
6275
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
6276
|
+
if (n.type !== "path" || n.visible === false || n.locked) return;
|
|
6277
|
+
if (hiddenOrLocked(this.doc.nodes, n.id)) return;
|
|
6278
|
+
const g = vectorStrokeWidthGeometry(n);
|
|
6279
|
+
if (!g) return;
|
|
6280
|
+
const m = this.worldOf(n.id);
|
|
6281
|
+
const local = applyMatrix(invertMatrix(m), p);
|
|
6282
|
+
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6283
|
+
if (!near2) return;
|
|
6284
|
+
const profile = strokeWidthProfileOf(n);
|
|
6285
|
+
const w = widthProfileAt(profile, near2.t);
|
|
6286
|
+
const reach = Math.max(near2.side === "left" ? w.left : w.right, 0) * g.unit + tol;
|
|
6287
|
+
if (near2.distance > reach || near2.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
|
|
6288
|
+
if (best && near2.distance >= best.distance) return;
|
|
6289
|
+
const nx = near2.tangent[1];
|
|
6290
|
+
const ny = -near2.tangent[0];
|
|
6291
|
+
const [cx, cy] = near2.point;
|
|
6292
|
+
best = {
|
|
6293
|
+
nodeId: n.id,
|
|
6294
|
+
subpath: near2.contour,
|
|
6295
|
+
t: near2.t,
|
|
6296
|
+
point: applyMatrix(m, { x: cx, y: cy }),
|
|
6297
|
+
left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
|
|
6298
|
+
right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
|
|
6299
|
+
distance: near2.distance
|
|
6300
|
+
};
|
|
6301
|
+
});
|
|
6302
|
+
if (!best) return null;
|
|
6303
|
+
const { distance: _d, ...spot } = best;
|
|
6304
|
+
return spot;
|
|
6305
|
+
}
|
|
6306
|
+
downWidth(screen, p, input) {
|
|
6307
|
+
const hit = this.hitWidthHandle(screen);
|
|
6308
|
+
const spot = hit ? null : this.strokeSpotAt(p);
|
|
6309
|
+
const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
|
|
6310
|
+
const node = nodeId ? this.getNode(nodeId) : void 0;
|
|
6311
|
+
const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
|
|
6312
|
+
if (!node || node.type !== "path" || !g) {
|
|
6313
|
+
if (this.widthPoint) {
|
|
6314
|
+
this.widthPoint = null;
|
|
6315
|
+
this.notify();
|
|
6316
|
+
}
|
|
6317
|
+
return;
|
|
6318
|
+
}
|
|
6319
|
+
const before = this.snapshot();
|
|
6320
|
+
const local = this.toLocal(node.id, p);
|
|
6321
|
+
let profile;
|
|
6322
|
+
let ref;
|
|
6323
|
+
let mode = "width";
|
|
6324
|
+
let side = "both";
|
|
6325
|
+
let t;
|
|
6326
|
+
let subpath;
|
|
6327
|
+
if (hit) {
|
|
6328
|
+
profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
6329
|
+
ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
|
|
6330
|
+
t = profile[ref.index]?.t ?? 0;
|
|
6331
|
+
subpath = hit.ref.subpath;
|
|
6332
|
+
if (hit.ref.part === "center") mode = "move";
|
|
6333
|
+
else if (input.alt) side = hit.ref.part;
|
|
6334
|
+
} else {
|
|
6335
|
+
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6336
|
+
const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
|
|
6337
|
+
profile = inserted.points;
|
|
6338
|
+
subpath = near2.contour;
|
|
6339
|
+
t = near2.t;
|
|
6340
|
+
ref = { nodeId: node.id, index: inserted.index, subpath };
|
|
6341
|
+
if (input.alt) side = near2.side;
|
|
6342
|
+
}
|
|
6343
|
+
const at2 = strokePositionPoint(g.line, subpath, t);
|
|
6344
|
+
if (!at2) return;
|
|
6345
|
+
const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6346
|
+
this.doc = base;
|
|
6347
|
+
this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
|
|
6348
|
+
this.drag = {
|
|
6349
|
+
kind: "width",
|
|
6350
|
+
ref,
|
|
6351
|
+
mode,
|
|
6352
|
+
side,
|
|
6353
|
+
origin: { x: at2.point[0], y: at2.point[1] },
|
|
6354
|
+
normal: { x: at2.tangent[1], y: -at2.tangent[0] },
|
|
6355
|
+
unit: g.unit,
|
|
6356
|
+
profile,
|
|
6357
|
+
before,
|
|
6358
|
+
base,
|
|
6359
|
+
start: p,
|
|
6360
|
+
moved: false,
|
|
6361
|
+
created: !hit
|
|
6362
|
+
};
|
|
6363
|
+
const w = profile[ref.index];
|
|
6364
|
+
this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
|
|
6365
|
+
this.notify(!hit);
|
|
6366
|
+
}
|
|
5060
6367
|
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
5061
6368
|
nearestOutline(p) {
|
|
5062
6369
|
let best = null;
|
|
@@ -5104,7 +6411,12 @@ var VectorEditorController = class {
|
|
|
5104
6411
|
case "pen":
|
|
5105
6412
|
return this.downPen(screen, p, input);
|
|
5106
6413
|
case "pencil":
|
|
5107
|
-
return this.downPencil(p);
|
|
6414
|
+
return this.downPencil(p, input);
|
|
6415
|
+
case "width":
|
|
6416
|
+
return this.downWidth(screen, p, input);
|
|
6417
|
+
case "shapeBuilder":
|
|
6418
|
+
case "paintBucket":
|
|
6419
|
+
return this.downBuilder(p, input);
|
|
5108
6420
|
case "scissors": {
|
|
5109
6421
|
const seg = this.nearestOutline(p);
|
|
5110
6422
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -5297,7 +6609,7 @@ var VectorEditorController = class {
|
|
|
5297
6609
|
this.interaction = { type: "pen", nodeId: node.id };
|
|
5298
6610
|
this.notify(true);
|
|
5299
6611
|
}
|
|
5300
|
-
downPencil(p) {
|
|
6612
|
+
downPencil(p, input = {}) {
|
|
5301
6613
|
const before = this.snapshot();
|
|
5302
6614
|
const node = {
|
|
5303
6615
|
id: this.newId(),
|
|
@@ -5311,7 +6623,11 @@ var VectorEditorController = class {
|
|
|
5311
6623
|
this.setDoc([...this.doc.nodes, node]);
|
|
5312
6624
|
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
5313
6625
|
lazy.push([p.x, p.y]);
|
|
5314
|
-
|
|
6626
|
+
const mode = this.editing.pencilPressure;
|
|
6627
|
+
const hasPressure = typeof input.pressure === "number" && Number.isFinite(input.pressure);
|
|
6628
|
+
const record = hasPressure && (mode === "all" || mode === "pen" && input.pointerType === "pen");
|
|
6629
|
+
const pressure = hasPressure ? input.pressure : 0.5;
|
|
6630
|
+
this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };
|
|
5315
6631
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
5316
6632
|
this.notify(true);
|
|
5317
6633
|
}
|
|
@@ -5330,6 +6646,11 @@ var VectorEditorController = class {
|
|
|
5330
6646
|
pointerMove(screen, input = {}) {
|
|
5331
6647
|
const p = this.screenToDocument(screen);
|
|
5332
6648
|
const drag = this.drag;
|
|
6649
|
+
if (!drag && (this.tool === "shapeBuilder" || this.tool === "paintBucket")) {
|
|
6650
|
+
this.pointer = p;
|
|
6651
|
+
if (this.hoverBuilder(p, !!input.alt)) this.notify();
|
|
6652
|
+
return;
|
|
6653
|
+
}
|
|
5333
6654
|
if (!drag) {
|
|
5334
6655
|
const hover = this.computeHover(screen);
|
|
5335
6656
|
const pointerMatters = this.tool === "pen" && !!this.pen;
|
|
@@ -5461,6 +6782,8 @@ var VectorEditorController = class {
|
|
|
5461
6782
|
this.notify(true);
|
|
5462
6783
|
return;
|
|
5463
6784
|
}
|
|
6785
|
+
case "builder":
|
|
6786
|
+
return this.moveBuilder(drag, p);
|
|
5464
6787
|
case "stroke": {
|
|
5465
6788
|
const last = drag.points[drag.points.length - 1];
|
|
5466
6789
|
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
@@ -5520,12 +6843,36 @@ var VectorEditorController = class {
|
|
|
5520
6843
|
this.notify(true);
|
|
5521
6844
|
return;
|
|
5522
6845
|
}
|
|
6846
|
+
case "width": {
|
|
6847
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
6848
|
+
drag.moved = true;
|
|
6849
|
+
const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
|
|
6850
|
+
const local = this.toLocal(node.id, p, drag.base);
|
|
6851
|
+
let profile;
|
|
6852
|
+
if (drag.mode === "move") {
|
|
6853
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
6854
|
+
const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
|
|
6855
|
+
if (!near2) return;
|
|
6856
|
+
profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
|
|
6857
|
+
} else {
|
|
6858
|
+
const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
|
|
6859
|
+
const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
|
|
6860
|
+
profile = setWidthPoint(drag.profile, drag.ref.index, patch);
|
|
6861
|
+
}
|
|
6862
|
+
this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6863
|
+
const w = profile[drag.ref.index];
|
|
6864
|
+
this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
|
|
6865
|
+
this.notify(true);
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
5523
6868
|
case "pencil": {
|
|
5524
6869
|
const last = drag.points[drag.points.length - 1];
|
|
5525
6870
|
let moved = false;
|
|
6871
|
+
if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
|
|
5526
6872
|
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
5527
6873
|
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
5528
6874
|
drag.points.push(q);
|
|
6875
|
+
drag.pressures?.push(drag.pressure);
|
|
5529
6876
|
moved = true;
|
|
5530
6877
|
}
|
|
5531
6878
|
const string = drag.lazy.string();
|
|
@@ -5606,7 +6953,7 @@ var VectorEditorController = class {
|
|
|
5606
6953
|
}
|
|
5607
6954
|
/** Primary button released. */
|
|
5608
6955
|
pointerUp(screen, input = {}) {
|
|
5609
|
-
if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
|
|
6956
|
+
if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, { ...input, pressure: void 0 });
|
|
5610
6957
|
const drag = this.drag;
|
|
5611
6958
|
this.drag = null;
|
|
5612
6959
|
this.interaction = null;
|
|
@@ -5658,6 +7005,17 @@ var VectorEditorController = class {
|
|
|
5658
7005
|
this.notify();
|
|
5659
7006
|
return;
|
|
5660
7007
|
}
|
|
7008
|
+
case "width": {
|
|
7009
|
+
if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
|
|
7010
|
+
if (drag.created) {
|
|
7011
|
+
this.doc = drag.before.doc;
|
|
7012
|
+
this.widthPoint = null;
|
|
7013
|
+
this.notify(true);
|
|
7014
|
+
} else this.notify();
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
case "builder":
|
|
7018
|
+
return this.upBuilder(drag);
|
|
5661
7019
|
case "stroke": {
|
|
5662
7020
|
this.notify();
|
|
5663
7021
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -5688,7 +7046,10 @@ var VectorEditorController = class {
|
|
|
5688
7046
|
this.notify(true);
|
|
5689
7047
|
return;
|
|
5690
7048
|
}
|
|
5691
|
-
for (const q of drag.lazy.finish())
|
|
7049
|
+
for (const q of drag.lazy.finish()) {
|
|
7050
|
+
drag.points.push(q);
|
|
7051
|
+
drag.pressures?.push(drag.pressure);
|
|
7052
|
+
}
|
|
5692
7053
|
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
5693
7054
|
if (shape) {
|
|
5694
7055
|
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
@@ -5703,7 +7064,8 @@ var VectorEditorController = class {
|
|
|
5703
7064
|
anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
|
|
5704
7065
|
}
|
|
5705
7066
|
const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
|
|
5706
|
-
|
|
7067
|
+
const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
|
|
7068
|
+
this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
|
|
5707
7069
|
this.selection = [drag.nodeId];
|
|
5708
7070
|
return this.commit("Draw", drag.before);
|
|
5709
7071
|
}
|
|
@@ -5725,7 +7087,8 @@ var VectorEditorController = class {
|
|
|
5725
7087
|
this.drag = null;
|
|
5726
7088
|
this.interaction = null;
|
|
5727
7089
|
this.guides = null;
|
|
5728
|
-
if (drag.kind === "pan") {
|
|
7090
|
+
if (drag.kind === "pan" || drag.kind === "builder") {
|
|
7091
|
+
if (drag.kind === "builder") this.builderPreview = null;
|
|
5729
7092
|
this.notify();
|
|
5730
7093
|
return;
|
|
5731
7094
|
}
|
|
@@ -5825,7 +7188,10 @@ var VectorEditorController = class {
|
|
|
5825
7188
|
switch (key) {
|
|
5826
7189
|
case "Escape":
|
|
5827
7190
|
if (this.drag) this.cancelDrag();
|
|
5828
|
-
else if (this.
|
|
7191
|
+
else if (this.tool === "width" && this.widthPoint) {
|
|
7192
|
+
this.widthPoint = null;
|
|
7193
|
+
this.notify();
|
|
7194
|
+
} else if (this.pen) this.finishPath();
|
|
5829
7195
|
else if (this.tool === "direct" && this.anchors.length) {
|
|
5830
7196
|
this.anchors = [];
|
|
5831
7197
|
this.notify();
|
|
@@ -5846,6 +7212,7 @@ var VectorEditorController = class {
|
|
|
5846
7212
|
return false;
|
|
5847
7213
|
case "Delete":
|
|
5848
7214
|
case "Backspace":
|
|
7215
|
+
if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
|
|
5849
7216
|
if (this.pen) {
|
|
5850
7217
|
const node = this.getNode(this.pen.nodeId);
|
|
5851
7218
|
const sp = node.subpaths[this.pen.subpath];
|
|
@@ -5927,6 +7294,31 @@ function scaleFor(handle, b, p, mods = {}) {
|
|
|
5927
7294
|
}
|
|
5928
7295
|
return { sx, sy, origin };
|
|
5929
7296
|
}
|
|
7297
|
+
function withStyle(node, style) {
|
|
7298
|
+
const next = { ...node, ...style };
|
|
7299
|
+
if ("strokeWidthProfile" in style) {
|
|
7300
|
+
const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
|
|
7301
|
+
if (p.length) next.strokeWidthProfile = p;
|
|
7302
|
+
else delete next.strokeWidthProfile;
|
|
7303
|
+
}
|
|
7304
|
+
return next;
|
|
7305
|
+
}
|
|
7306
|
+
function hiddenOrLocked(nodes, id) {
|
|
7307
|
+
const loc = locateVectorNode(nodes, id);
|
|
7308
|
+
if (!loc) return false;
|
|
7309
|
+
const n = loc.list[loc.index];
|
|
7310
|
+
if (n.locked || n.visible === false) return true;
|
|
7311
|
+
return loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;
|
|
7312
|
+
}
|
|
7313
|
+
function pencilPressureProfile(points, pressures) {
|
|
7314
|
+
const n = Math.min(points.length, pressures.length);
|
|
7315
|
+
if (n < 2) return [];
|
|
7316
|
+
const s = [0];
|
|
7317
|
+
for (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));
|
|
7318
|
+
const total = s[n - 1];
|
|
7319
|
+
if (!(total > 0)) return [];
|
|
7320
|
+
return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
|
|
7321
|
+
}
|
|
5930
7322
|
|
|
5931
7323
|
// src/vector/render.ts
|
|
5932
7324
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
@@ -6091,7 +7483,33 @@ function getVectorOverlayRenderData(state) {
|
|
|
6091
7483
|
});
|
|
6092
7484
|
if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
|
|
6093
7485
|
const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
|
|
6094
|
-
|
|
7486
|
+
const sc = (p) => {
|
|
7487
|
+
const q = toScreen(state, p);
|
|
7488
|
+
return { x: fmt(q.x), y: fmt(q.y) };
|
|
7489
|
+
};
|
|
7490
|
+
const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
|
|
7491
|
+
const widthPoints = getVectorWidthHandles(state).map((h) => {
|
|
7492
|
+
const c = sc(h);
|
|
7493
|
+
const hw = state.hover?.widthPoint;
|
|
7494
|
+
const hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;
|
|
7495
|
+
return { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === "width" && sameWidth(it.ref, h.ref) };
|
|
7496
|
+
});
|
|
7497
|
+
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7498
|
+
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7499
|
+
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7500
|
+
let builder = null;
|
|
7501
|
+
if (state.builder) {
|
|
7502
|
+
const v2 = state.viewport;
|
|
7503
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
7504
|
+
const d = (path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 });
|
|
7505
|
+
const trail = state.builder.trail && state.builder.trail.length > 1 ? state.builder.trail.map((q, i) => `${i ? "L" : "M"}${sc(q).x} ${sc(q).y}`).join("") : null;
|
|
7506
|
+
builder = { mode: state.builder.mode, faces: state.builder.faces.map(d), lines: state.builder.lines.map(d), trail };
|
|
7507
|
+
if (state.builder.mode !== "fill" && it?.type === "builder") {
|
|
7508
|
+
const n = state.builder.faces.length;
|
|
7509
|
+
status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder };
|
|
6095
7513
|
}
|
|
6096
7514
|
var VECTOR_OVERLAY_THEME = {
|
|
6097
7515
|
accent: "#0ea5e9",
|
|
@@ -6105,13 +7523,28 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
6105
7523
|
anchorSize: 7,
|
|
6106
7524
|
handleRadius: 3.5,
|
|
6107
7525
|
/** Radius of a live corner widget (a ring with a dot). */
|
|
6108
|
-
cornerWidgetRadius: 4
|
|
7526
|
+
cornerWidgetRadius: 4,
|
|
7527
|
+
/** Half the diagonal of a width point's diamond. */
|
|
7528
|
+
widthPointSize: 5,
|
|
7529
|
+
/** Shape Builder faces to merge and paint bucket regions. */
|
|
7530
|
+
builderFill: "rgba(14, 165, 233, 0.22)",
|
|
7531
|
+
/** Shape Builder faces and line pieces an Alt gesture deletes. */
|
|
7532
|
+
builderErase: "#ef4444",
|
|
7533
|
+
builderEraseFill: "rgba(239, 68, 68, 0.22)"
|
|
6109
7534
|
};
|
|
7535
|
+
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
7536
|
+
return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
|
|
7537
|
+
}
|
|
6110
7538
|
function toVectorModifiers(event) {
|
|
6111
7539
|
return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
|
|
6112
7540
|
}
|
|
6113
7541
|
function toVectorPointerInput(event) {
|
|
6114
|
-
return {
|
|
7542
|
+
return {
|
|
7543
|
+
...toVectorModifiers(event),
|
|
7544
|
+
button: event.button,
|
|
7545
|
+
...typeof event.pressure === "number" ? { pressure: event.pressure } : {},
|
|
7546
|
+
...event.pointerType ? { pointerType: event.pointerType } : {}
|
|
7547
|
+
};
|
|
6115
7548
|
}
|
|
6116
7549
|
function toVectorKeyInput(event) {
|
|
6117
7550
|
return { key: event.key, ...toVectorModifiers(event) };
|
|
@@ -6160,6 +7593,8 @@ function getVectorSelectionStyle(state) {
|
|
|
6160
7593
|
arrowAlign: first.arrowAlign ?? "extend",
|
|
6161
7594
|
startArrowScale: first.startArrowScale ?? 1,
|
|
6162
7595
|
endArrowScale: first.endArrowScale ?? 1,
|
|
7596
|
+
strokeWidthProfile: profileOf(first),
|
|
7597
|
+
widthProfile: widthProfilePresetId(profileOf(first)),
|
|
6163
7598
|
cornerRadius: c0.radius,
|
|
6164
7599
|
cornerKind: c0.kind,
|
|
6165
7600
|
corners: corners2.length,
|
|
@@ -6175,12 +7610,17 @@ function getVectorSelectionStyle(state) {
|
|
|
6175
7610
|
arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
|
|
6176
7611
|
startArrowScale: !same((p) => p.startArrowScale ?? 1),
|
|
6177
7612
|
endArrowScale: !same((p) => p.endArrowScale ?? 1),
|
|
7613
|
+
widthProfile: !same(profileOf),
|
|
6178
7614
|
cornerRadius: corners2.some((c) => c.radius !== c0.radius),
|
|
6179
7615
|
cornerKind: corners2.some((c) => c.kind !== c0.kind)
|
|
6180
7616
|
},
|
|
6181
7617
|
pathIds: paths.map((p) => p.id)
|
|
6182
7618
|
};
|
|
6183
7619
|
}
|
|
7620
|
+
function profileOf(p) {
|
|
7621
|
+
const points = sanitizeWidthProfile(p.strokeWidthProfile);
|
|
7622
|
+
return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
|
|
7623
|
+
}
|
|
6184
7624
|
var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
|
|
6185
7625
|
var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
|
|
6186
7626
|
var VECTOR_ARROWHEAD_LABELS = {
|
|
@@ -6206,6 +7646,14 @@ var VECTOR_PATH_ACTIONS = [
|
|
|
6206
7646
|
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
|
|
6207
7647
|
{ id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
|
|
6208
7648
|
];
|
|
7649
|
+
var VECTOR_PATHFINDER_ACTIONS = [
|
|
7650
|
+
{ id: "divide", label: "\u229E", title: "Divide", min: 1 },
|
|
7651
|
+
{ id: "trim", label: "\u29C9", title: "Trim", min: 2 },
|
|
7652
|
+
{ id: "merge", label: "\u29C8", title: "Merge", min: 2 },
|
|
7653
|
+
{ id: "crop", label: "\u2317", title: "Crop", min: 2 },
|
|
7654
|
+
{ id: "outline", label: "\u2B1A", title: "Outline", min: 1 },
|
|
7655
|
+
{ id: "minusBack", label: "\u2296", title: "Minus Back", min: 2 }
|
|
7656
|
+
];
|
|
6209
7657
|
var VECTOR_EDIT_TOGGLES = [
|
|
6210
7658
|
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
6211
7659
|
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
@@ -6232,6 +7680,19 @@ var POINTS = {
|
|
|
6232
7680
|
minItems: 1,
|
|
6233
7681
|
description: "Polyline [[x, y], \u2026] in document units."
|
|
6234
7682
|
};
|
|
7683
|
+
var WIDTH_POINTS = {
|
|
7684
|
+
type: ["array", "null"],
|
|
7685
|
+
items: {
|
|
7686
|
+
type: "object",
|
|
7687
|
+
properties: {
|
|
7688
|
+
t: { type: "number", minimum: 0, maximum: 1, description: "Position along each subpath by length (0 start, 1 end)." },
|
|
7689
|
+
left: { type: "number", minimum: 0, description: "Left side as a multiple of half the stroke width (1 = uniform)." },
|
|
7690
|
+
right: { type: "number", minimum: 0, description: "Right side (looking along the path, y down)." }
|
|
7691
|
+
},
|
|
7692
|
+
required: ["t", "left", "right"]
|
|
7693
|
+
},
|
|
7694
|
+
description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
|
|
7695
|
+
};
|
|
6235
7696
|
var PAINT = {
|
|
6236
7697
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
6237
7698
|
};
|
|
@@ -6248,7 +7709,8 @@ var STYLE_PROPS = {
|
|
|
6248
7709
|
endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
|
|
6249
7710
|
arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
|
|
6250
7711
|
startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
|
|
6251
|
-
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." }
|
|
7712
|
+
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
|
|
7713
|
+
strokeWidthProfile: WIDTH_POINTS
|
|
6252
7714
|
};
|
|
6253
7715
|
var pickStyle = (params) => {
|
|
6254
7716
|
const out = {};
|
|
@@ -6405,6 +7867,44 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6405
7867
|
return { ok: true };
|
|
6406
7868
|
}
|
|
6407
7869
|
},
|
|
7870
|
+
{
|
|
7871
|
+
...base,
|
|
7872
|
+
id: `${prefix}setWidthProfile`,
|
|
7873
|
+
label: "Width Profile\u2026",
|
|
7874
|
+
keywords: ["variable width", "taper", "tapered", "pressure", "profile", "brush", "calligraphy", "bulge", "thick", "thin"],
|
|
7875
|
+
description: "Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0\u20131, startEase / endEase 0\u20131) and / or `wave` (amount 0\u20131, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.",
|
|
7876
|
+
params: {
|
|
7877
|
+
type: "object",
|
|
7878
|
+
properties: {
|
|
7879
|
+
ids: IDS,
|
|
7880
|
+
preset: { type: "string", enum: VECTOR_WIDTH_PRESET_IDS },
|
|
7881
|
+
points: WIDTH_POINTS,
|
|
7882
|
+
taper: {
|
|
7883
|
+
type: "object",
|
|
7884
|
+
properties: {
|
|
7885
|
+
startLength: { type: "number", minimum: 0, maximum: 1 },
|
|
7886
|
+
endLength: { type: "number", minimum: 0, maximum: 1 },
|
|
7887
|
+
startWidth: { type: "number", minimum: 0, maximum: 1 },
|
|
7888
|
+
endWidth: { type: "number", minimum: 0, maximum: 1 },
|
|
7889
|
+
startEase: { type: "number", minimum: 0, maximum: 1 },
|
|
7890
|
+
endEase: { type: "number", minimum: 0, maximum: 1 }
|
|
7891
|
+
}
|
|
7892
|
+
},
|
|
7893
|
+
wave: { type: "object", properties: { amount: { type: "number", minimum: 0, maximum: 1 }, cycles: { type: "number", exclusiveMinimum: 0 }, phase: { type: "number" } } }
|
|
7894
|
+
}
|
|
7895
|
+
},
|
|
7896
|
+
enabled: enabledFor,
|
|
7897
|
+
undoable: true,
|
|
7898
|
+
run: (_ctx, params) => {
|
|
7899
|
+
const target = { ids: ids(params) };
|
|
7900
|
+
if (params?.preset) check(controller.setWidthProfile(params.preset, target));
|
|
7901
|
+
else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
|
|
7902
|
+
else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
|
|
7903
|
+
else fail("Pass `preset`, `points`, `taper` or `wave`.");
|
|
7904
|
+
const node = controller.getNode(target.ids[0]);
|
|
7905
|
+
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
7906
|
+
}
|
|
7907
|
+
},
|
|
6408
7908
|
{
|
|
6409
7909
|
...base,
|
|
6410
7910
|
id: `${prefix}outlineStroke`,
|
|
@@ -6477,6 +7977,62 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6477
7977
|
return { id };
|
|
6478
7978
|
}
|
|
6479
7979
|
},
|
|
7980
|
+
{
|
|
7981
|
+
...base,
|
|
7982
|
+
id: `${prefix}pathfinder`,
|
|
7983
|
+
label: "Pathfinder\u2026",
|
|
7984
|
+
keywords: ["divide", "trim", "merge", "crop", "outline", "minus back", "split", "faces"],
|
|
7985
|
+
description: "Pathfinder over the given paths (bottom to top): divide (every face its own shape, painted like the top-most shape over it; open unfilled paths cut), trim (hidden parts removed), merge (trim, then touching parts of one fill joined), crop (the top shape crops the others), outline (outlines cut into open strokes where they meet) or minusBack (the top shape minus everything below). Results replace the paths, grouped when there are several.",
|
|
7986
|
+
params: { type: "object", properties: { operation: { type: "string", enum: [...VECTOR_PATHFINDER_OPERATIONS] }, ids: IDS }, required: ["operation"] },
|
|
7987
|
+
undoable: true,
|
|
7988
|
+
run: (_ctx, params) => {
|
|
7989
|
+
if (!VECTOR_PATHFINDER_OPERATIONS.includes(params.operation)) fail(`Unknown Pathfinder operation "${params.operation}".`);
|
|
7990
|
+
const created = controller.pathfinder(params.operation, { ids: ids(params) });
|
|
7991
|
+
if (!created.length) fail("Nothing came out: select paths that overlap (two or more, except for divide and outline).");
|
|
7992
|
+
return { ids: created, selection: [...controller.getState().selection] };
|
|
7993
|
+
}
|
|
7994
|
+
},
|
|
7995
|
+
{
|
|
7996
|
+
...base,
|
|
7997
|
+
id: `${prefix}shapeBuilderFaces`,
|
|
7998
|
+
label: "List Shape Builder Faces",
|
|
7999
|
+
keywords: ["shape builder", "regions", "faces", "planar"],
|
|
8000
|
+
description: "Lists the faces the given paths divide the plane into: path data, area, a point inside and the ids of the paths filling each (empty when only outlines enclose it).",
|
|
8001
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
8002
|
+
run: (_ctx, params) => ({ faces: controller.getShapeBuilderFaces({ ids: ids(params) }) })
|
|
8003
|
+
},
|
|
8004
|
+
{
|
|
8005
|
+
...base,
|
|
8006
|
+
id: `${prefix}shapeBuilder`,
|
|
8007
|
+
label: "Shape Builder",
|
|
8008
|
+
keywords: ["merge faces", "build shape", "combine regions", "delete region"],
|
|
8009
|
+
description: "Shape Builder gesture over the faces of the given paths: `points` is the pointer path in document units (one point is a click; [[x, y], \u2026]; use the points from shapeBuilderFaces). Merges the faces it touches into one shape, or with `erase: true` deletes them and the pieces of unfilled open paths within `tolerance`. Shapes covering those faces lose that area.",
|
|
8010
|
+
params: {
|
|
8011
|
+
type: "object",
|
|
8012
|
+
properties: { points: POINTS, erase: { type: "boolean" }, tolerance: { type: "number", minimum: 0 }, ids: IDS },
|
|
8013
|
+
required: ["points"]
|
|
8014
|
+
},
|
|
8015
|
+
undoable: true,
|
|
8016
|
+
run: (_ctx, params) => {
|
|
8017
|
+
const result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });
|
|
8018
|
+
check(result);
|
|
8019
|
+
return { merged: result.merged, faces: result.faces, lines: result.lines };
|
|
8020
|
+
}
|
|
8021
|
+
},
|
|
8022
|
+
{
|
|
8023
|
+
...base,
|
|
8024
|
+
id: `${prefix}fillRegion`,
|
|
8025
|
+
label: "Fill Region",
|
|
8026
|
+
keywords: ["paint bucket", "live paint", "fill area", "enclosed"],
|
|
8027
|
+
description: "Paint bucket: fills the area the visible outlines enclose around `point` (crossings split, nested shapes left as holes) with a new path in the current fill, below the selection or on top. `gap` closes gaps up to that size between loose ends.",
|
|
8028
|
+
params: { type: "object", properties: { point: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: "[x, y] in document units." }, gap: { type: "number", minimum: 0 } }, required: ["point"] },
|
|
8029
|
+
undoable: true,
|
|
8030
|
+
run: (_ctx, params) => {
|
|
8031
|
+
const id = controller.fillRegion({ x: params.point[0], y: params.point[1] }, { gap: params.gap });
|
|
8032
|
+
if (!id) fail("No closed region surrounds that point.");
|
|
8033
|
+
return { id };
|
|
8034
|
+
}
|
|
8035
|
+
},
|
|
6480
8036
|
{
|
|
6481
8037
|
...base,
|
|
6482
8038
|
id: `${prefix}align`,
|
|
@@ -6711,7 +8267,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6711
8267
|
id: `${prefix}setEditing`,
|
|
6712
8268
|
label: "Path Editing Options\u2026",
|
|
6713
8269
|
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
6714
|
-
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `
|
|
8270
|
+
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).",
|
|
6715
8271
|
params: {
|
|
6716
8272
|
type: "object",
|
|
6717
8273
|
properties: {
|
|
@@ -6719,12 +8275,13 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6719
8275
|
curvatureMoves: { type: "boolean" },
|
|
6720
8276
|
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
6721
8277
|
recognizeShapes: { type: "boolean" },
|
|
6722
|
-
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
8278
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 },
|
|
8279
|
+
pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
|
|
6723
8280
|
}
|
|
6724
8281
|
},
|
|
6725
8282
|
run: (_ctx, params) => {
|
|
6726
8283
|
const patch = {};
|
|
6727
|
-
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
8284
|
+
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
6728
8285
|
controller.setEditing(patch);
|
|
6729
8286
|
return { editing: controller.getState().editing };
|
|
6730
8287
|
}
|
|
@@ -6780,10 +8337,15 @@ export {
|
|
|
6780
8337
|
VECTOR_CORNER_KIND_LABELS,
|
|
6781
8338
|
VECTOR_CORNER_WIDGET_INSET,
|
|
6782
8339
|
VECTOR_EDIT_TOGGLES,
|
|
8340
|
+
VECTOR_FILL_GAP,
|
|
6783
8341
|
VECTOR_INK_MAX_CORNERS,
|
|
6784
8342
|
VECTOR_OVERLAY_THEME,
|
|
8343
|
+
VECTOR_PATHFINDER_ACTIONS,
|
|
8344
|
+
VECTOR_PATHFINDER_LABELS,
|
|
8345
|
+
VECTOR_PATHFINDER_OPERATIONS,
|
|
6785
8346
|
VECTOR_PATH_ACTIONS,
|
|
6786
8347
|
VECTOR_PENCIL_STRING_PX,
|
|
8348
|
+
VECTOR_PRESSURE_NOMINAL,
|
|
6787
8349
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
6788
8350
|
VECTOR_STROKE_ALIGN_LABELS,
|
|
6789
8351
|
VECTOR_STYLE_KEYS,
|
|
@@ -6791,8 +8353,14 @@ export {
|
|
|
6791
8353
|
VECTOR_TOOL_GLYPHS,
|
|
6792
8354
|
VECTOR_TOOL_SHORTCUTS,
|
|
6793
8355
|
VECTOR_TRANSFORM_HANDLES,
|
|
8356
|
+
VECTOR_WIDTH_PRESETS,
|
|
8357
|
+
VECTOR_WIDTH_PRESET_IDS,
|
|
8358
|
+
VECTOR_WIDTH_PROFILE_MAX_POINTS,
|
|
8359
|
+
VECTOR_WIDTH_PROFILE_MAX_SCALE,
|
|
8360
|
+
VECTOR_WIDTH_SAME_T,
|
|
6794
8361
|
VectorEditorController,
|
|
6795
8362
|
VectorSnapTargets,
|
|
8363
|
+
addBuilderHit,
|
|
6796
8364
|
applyMatrix,
|
|
6797
8365
|
applyMatrixLinear,
|
|
6798
8366
|
applyVectorCornersAttribute,
|
|
@@ -6816,6 +8384,7 @@ export {
|
|
|
6816
8384
|
createVectorCommands,
|
|
6817
8385
|
curvatureMoveAnchor,
|
|
6818
8386
|
cutSubpathAt,
|
|
8387
|
+
dashIntervals,
|
|
6819
8388
|
decomposeRigid,
|
|
6820
8389
|
effectiveStrokeAlign,
|
|
6821
8390
|
ellipseSubpath,
|
|
@@ -6826,6 +8395,7 @@ export {
|
|
|
6826
8395
|
expandVectorCornersOf,
|
|
6827
8396
|
findVectorNode,
|
|
6828
8397
|
fitCornerRadius,
|
|
8398
|
+
generateWidthProfile,
|
|
6829
8399
|
getAnchor,
|
|
6830
8400
|
getAnchorWorld,
|
|
6831
8401
|
getEditablePathIds,
|
|
@@ -6836,6 +8406,7 @@ export {
|
|
|
6836
8406
|
getVectorSelectionBoxGeometry,
|
|
6837
8407
|
getVectorSelectionFrame,
|
|
6838
8408
|
getVectorSelectionStyle,
|
|
8409
|
+
getVectorWidthHandles,
|
|
6839
8410
|
getVisibleAnchors,
|
|
6840
8411
|
getVisibleHandles,
|
|
6841
8412
|
hasPaint,
|
|
@@ -6846,46 +8417,61 @@ export {
|
|
|
6846
8417
|
hitTestVectorStroke,
|
|
6847
8418
|
inferAnchorType,
|
|
6848
8419
|
insertAnchor,
|
|
8420
|
+
insertWidthPoint,
|
|
6849
8421
|
invertMatrix,
|
|
6850
8422
|
isClosedBezierShape,
|
|
6851
8423
|
isIdentityMatrix,
|
|
8424
|
+
isUniformWidthProfile,
|
|
6852
8425
|
isVectorTextTarget,
|
|
6853
8426
|
joinSubpaths,
|
|
6854
8427
|
knifePath,
|
|
6855
8428
|
lineSubpath,
|
|
6856
8429
|
locateVectorNode,
|
|
6857
8430
|
matrixRotation,
|
|
8431
|
+
mirrorWidthProfile,
|
|
6858
8432
|
moveAnchorHandle,
|
|
6859
8433
|
multiplyMatrices,
|
|
6860
8434
|
nearestSegment,
|
|
8435
|
+
nearestStrokePosition,
|
|
6861
8436
|
nextVectorCornerKind,
|
|
6862
8437
|
nodeMatrix,
|
|
6863
8438
|
parseSvgTransform,
|
|
6864
8439
|
parseVectorSvg,
|
|
8440
|
+
planShapeBuilder,
|
|
6865
8441
|
polygonSubpath,
|
|
6866
8442
|
polylineBounds,
|
|
8443
|
+
pressureWidthProfile,
|
|
6867
8444
|
recognizeInkShape,
|
|
6868
8445
|
rectSubpath,
|
|
6869
8446
|
removeAnchorKeepingShape,
|
|
6870
8447
|
removeAnchors,
|
|
6871
8448
|
removeAnchorsKeepingShape,
|
|
6872
8449
|
removeRedundantPoints,
|
|
8450
|
+
removeWidthPoints,
|
|
6873
8451
|
reverseSubpath,
|
|
8452
|
+
reverseWidthProfile,
|
|
6874
8453
|
rotateMatrix,
|
|
6875
8454
|
roundSubpaths,
|
|
8455
|
+
samplePolyline,
|
|
8456
|
+
sanitizeWidthProfile,
|
|
6876
8457
|
scaleFor,
|
|
6877
8458
|
scaleMatrix,
|
|
6878
8459
|
setAnchorCorner,
|
|
6879
8460
|
setSubpathCorners,
|
|
8461
|
+
setWidthPoint,
|
|
6880
8462
|
simplifyPolyline,
|
|
6881
8463
|
smoothSubpath,
|
|
6882
8464
|
splitIntoFaces,
|
|
6883
8465
|
splitSubpathAt,
|
|
6884
8466
|
starSubpath,
|
|
8467
|
+
strokePositionPoint,
|
|
8468
|
+
strokeWidthProfileOf,
|
|
8469
|
+
subpathEnclosesArea,
|
|
6885
8470
|
subpathPointAt,
|
|
6886
8471
|
subpathSegment,
|
|
6887
8472
|
subpathSegmentCount,
|
|
6888
8473
|
subpathsToBezier,
|
|
8474
|
+
taperWaveFactor,
|
|
6889
8475
|
toVectorKeyInput,
|
|
6890
8476
|
toVectorModifiers,
|
|
6891
8477
|
toVectorPointerInput,
|
|
@@ -6893,10 +8479,14 @@ export {
|
|
|
6893
8479
|
translateAnchor,
|
|
6894
8480
|
translateMatrix,
|
|
6895
8481
|
unionBounds,
|
|
8482
|
+
variableStrokeOutline,
|
|
8483
|
+
variableStrokeRings,
|
|
6896
8484
|
vectorCornerAt,
|
|
6897
8485
|
vectorCorners,
|
|
6898
8486
|
vectorCornersAttribute,
|
|
8487
|
+
vectorDiamondPath,
|
|
6899
8488
|
vectorDocumentToSvg,
|
|
8489
|
+
vectorFillMap,
|
|
6900
8490
|
vectorInkShapeName,
|
|
6901
8491
|
vectorNodeBounds,
|
|
6902
8492
|
vectorNodeRotation,
|
|
@@ -6905,15 +8495,28 @@ export {
|
|
|
6905
8495
|
vectorPaintToSvg,
|
|
6906
8496
|
vectorParentMatrix,
|
|
6907
8497
|
vectorPathData,
|
|
8498
|
+
vectorPathEntries,
|
|
6908
8499
|
vectorPathWorldBezier,
|
|
8500
|
+
vectorPathfinderShape,
|
|
6909
8501
|
vectorResizeCursor,
|
|
8502
|
+
vectorResultStyle,
|
|
8503
|
+
vectorShapeBuilderHit,
|
|
8504
|
+
vectorShapeBuilderMap,
|
|
8505
|
+
vectorStrokeLineArea,
|
|
6910
8506
|
vectorStrokeOutline,
|
|
6911
8507
|
vectorStrokeOutlinePath,
|
|
6912
8508
|
vectorStrokeParts,
|
|
6913
8509
|
vectorStrokeReach,
|
|
6914
8510
|
vectorStrokeSvgParts,
|
|
8511
|
+
vectorStrokeWidthGeometry,
|
|
6915
8512
|
vectorToolLabel,
|
|
6916
8513
|
vectorWorldMatrix,
|
|
6917
|
-
walkVectorNodes
|
|
8514
|
+
walkVectorNodes,
|
|
8515
|
+
widthProfileAt,
|
|
8516
|
+
widthProfileFromAttribute,
|
|
8517
|
+
widthProfileMaxScale,
|
|
8518
|
+
widthProfilePreset,
|
|
8519
|
+
widthProfilePresetId,
|
|
8520
|
+
widthProfileToAttribute
|
|
6918
8521
|
};
|
|
6919
8522
|
//# sourceMappingURL=index.js.map
|