@pixerate/editor 0.37.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 +7 -0
- package/dist/canvas/index.cjs +37 -25
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +2 -2
- package/dist/canvas/index.d.ts +2 -2
- package/dist/canvas/index.js +13 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-GVB7ZPLZ.cjs → chunk-7EDLVBOO.cjs} +649 -18
- package/dist/chunk-7EDLVBOO.cjs.map +1 -0
- package/dist/{chunk-HPEKB352.js → chunk-ZCCU3VNI.js} +646 -15
- package/dist/chunk-ZCCU3VNI.js.map +1 -0
- package/dist/{stroke-CWjFKal4.d.cts → pathfinder-Bd87_lsQ.d.cts} +234 -1
- package/dist/{stroke-ysgaPpAo.d.ts → pathfinder-fLmmFOJw.d.ts} +234 -1
- package/dist/vector/index.cjs +860 -274
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +242 -3
- package/dist/vector/index.d.ts +242 -3
- package/dist/vector/index.js +611 -25
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GVB7ZPLZ.cjs.map +0 -1
- package/dist/chunk-HPEKB352.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,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
outlineStroke,
|
|
19
20
|
pathBoolean,
|
|
20
21
|
pathToContours,
|
|
22
|
+
pathfinder,
|
|
21
23
|
pointInBezierPath,
|
|
22
24
|
polygonBoolean,
|
|
23
25
|
segmentBounds,
|
|
@@ -34,13 +36,30 @@ import {
|
|
|
34
36
|
transformBezierPath,
|
|
35
37
|
trimSegments,
|
|
36
38
|
windingNumber
|
|
37
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-ZCCU3VNI.js";
|
|
38
40
|
|
|
39
41
|
// src/vector/types.ts
|
|
40
42
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
41
43
|
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
42
|
-
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
|
+
];
|
|
43
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"];
|
|
44
63
|
|
|
45
64
|
// src/vector/corners.ts
|
|
46
65
|
var EPS = 1e-9;
|
|
@@ -344,14 +363,14 @@ function mirrorWidthProfile(points) {
|
|
|
344
363
|
function editable(points) {
|
|
345
364
|
return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
|
|
346
365
|
}
|
|
347
|
-
function insertWidthPoint(points, t,
|
|
366
|
+
function insertWidthPoint(points, t, sides2 = {}, options = {}) {
|
|
348
367
|
const pts = editable(points);
|
|
349
368
|
const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
|
|
350
369
|
const here = widthProfileAt(points?.length ? points : null, tt);
|
|
351
370
|
const p = {
|
|
352
371
|
t: tt,
|
|
353
|
-
left: clean(
|
|
354
|
-
right: clean(
|
|
372
|
+
left: clean(sides2.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
|
|
373
|
+
right: clean(sides2.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
|
|
355
374
|
};
|
|
356
375
|
const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
|
|
357
376
|
if (same >= 0 && !options.step) {
|
|
@@ -581,7 +600,7 @@ function variableStrokeRings(path, options) {
|
|
|
581
600
|
const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
|
|
582
601
|
if (!poly) continue;
|
|
583
602
|
const len2 = poly.s[poly.s.length - 1];
|
|
584
|
-
const
|
|
603
|
+
const sides2 = (s) => {
|
|
585
604
|
const w = widthProfileAt(options.profile, s / len2);
|
|
586
605
|
return [w.left * half, w.right * half];
|
|
587
606
|
};
|
|
@@ -595,7 +614,7 @@ function variableStrokeRings(path, options) {
|
|
|
595
614
|
pts.push({ p: pb, s: b, corner: false });
|
|
596
615
|
const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
|
|
597
616
|
if (run.length < 2) {
|
|
598
|
-
const [l, r] =
|
|
617
|
+
const [l, r] = sides2(a);
|
|
599
618
|
const w2 = (l + r) / 2;
|
|
600
619
|
if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
|
|
601
620
|
continue;
|
|
@@ -605,7 +624,7 @@ function variableStrokeRings(path, options) {
|
|
|
605
624
|
const dirs = [];
|
|
606
625
|
for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
|
|
607
626
|
const normals = dirs.map(leftNormal);
|
|
608
|
-
const w = run.map((q) =>
|
|
627
|
+
const w = run.map((q) => sides2(q.s));
|
|
609
628
|
const side = (sigma) => {
|
|
610
629
|
const k = sigma > 0 ? 0 : 1;
|
|
611
630
|
const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
|
|
@@ -1474,8 +1493,8 @@ function ellipseSubpath(b) {
|
|
|
1474
1493
|
]
|
|
1475
1494
|
};
|
|
1476
1495
|
}
|
|
1477
|
-
function polygonSubpath(b,
|
|
1478
|
-
const n = Math.max(3, Math.round(
|
|
1496
|
+
function polygonSubpath(b, sides2 = 6) {
|
|
1497
|
+
const n = Math.max(3, Math.round(sides2));
|
|
1479
1498
|
const cx = b.x + b.width / 2;
|
|
1480
1499
|
const cy = b.y + b.height / 2;
|
|
1481
1500
|
const anchors = [];
|
|
@@ -3157,13 +3176,13 @@ function loopSample(l) {
|
|
|
3157
3176
|
}
|
|
3158
3177
|
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
3159
3178
|
function groupFaces(loops) {
|
|
3160
|
-
const
|
|
3161
|
-
if (!
|
|
3162
|
-
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);
|
|
3163
3182
|
const sign = Math.sign(loopArea(largest));
|
|
3164
|
-
const outers =
|
|
3183
|
+
const outers = usable2.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
3165
3184
|
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
3166
|
-
for (const hole of
|
|
3185
|
+
for (const hole of usable2.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
3167
3186
|
const p = loopSample(hole);
|
|
3168
3187
|
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
3169
3188
|
if (parent) faces.get(parent).loops.push(hole);
|
|
@@ -3715,15 +3734,187 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3715
3734
|
return roundish && rms < 0.1 ? ellipse() : null;
|
|
3716
3735
|
}
|
|
3717
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
|
+
|
|
3718
3907
|
// src/vector/controller.ts
|
|
3719
3908
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
3909
|
+
var VECTOR_FILL_GAP = 6;
|
|
3720
3910
|
var DEFAULT_VECTOR_PATH_EDIT = {
|
|
3721
3911
|
keepShapeOnDelete: true,
|
|
3722
3912
|
curvatureMoves: false,
|
|
3723
3913
|
pencilSmoothing: 0,
|
|
3724
3914
|
recognizeShapes: false,
|
|
3725
3915
|
eraserSize: 10,
|
|
3726
|
-
pencilPressure: "pen"
|
|
3916
|
+
pencilPressure: "pen",
|
|
3917
|
+
fillGap: 0
|
|
3727
3918
|
};
|
|
3728
3919
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3729
3920
|
var VECTOR_STYLE_KEYS = [
|
|
@@ -3962,9 +4153,27 @@ var TOOL_LABELS = {
|
|
|
3962
4153
|
scissors: "Scissors",
|
|
3963
4154
|
knife: "Knife",
|
|
3964
4155
|
eraser: "Eraser",
|
|
3965
|
-
width: "Width"
|
|
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"
|
|
3966
4176
|
};
|
|
3967
|
-
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", w: "width" };
|
|
3968
4177
|
function vectorToolLabel(tool) {
|
|
3969
4178
|
return TOOL_LABELS[tool];
|
|
3970
4179
|
}
|
|
@@ -3981,7 +4190,9 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
3981
4190
|
scissors: "C",
|
|
3982
4191
|
knife: "K",
|
|
3983
4192
|
eraser: "E",
|
|
3984
|
-
width: "W"
|
|
4193
|
+
width: "W",
|
|
4194
|
+
shapeBuilder: "M",
|
|
4195
|
+
paintBucket: "B"
|
|
3985
4196
|
};
|
|
3986
4197
|
var VECTOR_TOOL_GLYPHS = {
|
|
3987
4198
|
select: "\u2196",
|
|
@@ -3996,7 +4207,9 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
3996
4207
|
scissors: "\u2702",
|
|
3997
4208
|
knife: "\u27CB",
|
|
3998
4209
|
eraser: "\u232B",
|
|
3999
|
-
width: "\u21F9"
|
|
4210
|
+
width: "\u21F9",
|
|
4211
|
+
shapeBuilder: "\u2295",
|
|
4212
|
+
paintBucket: "\u25CD"
|
|
4000
4213
|
};
|
|
4001
4214
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
4002
4215
|
var BOOLEAN_OPS = {
|
|
@@ -4006,6 +4219,14 @@ var BOOLEAN_OPS = {
|
|
|
4006
4219
|
exclude: "xor"
|
|
4007
4220
|
};
|
|
4008
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
|
+
};
|
|
4009
4230
|
function mapTree(nodes, fn) {
|
|
4010
4231
|
let changed = false;
|
|
4011
4232
|
const out = [];
|
|
@@ -4070,6 +4291,13 @@ var VectorEditorController = class {
|
|
|
4070
4291
|
hoverTargets = null;
|
|
4071
4292
|
spaceHeld = false;
|
|
4072
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;
|
|
4073
4301
|
createId;
|
|
4074
4302
|
onChange;
|
|
4075
4303
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -4144,6 +4372,7 @@ var VectorEditorController = class {
|
|
|
4144
4372
|
interaction: this.interaction,
|
|
4145
4373
|
pen: this.pen,
|
|
4146
4374
|
pointer: this.pointer,
|
|
4375
|
+
builder: this.builderPreview,
|
|
4147
4376
|
canUndo: h.canUndo,
|
|
4148
4377
|
canRedo: h.canRedo,
|
|
4149
4378
|
undoLabel: h.undoName,
|
|
@@ -4202,6 +4431,11 @@ var VectorEditorController = class {
|
|
|
4202
4431
|
if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
|
|
4203
4432
|
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4204
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";
|
|
4205
4439
|
default:
|
|
4206
4440
|
return "crosshair";
|
|
4207
4441
|
}
|
|
@@ -4328,6 +4562,8 @@ var VectorEditorController = class {
|
|
|
4328
4562
|
this.hover = null;
|
|
4329
4563
|
this.guides = null;
|
|
4330
4564
|
this.widthPoint = null;
|
|
4565
|
+
this.builderPreview = null;
|
|
4566
|
+
this.builderHoverKey = null;
|
|
4331
4567
|
if (tool !== "direct") {
|
|
4332
4568
|
this.anchors = [];
|
|
4333
4569
|
this.editingId = null;
|
|
@@ -5115,6 +5351,248 @@ var VectorEditorController = class {
|
|
|
5115
5351
|
return { ok: true };
|
|
5116
5352
|
}
|
|
5117
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
|
+
// -------------------------------------------------------------------------
|
|
5118
5596
|
// Anchor editing
|
|
5119
5597
|
// -------------------------------------------------------------------------
|
|
5120
5598
|
updateAnchors(doc, refs, fn) {
|
|
@@ -5651,8 +6129,8 @@ var VectorEditorController = class {
|
|
|
5651
6129
|
if (!frame) return null;
|
|
5652
6130
|
const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
|
|
5653
6131
|
const r = this.hitRadius + 2;
|
|
5654
|
-
const
|
|
5655
|
-
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;
|
|
5656
6134
|
return null;
|
|
5657
6135
|
}
|
|
5658
6136
|
hitHandle(screen) {
|
|
@@ -5936,6 +6414,9 @@ var VectorEditorController = class {
|
|
|
5936
6414
|
return this.downPencil(p, input);
|
|
5937
6415
|
case "width":
|
|
5938
6416
|
return this.downWidth(screen, p, input);
|
|
6417
|
+
case "shapeBuilder":
|
|
6418
|
+
case "paintBucket":
|
|
6419
|
+
return this.downBuilder(p, input);
|
|
5939
6420
|
case "scissors": {
|
|
5940
6421
|
const seg = this.nearestOutline(p);
|
|
5941
6422
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -6165,6 +6646,11 @@ var VectorEditorController = class {
|
|
|
6165
6646
|
pointerMove(screen, input = {}) {
|
|
6166
6647
|
const p = this.screenToDocument(screen);
|
|
6167
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
|
+
}
|
|
6168
6654
|
if (!drag) {
|
|
6169
6655
|
const hover = this.computeHover(screen);
|
|
6170
6656
|
const pointerMatters = this.tool === "pen" && !!this.pen;
|
|
@@ -6296,6 +6782,8 @@ var VectorEditorController = class {
|
|
|
6296
6782
|
this.notify(true);
|
|
6297
6783
|
return;
|
|
6298
6784
|
}
|
|
6785
|
+
case "builder":
|
|
6786
|
+
return this.moveBuilder(drag, p);
|
|
6299
6787
|
case "stroke": {
|
|
6300
6788
|
const last = drag.points[drag.points.length - 1];
|
|
6301
6789
|
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
@@ -6526,6 +7014,8 @@ var VectorEditorController = class {
|
|
|
6526
7014
|
} else this.notify();
|
|
6527
7015
|
return;
|
|
6528
7016
|
}
|
|
7017
|
+
case "builder":
|
|
7018
|
+
return this.upBuilder(drag);
|
|
6529
7019
|
case "stroke": {
|
|
6530
7020
|
this.notify();
|
|
6531
7021
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -6597,7 +7087,8 @@ var VectorEditorController = class {
|
|
|
6597
7087
|
this.drag = null;
|
|
6598
7088
|
this.interaction = null;
|
|
6599
7089
|
this.guides = null;
|
|
6600
|
-
if (drag.kind === "pan") {
|
|
7090
|
+
if (drag.kind === "pan" || drag.kind === "builder") {
|
|
7091
|
+
if (drag.kind === "builder") this.builderPreview = null;
|
|
6601
7092
|
this.notify();
|
|
6602
7093
|
return;
|
|
6603
7094
|
}
|
|
@@ -7006,7 +7497,19 @@ function getVectorOverlayRenderData(state) {
|
|
|
7006
7497
|
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7007
7498
|
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7008
7499
|
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7009
|
-
|
|
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 };
|
|
7010
7513
|
}
|
|
7011
7514
|
var VECTOR_OVERLAY_THEME = {
|
|
7012
7515
|
accent: "#0ea5e9",
|
|
@@ -7022,7 +7525,12 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
7022
7525
|
/** Radius of a live corner widget (a ring with a dot). */
|
|
7023
7526
|
cornerWidgetRadius: 4,
|
|
7024
7527
|
/** Half the diagonal of a width point's diamond. */
|
|
7025
|
-
widthPointSize: 5
|
|
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)"
|
|
7026
7534
|
};
|
|
7027
7535
|
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
7028
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`;
|
|
@@ -7138,6 +7646,14 @@ var VECTOR_PATH_ACTIONS = [
|
|
|
7138
7646
|
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
|
|
7139
7647
|
{ id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
|
|
7140
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
|
+
];
|
|
7141
7657
|
var VECTOR_EDIT_TOGGLES = [
|
|
7142
7658
|
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
7143
7659
|
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
@@ -7461,6 +7977,62 @@ function createVectorCommands(controller, options = {}) {
|
|
|
7461
7977
|
return { id };
|
|
7462
7978
|
}
|
|
7463
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
|
+
},
|
|
7464
8036
|
{
|
|
7465
8037
|
...base,
|
|
7466
8038
|
id: `${prefix}align`,
|
|
@@ -7765,8 +8337,12 @@ export {
|
|
|
7765
8337
|
VECTOR_CORNER_KIND_LABELS,
|
|
7766
8338
|
VECTOR_CORNER_WIDGET_INSET,
|
|
7767
8339
|
VECTOR_EDIT_TOGGLES,
|
|
8340
|
+
VECTOR_FILL_GAP,
|
|
7768
8341
|
VECTOR_INK_MAX_CORNERS,
|
|
7769
8342
|
VECTOR_OVERLAY_THEME,
|
|
8343
|
+
VECTOR_PATHFINDER_ACTIONS,
|
|
8344
|
+
VECTOR_PATHFINDER_LABELS,
|
|
8345
|
+
VECTOR_PATHFINDER_OPERATIONS,
|
|
7770
8346
|
VECTOR_PATH_ACTIONS,
|
|
7771
8347
|
VECTOR_PENCIL_STRING_PX,
|
|
7772
8348
|
VECTOR_PRESSURE_NOMINAL,
|
|
@@ -7784,6 +8360,7 @@ export {
|
|
|
7784
8360
|
VECTOR_WIDTH_SAME_T,
|
|
7785
8361
|
VectorEditorController,
|
|
7786
8362
|
VectorSnapTargets,
|
|
8363
|
+
addBuilderHit,
|
|
7787
8364
|
applyMatrix,
|
|
7788
8365
|
applyMatrixLinear,
|
|
7789
8366
|
applyVectorCornersAttribute,
|
|
@@ -7860,6 +8437,7 @@ export {
|
|
|
7860
8437
|
nodeMatrix,
|
|
7861
8438
|
parseSvgTransform,
|
|
7862
8439
|
parseVectorSvg,
|
|
8440
|
+
planShapeBuilder,
|
|
7863
8441
|
polygonSubpath,
|
|
7864
8442
|
polylineBounds,
|
|
7865
8443
|
pressureWidthProfile,
|
|
@@ -7874,6 +8452,7 @@ export {
|
|
|
7874
8452
|
reverseWidthProfile,
|
|
7875
8453
|
rotateMatrix,
|
|
7876
8454
|
roundSubpaths,
|
|
8455
|
+
samplePolyline,
|
|
7877
8456
|
sanitizeWidthProfile,
|
|
7878
8457
|
scaleFor,
|
|
7879
8458
|
scaleMatrix,
|
|
@@ -7887,6 +8466,7 @@ export {
|
|
|
7887
8466
|
starSubpath,
|
|
7888
8467
|
strokePositionPoint,
|
|
7889
8468
|
strokeWidthProfileOf,
|
|
8469
|
+
subpathEnclosesArea,
|
|
7890
8470
|
subpathPointAt,
|
|
7891
8471
|
subpathSegment,
|
|
7892
8472
|
subpathSegmentCount,
|
|
@@ -7906,6 +8486,7 @@ export {
|
|
|
7906
8486
|
vectorCornersAttribute,
|
|
7907
8487
|
vectorDiamondPath,
|
|
7908
8488
|
vectorDocumentToSvg,
|
|
8489
|
+
vectorFillMap,
|
|
7909
8490
|
vectorInkShapeName,
|
|
7910
8491
|
vectorNodeBounds,
|
|
7911
8492
|
vectorNodeRotation,
|
|
@@ -7914,8 +8495,13 @@ export {
|
|
|
7914
8495
|
vectorPaintToSvg,
|
|
7915
8496
|
vectorParentMatrix,
|
|
7916
8497
|
vectorPathData,
|
|
8498
|
+
vectorPathEntries,
|
|
7917
8499
|
vectorPathWorldBezier,
|
|
8500
|
+
vectorPathfinderShape,
|
|
7918
8501
|
vectorResizeCursor,
|
|
8502
|
+
vectorResultStyle,
|
|
8503
|
+
vectorShapeBuilderHit,
|
|
8504
|
+
vectorShapeBuilderMap,
|
|
7919
8505
|
vectorStrokeLineArea,
|
|
7920
8506
|
vectorStrokeOutline,
|
|
7921
8507
|
vectorStrokeOutlinePath,
|