@pixerate/editor 0.38.0 → 0.40.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 +3 -3
- package/dist/canvas/index.cjs +31 -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 +7 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-ZCCU3VNI.js → chunk-3LHWTGHW.js} +158 -9
- package/dist/chunk-3LHWTGHW.js.map +1 -0
- package/dist/{chunk-7EDLVBOO.cjs → chunk-VNYRI265.cjs} +159 -10
- package/dist/chunk-VNYRI265.cjs.map +1 -0
- package/dist/{pathfinder-fLmmFOJw.d.ts → pathfinder-D98S2lV5.d.ts} +20 -2
- package/dist/{pathfinder-Bd87_lsQ.d.cts → pathfinder-DKRcXiLS.d.cts} +20 -2
- package/dist/vector/index.cjs +678 -358
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +137 -20
- package/dist/vector/index.d.ts +137 -20
- package/dist/vector/index.js +396 -76
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7EDLVBOO.cjs.map +0 -1
- package/dist/chunk-ZCCU3VNI.js.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
closeContour,
|
|
9
9
|
contourSignedArea,
|
|
10
10
|
contoursToPath,
|
|
11
|
+
cutOutPiece,
|
|
11
12
|
distanceToBezierPath,
|
|
12
13
|
fitCubicWithTangents,
|
|
13
14
|
fitCubics,
|
|
14
15
|
flattenChain,
|
|
15
16
|
formatNumber,
|
|
16
17
|
gradientToSvgDef,
|
|
18
|
+
hatchPath,
|
|
17
19
|
isStraightCubic,
|
|
18
20
|
normalizeBezierPath,
|
|
19
21
|
outlineStroke,
|
|
@@ -36,7 +38,7 @@ import {
|
|
|
36
38
|
transformBezierPath,
|
|
37
39
|
trimSegments,
|
|
38
40
|
windingNumber
|
|
39
|
-
} from "../chunk-
|
|
41
|
+
} from "../chunk-3LHWTGHW.js";
|
|
40
42
|
|
|
41
43
|
// src/vector/types.ts
|
|
42
44
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
@@ -827,7 +829,11 @@ var capExtent = (style, width) => style.strokeLinecap === "round" || style.strok
|
|
|
827
829
|
function arrowheadWeight(style, end) {
|
|
828
830
|
const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
|
|
829
831
|
const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
830
|
-
|
|
832
|
+
const width = style.strokeWidth ?? 1;
|
|
833
|
+
const profile = strokeWidthProfileOf(style);
|
|
834
|
+
const at2 = profile ? widthProfileAt(profile, end === "start" ? 0 : 1) : null;
|
|
835
|
+
const here = at2 ? Math.max(width, width * (at2.left + at2.right) / 2) : width;
|
|
836
|
+
return Math.max(0.25, here * s);
|
|
831
837
|
}
|
|
832
838
|
function arrowheadInset(kind, hw, cap) {
|
|
833
839
|
const l = 4 * hw;
|
|
@@ -3815,29 +3821,48 @@ function vectorShapeBuilderMap(entries) {
|
|
|
3815
3821
|
});
|
|
3816
3822
|
const map = PlanarMap.build(inputs);
|
|
3817
3823
|
if (!map) return null;
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3824
|
+
let chains = null;
|
|
3825
|
+
const runs = (line2) => (chains ??= map.chains()).filter((c) => lines[c.source] === line2 && !(line2 && c.closed)).map((c) => {
|
|
3826
|
+
const path = contoursToPath([{ segments: c.segments, closed: c.closed }]);
|
|
3827
|
+
return { path, segments: c.segments, closed: c.closed, source: c.source, bounds: bezierPathBounds(path) };
|
|
3821
3828
|
});
|
|
3822
|
-
|
|
3829
|
+
const pieces = lines.some(Boolean) ? runs(true) : [];
|
|
3830
|
+
let edges = null;
|
|
3831
|
+
let size = 0;
|
|
3832
|
+
for (const input of inputs) {
|
|
3833
|
+
const b = bezierPathBounds(input.path);
|
|
3834
|
+
if (b) size = Math.max(size, b.maxX - b.minX, b.maxY - b.minY);
|
|
3835
|
+
}
|
|
3836
|
+
return {
|
|
3837
|
+
entries: [...entries],
|
|
3838
|
+
map,
|
|
3839
|
+
lines,
|
|
3840
|
+
pieces,
|
|
3841
|
+
get edges() {
|
|
3842
|
+
return edges ??= runs(false);
|
|
3843
|
+
},
|
|
3844
|
+
tolerance: 0.2 * Math.max(size / 2e3, 0.25)
|
|
3845
|
+
};
|
|
3823
3846
|
}
|
|
3824
|
-
function
|
|
3847
|
+
function pieceNear(pieces, p, tol) {
|
|
3825
3848
|
const point = [p.x, p.y];
|
|
3849
|
+
let best = null;
|
|
3850
|
+
pieces.forEach((piece, index) => {
|
|
3851
|
+
const b = piece.bounds;
|
|
3852
|
+
if (p.x < b.minX - tol || p.x > b.maxX + tol || p.y < b.minY - tol || p.y > b.maxY + tol) return;
|
|
3853
|
+
const d = distanceToBezierPath(piece.path, point);
|
|
3854
|
+
if (d <= tol && (!best || d < best.d)) best = { index, d };
|
|
3855
|
+
});
|
|
3856
|
+
return best;
|
|
3857
|
+
}
|
|
3858
|
+
function vectorShapeBuilderHit(m, p, lineTolerance = null) {
|
|
3826
3859
|
if (lineTolerance !== null && lineTolerance > 0) {
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
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 };
|
|
3860
|
+
const line2 = pieceNear(m.pieces, p, lineTolerance);
|
|
3861
|
+
const edge = pieceNear(m.edges, p, lineTolerance);
|
|
3862
|
+
if (edge && (!line2 || edge.d < line2.d)) return { edge: edge.index };
|
|
3863
|
+
if (line2) return { line: line2.index };
|
|
3839
3864
|
}
|
|
3840
|
-
const face = m.map.faceAt(
|
|
3865
|
+
const face = m.map.faceAt([p.x, p.y]);
|
|
3841
3866
|
return face >= 0 ? { face } : null;
|
|
3842
3867
|
}
|
|
3843
3868
|
function samplePolyline(points, step) {
|
|
@@ -3855,7 +3880,7 @@ function samplePolyline(points, step) {
|
|
|
3855
3880
|
}
|
|
3856
3881
|
function addBuilderHit(touched, hit) {
|
|
3857
3882
|
if (!hit) return false;
|
|
3858
|
-
const [list, i] = "face" in hit ? [touched.faces, hit.face] : [touched.lines, hit.line];
|
|
3883
|
+
const [list, i] = "face" in hit ? [touched.faces, hit.face] : "line" in hit ? [touched.lines, hit.line] : [touched.edges, hit.edge];
|
|
3859
3884
|
if (list.includes(i)) return false;
|
|
3860
3885
|
list.push(i);
|
|
3861
3886
|
return true;
|
|
@@ -3868,12 +3893,18 @@ function sides(m, piece) {
|
|
|
3868
3893
|
const off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));
|
|
3869
3894
|
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
3895
|
}
|
|
3896
|
+
function vectorBuilderErasedEdges(m, touched) {
|
|
3897
|
+
if (!touched.edges.length) return [];
|
|
3898
|
+
const faces = new Set(touched.faces);
|
|
3899
|
+
return touched.edges.filter((k) => m.edges[k] && !sides(m, m.edges[k]).some((f) => f >= 0 && faces.has(f)));
|
|
3900
|
+
}
|
|
3871
3901
|
function planShapeBuilder(m, touched, erase) {
|
|
3872
3902
|
const replace = /* @__PURE__ */ new Map();
|
|
3873
3903
|
const picked = new Set(touched.faces);
|
|
3874
3904
|
const faces = m.map.faces;
|
|
3875
3905
|
const sourcesOf = faces.map((_, i) => m.map.faceSources(i));
|
|
3876
|
-
m.
|
|
3906
|
+
const cuts = erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k]) : [];
|
|
3907
|
+
m.entries.forEach((entry, i) => {
|
|
3877
3908
|
if (m.lines[i]) {
|
|
3878
3909
|
const own = m.pieces.flatMap((piece, k) => piece.source === i ? [k] : []);
|
|
3879
3910
|
const keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));
|
|
@@ -3881,10 +3912,24 @@ function planShapeBuilder(m, touched, erase) {
|
|
|
3881
3912
|
replace.set(i, keep.length ? { kind: "pieces", paths: keep.map((k) => m.pieces[k].path) } : null);
|
|
3882
3913
|
return;
|
|
3883
3914
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3915
|
+
const inUnion = touched.faces.some((f) => sourcesOf[f].includes(i));
|
|
3916
|
+
if (!inUnion && !cuts.length) return;
|
|
3917
|
+
let rest;
|
|
3918
|
+
if (inUnion) {
|
|
3919
|
+
const kept = faces.flatMap((_, f) => sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []);
|
|
3920
|
+
rest = kept.length ? m.map.merge(kept) : [];
|
|
3921
|
+
} else rest = worldOf(entry, entry.node.subpaths.filter(usable));
|
|
3922
|
+
const fillCloses = hasPaint(entry.node.fill);
|
|
3923
|
+
let cut = false;
|
|
3924
|
+
for (const piece of cuts) {
|
|
3925
|
+
const r = rest.length ? cutOutPiece(rest, piece, fillCloses, m.tolerance) : null;
|
|
3926
|
+
if (r) {
|
|
3927
|
+
rest = r;
|
|
3928
|
+
cut = true;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
if (!inUnion && !cut) return;
|
|
3932
|
+
replace.set(i, rest.length ? { kind: inUnion ? "fill" : "outline", path: rest } : null);
|
|
3888
3933
|
});
|
|
3889
3934
|
let merged = null;
|
|
3890
3935
|
if (!erase && touched.faces.length) {
|
|
@@ -4037,6 +4082,12 @@ function vectorStrokeWidthGeometry(node) {
|
|
|
4037
4082
|
const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
|
|
4038
4083
|
return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
|
|
4039
4084
|
}
|
|
4085
|
+
function shiftWidthPoints(profile, indices, dt) {
|
|
4086
|
+
const moving = new Set(indices);
|
|
4087
|
+
const tagged = profile.map((p, i) => ({ p: moving.has(i) ? { ...p, t: Math.min(Math.max(p.t + dt, 0), 1) } : { ...p }, i }));
|
|
4088
|
+
tagged.sort((a, b) => a.p.t - b.p.t || a.i - b.i);
|
|
4089
|
+
return { points: tagged.map((x) => x.p), renumber: new Map(tagged.map((x, k) => [x.i, k])) };
|
|
4090
|
+
}
|
|
4040
4091
|
function getVectorWidthHandles(state) {
|
|
4041
4092
|
if (state.tool !== "width") return [];
|
|
4042
4093
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -4046,7 +4097,7 @@ function getVectorWidthHandles(state) {
|
|
|
4046
4097
|
}
|
|
4047
4098
|
if (state.hover?.nodeId) ids.add(state.hover.nodeId);
|
|
4048
4099
|
if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
|
|
4049
|
-
|
|
4100
|
+
for (const r of state.selectedWidthPoints) ids.add(r.nodeId);
|
|
4050
4101
|
const out = [];
|
|
4051
4102
|
for (const id of ids) {
|
|
4052
4103
|
const node = findVectorNode(state.document.nodes, id);
|
|
@@ -4065,7 +4116,6 @@ function getVectorWidthHandles(state) {
|
|
|
4065
4116
|
const p = applyMatrix(m, c);
|
|
4066
4117
|
const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
|
|
4067
4118
|
const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
|
|
4068
|
-
const sel = state.selectedWidthPoint;
|
|
4069
4119
|
out.push({
|
|
4070
4120
|
ref: { nodeId: id, index, subpath: sp },
|
|
4071
4121
|
x: p.x,
|
|
@@ -4075,7 +4125,7 @@ function getVectorWidthHandles(state) {
|
|
|
4075
4125
|
t: w.t,
|
|
4076
4126
|
leftScale: w.left,
|
|
4077
4127
|
rightScale: w.right,
|
|
4078
|
-
selected:
|
|
4128
|
+
selected: state.selectedWidthPoints.some((r2) => r2.nodeId === id && r2.index === index)
|
|
4079
4129
|
});
|
|
4080
4130
|
});
|
|
4081
4131
|
}
|
|
@@ -4271,7 +4321,8 @@ var VectorEditorController = class {
|
|
|
4271
4321
|
doc;
|
|
4272
4322
|
selection = [];
|
|
4273
4323
|
anchors = [];
|
|
4274
|
-
|
|
4324
|
+
widthPoints = [];
|
|
4325
|
+
widthEdit = null;
|
|
4275
4326
|
editingId = null;
|
|
4276
4327
|
pen = null;
|
|
4277
4328
|
tool;
|
|
@@ -4357,7 +4408,8 @@ var VectorEditorController = class {
|
|
|
4357
4408
|
tool: this.tool,
|
|
4358
4409
|
selection: this.selection,
|
|
4359
4410
|
selectedAnchors: this.anchors,
|
|
4360
|
-
|
|
4411
|
+
selectedWidthPoints: this.widthPoints,
|
|
4412
|
+
widthPointEdit: this.widthEdit,
|
|
4361
4413
|
editingId: this.editingId,
|
|
4362
4414
|
selectionBounds: this.boundsOf(this.selection),
|
|
4363
4415
|
selectionFrame: this.frameOf(this.selectedRoots()),
|
|
@@ -4428,7 +4480,7 @@ var VectorEditorController = class {
|
|
|
4428
4480
|
case "pen":
|
|
4429
4481
|
return h?.anchor ? "pointer" : "crosshair";
|
|
4430
4482
|
case "width":
|
|
4431
|
-
if (this.drag?.kind === "width") return this.drag.mode === "
|
|
4483
|
+
if (this.drag?.kind === "width") return this.drag.mode === "width" ? "ew-resize" : "grabbing";
|
|
4432
4484
|
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4433
4485
|
return h?.strokeSpot ? "copy" : "default";
|
|
4434
4486
|
case "shapeBuilder":
|
|
@@ -4448,6 +4500,7 @@ var VectorEditorController = class {
|
|
|
4448
4500
|
}
|
|
4449
4501
|
restore(s) {
|
|
4450
4502
|
const docChanged = s.doc !== this.doc || this.committing;
|
|
4503
|
+
if (!this.committing) this.widthEdit = null;
|
|
4451
4504
|
this.committing = false;
|
|
4452
4505
|
this.applying = true;
|
|
4453
4506
|
try {
|
|
@@ -4456,6 +4509,13 @@ var VectorEditorController = class {
|
|
|
4456
4509
|
this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
|
|
4457
4510
|
this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
|
|
4458
4511
|
this.pen = s.pen && this.getNode(s.pen.nodeId)?.subpaths?.[s.pen.subpath] ? s.pen : null;
|
|
4512
|
+
if (this.widthPoints.length) {
|
|
4513
|
+
const kept = this.widthPoints.filter((r) => {
|
|
4514
|
+
const n = this.getNode(r.nodeId);
|
|
4515
|
+
return n?.type === "path" && !!n.strokeWidthProfile?.[r.index];
|
|
4516
|
+
});
|
|
4517
|
+
if (kept.length !== this.widthPoints.length) this.widthPoints = kept;
|
|
4518
|
+
}
|
|
4459
4519
|
} finally {
|
|
4460
4520
|
this.applying = false;
|
|
4461
4521
|
}
|
|
@@ -4561,7 +4621,8 @@ var VectorEditorController = class {
|
|
|
4561
4621
|
this.tool = tool;
|
|
4562
4622
|
this.hover = null;
|
|
4563
4623
|
this.guides = null;
|
|
4564
|
-
this.
|
|
4624
|
+
this.widthPoints = [];
|
|
4625
|
+
this.widthEdit = null;
|
|
4565
4626
|
this.builderPreview = null;
|
|
4566
4627
|
this.builderHoverKey = null;
|
|
4567
4628
|
if (tool !== "direct") {
|
|
@@ -4837,18 +4898,143 @@ var VectorEditorController = class {
|
|
|
4837
4898
|
const p = profile;
|
|
4838
4899
|
points = p.taper || p.wave ? generateWidthProfile(p) : [];
|
|
4839
4900
|
}
|
|
4840
|
-
this.
|
|
4901
|
+
this.widthPoints = [];
|
|
4902
|
+
this.widthEdit = null;
|
|
4841
4903
|
return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
|
|
4842
4904
|
}
|
|
4843
|
-
/** Removes one width point of a path's profile
|
|
4905
|
+
/** Removes one width point of a path's profile. */
|
|
4844
4906
|
removeWidthPoint(ref) {
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4907
|
+
return this.removeWidthPoints([ref]);
|
|
4908
|
+
}
|
|
4909
|
+
/** Removes width points (default: the selected ones — the width tool's Delete), as one undo step. */
|
|
4910
|
+
removeWidthPoints(refs = this.widthPoints) {
|
|
4911
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4912
|
+
for (const r of refs) {
|
|
4913
|
+
const node = this.getNode(r.nodeId);
|
|
4914
|
+
if (node?.type !== "path" || !node.strokeWidthProfile?.[r.index]) continue;
|
|
4915
|
+
byNode.set(r.nodeId, [...byNode.get(r.nodeId) ?? [], r.index]);
|
|
4916
|
+
}
|
|
4917
|
+
if (!byNode.size) return { ok: false, message: "There is no such width point." };
|
|
4918
|
+
const count = [...byNode.values()].reduce((n, l) => n + new Set(l).size, 0);
|
|
4919
|
+
this.widthPoints = [];
|
|
4920
|
+
this.widthEdit = null;
|
|
4921
|
+
this.change(count > 1 ? "Delete Width Points" : "Delete Width Point", () => {
|
|
4922
|
+
for (const [id, indices] of byNode) {
|
|
4923
|
+
const node = this.getNode(id);
|
|
4924
|
+
const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), indices);
|
|
4925
|
+
this.replaceNode(id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null }));
|
|
4926
|
+
}
|
|
4927
|
+
});
|
|
4928
|
+
return { ok: true };
|
|
4929
|
+
}
|
|
4930
|
+
/** Selects width points (width tool); points on other paths than the first are dropped. */
|
|
4931
|
+
selectWidthPoints(refs) {
|
|
4932
|
+
const first = refs[0]?.nodeId;
|
|
4933
|
+
const next = [];
|
|
4934
|
+
for (const r of refs) {
|
|
4935
|
+
if (r.nodeId !== first || next.some((q) => q.index === r.index)) continue;
|
|
4936
|
+
const n = this.getNode(r.nodeId);
|
|
4937
|
+
if (n?.type === "path" && n.strokeWidthProfile?.[r.index]) next.push({ nodeId: r.nodeId, index: r.index, subpath: r.subpath ?? 0 });
|
|
4938
|
+
}
|
|
4939
|
+
this.widthPoints = next;
|
|
4940
|
+
this.notify();
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Sets one width point numerically: `index` names an existing point of path `nodeId`, null
|
|
4944
|
+
* (or left out) adds one at `values.t`. `left` / `right` are side widths in document units
|
|
4945
|
+
* (how far the stroke reaches on each side of the path), `t` the position along each subpath
|
|
4946
|
+
* (0–1). A point moved onto another one's `t` makes a step there. Selects the point and
|
|
4947
|
+
* refreshes an open width-point editor; one undo step. Ported from ArtCraft vectorcraft's
|
|
4948
|
+
* Width Point Edit dialog (`crates/tools/src/distort/width.rs`, MIT OR Apache-2.0).
|
|
4949
|
+
*/
|
|
4950
|
+
setWidthPoint(target, values) {
|
|
4951
|
+
const node = this.getNode(target.nodeId);
|
|
4952
|
+
if (node?.type !== "path") return { ok: false, message: "There is no such path." };
|
|
4953
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
4954
|
+
if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
|
|
4955
|
+
const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
|
|
4956
|
+
const t = num(values.t);
|
|
4957
|
+
const left = num(values.left);
|
|
4958
|
+
const right = num(values.right);
|
|
4959
|
+
const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
|
|
4960
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
4961
|
+
let points;
|
|
4962
|
+
let index;
|
|
4963
|
+
const existing = target.index ?? null;
|
|
4964
|
+
if (existing === null) {
|
|
4965
|
+
if (t === void 0) return { ok: false, message: "A new width point needs a position t." };
|
|
4966
|
+
if (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: "The profile has too many width points." };
|
|
4967
|
+
({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));
|
|
4968
|
+
} else {
|
|
4969
|
+
const p = profile[existing];
|
|
4970
|
+
if (!p) return { ok: false, message: "There is no such width point." };
|
|
4971
|
+
const sides2 = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };
|
|
4972
|
+
const tt = t === void 0 ? p.t : Math.min(Math.max(t, 0), 1);
|
|
4973
|
+
if (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {
|
|
4974
|
+
points = setWidthPoint(profile, existing, sides2);
|
|
4975
|
+
index = existing;
|
|
4976
|
+
} else {
|
|
4977
|
+
const rest = removeWidthPoints(profile, [existing]);
|
|
4978
|
+
if (rest.length) ({ points, index } = insertWidthPoint(rest, tt, sides2, { step: true }));
|
|
4979
|
+
else {
|
|
4980
|
+
points = [{ t: tt, ...sides2 }];
|
|
4981
|
+
index = 0;
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
const subpath = this.widthEdit?.nodeId === node.id ? this.widthEdit.subpath : this.widthPoints.find((r) => r.nodeId === node.id)?.subpath ?? 0;
|
|
4986
|
+
const edit = this.widthEdit;
|
|
4987
|
+
this.change("Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: points })));
|
|
4988
|
+
this.widthPoints = [{ nodeId: node.id, index, subpath }];
|
|
4989
|
+
if (edit?.nodeId === node.id && (edit.index === existing || existing === null)) this.widthEdit = this.widthEditFor(node.id, subpath, index, null);
|
|
4990
|
+
this.notify();
|
|
4850
4991
|
return { ok: true };
|
|
4851
4992
|
}
|
|
4993
|
+
/**
|
|
4994
|
+
* Opens the width-point editor (what a double-click with the width tool does) on a width
|
|
4995
|
+
* point, or on a spot of a stroke (`t` without `index`: applying it adds a point there).
|
|
4996
|
+
* Returns false when the path has no visible stroke.
|
|
4997
|
+
*/
|
|
4998
|
+
openWidthPointEdit(target) {
|
|
4999
|
+
const edit = this.widthEditFor(target.nodeId, target.subpath ?? 0, target.index ?? null, target.t ?? null);
|
|
5000
|
+
if (!edit) return false;
|
|
5001
|
+
this.widthEdit = edit;
|
|
5002
|
+
this.widthPoints = edit.index === null ? [] : [{ nodeId: edit.nodeId, index: edit.index, subpath: edit.subpath }];
|
|
5003
|
+
this.notify();
|
|
5004
|
+
return true;
|
|
5005
|
+
}
|
|
5006
|
+
/** Closes the width-point editor without changing anything. */
|
|
5007
|
+
closeWidthPointEdit() {
|
|
5008
|
+
if (!this.widthEdit) return;
|
|
5009
|
+
this.widthEdit = null;
|
|
5010
|
+
this.notify();
|
|
5011
|
+
}
|
|
5012
|
+
/** Applies `values` to the width point being edited (see `setWidthPoint`) and closes the editor. */
|
|
5013
|
+
commitWidthPointEdit(values) {
|
|
5014
|
+
const edit = this.widthEdit;
|
|
5015
|
+
if (!edit) return { ok: false, message: "No width point is being edited." };
|
|
5016
|
+
const r = this.setWidthPoint({ nodeId: edit.nodeId, index: edit.index }, edit.index === null ? { t: edit.t, ...values } : values);
|
|
5017
|
+
if (r.ok) this.closeWidthPointEdit();
|
|
5018
|
+
return r;
|
|
5019
|
+
}
|
|
5020
|
+
/** The editor's view of point `index` (or of the stroke at `t` when index is null) of `nodeId`. */
|
|
5021
|
+
widthEditFor(nodeId, subpath, index, t) {
|
|
5022
|
+
const node = this.getNode(nodeId);
|
|
5023
|
+
if (node?.type !== "path") return null;
|
|
5024
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
5025
|
+
if (!g) return null;
|
|
5026
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
5027
|
+
const p = index === null ? null : profile[index];
|
|
5028
|
+
if (index !== null && !p) return null;
|
|
5029
|
+
const tt = p ? p.t : Math.min(Math.max(t ?? 0, 0), 1);
|
|
5030
|
+
const sides2 = p ?? widthProfileAt(profile.length ? profile : null, tt);
|
|
5031
|
+
const sp = Math.min(Math.max(0, subpath), Math.max(0, node.subpaths.length - 1));
|
|
5032
|
+
const at2 = strokePositionPoint(g.line, sp, tt);
|
|
5033
|
+
const round = (v2) => Math.round(v2 * 1e6) / 1e6;
|
|
5034
|
+
const q = at2 ? applyMatrix(this.worldOf(nodeId), { x: at2.point[0], y: at2.point[1] }) : { x: 0, y: 0 };
|
|
5035
|
+
const point = { x: round(q.x), y: round(q.y) };
|
|
5036
|
+
return { nodeId, subpath: sp, index, t: round(tt), left: round(sides2.left * g.unit), right: round(sides2.right * g.unit), point };
|
|
5037
|
+
}
|
|
4852
5038
|
/**
|
|
4853
5039
|
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
4854
5040
|
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
@@ -5426,20 +5612,23 @@ var VectorEditorController = class {
|
|
|
5426
5612
|
* Shape Builder gesture along `points` (document units; one point is a click) over the faces
|
|
5427
5613
|
* the selected (or given) paths make. Merges the faces touched into one shape styled like the
|
|
5428
5614
|
* top-most shape over the first face (the current fill for a face only outlines enclose);
|
|
5429
|
-
* with `erase` deletes them,
|
|
5430
|
-
*
|
|
5615
|
+
* with `erase` deletes them, the pieces of unfilled open paths and the edges of other outlines
|
|
5616
|
+
* (cut where paths meet) within `tolerance`: a deleted edge opens a closed subpath there,
|
|
5617
|
+
* splits an open one or takes a subpath it runs all the way round, and edges bounding a face
|
|
5618
|
+
* deleted in the same gesture go with the face. Every shape covering a touched face loses
|
|
5619
|
+
* that area; paths keep their own transforms. One undo step.
|
|
5431
5620
|
*/
|
|
5432
5621
|
shapeBuilder(points, options = {}) {
|
|
5433
|
-
const none = (message) => ({ ok: false, message, merged: null, faces: 0, lines: 0 });
|
|
5622
|
+
const none = (message) => ({ ok: false, message, merged: null, faces: 0, lines: 0, edges: 0 });
|
|
5434
5623
|
if (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none("Give the points of the gesture.");
|
|
5435
5624
|
const m = this.shapeBuilderMap(options.ids);
|
|
5436
5625
|
if (!m) return none("Select paths that make faces.");
|
|
5437
5626
|
const erase = !!options.erase;
|
|
5438
5627
|
const tol = erase ? options.tolerance ?? this.builderLineTolerance() : null;
|
|
5439
|
-
const touched = { faces: [], lines: [] };
|
|
5628
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5440
5629
|
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
5630
|
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.");
|
|
5631
|
+
if (!touched.faces.length && !touched.lines.length && !touched.edges.length) return none("There is no face under the pointer.");
|
|
5443
5632
|
return this.applyShapeBuilder(m, touched, erase);
|
|
5444
5633
|
}
|
|
5445
5634
|
applyShapeBuilder(m, touched, erase) {
|
|
@@ -5455,6 +5644,7 @@ var VectorEditorController = class {
|
|
|
5455
5644
|
const r = plan.replace.get(i);
|
|
5456
5645
|
if (r === void 0) list.push(node);
|
|
5457
5646
|
else if (r?.kind === "fill") list.push({ ...node, fillRule: "nonzero", subpaths: bezierToSubpaths(toLocal(r.path)) });
|
|
5647
|
+
else if (r?.kind === "outline") list.push({ ...node, subpaths: bezierToSubpaths(toLocal(r.path)) });
|
|
5458
5648
|
else if (r?.kind === "pieces") {
|
|
5459
5649
|
r.paths.forEach(
|
|
5460
5650
|
(path, k) => list.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })
|
|
@@ -5485,7 +5675,7 @@ var VectorEditorController = class {
|
|
|
5485
5675
|
this.editingId = null;
|
|
5486
5676
|
});
|
|
5487
5677
|
this.builderCache = null;
|
|
5488
|
-
return { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length };
|
|
5678
|
+
return { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length, edges: erase ? vectorBuilderErasedEdges(m, touched).length : 0 };
|
|
5489
5679
|
}
|
|
5490
5680
|
/** The paint bucket's map of the visible outlines, cached per document and gap. */
|
|
5491
5681
|
fillMap(gap) {
|
|
@@ -5542,7 +5732,9 @@ var VectorEditorController = class {
|
|
|
5542
5732
|
const key = hit ? `${alt ? "erase" : "merge"}:${JSON.stringify(hit)}` : null;
|
|
5543
5733
|
if (key === this.builderHoverKey) return false;
|
|
5544
5734
|
this.builderHoverKey = key;
|
|
5545
|
-
|
|
5735
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5736
|
+
addBuilderHit(touched, hit);
|
|
5737
|
+
this.builderPreview = hit && m ? this.builderPreviewOf(m, touched, alt, null) : null;
|
|
5546
5738
|
return true;
|
|
5547
5739
|
}
|
|
5548
5740
|
builderPreviewOf(m, touched, erase, trail) {
|
|
@@ -5550,7 +5742,8 @@ var VectorEditorController = class {
|
|
|
5550
5742
|
return {
|
|
5551
5743
|
mode: erase ? "erase" : "merge",
|
|
5552
5744
|
faces: touched.faces.map((f) => faces[f].path),
|
|
5553
|
-
|
|
5745
|
+
// Edges bounding a face touched too go with the face, so they are not shown apart.
|
|
5746
|
+
lines: [...touched.lines.map((k) => m.pieces[k].path), ...erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k].path) : []],
|
|
5554
5747
|
trail
|
|
5555
5748
|
};
|
|
5556
5749
|
}
|
|
@@ -5566,7 +5759,7 @@ var VectorEditorController = class {
|
|
|
5566
5759
|
const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
|
|
5567
5760
|
if (!m) return;
|
|
5568
5761
|
const erase = !!input.alt;
|
|
5569
|
-
const touched = { faces: [], lines: [] };
|
|
5762
|
+
const touched = { faces: [], lines: [], edges: [] };
|
|
5570
5763
|
addBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));
|
|
5571
5764
|
this.drag = { kind: "builder", erase, points: [p], touched, map: m };
|
|
5572
5765
|
this.interaction = { type: "builder", mode: erase ? "erase" : "merge", points: [p] };
|
|
@@ -5586,7 +5779,7 @@ var VectorEditorController = class {
|
|
|
5586
5779
|
upBuilder(drag) {
|
|
5587
5780
|
this.builderPreview = null;
|
|
5588
5781
|
this.builderHoverKey = null;
|
|
5589
|
-
if (!drag.touched.faces.length && !drag.touched.lines.length) {
|
|
5782
|
+
if (!drag.touched.faces.length && !drag.touched.lines.length && !drag.touched.edges.length) {
|
|
5590
5783
|
this.notify();
|
|
5591
5784
|
return;
|
|
5592
5785
|
}
|
|
@@ -6250,13 +6443,19 @@ var VectorEditorController = class {
|
|
|
6250
6443
|
}
|
|
6251
6444
|
}
|
|
6252
6445
|
widthView() {
|
|
6253
|
-
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction,
|
|
6446
|
+
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoints: this.widthPoints };
|
|
6254
6447
|
}
|
|
6255
6448
|
/** The width point centre or side end under `screen` (width tool). */
|
|
6256
6449
|
hitWidthHandle(screen) {
|
|
6450
|
+
const shown = this.hitWidthHandleIn(screen, this.widthView());
|
|
6451
|
+
if (shown) return shown;
|
|
6452
|
+
const spot = this.strokeSpotAt(this.screenToDocument(screen));
|
|
6453
|
+
return spot && spot.nodeId !== this.hover?.nodeId ? this.hitWidthHandleIn(screen, { ...this.widthView(), hover: { nodeId: spot.nodeId } }) : null;
|
|
6454
|
+
}
|
|
6455
|
+
hitWidthHandleIn(screen, view) {
|
|
6257
6456
|
let best = null;
|
|
6258
6457
|
let bestD = Infinity;
|
|
6259
|
-
for (const h of getVectorWidthHandles(
|
|
6458
|
+
for (const h of getVectorWidthHandles(view)) {
|
|
6260
6459
|
for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
|
|
6261
6460
|
const s = this.documentToScreen(q);
|
|
6262
6461
|
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
@@ -6309,11 +6508,10 @@ var VectorEditorController = class {
|
|
|
6309
6508
|
const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
|
|
6310
6509
|
const node = nodeId ? this.getNode(nodeId) : void 0;
|
|
6311
6510
|
const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
|
|
6511
|
+
if (this.widthEdit) this.widthEdit = null;
|
|
6312
6512
|
if (!node || node.type !== "path" || !g) {
|
|
6313
|
-
if (
|
|
6314
|
-
|
|
6315
|
-
this.notify();
|
|
6316
|
-
}
|
|
6513
|
+
if (!input.shift) this.widthPoints = [];
|
|
6514
|
+
this.notify();
|
|
6317
6515
|
return;
|
|
6318
6516
|
}
|
|
6319
6517
|
const before = this.snapshot();
|
|
@@ -6322,6 +6520,8 @@ var VectorEditorController = class {
|
|
|
6322
6520
|
let ref;
|
|
6323
6521
|
let mode = "width";
|
|
6324
6522
|
let side = "both";
|
|
6523
|
+
let group = [];
|
|
6524
|
+
let shift = false;
|
|
6325
6525
|
let t;
|
|
6326
6526
|
let subpath;
|
|
6327
6527
|
if (hit) {
|
|
@@ -6329,8 +6529,20 @@ var VectorEditorController = class {
|
|
|
6329
6529
|
ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
|
|
6330
6530
|
t = profile[ref.index]?.t ?? 0;
|
|
6331
6531
|
subpath = hit.ref.subpath;
|
|
6332
|
-
if (hit.ref.part === "center") mode = "move";
|
|
6532
|
+
if (hit.ref.part === "center") mode = input.alt && profile.length < VECTOR_WIDTH_PROFILE_MAX_POINTS ? "copy" : "move";
|
|
6333
6533
|
else if (input.alt) side = hit.ref.part;
|
|
6534
|
+
const same = this.widthPoints.length > 0 && this.widthPoints[0].nodeId === node.id;
|
|
6535
|
+
const selected = same && this.widthPoints.some((r) => r.index === ref.index);
|
|
6536
|
+
if (input.shift && mode !== "copy") {
|
|
6537
|
+
if (selected) {
|
|
6538
|
+
this.widthPoints = this.widthPoints.filter((r) => r.index !== ref.index);
|
|
6539
|
+
this.notify();
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6542
|
+
this.widthPoints = same ? [...this.widthPoints, ref] : [ref];
|
|
6543
|
+
shift = true;
|
|
6544
|
+
} else if (mode === "copy" || !selected) this.widthPoints = [ref];
|
|
6545
|
+
if (mode !== "copy") group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);
|
|
6334
6546
|
} else {
|
|
6335
6547
|
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
6336
6548
|
const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
|
|
@@ -6339,17 +6551,21 @@ var VectorEditorController = class {
|
|
|
6339
6551
|
t = near2.t;
|
|
6340
6552
|
ref = { nodeId: node.id, index: inserted.index, subpath };
|
|
6341
6553
|
if (input.alt) side = near2.side;
|
|
6554
|
+
this.widthPoints = [ref];
|
|
6342
6555
|
}
|
|
6343
6556
|
const at2 = strokePositionPoint(g.line, subpath, t);
|
|
6344
6557
|
if (!at2) return;
|
|
6345
6558
|
const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6346
6559
|
this.doc = base;
|
|
6347
|
-
this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
|
|
6348
6560
|
this.drag = {
|
|
6349
6561
|
kind: "width",
|
|
6350
6562
|
ref,
|
|
6351
6563
|
mode,
|
|
6352
6564
|
side,
|
|
6565
|
+
source: ref.index,
|
|
6566
|
+
group,
|
|
6567
|
+
shift,
|
|
6568
|
+
renumber: null,
|
|
6353
6569
|
origin: { x: at2.point[0], y: at2.point[1] },
|
|
6354
6570
|
normal: { x: at2.tangent[1], y: -at2.tangent[0] },
|
|
6355
6571
|
unit: g.unit,
|
|
@@ -6364,6 +6580,31 @@ var VectorEditorController = class {
|
|
|
6364
6580
|
this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
|
|
6365
6581
|
this.notify(!hit);
|
|
6366
6582
|
}
|
|
6583
|
+
/**
|
|
6584
|
+
* A width-tool slide to `t` on the dragged point's subpath lands exactly on another width
|
|
6585
|
+
* point (not `skip`) within the pick distance on screen: dropping a point onto another makes
|
|
6586
|
+
* a discontinuous point (two points at one `t`, the width steps there).
|
|
6587
|
+
*/
|
|
6588
|
+
snapWidthT(drag, node, line2, t, skip) {
|
|
6589
|
+
const m = this.worldOf(node.id, drag.base);
|
|
6590
|
+
const at2 = strokePositionPoint(line2, drag.ref.subpath, t);
|
|
6591
|
+
if (!at2) return t;
|
|
6592
|
+
const here = this.documentToScreen(applyMatrix(m, { x: at2.point[0], y: at2.point[1] }));
|
|
6593
|
+
let best = t;
|
|
6594
|
+
let bestD = this.hitRadius;
|
|
6595
|
+
drag.profile.forEach((w, i) => {
|
|
6596
|
+
if (i === skip || drag.group.includes(i)) return;
|
|
6597
|
+
const q = strokePositionPoint(line2, drag.ref.subpath, w.t);
|
|
6598
|
+
if (!q) return;
|
|
6599
|
+
const s = this.documentToScreen(applyMatrix(m, { x: q.point[0], y: q.point[1] }));
|
|
6600
|
+
const d = Math.hypot(s.x - here.x, s.y - here.y);
|
|
6601
|
+
if (d <= bestD) {
|
|
6602
|
+
best = w.t;
|
|
6603
|
+
bestD = d;
|
|
6604
|
+
}
|
|
6605
|
+
});
|
|
6606
|
+
return best;
|
|
6607
|
+
}
|
|
6367
6608
|
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
6368
6609
|
nearestOutline(p) {
|
|
6369
6610
|
let best = null;
|
|
@@ -6848,19 +7089,35 @@ var VectorEditorController = class {
|
|
|
6848
7089
|
drag.moved = true;
|
|
6849
7090
|
const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
|
|
6850
7091
|
const local = this.toLocal(node.id, p, drag.base);
|
|
7092
|
+
const src = drag.profile[drag.source];
|
|
6851
7093
|
let profile;
|
|
6852
|
-
|
|
7094
|
+
let index = drag.source;
|
|
7095
|
+
if (drag.mode !== "width") {
|
|
6853
7096
|
const g = vectorStrokeWidthGeometry(node);
|
|
6854
7097
|
const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
|
|
6855
|
-
if (!near2) return;
|
|
6856
|
-
|
|
7098
|
+
if (!g || !near2) return;
|
|
7099
|
+
const t = drag.group.length ? near2.t : this.snapWidthT(drag, node, g.line, near2.t, drag.source);
|
|
7100
|
+
if (drag.mode === "copy") {
|
|
7101
|
+
({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));
|
|
7102
|
+
} else if (drag.group.length) {
|
|
7103
|
+
const shifted = shiftWidthPoints(drag.profile, [drag.source, ...drag.group], t - src.t);
|
|
7104
|
+
profile = shifted.points;
|
|
7105
|
+
drag.renumber = shifted.renumber;
|
|
7106
|
+
index = shifted.renumber.get(drag.source) ?? drag.source;
|
|
7107
|
+
} else profile = setWidthPoint(drag.profile, drag.source, { t });
|
|
6857
7108
|
} else {
|
|
6858
7109
|
const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
|
|
6859
7110
|
const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
|
|
6860
|
-
profile = setWidthPoint(drag.profile, drag.
|
|
7111
|
+
profile = setWidthPoint(drag.profile, drag.source, patch);
|
|
7112
|
+
if (drag.group.length) {
|
|
7113
|
+
const dl = profile[drag.source].left - src.left;
|
|
7114
|
+
const dr = profile[drag.source].right - src.right;
|
|
7115
|
+
for (const i of drag.group) profile = setWidthPoint(profile, i, { left: Math.max(0, drag.profile[i].left + dl), right: Math.max(0, drag.profile[i].right + dr) });
|
|
7116
|
+
}
|
|
6861
7117
|
}
|
|
6862
7118
|
this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6863
|
-
|
|
7119
|
+
drag.ref = { ...drag.ref, index };
|
|
7120
|
+
const w = profile[index];
|
|
6864
7121
|
this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
|
|
6865
7122
|
this.notify(true);
|
|
6866
7123
|
return;
|
|
@@ -7006,12 +7263,23 @@ var VectorEditorController = class {
|
|
|
7006
7263
|
return;
|
|
7007
7264
|
}
|
|
7008
7265
|
case "width": {
|
|
7009
|
-
if (drag.moved)
|
|
7266
|
+
if (drag.moved) {
|
|
7267
|
+
const several = drag.group.length > 0;
|
|
7268
|
+
if (drag.mode === "copy") this.widthPoints = [drag.ref];
|
|
7269
|
+
else if (drag.mode === "move" && several) {
|
|
7270
|
+
this.widthPoints = [drag.source, ...drag.group].map((i) => ({ ...drag.ref, index: drag.renumber?.get(i) ?? i }));
|
|
7271
|
+
} else if (!several) this.widthPoints = [drag.ref];
|
|
7272
|
+
const label = drag.mode === "copy" ? "Copy Width Point" : drag.mode === "move" ? several ? "Move Width Points" : "Move Width Point" : several ? "Width Points" : "Width Point";
|
|
7273
|
+
return this.commit(label, drag.before);
|
|
7274
|
+
}
|
|
7010
7275
|
if (drag.created) {
|
|
7011
7276
|
this.doc = drag.before.doc;
|
|
7012
|
-
this.
|
|
7277
|
+
this.widthPoints = [];
|
|
7013
7278
|
this.notify(true);
|
|
7014
|
-
} else
|
|
7279
|
+
} else {
|
|
7280
|
+
if (!drag.shift && drag.group.length) this.widthPoints = [drag.ref];
|
|
7281
|
+
this.notify();
|
|
7282
|
+
}
|
|
7015
7283
|
return;
|
|
7016
7284
|
}
|
|
7017
7285
|
case "builder":
|
|
@@ -7100,7 +7368,10 @@ var VectorEditorController = class {
|
|
|
7100
7368
|
}
|
|
7101
7369
|
this.restore(drag.before);
|
|
7102
7370
|
}
|
|
7103
|
-
/**
|
|
7371
|
+
/**
|
|
7372
|
+
* Double click: select tool enters path editing, direct tool toggles an anchor or adds one,
|
|
7373
|
+
* width tool opens the width-point editor (`widthPointEdit`).
|
|
7374
|
+
*/
|
|
7104
7375
|
doubleClick(screen) {
|
|
7105
7376
|
const p = this.screenToDocument(screen);
|
|
7106
7377
|
if (this.tool === "select") {
|
|
@@ -7128,6 +7399,16 @@ var VectorEditorController = class {
|
|
|
7128
7399
|
}
|
|
7129
7400
|
return;
|
|
7130
7401
|
}
|
|
7402
|
+
if (this.tool === "width") {
|
|
7403
|
+
const hit = this.hitWidthHandle(screen);
|
|
7404
|
+
if (hit) {
|
|
7405
|
+
this.openWidthPointEdit(hit.ref);
|
|
7406
|
+
return;
|
|
7407
|
+
}
|
|
7408
|
+
const spot = this.strokeSpotAt(p);
|
|
7409
|
+
if (spot) this.openWidthPointEdit({ nodeId: spot.nodeId, subpath: spot.subpath, t: spot.t });
|
|
7410
|
+
return;
|
|
7411
|
+
}
|
|
7131
7412
|
if (this.tool === "pen") this.finishPath();
|
|
7132
7413
|
}
|
|
7133
7414
|
// -------------------------------------------------------------------------
|
|
@@ -7188,8 +7469,9 @@ var VectorEditorController = class {
|
|
|
7188
7469
|
switch (key) {
|
|
7189
7470
|
case "Escape":
|
|
7190
7471
|
if (this.drag) this.cancelDrag();
|
|
7191
|
-
else if (this.
|
|
7192
|
-
|
|
7472
|
+
else if (this.widthEdit) this.closeWidthPointEdit();
|
|
7473
|
+
else if (this.tool === "width" && this.widthPoints.length) {
|
|
7474
|
+
this.widthPoints = [];
|
|
7193
7475
|
this.notify();
|
|
7194
7476
|
} else if (this.pen) this.finishPath();
|
|
7195
7477
|
else if (this.tool === "direct" && this.anchors.length) {
|
|
@@ -7212,7 +7494,7 @@ var VectorEditorController = class {
|
|
|
7212
7494
|
return false;
|
|
7213
7495
|
case "Delete":
|
|
7214
7496
|
case "Backspace":
|
|
7215
|
-
if (this.tool === "width" && this.
|
|
7497
|
+
if (this.tool === "width" && this.widthPoints.length) return this.removeWidthPoints().ok;
|
|
7216
7498
|
if (this.pen) {
|
|
7217
7499
|
const node = this.getNode(this.pen.nodeId);
|
|
7218
7500
|
const sp = node.subpaths[this.pen.subpath];
|
|
@@ -7323,6 +7605,7 @@ function pencilPressureProfile(points, pressures) {
|
|
|
7323
7605
|
// src/vector/render.ts
|
|
7324
7606
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
7325
7607
|
var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
|
|
7608
|
+
var VECTOR_BUILDER_HATCH_GAP = 6;
|
|
7326
7609
|
function renderNode(node, defs, idPrefix) {
|
|
7327
7610
|
if (node.visible === false) return null;
|
|
7328
7611
|
if (node.type === "group") {
|
|
@@ -7496,14 +7779,22 @@ function getVectorOverlayRenderData(state) {
|
|
|
7496
7779
|
});
|
|
7497
7780
|
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7498
7781
|
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7499
|
-
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7782
|
+
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt(it.width)}`;
|
|
7500
7783
|
let builder = null;
|
|
7501
7784
|
if (state.builder) {
|
|
7502
7785
|
const v2 = state.viewport;
|
|
7503
7786
|
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
7504
|
-
const
|
|
7787
|
+
const svg = (path) => bezierPathToSvg(path, { precision: 2 });
|
|
7788
|
+
const screen = state.builder.faces.map((f) => transformBezierPath(f, view));
|
|
7789
|
+
const hatch = state.builder.mode !== "fill";
|
|
7505
7790
|
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 = {
|
|
7791
|
+
builder = {
|
|
7792
|
+
mode: state.builder.mode,
|
|
7793
|
+
faces: screen.map(svg),
|
|
7794
|
+
hatches: screen.map((f) => hatch ? svg(hatchPath(f, { gap: VECTOR_BUILDER_HATCH_GAP })) : ""),
|
|
7795
|
+
lines: state.builder.lines.map((l) => svg(transformBezierPath(l, view))),
|
|
7796
|
+
trail
|
|
7797
|
+
};
|
|
7507
7798
|
if (state.builder.mode !== "fill" && it?.type === "builder") {
|
|
7508
7799
|
const n = state.builder.faces.length;
|
|
7509
7800
|
status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
|
|
@@ -7530,7 +7821,9 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
7530
7821
|
builderFill: "rgba(14, 165, 233, 0.22)",
|
|
7531
7822
|
/** Shape Builder faces and line pieces an Alt gesture deletes. */
|
|
7532
7823
|
builderErase: "#ef4444",
|
|
7533
|
-
builderEraseFill: "rgba(239, 68, 68, 0.22)"
|
|
7824
|
+
builderEraseFill: "rgba(239, 68, 68, 0.22)",
|
|
7825
|
+
/** Opacity of the hatch lines over Shape Builder faces (drawn in the face's outline colour). */
|
|
7826
|
+
builderHatchOpacity: 0.6
|
|
7534
7827
|
};
|
|
7535
7828
|
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
7536
7829
|
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`;
|
|
@@ -7905,6 +8198,31 @@ function createVectorCommands(controller, options = {}) {
|
|
|
7905
8198
|
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
7906
8199
|
}
|
|
7907
8200
|
},
|
|
8201
|
+
{
|
|
8202
|
+
...base,
|
|
8203
|
+
id: `${prefix}setWidthPoint`,
|
|
8204
|
+
label: "Width Point\u2026",
|
|
8205
|
+
keywords: ["width point", "variable width", "stroke width at", "taper", "profile"],
|
|
8206
|
+
description: "Sets one width point of a path's stroke profile numerically: `index` names an existing point (see `vector.setWidthProfile`'s result), leave it out to add one at `t`. `left` / `right` are side widths in document units (how far the stroke reaches on each side), `t` the position along each subpath (0\u20131); a point moved onto another's `t` makes a step. Returns the profile.",
|
|
8207
|
+
params: {
|
|
8208
|
+
type: "object",
|
|
8209
|
+
properties: {
|
|
8210
|
+
id: { type: "string", description: "The path." },
|
|
8211
|
+
index: { type: "integer", minimum: 0 },
|
|
8212
|
+
t: { type: "number", minimum: 0, maximum: 1 },
|
|
8213
|
+
left: { type: "number", minimum: 0 },
|
|
8214
|
+
right: { type: "number", minimum: 0 }
|
|
8215
|
+
},
|
|
8216
|
+
required: ["id"]
|
|
8217
|
+
},
|
|
8218
|
+
undoable: true,
|
|
8219
|
+
run: (_ctx, params) => {
|
|
8220
|
+
if (!params?.id) fail("Pass the path `id`.");
|
|
8221
|
+
check(controller.setWidthPoint({ nodeId: params.id, index: params.index ?? null }, { t: params.t, left: params.left, right: params.right }));
|
|
8222
|
+
const node = controller.getNode(params.id);
|
|
8223
|
+
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
8224
|
+
}
|
|
8225
|
+
},
|
|
7908
8226
|
{
|
|
7909
8227
|
...base,
|
|
7910
8228
|
id: `${prefix}outlineStroke`,
|
|
@@ -8006,7 +8324,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8006
8324
|
id: `${prefix}shapeBuilder`,
|
|
8007
8325
|
label: "Shape Builder",
|
|
8008
8326
|
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
|
|
8327
|
+
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.",
|
|
8010
8328
|
params: {
|
|
8011
8329
|
type: "object",
|
|
8012
8330
|
properties: { points: POINTS, erase: { type: "boolean" }, tolerance: { type: "number", minimum: 0 }, ids: IDS },
|
|
@@ -8016,7 +8334,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
8016
8334
|
run: (_ctx, params) => {
|
|
8017
8335
|
const result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });
|
|
8018
8336
|
check(result);
|
|
8019
|
-
return { merged: result.merged, faces: result.faces, lines: result.lines };
|
|
8337
|
+
return { merged: result.merged, faces: result.faces, lines: result.lines, edges: result.edges };
|
|
8020
8338
|
}
|
|
8021
8339
|
},
|
|
8022
8340
|
{
|
|
@@ -8331,6 +8649,7 @@ export {
|
|
|
8331
8649
|
LazyMouse,
|
|
8332
8650
|
VECTOR_ARROWHEADS,
|
|
8333
8651
|
VECTOR_ARROWHEAD_LABELS,
|
|
8652
|
+
VECTOR_BUILDER_HATCH_GAP,
|
|
8334
8653
|
VECTOR_CLIPBOARD_TYPE,
|
|
8335
8654
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
8336
8655
|
VECTOR_CORNER_KINDS,
|
|
@@ -8481,6 +8800,7 @@ export {
|
|
|
8481
8800
|
unionBounds,
|
|
8482
8801
|
variableStrokeOutline,
|
|
8483
8802
|
variableStrokeRings,
|
|
8803
|
+
vectorBuilderErasedEdges,
|
|
8484
8804
|
vectorCornerAt,
|
|
8485
8805
|
vectorCorners,
|
|
8486
8806
|
vectorCornersAttribute,
|