@pixerate/editor 0.37.0 → 0.39.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 +43 -25
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +16 -3
- package/dist/canvas/index.d.ts +16 -3
- package/dist/canvas/index.js +19 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-HPEKB352.js → chunk-3LHWTGHW.js} +803 -23
- package/dist/chunk-3LHWTGHW.js.map +1 -0
- package/dist/{chunk-GVB7ZPLZ.cjs → chunk-VNYRI265.cjs} +806 -26
- package/dist/chunk-VNYRI265.cjs.map +1 -0
- package/dist/{stroke-ysgaPpAo.d.ts → pathfinder-D98S2lV5.d.ts} +252 -1
- package/dist/{stroke-CWjFKal4.d.cts → pathfinder-DKRcXiLS.d.cts} +252 -1
- package/dist/vector/index.cjs +867 -220
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +285 -3
- package/dist/vector/index.d.ts +285 -3
- package/dist/vector/index.js +672 -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,22 +2,26 @@ import {
|
|
|
2
2
|
HistoryManager
|
|
3
3
|
} from "../chunk-QVZZGT4X.js";
|
|
4
4
|
import {
|
|
5
|
+
PlanarMap,
|
|
5
6
|
bezierPathBounds,
|
|
6
7
|
bezierPathToSvg,
|
|
7
8
|
closeContour,
|
|
8
9
|
contourSignedArea,
|
|
9
10
|
contoursToPath,
|
|
11
|
+
cutOutPiece,
|
|
10
12
|
distanceToBezierPath,
|
|
11
13
|
fitCubicWithTangents,
|
|
12
14
|
fitCubics,
|
|
13
15
|
flattenChain,
|
|
14
16
|
formatNumber,
|
|
15
17
|
gradientToSvgDef,
|
|
18
|
+
hatchPath,
|
|
16
19
|
isStraightCubic,
|
|
17
20
|
normalizeBezierPath,
|
|
18
21
|
outlineStroke,
|
|
19
22
|
pathBoolean,
|
|
20
23
|
pathToContours,
|
|
24
|
+
pathfinder,
|
|
21
25
|
pointInBezierPath,
|
|
22
26
|
polygonBoolean,
|
|
23
27
|
segmentBounds,
|
|
@@ -34,13 +38,30 @@ import {
|
|
|
34
38
|
transformBezierPath,
|
|
35
39
|
trimSegments,
|
|
36
40
|
windingNumber
|
|
37
|
-
} from "../chunk-
|
|
41
|
+
} from "../chunk-3LHWTGHW.js";
|
|
38
42
|
|
|
39
43
|
// src/vector/types.ts
|
|
40
44
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
41
45
|
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
42
|
-
var VECTOR_TOOLS = [
|
|
46
|
+
var VECTOR_TOOLS = [
|
|
47
|
+
"select",
|
|
48
|
+
"direct",
|
|
49
|
+
"pen",
|
|
50
|
+
"pencil",
|
|
51
|
+
"rect",
|
|
52
|
+
"ellipse",
|
|
53
|
+
"polygon",
|
|
54
|
+
"star",
|
|
55
|
+
"line",
|
|
56
|
+
"scissors",
|
|
57
|
+
"knife",
|
|
58
|
+
"eraser",
|
|
59
|
+
"width",
|
|
60
|
+
"shapeBuilder",
|
|
61
|
+
"paintBucket"
|
|
62
|
+
];
|
|
43
63
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
64
|
+
var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
|
|
44
65
|
|
|
45
66
|
// src/vector/corners.ts
|
|
46
67
|
var EPS = 1e-9;
|
|
@@ -344,14 +365,14 @@ function mirrorWidthProfile(points) {
|
|
|
344
365
|
function editable(points) {
|
|
345
366
|
return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
|
|
346
367
|
}
|
|
347
|
-
function insertWidthPoint(points, t,
|
|
368
|
+
function insertWidthPoint(points, t, sides2 = {}, options = {}) {
|
|
348
369
|
const pts = editable(points);
|
|
349
370
|
const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
|
|
350
371
|
const here = widthProfileAt(points?.length ? points : null, tt);
|
|
351
372
|
const p = {
|
|
352
373
|
t: tt,
|
|
353
|
-
left: clean(
|
|
354
|
-
right: clean(
|
|
374
|
+
left: clean(sides2.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
|
|
375
|
+
right: clean(sides2.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
|
|
355
376
|
};
|
|
356
377
|
const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
|
|
357
378
|
if (same >= 0 && !options.step) {
|
|
@@ -581,7 +602,7 @@ function variableStrokeRings(path, options) {
|
|
|
581
602
|
const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
|
|
582
603
|
if (!poly) continue;
|
|
583
604
|
const len2 = poly.s[poly.s.length - 1];
|
|
584
|
-
const
|
|
605
|
+
const sides2 = (s) => {
|
|
585
606
|
const w = widthProfileAt(options.profile, s / len2);
|
|
586
607
|
return [w.left * half, w.right * half];
|
|
587
608
|
};
|
|
@@ -595,7 +616,7 @@ function variableStrokeRings(path, options) {
|
|
|
595
616
|
pts.push({ p: pb, s: b, corner: false });
|
|
596
617
|
const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
|
|
597
618
|
if (run.length < 2) {
|
|
598
|
-
const [l, r] =
|
|
619
|
+
const [l, r] = sides2(a);
|
|
599
620
|
const w2 = (l + r) / 2;
|
|
600
621
|
if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
|
|
601
622
|
continue;
|
|
@@ -605,7 +626,7 @@ function variableStrokeRings(path, options) {
|
|
|
605
626
|
const dirs = [];
|
|
606
627
|
for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
|
|
607
628
|
const normals = dirs.map(leftNormal);
|
|
608
|
-
const w = run.map((q) =>
|
|
629
|
+
const w = run.map((q) => sides2(q.s));
|
|
609
630
|
const side = (sigma) => {
|
|
610
631
|
const k = sigma > 0 ? 0 : 1;
|
|
611
632
|
const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
|
|
@@ -1474,8 +1495,8 @@ function ellipseSubpath(b) {
|
|
|
1474
1495
|
]
|
|
1475
1496
|
};
|
|
1476
1497
|
}
|
|
1477
|
-
function polygonSubpath(b,
|
|
1478
|
-
const n = Math.max(3, Math.round(
|
|
1498
|
+
function polygonSubpath(b, sides2 = 6) {
|
|
1499
|
+
const n = Math.max(3, Math.round(sides2));
|
|
1479
1500
|
const cx = b.x + b.width / 2;
|
|
1480
1501
|
const cy = b.y + b.height / 2;
|
|
1481
1502
|
const anchors = [];
|
|
@@ -3157,13 +3178,13 @@ function loopSample(l) {
|
|
|
3157
3178
|
}
|
|
3158
3179
|
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
3159
3180
|
function groupFaces(loops) {
|
|
3160
|
-
const
|
|
3161
|
-
if (!
|
|
3162
|
-
const largest =
|
|
3181
|
+
const usable2 = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
3182
|
+
if (!usable2.length) return [];
|
|
3183
|
+
const largest = usable2.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
3163
3184
|
const sign = Math.sign(loopArea(largest));
|
|
3164
|
-
const outers =
|
|
3185
|
+
const outers = usable2.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
3165
3186
|
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
3166
|
-
for (const hole of
|
|
3187
|
+
for (const hole of usable2.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
3167
3188
|
const p = loopSample(hole);
|
|
3168
3189
|
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
3169
3190
|
if (parent) faces.get(parent).loops.push(hole);
|
|
@@ -3715,15 +3736,226 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3715
3736
|
return roundish && rms < 0.1 ? ellipse() : null;
|
|
3716
3737
|
}
|
|
3717
3738
|
|
|
3739
|
+
// src/vector/builder.ts
|
|
3740
|
+
function vectorPathEntries(doc, roots, options = {}) {
|
|
3741
|
+
const out = [];
|
|
3742
|
+
const visit = (node, parent, inside) => {
|
|
3743
|
+
if (node.visible === false || node.locked && !options.locked) return;
|
|
3744
|
+
const picked = inside || roots === null || roots.includes(node.id);
|
|
3745
|
+
if (node.type === "group") {
|
|
3746
|
+
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
3747
|
+
for (const c of node.children) visit(c, m, picked);
|
|
3748
|
+
} else if (picked) out.push({ node, parent });
|
|
3749
|
+
};
|
|
3750
|
+
for (const n of doc.nodes) visit(n, IDENTITY_MATRIX, false);
|
|
3751
|
+
return out;
|
|
3752
|
+
}
|
|
3753
|
+
var usable = (sp) => sp.anchors.length > 1;
|
|
3754
|
+
function worldOf(entry, subpaths) {
|
|
3755
|
+
return vectorPathWorldBezier({ ...entry.node, subpaths }, entry.parent);
|
|
3756
|
+
}
|
|
3757
|
+
function subpathEnclosesArea(sp) {
|
|
3758
|
+
const contour = pathToContours(subpathsToBezier([{ ...sp, closed: true }]))[0];
|
|
3759
|
+
if (!contour) return false;
|
|
3760
|
+
const segs = closeContour(contour, true);
|
|
3761
|
+
const b = bezierPathBounds(contoursToPath([{ segments: segs, closed: true }]));
|
|
3762
|
+
if (!b) return false;
|
|
3763
|
+
const w = b.maxX - b.minX;
|
|
3764
|
+
const h = b.maxY - b.minY;
|
|
3765
|
+
return Math.abs(contourSignedArea(segs)) > 1e-9 * (w * w + h * h);
|
|
3766
|
+
}
|
|
3767
|
+
var paintKey = (paint) => typeof paint === "string" ? paint.trim().toLowerCase() : JSON.stringify(paint ?? null);
|
|
3768
|
+
function vectorPathfinderShape(entry, op) {
|
|
3769
|
+
const { node } = entry;
|
|
3770
|
+
const subpaths = node.subpaths.filter(usable);
|
|
3771
|
+
const closed = subpaths.map((s) => ({ ...s, closed: true }));
|
|
3772
|
+
const fillRule = node.fillRule ?? "nonzero";
|
|
3773
|
+
if (hasPaint(node.fill)) return { path: worldOf(entry, closed), fillRule, key: `fill:${paintKey(node.fill)}`, paint: "fill" };
|
|
3774
|
+
const open = subpaths.length > 0 && subpaths.every((s) => !s.closed);
|
|
3775
|
+
if (op === "divide" && open) return { path: worldOf(entry, subpaths), line: true, paint: "line" };
|
|
3776
|
+
if (hasPaint(node.stroke) && (node.strokeWidth ?? 1) > 0) {
|
|
3777
|
+
const path = vectorStrokeOutline({ ...node, subpaths }, entry.parent);
|
|
3778
|
+
return { path, fillRule: "nonzero", key: `fill:${paintKey(node.stroke)}`, paint: "stroke" };
|
|
3779
|
+
}
|
|
3780
|
+
return { path: worldOf(entry, closed), fillRule, key: "none", paint: "outline" };
|
|
3781
|
+
}
|
|
3782
|
+
var STROKE_KEYS = [
|
|
3783
|
+
"stroke",
|
|
3784
|
+
"strokeWidth",
|
|
3785
|
+
"strokeLinecap",
|
|
3786
|
+
"strokeLinejoin",
|
|
3787
|
+
"strokeDasharray",
|
|
3788
|
+
"strokeAlign",
|
|
3789
|
+
"startArrow",
|
|
3790
|
+
"endArrow",
|
|
3791
|
+
"arrowAlign",
|
|
3792
|
+
"startArrowScale",
|
|
3793
|
+
"endArrowScale",
|
|
3794
|
+
"strokeWidthProfile"
|
|
3795
|
+
];
|
|
3796
|
+
function vectorResultStyle(node, paint, edge = false) {
|
|
3797
|
+
const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
|
|
3798
|
+
const base = { ...rest, fillRule: "nonzero" };
|
|
3799
|
+
if (edge) {
|
|
3800
|
+
for (const k of STROKE_KEYS) delete base[k];
|
|
3801
|
+
const stroke = paint === "stroke" || !hasPaint(node.fill) ? node.stroke : node.fill;
|
|
3802
|
+
return { ...base, fill: null, stroke: stroke ?? "#000000", strokeWidth: node.strokeWidth ?? 1 };
|
|
3803
|
+
}
|
|
3804
|
+
if (paint === "stroke") {
|
|
3805
|
+
for (const k of STROKE_KEYS) delete base[k];
|
|
3806
|
+
return { ...base, fill: node.stroke ?? null, stroke: null };
|
|
3807
|
+
}
|
|
3808
|
+
return base;
|
|
3809
|
+
}
|
|
3810
|
+
function vectorShapeBuilderMap(entries) {
|
|
3811
|
+
const lines = [];
|
|
3812
|
+
const inputs = entries.map((entry) => {
|
|
3813
|
+
const filled = hasPaint(entry.node.fill);
|
|
3814
|
+
const subpaths = entry.node.subpaths.filter(usable).map((sp) => !sp.closed && filled && subpathEnclosesArea(sp) ? { ...sp, closed: true } : sp);
|
|
3815
|
+
lines.push(!subpaths.some((s) => s.closed));
|
|
3816
|
+
return { path: worldOf(entry, subpaths), fillRule: entry.node.fillRule ?? "nonzero" };
|
|
3817
|
+
});
|
|
3818
|
+
const map = PlanarMap.build(inputs);
|
|
3819
|
+
if (!map) return null;
|
|
3820
|
+
let chains = null;
|
|
3821
|
+
const runs = (line2) => (chains ??= map.chains()).filter((c) => lines[c.source] === line2 && !(line2 && c.closed)).map((c) => {
|
|
3822
|
+
const path = contoursToPath([{ segments: c.segments, closed: c.closed }]);
|
|
3823
|
+
return { path, segments: c.segments, closed: c.closed, source: c.source, bounds: bezierPathBounds(path) };
|
|
3824
|
+
});
|
|
3825
|
+
const pieces = lines.some(Boolean) ? runs(true) : [];
|
|
3826
|
+
let edges = null;
|
|
3827
|
+
let size = 0;
|
|
3828
|
+
for (const input of inputs) {
|
|
3829
|
+
const b = bezierPathBounds(input.path);
|
|
3830
|
+
if (b) size = Math.max(size, b.maxX - b.minX, b.maxY - b.minY);
|
|
3831
|
+
}
|
|
3832
|
+
return {
|
|
3833
|
+
entries: [...entries],
|
|
3834
|
+
map,
|
|
3835
|
+
lines,
|
|
3836
|
+
pieces,
|
|
3837
|
+
get edges() {
|
|
3838
|
+
return edges ??= runs(false);
|
|
3839
|
+
},
|
|
3840
|
+
tolerance: 0.2 * Math.max(size / 2e3, 0.25)
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
function pieceNear(pieces, p, tol) {
|
|
3844
|
+
const point = [p.x, p.y];
|
|
3845
|
+
let best = null;
|
|
3846
|
+
pieces.forEach((piece, index) => {
|
|
3847
|
+
const b = piece.bounds;
|
|
3848
|
+
if (p.x < b.minX - tol || p.x > b.maxX + tol || p.y < b.minY - tol || p.y > b.maxY + tol) return;
|
|
3849
|
+
const d = distanceToBezierPath(piece.path, point);
|
|
3850
|
+
if (d <= tol && (!best || d < best.d)) best = { index, d };
|
|
3851
|
+
});
|
|
3852
|
+
return best;
|
|
3853
|
+
}
|
|
3854
|
+
function vectorShapeBuilderHit(m, p, lineTolerance = null) {
|
|
3855
|
+
if (lineTolerance !== null && lineTolerance > 0) {
|
|
3856
|
+
const line2 = pieceNear(m.pieces, p, lineTolerance);
|
|
3857
|
+
const edge = pieceNear(m.edges, p, lineTolerance);
|
|
3858
|
+
if (edge && (!line2 || edge.d < line2.d)) return { edge: edge.index };
|
|
3859
|
+
if (line2) return { line: line2.index };
|
|
3860
|
+
}
|
|
3861
|
+
const face = m.map.faceAt([p.x, p.y]);
|
|
3862
|
+
return face >= 0 ? { face } : null;
|
|
3863
|
+
}
|
|
3864
|
+
function samplePolyline(points, step) {
|
|
3865
|
+
const out = [];
|
|
3866
|
+
const s = Math.max(step, 1e-3);
|
|
3867
|
+
points.forEach((p, i) => {
|
|
3868
|
+
if (i > 0) {
|
|
3869
|
+
const a = points[i - 1];
|
|
3870
|
+
const n = Math.min(4e3, Math.ceil(Math.hypot(p.x - a.x, p.y - a.y) / s));
|
|
3871
|
+
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 });
|
|
3872
|
+
}
|
|
3873
|
+
out.push(p);
|
|
3874
|
+
});
|
|
3875
|
+
return out;
|
|
3876
|
+
}
|
|
3877
|
+
function addBuilderHit(touched, hit) {
|
|
3878
|
+
if (!hit) return false;
|
|
3879
|
+
const [list, i] = "face" in hit ? [touched.faces, hit.face] : "line" in hit ? [touched.lines, hit.line] : [touched.edges, hit.edge];
|
|
3880
|
+
if (list.includes(i)) return false;
|
|
3881
|
+
list.push(i);
|
|
3882
|
+
return true;
|
|
3883
|
+
}
|
|
3884
|
+
function sides(m, piece) {
|
|
3885
|
+
const seg = piece.segments[Math.floor(piece.segments.length / 2)];
|
|
3886
|
+
const mid = segmentPoint(seg, 0.5);
|
|
3887
|
+
const t = segmentTangent(seg, 0.5);
|
|
3888
|
+
const chord = Math.hypot(seg[seg.length - 1][0] - seg[0][0], seg[seg.length - 1][1] - seg[0][1]);
|
|
3889
|
+
const off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));
|
|
3890
|
+
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])];
|
|
3891
|
+
}
|
|
3892
|
+
function vectorBuilderErasedEdges(m, touched) {
|
|
3893
|
+
if (!touched.edges.length) return [];
|
|
3894
|
+
const faces = new Set(touched.faces);
|
|
3895
|
+
return touched.edges.filter((k) => m.edges[k] && !sides(m, m.edges[k]).some((f) => f >= 0 && faces.has(f)));
|
|
3896
|
+
}
|
|
3897
|
+
function planShapeBuilder(m, touched, erase) {
|
|
3898
|
+
const replace = /* @__PURE__ */ new Map();
|
|
3899
|
+
const picked = new Set(touched.faces);
|
|
3900
|
+
const faces = m.map.faces;
|
|
3901
|
+
const sourcesOf = faces.map((_, i) => m.map.faceSources(i));
|
|
3902
|
+
const cuts = erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k]) : [];
|
|
3903
|
+
m.entries.forEach((entry, i) => {
|
|
3904
|
+
if (m.lines[i]) {
|
|
3905
|
+
const own = m.pieces.flatMap((piece, k) => piece.source === i ? [k] : []);
|
|
3906
|
+
const keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));
|
|
3907
|
+
if (keep.length === own.length) return;
|
|
3908
|
+
replace.set(i, keep.length ? { kind: "pieces", paths: keep.map((k) => m.pieces[k].path) } : null);
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const inUnion = touched.faces.some((f) => sourcesOf[f].includes(i));
|
|
3912
|
+
if (!inUnion && !cuts.length) return;
|
|
3913
|
+
let rest;
|
|
3914
|
+
if (inUnion) {
|
|
3915
|
+
const kept = faces.flatMap((_, f) => sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []);
|
|
3916
|
+
rest = kept.length ? m.map.merge(kept) : [];
|
|
3917
|
+
} else rest = worldOf(entry, entry.node.subpaths.filter(usable));
|
|
3918
|
+
const fillCloses = hasPaint(entry.node.fill);
|
|
3919
|
+
let cut = false;
|
|
3920
|
+
for (const piece of cuts) {
|
|
3921
|
+
const r = rest.length ? cutOutPiece(rest, piece, fillCloses, m.tolerance) : null;
|
|
3922
|
+
if (r) {
|
|
3923
|
+
rest = r;
|
|
3924
|
+
cut = true;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (!inUnion && !cut) return;
|
|
3928
|
+
replace.set(i, rest.length ? { kind: inUnion ? "fill" : "outline", path: rest } : null);
|
|
3929
|
+
});
|
|
3930
|
+
let merged = null;
|
|
3931
|
+
if (!erase && touched.faces.length) {
|
|
3932
|
+
const path = m.map.merge(touched.faces);
|
|
3933
|
+
const covered = touched.faces.map((f) => sourcesOf[f]).find((s) => s.length);
|
|
3934
|
+
const lastShape = m.lines.lastIndexOf(false);
|
|
3935
|
+
const style = covered ? covered[covered.length - 1] : lastShape >= 0 ? lastShape : m.entries.length - 1;
|
|
3936
|
+
if (path.length) merged = { path, style, currentFill: !covered };
|
|
3937
|
+
}
|
|
3938
|
+
return { replace, merged };
|
|
3939
|
+
}
|
|
3940
|
+
function vectorFillMap(doc, gap = 0) {
|
|
3941
|
+
const entries = vectorPathEntries(doc, null, { locked: true });
|
|
3942
|
+
return PlanarMap.build(
|
|
3943
|
+
entries.map((e) => ({ path: worldOf(e, e.node.subpaths.filter(usable)), fills: false })),
|
|
3944
|
+
{ gap, maxSegments: 2e4 }
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3718
3948
|
// src/vector/controller.ts
|
|
3719
3949
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
3950
|
+
var VECTOR_FILL_GAP = 6;
|
|
3720
3951
|
var DEFAULT_VECTOR_PATH_EDIT = {
|
|
3721
3952
|
keepShapeOnDelete: true,
|
|
3722
3953
|
curvatureMoves: false,
|
|
3723
3954
|
pencilSmoothing: 0,
|
|
3724
3955
|
recognizeShapes: false,
|
|
3725
3956
|
eraserSize: 10,
|
|
3726
|
-
pencilPressure: "pen"
|
|
3957
|
+
pencilPressure: "pen",
|
|
3958
|
+
fillGap: 0
|
|
3727
3959
|
};
|
|
3728
3960
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3729
3961
|
var VECTOR_STYLE_KEYS = [
|
|
@@ -3962,9 +4194,27 @@ var TOOL_LABELS = {
|
|
|
3962
4194
|
scissors: "Scissors",
|
|
3963
4195
|
knife: "Knife",
|
|
3964
4196
|
eraser: "Eraser",
|
|
3965
|
-
width: "Width"
|
|
4197
|
+
width: "Width",
|
|
4198
|
+
shapeBuilder: "Shape Builder",
|
|
4199
|
+
paintBucket: "Paint Bucket"
|
|
4200
|
+
};
|
|
4201
|
+
var TOOL_KEYS = {
|
|
4202
|
+
v: "select",
|
|
4203
|
+
a: "direct",
|
|
4204
|
+
p: "pen",
|
|
4205
|
+
n: "pencil",
|
|
4206
|
+
r: "rect",
|
|
4207
|
+
o: "ellipse",
|
|
4208
|
+
y: "polygon",
|
|
4209
|
+
s: "star",
|
|
4210
|
+
l: "line",
|
|
4211
|
+
c: "scissors",
|
|
4212
|
+
k: "knife",
|
|
4213
|
+
e: "eraser",
|
|
4214
|
+
w: "width",
|
|
4215
|
+
m: "shapeBuilder",
|
|
4216
|
+
b: "paintBucket"
|
|
3966
4217
|
};
|
|
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
4218
|
function vectorToolLabel(tool) {
|
|
3969
4219
|
return TOOL_LABELS[tool];
|
|
3970
4220
|
}
|
|
@@ -3981,7 +4231,9 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
3981
4231
|
scissors: "C",
|
|
3982
4232
|
knife: "K",
|
|
3983
4233
|
eraser: "E",
|
|
3984
|
-
width: "W"
|
|
4234
|
+
width: "W",
|
|
4235
|
+
shapeBuilder: "M",
|
|
4236
|
+
paintBucket: "B"
|
|
3985
4237
|
};
|
|
3986
4238
|
var VECTOR_TOOL_GLYPHS = {
|
|
3987
4239
|
select: "\u2196",
|
|
@@ -3996,7 +4248,9 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
3996
4248
|
scissors: "\u2702",
|
|
3997
4249
|
knife: "\u27CB",
|
|
3998
4250
|
eraser: "\u232B",
|
|
3999
|
-
width: "\u21F9"
|
|
4251
|
+
width: "\u21F9",
|
|
4252
|
+
shapeBuilder: "\u2295",
|
|
4253
|
+
paintBucket: "\u25CD"
|
|
4000
4254
|
};
|
|
4001
4255
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
4002
4256
|
var BOOLEAN_OPS = {
|
|
@@ -4006,6 +4260,14 @@ var BOOLEAN_OPS = {
|
|
|
4006
4260
|
exclude: "xor"
|
|
4007
4261
|
};
|
|
4008
4262
|
var BOOLEAN_LABELS = { union: "Union", subtract: "Subtract", intersect: "Intersect", exclude: "Exclude" };
|
|
4263
|
+
var VECTOR_PATHFINDER_LABELS = {
|
|
4264
|
+
divide: "Divide",
|
|
4265
|
+
trim: "Trim",
|
|
4266
|
+
merge: "Merge",
|
|
4267
|
+
crop: "Crop",
|
|
4268
|
+
outline: "Outline",
|
|
4269
|
+
minusBack: "Minus Back"
|
|
4270
|
+
};
|
|
4009
4271
|
function mapTree(nodes, fn) {
|
|
4010
4272
|
let changed = false;
|
|
4011
4273
|
const out = [];
|
|
@@ -4070,6 +4332,13 @@ var VectorEditorController = class {
|
|
|
4070
4332
|
hoverTargets = null;
|
|
4071
4333
|
spaceHeld = false;
|
|
4072
4334
|
clipboard = null;
|
|
4335
|
+
/** Shape Builder map of the selection, kept while the document and the selection stay the same. */
|
|
4336
|
+
builderCache = null;
|
|
4337
|
+
/** Paint bucket map of the visible outlines. */
|
|
4338
|
+
fillCache = null;
|
|
4339
|
+
builderPreview = null;
|
|
4340
|
+
/** What the highlight shows, to skip notifying when the pointer stays over the same face. */
|
|
4341
|
+
builderHoverKey = null;
|
|
4073
4342
|
createId;
|
|
4074
4343
|
onChange;
|
|
4075
4344
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -4144,6 +4413,7 @@ var VectorEditorController = class {
|
|
|
4144
4413
|
interaction: this.interaction,
|
|
4145
4414
|
pen: this.pen,
|
|
4146
4415
|
pointer: this.pointer,
|
|
4416
|
+
builder: this.builderPreview,
|
|
4147
4417
|
canUndo: h.canUndo,
|
|
4148
4418
|
canRedo: h.canRedo,
|
|
4149
4419
|
undoLabel: h.undoName,
|
|
@@ -4202,6 +4472,11 @@ var VectorEditorController = class {
|
|
|
4202
4472
|
if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
|
|
4203
4473
|
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4204
4474
|
return h?.strokeSpot ? "copy" : "default";
|
|
4475
|
+
case "shapeBuilder":
|
|
4476
|
+
if (!this.builderPreview?.faces.length && !this.builderPreview?.lines.length) return "crosshair";
|
|
4477
|
+
return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
|
|
4478
|
+
case "paintBucket":
|
|
4479
|
+
return this.builderPreview ? "cell" : "crosshair";
|
|
4205
4480
|
default:
|
|
4206
4481
|
return "crosshair";
|
|
4207
4482
|
}
|
|
@@ -4328,6 +4603,8 @@ var VectorEditorController = class {
|
|
|
4328
4603
|
this.hover = null;
|
|
4329
4604
|
this.guides = null;
|
|
4330
4605
|
this.widthPoint = null;
|
|
4606
|
+
this.builderPreview = null;
|
|
4607
|
+
this.builderHoverKey = null;
|
|
4331
4608
|
if (tool !== "direct") {
|
|
4332
4609
|
this.anchors = [];
|
|
4333
4610
|
this.editingId = null;
|
|
@@ -5115,6 +5392,255 @@ var VectorEditorController = class {
|
|
|
5115
5392
|
return { ok: true };
|
|
5116
5393
|
}
|
|
5117
5394
|
// -------------------------------------------------------------------------
|
|
5395
|
+
// Pathfinder, Shape Builder and paint bucket
|
|
5396
|
+
// -------------------------------------------------------------------------
|
|
5397
|
+
/**
|
|
5398
|
+
* Pathfinder: Divide, Trim, Merge, Crop, Outline or Minus Back over the selected (or given)
|
|
5399
|
+
* paths, in document space (rotated nodes and groups included, live corners cut). Unfilled
|
|
5400
|
+
* stroked paths take part as the area their stroke paints (Divide: open ones cut instead).
|
|
5401
|
+
* The results replace the operands at the place of the top-most one, grouped when there are
|
|
5402
|
+
* several; each keeps the style of the operand whose area it is. Returns the new path ids,
|
|
5403
|
+
* or an empty list when nothing came out.
|
|
5404
|
+
*/
|
|
5405
|
+
pathfinder(op, options = {}) {
|
|
5406
|
+
const roots = options.ids ?? this.selectedRoots();
|
|
5407
|
+
const entries = vectorPathEntries(this.doc, roots);
|
|
5408
|
+
if (entries.length < (op === "divide" || op === "outline" ? 1 : 2)) return [];
|
|
5409
|
+
const shapes = entries.map((e) => vectorPathfinderShape(e, op));
|
|
5410
|
+
const result = pathfinder(op, shapes);
|
|
5411
|
+
if (result.error || !result.pieces.length) return [];
|
|
5412
|
+
const last = entries[entries.length - 1].node.id;
|
|
5413
|
+
const topRoot = [...roots].reverse().find((r) => {
|
|
5414
|
+
const n = this.getNode(r);
|
|
5415
|
+
return !!n && !!findVectorNode([n], last);
|
|
5416
|
+
}) ?? last;
|
|
5417
|
+
const nodes = [];
|
|
5418
|
+
for (const piece of result.pieces) {
|
|
5419
|
+
const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
|
|
5420
|
+
if (!subpaths.length) continue;
|
|
5421
|
+
const source = entries[piece.source].node;
|
|
5422
|
+
const style = vectorResultStyle(source, shapes[piece.source].paint, op === "outline");
|
|
5423
|
+
nodes.push({ ...style, type: "path", id: this.newId(), name: source.name ?? "Path", subpaths });
|
|
5424
|
+
}
|
|
5425
|
+
if (!nodes.length) return [];
|
|
5426
|
+
const label = VECTOR_PATHFINDER_LABELS[op];
|
|
5427
|
+
const container = nodes.length > 1 ? { id: this.newId(), type: "group", name: label, children: nodes } : nodes[0];
|
|
5428
|
+
const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
|
|
5429
|
+
this.change(label, () => {
|
|
5430
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === topRoot ? container : drop.has(n.id) ? null : n));
|
|
5431
|
+
this.selection = [container.id];
|
|
5432
|
+
this.anchors = [];
|
|
5433
|
+
this.editingId = null;
|
|
5434
|
+
});
|
|
5435
|
+
return nodes.map((n) => n.id);
|
|
5436
|
+
}
|
|
5437
|
+
/** The Shape Builder map of the selected (or given) paths, cached per document and selection. */
|
|
5438
|
+
shapeBuilderMap(ids) {
|
|
5439
|
+
const roots = ids ?? this.selectedRoots();
|
|
5440
|
+
const key = roots.join("\0");
|
|
5441
|
+
if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
|
|
5442
|
+
const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
|
|
5443
|
+
const map = entries.length ? vectorShapeBuilderMap(entries) : null;
|
|
5444
|
+
this.builderCache = { doc: this.doc, key, map };
|
|
5445
|
+
return map;
|
|
5446
|
+
}
|
|
5447
|
+
/** The faces the Shape Builder offers for the selected (or given) paths (document units). */
|
|
5448
|
+
getShapeBuilderFaces(options = {}) {
|
|
5449
|
+
const m = this.shapeBuilderMap(options.ids);
|
|
5450
|
+
if (!m) return [];
|
|
5451
|
+
return m.map.faces.map((f, index) => {
|
|
5452
|
+
const p = m.map.faceInterior(index);
|
|
5453
|
+
return {
|
|
5454
|
+
index,
|
|
5455
|
+
d: bezierPathToSvg(f.path, { precision: 3 }),
|
|
5456
|
+
area: f.area,
|
|
5457
|
+
sources: m.map.faceSources(index).map((i) => m.entries[i].node.id),
|
|
5458
|
+
point: p ? { x: p[0], y: p[1] } : null
|
|
5459
|
+
};
|
|
5460
|
+
});
|
|
5461
|
+
}
|
|
5462
|
+
/** Pick distance of the Shape Builder's erase for line pieces (document units). */
|
|
5463
|
+
builderLineTolerance() {
|
|
5464
|
+
return Math.max(this.hitRadius, 4) / this.viewport.zoom;
|
|
5465
|
+
}
|
|
5466
|
+
/**
|
|
5467
|
+
* Shape Builder gesture along `points` (document units; one point is a click) over the faces
|
|
5468
|
+
* the selected (or given) paths make. Merges the faces touched into one shape styled like the
|
|
5469
|
+
* top-most shape over the first face (the current fill for a face only outlines enclose);
|
|
5470
|
+
* with `erase` deletes them, the pieces of unfilled open paths and the edges of other outlines
|
|
5471
|
+
* (cut where paths meet) within `tolerance`: a deleted edge opens a closed subpath there,
|
|
5472
|
+
* splits an open one or takes a subpath it runs all the way round, and edges bounding a face
|
|
5473
|
+
* deleted in the same gesture go with the face. Every shape covering a touched face loses
|
|
5474
|
+
* that area; paths keep their own transforms. One undo step.
|
|
5475
|
+
*/
|
|
5476
|
+
shapeBuilder(points, options = {}) {
|
|
5477
|
+
const none = (message) => ({ ok: false, message, merged: null, faces: 0, lines: 0, edges: 0 });
|
|
5478
|
+
if (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none("Give the points of the gesture.");
|
|
5479
|
+
const m = this.shapeBuilderMap(options.ids);
|
|
5480
|
+
if (!m) return none("Select paths that make faces.");
|
|
5481
|
+
const erase = !!options.erase;
|
|
5482
|
+
const tol = erase ? options.tolerance ?? this.builderLineTolerance() : null;
|
|
5483
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5484
|
+
const size = m.map.faces.reduce((s, f) => Math.max(s, f.bounds.maxX - f.bounds.minX, f.bounds.maxY - f.bounds.minY), 1);
|
|
5485
|
+
for (const p of samplePolyline(points, Math.max(size / 2e3, 0.25 / this.viewport.zoom))) addBuilderHit(touched, vectorShapeBuilderHit(m, p, tol));
|
|
5486
|
+
if (!touched.faces.length && !touched.lines.length && !touched.edges.length) return none("There is no face under the pointer.");
|
|
5487
|
+
return this.applyShapeBuilder(m, touched, erase);
|
|
5488
|
+
}
|
|
5489
|
+
applyShapeBuilder(m, touched, erase) {
|
|
5490
|
+
const plan = planShapeBuilder(m, touched, erase);
|
|
5491
|
+
let mergedId = null;
|
|
5492
|
+
const created = /* @__PURE__ */ new Map();
|
|
5493
|
+
this.change(erase ? "Shape Builder Delete" : "Shape Builder", () => {
|
|
5494
|
+
let nodes = this.doc.nodes;
|
|
5495
|
+
m.entries.forEach((entry, i) => {
|
|
5496
|
+
const node = entry.node;
|
|
5497
|
+
const toLocal = (path) => transformBezierPath(path, invertMatrix(multiplyMatrices(nodeMatrix(node), entry.parent)));
|
|
5498
|
+
const list = [];
|
|
5499
|
+
const r = plan.replace.get(i);
|
|
5500
|
+
if (r === void 0) list.push(node);
|
|
5501
|
+
else if (r?.kind === "fill") list.push({ ...node, fillRule: "nonzero", subpaths: bezierToSubpaths(toLocal(r.path)) });
|
|
5502
|
+
else if (r?.kind === "outline") list.push({ ...node, subpaths: bezierToSubpaths(toLocal(r.path)) });
|
|
5503
|
+
else if (r?.kind === "pieces") {
|
|
5504
|
+
r.paths.forEach(
|
|
5505
|
+
(path, k) => list.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })
|
|
5506
|
+
);
|
|
5507
|
+
}
|
|
5508
|
+
if (plan.merged && plan.merged.style === i) {
|
|
5509
|
+
mergedId = this.newId();
|
|
5510
|
+
const merged = {
|
|
5511
|
+
...vectorResultStyle(node, "fill"),
|
|
5512
|
+
type: "path",
|
|
5513
|
+
id: mergedId,
|
|
5514
|
+
name: "Shape",
|
|
5515
|
+
...node.transform ? { transform: node.transform } : {},
|
|
5516
|
+
subpaths: bezierToSubpaths(toLocal(plan.merged.path))
|
|
5517
|
+
};
|
|
5518
|
+
if (plan.merged.currentFill || !hasPaint(merged.fill)) merged.fill = hasPaint(this.defaults.fill) ? this.defaults.fill : "#808080";
|
|
5519
|
+
list.push(merged);
|
|
5520
|
+
}
|
|
5521
|
+
if (r === void 0 && list.length === 1) return;
|
|
5522
|
+
created.set(node.id, list.map((n) => n.id));
|
|
5523
|
+
nodes = spliceTree(nodes, node.id, list);
|
|
5524
|
+
});
|
|
5525
|
+
this.setDoc(nodes);
|
|
5526
|
+
const selection = this.selection.flatMap((id) => created.get(id) ?? [id]);
|
|
5527
|
+
if (mergedId && !selection.includes(mergedId)) selection.push(mergedId);
|
|
5528
|
+
this.selection = selection.filter((id, k) => !!this.getNode(id) && selection.indexOf(id) === k);
|
|
5529
|
+
this.anchors = [];
|
|
5530
|
+
this.editingId = null;
|
|
5531
|
+
});
|
|
5532
|
+
this.builderCache = null;
|
|
5533
|
+
return { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length, edges: erase ? vectorBuilderErasedEdges(m, touched).length : 0 };
|
|
5534
|
+
}
|
|
5535
|
+
/** The paint bucket's map of the visible outlines, cached per document and gap. */
|
|
5536
|
+
fillMap(gap) {
|
|
5537
|
+
if (this.fillCache?.doc === this.doc && this.fillCache.gap === gap) return this.fillCache.map;
|
|
5538
|
+
const map = vectorFillMap(this.doc, gap);
|
|
5539
|
+
this.fillCache = { doc: this.doc, gap, map };
|
|
5540
|
+
return map;
|
|
5541
|
+
}
|
|
5542
|
+
/**
|
|
5543
|
+
* Paint bucket: the region the visible outlines enclose around `point` (document units),
|
|
5544
|
+
* crossings split and islands left as holes, becomes a new path filled with the current fill
|
|
5545
|
+
* default, placed just below the selection (or on top when nothing is selected). `gap`
|
|
5546
|
+
* (default `editing.fillGap`) closes gaps up to that size between loose ends. Returns the new
|
|
5547
|
+
* path's id, or null when nothing encloses the point.
|
|
5548
|
+
*/
|
|
5549
|
+
fillRegion(point, options = {}) {
|
|
5550
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
|
|
5551
|
+
const region = this.fillMap(options.gap ?? this.editing.fillGap)?.regionAt([point.x, point.y]);
|
|
5552
|
+
if (!region?.length) return null;
|
|
5553
|
+
let below = null;
|
|
5554
|
+
const roots = new Set(this.selectedRoots());
|
|
5555
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
5556
|
+
if (!below && roots.has(n.id)) below = n.id;
|
|
5557
|
+
});
|
|
5558
|
+
const subpaths = bezierToSubpaths(below ? this.intoParentSpace(below, region) : region);
|
|
5559
|
+
if (!subpaths.length) return null;
|
|
5560
|
+
const id = this.newId();
|
|
5561
|
+
const node = { id, type: "path", name: "Fill", ...this.styleFor(false), stroke: null, fillRule: "nonzero", subpaths };
|
|
5562
|
+
if (!hasPaint(node.fill)) node.fill = "#808080";
|
|
5563
|
+
const target = below;
|
|
5564
|
+
this.change("Fill Region", () => {
|
|
5565
|
+
if (target) {
|
|
5566
|
+
const current = this.getNode(target);
|
|
5567
|
+
this.setDoc(spliceTree(this.doc.nodes, target, [node, current]));
|
|
5568
|
+
} else this.setDoc([...this.doc.nodes, node]);
|
|
5569
|
+
if (!target) this.selection = [id];
|
|
5570
|
+
this.anchors = [];
|
|
5571
|
+
this.editingId = null;
|
|
5572
|
+
});
|
|
5573
|
+
return id;
|
|
5574
|
+
}
|
|
5575
|
+
/** Updates the Shape Builder / paint bucket highlight for the pointer at `p`; true when it changed. */
|
|
5576
|
+
hoverBuilder(p, alt) {
|
|
5577
|
+
if (this.tool === "paintBucket") {
|
|
5578
|
+
const region = this.fillMap(this.editing.fillGap)?.regionAt([p.x, p.y]) ?? null;
|
|
5579
|
+
const key2 = region ? bezierPathToSvg(region, { precision: 2 }) : null;
|
|
5580
|
+
if (key2 === this.builderHoverKey) return false;
|
|
5581
|
+
this.builderHoverKey = key2;
|
|
5582
|
+
this.builderPreview = region ? { mode: "fill", faces: [region], lines: [], trail: null } : null;
|
|
5583
|
+
return true;
|
|
5584
|
+
}
|
|
5585
|
+
const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
|
|
5586
|
+
const hit = m ? vectorShapeBuilderHit(m, p, alt ? this.builderLineTolerance() : null) : null;
|
|
5587
|
+
const key = hit ? `${alt ? "erase" : "merge"}:${JSON.stringify(hit)}` : null;
|
|
5588
|
+
if (key === this.builderHoverKey) return false;
|
|
5589
|
+
this.builderHoverKey = key;
|
|
5590
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5591
|
+
addBuilderHit(touched, hit);
|
|
5592
|
+
this.builderPreview = hit && m ? this.builderPreviewOf(m, touched, alt, null) : null;
|
|
5593
|
+
return true;
|
|
5594
|
+
}
|
|
5595
|
+
builderPreviewOf(m, touched, erase, trail) {
|
|
5596
|
+
const faces = m.map.faces;
|
|
5597
|
+
return {
|
|
5598
|
+
mode: erase ? "erase" : "merge",
|
|
5599
|
+
faces: touched.faces.map((f) => faces[f].path),
|
|
5600
|
+
// Edges bounding a face touched too go with the face, so they are not shown apart.
|
|
5601
|
+
lines: [...touched.lines.map((k) => m.pieces[k].path), ...erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k].path) : []],
|
|
5602
|
+
trail
|
|
5603
|
+
};
|
|
5604
|
+
}
|
|
5605
|
+
downBuilder(p, input) {
|
|
5606
|
+
this.builderHoverKey = null;
|
|
5607
|
+
if (this.tool === "paintBucket") {
|
|
5608
|
+
this.fillRegion(p);
|
|
5609
|
+
this.builderPreview = null;
|
|
5610
|
+
this.hoverBuilder(p, false);
|
|
5611
|
+
this.notify();
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
|
|
5615
|
+
if (!m) return;
|
|
5616
|
+
const erase = !!input.alt;
|
|
5617
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5618
|
+
addBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));
|
|
5619
|
+
this.drag = { kind: "builder", erase, points: [p], touched, map: m };
|
|
5620
|
+
this.interaction = { type: "builder", mode: erase ? "erase" : "merge", points: [p] };
|
|
5621
|
+
this.builderPreview = this.builderPreviewOf(m, touched, erase, [p]);
|
|
5622
|
+
this.notify();
|
|
5623
|
+
}
|
|
5624
|
+
moveBuilder(drag, p) {
|
|
5625
|
+
const last = drag.points[drag.points.length - 1];
|
|
5626
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1) return;
|
|
5627
|
+
const tol = drag.erase ? this.builderLineTolerance() : null;
|
|
5628
|
+
for (const q of samplePolyline([last, p], 2 / this.viewport.zoom)) addBuilderHit(drag.touched, vectorShapeBuilderHit(drag.map, q, tol));
|
|
5629
|
+
drag.points.push(p);
|
|
5630
|
+
this.interaction = { type: "builder", mode: drag.erase ? "erase" : "merge", points: drag.points.slice() };
|
|
5631
|
+
this.builderPreview = this.builderPreviewOf(drag.map, drag.touched, drag.erase, drag.points.slice());
|
|
5632
|
+
this.notify();
|
|
5633
|
+
}
|
|
5634
|
+
upBuilder(drag) {
|
|
5635
|
+
this.builderPreview = null;
|
|
5636
|
+
this.builderHoverKey = null;
|
|
5637
|
+
if (!drag.touched.faces.length && !drag.touched.lines.length && !drag.touched.edges.length) {
|
|
5638
|
+
this.notify();
|
|
5639
|
+
return;
|
|
5640
|
+
}
|
|
5641
|
+
this.applyShapeBuilder(drag.map, drag.touched, drag.erase);
|
|
5642
|
+
}
|
|
5643
|
+
// -------------------------------------------------------------------------
|
|
5118
5644
|
// Anchor editing
|
|
5119
5645
|
// -------------------------------------------------------------------------
|
|
5120
5646
|
updateAnchors(doc, refs, fn) {
|
|
@@ -5651,8 +6177,8 @@ var VectorEditorController = class {
|
|
|
5651
6177
|
if (!frame) return null;
|
|
5652
6178
|
const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
|
|
5653
6179
|
const r = this.hitRadius + 2;
|
|
5654
|
-
const
|
|
5655
|
-
for (const h of
|
|
6180
|
+
const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
|
|
6181
|
+
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
6182
|
return null;
|
|
5657
6183
|
}
|
|
5658
6184
|
hitHandle(screen) {
|
|
@@ -5936,6 +6462,9 @@ var VectorEditorController = class {
|
|
|
5936
6462
|
return this.downPencil(p, input);
|
|
5937
6463
|
case "width":
|
|
5938
6464
|
return this.downWidth(screen, p, input);
|
|
6465
|
+
case "shapeBuilder":
|
|
6466
|
+
case "paintBucket":
|
|
6467
|
+
return this.downBuilder(p, input);
|
|
5939
6468
|
case "scissors": {
|
|
5940
6469
|
const seg = this.nearestOutline(p);
|
|
5941
6470
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -6165,6 +6694,11 @@ var VectorEditorController = class {
|
|
|
6165
6694
|
pointerMove(screen, input = {}) {
|
|
6166
6695
|
const p = this.screenToDocument(screen);
|
|
6167
6696
|
const drag = this.drag;
|
|
6697
|
+
if (!drag && (this.tool === "shapeBuilder" || this.tool === "paintBucket")) {
|
|
6698
|
+
this.pointer = p;
|
|
6699
|
+
if (this.hoverBuilder(p, !!input.alt)) this.notify();
|
|
6700
|
+
return;
|
|
6701
|
+
}
|
|
6168
6702
|
if (!drag) {
|
|
6169
6703
|
const hover = this.computeHover(screen);
|
|
6170
6704
|
const pointerMatters = this.tool === "pen" && !!this.pen;
|
|
@@ -6296,6 +6830,8 @@ var VectorEditorController = class {
|
|
|
6296
6830
|
this.notify(true);
|
|
6297
6831
|
return;
|
|
6298
6832
|
}
|
|
6833
|
+
case "builder":
|
|
6834
|
+
return this.moveBuilder(drag, p);
|
|
6299
6835
|
case "stroke": {
|
|
6300
6836
|
const last = drag.points[drag.points.length - 1];
|
|
6301
6837
|
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
@@ -6526,6 +7062,8 @@ var VectorEditorController = class {
|
|
|
6526
7062
|
} else this.notify();
|
|
6527
7063
|
return;
|
|
6528
7064
|
}
|
|
7065
|
+
case "builder":
|
|
7066
|
+
return this.upBuilder(drag);
|
|
6529
7067
|
case "stroke": {
|
|
6530
7068
|
this.notify();
|
|
6531
7069
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -6597,7 +7135,8 @@ var VectorEditorController = class {
|
|
|
6597
7135
|
this.drag = null;
|
|
6598
7136
|
this.interaction = null;
|
|
6599
7137
|
this.guides = null;
|
|
6600
|
-
if (drag.kind === "pan") {
|
|
7138
|
+
if (drag.kind === "pan" || drag.kind === "builder") {
|
|
7139
|
+
if (drag.kind === "builder") this.builderPreview = null;
|
|
6601
7140
|
this.notify();
|
|
6602
7141
|
return;
|
|
6603
7142
|
}
|
|
@@ -6832,6 +7371,7 @@ function pencilPressureProfile(points, pressures) {
|
|
|
6832
7371
|
// src/vector/render.ts
|
|
6833
7372
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
6834
7373
|
var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
|
|
7374
|
+
var VECTOR_BUILDER_HATCH_GAP = 6;
|
|
6835
7375
|
function renderNode(node, defs, idPrefix) {
|
|
6836
7376
|
if (node.visible === false) return null;
|
|
6837
7377
|
if (node.type === "group") {
|
|
@@ -7006,7 +7546,27 @@ function getVectorOverlayRenderData(state) {
|
|
|
7006
7546
|
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7007
7547
|
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7008
7548
|
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7009
|
-
|
|
7549
|
+
let builder = null;
|
|
7550
|
+
if (state.builder) {
|
|
7551
|
+
const v2 = state.viewport;
|
|
7552
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
7553
|
+
const svg = (path) => bezierPathToSvg(path, { precision: 2 });
|
|
7554
|
+
const screen = state.builder.faces.map((f) => transformBezierPath(f, view));
|
|
7555
|
+
const hatch = state.builder.mode !== "fill";
|
|
7556
|
+
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;
|
|
7557
|
+
builder = {
|
|
7558
|
+
mode: state.builder.mode,
|
|
7559
|
+
faces: screen.map(svg),
|
|
7560
|
+
hatches: screen.map((f) => hatch ? svg(hatchPath(f, { gap: VECTOR_BUILDER_HATCH_GAP })) : ""),
|
|
7561
|
+
lines: state.builder.lines.map((l) => svg(transformBezierPath(l, view))),
|
|
7562
|
+
trail
|
|
7563
|
+
};
|
|
7564
|
+
if (state.builder.mode !== "fill" && it?.type === "builder") {
|
|
7565
|
+
const n = state.builder.faces.length;
|
|
7566
|
+
status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder };
|
|
7010
7570
|
}
|
|
7011
7571
|
var VECTOR_OVERLAY_THEME = {
|
|
7012
7572
|
accent: "#0ea5e9",
|
|
@@ -7022,7 +7582,14 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
7022
7582
|
/** Radius of a live corner widget (a ring with a dot). */
|
|
7023
7583
|
cornerWidgetRadius: 4,
|
|
7024
7584
|
/** Half the diagonal of a width point's diamond. */
|
|
7025
|
-
widthPointSize: 5
|
|
7585
|
+
widthPointSize: 5,
|
|
7586
|
+
/** Shape Builder faces to merge and paint bucket regions. */
|
|
7587
|
+
builderFill: "rgba(14, 165, 233, 0.22)",
|
|
7588
|
+
/** Shape Builder faces and line pieces an Alt gesture deletes. */
|
|
7589
|
+
builderErase: "#ef4444",
|
|
7590
|
+
builderEraseFill: "rgba(239, 68, 68, 0.22)",
|
|
7591
|
+
/** Opacity of the hatch lines over Shape Builder faces (drawn in the face's outline colour). */
|
|
7592
|
+
builderHatchOpacity: 0.6
|
|
7026
7593
|
};
|
|
7027
7594
|
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
7028
7595
|
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 +7705,14 @@ var VECTOR_PATH_ACTIONS = [
|
|
|
7138
7705
|
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
|
|
7139
7706
|
{ id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
|
|
7140
7707
|
];
|
|
7708
|
+
var VECTOR_PATHFINDER_ACTIONS = [
|
|
7709
|
+
{ id: "divide", label: "\u229E", title: "Divide", min: 1 },
|
|
7710
|
+
{ id: "trim", label: "\u29C9", title: "Trim", min: 2 },
|
|
7711
|
+
{ id: "merge", label: "\u29C8", title: "Merge", min: 2 },
|
|
7712
|
+
{ id: "crop", label: "\u2317", title: "Crop", min: 2 },
|
|
7713
|
+
{ id: "outline", label: "\u2B1A", title: "Outline", min: 1 },
|
|
7714
|
+
{ id: "minusBack", label: "\u2296", title: "Minus Back", min: 2 }
|
|
7715
|
+
];
|
|
7141
7716
|
var VECTOR_EDIT_TOGGLES = [
|
|
7142
7717
|
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
7143
7718
|
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
@@ -7461,6 +8036,62 @@ function createVectorCommands(controller, options = {}) {
|
|
|
7461
8036
|
return { id };
|
|
7462
8037
|
}
|
|
7463
8038
|
},
|
|
8039
|
+
{
|
|
8040
|
+
...base,
|
|
8041
|
+
id: `${prefix}pathfinder`,
|
|
8042
|
+
label: "Pathfinder\u2026",
|
|
8043
|
+
keywords: ["divide", "trim", "merge", "crop", "outline", "minus back", "split", "faces"],
|
|
8044
|
+
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.",
|
|
8045
|
+
params: { type: "object", properties: { operation: { type: "string", enum: [...VECTOR_PATHFINDER_OPERATIONS] }, ids: IDS }, required: ["operation"] },
|
|
8046
|
+
undoable: true,
|
|
8047
|
+
run: (_ctx, params) => {
|
|
8048
|
+
if (!VECTOR_PATHFINDER_OPERATIONS.includes(params.operation)) fail(`Unknown Pathfinder operation "${params.operation}".`);
|
|
8049
|
+
const created = controller.pathfinder(params.operation, { ids: ids(params) });
|
|
8050
|
+
if (!created.length) fail("Nothing came out: select paths that overlap (two or more, except for divide and outline).");
|
|
8051
|
+
return { ids: created, selection: [...controller.getState().selection] };
|
|
8052
|
+
}
|
|
8053
|
+
},
|
|
8054
|
+
{
|
|
8055
|
+
...base,
|
|
8056
|
+
id: `${prefix}shapeBuilderFaces`,
|
|
8057
|
+
label: "List Shape Builder Faces",
|
|
8058
|
+
keywords: ["shape builder", "regions", "faces", "planar"],
|
|
8059
|
+
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).",
|
|
8060
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
8061
|
+
run: (_ctx, params) => ({ faces: controller.getShapeBuilderFaces({ ids: ids(params) }) })
|
|
8062
|
+
},
|
|
8063
|
+
{
|
|
8064
|
+
...base,
|
|
8065
|
+
id: `${prefix}shapeBuilder`,
|
|
8066
|
+
label: "Shape Builder",
|
|
8067
|
+
keywords: ["merge faces", "build shape", "combine regions", "delete region"],
|
|
8068
|
+
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, the pieces of unfilled open paths and the edges of other outlines (cut where paths meet) within `tolerance`; a deleted edge opens its path there, and edges bounding a deleted face go with it. Shapes covering those faces lose that area. Returns the counts of faces, line pieces and edges.",
|
|
8069
|
+
params: {
|
|
8070
|
+
type: "object",
|
|
8071
|
+
properties: { points: POINTS, erase: { type: "boolean" }, tolerance: { type: "number", minimum: 0 }, ids: IDS },
|
|
8072
|
+
required: ["points"]
|
|
8073
|
+
},
|
|
8074
|
+
undoable: true,
|
|
8075
|
+
run: (_ctx, params) => {
|
|
8076
|
+
const result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });
|
|
8077
|
+
check(result);
|
|
8078
|
+
return { merged: result.merged, faces: result.faces, lines: result.lines, edges: result.edges };
|
|
8079
|
+
}
|
|
8080
|
+
},
|
|
8081
|
+
{
|
|
8082
|
+
...base,
|
|
8083
|
+
id: `${prefix}fillRegion`,
|
|
8084
|
+
label: "Fill Region",
|
|
8085
|
+
keywords: ["paint bucket", "live paint", "fill area", "enclosed"],
|
|
8086
|
+
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.",
|
|
8087
|
+
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"] },
|
|
8088
|
+
undoable: true,
|
|
8089
|
+
run: (_ctx, params) => {
|
|
8090
|
+
const id = controller.fillRegion({ x: params.point[0], y: params.point[1] }, { gap: params.gap });
|
|
8091
|
+
if (!id) fail("No closed region surrounds that point.");
|
|
8092
|
+
return { id };
|
|
8093
|
+
}
|
|
8094
|
+
},
|
|
7464
8095
|
{
|
|
7465
8096
|
...base,
|
|
7466
8097
|
id: `${prefix}align`,
|
|
@@ -7759,14 +8390,19 @@ export {
|
|
|
7759
8390
|
LazyMouse,
|
|
7760
8391
|
VECTOR_ARROWHEADS,
|
|
7761
8392
|
VECTOR_ARROWHEAD_LABELS,
|
|
8393
|
+
VECTOR_BUILDER_HATCH_GAP,
|
|
7762
8394
|
VECTOR_CLIPBOARD_TYPE,
|
|
7763
8395
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
7764
8396
|
VECTOR_CORNER_KINDS,
|
|
7765
8397
|
VECTOR_CORNER_KIND_LABELS,
|
|
7766
8398
|
VECTOR_CORNER_WIDGET_INSET,
|
|
7767
8399
|
VECTOR_EDIT_TOGGLES,
|
|
8400
|
+
VECTOR_FILL_GAP,
|
|
7768
8401
|
VECTOR_INK_MAX_CORNERS,
|
|
7769
8402
|
VECTOR_OVERLAY_THEME,
|
|
8403
|
+
VECTOR_PATHFINDER_ACTIONS,
|
|
8404
|
+
VECTOR_PATHFINDER_LABELS,
|
|
8405
|
+
VECTOR_PATHFINDER_OPERATIONS,
|
|
7770
8406
|
VECTOR_PATH_ACTIONS,
|
|
7771
8407
|
VECTOR_PENCIL_STRING_PX,
|
|
7772
8408
|
VECTOR_PRESSURE_NOMINAL,
|
|
@@ -7784,6 +8420,7 @@ export {
|
|
|
7784
8420
|
VECTOR_WIDTH_SAME_T,
|
|
7785
8421
|
VectorEditorController,
|
|
7786
8422
|
VectorSnapTargets,
|
|
8423
|
+
addBuilderHit,
|
|
7787
8424
|
applyMatrix,
|
|
7788
8425
|
applyMatrixLinear,
|
|
7789
8426
|
applyVectorCornersAttribute,
|
|
@@ -7860,6 +8497,7 @@ export {
|
|
|
7860
8497
|
nodeMatrix,
|
|
7861
8498
|
parseSvgTransform,
|
|
7862
8499
|
parseVectorSvg,
|
|
8500
|
+
planShapeBuilder,
|
|
7863
8501
|
polygonSubpath,
|
|
7864
8502
|
polylineBounds,
|
|
7865
8503
|
pressureWidthProfile,
|
|
@@ -7874,6 +8512,7 @@ export {
|
|
|
7874
8512
|
reverseWidthProfile,
|
|
7875
8513
|
rotateMatrix,
|
|
7876
8514
|
roundSubpaths,
|
|
8515
|
+
samplePolyline,
|
|
7877
8516
|
sanitizeWidthProfile,
|
|
7878
8517
|
scaleFor,
|
|
7879
8518
|
scaleMatrix,
|
|
@@ -7887,6 +8526,7 @@ export {
|
|
|
7887
8526
|
starSubpath,
|
|
7888
8527
|
strokePositionPoint,
|
|
7889
8528
|
strokeWidthProfileOf,
|
|
8529
|
+
subpathEnclosesArea,
|
|
7890
8530
|
subpathPointAt,
|
|
7891
8531
|
subpathSegment,
|
|
7892
8532
|
subpathSegmentCount,
|
|
@@ -7901,11 +8541,13 @@ export {
|
|
|
7901
8541
|
unionBounds,
|
|
7902
8542
|
variableStrokeOutline,
|
|
7903
8543
|
variableStrokeRings,
|
|
8544
|
+
vectorBuilderErasedEdges,
|
|
7904
8545
|
vectorCornerAt,
|
|
7905
8546
|
vectorCorners,
|
|
7906
8547
|
vectorCornersAttribute,
|
|
7907
8548
|
vectorDiamondPath,
|
|
7908
8549
|
vectorDocumentToSvg,
|
|
8550
|
+
vectorFillMap,
|
|
7909
8551
|
vectorInkShapeName,
|
|
7910
8552
|
vectorNodeBounds,
|
|
7911
8553
|
vectorNodeRotation,
|
|
@@ -7914,8 +8556,13 @@ export {
|
|
|
7914
8556
|
vectorPaintToSvg,
|
|
7915
8557
|
vectorParentMatrix,
|
|
7916
8558
|
vectorPathData,
|
|
8559
|
+
vectorPathEntries,
|
|
7917
8560
|
vectorPathWorldBezier,
|
|
8561
|
+
vectorPathfinderShape,
|
|
7918
8562
|
vectorResizeCursor,
|
|
8563
|
+
vectorResultStyle,
|
|
8564
|
+
vectorShapeBuilderHit,
|
|
8565
|
+
vectorShapeBuilderMap,
|
|
7919
8566
|
vectorStrokeLineArea,
|
|
7920
8567
|
vectorStrokeOutline,
|
|
7921
8568
|
vectorStrokeOutlinePath,
|