@pixerate/editor 0.34.0 → 0.36.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 +14 -0
- package/dist/canvas/index.cjs +178 -623
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +10 -1
- package/dist/canvas/index.d.ts +10 -1
- package/dist/canvas/index.js +35 -480
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-UG5DVXJK.cjs → chunk-BGNPT5PE.cjs} +446 -6
- package/dist/chunk-BGNPT5PE.cjs.map +1 -0
- package/dist/{chunk-RK6HUKDI.js → chunk-EGPIVUEF.js} +451 -11
- package/dist/chunk-EGPIVUEF.js.map +1 -0
- package/dist/vector/index.cjs +2943 -401
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +696 -24
- package/dist/vector/index.d.ts +696 -24
- package/dist/vector/index.js +2880 -338
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-RK6HUKDI.js.map +0 -1
- package/dist/chunk-UG5DVXJK.cjs.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -4,33 +4,460 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
bezierPathBounds,
|
|
6
6
|
bezierPathToSvg,
|
|
7
|
+
closeContour,
|
|
8
|
+
contourSignedArea,
|
|
9
|
+
contoursToPath,
|
|
7
10
|
distanceToBezierPath,
|
|
11
|
+
fitCubicWithTangents,
|
|
8
12
|
fitCubics,
|
|
9
13
|
formatNumber,
|
|
10
14
|
gradientToSvgDef,
|
|
15
|
+
normalizeBezierPath,
|
|
16
|
+
outlineStroke,
|
|
11
17
|
pathBoolean,
|
|
18
|
+
pathToContours,
|
|
12
19
|
pointInBezierPath,
|
|
20
|
+
segmentBounds,
|
|
13
21
|
segmentDerivative,
|
|
14
22
|
segmentIntersections,
|
|
23
|
+
segmentLength,
|
|
15
24
|
segmentNearest,
|
|
25
|
+
segmentParamAtLength,
|
|
16
26
|
segmentPoint,
|
|
27
|
+
segmentTangent,
|
|
17
28
|
splitSegment,
|
|
29
|
+
subSegment,
|
|
18
30
|
toBezierPath,
|
|
19
|
-
transformBezierPath
|
|
20
|
-
|
|
31
|
+
transformBezierPath,
|
|
32
|
+
trimSegments,
|
|
33
|
+
windingNumber
|
|
34
|
+
} from "../chunk-EGPIVUEF.js";
|
|
21
35
|
|
|
22
36
|
// src/vector/types.ts
|
|
23
|
-
var
|
|
37
|
+
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
38
|
+
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
39
|
+
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
|
|
24
40
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
25
41
|
|
|
26
|
-
// src/vector/
|
|
42
|
+
// src/vector/corners.ts
|
|
27
43
|
var EPS = 1e-9;
|
|
44
|
+
var MIN_SINE = 1e-6;
|
|
45
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
46
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
47
|
+
var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
|
|
48
|
+
var dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
49
|
+
var cross = (a, b) => a.x * b.y - a.y * b.x;
|
|
50
|
+
var len = (a) => Math.hypot(a.x, a.y);
|
|
51
|
+
var pt = (p) => [p.x, p.y];
|
|
52
|
+
var vec = (p) => ({ x: p[0], y: p[1] });
|
|
53
|
+
var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
|
|
54
|
+
var Side = class {
|
|
55
|
+
curve;
|
|
56
|
+
straight;
|
|
57
|
+
length;
|
|
58
|
+
constructor(a, h1, h2, b) {
|
|
59
|
+
this.straight = onAnchor(a, h1) && onAnchor(b, h2);
|
|
60
|
+
this.curve = this.straight ? [pt(a), pt(b)] : [pt(a), pt(h1 ?? a), pt(h2 ?? b), pt(b)];
|
|
61
|
+
this.length = this.straight ? Math.hypot(b.x - a.x, b.y - a.y) : segmentLength(this.curve);
|
|
62
|
+
}
|
|
63
|
+
/** Unit direction the side leaves the corner in (towards its first control point off the corner). */
|
|
64
|
+
direction() {
|
|
65
|
+
const p0 = vec(this.curve[0]);
|
|
66
|
+
for (const q of this.curve.slice(1)) {
|
|
67
|
+
const d = sub(vec(q), p0);
|
|
68
|
+
const l = len(d);
|
|
69
|
+
if (l > EPS) return mul(d, 1 / l);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** The point `s` along the side from the corner: its parameter, position and the direction the side runs on in there. */
|
|
74
|
+
at(s, u) {
|
|
75
|
+
if (this.straight) {
|
|
76
|
+
const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
|
|
77
|
+
return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
|
|
78
|
+
}
|
|
79
|
+
const t = segmentParamAtLength(this.curve, s, this.length);
|
|
80
|
+
const d = vec(segmentDerivative(this.curve, t));
|
|
81
|
+
const l = len(d);
|
|
82
|
+
return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function cornerOf(sp, s, i) {
|
|
86
|
+
const n = sp.anchors.length;
|
|
87
|
+
if (n < 2 || !sp.closed && (i === 0 || i === n - 1)) return null;
|
|
88
|
+
if (n === 2 && sp.closed) return null;
|
|
89
|
+
const a = sp.anchors[i];
|
|
90
|
+
if (a.type === "smooth" || a.type === "symmetric") return null;
|
|
91
|
+
const prev = sp.anchors[(i - 1 + n) % n];
|
|
92
|
+
const next = sp.anchors[(i + 1) % n];
|
|
93
|
+
const sideU = new Side(a, a.in, prev.out, prev);
|
|
94
|
+
const sideV = new Side(a, a.out, next.in, next);
|
|
95
|
+
const u = sideU.direction();
|
|
96
|
+
const v2 = sideV.direction();
|
|
97
|
+
if (!u || !v2) return null;
|
|
98
|
+
const lu = sideU.length;
|
|
99
|
+
const lv = sideV.length;
|
|
100
|
+
if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
|
|
101
|
+
const cos = Math.max(-1, Math.min(1, dot(u, v2)));
|
|
102
|
+
const sin = Math.abs(cross(u, v2));
|
|
103
|
+
if (sin <= MIN_SINE) return null;
|
|
104
|
+
return { subpath: s, index: i, at: { x: a.x, y: a.y }, u, v: v2, lu, lv, cos, sin, angle: Math.atan2(sin, cos) * 180 / Math.PI, maxRadius: Math.min(lu, lv) / 2 * sin / (1 + cos), sideU, sideV };
|
|
105
|
+
}
|
|
106
|
+
var strip = ({ sideU: _u, sideV: _v, ...info }) => info;
|
|
107
|
+
function vectorCornerAt(sp, index, subpath = 0) {
|
|
108
|
+
const c = cornerOf(sp, subpath, index);
|
|
109
|
+
return c ? strip(c) : null;
|
|
110
|
+
}
|
|
111
|
+
function vectorCorners(subpaths) {
|
|
112
|
+
const out = [];
|
|
113
|
+
subpaths.forEach((sp, s) => {
|
|
114
|
+
for (let i = 0; i < sp.anchors.length; i++) {
|
|
115
|
+
const c = cornerOf(sp, s, i);
|
|
116
|
+
if (c) out.push(strip(c));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
|
|
122
|
+
var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
|
|
123
|
+
var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
|
|
124
|
+
var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
|
|
125
|
+
function arcHandle(cos) {
|
|
126
|
+
const c = Math.max(-1, Math.min(1, cos));
|
|
127
|
+
const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
|
|
128
|
+
const cosHalf = Math.sqrt(Math.max(0, (1 + c) / 2));
|
|
129
|
+
return 4 / 3 * sinHalf / (1 + cosHalf);
|
|
130
|
+
}
|
|
131
|
+
function squareTo(a, b) {
|
|
132
|
+
const w = sub(b, mul(a, dot(a, b)));
|
|
133
|
+
const l = len(w);
|
|
134
|
+
return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
|
|
135
|
+
}
|
|
136
|
+
function cutCorner(c, radius, kind) {
|
|
137
|
+
const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
|
|
138
|
+
if (!(s > EPS)) return null;
|
|
139
|
+
const a = c.sideU.at(s, c.u);
|
|
140
|
+
const b = c.sideV.at(s, c.v);
|
|
141
|
+
if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
|
|
142
|
+
if (kind === "inverted") {
|
|
143
|
+
const h = s * arcHandle(dot(a.dir, b.dir));
|
|
144
|
+
return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(squareTo(a.dir, b.dir), h)), in: add(b.p, mul(squareTo(b.dir, a.dir), h)), end: b.p };
|
|
145
|
+
}
|
|
146
|
+
const d1 = mul(a.dir, -1);
|
|
147
|
+
const d2 = mul(b.dir, -1);
|
|
148
|
+
const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
|
|
149
|
+
const chord = len(sub(b.p, a.p));
|
|
150
|
+
const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
|
|
151
|
+
if (!Number.isFinite(k)) return null;
|
|
152
|
+
return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(d1, k)), in: add(b.p, mul(d2, k)), end: b.p };
|
|
153
|
+
}
|
|
154
|
+
var radiusOf = (a) => {
|
|
155
|
+
const r = a.corner?.radius;
|
|
156
|
+
return typeof r === "number" && r > 0 && Number.isFinite(r) ? r : 0;
|
|
157
|
+
};
|
|
158
|
+
function hasVectorCorners(subpaths) {
|
|
159
|
+
return subpaths.some((sp) => sp.anchors.some((a) => radiusOf(a) > 0));
|
|
160
|
+
}
|
|
161
|
+
function expandVectorCornersOf(sp, subpath = 0) {
|
|
162
|
+
const n = sp.anchors.length;
|
|
163
|
+
const cuts = sp.anchors.map((a, i) => {
|
|
164
|
+
const r = radiusOf(a);
|
|
165
|
+
if (!r) return null;
|
|
166
|
+
const c = cornerOf(sp, subpath, i);
|
|
167
|
+
if (!c) return null;
|
|
168
|
+
const fitted = fitCornerRadius(c, r);
|
|
169
|
+
return fitted > EPS ? cutCorner(c, fitted, a.corner?.kind ?? "round") : null;
|
|
170
|
+
});
|
|
171
|
+
if (!cuts.some(Boolean)) return { subpath: { closed: sp.closed, anchors: sp.anchors.map(plain) }, sources: sp.anchors.map((_, i) => i) };
|
|
172
|
+
const trimmed = sp.anchors.map((a, j) => {
|
|
173
|
+
const k = (j + 1) % n;
|
|
174
|
+
if (k === 0 && !sp.closed) return void 0;
|
|
175
|
+
const b = sp.anchors[k];
|
|
176
|
+
const from = cuts[j];
|
|
177
|
+
const to = cuts[k];
|
|
178
|
+
if (!from && !to) return void 0;
|
|
179
|
+
if (onAnchor(a, a.out) && onAnchor(b, b.in)) return { out: null, in: null };
|
|
180
|
+
const s0 = from ? from.tv : 0;
|
|
181
|
+
const s1 = to ? 1 - to.tu : 1;
|
|
182
|
+
const seg = [pt(a), pt(a.out ?? a), pt(b.in ?? b), pt(b)];
|
|
183
|
+
const part = subSegment(seg, s0, Math.max(s0, s1));
|
|
184
|
+
return { out: vec(part[1]), in: vec(part[2]) };
|
|
185
|
+
});
|
|
186
|
+
const anchors = [];
|
|
187
|
+
const sources = [];
|
|
188
|
+
let closing = null;
|
|
189
|
+
const handleIn = (i, a) => {
|
|
190
|
+
const t = trimmed[(i - 1 + n) % n];
|
|
191
|
+
return t ? t.in : a.in ?? null;
|
|
192
|
+
};
|
|
193
|
+
const handleOut = (i, a) => {
|
|
194
|
+
const t = trimmed[i];
|
|
195
|
+
return t ? t.out : a.out ?? null;
|
|
196
|
+
};
|
|
197
|
+
sp.anchors.forEach((a, i) => {
|
|
198
|
+
const c = cuts[i];
|
|
199
|
+
if (!c) {
|
|
200
|
+
anchors.push(withHandles({ x: a.x, y: a.y, ...a.type ? { type: a.type } : {} }, handleIn(i, a), handleOut(i, a)));
|
|
201
|
+
sources.push(i);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const start = withHandles({ x: c.start.x, y: c.start.y }, handleIn(i, a), c.out);
|
|
205
|
+
const end = withHandles({ x: c.end.x, y: c.end.y }, c.in, handleOut(i, a));
|
|
206
|
+
if (i === 0 && sp.closed) closing = start;
|
|
207
|
+
else {
|
|
208
|
+
anchors.push(start);
|
|
209
|
+
sources.push(i);
|
|
210
|
+
}
|
|
211
|
+
anchors.push(end);
|
|
212
|
+
sources.push(i);
|
|
213
|
+
});
|
|
214
|
+
if (closing) {
|
|
215
|
+
anchors.push(closing);
|
|
216
|
+
sources.push(0);
|
|
217
|
+
}
|
|
218
|
+
return { subpath: { closed: sp.closed, anchors }, sources };
|
|
219
|
+
}
|
|
220
|
+
function withHandles(a, hin, hout) {
|
|
221
|
+
if (hin && !onAnchor(a, hin)) a.in = { x: hin.x, y: hin.y };
|
|
222
|
+
if (hout && !onAnchor(a, hout)) a.out = { x: hout.x, y: hout.y };
|
|
223
|
+
return a;
|
|
224
|
+
}
|
|
225
|
+
function plain(a) {
|
|
226
|
+
if (!("corner" in a)) return a;
|
|
227
|
+
const { corner: _c, ...rest } = a;
|
|
228
|
+
return rest;
|
|
229
|
+
}
|
|
230
|
+
function expandVectorCorners(subpaths) {
|
|
231
|
+
if (!hasVectorCorners(subpaths)) return subpaths;
|
|
232
|
+
return subpaths.map((sp, s) => sp.anchors.some((a) => radiusOf(a) > 0) ? expandVectorCornersOf(sp, s).subpath : sp);
|
|
233
|
+
}
|
|
234
|
+
function setAnchorCorner(a, radius, kind) {
|
|
235
|
+
const { corner, ...rest } = a;
|
|
236
|
+
const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
|
|
237
|
+
const k = kind ?? corner?.kind;
|
|
238
|
+
if (r <= 0 && !kind) return rest;
|
|
239
|
+
return { ...rest, corner: { radius: r, ...k && k !== "round" ? { kind: k } : {} } };
|
|
240
|
+
}
|
|
241
|
+
function nextVectorCornerKind(kind) {
|
|
242
|
+
return kind === "inverted" ? "chamfer" : kind === "chamfer" ? "round" : "inverted";
|
|
243
|
+
}
|
|
244
|
+
function setSubpathCorners(sp, radius, kind) {
|
|
245
|
+
return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/vector/stroke.ts
|
|
249
|
+
var KAPPA = 0.5522847498307936;
|
|
250
|
+
var NOTCH = 0.7;
|
|
251
|
+
function isClosedBezierShape(path) {
|
|
252
|
+
const contours = pathToContours(path).filter((c) => c.segments.length > 0);
|
|
253
|
+
return contours.length > 0 && contours.every((c) => c.closed);
|
|
254
|
+
}
|
|
255
|
+
function effectiveStrokeAlign(style, closedShape) {
|
|
256
|
+
const a = style.strokeAlign ?? "center";
|
|
257
|
+
return closedShape ? a : "center";
|
|
258
|
+
}
|
|
259
|
+
function hasStrokeExtras(style) {
|
|
260
|
+
return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow;
|
|
261
|
+
}
|
|
262
|
+
var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
|
|
263
|
+
function arrowheadWeight(style, end) {
|
|
264
|
+
const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
|
|
265
|
+
const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
266
|
+
return Math.max(0.25, (style.strokeWidth ?? 1) * s);
|
|
267
|
+
}
|
|
268
|
+
function arrowheadInset(kind, hw, cap) {
|
|
269
|
+
const l = 4 * hw;
|
|
270
|
+
const solid = (back) => Math.max(back / 2, cap);
|
|
271
|
+
switch (kind) {
|
|
272
|
+
case "triangle":
|
|
273
|
+
case "circle":
|
|
274
|
+
case "square":
|
|
275
|
+
return solid(l);
|
|
276
|
+
case "arrow":
|
|
277
|
+
return solid(NOTCH * l);
|
|
278
|
+
case "bar":
|
|
279
|
+
return solid(0.75 * hw);
|
|
280
|
+
case "open":
|
|
281
|
+
return Math.max(hw * Math.sqrt(l * l + l * l / 4) / (l / 2), cap);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
var v = (x, y) => ({ x, y });
|
|
285
|
+
var plus = (a, b, k = 1) => ({ x: a.x + b.x * k, y: a.y + b.y * k });
|
|
286
|
+
var unit = (a) => {
|
|
287
|
+
const l = Math.hypot(a.x, a.y);
|
|
288
|
+
return l > 0 ? { x: a.x / l, y: a.y / l } : { x: 1, y: 0 };
|
|
289
|
+
};
|
|
290
|
+
function polygon(pts) {
|
|
291
|
+
return [{ type: "M", x: pts[0].x, y: pts[0].y }, ...pts.slice(1).map((p) => ({ type: "L", x: p.x, y: p.y })), { type: "Z" }];
|
|
292
|
+
}
|
|
293
|
+
function circle(c, r) {
|
|
294
|
+
const k = r * KAPPA;
|
|
295
|
+
const { x, y } = c;
|
|
296
|
+
return [
|
|
297
|
+
{ type: "M", x: x + r, y },
|
|
298
|
+
{ type: "C", x1: x + r, y1: y + k, x2: x + k, y2: y + r, x, y: y + r },
|
|
299
|
+
{ type: "C", x1: x - k, y1: y + r, x2: x - r, y2: y + k, x: x - r, y },
|
|
300
|
+
{ type: "C", x1: x - r, y1: y - k, x2: x - k, y2: y - r, x, y: y - r },
|
|
301
|
+
{ type: "C", x1: x + k, y1: y - r, x2: x + r, y2: y - k, x: x + r, y },
|
|
302
|
+
{ type: "Z" }
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
function arrowheadPath(kind, tip, dir, hw) {
|
|
306
|
+
const n = v(-dir.y, dir.x);
|
|
307
|
+
const l = 4 * hw;
|
|
308
|
+
const half = l / 2;
|
|
309
|
+
const base = plus(tip, dir, -l);
|
|
310
|
+
switch (kind) {
|
|
311
|
+
case "triangle":
|
|
312
|
+
return polygon([tip, plus(base, n, half), plus(base, n, -half)]);
|
|
313
|
+
case "arrow":
|
|
314
|
+
return polygon([tip, plus(base, n, half), plus(tip, dir, -NOTCH * l), plus(base, n, -half)]);
|
|
315
|
+
case "open": {
|
|
316
|
+
const t = hw;
|
|
317
|
+
const a = plus(base, n, half);
|
|
318
|
+
const b = plus(base, n, -half);
|
|
319
|
+
const ua = unit(plus(a, tip, -1));
|
|
320
|
+
const ub = unit(plus(b, tip, -1));
|
|
321
|
+
const na = v(-ua.y, ua.x);
|
|
322
|
+
const nb = v(ub.y, -ub.x);
|
|
323
|
+
const inner = plus(tip, dir, -(t * Math.hypot(a.x - tip.x, a.y - tip.y)) / half);
|
|
324
|
+
return polygon([tip, a, plus(a, na, t), inner, plus(b, nb, t), b]);
|
|
325
|
+
}
|
|
326
|
+
case "circle":
|
|
327
|
+
return circle(plus(tip, dir, -half), half);
|
|
328
|
+
case "square":
|
|
329
|
+
return polygon([plus(tip, n, half), plus(base, n, half), plus(base, n, -half), plus(tip, n, -half)]);
|
|
330
|
+
case "bar": {
|
|
331
|
+
const t = 0.75 * hw;
|
|
332
|
+
return polygon([plus(tip, n, half), plus(plus(tip, n, half), dir, -t), plus(plus(tip, n, -half), dir, -t), plus(tip, n, -half)]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
var toVec = (p) => ({ x: p[0], y: p[1] });
|
|
337
|
+
var segLen = (s) => segmentLength(s);
|
|
338
|
+
function endOf(segs, atStart) {
|
|
339
|
+
const long = (s2) => segLen(s2) > 1e-9;
|
|
340
|
+
if (atStart) {
|
|
341
|
+
const s2 = segs.find(long) ?? segs[0];
|
|
342
|
+
const t2 = segmentTangent(s2, 0);
|
|
343
|
+
return { p: toVec(segs[0][0]), out: v(-t2[0], -t2[1]) };
|
|
344
|
+
}
|
|
345
|
+
const s = [...segs].reverse().find(long) ?? segs[segs.length - 1];
|
|
346
|
+
const t = segmentTangent(s, 1);
|
|
347
|
+
const last = segs[segs.length - 1];
|
|
348
|
+
return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
|
|
349
|
+
}
|
|
350
|
+
function pointAlong(segs, fromStart, dist3) {
|
|
351
|
+
const order = fromStart ? segs : [...segs].reverse();
|
|
352
|
+
let acc = 0;
|
|
353
|
+
for (const s of order) {
|
|
354
|
+
const l = segLen(s);
|
|
355
|
+
if (acc + l >= dist3) {
|
|
356
|
+
const want = fromStart ? dist3 - acc : l - (dist3 - acc);
|
|
357
|
+
const pts = trimSegments([s], 0, want);
|
|
358
|
+
const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
|
|
359
|
+
return toVec(p);
|
|
360
|
+
}
|
|
361
|
+
acc += l;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
function vectorStrokeParts(path, style) {
|
|
366
|
+
const width = style.strokeWidth ?? 1;
|
|
367
|
+
const contours = pathToContours(path).filter((c) => c.segments.length > 0);
|
|
368
|
+
const closedShape = contours.length > 0 && contours.every((c) => c.closed);
|
|
369
|
+
const align = effectiveStrokeAlign(style, closedShape);
|
|
370
|
+
const drawWidth = align === "center" ? width : width * 2;
|
|
371
|
+
const heads = [];
|
|
372
|
+
if (!contours.length || !style.startArrow && !style.endArrow) return { align, line: path, drawWidth, heads };
|
|
373
|
+
const first = contours[0];
|
|
374
|
+
const last = contours[contours.length - 1];
|
|
375
|
+
const cap = capExtent(style, width);
|
|
376
|
+
const tipMode = style.arrowAlign === "tip";
|
|
377
|
+
const trims = contours.map(() => [0, 0]);
|
|
378
|
+
const place = (end, contour, ci) => {
|
|
379
|
+
const kind = end === "start" ? style.startArrow : style.endArrow;
|
|
380
|
+
if (!kind || contour.closed) return;
|
|
381
|
+
const hw = arrowheadWeight(style, end);
|
|
382
|
+
const inset = arrowheadInset(kind, hw, cap);
|
|
383
|
+
const atStart = end === "start";
|
|
384
|
+
const { p, out } = endOf(contour.segments, atStart);
|
|
385
|
+
let tip;
|
|
386
|
+
let dir;
|
|
387
|
+
if (tipMode) {
|
|
388
|
+
const q = pointAlong(contour.segments, atStart, inset);
|
|
389
|
+
const d = q ? v(p.x - q.x, p.y - q.y) : null;
|
|
390
|
+
dir = d && Math.hypot(d.x, d.y) > 1e-9 ? unit(d) : out;
|
|
391
|
+
tip = p;
|
|
392
|
+
trims[ci][atStart ? 0 : 1] = inset;
|
|
393
|
+
} else {
|
|
394
|
+
tip = plus(p, out, inset);
|
|
395
|
+
dir = out;
|
|
396
|
+
}
|
|
397
|
+
heads.push({ end, kind, tip, dir, inset, path: arrowheadPath(kind, tip, dir, hw) });
|
|
398
|
+
};
|
|
399
|
+
place("start", first, 0);
|
|
400
|
+
place("end", last, contours.length - 1);
|
|
401
|
+
if (!tipMode || !heads.length) return { align, line: path, drawWidth, heads };
|
|
402
|
+
const trimmed = contours.map((c, i) => {
|
|
403
|
+
const [s0, s1] = trims[i];
|
|
404
|
+
if (!s0 && !s1) return c;
|
|
405
|
+
const lengths = c.segments.map(segLen);
|
|
406
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
407
|
+
return { closed: false, segments: total - s1 > s0 ? trimSegments(c.segments, s0, total - s1, lengths) : [] };
|
|
408
|
+
});
|
|
409
|
+
return { align, line: contoursToPath(trimmed), drawWidth, heads };
|
|
410
|
+
}
|
|
411
|
+
function vectorStrokeOutlinePath(path, style) {
|
|
412
|
+
const parts = vectorStrokeParts(path, style);
|
|
413
|
+
const stroke = outlineStroke(parts.line, {
|
|
414
|
+
width: parts.drawWidth,
|
|
415
|
+
join: style.strokeLinejoin ?? "miter",
|
|
416
|
+
cap: style.strokeLinecap ?? "butt",
|
|
417
|
+
...style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {}
|
|
418
|
+
});
|
|
419
|
+
const fill = { path, fillRule: style.fillRule ?? "nonzero" };
|
|
420
|
+
let area = stroke;
|
|
421
|
+
if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
|
|
422
|
+
else if (parts.align === "outside") area = pathBoolean("difference", [stroke, fill]).path;
|
|
423
|
+
if (!parts.heads.length) return area;
|
|
424
|
+
return pathBoolean("union", [area, ...parts.heads.map((h) => h.path)]).path;
|
|
425
|
+
}
|
|
426
|
+
function hitTestVectorStroke(path, p, style, tolerance = 0) {
|
|
427
|
+
const parts = vectorStrokeParts(path, style);
|
|
428
|
+
const point = [p.x, p.y];
|
|
429
|
+
const width = style.strokeWidth ?? 1;
|
|
430
|
+
for (const h of parts.heads) {
|
|
431
|
+
if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
|
|
432
|
+
}
|
|
433
|
+
if (!parts.line.length) return false;
|
|
434
|
+
const d = distanceToBezierPath(parts.line, point);
|
|
435
|
+
if (parts.align === "center") return d <= width / 2 + tolerance;
|
|
436
|
+
if (d > width + tolerance) return false;
|
|
437
|
+
if (d <= tolerance) return true;
|
|
438
|
+
const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
|
|
439
|
+
return parts.align === "inside" ? inside : !inside;
|
|
440
|
+
}
|
|
441
|
+
function vectorStrokeReach(style) {
|
|
442
|
+
const width = style.strokeWidth ?? 1;
|
|
443
|
+
const align = style.strokeAlign ?? "center";
|
|
444
|
+
let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
|
|
445
|
+
if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
|
|
446
|
+
for (const end of ["start", "end"]) {
|
|
447
|
+
if (end === "start" ? style.startArrow : style.endArrow) reach = Math.max(reach, 8 * arrowheadWeight(style, end) + width);
|
|
448
|
+
}
|
|
449
|
+
return reach;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/vector/geometry.ts
|
|
453
|
+
var EPS2 = 1e-9;
|
|
28
454
|
var cloneAnchor = (a) => ({
|
|
29
455
|
x: a.x,
|
|
30
456
|
y: a.y,
|
|
31
457
|
...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
|
|
32
458
|
...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
|
|
33
|
-
...a.type && a.type !== "corner" ? { type: a.type } : {}
|
|
459
|
+
...a.type && a.type !== "corner" ? { type: a.type } : {},
|
|
460
|
+
...a.corner ? { corner: { ...a.corner } } : {}
|
|
34
461
|
});
|
|
35
462
|
var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
|
|
36
463
|
function cloneVectorNode(node) {
|
|
@@ -48,7 +475,7 @@ function hasPaint(paint) {
|
|
|
48
475
|
return true;
|
|
49
476
|
}
|
|
50
477
|
var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
|
|
51
|
-
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) <
|
|
478
|
+
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
|
|
52
479
|
function subpathSegmentCount(sp) {
|
|
53
480
|
const n = sp.anchors.length;
|
|
54
481
|
if (n < 2) return 0;
|
|
@@ -62,9 +489,9 @@ function subpathSegment(sp, i) {
|
|
|
62
489
|
const c2 = handleOf(b, "in");
|
|
63
490
|
return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
|
|
64
491
|
}
|
|
65
|
-
function subpathsToBezier(subpaths) {
|
|
492
|
+
function subpathsToBezier(subpaths, options = {}) {
|
|
66
493
|
const out = [];
|
|
67
|
-
for (const sp of subpaths) {
|
|
494
|
+
for (const sp of options.corners === false ? subpaths : expandVectorCorners(subpaths)) {
|
|
68
495
|
if (sp.anchors.length === 0) continue;
|
|
69
496
|
const first = sp.anchors[0];
|
|
70
497
|
out.push({ type: "M", x: first.x, y: first.y });
|
|
@@ -94,9 +521,9 @@ function inferAnchorType(a) {
|
|
|
94
521
|
const oy = a.out.y - a.y;
|
|
95
522
|
const li = Math.hypot(ix, iy);
|
|
96
523
|
const lo = Math.hypot(ox, oy);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
if (Math.abs(
|
|
524
|
+
const cross2 = (ix * oy - iy * ox) / (li * lo);
|
|
525
|
+
const dot2 = (ix * ox + iy * oy) / (li * lo);
|
|
526
|
+
if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
|
|
100
527
|
return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
|
|
101
528
|
}
|
|
102
529
|
function bezierToSubpaths(input) {
|
|
@@ -239,8 +666,12 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
|
239
666
|
if (path.length === 0) return false;
|
|
240
667
|
const point = [p.x, p.y];
|
|
241
668
|
if (hasPaint(node.fill) && pointInBezierPath(path, point, node.fillRule ?? "nonzero")) return true;
|
|
242
|
-
|
|
243
|
-
return distanceToBezierPath(path, point) <=
|
|
669
|
+
if (hasPaint(node.stroke)) return hitTestVectorStroke(path, p, node, tolerance);
|
|
670
|
+
return distanceToBezierPath(path, point) <= tolerance;
|
|
671
|
+
}
|
|
672
|
+
function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
|
|
673
|
+
if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
|
|
674
|
+
return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
|
|
244
675
|
}
|
|
245
676
|
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
246
677
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -258,8 +689,8 @@ function nearestSegment(node, p) {
|
|
|
258
689
|
const seg = subpathSegment(sp, i);
|
|
259
690
|
const hit = segmentNearest(seg, [p.x, p.y]);
|
|
260
691
|
if (!best || hit.distance < best.distance) {
|
|
261
|
-
const
|
|
262
|
-
best = { subpath: s, segment: i, t: hit.t, point: { x:
|
|
692
|
+
const pt2 = segmentPoint(seg, hit.t);
|
|
693
|
+
best = { subpath: s, segment: i, t: hit.t, point: { x: pt2[0], y: pt2[1] }, distance: hit.distance };
|
|
263
694
|
}
|
|
264
695
|
}
|
|
265
696
|
});
|
|
@@ -293,8 +724,8 @@ function invertMatrix(m) {
|
|
|
293
724
|
if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
|
|
294
725
|
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
295
726
|
}
|
|
296
|
-
function applyMatrixLinear(m,
|
|
297
|
-
return { x: m[0] *
|
|
727
|
+
function applyMatrixLinear(m, v2) {
|
|
728
|
+
return { x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y };
|
|
298
729
|
}
|
|
299
730
|
function matrixRotation(m) {
|
|
300
731
|
const [a, b, c, d] = m;
|
|
@@ -328,6 +759,7 @@ function bakeIntoGeometry(node, m) {
|
|
|
328
759
|
if (a.in) next.in = applyMatrix(m, a.in);
|
|
329
760
|
if (a.out) next.out = applyMatrix(m, a.out);
|
|
330
761
|
if (a.type) next.type = a.type;
|
|
762
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
331
763
|
return next;
|
|
332
764
|
})
|
|
333
765
|
}))
|
|
@@ -379,12 +811,12 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
|
|
|
379
811
|
if (breakHandles) delete next.type;
|
|
380
812
|
const dx = pos.x - a.x;
|
|
381
813
|
const dy = pos.y - a.y;
|
|
382
|
-
const
|
|
814
|
+
const len2 = Math.hypot(dx, dy);
|
|
383
815
|
if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
|
|
384
|
-
else if (type === "smooth" &&
|
|
816
|
+
else if (type === "smooth" && len2 > EPS2) {
|
|
385
817
|
const o = a[other];
|
|
386
|
-
const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) :
|
|
387
|
-
next[other] = { x: a.x - dx /
|
|
818
|
+
const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
|
|
819
|
+
next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
|
|
388
820
|
}
|
|
389
821
|
if (isOnAnchor(next, next.in)) delete next.in;
|
|
390
822
|
if (isOnAnchor(next, next.out)) delete next.out;
|
|
@@ -411,15 +843,15 @@ function convertAnchor(sp, index, type) {
|
|
|
411
843
|
else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
|
|
412
844
|
else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
|
|
413
845
|
else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
|
|
414
|
-
if (!dir || Math.hypot(dir.x, dir.y) <
|
|
846
|
+
if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
|
|
415
847
|
const dl = Math.hypot(dir.x, dir.y);
|
|
416
848
|
const ux = dir.x / dl;
|
|
417
849
|
const uy = dir.y / dl;
|
|
418
850
|
lenIn = a.in && !isOnAnchor(a, a.in) ? Math.hypot(a.in.x - a.x, a.in.y - a.y) : prev ? Math.hypot(prev.x - a.x, prev.y - a.y) / 3 : 0;
|
|
419
851
|
lenOut = a.out && !isOnAnchor(a, a.out) ? Math.hypot(a.out.x - a.x, a.out.y - a.y) : next ? Math.hypot(next.x - a.x, next.y - a.y) / 3 : 0;
|
|
420
852
|
if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
|
|
421
|
-
if (lenIn >
|
|
422
|
-
if (lenOut >
|
|
853
|
+
if (lenIn > EPS2) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
|
|
854
|
+
if (lenOut > EPS2) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
|
|
423
855
|
a.type = type;
|
|
424
856
|
return { anchors, closed: sp.closed };
|
|
425
857
|
}
|
|
@@ -455,37 +887,24 @@ function reverseSubpath(sp) {
|
|
|
455
887
|
if (a.out) next.in = { ...a.out };
|
|
456
888
|
if (a.in) next.out = { ...a.in };
|
|
457
889
|
if (a.type) next.type = a.type;
|
|
890
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
458
891
|
return next;
|
|
459
892
|
});
|
|
460
893
|
return { anchors, closed: sp.closed };
|
|
461
894
|
}
|
|
462
|
-
var
|
|
463
|
-
function rectSubpath(b, radius = 0) {
|
|
895
|
+
var KAPPA2 = 0.5522847498307936;
|
|
896
|
+
function rectSubpath(b, radius = 0, kind) {
|
|
464
897
|
const { x, y, width: w, height: h } = b;
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
return { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
|
|
468
|
-
}
|
|
469
|
-
const k = r * KAPPA;
|
|
470
|
-
const anchors = [
|
|
471
|
-
{ x: x + r, y, in: { x: x + r - k, y } },
|
|
472
|
-
{ x: x + w - r, y, out: { x: x + w - r + k, y } },
|
|
473
|
-
{ x: x + w, y: y + r, in: { x: x + w, y: y + r - k } },
|
|
474
|
-
{ x: x + w, y: y + h - r, out: { x: x + w, y: y + h - r + k } },
|
|
475
|
-
{ x: x + w - r, y: y + h, in: { x: x + w - r + k, y: y + h } },
|
|
476
|
-
{ x: x + r, y: y + h, out: { x: x + r - k, y: y + h } },
|
|
477
|
-
{ x, y: y + h - r, in: { x, y: y + h - r + k } },
|
|
478
|
-
{ x, y: y + r, out: { x, y: y + r - k } }
|
|
479
|
-
];
|
|
480
|
-
return { anchors, closed: true };
|
|
898
|
+
const sp = { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
|
|
899
|
+
return radius > 0 ? setSubpathCorners(sp, radius, kind) : sp;
|
|
481
900
|
}
|
|
482
901
|
function ellipseSubpath(b) {
|
|
483
902
|
const rx = b.width / 2;
|
|
484
903
|
const ry = b.height / 2;
|
|
485
904
|
const cx = b.x + rx;
|
|
486
905
|
const cy = b.y + ry;
|
|
487
|
-
const kx = rx *
|
|
488
|
-
const ky = ry *
|
|
906
|
+
const kx = rx * KAPPA2;
|
|
907
|
+
const ky = ry * KAPPA2;
|
|
489
908
|
return {
|
|
490
909
|
closed: true,
|
|
491
910
|
anchors: [
|
|
@@ -533,15 +952,52 @@ function roundSubpaths(subpaths, precision = 3) {
|
|
|
533
952
|
}
|
|
534
953
|
|
|
535
954
|
// src/vector/svg.ts
|
|
955
|
+
function vectorCornersAttribute(subpaths, precision = 3) {
|
|
956
|
+
const out = [];
|
|
957
|
+
subpaths.forEach(
|
|
958
|
+
(sp, s) => sp.anchors.forEach((a, i) => {
|
|
959
|
+
if (!a.corner || !(a.corner.radius > 0)) return;
|
|
960
|
+
out.push(`${s}.${i}:${formatNumber(a.corner.radius, precision)}${a.corner.kind && a.corner.kind !== "round" ? `:${a.corner.kind}` : ""}`);
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
return out.join(" ");
|
|
964
|
+
}
|
|
965
|
+
function applyVectorCornersAttribute(subpaths, value) {
|
|
966
|
+
for (const item of value.trim().split(/\s+/)) {
|
|
967
|
+
const m = /^(\d+)\.(\d+):([\d.eE+-]+)(?::([a-z]+))?$/.exec(item);
|
|
968
|
+
if (!m) continue;
|
|
969
|
+
const a = subpaths[+m[1]]?.anchors[+m[2]];
|
|
970
|
+
const radius = parseFloat(m[3]);
|
|
971
|
+
if (!a || !(radius > 0)) continue;
|
|
972
|
+
const kind = VECTOR_CORNER_KINDS.includes(m[4]) ? m[4] : void 0;
|
|
973
|
+
a.corner = { radius, ...kind && kind !== "round" ? { kind } : {} };
|
|
974
|
+
}
|
|
975
|
+
return subpaths;
|
|
976
|
+
}
|
|
536
977
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
537
978
|
function vectorPaintToSvg(paint, gradientId) {
|
|
538
979
|
if (!hasPaint(paint)) return { value: "none", gradient: null };
|
|
539
980
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
540
981
|
return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
|
|
541
982
|
}
|
|
983
|
+
function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
984
|
+
if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
|
|
985
|
+
const path = subpathsToBezier(node.subpaths);
|
|
986
|
+
const parts = vectorStrokeParts(path, node);
|
|
987
|
+
if (parts.align === "center" && !parts.heads.length) return void 0;
|
|
988
|
+
let clip = null;
|
|
989
|
+
if (parts.align !== "center") {
|
|
990
|
+
const b = bezierPathBounds(path);
|
|
991
|
+
const pad = vectorStrokeReach(node) + 1;
|
|
992
|
+
const bounds = b ? { x: round2(b.minX - pad), y: round2(b.minY - pad), width: round2(b.maxX - b.minX + 2 * pad), height: round2(b.maxY - b.minY + 2 * pad) } : { x: 0, y: 0, width: 0, height: 0 };
|
|
993
|
+
clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
|
|
994
|
+
}
|
|
995
|
+
return { align: parts.align, d: bezierPathToSvg(parts.line, { precision }), width: parts.drawWidth, clip, heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision })) };
|
|
996
|
+
}
|
|
997
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
542
998
|
function gradientMarkup(def) {
|
|
543
999
|
const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
|
|
544
|
-
const attrs = Object.entries(def.attrs).map(([k,
|
|
1000
|
+
const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
|
|
545
1001
|
const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
|
|
546
1002
|
return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
|
|
547
1003
|
}
|
|
@@ -572,15 +1028,53 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
572
1028
|
const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
|
|
573
1029
|
if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
|
|
574
1030
|
if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (node.
|
|
582
|
-
|
|
583
|
-
|
|
1031
|
+
const d = vectorPathData(node, precision);
|
|
1032
|
+
const corners2 = vectorCornersAttribute(node.subpaths, precision);
|
|
1033
|
+
const live = corners2 ? ` data-vector-d="${bezierPathToSvg(subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
|
|
1034
|
+
const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
|
|
1035
|
+
const lineAttrs = (width) => {
|
|
1036
|
+
let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
|
|
1037
|
+
if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
|
|
1038
|
+
if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
|
|
1039
|
+
if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
|
|
1040
|
+
return out2;
|
|
1041
|
+
};
|
|
1042
|
+
const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
|
|
1043
|
+
if (!parts) {
|
|
1044
|
+
lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(node.strokeWidth ?? 1) : ""}${live}/>`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
let data = ` data-vector-path="" data-vector-stroke-width="${num(node.strokeWidth ?? 1)}"`;
|
|
1048
|
+
if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
|
|
1049
|
+
if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
|
|
1050
|
+
if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
|
|
1051
|
+
if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
|
|
1052
|
+
if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
|
|
1053
|
+
if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
|
|
1054
|
+
lines.push(`${indent(depth)}<g${common(node)}${data}>`);
|
|
1055
|
+
lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
|
|
1056
|
+
if (options.strokes === "outline") {
|
|
1057
|
+
const area = bezierPathToSvg(vectorStrokeOutlinePath(subpathsToBezier(node.subpaths), node), { precision });
|
|
1058
|
+
lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${area}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1059
|
+
} else {
|
|
1060
|
+
let clip = "";
|
|
1061
|
+
if (parts.clip) {
|
|
1062
|
+
const rule = node.fillRule === "evenodd" ? "evenodd" : "nonzero";
|
|
1063
|
+
const id = escapeAttr(parts.clip.id);
|
|
1064
|
+
if (parts.clip.kind === "clip") {
|
|
1065
|
+
defs.push(`<clipPath id="${id}"><path d="${d}" clip-rule="${rule}"/></clipPath>`);
|
|
1066
|
+
clip = ` clip-path="url(#${id})"`;
|
|
1067
|
+
} else {
|
|
1068
|
+
const b = parts.clip.bounds;
|
|
1069
|
+
const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
|
|
1070
|
+
defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
|
|
1071
|
+
clip = ` mask="url(#${id})"`;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
|
|
1075
|
+
for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1076
|
+
}
|
|
1077
|
+
lines.push(`${indent(depth)}</g>`);
|
|
584
1078
|
};
|
|
585
1079
|
for (const node of doc.nodes) emit(node, 1);
|
|
586
1080
|
const w = num(doc.width);
|
|
@@ -755,11 +1249,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
755
1249
|
const rawGradients = /* @__PURE__ */ new Map();
|
|
756
1250
|
collectGradients(root, rawGradients);
|
|
757
1251
|
const paint = (value) => {
|
|
758
|
-
const
|
|
759
|
-
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(
|
|
1252
|
+
const v2 = value.trim();
|
|
1253
|
+
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v2);
|
|
760
1254
|
if (url) return resolveGradient(url[1], rawGradients) ?? null;
|
|
761
|
-
if (
|
|
762
|
-
return
|
|
1255
|
+
if (v2 === "none" || v2 === "") return null;
|
|
1256
|
+
return v2;
|
|
763
1257
|
};
|
|
764
1258
|
const viewBox = numberList(root.attrs.viewBox);
|
|
765
1259
|
const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
|
|
@@ -769,8 +1263,13 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
769
1263
|
const shapeOf = (el) => {
|
|
770
1264
|
const a = el.attrs;
|
|
771
1265
|
switch (el.name) {
|
|
772
|
-
case "path":
|
|
1266
|
+
case "path": {
|
|
1267
|
+
if (a["data-vector-d"] && a["data-vector-corners"]) {
|
|
1268
|
+
const sharp = bezierToSubpaths(a["data-vector-d"]);
|
|
1269
|
+
if (sharp.length) return applyVectorCornersAttribute(sharp, a["data-vector-corners"]);
|
|
1270
|
+
}
|
|
773
1271
|
return a.d ? bezierToSubpaths(a.d) : null;
|
|
1272
|
+
}
|
|
774
1273
|
case "rect": {
|
|
775
1274
|
const w = length(a.width);
|
|
776
1275
|
const h = length(a.height);
|
|
@@ -804,6 +1303,56 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
804
1303
|
}
|
|
805
1304
|
return null;
|
|
806
1305
|
};
|
|
1306
|
+
const strokeStyle = (s) => ({
|
|
1307
|
+
...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
|
|
1308
|
+
...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
|
|
1309
|
+
...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
|
|
1310
|
+
...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
|
|
1311
|
+
...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
|
|
1312
|
+
});
|
|
1313
|
+
const placed = (node, m) => {
|
|
1314
|
+
if (m === IDENTITY_MATRIX) return node;
|
|
1315
|
+
const next = transformVectorNode(node, m);
|
|
1316
|
+
const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
1317
|
+
if (hasPaint(next.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) next.strokeWidth = (next.strokeWidth ?? 1) * strokeScale;
|
|
1318
|
+
return next;
|
|
1319
|
+
};
|
|
1320
|
+
const mergedPath = (g, style, m, meta) => {
|
|
1321
|
+
const part = (name) => g.children.find((c) => c.name === "path" && c.attrs["data-vector-part"] === name);
|
|
1322
|
+
const fillEl = part("fill");
|
|
1323
|
+
const subpaths = fillEl ? shapeOf(fillEl) : null;
|
|
1324
|
+
if (!fillEl || !subpaths?.length) return null;
|
|
1325
|
+
const a = g.attrs;
|
|
1326
|
+
const fs = readStyle(fillEl, style);
|
|
1327
|
+
const strokeEl = part("stroke");
|
|
1328
|
+
const outlineEl = part("stroke-outline");
|
|
1329
|
+
const ss = strokeEl ? readStyle(strokeEl, style) : null;
|
|
1330
|
+
const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
|
|
1331
|
+
const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
|
|
1332
|
+
const scale = (v2) => {
|
|
1333
|
+
const n = v2 === void 0 ? NaN : parseFloat(v2);
|
|
1334
|
+
return n > 0 ? n : void 0;
|
|
1335
|
+
};
|
|
1336
|
+
const align = a["data-vector-stroke-align"];
|
|
1337
|
+
const node = {
|
|
1338
|
+
id: idFor(g),
|
|
1339
|
+
type: "path",
|
|
1340
|
+
...meta,
|
|
1341
|
+
subpaths,
|
|
1342
|
+
fill: paint(fs.fill),
|
|
1343
|
+
stroke: ss ? paint(ss.stroke) : outlineEl ? paint(readStyle(outlineEl, style).fill) : null,
|
|
1344
|
+
...ss ? strokeStyle(ss) : {},
|
|
1345
|
+
...fs.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
|
|
1346
|
+
strokeWidth: width2,
|
|
1347
|
+
...align === "inside" || align === "outside" ? { strokeAlign: align } : {},
|
|
1348
|
+
...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
|
|
1349
|
+
...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
|
|
1350
|
+
...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
|
|
1351
|
+
...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
|
|
1352
|
+
...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {}
|
|
1353
|
+
};
|
|
1354
|
+
return placed(node, m);
|
|
1355
|
+
};
|
|
807
1356
|
const visit = (el, style, matrix) => {
|
|
808
1357
|
const out = [];
|
|
809
1358
|
for (const child of el.children) {
|
|
@@ -817,6 +1366,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
817
1366
|
...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
|
|
818
1367
|
...hidden ? { visible: false } : {}
|
|
819
1368
|
};
|
|
1369
|
+
if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
|
|
1370
|
+
const node2 = mergedPath(child, s, m, meta);
|
|
1371
|
+
if (node2) out.push(node2);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
820
1374
|
if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
|
|
821
1375
|
const children = visit(child, s, m);
|
|
822
1376
|
if (!children.length) continue;
|
|
@@ -826,24 +1380,18 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
826
1380
|
}
|
|
827
1381
|
const subpaths = shapeOf(child);
|
|
828
1382
|
if (!subpaths || !subpaths.length) continue;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
};
|
|
842
|
-
if (m !== IDENTITY_MATRIX) {
|
|
843
|
-
node = transformVectorNode(node, m);
|
|
844
|
-
const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
845
|
-
if (hasPaint(node.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) node.strokeWidth = (node.strokeWidth ?? 1) * strokeScale;
|
|
846
|
-
}
|
|
1383
|
+
const node = placed(
|
|
1384
|
+
{
|
|
1385
|
+
id: idFor(child),
|
|
1386
|
+
type: "path",
|
|
1387
|
+
...meta,
|
|
1388
|
+
subpaths,
|
|
1389
|
+
fill: child.name === "line" ? null : paint(s.fill),
|
|
1390
|
+
stroke: paint(s.stroke),
|
|
1391
|
+
...strokeStyle(s)
|
|
1392
|
+
},
|
|
1393
|
+
m
|
|
1394
|
+
);
|
|
847
1395
|
out.push(node);
|
|
848
1396
|
}
|
|
849
1397
|
return out;
|
|
@@ -866,9 +1414,9 @@ function constrainToLeave(leave, p) {
|
|
|
866
1414
|
return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
|
|
867
1415
|
}
|
|
868
1416
|
default: {
|
|
869
|
-
const
|
|
1417
|
+
const len2 = Math.hypot(dx, dy);
|
|
870
1418
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
871
|
-
return { x: leave.at.x + Math.cos(a) *
|
|
1419
|
+
return { x: leave.at.x + Math.cos(a) * len2, y: leave.at.y + Math.sin(a) * len2 };
|
|
872
1420
|
}
|
|
873
1421
|
}
|
|
874
1422
|
}
|
|
@@ -877,24 +1425,24 @@ var CROSSING_PAIRS = 32;
|
|
|
877
1425
|
var CURVE_SAMPLES = 48;
|
|
878
1426
|
var BISECT = 50;
|
|
879
1427
|
var EQUAL = 1e-6;
|
|
880
|
-
function bound(sorted, key,
|
|
1428
|
+
function bound(sorted, key, v2, strict) {
|
|
881
1429
|
let lo = 0;
|
|
882
1430
|
let hi = sorted.length;
|
|
883
1431
|
while (lo < hi) {
|
|
884
1432
|
const mid = lo + hi >>> 1;
|
|
885
1433
|
const k = key(sorted[mid]);
|
|
886
|
-
if (strict ? k <=
|
|
1434
|
+
if (strict ? k <= v2 : k < v2) lo = mid + 1;
|
|
887
1435
|
else hi = mid;
|
|
888
1436
|
}
|
|
889
1437
|
return lo;
|
|
890
1438
|
}
|
|
891
|
-
function within(sorted, key,
|
|
892
|
-
return sorted.slice(bound(sorted, key,
|
|
1439
|
+
function within(sorted, key, v2, tol) {
|
|
1440
|
+
return sorted.slice(bound(sorted, key, v2 - tol, false), bound(sorted, key, v2 + tol, true));
|
|
893
1441
|
}
|
|
894
|
-
var linesNear = (lines,
|
|
1442
|
+
var linesNear = (lines, v2, tol) => within(lines, (l) => l.v, v2, tol);
|
|
895
1443
|
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
896
1444
|
var toPoint = (p) => [p.x, p.y];
|
|
897
|
-
var
|
|
1445
|
+
var toVec2 = (p) => ({ x: p[0], y: p[1] });
|
|
898
1446
|
var LABELS = {
|
|
899
1447
|
anchor: "anchor",
|
|
900
1448
|
centre: "centre",
|
|
@@ -1012,7 +1560,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1012
1560
|
const count = subpathSegmentCount(sp);
|
|
1013
1561
|
for (let i = 0; i < count && budget > 0; i++) {
|
|
1014
1562
|
if (moving(i) || moving((i + 1) % n)) continue;
|
|
1015
|
-
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m,
|
|
1563
|
+
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec2(q))));
|
|
1016
1564
|
let minX = Infinity;
|
|
1017
1565
|
let minY = Infinity;
|
|
1018
1566
|
let maxX = -Infinity;
|
|
@@ -1029,13 +1577,13 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1029
1577
|
return budget;
|
|
1030
1578
|
}
|
|
1031
1579
|
pushRect(r, kind, centre) {
|
|
1032
|
-
const
|
|
1580
|
+
const corners2 = [
|
|
1033
1581
|
{ x: r.x, y: r.y },
|
|
1034
1582
|
{ x: r.x + r.width, y: r.y },
|
|
1035
1583
|
{ x: r.x + r.width, y: r.y + r.height },
|
|
1036
1584
|
{ x: r.x, y: r.y + r.height }
|
|
1037
1585
|
];
|
|
1038
|
-
for (const p of
|
|
1586
|
+
for (const p of corners2) {
|
|
1039
1587
|
this.xs.push({ v: p.x, from: p, kind });
|
|
1040
1588
|
this.ys.push({ v: p.y, from: p, kind });
|
|
1041
1589
|
}
|
|
@@ -1136,14 +1684,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1136
1684
|
}
|
|
1137
1685
|
const point = this.nearestPoint(p, tol);
|
|
1138
1686
|
if (point) return at(point.p, point.kind);
|
|
1139
|
-
const
|
|
1140
|
-
const crossing = this.crossing(
|
|
1687
|
+
const near2 = this.segmentsNear(p, tol);
|
|
1688
|
+
const crossing = this.crossing(near2, p, tol);
|
|
1141
1689
|
if (crossing) return at(crossing, "intersect");
|
|
1142
1690
|
if (leave && leave.curves !== false) {
|
|
1143
|
-
const foot = this.curvePoint(
|
|
1691
|
+
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1144
1692
|
if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1145
1693
|
}
|
|
1146
|
-
const on = this.onSegment(
|
|
1694
|
+
const on = this.onSegment(near2);
|
|
1147
1695
|
if (on) return at(on, "path");
|
|
1148
1696
|
if (leave?.angles?.length) {
|
|
1149
1697
|
const c = construction(leave, p, tol);
|
|
@@ -1156,9 +1704,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1156
1704
|
return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
|
|
1157
1705
|
}
|
|
1158
1706
|
}
|
|
1159
|
-
const nearest = (lines2,
|
|
1707
|
+
const nearest = (lines2, v2) => {
|
|
1160
1708
|
let best = null;
|
|
1161
|
-
for (const l of linesNear(lines2,
|
|
1709
|
+
for (const l of linesNear(lines2, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
|
|
1162
1710
|
return best;
|
|
1163
1711
|
};
|
|
1164
1712
|
const bx = nearest(this.xs, p.x);
|
|
@@ -1218,26 +1766,26 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1218
1766
|
/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
|
|
1219
1767
|
segmentsNear(p, tol) {
|
|
1220
1768
|
const out = [];
|
|
1221
|
-
const
|
|
1769
|
+
const pt2 = toPoint(p);
|
|
1222
1770
|
for (const s of this.segments) {
|
|
1223
1771
|
if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
|
|
1224
|
-
const hit = segmentNearest(s.seg,
|
|
1772
|
+
const hit = segmentNearest(s.seg, pt2);
|
|
1225
1773
|
if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
|
|
1226
1774
|
}
|
|
1227
1775
|
return out.sort((a, b) => a.distance - b.distance);
|
|
1228
1776
|
}
|
|
1229
|
-
onSegment(
|
|
1230
|
-
return
|
|
1777
|
+
onSegment(near2) {
|
|
1778
|
+
return near2.length ? toVec2(segmentPoint(near2[0].seg, near2[0].t)) : null;
|
|
1231
1779
|
}
|
|
1232
1780
|
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1233
|
-
crossing(
|
|
1234
|
-
const list =
|
|
1781
|
+
crossing(near2, p, tol) {
|
|
1782
|
+
const list = near2.slice(0, CROSSING_PAIRS);
|
|
1235
1783
|
let best = null;
|
|
1236
1784
|
let bestD = Infinity;
|
|
1237
1785
|
for (let i = 0; i < list.length; i++) {
|
|
1238
1786
|
for (let j = i + 1; j < list.length; j++) {
|
|
1239
1787
|
for (const hit of segmentIntersections(list[i].seg, list[j].seg)) {
|
|
1240
|
-
const q =
|
|
1788
|
+
const q = toVec2(hit.point);
|
|
1241
1789
|
const d = distance(q, p);
|
|
1242
1790
|
if (d <= tol && d < bestD) {
|
|
1243
1791
|
best = q;
|
|
@@ -1253,9 +1801,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1253
1801
|
* (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
|
|
1254
1802
|
* bracketed by sign changes of samples and narrowed by bisection.
|
|
1255
1803
|
*/
|
|
1256
|
-
curvePoint(
|
|
1804
|
+
curvePoint(near2, base, p, tol) {
|
|
1257
1805
|
let best = null;
|
|
1258
|
-
for (const { seg } of
|
|
1806
|
+
for (const { seg } of near2) {
|
|
1259
1807
|
for (const kind of ["perpendicular", "tangent"]) {
|
|
1260
1808
|
if (kind === "tangent" && seg.length === 2) continue;
|
|
1261
1809
|
for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
|
|
@@ -1278,7 +1826,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1278
1826
|
const axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };
|
|
1279
1827
|
const nearest = (lines, values) => {
|
|
1280
1828
|
let best = null;
|
|
1281
|
-
for (const
|
|
1829
|
+
for (const v2 of values) for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.d)) best = { d: l.v - v2, from: l.from };
|
|
1282
1830
|
return best;
|
|
1283
1831
|
};
|
|
1284
1832
|
const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
|
|
@@ -1321,7 +1869,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1321
1869
|
evenSpacing(r, horizontal, tol) {
|
|
1322
1870
|
const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
|
|
1323
1871
|
const [r0, r1] = span(r, horizontal);
|
|
1324
|
-
const
|
|
1872
|
+
const len2 = r1 - r0;
|
|
1325
1873
|
const [c0, c1] = span(r, !horizontal);
|
|
1326
1874
|
const row = this.boxes.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] })).filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol)).sort((a, b) => a.a0 - b.a0);
|
|
1327
1875
|
if (!row.length) return null;
|
|
@@ -1341,17 +1889,17 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1341
1889
|
if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
|
|
1342
1890
|
}
|
|
1343
1891
|
const starts = [];
|
|
1344
|
-
if (before && after && after.a0 - before.a1 >
|
|
1892
|
+
if (before && after && after.a0 - before.a1 > len2) starts.push({ start: (before.a1 + after.a0 - len2) / 2, repeats: null });
|
|
1345
1893
|
for (const { g } of pairs) {
|
|
1346
1894
|
if (before) starts.push({ start: before.a1 + g, repeats: g });
|
|
1347
|
-
if (after) starts.push({ start: after.a0 - g -
|
|
1895
|
+
if (after) starts.push({ start: after.a0 - g - len2, repeats: g });
|
|
1348
1896
|
}
|
|
1349
1897
|
let pick = null;
|
|
1350
1898
|
for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
|
|
1351
1899
|
if (!pick) return null;
|
|
1352
1900
|
const { start, repeats } = pick;
|
|
1353
1901
|
const at = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
1354
|
-
const end = start +
|
|
1902
|
+
const end = start + len2;
|
|
1355
1903
|
const gaps = [];
|
|
1356
1904
|
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
|
|
1357
1905
|
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
|
|
@@ -1362,9 +1910,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1362
1910
|
}
|
|
1363
1911
|
/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
|
|
1364
1912
|
gapMarks([u0, u1, c], horizontal, tick, out) {
|
|
1365
|
-
const
|
|
1366
|
-
out.lines.push(line("spacing",
|
|
1367
|
-
out.labels.push(this.label(
|
|
1913
|
+
const pt2 = (u, v2) => horizontal ? { x: u, y: v2 } : { x: v2, y: u };
|
|
1914
|
+
out.lines.push(line("spacing", pt2(u0, c), pt2(u1, c)), line("spacing", pt2(u0, c - tick), pt2(u0, c + tick)), line("spacing", pt2(u1, c - tick), pt2(u1, c + tick)));
|
|
1915
|
+
out.labels.push(this.label(pt2((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
|
|
1368
1916
|
}
|
|
1369
1917
|
/** The other objects' side length nearest `length`, within `tol`, or null. */
|
|
1370
1918
|
matchSize(length2, tol) {
|
|
@@ -1391,9 +1939,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1391
1939
|
const hp = handlePos(handle, frame);
|
|
1392
1940
|
const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
|
|
1393
1941
|
const valid = (n, s) => Number.isFinite(n) && n * s > 0;
|
|
1394
|
-
const nearest = (
|
|
1942
|
+
const nearest = (v2, lines) => {
|
|
1395
1943
|
let best = null;
|
|
1396
|
-
for (const l of linesNear(lines,
|
|
1944
|
+
for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
|
|
1397
1945
|
return best;
|
|
1398
1946
|
};
|
|
1399
1947
|
let nx = sx;
|
|
@@ -1444,9 +1992,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1444
1992
|
};
|
|
1445
1993
|
for (const h of [handle, ...ends]) {
|
|
1446
1994
|
const hv = handlePos(h, frame);
|
|
1447
|
-
const
|
|
1448
|
-
const base = applyMatrix(toDoc, { x: origin.x + c.x *
|
|
1449
|
-
const dir = applyMatrix(toDoc, { x: e.x *
|
|
1995
|
+
const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
|
|
1996
|
+
const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
|
|
1997
|
+
const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
|
|
1450
1998
|
const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
|
|
1451
1999
|
const l2 = dir.x * dir.x + dir.y * dir.y;
|
|
1452
2000
|
const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
|
|
@@ -1479,16 +2027,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1479
2027
|
const sized = [];
|
|
1480
2028
|
if (!proportional) {
|
|
1481
2029
|
if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
|
|
1482
|
-
const
|
|
1483
|
-
if (
|
|
1484
|
-
out.sx = Math.sign(out.sx || 1) * (
|
|
2030
|
+
const len2 = this.matchSize(Math.abs(out.sx) * frame.width, tol);
|
|
2031
|
+
if (len2 !== null && len2 > 0) {
|
|
2032
|
+
out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
|
|
1485
2033
|
sized.push("x");
|
|
1486
2034
|
}
|
|
1487
2035
|
}
|
|
1488
2036
|
if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
out.sy = Math.sign(out.sy || 1) * (
|
|
2037
|
+
const len2 = this.matchSize(Math.abs(out.sy) * frame.height, tol);
|
|
2038
|
+
if (len2 !== null && len2 > 0) {
|
|
2039
|
+
out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
|
|
1492
2040
|
sized.push("y");
|
|
1493
2041
|
}
|
|
1494
2042
|
}
|
|
@@ -1539,14 +2087,14 @@ function line(kind, a, b) {
|
|
|
1539
2087
|
return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
|
|
1540
2088
|
}
|
|
1541
2089
|
function construction(leave, p, tol) {
|
|
1542
|
-
const
|
|
1543
|
-
if (Math.hypot(
|
|
2090
|
+
const v2 = { x: p.x - leave.at.x, y: p.y - leave.at.y };
|
|
2091
|
+
if (Math.hypot(v2.x, v2.y) <= tol) return null;
|
|
1544
2092
|
let best = null;
|
|
1545
2093
|
for (const a of leave.angles ?? []) {
|
|
1546
2094
|
const r = a * Math.PI / 180;
|
|
1547
2095
|
const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
|
|
1548
2096
|
const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
|
|
1549
|
-
const k =
|
|
2097
|
+
const k = v2.x * dir.x + v2.y * dir.y;
|
|
1550
2098
|
const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
|
|
1551
2099
|
const d = distance(point, p);
|
|
1552
2100
|
if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
|
|
@@ -1571,7 +2119,7 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1571
2119
|
vals.push(cond(r, d));
|
|
1572
2120
|
scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
|
|
1573
2121
|
}
|
|
1574
|
-
if (!(scale > 0) || vals.every((
|
|
2122
|
+
if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
|
|
1575
2123
|
const out = [];
|
|
1576
2124
|
for (let i = 0; i < CURVE_SAMPLES; i++) {
|
|
1577
2125
|
const ga = vals[i];
|
|
@@ -1603,136 +2151,1136 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1603
2151
|
return out;
|
|
1604
2152
|
}
|
|
1605
2153
|
|
|
1606
|
-
// src/vector/
|
|
1607
|
-
var
|
|
1608
|
-
var
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
stroke: "#18181b",
|
|
1612
|
-
strokeWidth: 2,
|
|
1613
|
-
polygonSides: 6,
|
|
1614
|
-
starPoints: 5,
|
|
1615
|
-
starInnerRatio: 0.5,
|
|
1616
|
-
cornerRadius: 0
|
|
2154
|
+
// src/vector/pathEdit.ts
|
|
2155
|
+
var EPS3 = 1e-9;
|
|
2156
|
+
var unit2 = (x, y) => {
|
|
2157
|
+
const l = Math.hypot(x, y);
|
|
2158
|
+
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1617
2159
|
};
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
const node = findVectorNode(state.document.nodes, id);
|
|
1630
|
-
if (node && !node.locked) {
|
|
1631
|
-
for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
2160
|
+
function cubicOf(sp, i) {
|
|
2161
|
+
const s = subpathSegment(sp, i);
|
|
2162
|
+
if (s.length === 4) return s;
|
|
2163
|
+
const [a, b] = s;
|
|
2164
|
+
return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
|
|
2165
|
+
}
|
|
2166
|
+
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
2167
|
+
function startTangent(c) {
|
|
2168
|
+
for (let k = 1; k < 4; k++) {
|
|
2169
|
+
const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
2170
|
+
if (u) return u;
|
|
1634
2171
|
}
|
|
1635
|
-
return [
|
|
2172
|
+
return [1, 0];
|
|
1636
2173
|
}
|
|
1637
|
-
function
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
node.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));
|
|
2174
|
+
function endTangent(c) {
|
|
2175
|
+
for (let k = 2; k >= 0; k--) {
|
|
2176
|
+
const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
2177
|
+
if (u) return u;
|
|
1642
2178
|
}
|
|
1643
|
-
return
|
|
2179
|
+
return [1, 0];
|
|
1644
2180
|
}
|
|
1645
|
-
function
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2181
|
+
function sample(c, n, out, withStart) {
|
|
2182
|
+
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
|
|
2183
|
+
}
|
|
2184
|
+
var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
|
|
2185
|
+
function tidy(a, keepType = true) {
|
|
2186
|
+
const next = cloneAnchor(a);
|
|
2187
|
+
if (onAnchor2(next, next.in)) delete next.in;
|
|
2188
|
+
if (onAnchor2(next, next.out)) delete next.out;
|
|
2189
|
+
const shown = inferAnchorType(next);
|
|
2190
|
+
const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
|
|
2191
|
+
if (type === "corner") delete next.type;
|
|
2192
|
+
else next.type = type;
|
|
2193
|
+
return next;
|
|
2194
|
+
}
|
|
2195
|
+
function neighbours(sp, i) {
|
|
2196
|
+
const n = sp.anchors.length;
|
|
2197
|
+
const wrap = sp.closed && n > 1;
|
|
2198
|
+
return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
|
|
2199
|
+
}
|
|
2200
|
+
function goldenMin(f, a, b) {
|
|
2201
|
+
const R = 0.618033988749895;
|
|
2202
|
+
let x1 = b - R * (b - a);
|
|
2203
|
+
let x2 = a + R * (b - a);
|
|
2204
|
+
let f1 = f(x1);
|
|
2205
|
+
let f2 = f(x2);
|
|
2206
|
+
for (let i = 0; i < 40; i++) {
|
|
2207
|
+
if (f1.error < f2.error || Number.isNaN(f2.error)) {
|
|
2208
|
+
b = x2;
|
|
2209
|
+
x2 = x1;
|
|
2210
|
+
f2 = f1;
|
|
2211
|
+
x1 = b - R * (b - a);
|
|
2212
|
+
f1 = f(x1);
|
|
2213
|
+
} else {
|
|
2214
|
+
a = x1;
|
|
2215
|
+
x1 = x2;
|
|
2216
|
+
f1 = f2;
|
|
2217
|
+
x2 = a + R * (b - a);
|
|
2218
|
+
f2 = f(x2);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return f1.error <= f2.error ? f1 : f2;
|
|
2222
|
+
}
|
|
2223
|
+
function fitAcross(pts, N, t0, t1) {
|
|
2224
|
+
const fit = (k) => {
|
|
2225
|
+
const u = [];
|
|
2226
|
+
for (let i = 0; i <= N; i++) u.push(k * i / N);
|
|
2227
|
+
for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
|
|
2228
|
+
return fitCubicWithTangents(pts, u, t0, t1);
|
|
1656
2229
|
};
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2230
|
+
return goldenMin(fit, 0, 1);
|
|
2231
|
+
}
|
|
2232
|
+
function removeAnchorKeepingShape(sp, index) {
|
|
2233
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
2234
|
+
const n = anchors.length;
|
|
2235
|
+
if (index < 0 || index >= n) return { anchors, closed: sp.closed };
|
|
2236
|
+
if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
|
|
2237
|
+
const pi = (index + n - 1) % n;
|
|
2238
|
+
const ni = (index + 1) % n;
|
|
2239
|
+
if (!(isLine(sp, pi) && isLine(sp, index))) {
|
|
2240
|
+
const left = cubicOf(sp, pi);
|
|
2241
|
+
const right = cubicOf(sp, index);
|
|
2242
|
+
const N = 16;
|
|
2243
|
+
const pts = [];
|
|
2244
|
+
sample(left, N, pts, true);
|
|
2245
|
+
sample(right, N, pts, false);
|
|
2246
|
+
const t0 = startTangent(left);
|
|
2247
|
+
const t1 = endTangent(right);
|
|
2248
|
+
const { curve } = fitAcross(pts, N, t0, t1);
|
|
2249
|
+
if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
|
|
2250
|
+
anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
|
|
2251
|
+
anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
|
|
2252
|
+
}
|
|
1664
2253
|
}
|
|
1665
|
-
return out;
|
|
1666
2254
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
if (!sp) continue;
|
|
1674
|
-
const n = sp.anchors.length;
|
|
1675
|
-
push(node, ref.subpath, ref.index, "in");
|
|
1676
|
-
push(node, ref.subpath, ref.index, "out");
|
|
1677
|
-
const prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;
|
|
1678
|
-
const next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;
|
|
1679
|
-
if (prev >= 0) push(node, ref.subpath, prev, "out");
|
|
1680
|
-
if (next >= 0) push(node, ref.subpath, next, "in");
|
|
2255
|
+
anchors.splice(index, 1);
|
|
2256
|
+
let closed = sp.closed;
|
|
2257
|
+
if (!closed && anchors.length) {
|
|
2258
|
+
anchors[0] = tidy({ ...anchors[0], in: null });
|
|
2259
|
+
const last = anchors.length - 1;
|
|
2260
|
+
anchors[last] = tidy({ ...anchors[last], out: null });
|
|
1681
2261
|
}
|
|
1682
|
-
|
|
2262
|
+
if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
|
|
2263
|
+
return { anchors, closed };
|
|
1683
2264
|
}
|
|
1684
|
-
function
|
|
1685
|
-
const
|
|
1686
|
-
return
|
|
2265
|
+
function removeAnchorsKeepingShape(sp, indices) {
|
|
2266
|
+
const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
|
|
2267
|
+
return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
|
|
1687
2268
|
}
|
|
1688
|
-
function
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
const
|
|
1693
|
-
|
|
2269
|
+
function bendSegment(sp, segment, t, p) {
|
|
2270
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
2271
|
+
const n = anchors.length;
|
|
2272
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
|
|
2273
|
+
const tc = Math.min(0.95, Math.max(0.05, t));
|
|
2274
|
+
const ia = segment;
|
|
2275
|
+
const ib = (segment + 1) % n;
|
|
2276
|
+
const a = anchors[ia];
|
|
2277
|
+
const b = anchors[ib];
|
|
2278
|
+
const k = 3 * tc * (1 - tc);
|
|
2279
|
+
const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
|
|
2280
|
+
const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
|
|
2281
|
+
const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
|
|
2282
|
+
const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
|
|
2283
|
+
anchors[ia] = moveAnchorHandle(a, "out", c1);
|
|
2284
|
+
anchors[ib] = moveAnchorHandle(b, "in", c2);
|
|
2285
|
+
return { anchors, closed: sp.closed };
|
|
1694
2286
|
}
|
|
1695
|
-
function
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
|
|
1699
|
-
const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
|
|
1700
|
-
const angle = same ? angles[0] : 0;
|
|
1701
|
-
const unrotate = rotateMatrix(-angle);
|
|
1702
|
-
const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
|
|
1703
|
-
return b ? { ...b, angle } : null;
|
|
2287
|
+
function subpathPointAt(sp, segment, t) {
|
|
2288
|
+
const [x, y] = segmentPoint(subpathSegment(sp, segment), t);
|
|
2289
|
+
return { x, y };
|
|
1704
2290
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const
|
|
1708
|
-
return
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
{ handle: "e", x: x + w, y: y + h / 2 },
|
|
1713
|
-
{ handle: "se", x: x + w, y: y + h },
|
|
1714
|
-
{ handle: "s", x: x + w / 2, y: y + h },
|
|
1715
|
-
{ handle: "sw", x, y: y + h },
|
|
1716
|
-
{ handle: "w", x, y: y + h / 2 },
|
|
1717
|
-
{ handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
|
|
2291
|
+
function curvatureMoveAnchor(sp, index, q) {
|
|
2292
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
2293
|
+
const a = sp.anchors[index];
|
|
2294
|
+
if (!a) return { anchors, closed: sp.closed };
|
|
2295
|
+
const own = [
|
|
2296
|
+
a.in && !onAnchor2(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
|
|
2297
|
+
a.out && !onAnchor2(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
|
|
1718
2298
|
];
|
|
2299
|
+
const ends = [null, null];
|
|
2300
|
+
neighbours(sp, index).forEach((j, side) => {
|
|
2301
|
+
if (j < 0 || j === index) return;
|
|
2302
|
+
const b = anchors[j];
|
|
2303
|
+
const old = Math.hypot(a.x - b.x, a.y - b.y);
|
|
2304
|
+
const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
|
|
2305
|
+
const key = side === 0 ? "out" : "in";
|
|
2306
|
+
const h = b[key];
|
|
2307
|
+
if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
|
|
2308
|
+
const o = own[side];
|
|
2309
|
+
if (o) own[side] = { x: o.x * s, y: o.y * s };
|
|
2310
|
+
ends[side] = { x: b.x, y: b.y };
|
|
2311
|
+
});
|
|
2312
|
+
const moved = { x: q.x, y: q.y };
|
|
2313
|
+
if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
|
|
2314
|
+
if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
|
|
2315
|
+
if (a.type) moved.type = a.type;
|
|
2316
|
+
if (a.corner) moved.corner = { ...a.corner };
|
|
2317
|
+
const [prev, next] = ends;
|
|
2318
|
+
if (a.type && a.type !== "corner" && prev && next) {
|
|
2319
|
+
const dx = next.x - prev.x;
|
|
2320
|
+
const dy = next.y - prev.y;
|
|
2321
|
+
const l = Math.hypot(dx, dy);
|
|
2322
|
+
if (l > 1e-9) {
|
|
2323
|
+
const len2 = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
|
|
2324
|
+
let li = len2(own[0], prev);
|
|
2325
|
+
let lo = len2(own[1], next);
|
|
2326
|
+
if (a.type === "symmetric") li = lo = (li + lo) / 2;
|
|
2327
|
+
moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
|
|
2328
|
+
moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
anchors[index] = moved;
|
|
2332
|
+
return { anchors, closed: sp.closed };
|
|
1719
2333
|
}
|
|
1720
|
-
function
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
const
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2334
|
+
function cutSubpathAt(sp, at) {
|
|
2335
|
+
const n = sp.anchors.length;
|
|
2336
|
+
const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
|
|
2337
|
+
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
2338
|
+
const first = cuts[0];
|
|
2339
|
+
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
2340
|
+
const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
|
|
2341
|
+
const pieces = [];
|
|
2342
|
+
let cur = [];
|
|
2343
|
+
run.forEach((a, i) => {
|
|
2344
|
+
if (!cutSet.has(i)) {
|
|
2345
|
+
cur.push(cloneAnchor(a));
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (cur.length) {
|
|
2349
|
+
cur.push(tidy({ ...a, out: null }, false));
|
|
2350
|
+
pieces.push({ anchors: cur, closed: false });
|
|
2351
|
+
cur = [];
|
|
2352
|
+
}
|
|
2353
|
+
if (i < n) cur.push(tidy({ ...a, in: null }, false));
|
|
2354
|
+
});
|
|
2355
|
+
if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
|
|
2356
|
+
return pieces;
|
|
1734
2357
|
}
|
|
1735
|
-
|
|
2358
|
+
function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
2359
|
+
const n = sp.anchors.length;
|
|
2360
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
2361
|
+
if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
|
|
2362
|
+
if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
|
|
2363
|
+
const { subpath, index } = insertAnchor(sp, segment, t);
|
|
2364
|
+
return cutSubpathAt(subpath, [index]);
|
|
2365
|
+
}
|
|
2366
|
+
var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
2367
|
+
var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
2368
|
+
function reverse(sp) {
|
|
2369
|
+
return {
|
|
2370
|
+
closed: sp.closed,
|
|
2371
|
+
anchors: sp.anchors.slice().reverse().map((a) => {
|
|
2372
|
+
const next = { x: a.x, y: a.y };
|
|
2373
|
+
if (a.out) next.in = { ...a.out };
|
|
2374
|
+
if (a.in) next.out = { ...a.in };
|
|
2375
|
+
if (a.type) next.type = a.type;
|
|
2376
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
2377
|
+
return next;
|
|
2378
|
+
})
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
function joinSubpaths(subpaths, tolerance) {
|
|
2382
|
+
const closed = [];
|
|
2383
|
+
let open = [];
|
|
2384
|
+
for (const sp of subpaths) {
|
|
2385
|
+
const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
2386
|
+
if (sp.closed || sp.anchors.length < 2) {
|
|
2387
|
+
if (sp.anchors.length) closed.push(copy);
|
|
2388
|
+
} else open.push(copy);
|
|
2389
|
+
}
|
|
2390
|
+
if (open.length === 1) {
|
|
2391
|
+
const sp = open[0];
|
|
2392
|
+
const first = sp.anchors[0];
|
|
2393
|
+
const last = sp.anchors[sp.anchors.length - 1];
|
|
2394
|
+
if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
|
|
2395
|
+
sp.anchors.pop();
|
|
2396
|
+
sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
|
|
2397
|
+
} else {
|
|
2398
|
+
sp.anchors[0] = tidy({ ...first, in: null }, false);
|
|
2399
|
+
sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
|
|
2400
|
+
}
|
|
2401
|
+
sp.closed = true;
|
|
2402
|
+
return [...closed, sp];
|
|
2403
|
+
}
|
|
2404
|
+
while (open.length > 1) {
|
|
2405
|
+
let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
|
|
2406
|
+
for (let i = 0; i < open.length; i++) {
|
|
2407
|
+
for (let j = i + 1; j < open.length; j++) {
|
|
2408
|
+
for (const ei of [false, true]) {
|
|
2409
|
+
for (const ej of [false, true]) {
|
|
2410
|
+
const d = dist(endOf2(open[i], ei), endOf2(open[j], ej));
|
|
2411
|
+
if (d < best.d) best = { d, i, j, ei, ej };
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
let a = open[best.i];
|
|
2417
|
+
let b = open[best.j];
|
|
2418
|
+
open = open.filter((_, k) => k !== best.j);
|
|
2419
|
+
if (!best.ei) a = reverse(a);
|
|
2420
|
+
if (best.ej) b = reverse(b);
|
|
2421
|
+
const anchors = a.anchors;
|
|
2422
|
+
if (best.d <= tolerance) {
|
|
2423
|
+
const [head, ...rest] = b.anchors;
|
|
2424
|
+
const l = anchors[anchors.length - 1];
|
|
2425
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: head.out ?? null }, false);
|
|
2426
|
+
anchors.push(...rest);
|
|
2427
|
+
} else {
|
|
2428
|
+
const l = anchors[anchors.length - 1];
|
|
2429
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
|
|
2430
|
+
anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
|
|
2431
|
+
}
|
|
2432
|
+
open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
|
|
2433
|
+
}
|
|
2434
|
+
return [...closed, ...open];
|
|
2435
|
+
}
|
|
2436
|
+
function smoothSubpath(sp, amount = 1, only) {
|
|
2437
|
+
const t = Math.min(1, Math.max(0, amount));
|
|
2438
|
+
const orig = sp.anchors;
|
|
2439
|
+
const n = orig.length;
|
|
2440
|
+
const anchors = orig.map(cloneAnchor);
|
|
2441
|
+
if (n < 3 || t === 0) return { anchors, closed: sp.closed };
|
|
2442
|
+
for (let i = 0; i < n; i++) {
|
|
2443
|
+
if (!sp.closed && (i === 0 || i === n - 1)) continue;
|
|
2444
|
+
if (only && !only.has(i)) continue;
|
|
2445
|
+
const prev = orig[(i + n - 1) % n];
|
|
2446
|
+
const next = orig[(i + 1) % n];
|
|
2447
|
+
const p = orig[i];
|
|
2448
|
+
const dir = unit2(next.x - prev.x, next.y - prev.y);
|
|
2449
|
+
if (!dir) continue;
|
|
2450
|
+
const li = dist(p, prev) / 3;
|
|
2451
|
+
const lo = dist(p, next) / 3;
|
|
2452
|
+
const hin = p.in ?? p;
|
|
2453
|
+
const hout = p.out ?? p;
|
|
2454
|
+
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
2455
|
+
anchors[i] = tidy(
|
|
2456
|
+
{
|
|
2457
|
+
x: p.x,
|
|
2458
|
+
y: p.y,
|
|
2459
|
+
in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
|
|
2460
|
+
out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
|
|
2461
|
+
},
|
|
2462
|
+
false
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
return { anchors, closed: sp.closed };
|
|
2466
|
+
}
|
|
2467
|
+
function segsOf(sp) {
|
|
2468
|
+
const out = [];
|
|
2469
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
2470
|
+
const s = subpathSegment(sp, i);
|
|
2471
|
+
out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
|
|
2472
|
+
}
|
|
2473
|
+
return out;
|
|
2474
|
+
}
|
|
2475
|
+
function segsToSubpath(segs, closed, types) {
|
|
2476
|
+
if (!segs.length) return null;
|
|
2477
|
+
const anchors = [];
|
|
2478
|
+
const at = (p) => ({ x: p[0], y: p[1] });
|
|
2479
|
+
anchors.push(at(segs[0].c[0]));
|
|
2480
|
+
for (const s of segs) {
|
|
2481
|
+
const end = s.c[s.c.length - 1];
|
|
2482
|
+
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
2483
|
+
const next = at(end);
|
|
2484
|
+
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
2485
|
+
anchors.push(next);
|
|
2486
|
+
}
|
|
2487
|
+
if (closed) {
|
|
2488
|
+
const last = anchors.pop();
|
|
2489
|
+
if (last.in) anchors[0].in = last.in;
|
|
2490
|
+
}
|
|
2491
|
+
const typed = anchors.map((a) => {
|
|
2492
|
+
const type = types.get(`${a.x},${a.y}`);
|
|
2493
|
+
return tidy(type ? { ...a, type } : a, !!type);
|
|
2494
|
+
});
|
|
2495
|
+
return { anchors: typed, closed };
|
|
2496
|
+
}
|
|
2497
|
+
var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
|
|
2498
|
+
function mergePair(a, b, tol) {
|
|
2499
|
+
const a0 = a.c[0];
|
|
2500
|
+
const a3 = a.c[a.c.length - 1];
|
|
2501
|
+
const b3 = b.c[b.c.length - 1];
|
|
2502
|
+
if (a.line && b.line) {
|
|
2503
|
+
const cx = b3[0] - a0[0];
|
|
2504
|
+
const cy = b3[1] - a0[1];
|
|
2505
|
+
const len2 = Math.hypot(cx, cy);
|
|
2506
|
+
if (len2 < 1e-12) return null;
|
|
2507
|
+
const mx = a3[0] - a0[0];
|
|
2508
|
+
const my = a3[1] - a0[1];
|
|
2509
|
+
const along = (mx * cx + my * cy) / len2;
|
|
2510
|
+
return Math.abs((mx * cy - my * cx) / len2) <= tol && along >= 0 && along <= len2 ? { c: [a0, b3], line: true } : null;
|
|
2511
|
+
}
|
|
2512
|
+
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
2513
|
+
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
2514
|
+
const ta = endTangent(ca);
|
|
2515
|
+
const tb = startTangent(cb);
|
|
2516
|
+
if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
|
|
2517
|
+
const pts = [];
|
|
2518
|
+
sample(ca, 16, pts, true);
|
|
2519
|
+
sample(cb, 16, pts, false);
|
|
2520
|
+
const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
|
|
2521
|
+
return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
|
|
2522
|
+
}
|
|
2523
|
+
function removeRedundantPoints(sp, tolerance) {
|
|
2524
|
+
const tol = Math.max(tolerance, 1e-9);
|
|
2525
|
+
const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
|
|
2526
|
+
const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
|
|
2527
|
+
if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
2528
|
+
const minSegs = sp.closed ? 2 : 1;
|
|
2529
|
+
let changed = true;
|
|
2530
|
+
while (changed && segs.length > minSegs) {
|
|
2531
|
+
changed = false;
|
|
2532
|
+
const n = segs.length;
|
|
2533
|
+
const joints = sp.closed ? n : n - 1;
|
|
2534
|
+
for (let j = 0; j < joints; j++) {
|
|
2535
|
+
const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
|
|
2536
|
+
const m = mergePair(segs[i0], segs[i1], tol);
|
|
2537
|
+
if (!m) continue;
|
|
2538
|
+
segs[i0] = m;
|
|
2539
|
+
segs.splice(i1, 1);
|
|
2540
|
+
if (sp.closed && i1 === 0) segs.unshift(segs.pop());
|
|
2541
|
+
changed = true;
|
|
2542
|
+
break;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// src/vector/pathCut.ts
|
|
2549
|
+
var loopArea = (l) => contourSignedArea(l.segs);
|
|
2550
|
+
function loopSample(l) {
|
|
2551
|
+
const k = l.chord.findIndex((c) => !c);
|
|
2552
|
+
return segmentPoint(l.segs[k >= 0 ? k : 0], 0.5);
|
|
2553
|
+
}
|
|
2554
|
+
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
2555
|
+
function groupFaces(loops) {
|
|
2556
|
+
const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
2557
|
+
if (!usable.length) return [];
|
|
2558
|
+
const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
2559
|
+
const sign = Math.sign(loopArea(largest));
|
|
2560
|
+
const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
2561
|
+
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
2562
|
+
for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
2563
|
+
const p = loopSample(hole);
|
|
2564
|
+
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
2565
|
+
if (parent) faces.get(parent).loops.push(hole);
|
|
2566
|
+
}
|
|
2567
|
+
return outers.reverse().map((o) => faces.get(o));
|
|
2568
|
+
}
|
|
2569
|
+
function faceToPath(face) {
|
|
2570
|
+
return contoursToPath(face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
2571
|
+
}
|
|
2572
|
+
function facesOf(path, fillRule) {
|
|
2573
|
+
const normalized = normalizeBezierPath(path, fillRule);
|
|
2574
|
+
const loops = pathToContours(normalized).map((c) => {
|
|
2575
|
+
const segs = closeContour(c, true).map((s) => s.slice());
|
|
2576
|
+
return { segs, chord: segs.map(() => false) };
|
|
2577
|
+
});
|
|
2578
|
+
return groupFaces(loops);
|
|
2579
|
+
}
|
|
2580
|
+
function splitIntoFaces(path, fillRule = "nonzero") {
|
|
2581
|
+
return facesOf(path, fillRule).map(faceToPath);
|
|
2582
|
+
}
|
|
2583
|
+
function rootsInUnit(a, b, c, d, scale) {
|
|
2584
|
+
const f = (t) => ((a * t + b) * t + c) * t + d;
|
|
2585
|
+
const tol = 1e-12 * Math.max(1, scale);
|
|
2586
|
+
const cuts = [0];
|
|
2587
|
+
const qa = 3 * a;
|
|
2588
|
+
const qb = 2 * b;
|
|
2589
|
+
if (Math.abs(qa) > 1e-18) {
|
|
2590
|
+
const disc = qb * qb - 4 * qa * c;
|
|
2591
|
+
if (disc >= 0) {
|
|
2592
|
+
const r = Math.sqrt(disc);
|
|
2593
|
+
for (const t of [(-qb - r) / (2 * qa), (-qb + r) / (2 * qa)].sort((x, y) => x - y)) if (t > 0 && t < 1) cuts.push(t);
|
|
2594
|
+
}
|
|
2595
|
+
} else if (Math.abs(qb) > 1e-18) {
|
|
2596
|
+
const t = -c / qb;
|
|
2597
|
+
if (t > 0 && t < 1) cuts.push(t);
|
|
2598
|
+
}
|
|
2599
|
+
cuts.push(1);
|
|
2600
|
+
const roots = [];
|
|
2601
|
+
const push = (t) => {
|
|
2602
|
+
if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
|
|
2603
|
+
};
|
|
2604
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
2605
|
+
let lo = cuts[i];
|
|
2606
|
+
let hi = cuts[i + 1];
|
|
2607
|
+
let flo = f(lo);
|
|
2608
|
+
const fhi = f(hi);
|
|
2609
|
+
if (Math.abs(flo) <= tol) push(lo);
|
|
2610
|
+
if (Math.abs(fhi) <= tol) push(hi);
|
|
2611
|
+
if (flo * fhi >= 0) continue;
|
|
2612
|
+
for (let k = 0; k < 80; k++) {
|
|
2613
|
+
const m = (lo + hi) / 2;
|
|
2614
|
+
const fm = f(m);
|
|
2615
|
+
if (fm === 0) {
|
|
2616
|
+
lo = hi = m;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
if (fm * flo < 0) hi = m;
|
|
2620
|
+
else {
|
|
2621
|
+
lo = m;
|
|
2622
|
+
flo = fm;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
push((lo + hi) / 2);
|
|
2626
|
+
}
|
|
2627
|
+
return roots.sort((x, y) => x - y);
|
|
2628
|
+
}
|
|
2629
|
+
function lineHits(p, q, seg) {
|
|
2630
|
+
const dx = q[0] - p[0];
|
|
2631
|
+
const dy = q[1] - p[1];
|
|
2632
|
+
const len2 = dx * dx + dy * dy;
|
|
2633
|
+
if (len2 < 1e-24) return [];
|
|
2634
|
+
const h = (pt2) => (pt2[0] - p[0]) * -dy + (pt2[1] - p[1]) * dx;
|
|
2635
|
+
let coeffs;
|
|
2636
|
+
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
2637
|
+
else {
|
|
2638
|
+
const [h0, h1, h2, h3] = seg.map(h);
|
|
2639
|
+
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
2640
|
+
}
|
|
2641
|
+
if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
|
|
2642
|
+
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
|
|
2643
|
+
const out = [];
|
|
2644
|
+
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
2645
|
+
const point = segmentPoint(seg, t);
|
|
2646
|
+
const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
|
|
2647
|
+
if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
|
|
2648
|
+
}
|
|
2649
|
+
return out;
|
|
2650
|
+
}
|
|
2651
|
+
var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
|
|
2652
|
+
var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
|
|
2653
|
+
function arc(l, a, b, eps) {
|
|
2654
|
+
const n = l.segs.length;
|
|
2655
|
+
const segs = [];
|
|
2656
|
+
const chord = [];
|
|
2657
|
+
const push = (k, t0, t1) => {
|
|
2658
|
+
const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
|
|
2659
|
+
if (!degenerate(piece, eps)) {
|
|
2660
|
+
segs.push(piece);
|
|
2661
|
+
chord.push(l.chord[k]);
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
if (a.seg === b.seg && b.t >= a.t + 1e-12) {
|
|
2665
|
+
push(a.seg, a.t, b.t);
|
|
2666
|
+
return { segs, chord };
|
|
2667
|
+
}
|
|
2668
|
+
push(a.seg, a.t, 1);
|
|
2669
|
+
for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
|
|
2670
|
+
push(b.seg, 0, b.t);
|
|
2671
|
+
return { segs, chord };
|
|
2672
|
+
}
|
|
2673
|
+
function polySegs(points, eps) {
|
|
2674
|
+
const out = [];
|
|
2675
|
+
for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
|
|
2676
|
+
return out;
|
|
2677
|
+
}
|
|
2678
|
+
function knifePath(path, fillRule, knife) {
|
|
2679
|
+
const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2680
|
+
if (pts.length < 2) return null;
|
|
2681
|
+
let faces = facesOf(path, fillRule);
|
|
2682
|
+
if (!faces.length) return null;
|
|
2683
|
+
const b = bezierPathBounds(path);
|
|
2684
|
+
const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
|
|
2685
|
+
const eps = size * 1e-9;
|
|
2686
|
+
let cursor = -Infinity;
|
|
2687
|
+
let cuts = 0;
|
|
2688
|
+
for (let guard = 0; guard < 512; guard++) {
|
|
2689
|
+
let best = null;
|
|
2690
|
+
faces.forEach((face2, fi2) => {
|
|
2691
|
+
const hits = [];
|
|
2692
|
+
for (let m = 0; m < pts.length - 1; m++) {
|
|
2693
|
+
if (m + 1 < cursor - 1e-9) continue;
|
|
2694
|
+
const kb = { minX: Math.min(pts[m][0], pts[m + 1][0]), maxX: Math.max(pts[m][0], pts[m + 1][0]), minY: Math.min(pts[m][1], pts[m + 1][1]), maxY: Math.max(pts[m][1], pts[m + 1][1]) };
|
|
2695
|
+
face2.loops.forEach(
|
|
2696
|
+
(l, li) => l.segs.forEach((seg, si) => {
|
|
2697
|
+
const sb = segmentBounds(seg);
|
|
2698
|
+
if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
|
|
2699
|
+
for (const h of lineHits(pts[m], pts[m + 1], seg)) {
|
|
2700
|
+
const s = m + h.s;
|
|
2701
|
+
if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
|
|
2702
|
+
}
|
|
2703
|
+
})
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
hits.sort((x, y) => x.s - y.s);
|
|
2707
|
+
const unique = hits.filter((h, i) => i === 0 || h.s - hits[i - 1].s > 1e-9 || !near(h.point, hits[i - 1].point, eps * 10));
|
|
2708
|
+
const inside = (s) => {
|
|
2709
|
+
const m = Math.min(pts.length - 2, Math.floor(s));
|
|
2710
|
+
const u = s - m;
|
|
2711
|
+
const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
|
|
2712
|
+
return windingNumber(face2.loops.map((l) => l.segs), q) !== 0;
|
|
2713
|
+
};
|
|
2714
|
+
for (let i = 0; i < unique.length - 1; i++) {
|
|
2715
|
+
const from2 = unique[i];
|
|
2716
|
+
const to2 = unique[i + 1];
|
|
2717
|
+
if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
|
|
2718
|
+
if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
|
|
2719
|
+
break;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
if (!best) break;
|
|
2723
|
+
const { face: fi, from, to } = best;
|
|
2724
|
+
const face = faces[fi];
|
|
2725
|
+
const corners2 = [from.point];
|
|
2726
|
+
for (let m = Math.floor(from.s) + 1; m <= to.s - 1e-12 && m < pts.length; m++) if (m > from.s) corners2.push(pts[m]);
|
|
2727
|
+
corners2.push(to.point);
|
|
2728
|
+
const forward = polySegs(corners2, eps);
|
|
2729
|
+
const back = polySegs(corners2.slice().reverse(), eps);
|
|
2730
|
+
const la = face.loops[from.loop];
|
|
2731
|
+
const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
|
|
2732
|
+
if (from.loop === to.loop) {
|
|
2733
|
+
const ab = arc(la, from, to, eps);
|
|
2734
|
+
const ba = arc(la, to, from, eps);
|
|
2735
|
+
loops.push(
|
|
2736
|
+
{ segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
|
|
2737
|
+
{ segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
|
|
2738
|
+
);
|
|
2739
|
+
} else {
|
|
2740
|
+
const lb = face.loops[to.loop];
|
|
2741
|
+
const roundA = arc(la, from, from, eps);
|
|
2742
|
+
const roundB = arc(lb, to, to, eps);
|
|
2743
|
+
loops.push({
|
|
2744
|
+
segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
|
|
2745
|
+
chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
|
|
2749
|
+
cursor = to.s;
|
|
2750
|
+
cuts++;
|
|
2751
|
+
}
|
|
2752
|
+
return cuts ? faces.map(faceToPath) : null;
|
|
2753
|
+
}
|
|
2754
|
+
function eraserRegion(points, width) {
|
|
2755
|
+
const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2756
|
+
const r = width / 2;
|
|
2757
|
+
if (!pts.length || !(r > 0)) return [];
|
|
2758
|
+
const moved = pts.some((p) => !near(p, pts[0], 1e-9));
|
|
2759
|
+
if (!moved) {
|
|
2760
|
+
const [cx, cy] = pts[0];
|
|
2761
|
+
const k = r * 0.5522847498307936;
|
|
2762
|
+
return [
|
|
2763
|
+
{ type: "M", x: cx, y: cy - r },
|
|
2764
|
+
{ type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
|
|
2765
|
+
{ type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
|
|
2766
|
+
{ type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
|
|
2767
|
+
{ type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
|
|
2768
|
+
{ type: "Z" }
|
|
2769
|
+
];
|
|
2770
|
+
}
|
|
2771
|
+
const line2 = [{ type: "M", x: pts[0][0], y: pts[0][1] }, ...pts.slice(1).map((p) => ({ type: "L", x: p[0], y: p[1] }))];
|
|
2772
|
+
return outlineStroke(line2, { width, cap: "round", join: "round" });
|
|
2773
|
+
}
|
|
2774
|
+
function eraseFromFill(path, fillRule, region) {
|
|
2775
|
+
const overlap = pathBoolean("intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2776
|
+
if (!pathToContours(overlap).some((c) => Math.abs(contourSignedArea(closeContour(c, true))) > 1e-9)) return null;
|
|
2777
|
+
const rest = pathBoolean("difference", [{ path, fillRule }, { path: region }]).path;
|
|
2778
|
+
return splitIntoFaces(rest);
|
|
2779
|
+
}
|
|
2780
|
+
function eraseFromOutline(sp, region) {
|
|
2781
|
+
const regionSegs = pathToContours(region).flatMap((c) => closeContour(c, true));
|
|
2782
|
+
if (!regionSegs.length) return null;
|
|
2783
|
+
const rb = bezierPathBounds(region);
|
|
2784
|
+
const count = subpathSegmentCount(sp);
|
|
2785
|
+
if (!rb || count === 0) return null;
|
|
2786
|
+
const removed = (p) => pointInBezierPath(region, p, "nonzero");
|
|
2787
|
+
const runs = [];
|
|
2788
|
+
let cur = [];
|
|
2789
|
+
let any = false;
|
|
2790
|
+
for (let i = 0; i < count; i++) {
|
|
2791
|
+
const seg = subpathSegment(sp, i);
|
|
2792
|
+
const sb = segmentBounds(seg);
|
|
2793
|
+
const ts = [0, 1];
|
|
2794
|
+
if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
|
|
2795
|
+
for (const r of regionSegs) {
|
|
2796
|
+
const b = segmentBounds(r);
|
|
2797
|
+
if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
|
|
2798
|
+
for (const hit of segmentIntersections(seg, r)) if (hit.ta > 1e-9 && hit.ta < 1 - 1e-9) ts.push(hit.ta);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
ts.sort((a, b) => a - b);
|
|
2802
|
+
for (let k = 0; k < ts.length - 1; k++) {
|
|
2803
|
+
const t0 = ts[k];
|
|
2804
|
+
const t1 = ts[k + 1];
|
|
2805
|
+
if (t1 - t0 < 1e-9) continue;
|
|
2806
|
+
if (removed(segmentPoint(seg, (t0 + t1) / 2))) {
|
|
2807
|
+
any = true;
|
|
2808
|
+
if (cur.length) runs.push(cur);
|
|
2809
|
+
cur = [];
|
|
2810
|
+
} else cur.push({ seg: i, t0, t1 });
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
if (cur.length) runs.push(cur);
|
|
2814
|
+
if (!any) return null;
|
|
2815
|
+
if (sp.closed && runs.length >= 2) {
|
|
2816
|
+
const first = runs[0][0];
|
|
2817
|
+
const lastRun = runs[runs.length - 1];
|
|
2818
|
+
const last = lastRun[lastRun.length - 1];
|
|
2819
|
+
if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
|
|
2820
|
+
}
|
|
2821
|
+
const out = [];
|
|
2822
|
+
for (const run of runs) {
|
|
2823
|
+
const anchors = [];
|
|
2824
|
+
for (const { seg, t0, t1 } of run) {
|
|
2825
|
+
const c = subSegment(subpathSegment(sp, seg), t0, t1);
|
|
2826
|
+
const line2 = c.length === 2;
|
|
2827
|
+
const start = c[0];
|
|
2828
|
+
const end = c[c.length - 1];
|
|
2829
|
+
if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
|
|
2830
|
+
if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
|
|
2831
|
+
const next = { x: end[0], y: end[1] };
|
|
2832
|
+
if (!line2) next.in = { x: c[2][0], y: c[2][1] };
|
|
2833
|
+
anchors.push(next);
|
|
2834
|
+
}
|
|
2835
|
+
if (anchors.length < 2) continue;
|
|
2836
|
+
for (const a of anchors) {
|
|
2837
|
+
const type = inferAnchorType(a);
|
|
2838
|
+
if (type !== "corner") a.type = type;
|
|
2839
|
+
}
|
|
2840
|
+
out.push({ anchors, closed: false });
|
|
2841
|
+
}
|
|
2842
|
+
return out;
|
|
2843
|
+
}
|
|
2844
|
+
function polylineBounds(points, pad = 0) {
|
|
2845
|
+
if (!points.length) return null;
|
|
2846
|
+
let minX = Infinity;
|
|
2847
|
+
let minY = Infinity;
|
|
2848
|
+
let maxX = -Infinity;
|
|
2849
|
+
let maxY = -Infinity;
|
|
2850
|
+
for (const [x, y] of points) {
|
|
2851
|
+
minX = Math.min(minX, x);
|
|
2852
|
+
minY = Math.min(minY, y);
|
|
2853
|
+
maxX = Math.max(maxX, x);
|
|
2854
|
+
maxY = Math.max(maxY, y);
|
|
2855
|
+
}
|
|
2856
|
+
return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
|
|
2857
|
+
}
|
|
2858
|
+
function simplifyPolyline(points, tolerance) {
|
|
2859
|
+
const n = points.length;
|
|
2860
|
+
if (n < 3) return points.slice();
|
|
2861
|
+
const keep = new Array(n).fill(false);
|
|
2862
|
+
keep[0] = keep[n - 1] = true;
|
|
2863
|
+
const stack = [[0, n - 1]];
|
|
2864
|
+
while (stack.length) {
|
|
2865
|
+
const [a, b] = stack.pop();
|
|
2866
|
+
const [ax, ay] = points[a];
|
|
2867
|
+
const dx = points[b][0] - ax;
|
|
2868
|
+
const dy = points[b][1] - ay;
|
|
2869
|
+
const len2 = Math.hypot(dx, dy);
|
|
2870
|
+
let far = -1;
|
|
2871
|
+
let farD = 0;
|
|
2872
|
+
for (let i = a + 1; i < b; i++) {
|
|
2873
|
+
const [px, py] = points[i];
|
|
2874
|
+
const d = len2 < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len2;
|
|
2875
|
+
if (d > farD) {
|
|
2876
|
+
farD = d;
|
|
2877
|
+
far = i;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
if (far >= 0 && farD > tolerance) {
|
|
2881
|
+
keep[far] = true;
|
|
2882
|
+
stack.push([a, far], [far, b]);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return points.filter((_, i) => keep[i]);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/vector/ink.ts
|
|
2889
|
+
var LazyMouse = class {
|
|
2890
|
+
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
2891
|
+
constructor(length2) {
|
|
2892
|
+
this.length = length2;
|
|
2893
|
+
}
|
|
2894
|
+
length;
|
|
2895
|
+
pos = null;
|
|
2896
|
+
last = null;
|
|
2897
|
+
push(p) {
|
|
2898
|
+
this.last = p;
|
|
2899
|
+
if (!(this.length > 0)) return [p];
|
|
2900
|
+
const pos = this.pos;
|
|
2901
|
+
if (!pos) {
|
|
2902
|
+
this.pos = p;
|
|
2903
|
+
return [p];
|
|
2904
|
+
}
|
|
2905
|
+
const dx = p[0] - pos[0];
|
|
2906
|
+
const dy = p[1] - pos[1];
|
|
2907
|
+
const d = Math.hypot(dx, dy);
|
|
2908
|
+
if (d <= this.length) return [];
|
|
2909
|
+
const k = (d - this.length) / d;
|
|
2910
|
+
const next = [pos[0] + dx * k, pos[1] + dy * k];
|
|
2911
|
+
this.pos = next;
|
|
2912
|
+
return [next];
|
|
2913
|
+
}
|
|
2914
|
+
/** The string from the pen to the pointer while they are apart, else null. */
|
|
2915
|
+
string() {
|
|
2916
|
+
const { pos, last } = this;
|
|
2917
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return null;
|
|
2918
|
+
return [pos, last];
|
|
2919
|
+
}
|
|
2920
|
+
/** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */
|
|
2921
|
+
finish() {
|
|
2922
|
+
const { pos, last } = this;
|
|
2923
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return [];
|
|
2924
|
+
this.pos = last;
|
|
2925
|
+
return [last];
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
var VECTOR_INK_MAX_CORNERS = 8;
|
|
2929
|
+
var MAX_POINTS = 1e5;
|
|
2930
|
+
function vectorInkShapeName(shape) {
|
|
2931
|
+
switch (shape.kind) {
|
|
2932
|
+
case "line":
|
|
2933
|
+
return "line";
|
|
2934
|
+
case "rect":
|
|
2935
|
+
return shape.width === shape.height ? "square" : "rectangle";
|
|
2936
|
+
case "ellipse":
|
|
2937
|
+
return shape.width === shape.height ? "circle" : "ellipse";
|
|
2938
|
+
case "polygon":
|
|
2939
|
+
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2943
|
+
function bbox(pts) {
|
|
2944
|
+
if (!pts.length) return [0, 0, 0, 0];
|
|
2945
|
+
let [x0, y0] = pts[0];
|
|
2946
|
+
let [x1, y1] = pts[0];
|
|
2947
|
+
for (const [x, y] of pts) {
|
|
2948
|
+
x0 = Math.min(x0, x);
|
|
2949
|
+
y0 = Math.min(y0, y);
|
|
2950
|
+
x1 = Math.max(x1, x);
|
|
2951
|
+
y1 = Math.max(y1, y);
|
|
2952
|
+
}
|
|
2953
|
+
return [x0, y0, x1 - x0, y1 - y0];
|
|
2954
|
+
}
|
|
2955
|
+
function segDistance(a, b, p) {
|
|
2956
|
+
const dx = b[0] - a[0];
|
|
2957
|
+
const dy = b[1] - a[1];
|
|
2958
|
+
const len2 = dx * dx + dy * dy;
|
|
2959
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2960
|
+
return dist2([a[0] + t * dx, a[1] + t * dy], p);
|
|
2961
|
+
}
|
|
2962
|
+
function polygonDistance(c, p) {
|
|
2963
|
+
let best = Infinity;
|
|
2964
|
+
for (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));
|
|
2965
|
+
return best;
|
|
2966
|
+
}
|
|
2967
|
+
function ellipseRms(p, x0, y0, w, h) {
|
|
2968
|
+
const rx = w / 2;
|
|
2969
|
+
const ry = h / 2;
|
|
2970
|
+
if (rx <= 0 || ry <= 0 || !p.length) return Infinity;
|
|
2971
|
+
const cx = x0 + rx;
|
|
2972
|
+
const cy = y0 + ry;
|
|
2973
|
+
let sum = 0;
|
|
2974
|
+
for (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;
|
|
2975
|
+
return Math.sqrt(sum / p.length);
|
|
2976
|
+
}
|
|
2977
|
+
function snapSquare(x, y, w, h) {
|
|
2978
|
+
if (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {
|
|
2979
|
+
const s = (w + h) / 2;
|
|
2980
|
+
return [x + (w - s) / 2, y + (h - s) / 2, s, s];
|
|
2981
|
+
}
|
|
2982
|
+
return [x, y, w, h];
|
|
2983
|
+
}
|
|
2984
|
+
function turning(a, b, c) {
|
|
2985
|
+
const u = [b[0] - a[0], b[1] - a[1]];
|
|
2986
|
+
const v2 = [c[0] - b[0], c[1] - b[1]];
|
|
2987
|
+
const lu = Math.hypot(u[0], u[1]);
|
|
2988
|
+
const lv = Math.hypot(v2[0], v2[1]);
|
|
2989
|
+
if (lu <= 0 || lv <= 0) return 0;
|
|
2990
|
+
return Math.acos(Math.min(1, Math.max(-1, (u[0] * v2[0] + u[1] * v2[1]) / (lu * lv)))) * 180 / Math.PI;
|
|
2991
|
+
}
|
|
2992
|
+
function rdp(p, eps) {
|
|
2993
|
+
const n = p.length;
|
|
2994
|
+
if (n < 3) return p.slice();
|
|
2995
|
+
const keep = new Array(n).fill(false);
|
|
2996
|
+
keep[0] = keep[n - 1] = true;
|
|
2997
|
+
const stack = [[0, n - 1]];
|
|
2998
|
+
while (stack.length) {
|
|
2999
|
+
const [a, b] = stack.pop();
|
|
3000
|
+
let far = a;
|
|
3001
|
+
let farD = 0;
|
|
3002
|
+
for (let i = a + 1; i < b; i++) {
|
|
3003
|
+
const d = segDistance(p[a], p[b], p[i]);
|
|
3004
|
+
if (d > farD) {
|
|
3005
|
+
farD = d;
|
|
3006
|
+
far = i;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (farD > eps) {
|
|
3010
|
+
keep[far] = true;
|
|
3011
|
+
stack.push([a, far], [far, b]);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return p.filter((_, i) => keep[i]);
|
|
3015
|
+
}
|
|
3016
|
+
function corners(p, diag) {
|
|
3017
|
+
const eps = 0.05 * diag;
|
|
3018
|
+
const start = p[0];
|
|
3019
|
+
let far = 0;
|
|
3020
|
+
let farD = 0;
|
|
3021
|
+
p.forEach((q, i) => {
|
|
3022
|
+
const d = dist2(start, q);
|
|
3023
|
+
if (d > farD) {
|
|
3024
|
+
farD = d;
|
|
3025
|
+
far = i;
|
|
3026
|
+
}
|
|
3027
|
+
});
|
|
3028
|
+
const a = p.slice(0, far);
|
|
3029
|
+
const b = p.slice(far);
|
|
3030
|
+
const ring = rdp([...a, ...b.slice(0, 1)], eps);
|
|
3031
|
+
const back = rdp([...b, start], eps);
|
|
3032
|
+
ring.pop();
|
|
3033
|
+
back.pop();
|
|
3034
|
+
ring.push(...back);
|
|
3035
|
+
while (ring.length > 3) {
|
|
3036
|
+
const n = ring.length;
|
|
3037
|
+
const turn = (i) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);
|
|
3038
|
+
let flat = 0;
|
|
3039
|
+
let flatT = Infinity;
|
|
3040
|
+
for (let i = 0; i < n; i++) {
|
|
3041
|
+
const t = turn(i);
|
|
3042
|
+
if (t < flatT) {
|
|
3043
|
+
flatT = t;
|
|
3044
|
+
flat = i;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
let short = -1;
|
|
3048
|
+
for (let j = 0; j < n; j++) {
|
|
3049
|
+
if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
3050
|
+
short = j;
|
|
3051
|
+
break;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (flatT < 30) ring.splice(flat, 1);
|
|
3055
|
+
else if (short >= 0) {
|
|
3056
|
+
const k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;
|
|
3057
|
+
ring.splice(k, 1);
|
|
3058
|
+
} else break;
|
|
3059
|
+
}
|
|
3060
|
+
return ring;
|
|
3061
|
+
}
|
|
3062
|
+
function upright(c) {
|
|
3063
|
+
const n = c.length;
|
|
3064
|
+
const dirs = c.map((a, i) => {
|
|
3065
|
+
const b = c[(i + 1) % n];
|
|
3066
|
+
const deg = (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI % 180 + 180) % 180;
|
|
3067
|
+
if (deg <= 15 || deg >= 165) return "across";
|
|
3068
|
+
if (deg >= 75 && deg <= 105) return "down";
|
|
3069
|
+
return null;
|
|
3070
|
+
});
|
|
3071
|
+
return dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);
|
|
3072
|
+
}
|
|
3073
|
+
function recognizeInkShape(points, options = {}) {
|
|
3074
|
+
const minSize = options.minSize ?? 6;
|
|
3075
|
+
const p = [];
|
|
3076
|
+
for (const q of points.slice(0, MAX_POINTS)) {
|
|
3077
|
+
if (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;
|
|
3078
|
+
const last2 = p[p.length - 1];
|
|
3079
|
+
if (!last2 || last2[0] !== q[0] || last2[1] !== q[1]) p.push([q[0], q[1]]);
|
|
3080
|
+
}
|
|
3081
|
+
if (p.length < 2) return null;
|
|
3082
|
+
const first = p[0];
|
|
3083
|
+
const last = p[p.length - 1];
|
|
3084
|
+
const [x0, y0, w, h] = bbox(p);
|
|
3085
|
+
const size = Math.max(w, h);
|
|
3086
|
+
const diag = Math.hypot(w, h);
|
|
3087
|
+
if (size < minSize) return null;
|
|
3088
|
+
let length2 = 0;
|
|
3089
|
+
for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
|
|
3090
|
+
const gap = dist2(first, last);
|
|
3091
|
+
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
3092
|
+
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
3093
|
+
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
3094
|
+
}
|
|
3095
|
+
if (gap > 0.2 * size || length2 < 2 * size) return null;
|
|
3096
|
+
const rms = ellipseRms(p, x0, y0, w, h);
|
|
3097
|
+
const roundish = w > 0.2 * size && h > 0.2 * size;
|
|
3098
|
+
const ellipse = () => {
|
|
3099
|
+
const [x, y, sw, sh] = snapSquare(x0, y0, w, h);
|
|
3100
|
+
return { kind: "ellipse", x, y, width: sw, height: sh };
|
|
3101
|
+
};
|
|
3102
|
+
if (roundish && rms < 0.06) return ellipse();
|
|
3103
|
+
const c = corners(p, diag);
|
|
3104
|
+
const fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);
|
|
3105
|
+
if (fits && c.length === 4 && upright(c)) {
|
|
3106
|
+
const [bx, by, bw, bh] = bbox(c);
|
|
3107
|
+
const [x, y, sw, sh] = snapSquare(bx, by, bw, bh);
|
|
3108
|
+
return { kind: "rect", x, y, width: sw, height: sh };
|
|
3109
|
+
}
|
|
3110
|
+
if (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: "polygon", points: c };
|
|
3111
|
+
return roundish && rms < 0.1 ? ellipse() : null;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// src/vector/controller.ts
|
|
3115
|
+
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
3116
|
+
var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
|
|
3117
|
+
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3118
|
+
var VECTOR_STYLE_KEYS = [
|
|
3119
|
+
"fill",
|
|
3120
|
+
"stroke",
|
|
3121
|
+
"strokeWidth",
|
|
3122
|
+
"strokeLinecap",
|
|
3123
|
+
"strokeLinejoin",
|
|
3124
|
+
"strokeDasharray",
|
|
3125
|
+
"fillRule",
|
|
3126
|
+
"strokeAlign",
|
|
3127
|
+
"startArrow",
|
|
3128
|
+
"endArrow",
|
|
3129
|
+
"arrowAlign",
|
|
3130
|
+
"startArrowScale",
|
|
3131
|
+
"endArrowScale"
|
|
3132
|
+
];
|
|
3133
|
+
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
3134
|
+
var DEFAULT_DEFAULTS = {
|
|
3135
|
+
fill: "#d4d4d8",
|
|
3136
|
+
stroke: "#18181b",
|
|
3137
|
+
strokeWidth: 2,
|
|
3138
|
+
polygonSides: 6,
|
|
3139
|
+
starPoints: 5,
|
|
3140
|
+
starInnerRatio: 0.5,
|
|
3141
|
+
cornerRadius: 0
|
|
3142
|
+
};
|
|
3143
|
+
var MIN_ZOOM = 0.05;
|
|
3144
|
+
var MAX_ZOOM = 64;
|
|
3145
|
+
var VECTOR_ROTATE_HANDLE_OFFSET = 24;
|
|
3146
|
+
var sameRef = (a, b) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
|
|
3147
|
+
function getEditablePathIds(state) {
|
|
3148
|
+
if (state.tool !== "direct" && state.tool !== "pen") return [];
|
|
3149
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3150
|
+
if (state.pen) ids.add(state.pen.nodeId);
|
|
3151
|
+
if (state.tool === "direct") {
|
|
3152
|
+
if (state.editingId) ids.add(state.editingId);
|
|
3153
|
+
for (const id of state.selection) {
|
|
3154
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
3155
|
+
if (node && !node.locked) {
|
|
3156
|
+
for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return [...ids].filter((id) => findVectorNode(state.document.nodes, id)?.type === "path");
|
|
3161
|
+
}
|
|
3162
|
+
function getVisibleAnchors(state) {
|
|
3163
|
+
const out = [];
|
|
3164
|
+
for (const id of getEditablePathIds(state)) {
|
|
3165
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
3166
|
+
node.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));
|
|
3167
|
+
}
|
|
3168
|
+
return out;
|
|
3169
|
+
}
|
|
3170
|
+
function getVisibleHandles(state) {
|
|
3171
|
+
const out = [];
|
|
3172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3173
|
+
const push = (node, s, i, handle) => {
|
|
3174
|
+
const sp = node.subpaths[s];
|
|
3175
|
+
const a = sp?.anchors[i];
|
|
3176
|
+
if (!a || !a[handle]) return;
|
|
3177
|
+
const key = `${node.id}:${s}:${i}:${handle}`;
|
|
3178
|
+
if (seen.has(key)) return;
|
|
3179
|
+
seen.add(key);
|
|
3180
|
+
out.push({ nodeId: node.id, subpath: s, index: i, handle });
|
|
3181
|
+
};
|
|
3182
|
+
if (state.tool === "pen" && state.pen) {
|
|
3183
|
+
const node = findVectorNode(state.document.nodes, state.pen.nodeId);
|
|
3184
|
+
const sp = node?.type === "path" ? node.subpaths[state.pen.subpath] : void 0;
|
|
3185
|
+
if (node?.type === "path" && sp) {
|
|
3186
|
+
const last = sp.anchors.length - 1;
|
|
3187
|
+
push(node, state.pen.subpath, last, "in");
|
|
3188
|
+
push(node, state.pen.subpath, last, "out");
|
|
3189
|
+
}
|
|
3190
|
+
return out;
|
|
3191
|
+
}
|
|
3192
|
+
if (state.tool !== "direct") return out;
|
|
3193
|
+
const editable = new Set(getEditablePathIds(state));
|
|
3194
|
+
for (const ref of state.selectedAnchors) {
|
|
3195
|
+
if (!editable.has(ref.nodeId)) continue;
|
|
3196
|
+
const node = findVectorNode(state.document.nodes, ref.nodeId);
|
|
3197
|
+
const sp = node.subpaths[ref.subpath];
|
|
3198
|
+
if (!sp) continue;
|
|
3199
|
+
const n = sp.anchors.length;
|
|
3200
|
+
push(node, ref.subpath, ref.index, "in");
|
|
3201
|
+
push(node, ref.subpath, ref.index, "out");
|
|
3202
|
+
const prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;
|
|
3203
|
+
const next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;
|
|
3204
|
+
if (prev >= 0) push(node, ref.subpath, prev, "out");
|
|
3205
|
+
if (next >= 0) push(node, ref.subpath, next, "in");
|
|
3206
|
+
}
|
|
3207
|
+
return out;
|
|
3208
|
+
}
|
|
3209
|
+
var VECTOR_CORNER_WIDGET_INSET = 10;
|
|
3210
|
+
function getVectorCornerWidgets(state) {
|
|
3211
|
+
if (state.tool !== "direct" || state.pen) return [];
|
|
3212
|
+
const zoom = state.viewport.zoom;
|
|
3213
|
+
const out = [];
|
|
3214
|
+
for (const id of getEditablePathIds(state)) {
|
|
3215
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
3216
|
+
const picked = state.selectedAnchors.filter((r) => r.nodeId === id);
|
|
3217
|
+
const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
|
|
3218
|
+
for (const c of vectorCorners(node.subpaths)) {
|
|
3219
|
+
if (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;
|
|
3220
|
+
const side = Math.min(c.lu, c.lv);
|
|
3221
|
+
if (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;
|
|
3222
|
+
const a = node.subpaths[c.subpath].anchors[c.index];
|
|
3223
|
+
const radius = a.corner?.radius ?? 0;
|
|
3224
|
+
const drawn = fitCornerRadius(c, radius);
|
|
3225
|
+
const k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);
|
|
3226
|
+
const p = applyMatrix(m, cornerBisectorPoint(c, k));
|
|
3227
|
+
out.push({ ref: { nodeId: id, subpath: c.subpath, index: c.index }, x: p.x, y: p.y, radius, drawnRadius: drawn, kind: a.corner?.kind ?? "round", corner: c });
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return out;
|
|
3231
|
+
}
|
|
3232
|
+
function getAnchor(doc, ref) {
|
|
3233
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3234
|
+
return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
|
|
3235
|
+
}
|
|
3236
|
+
function getAnchorWorld(doc, ref) {
|
|
3237
|
+
const a = getAnchor(doc, ref);
|
|
3238
|
+
if (!a) return void 0;
|
|
3239
|
+
const m = vectorWorldMatrix(doc.nodes, ref.nodeId) ?? IDENTITY_MATRIX;
|
|
3240
|
+
const p = applyMatrix(m, a);
|
|
3241
|
+
return { x: p.x, y: p.y, ...a.in ? { in: applyMatrix(m, a.in) } : {}, ...a.out ? { out: applyMatrix(m, a.out) } : {}, ...a.type ? { type: a.type } : {} };
|
|
3242
|
+
}
|
|
3243
|
+
function getVectorSelectionFrame(doc, ids) {
|
|
3244
|
+
const nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: vectorParentMatrix(doc.nodes, id) ?? IDENTITY_MATRIX })).filter((n) => n.node);
|
|
3245
|
+
if (!nodes.length) return null;
|
|
3246
|
+
const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
|
|
3247
|
+
const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
|
|
3248
|
+
const angle = same ? angles[0] : 0;
|
|
3249
|
+
const unrotate = rotateMatrix(-angle);
|
|
3250
|
+
const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
|
|
3251
|
+
return b ? { ...b, angle } : null;
|
|
3252
|
+
}
|
|
3253
|
+
var normalizeAngle = (a) => ((a + 180) % 360 + 360) % 360 - 180;
|
|
3254
|
+
function getTransformHandlePositions(box) {
|
|
3255
|
+
const { x, y, width: w, height: h } = box;
|
|
3256
|
+
return [
|
|
3257
|
+
{ handle: "nw", x, y },
|
|
3258
|
+
{ handle: "n", x: x + w / 2, y },
|
|
3259
|
+
{ handle: "ne", x: x + w, y },
|
|
3260
|
+
{ handle: "e", x: x + w, y: y + h / 2 },
|
|
3261
|
+
{ handle: "se", x: x + w, y: y + h },
|
|
3262
|
+
{ handle: "s", x: x + w / 2, y: y + h },
|
|
3263
|
+
{ handle: "sw", x, y: y + h },
|
|
3264
|
+
{ handle: "w", x, y: y + h / 2 },
|
|
3265
|
+
{ handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
|
|
3266
|
+
];
|
|
3267
|
+
}
|
|
3268
|
+
function getVectorSelectionBoxGeometry(frame, viewport) {
|
|
3269
|
+
const z = viewport.zoom;
|
|
3270
|
+
const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
|
|
3271
|
+
const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
|
|
3272
|
+
const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
|
|
3273
|
+
const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
|
|
3274
|
+
const corners2 = [
|
|
3275
|
+
{ x: box.x, y: box.y },
|
|
3276
|
+
{ x: box.x + box.width, y: box.y },
|
|
3277
|
+
{ x: box.x + box.width, y: box.y + box.height },
|
|
3278
|
+
{ x: box.x, y: box.y + box.height }
|
|
3279
|
+
].map(toScreen2);
|
|
3280
|
+
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
3281
|
+
return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
|
|
3282
|
+
}
|
|
3283
|
+
var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
|
|
1736
3284
|
var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
|
|
1737
3285
|
function vectorResizeCursor(handle, angle = 0) {
|
|
1738
3286
|
if (handle === "rotate") return "grab";
|
|
@@ -1748,9 +3296,12 @@ var TOOL_LABELS = {
|
|
|
1748
3296
|
ellipse: "Ellipse",
|
|
1749
3297
|
polygon: "Polygon",
|
|
1750
3298
|
star: "Star",
|
|
1751
|
-
line: "Line"
|
|
3299
|
+
line: "Line",
|
|
3300
|
+
scissors: "Scissors",
|
|
3301
|
+
knife: "Knife",
|
|
3302
|
+
eraser: "Eraser"
|
|
1752
3303
|
};
|
|
1753
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse",
|
|
3304
|
+
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
1754
3305
|
function vectorToolLabel(tool) {
|
|
1755
3306
|
return TOOL_LABELS[tool];
|
|
1756
3307
|
}
|
|
@@ -1763,7 +3314,10 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
1763
3314
|
ellipse: "O",
|
|
1764
3315
|
polygon: "Y",
|
|
1765
3316
|
star: "S",
|
|
1766
|
-
line: "L"
|
|
3317
|
+
line: "L",
|
|
3318
|
+
scissors: "C",
|
|
3319
|
+
knife: "K",
|
|
3320
|
+
eraser: "E"
|
|
1767
3321
|
};
|
|
1768
3322
|
var VECTOR_TOOL_GLYPHS = {
|
|
1769
3323
|
select: "\u2196",
|
|
@@ -1774,8 +3328,12 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
1774
3328
|
ellipse: "\u25EF",
|
|
1775
3329
|
polygon: "\u2B21",
|
|
1776
3330
|
star: "\u2606",
|
|
1777
|
-
line: "\u2571"
|
|
3331
|
+
line: "\u2571",
|
|
3332
|
+
scissors: "\u2702",
|
|
3333
|
+
knife: "\u27CB",
|
|
3334
|
+
eraser: "\u232B"
|
|
1778
3335
|
};
|
|
3336
|
+
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
1779
3337
|
var BOOLEAN_OPS = {
|
|
1780
3338
|
union: "union",
|
|
1781
3339
|
subtract: "difference",
|
|
@@ -1797,6 +3355,28 @@ function mapTree(nodes, fn) {
|
|
|
1797
3355
|
}
|
|
1798
3356
|
return changed ? out : nodes;
|
|
1799
3357
|
}
|
|
3358
|
+
function spliceTree(nodes, id, replacements) {
|
|
3359
|
+
let changed = false;
|
|
3360
|
+
const out = [];
|
|
3361
|
+
for (const node of nodes) {
|
|
3362
|
+
if (node.id === id) {
|
|
3363
|
+
changed = true;
|
|
3364
|
+
out.push(...replacements);
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
if (node.type === "group") {
|
|
3368
|
+
const children = spliceTree(node.children, id, replacements);
|
|
3369
|
+
if (children !== node.children) {
|
|
3370
|
+
changed = true;
|
|
3371
|
+
out.push({ ...node, children });
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
out.push(node);
|
|
3376
|
+
}
|
|
3377
|
+
return changed ? out : nodes;
|
|
3378
|
+
}
|
|
3379
|
+
var toPoint2 = (p) => [p.x, p.y];
|
|
1800
3380
|
var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
1801
3381
|
var idCounter = 0;
|
|
1802
3382
|
var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
|
|
@@ -1811,6 +3391,7 @@ var VectorEditorController = class {
|
|
|
1811
3391
|
defaults;
|
|
1812
3392
|
viewport;
|
|
1813
3393
|
snap;
|
|
3394
|
+
editing;
|
|
1814
3395
|
hitRadius;
|
|
1815
3396
|
hover = null;
|
|
1816
3397
|
pointer = null;
|
|
@@ -1839,6 +3420,7 @@ var VectorEditorController = class {
|
|
|
1839
3420
|
this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
|
|
1840
3421
|
this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
|
|
1841
3422
|
this.snap = { ...DEFAULT_VECTOR_SNAP, ...options.snap ?? {} };
|
|
3423
|
+
this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...options.editing ?? {} };
|
|
1842
3424
|
this.hitRadius = options.hitRadius ?? 6;
|
|
1843
3425
|
this.createId = options.createId ?? defaultCreateId;
|
|
1844
3426
|
this.onChange = options.onChange;
|
|
@@ -1887,6 +3469,7 @@ var VectorEditorController = class {
|
|
|
1887
3469
|
viewport: this.viewport,
|
|
1888
3470
|
defaults: this.defaults,
|
|
1889
3471
|
snap: this.snap,
|
|
3472
|
+
editing: this.editing,
|
|
1890
3473
|
guides: this.guides,
|
|
1891
3474
|
hitRadius: this.hitRadius,
|
|
1892
3475
|
hover: this.hover,
|
|
@@ -1943,7 +3526,7 @@ var VectorEditorController = class {
|
|
|
1943
3526
|
if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
|
|
1944
3527
|
return h?.nodeId ? "move" : "default";
|
|
1945
3528
|
case "direct":
|
|
1946
|
-
if (h?.anchor || h?.handle) return "pointer";
|
|
3529
|
+
if (h?.anchor || h?.handle || h?.corner) return "pointer";
|
|
1947
3530
|
if (h?.segment) return "copy";
|
|
1948
3531
|
return h?.nodeId ? "move" : "default";
|
|
1949
3532
|
case "pen":
|
|
@@ -2028,12 +3611,12 @@ var VectorEditorController = class {
|
|
|
2028
3611
|
// Viewport
|
|
2029
3612
|
// -------------------------------------------------------------------------
|
|
2030
3613
|
screenToDocument(p) {
|
|
2031
|
-
const
|
|
2032
|
-
return { x: (p.x -
|
|
3614
|
+
const v2 = this.viewport;
|
|
3615
|
+
return { x: (p.x - v2.x) / v2.zoom, y: (p.y - v2.y) / v2.zoom };
|
|
2033
3616
|
}
|
|
2034
3617
|
documentToScreen(p) {
|
|
2035
|
-
const
|
|
2036
|
-
return { x: p.x *
|
|
3618
|
+
const v2 = this.viewport;
|
|
3619
|
+
return { x: p.x * v2.zoom + v2.x, y: p.y * v2.zoom + v2.y };
|
|
2037
3620
|
}
|
|
2038
3621
|
setViewport(viewport) {
|
|
2039
3622
|
const next = { ...this.viewport, ...viewport };
|
|
@@ -2088,13 +3671,21 @@ var VectorEditorController = class {
|
|
|
2088
3671
|
if (!this.snap.guides || !this.snap.objects) this.guides = null;
|
|
2089
3672
|
this.notify();
|
|
2090
3673
|
}
|
|
3674
|
+
/** Changes how deleting, dragging anchors, the pencil and the eraser behave. */
|
|
3675
|
+
setEditing(editing) {
|
|
3676
|
+
const next = { ...this.editing, ...editing };
|
|
3677
|
+
next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
|
|
3678
|
+
if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
|
|
3679
|
+
this.editing = next;
|
|
3680
|
+
this.notify();
|
|
3681
|
+
}
|
|
2091
3682
|
/** Turns object snapping on or off (the toolbar's snapping toggle). */
|
|
2092
3683
|
toggleSnapping(on = !this.snap.objects) {
|
|
2093
3684
|
this.setSnap({ objects: on });
|
|
2094
3685
|
}
|
|
2095
|
-
gridValue(
|
|
3686
|
+
gridValue(v2) {
|
|
2096
3687
|
const g = this.snap.grid;
|
|
2097
|
-
return g > 0 ? Math.round(
|
|
3688
|
+
return g > 0 ? Math.round(v2 / g) * g : v2;
|
|
2098
3689
|
}
|
|
2099
3690
|
snapPoint(p) {
|
|
2100
3691
|
return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
|
|
@@ -2150,7 +3741,7 @@ var VectorEditorController = class {
|
|
|
2150
3741
|
const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
|
|
2151
3742
|
return this.snapDrawn(p, leave, input, () => this.drawingTargets(this.pen?.nodeId ?? null));
|
|
2152
3743
|
}
|
|
2153
|
-
if (
|
|
3744
|
+
if (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
2154
3745
|
this.setGuides(null);
|
|
2155
3746
|
return p;
|
|
2156
3747
|
}
|
|
@@ -2250,7 +3841,7 @@ var VectorEditorController = class {
|
|
|
2250
3841
|
let subpath;
|
|
2251
3842
|
switch (input.kind) {
|
|
2252
3843
|
case "rect":
|
|
2253
|
-
subpath = rectSubpath(b
|
|
3844
|
+
subpath = rectSubpath(b);
|
|
2254
3845
|
break;
|
|
2255
3846
|
case "ellipse":
|
|
2256
3847
|
subpath = ellipseSubpath(b);
|
|
@@ -2265,10 +3856,13 @@ var VectorEditorController = class {
|
|
|
2265
3856
|
subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
|
|
2266
3857
|
break;
|
|
2267
3858
|
}
|
|
3859
|
+
const radius = input.cornerRadius ?? d.cornerRadius;
|
|
3860
|
+
const kind = input.cornerKind ?? d.cornerKind;
|
|
3861
|
+
if ((input.kind === "rect" || input.kind === "polygon" || input.kind === "star") && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);
|
|
2268
3862
|
const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
2269
3863
|
const style = this.styleFor(input.kind === "line");
|
|
2270
3864
|
const own = {};
|
|
2271
|
-
for (const key of
|
|
3865
|
+
for (const key of VECTOR_STYLE_KEYS) {
|
|
2272
3866
|
if (input[key] !== void 0) own[key] = input[key];
|
|
2273
3867
|
}
|
|
2274
3868
|
return { id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
|
|
@@ -2291,6 +3885,8 @@ var VectorEditorController = class {
|
|
|
2291
3885
|
if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
|
|
2292
3886
|
if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
|
|
2293
3887
|
if (d.fillRule) out.fillRule = d.fillRule;
|
|
3888
|
+
if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
|
|
3889
|
+
for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
|
|
2294
3890
|
return out;
|
|
2295
3891
|
}
|
|
2296
3892
|
/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
|
|
@@ -2315,6 +3911,95 @@ var VectorEditorController = class {
|
|
|
2315
3911
|
});
|
|
2316
3912
|
return { ok: true };
|
|
2317
3913
|
}
|
|
3914
|
+
/**
|
|
3915
|
+
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
3916
|
+
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
3917
|
+
* corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.
|
|
3918
|
+
*/
|
|
3919
|
+
setCornerRadius(radius, options = {}) {
|
|
3920
|
+
if (!Number.isFinite(radius) || radius < 0) return { ok: false, message: "The radius must be a number of at least 0." };
|
|
3921
|
+
const refs = this.cornerTargets(options);
|
|
3922
|
+
if (!refs.length) return { ok: false, message: "Nothing is selected." };
|
|
3923
|
+
this.change(radius > 0 ? "Corner Radius" : "Sharpen Corners", () => {
|
|
3924
|
+
this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind));
|
|
3925
|
+
});
|
|
3926
|
+
return { ok: true };
|
|
3927
|
+
}
|
|
3928
|
+
/** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */
|
|
3929
|
+
setCornerKind(kind, options = {}) {
|
|
3930
|
+
const refs = this.cornerTargets(options);
|
|
3931
|
+
if (!refs.length) return { ok: false, message: "Nothing is selected." };
|
|
3932
|
+
this.change("Corner Kind", () => {
|
|
3933
|
+
this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
|
|
3934
|
+
});
|
|
3935
|
+
return { ok: true };
|
|
3936
|
+
}
|
|
3937
|
+
/** Anchors a corner change applies to (see `setCornerRadius`). */
|
|
3938
|
+
cornerTargets(options) {
|
|
3939
|
+
if (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));
|
|
3940
|
+
if (!options.ids && this.tool === "direct" && this.anchors.length) return this.anchors.slice();
|
|
3941
|
+
const out = [];
|
|
3942
|
+
for (const id of options.ids ?? this.selection) {
|
|
3943
|
+
const node = this.getNode(id);
|
|
3944
|
+
if (!node) continue;
|
|
3945
|
+
for (const p of collectPathNodes(node)) {
|
|
3946
|
+
if (p.locked) continue;
|
|
3947
|
+
p.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
return out;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the
|
|
3954
|
+
* area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy
|
|
3955
|
+
* of the path that keeps the fill (left out when there is none). Returns the new paths' ids.
|
|
3956
|
+
*/
|
|
3957
|
+
outlineStroke(options = {}) {
|
|
3958
|
+
const roots = options.ids ?? this.selectedRoots();
|
|
3959
|
+
const paths = [];
|
|
3960
|
+
for (const id of roots) {
|
|
3961
|
+
const node = this.getNode(id);
|
|
3962
|
+
if (node) {
|
|
3963
|
+
for (const p of collectPathNodes(node)) if (!p.locked && hasPaint(p.stroke) && (p.strokeWidth ?? 1) > 0) paths.push(p);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
if (!paths.length) return [];
|
|
3967
|
+
const created = [];
|
|
3968
|
+
const replace = /* @__PURE__ */ new Map();
|
|
3969
|
+
for (const p of paths) {
|
|
3970
|
+
const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
|
|
3971
|
+
if (!outline.length) continue;
|
|
3972
|
+
const {
|
|
3973
|
+
stroke,
|
|
3974
|
+
strokeWidth: _w,
|
|
3975
|
+
strokeLinecap: _c,
|
|
3976
|
+
strokeLinejoin: _j,
|
|
3977
|
+
strokeDasharray: _d,
|
|
3978
|
+
strokeAlign: _a,
|
|
3979
|
+
startArrow: _s,
|
|
3980
|
+
endArrow: _e,
|
|
3981
|
+
arrowAlign: _al,
|
|
3982
|
+
startArrowScale: _ss,
|
|
3983
|
+
endArrowScale: _es,
|
|
3984
|
+
...rest
|
|
3985
|
+
} = cloneVectorNode(p);
|
|
3986
|
+
const id = this.newId();
|
|
3987
|
+
const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
|
|
3988
|
+
const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
|
|
3989
|
+
created.push(id);
|
|
3990
|
+
replace.set(p.id, list);
|
|
3991
|
+
}
|
|
3992
|
+
if (!created.length) return [];
|
|
3993
|
+
this.change("Outline Stroke", () => {
|
|
3994
|
+
let nodes = this.doc.nodes;
|
|
3995
|
+
for (const [id, list] of replace) nodes = spliceTree(nodes, id, list);
|
|
3996
|
+
this.setDoc(nodes);
|
|
3997
|
+
this.selection = created;
|
|
3998
|
+
this.anchors = [];
|
|
3999
|
+
this.editingId = null;
|
|
4000
|
+
});
|
|
4001
|
+
return created;
|
|
4002
|
+
}
|
|
2318
4003
|
rename(id, name) {
|
|
2319
4004
|
return this.updateNode(id, { name }, { label: "Rename" });
|
|
2320
4005
|
}
|
|
@@ -2753,12 +4438,47 @@ var VectorEditorController = class {
|
|
|
2753
4438
|
return translateAnchor(a, d.x, d.y);
|
|
2754
4439
|
});
|
|
2755
4440
|
}
|
|
2756
|
-
/**
|
|
2757
|
-
|
|
4441
|
+
/**
|
|
4442
|
+
* Moves one anchor to a document position. Its handles follow; with `curvature` (default:
|
|
4443
|
+
* the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.
|
|
4444
|
+
*/
|
|
4445
|
+
moveAnchor(ref, to, options = {}) {
|
|
2758
4446
|
const a = getAnchorWorld(this.doc, ref);
|
|
2759
4447
|
if (!a) return { ok: false, message: "There is no such anchor." };
|
|
4448
|
+
const curvature = options.curvature ?? this.editing.curvatureMoves;
|
|
2760
4449
|
this.change("Move Point", () => {
|
|
2761
|
-
this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
4450
|
+
this.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
4451
|
+
});
|
|
4452
|
+
return { ok: true };
|
|
4453
|
+
}
|
|
4454
|
+
/** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */
|
|
4455
|
+
curvatureMoved(doc, ref, to) {
|
|
4456
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
4457
|
+
if (node?.type !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
4458
|
+
const q = this.toLocal(ref.nodeId, to, doc);
|
|
4459
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp);
|
|
4460
|
+
const nodes = mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n);
|
|
4461
|
+
return { ...doc, nodes };
|
|
4462
|
+
}
|
|
4463
|
+
/** `doc` with segment `ref` bent through document point `p` at parameter `t`. */
|
|
4464
|
+
bent(doc, ref, t, p) {
|
|
4465
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
4466
|
+
if (node?.type !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
4467
|
+
const q = this.toLocal(ref.nodeId, p, doc);
|
|
4468
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? bendSegment(sp, ref.segment, t, q) : sp);
|
|
4469
|
+
return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n) };
|
|
4470
|
+
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Bends a segment so the curve passes through document point `to` at parameter `t`
|
|
4473
|
+
* (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.
|
|
4474
|
+
*/
|
|
4475
|
+
bendSegment(ref, to, t = 0.5) {
|
|
4476
|
+
const node = this.getNode(ref.nodeId);
|
|
4477
|
+
const sp = node?.type === "path" ? node.subpaths[ref.subpath] : void 0;
|
|
4478
|
+
if (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: "There is no such segment." };
|
|
4479
|
+
if (node.locked) return { ok: false, message: "The path is locked." };
|
|
4480
|
+
this.change("Bend Segment", () => {
|
|
4481
|
+
this.doc = this.bent(this.doc, ref, t, to);
|
|
2762
4482
|
});
|
|
2763
4483
|
return { ok: true };
|
|
2764
4484
|
}
|
|
@@ -2786,66 +4506,338 @@ var VectorEditorController = class {
|
|
|
2786
4506
|
});
|
|
2787
4507
|
return { ok: true };
|
|
2788
4508
|
}
|
|
2789
|
-
/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
|
|
2790
|
-
insertAnchorAt(nodeId, point) {
|
|
4509
|
+
/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
|
|
4510
|
+
insertAnchorAt(nodeId, point) {
|
|
4511
|
+
const node = this.getNode(nodeId);
|
|
4512
|
+
if (node?.type !== "path") return null;
|
|
4513
|
+
const hit = nearestSegment(node, this.toLocal(nodeId, point));
|
|
4514
|
+
if (!hit) return null;
|
|
4515
|
+
const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
|
|
4516
|
+
const ref = { nodeId, subpath: hit.subpath, index };
|
|
4517
|
+
this.change("Add Point", () => {
|
|
4518
|
+
this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
|
|
4519
|
+
this.editingId = nodeId;
|
|
4520
|
+
this.anchors = [ref];
|
|
4521
|
+
});
|
|
4522
|
+
return ref;
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go
|
|
4526
|
+
* too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'
|
|
4527
|
+
* handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.
|
|
4528
|
+
*/
|
|
4529
|
+
deleteAnchors(refs = this.anchors, options = {}) {
|
|
4530
|
+
if (!refs.length) return { ok: false, message: "Select points to delete." };
|
|
4531
|
+
const keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;
|
|
4532
|
+
this.change("Delete Points", () => {
|
|
4533
|
+
const nodes = mapTree(this.doc.nodes, (n) => {
|
|
4534
|
+
if (n.type !== "path" || n.locked) return n;
|
|
4535
|
+
const mine = refs.filter((r) => r.nodeId === n.id);
|
|
4536
|
+
if (!mine.length) return n;
|
|
4537
|
+
const subpaths = n.subpaths.map((sp, s) => {
|
|
4538
|
+
const indices = mine.filter((r) => r.subpath === s).map((r) => r.index);
|
|
4539
|
+
if (!indices.length) return sp;
|
|
4540
|
+
return keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);
|
|
4541
|
+
}).filter((sp) => sp.anchors.length > 1);
|
|
4542
|
+
return subpaths.length ? { ...n, subpaths } : null;
|
|
4543
|
+
});
|
|
4544
|
+
this.setDoc(nodes);
|
|
4545
|
+
this.anchors = [];
|
|
4546
|
+
this.selection = this.selection.filter((id) => this.getNode(id));
|
|
4547
|
+
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
4548
|
+
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
4549
|
+
});
|
|
4550
|
+
return { ok: true };
|
|
4551
|
+
}
|
|
4552
|
+
/** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
|
|
4553
|
+
setClosed(closed, nodeId) {
|
|
4554
|
+
const targets = nodeId ? (() => {
|
|
4555
|
+
const n = this.getNode(nodeId);
|
|
4556
|
+
return n?.type === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
|
|
4557
|
+
})() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
|
|
4558
|
+
if (!targets.length) return { ok: false, message: "Select a path or points." };
|
|
4559
|
+
this.change(closed ? "Close Path" : "Open Path", () => {
|
|
4560
|
+
this.setDoc(
|
|
4561
|
+
mapTree(
|
|
4562
|
+
this.doc.nodes,
|
|
4563
|
+
(n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
|
|
4564
|
+
)
|
|
4565
|
+
);
|
|
4566
|
+
});
|
|
4567
|
+
return { ok: true };
|
|
4568
|
+
}
|
|
4569
|
+
/** Reverses the direction of every subpath of the selected paths. */
|
|
4570
|
+
reversePaths(ids = this.selection) {
|
|
4571
|
+
const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
|
|
4572
|
+
if (!set.size) return { ok: false, message: "Select a path." };
|
|
4573
|
+
this.change("Reverse Path", () => {
|
|
4574
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
|
|
4575
|
+
this.anchors = [];
|
|
4576
|
+
});
|
|
4577
|
+
return { ok: true };
|
|
4578
|
+
}
|
|
4579
|
+
// -------------------------------------------------------------------------
|
|
4580
|
+
// Path clean-up, Scissors, Knife and Eraser
|
|
4581
|
+
// -------------------------------------------------------------------------
|
|
4582
|
+
/** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */
|
|
4583
|
+
pathTargets(ids) {
|
|
4584
|
+
const roots = ids ?? (this.tool === "direct" && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection);
|
|
4585
|
+
const out = /* @__PURE__ */ new Map();
|
|
4586
|
+
for (const id of roots) {
|
|
4587
|
+
const node = this.getNode(id);
|
|
4588
|
+
if (node && !node.locked) {
|
|
4589
|
+
for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
const order = [];
|
|
4593
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
4594
|
+
if (out.has(n.id)) order.push(n.id);
|
|
4595
|
+
});
|
|
4596
|
+
return order.map((id) => out.get(id));
|
|
4597
|
+
}
|
|
4598
|
+
/** Subpaths of node `from` mapped into node `to`'s own space. */
|
|
4599
|
+
subpathsIn(from, to) {
|
|
4600
|
+
const m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));
|
|
4601
|
+
if (isRoot(m)) return from.subpaths;
|
|
4602
|
+
const map = (p) => applyMatrix(m, p);
|
|
4603
|
+
return from.subpaths.map((sp) => ({
|
|
4604
|
+
closed: sp.closed,
|
|
4605
|
+
anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {}, ...a.corner ? { corner: { ...a.corner } } : {} }))
|
|
4606
|
+
}));
|
|
4607
|
+
}
|
|
4608
|
+
/**
|
|
4609
|
+
* Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and
|
|
4610
|
+
* place, the others go just above it with the same style). No pieces removes the path.
|
|
4611
|
+
* Returns the ids of the paths now standing in for it.
|
|
4612
|
+
*/
|
|
4613
|
+
replaceWithPieces(node, pieces) {
|
|
4614
|
+
const nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }));
|
|
4615
|
+
this.setDoc(spliceTree(this.doc.nodes, node.id, nodes));
|
|
4616
|
+
return nodes.map((n) => n.id);
|
|
4617
|
+
}
|
|
4618
|
+
/** Clears anchor selections, editing and pen state that no longer point at anything. */
|
|
4619
|
+
dropStaleRefs() {
|
|
4620
|
+
this.selection = this.selection.filter((id) => this.getNode(id));
|
|
4621
|
+
this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
|
|
4622
|
+
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
4623
|
+
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Join: the open subpaths of the selected (or given) paths become one open path, joined
|
|
4627
|
+
* nearest endpoints first (ends within `tolerance` document units merge into one anchor,
|
|
4628
|
+
* others get a straight segment); a single open subpath is closed. The result replaces the
|
|
4629
|
+
* lowest path, in its space and style. Default tolerance: the pick distance.
|
|
4630
|
+
*/
|
|
4631
|
+
joinPaths(options = {}) {
|
|
4632
|
+
const paths = this.pathTargets(options.ids);
|
|
4633
|
+
if (!paths.length) return { ok: false, message: "Select paths to join." };
|
|
4634
|
+
const base = paths[0];
|
|
4635
|
+
const all = paths.flatMap((p) => this.subpathsIn(p, base.id));
|
|
4636
|
+
if (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: "There are no open paths to join." };
|
|
4637
|
+
const subpaths = joinSubpaths(all, options.tolerance ?? this.tolerance());
|
|
4638
|
+
const drop = new Set(paths.slice(1).map((p) => p.id));
|
|
4639
|
+
this.change("Join", () => {
|
|
4640
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n));
|
|
4641
|
+
this.selection = [base.id];
|
|
4642
|
+
this.anchors = [];
|
|
4643
|
+
if (this.editingId && drop.has(this.editingId)) this.editingId = base.id;
|
|
4644
|
+
this.dropStaleRefs();
|
|
4645
|
+
});
|
|
4646
|
+
return { ok: true };
|
|
4647
|
+
}
|
|
4648
|
+
/** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */
|
|
4649
|
+
averageAnchors(axis = "both", refs = this.anchors) {
|
|
4650
|
+
const points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p) => !!p.at);
|
|
4651
|
+
if (points.length < 2) return { ok: false, message: "Select at least two points to average." };
|
|
4652
|
+
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
4653
|
+
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
4654
|
+
this.change("Average", () => {
|
|
4655
|
+
for (const { ref, at } of points) {
|
|
4656
|
+
const dx = axis === "horizontal" ? 0 : cx - at.x;
|
|
4657
|
+
const dy = axis === "vertical" ? 0 : cy - at.y;
|
|
4658
|
+
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
4659
|
+
}
|
|
4660
|
+
});
|
|
4661
|
+
return { ok: true };
|
|
4662
|
+
}
|
|
4663
|
+
/** Applies `fn` to every subpath of the target paths as one undo step. */
|
|
4664
|
+
editSubpaths(label, ids, fn) {
|
|
4665
|
+
const paths = this.pathTargets(ids);
|
|
4666
|
+
if (!paths.length) return { ok: false, message: "Select a path." };
|
|
4667
|
+
const set = new Set(paths.map((p) => p.id));
|
|
4668
|
+
this.change(label, () => {
|
|
4669
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n));
|
|
4670
|
+
this.dropStaleRefs();
|
|
4671
|
+
});
|
|
4672
|
+
return { ok: true };
|
|
4673
|
+
}
|
|
4674
|
+
/**
|
|
4675
|
+
* Remove Redundant Points: drops anchors of the selected (or given) paths that can go
|
|
4676
|
+
* without moving the outline more than `tolerance` document units (default 0.5).
|
|
4677
|
+
*/
|
|
4678
|
+
removeRedundantPoints(options = {}) {
|
|
4679
|
+
const tolerance = options.tolerance ?? 0.5;
|
|
4680
|
+
return this.editSubpaths("Remove Redundant Points", options.ids, (sp) => removeRedundantPoints(sp, tolerance));
|
|
4681
|
+
}
|
|
4682
|
+
/**
|
|
4683
|
+
* Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct
|
|
4684
|
+
* tool and points selected, only those points change.
|
|
4685
|
+
*/
|
|
4686
|
+
smoothPaths(options = {}) {
|
|
4687
|
+
const refs = options.refs ?? (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null);
|
|
4688
|
+
const ids = options.ids ?? (refs ? [...new Set(refs.map((r) => r.nodeId))] : void 0);
|
|
4689
|
+
return this.editSubpaths("Smooth", ids, (sp, node, i) => {
|
|
4690
|
+
const only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : void 0;
|
|
4691
|
+
return only && !only.size ? sp : smoothSubpath(sp, options.amount ?? 1, only);
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
/** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */
|
|
4695
|
+
applyCuts(node, cut) {
|
|
4696
|
+
const results = node.subpaths.map((sp, i) => cut(sp, i));
|
|
4697
|
+
if (results.every((r) => !r)) return null;
|
|
4698
|
+
const subpaths = node.subpaths.flatMap((sp, i) => results[i] ?? [sp]);
|
|
4699
|
+
if (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));
|
|
4700
|
+
this.replaceNode(node.id, { ...node, subpaths });
|
|
4701
|
+
return [node.id];
|
|
4702
|
+
}
|
|
4703
|
+
/**
|
|
4704
|
+
* Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an
|
|
4705
|
+
* anchor when within the pick distance of one). A closed subpath opens there; an open one
|
|
4706
|
+
* splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.
|
|
4707
|
+
*/
|
|
4708
|
+
cutPathAt(nodeId, point) {
|
|
2791
4709
|
const node = this.getNode(nodeId);
|
|
2792
|
-
if (node?.type !== "path") return null;
|
|
2793
|
-
const
|
|
4710
|
+
if (node?.type !== "path" || node.locked) return null;
|
|
4711
|
+
const local = this.toLocal(nodeId, point);
|
|
4712
|
+
const hit = nearestSegment(node, local);
|
|
2794
4713
|
if (!hit) return null;
|
|
2795
|
-
const
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
4714
|
+
const sp = node.subpaths[hit.subpath];
|
|
4715
|
+
const n = sp.anchors.length;
|
|
4716
|
+
const near2 = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());
|
|
4717
|
+
if (!sp.closed && n > 1 && (near2 === 0 || near2 === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
|
|
4718
|
+
let ids = null;
|
|
4719
|
+
this.change("Cut Path", () => {
|
|
4720
|
+
ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near2 !== void 0 ? cutSubpathAt(s, [near2]) : splitSubpathAt(s, hit.segment, hit.t));
|
|
4721
|
+
if (ids) {
|
|
4722
|
+
this.selection = ids;
|
|
4723
|
+
this.anchors = [];
|
|
4724
|
+
if (this.editingId === nodeId) this.editingId = null;
|
|
4725
|
+
}
|
|
4726
|
+
this.dropStaleRefs();
|
|
2801
4727
|
});
|
|
2802
|
-
return
|
|
4728
|
+
return ids;
|
|
2803
4729
|
}
|
|
2804
|
-
/**
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
this.
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
4730
|
+
/** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */
|
|
4731
|
+
cutAtAnchors(refs = this.anchors) {
|
|
4732
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4733
|
+
for (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [...byNode.get(r.nodeId) ?? [], r]);
|
|
4734
|
+
if (!byNode.size) return { ok: false, message: "Select points to cut at." };
|
|
4735
|
+
const created = [];
|
|
4736
|
+
this.change("Cut Path", () => {
|
|
4737
|
+
for (const [id, list] of byNode) {
|
|
4738
|
+
const node = this.getNode(id);
|
|
4739
|
+
if (node.locked) continue;
|
|
4740
|
+
const ids = this.applyCuts(node, (sp, i) => {
|
|
4741
|
+
const at = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
4742
|
+
const pieces = at.length ? cutSubpathAt(sp, at) : null;
|
|
4743
|
+
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4744
|
+
});
|
|
4745
|
+
if (ids) created.push(...ids);
|
|
4746
|
+
}
|
|
4747
|
+
if (created.length) this.selection = created;
|
|
2816
4748
|
this.anchors = [];
|
|
2817
|
-
this.
|
|
2818
|
-
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
2819
|
-
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
4749
|
+
this.dropStaleRefs();
|
|
2820
4750
|
});
|
|
2821
|
-
return { ok: true };
|
|
4751
|
+
return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
|
|
2822
4752
|
}
|
|
2823
|
-
/**
|
|
2824
|
-
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
})() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
|
|
2829
|
-
if (!targets.length) return { ok: false, message: "Select a path or points." };
|
|
2830
|
-
this.change(closed ? "Close Path" : "Open Path", () => {
|
|
2831
|
-
this.setDoc(
|
|
2832
|
-
mapTree(
|
|
2833
|
-
this.doc.nodes,
|
|
2834
|
-
(n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
|
|
2835
|
-
)
|
|
2836
|
-
);
|
|
4753
|
+
/** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */
|
|
4754
|
+
strokeTargets(points, ids, pad) {
|
|
4755
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
4756
|
+
walkVectorNodes(this.doc.nodes, (n, parent) => {
|
|
4757
|
+
if (n.visible === false || n.locked || parent && blocked.has(parent.id)) blocked.add(n.id);
|
|
2837
4758
|
});
|
|
2838
|
-
|
|
4759
|
+
const visible = (p) => !blocked.has(p.id);
|
|
4760
|
+
if (ids?.length || this.selection.length) return this.pathTargets(ids ?? this.selection).filter(visible);
|
|
4761
|
+
const box = polylineBounds(points.map(toPoint2), pad);
|
|
4762
|
+
if (!box) return [];
|
|
4763
|
+
const out = [];
|
|
4764
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
4765
|
+
if (n.type !== "path" || !visible(n)) return;
|
|
4766
|
+
const b = vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);
|
|
4767
|
+
if (b && boundsIntersect(b, box)) out.push(n);
|
|
4768
|
+
});
|
|
4769
|
+
return out;
|
|
2839
4770
|
}
|
|
2840
|
-
/**
|
|
2841
|
-
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
4771
|
+
/** A stroke's points in node `id`'s own space. */
|
|
4772
|
+
localPoints(id, points) {
|
|
4773
|
+
const inv = invertMatrix(this.worldOf(id));
|
|
4774
|
+
return points.map((p) => toPoint2(applyMatrix(inv, p)));
|
|
4775
|
+
}
|
|
4776
|
+
/**
|
|
4777
|
+
* Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses
|
|
4778
|
+
* them from edge to edge, with no gap between the pieces. It cuts the given paths, the
|
|
4779
|
+
* selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty
|
|
4780
|
+
* when nothing was cut).
|
|
4781
|
+
*/
|
|
4782
|
+
knife(points, options = {}) {
|
|
4783
|
+
const pts = simplifyPolyline(points.map(toPoint2), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));
|
|
4784
|
+
if (pts.length < 2) return [];
|
|
4785
|
+
const plan = [];
|
|
4786
|
+
for (const node of this.strokeTargets(pts, options.ids, 0)) {
|
|
4787
|
+
const closed = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4788
|
+
if (!closed.length || closed.some((sp) => !sp.closed)) continue;
|
|
4789
|
+
const result = knifePath(subpathsToBezier(closed), node.fillRule ?? "nonzero", this.localPoints(node.id, pts));
|
|
4790
|
+
if (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });
|
|
4791
|
+
}
|
|
4792
|
+
if (!plan.length) return [];
|
|
4793
|
+
const created = [];
|
|
4794
|
+
this.change("Knife", () => {
|
|
4795
|
+
for (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));
|
|
4796
|
+
this.selection = created;
|
|
2846
4797
|
this.anchors = [];
|
|
4798
|
+
this.dropStaleRefs();
|
|
2847
4799
|
});
|
|
2848
|
-
return
|
|
4800
|
+
return created;
|
|
4801
|
+
}
|
|
4802
|
+
/**
|
|
4803
|
+
* Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)
|
|
4804
|
+
* along `points` (document space). Filled closed paths lose that area (pieces that come
|
|
4805
|
+
* apart become separate paths); outlines are cut where they pass under it. It erases from
|
|
4806
|
+
* the given paths, the selection, or (with neither) every path it reaches. Returns the ids
|
|
4807
|
+
* of the paths it changed or created.
|
|
4808
|
+
*/
|
|
4809
|
+
erase(points, options = {}) {
|
|
4810
|
+
const width = options.size ?? this.editing.eraserSize;
|
|
4811
|
+
if (!(width > 0) || !points.length) return [];
|
|
4812
|
+
const pts = simplifyPolyline(points.map(toPoint2), Math.min(width * 0.05, 0.25 / this.viewport.zoom));
|
|
4813
|
+
const region = eraserRegion(pts, width);
|
|
4814
|
+
if (!region.length) return [];
|
|
4815
|
+
const plan = [];
|
|
4816
|
+
const rb = bezierPathBounds(region);
|
|
4817
|
+
const reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;
|
|
4818
|
+
for (const node of this.strokeTargets(reach, options.ids, 0)) {
|
|
4819
|
+
const local = transformBezierPath(region, invertMatrix(this.worldOf(node.id)));
|
|
4820
|
+
const live = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4821
|
+
if (!live.length) continue;
|
|
4822
|
+
if (hasPaint(node.fill) && live.every((sp) => sp.closed)) {
|
|
4823
|
+
const faces = eraseFromFill(subpathsToBezier(live), node.fillRule ?? "nonzero", local);
|
|
4824
|
+
if (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });
|
|
4825
|
+
continue;
|
|
4826
|
+
}
|
|
4827
|
+
const results = live.map((sp) => eraseFromOutline(sp, local));
|
|
4828
|
+
if (results.every((r) => !r)) continue;
|
|
4829
|
+
const subpaths = live.flatMap((sp, i) => results[i] ?? [sp]);
|
|
4830
|
+
plan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });
|
|
4831
|
+
}
|
|
4832
|
+
if (!plan.length) return [];
|
|
4833
|
+
const changed = [];
|
|
4834
|
+
this.change("Erase", () => {
|
|
4835
|
+
for (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));
|
|
4836
|
+
this.anchors = [];
|
|
4837
|
+
this.dropStaleRefs();
|
|
4838
|
+
if (this.selection.length || options.ids) this.selection = [.../* @__PURE__ */ new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];
|
|
4839
|
+
});
|
|
4840
|
+
return changed;
|
|
2849
4841
|
}
|
|
2850
4842
|
/** Ends the pen path being drawn (a lone anchor is removed). */
|
|
2851
4843
|
finishPath() {
|
|
@@ -2965,6 +4957,22 @@ var VectorEditorController = class {
|
|
|
2965
4957
|
}
|
|
2966
4958
|
return null;
|
|
2967
4959
|
}
|
|
4960
|
+
cornerView() {
|
|
4961
|
+
return { ...this.view(), viewport: this.viewport };
|
|
4962
|
+
}
|
|
4963
|
+
hitCornerWidget(screen) {
|
|
4964
|
+
let best = null;
|
|
4965
|
+
let bestD = Infinity;
|
|
4966
|
+
for (const w of getVectorCornerWidgets(this.cornerView())) {
|
|
4967
|
+
const s = this.documentToScreen(w);
|
|
4968
|
+
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
4969
|
+
if (d <= this.hitRadius && d < bestD) {
|
|
4970
|
+
best = w;
|
|
4971
|
+
bestD = d;
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
return best;
|
|
4975
|
+
}
|
|
2968
4976
|
hitAnchor(screen, refs) {
|
|
2969
4977
|
let best = null;
|
|
2970
4978
|
let bestD = Infinity;
|
|
@@ -3023,6 +5031,8 @@ var VectorEditorController = class {
|
|
|
3023
5031
|
if (handle) return { nodeId: handle.nodeId, handle };
|
|
3024
5032
|
const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
|
|
3025
5033
|
if (anchor) return { nodeId: anchor.nodeId, anchor };
|
|
5034
|
+
const widget = this.hitCornerWidget(screen);
|
|
5035
|
+
if (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };
|
|
3026
5036
|
for (const id of getEditablePathIds(this.view())) {
|
|
3027
5037
|
const node = this.getNode(id);
|
|
3028
5038
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -3039,10 +5049,38 @@ var VectorEditorController = class {
|
|
|
3039
5049
|
const anchor = this.hitAnchor(screen, refs);
|
|
3040
5050
|
return anchor ? { nodeId: anchor.nodeId, anchor } : null;
|
|
3041
5051
|
}
|
|
5052
|
+
case "scissors": {
|
|
5053
|
+
const seg = this.nearestOutline(p);
|
|
5054
|
+
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
5055
|
+
}
|
|
3042
5056
|
default:
|
|
3043
5057
|
return null;
|
|
3044
5058
|
}
|
|
3045
5059
|
}
|
|
5060
|
+
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
5061
|
+
nearestOutline(p) {
|
|
5062
|
+
let best = null;
|
|
5063
|
+
const tol = this.tolerance();
|
|
5064
|
+
const visit = (list, parent) => {
|
|
5065
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
5066
|
+
const n = list[i];
|
|
5067
|
+
if (n.visible === false || n.locked) continue;
|
|
5068
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
5069
|
+
if (n.type === "group") {
|
|
5070
|
+
visit(n.children, m);
|
|
5071
|
+
continue;
|
|
5072
|
+
}
|
|
5073
|
+
const hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));
|
|
5074
|
+
if (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {
|
|
5075
|
+
best = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
};
|
|
5079
|
+
visit(this.doc.nodes, IDENTITY_MATRIX);
|
|
5080
|
+
if (!best) return null;
|
|
5081
|
+
const { distance: _d, ...seg } = best;
|
|
5082
|
+
return seg;
|
|
5083
|
+
}
|
|
3046
5084
|
sameHover(a, b) {
|
|
3047
5085
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
3048
5086
|
}
|
|
@@ -3067,6 +5105,17 @@ var VectorEditorController = class {
|
|
|
3067
5105
|
return this.downPen(screen, p, input);
|
|
3068
5106
|
case "pencil":
|
|
3069
5107
|
return this.downPencil(p);
|
|
5108
|
+
case "scissors": {
|
|
5109
|
+
const seg = this.nearestOutline(p);
|
|
5110
|
+
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
case "knife":
|
|
5114
|
+
case "eraser":
|
|
5115
|
+
this.drag = { kind: "stroke", tool: this.tool, points: [p], before: this.snapshot() };
|
|
5116
|
+
this.interaction = { type: "stroke", tool: this.tool, points: [p], width: this.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
5117
|
+
this.notify();
|
|
5118
|
+
return;
|
|
3070
5119
|
default:
|
|
3071
5120
|
return this.downShape(p, input);
|
|
3072
5121
|
}
|
|
@@ -3141,6 +5190,13 @@ var VectorEditorController = class {
|
|
|
3141
5190
|
this.notify();
|
|
3142
5191
|
return;
|
|
3143
5192
|
}
|
|
5193
|
+
const widget = this.hitCornerWidget(screen);
|
|
5194
|
+
if (widget) {
|
|
5195
|
+
const refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);
|
|
5196
|
+
this.drag = { kind: "corner", widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };
|
|
5197
|
+
this.notify();
|
|
5198
|
+
return;
|
|
5199
|
+
}
|
|
3144
5200
|
for (const id of getEditablePathIds(this.view())) {
|
|
3145
5201
|
const node = this.getNode(id);
|
|
3146
5202
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -3161,7 +5217,7 @@ var VectorEditorController = class {
|
|
|
3161
5217
|
const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
|
|
3162
5218
|
this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
|
|
3163
5219
|
this.editingId = id;
|
|
3164
|
-
this.drag = { kind: "
|
|
5220
|
+
this.drag = { kind: "bend", ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };
|
|
3165
5221
|
this.notify();
|
|
3166
5222
|
return;
|
|
3167
5223
|
}
|
|
@@ -3198,10 +5254,10 @@ var VectorEditorController = class {
|
|
|
3198
5254
|
}
|
|
3199
5255
|
const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
|
|
3200
5256
|
const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
|
|
3201
|
-
let
|
|
3202
|
-
|
|
5257
|
+
let pt3 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
|
|
5258
|
+
pt3 = this.toLocal(pen.nodeId, pt3);
|
|
3203
5259
|
const node2 = this.getNode(pen.nodeId);
|
|
3204
|
-
const anchors = [...sp.anchors, { x:
|
|
5260
|
+
const anchors = [...sp.anchors, { x: pt3.x, y: pt3.y }];
|
|
3205
5261
|
this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
|
|
3206
5262
|
const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
|
|
3207
5263
|
this.anchors = [ref2];
|
|
@@ -3229,8 +5285,8 @@ var VectorEditorController = class {
|
|
|
3229
5285
|
this.notify(this.doc !== before.doc);
|
|
3230
5286
|
return;
|
|
3231
5287
|
}
|
|
3232
|
-
const
|
|
3233
|
-
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x:
|
|
5288
|
+
const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
5289
|
+
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
|
|
3234
5290
|
this.setDoc([...this.doc.nodes, node]);
|
|
3235
5291
|
this.pen = { nodeId: node.id, subpath: 0 };
|
|
3236
5292
|
this.selection = [node.id];
|
|
@@ -3253,7 +5309,9 @@ var VectorEditorController = class {
|
|
|
3253
5309
|
subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
|
|
3254
5310
|
};
|
|
3255
5311
|
this.setDoc([...this.doc.nodes, node]);
|
|
3256
|
-
|
|
5312
|
+
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
5313
|
+
lazy.push([p.x, p.y]);
|
|
5314
|
+
this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
|
|
3257
5315
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
3258
5316
|
this.notify(true);
|
|
3259
5317
|
}
|
|
@@ -3365,7 +5423,10 @@ var VectorEditorController = class {
|
|
|
3365
5423
|
dx = q.x - lead.x;
|
|
3366
5424
|
dy = q.y - lead.y;
|
|
3367
5425
|
} else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
|
|
3368
|
-
|
|
5426
|
+
const single = this.anchors.length === 1 ? this.anchors[0] : null;
|
|
5427
|
+
const from = single ? getAnchorWorld(drag.base, single) : void 0;
|
|
5428
|
+
if (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });
|
|
5429
|
+
else this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
|
|
3369
5430
|
this.interaction = { type: "anchors", dx, dy };
|
|
3370
5431
|
this.notify(true);
|
|
3371
5432
|
return;
|
|
@@ -3373,13 +5434,41 @@ var VectorEditorController = class {
|
|
|
3373
5434
|
case "handle": {
|
|
3374
5435
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
3375
5436
|
const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
|
|
3376
|
-
const
|
|
3377
|
-
const local = this.toLocal(drag.ref.nodeId,
|
|
5437
|
+
const pt2 = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
|
|
5438
|
+
const local = this.toLocal(drag.ref.nodeId, pt2, drag.base);
|
|
3378
5439
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
|
|
3379
5440
|
this.interaction = { type: "handle", ref: drag.ref };
|
|
3380
5441
|
this.notify(true);
|
|
3381
5442
|
return;
|
|
3382
5443
|
}
|
|
5444
|
+
case "corner": {
|
|
5445
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;
|
|
5446
|
+
drag.moved = true;
|
|
5447
|
+
const c = drag.widget.corner;
|
|
5448
|
+
const inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));
|
|
5449
|
+
const d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });
|
|
5450
|
+
const radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));
|
|
5451
|
+
this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));
|
|
5452
|
+
this.interaction = { type: "corner", ref: drag.widget.ref, radius };
|
|
5453
|
+
this.notify(true);
|
|
5454
|
+
return;
|
|
5455
|
+
}
|
|
5456
|
+
case "bend": {
|
|
5457
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
5458
|
+
drag.moved = true;
|
|
5459
|
+
this.doc = this.bent(drag.base, drag.ref, drag.t, p);
|
|
5460
|
+
this.interaction = { type: "bend", ref: drag.ref };
|
|
5461
|
+
this.notify(true);
|
|
5462
|
+
return;
|
|
5463
|
+
}
|
|
5464
|
+
case "stroke": {
|
|
5465
|
+
const last = drag.points[drag.points.length - 1];
|
|
5466
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
5467
|
+
drag.points.push(p);
|
|
5468
|
+
this.interaction = { type: "stroke", tool: drag.tool, points: drag.points.slice(), width: drag.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
5469
|
+
this.notify();
|
|
5470
|
+
return;
|
|
5471
|
+
}
|
|
3383
5472
|
case "pull": {
|
|
3384
5473
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
3385
5474
|
if (!anchor) return;
|
|
@@ -3389,10 +5478,10 @@ var VectorEditorController = class {
|
|
|
3389
5478
|
return;
|
|
3390
5479
|
}
|
|
3391
5480
|
this.guides = null;
|
|
3392
|
-
let
|
|
3393
|
-
if (input.shift)
|
|
5481
|
+
let pt2 = p;
|
|
5482
|
+
if (input.shift) pt2 = constrain45(anchor, pt2);
|
|
3394
5483
|
const side = drag.closing ? "in" : "out";
|
|
3395
|
-
const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x -
|
|
5484
|
+
const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt2.x, y: 2 * anchor.y - pt2.y } : pt2, drag.base);
|
|
3396
5485
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
|
|
3397
5486
|
if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
|
|
3398
5487
|
return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
|
|
@@ -3433,10 +5522,22 @@ var VectorEditorController = class {
|
|
|
3433
5522
|
}
|
|
3434
5523
|
case "pencil": {
|
|
3435
5524
|
const last = drag.points[drag.points.length - 1];
|
|
3436
|
-
|
|
3437
|
-
drag.
|
|
5525
|
+
let moved = false;
|
|
5526
|
+
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
5527
|
+
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
5528
|
+
drag.points.push(q);
|
|
5529
|
+
moved = true;
|
|
5530
|
+
}
|
|
5531
|
+
const string = drag.lazy.string();
|
|
3438
5532
|
const node = this.getNode(drag.nodeId);
|
|
3439
5533
|
if (node?.type !== "path") return;
|
|
5534
|
+
if (!moved) {
|
|
5535
|
+
if (string && this.interaction?.type === "draw") {
|
|
5536
|
+
this.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };
|
|
5537
|
+
this.notify();
|
|
5538
|
+
}
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
3440
5541
|
this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
|
|
3441
5542
|
const xs = drag.points.map((q) => q[0]);
|
|
3442
5543
|
const ys = drag.points.map((q) => q[1]);
|
|
@@ -3444,7 +5545,8 @@ var VectorEditorController = class {
|
|
|
3444
5545
|
type: "draw",
|
|
3445
5546
|
tool: "pencil",
|
|
3446
5547
|
nodeId: drag.nodeId,
|
|
3447
|
-
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }
|
|
5548
|
+
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },
|
|
5549
|
+
...string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] } : {}
|
|
3448
5550
|
};
|
|
3449
5551
|
this.notify(true);
|
|
3450
5552
|
return;
|
|
@@ -3492,12 +5594,12 @@ var VectorEditorController = class {
|
|
|
3492
5594
|
const guides = { lines: [], labels: [] };
|
|
3493
5595
|
for (const axis of ["x", "y"]) {
|
|
3494
5596
|
const d = end[axis] - start[axis];
|
|
3495
|
-
const
|
|
3496
|
-
if (
|
|
3497
|
-
out[axis] = start[axis] + Math.sign(d) *
|
|
5597
|
+
const len2 = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
|
|
5598
|
+
if (len2 === null || len2 <= 0) continue;
|
|
5599
|
+
out[axis] = start[axis] + Math.sign(d) * len2 / k;
|
|
3498
5600
|
const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
|
|
3499
5601
|
guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
|
|
3500
|
-
guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(
|
|
5602
|
+
guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(len2 * 100) / 100}` });
|
|
3501
5603
|
}
|
|
3502
5604
|
this.setGuides(guides);
|
|
3503
5605
|
return out;
|
|
@@ -3536,6 +5638,32 @@ var VectorEditorController = class {
|
|
|
3536
5638
|
return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
|
|
3537
5639
|
case "handle":
|
|
3538
5640
|
return this.commit("Move Handle", drag.before);
|
|
5641
|
+
case "bend":
|
|
5642
|
+
if (!drag.moved) {
|
|
5643
|
+
this.notify();
|
|
5644
|
+
return;
|
|
5645
|
+
}
|
|
5646
|
+
return this.commit("Bend Segment", drag.before);
|
|
5647
|
+
case "corner": {
|
|
5648
|
+
if (drag.moved) return this.commit("Corner Radius", drag.before);
|
|
5649
|
+
if (drag.alt) {
|
|
5650
|
+
const kind = nextVectorCornerKind(drag.widget.kind);
|
|
5651
|
+
this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
|
|
5652
|
+
return this.commit("Corner Kind", drag.before);
|
|
5653
|
+
}
|
|
5654
|
+
const ref = drag.widget.ref;
|
|
5655
|
+
const has = this.anchors.some((a) => sameRef(a, ref));
|
|
5656
|
+
this.editingId = ref.nodeId;
|
|
5657
|
+
this.anchors = drag.shift ? has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref] : [ref];
|
|
5658
|
+
this.notify();
|
|
5659
|
+
return;
|
|
5660
|
+
}
|
|
5661
|
+
case "stroke": {
|
|
5662
|
+
this.notify();
|
|
5663
|
+
if (drag.tool === "knife") this.knife(drag.points);
|
|
5664
|
+
else this.erase(drag.points);
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
3539
5667
|
case "pull":
|
|
3540
5668
|
if (drag.closing) this.pen = null;
|
|
3541
5669
|
return this.commit(drag.label, drag.before);
|
|
@@ -3560,6 +5688,13 @@ var VectorEditorController = class {
|
|
|
3560
5688
|
this.notify(true);
|
|
3561
5689
|
return;
|
|
3562
5690
|
}
|
|
5691
|
+
for (const q of drag.lazy.finish()) drag.points.push(q);
|
|
5692
|
+
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
5693
|
+
if (shape) {
|
|
5694
|
+
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
5695
|
+
this.selection = [drag.nodeId];
|
|
5696
|
+
return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
|
|
5697
|
+
}
|
|
3563
5698
|
const fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
|
|
3564
5699
|
const anchors = [];
|
|
3565
5700
|
for (const c of fitted) {
|
|
@@ -3666,6 +5801,8 @@ var VectorEditorController = class {
|
|
|
3666
5801
|
case "g":
|
|
3667
5802
|
input.shift ? this.ungroup() : this.group();
|
|
3668
5803
|
return true;
|
|
5804
|
+
case "j":
|
|
5805
|
+
return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
|
|
3669
5806
|
case "]":
|
|
3670
5807
|
this.arrange(input.alt ? "front" : "forward");
|
|
3671
5808
|
return true;
|
|
@@ -3746,12 +5883,28 @@ var VectorEditorController = class {
|
|
|
3746
5883
|
}
|
|
3747
5884
|
}
|
|
3748
5885
|
};
|
|
5886
|
+
function inkShapeLabel(shape) {
|
|
5887
|
+
const name = vectorInkShapeName(shape);
|
|
5888
|
+
return name[0].toUpperCase() + name.slice(1);
|
|
5889
|
+
}
|
|
5890
|
+
function inkShapeSubpath(shape) {
|
|
5891
|
+
switch (shape.kind) {
|
|
5892
|
+
case "line":
|
|
5893
|
+
return lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });
|
|
5894
|
+
case "rect":
|
|
5895
|
+
return rectSubpath(shape);
|
|
5896
|
+
case "ellipse":
|
|
5897
|
+
return ellipseSubpath(shape);
|
|
5898
|
+
case "polygon":
|
|
5899
|
+
return { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
3749
5902
|
function constrain45(origin, p) {
|
|
3750
5903
|
const dx = p.x - origin.x;
|
|
3751
5904
|
const dy = p.y - origin.y;
|
|
3752
|
-
const
|
|
5905
|
+
const len2 = Math.hypot(dx, dy);
|
|
3753
5906
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
3754
|
-
return { x: origin.x + Math.cos(a) *
|
|
5907
|
+
return { x: origin.x + Math.cos(a) * len2, y: origin.y + Math.sin(a) * len2 };
|
|
3755
5908
|
}
|
|
3756
5909
|
function scaleFor(handle, b, p, mods = {}) {
|
|
3757
5910
|
const west = handle.includes("w");
|
|
@@ -3788,10 +5941,12 @@ function renderNode(node, defs, idPrefix) {
|
|
|
3788
5941
|
const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
|
|
3789
5942
|
if (fill.gradient) defs.push(fill.gradient);
|
|
3790
5943
|
if (stroke.gradient) defs.push(stroke.gradient);
|
|
5944
|
+
const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
|
|
3791
5945
|
return {
|
|
3792
5946
|
kind: "path",
|
|
3793
5947
|
id: node.id,
|
|
3794
5948
|
...node.transform ? { transform: fmtMatrix(node.transform) } : {},
|
|
5949
|
+
...strokeParts ? { strokeParts } : {},
|
|
3795
5950
|
d: vectorPathData(node),
|
|
3796
5951
|
fill: fill.value,
|
|
3797
5952
|
stroke: stroke.value,
|
|
@@ -3807,9 +5962,9 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
3807
5962
|
const defs = [];
|
|
3808
5963
|
const prefix = options.idPrefix ?? "ve-";
|
|
3809
5964
|
const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
|
|
3810
|
-
const
|
|
5965
|
+
const v2 = state.viewport;
|
|
3811
5966
|
return {
|
|
3812
|
-
transform: `translate(${fmt(
|
|
5967
|
+
transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
|
|
3813
5968
|
width: state.document.width,
|
|
3814
5969
|
height: state.document.height,
|
|
3815
5970
|
...state.document.background ? { background: state.document.background } : {},
|
|
@@ -3819,8 +5974,8 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
3819
5974
|
}
|
|
3820
5975
|
var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
|
|
3821
5976
|
function screenOutline(state, node) {
|
|
3822
|
-
const
|
|
3823
|
-
const view = [
|
|
5977
|
+
const v2 = state.viewport;
|
|
5978
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
3824
5979
|
const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
|
|
3825
5980
|
return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
|
|
3826
5981
|
}
|
|
@@ -3917,7 +6072,26 @@ function getVectorOverlayRenderData(state) {
|
|
|
3917
6072
|
guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
|
|
3918
6073
|
}
|
|
3919
6074
|
}
|
|
3920
|
-
|
|
6075
|
+
let toolStroke = null;
|
|
6076
|
+
if (it?.type === "stroke") {
|
|
6077
|
+
const pts = it.points.map((q) => toScreen(state, q));
|
|
6078
|
+
const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
|
|
6079
|
+
toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
|
|
6080
|
+
}
|
|
6081
|
+
let pencilString = null;
|
|
6082
|
+
if (it?.type === "draw" && it.string) {
|
|
6083
|
+
const a = toScreen(state, it.string[0]);
|
|
6084
|
+
const b = toScreen(state, it.string[1]);
|
|
6085
|
+
pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
|
|
6086
|
+
}
|
|
6087
|
+
const dragged = it?.type === "corner" ? it.ref : null;
|
|
6088
|
+
const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
|
|
6089
|
+
const s = toScreen(state, w);
|
|
6090
|
+
return { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };
|
|
6091
|
+
});
|
|
6092
|
+
if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
|
|
6093
|
+
const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
|
|
6094
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets };
|
|
3921
6095
|
}
|
|
3922
6096
|
var VECTOR_OVERLAY_THEME = {
|
|
3923
6097
|
accent: "#0ea5e9",
|
|
@@ -3925,9 +6099,13 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
3925
6099
|
guide: "#ff3dfc",
|
|
3926
6100
|
handleFill: "#ffffff",
|
|
3927
6101
|
marqueeFill: "rgba(14, 165, 233, 0.08)",
|
|
6102
|
+
/** The eraser's swath while it is dragged. */
|
|
6103
|
+
eraserFill: "rgba(239, 68, 68, 0.25)",
|
|
3928
6104
|
transformHandleSize: 8,
|
|
3929
6105
|
anchorSize: 7,
|
|
3930
|
-
handleRadius: 3.5
|
|
6106
|
+
handleRadius: 3.5,
|
|
6107
|
+
/** Radius of a live corner widget (a ring with a dot). */
|
|
6108
|
+
cornerWidgetRadius: 4
|
|
3931
6109
|
};
|
|
3932
6110
|
function toVectorModifiers(event) {
|
|
3933
6111
|
return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
|
|
@@ -3951,18 +6129,68 @@ function getVectorSelectionStyle(state) {
|
|
|
3951
6129
|
const node = findVectorNode(state.document.nodes, id);
|
|
3952
6130
|
if (node) paths.push(...collect(node));
|
|
3953
6131
|
}
|
|
6132
|
+
const anchorMode = state.tool === "direct" && !!state.selectedAnchors?.length;
|
|
6133
|
+
if (anchorMode) {
|
|
6134
|
+
for (const r of state.selectedAnchors) {
|
|
6135
|
+
if (paths.some((p) => p.id === r.nodeId)) continue;
|
|
6136
|
+
const node = findVectorNode(state.document.nodes, r.nodeId);
|
|
6137
|
+
if (node?.type === "path") paths.push(node);
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
3954
6140
|
if (!paths.length) return null;
|
|
3955
6141
|
const first = paths[0];
|
|
3956
6142
|
const same = (get) => paths.every((p) => JSON.stringify(get(p) ?? null) === JSON.stringify(get(first) ?? null));
|
|
6143
|
+
const corners2 = [];
|
|
6144
|
+
for (const p of paths) {
|
|
6145
|
+
for (const c of vectorCorners(p.subpaths)) {
|
|
6146
|
+
if (anchorMode && !state.selectedAnchors.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;
|
|
6147
|
+
const corner = p.subpaths[c.subpath].anchors[c.index].corner;
|
|
6148
|
+
corners2.push({ radius: corner?.radius ?? 0, kind: corner?.kind ?? "round" });
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
const c0 = corners2[0] ?? { radius: 0, kind: "round" };
|
|
3957
6152
|
return {
|
|
3958
6153
|
fill: first.fill ?? null,
|
|
3959
6154
|
stroke: first.stroke ?? null,
|
|
3960
6155
|
strokeWidth: first.strokeWidth ?? 1,
|
|
3961
6156
|
opacity: first.opacity ?? 1,
|
|
3962
|
-
|
|
6157
|
+
strokeAlign: first.strokeAlign ?? "center",
|
|
6158
|
+
startArrow: first.startArrow ?? null,
|
|
6159
|
+
endArrow: first.endArrow ?? null,
|
|
6160
|
+
arrowAlign: first.arrowAlign ?? "extend",
|
|
6161
|
+
startArrowScale: first.startArrowScale ?? 1,
|
|
6162
|
+
endArrowScale: first.endArrowScale ?? 1,
|
|
6163
|
+
cornerRadius: c0.radius,
|
|
6164
|
+
cornerKind: c0.kind,
|
|
6165
|
+
corners: corners2.length,
|
|
6166
|
+
hasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),
|
|
6167
|
+
mixed: {
|
|
6168
|
+
fill: !same((p) => p.fill),
|
|
6169
|
+
stroke: !same((p) => p.stroke),
|
|
6170
|
+
strokeWidth: !same((p) => p.strokeWidth ?? 1),
|
|
6171
|
+
opacity: !same((p) => p.opacity ?? 1),
|
|
6172
|
+
strokeAlign: !same((p) => p.strokeAlign ?? "center"),
|
|
6173
|
+
startArrow: !same((p) => p.startArrow ?? null),
|
|
6174
|
+
endArrow: !same((p) => p.endArrow ?? null),
|
|
6175
|
+
arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
|
|
6176
|
+
startArrowScale: !same((p) => p.startArrowScale ?? 1),
|
|
6177
|
+
endArrowScale: !same((p) => p.endArrowScale ?? 1),
|
|
6178
|
+
cornerRadius: corners2.some((c) => c.radius !== c0.radius),
|
|
6179
|
+
cornerKind: corners2.some((c) => c.kind !== c0.kind)
|
|
6180
|
+
},
|
|
3963
6181
|
pathIds: paths.map((p) => p.id)
|
|
3964
6182
|
};
|
|
3965
6183
|
}
|
|
6184
|
+
var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
|
|
6185
|
+
var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
|
|
6186
|
+
var VECTOR_ARROWHEAD_LABELS = {
|
|
6187
|
+
triangle: "Triangle",
|
|
6188
|
+
arrow: "Arrow",
|
|
6189
|
+
open: "Open arrow",
|
|
6190
|
+
circle: "Circle",
|
|
6191
|
+
square: "Square",
|
|
6192
|
+
bar: "Bar"
|
|
6193
|
+
};
|
|
3966
6194
|
function vectorPaintToHex(paint) {
|
|
3967
6195
|
if (typeof paint !== "string") return null;
|
|
3968
6196
|
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
|
|
@@ -3970,12 +6198,40 @@ function vectorPaintToHex(paint) {
|
|
|
3970
6198
|
const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
3971
6199
|
return `#${hex.toLowerCase()}`;
|
|
3972
6200
|
}
|
|
6201
|
+
var VECTOR_PATH_ACTIONS = [
|
|
6202
|
+
{ id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
|
|
6203
|
+
{ id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
|
|
6204
|
+
{ id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
|
|
6205
|
+
{ id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
|
|
6206
|
+
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
|
|
6207
|
+
{ id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
|
|
6208
|
+
];
|
|
6209
|
+
var VECTOR_EDIT_TOGGLES = [
|
|
6210
|
+
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
6211
|
+
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
6212
|
+
{ key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
|
|
6213
|
+
];
|
|
3973
6214
|
|
|
3974
6215
|
// src/vector/commands.ts
|
|
3975
6216
|
var fail = (message) => {
|
|
3976
6217
|
throw new Error(message);
|
|
3977
6218
|
};
|
|
3978
6219
|
var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
|
|
6220
|
+
var ANCHORS = {
|
|
6221
|
+
type: "array",
|
|
6222
|
+
items: {
|
|
6223
|
+
type: "object",
|
|
6224
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 } },
|
|
6225
|
+
required: ["nodeId", "subpath", "index"]
|
|
6226
|
+
},
|
|
6227
|
+
description: "Anchors as { nodeId, subpath, index } (default: the selected points)."
|
|
6228
|
+
};
|
|
6229
|
+
var POINTS = {
|
|
6230
|
+
type: "array",
|
|
6231
|
+
items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
|
|
6232
|
+
minItems: 1,
|
|
6233
|
+
description: "Polyline [[x, y], \u2026] in document units."
|
|
6234
|
+
};
|
|
3979
6235
|
var PAINT = {
|
|
3980
6236
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
3981
6237
|
};
|
|
@@ -3986,7 +6242,13 @@ var STYLE_PROPS = {
|
|
|
3986
6242
|
strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
|
|
3987
6243
|
strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
|
|
3988
6244
|
strokeDasharray: { type: "array", items: { type: "number" } },
|
|
3989
|
-
fillRule: { type: "string", enum: ["nonzero", "evenodd"] }
|
|
6245
|
+
fillRule: { type: "string", enum: ["nonzero", "evenodd"] },
|
|
6246
|
+
strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred)." },
|
|
6247
|
+
startArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the start of an open path, or null for none." },
|
|
6248
|
+
endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
|
|
6249
|
+
arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
|
|
6250
|
+
startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
|
|
6251
|
+
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." }
|
|
3990
6252
|
};
|
|
3991
6253
|
var pickStyle = (params) => {
|
|
3992
6254
|
const out = {};
|
|
@@ -4008,6 +6270,14 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4008
6270
|
return list2;
|
|
4009
6271
|
};
|
|
4010
6272
|
const enabledFor = (_, params) => params?.ids?.length ? true : hasSelection();
|
|
6273
|
+
const anchorsOf = (refs) => {
|
|
6274
|
+
if (!refs) return [...controller.getState().selectedAnchors];
|
|
6275
|
+
for (const r of refs) {
|
|
6276
|
+
const node = controller.getNode(r.nodeId);
|
|
6277
|
+
if (node?.type !== "path" || !node.subpaths[r.subpath]?.anchors[r.index]) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of "${r.nodeId}".`);
|
|
6278
|
+
}
|
|
6279
|
+
return refs;
|
|
6280
|
+
};
|
|
4011
6281
|
const list = () => {
|
|
4012
6282
|
const state = controller.getState();
|
|
4013
6283
|
const out = [];
|
|
@@ -4071,7 +6341,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4071
6341
|
y: { type: "number" },
|
|
4072
6342
|
width: { type: "number", description: "For a line: the x offset of its end." },
|
|
4073
6343
|
height: { type: "number", description: "For a line: the y offset of its end." },
|
|
4074
|
-
cornerRadius: { type: "number", minimum: 0 },
|
|
6344
|
+
cornerRadius: { type: "number", minimum: 0, description: "Live corner radius of a rect, polygon or star." },
|
|
6345
|
+
cornerKind: { type: "string", enum: VECTOR_CORNER_KINDS },
|
|
4075
6346
|
sides: { type: "integer", minimum: 3 },
|
|
4076
6347
|
points: { type: "integer", minimum: 3 },
|
|
4077
6348
|
innerRatio: { type: "number", minimum: 0, maximum: 1 },
|
|
@@ -4117,6 +6388,38 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4117
6388
|
return { ok: true };
|
|
4118
6389
|
}
|
|
4119
6390
|
},
|
|
6391
|
+
{
|
|
6392
|
+
...base,
|
|
6393
|
+
id: `${prefix}setCorners`,
|
|
6394
|
+
label: "Corner Radius\u2026",
|
|
6395
|
+
keywords: ["round", "rounded", "corners", "radius", "fillet", "chamfer", "live corners"],
|
|
6396
|
+
description: "Sets the live corner radius (and optionally `kind`: round, inverted or chamfer) of `anchors`, else the selected points (direct tool), else every corner of `ids` / the selection. Radius 0 makes corners sharp; radii shrink to fit short sides. The anchors stay editable.",
|
|
6397
|
+
params: { type: "object", properties: { radius: { type: "number", minimum: 0 }, kind: { type: "string", enum: VECTOR_CORNER_KINDS }, ids: IDS, anchors: ANCHORS } },
|
|
6398
|
+
undoable: true,
|
|
6399
|
+
run: (_ctx, params) => {
|
|
6400
|
+
const refs = params?.anchors ? anchorsOf(params.anchors) : void 0;
|
|
6401
|
+
const target = { refs, ids: params?.ids ? ids(params) : void 0 };
|
|
6402
|
+
if (params?.radius !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));
|
|
6403
|
+
else if (params?.kind) check(controller.setCornerKind(params.kind, target));
|
|
6404
|
+
else fail("Pass `radius` and / or `kind`.");
|
|
6405
|
+
return { ok: true };
|
|
6406
|
+
}
|
|
6407
|
+
},
|
|
6408
|
+
{
|
|
6409
|
+
...base,
|
|
6410
|
+
id: `${prefix}outlineStroke`,
|
|
6411
|
+
label: "Outline Stroke",
|
|
6412
|
+
keywords: ["expand", "stroke to path", "outline", "convert stroke"],
|
|
6413
|
+
description: "Turns the stroke of paths into filled shapes of the area it paints (alignment, caps, joins, dashes and arrowheads included). Returns the new ids.",
|
|
6414
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
6415
|
+
enabled: enabledFor,
|
|
6416
|
+
undoable: true,
|
|
6417
|
+
run: (_ctx, params) => {
|
|
6418
|
+
const created = controller.outlineStroke({ ids: params?.ids ? ids(params) : void 0 });
|
|
6419
|
+
if (!created.length) fail("No selected path has a stroke.");
|
|
6420
|
+
return { ids: created };
|
|
6421
|
+
}
|
|
6422
|
+
},
|
|
4120
6423
|
{
|
|
4121
6424
|
...base,
|
|
4122
6425
|
id: `${prefix}transform`,
|
|
@@ -4246,6 +6549,186 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4246
6549
|
return { selection: controller.getState().selection };
|
|
4247
6550
|
}
|
|
4248
6551
|
},
|
|
6552
|
+
{
|
|
6553
|
+
...base,
|
|
6554
|
+
id: `${prefix}deleteAnchors`,
|
|
6555
|
+
label: "Delete Points",
|
|
6556
|
+
keywords: ["remove", "anchor", "point", "node"],
|
|
6557
|
+
description: "Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.",
|
|
6558
|
+
params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
|
|
6559
|
+
undoable: true,
|
|
6560
|
+
run: (_ctx, params) => {
|
|
6561
|
+
check(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));
|
|
6562
|
+
return { ok: true };
|
|
6563
|
+
}
|
|
6564
|
+
},
|
|
6565
|
+
{
|
|
6566
|
+
...base,
|
|
6567
|
+
id: `${prefix}moveAnchor`,
|
|
6568
|
+
label: "Move Point\u2026",
|
|
6569
|
+
keywords: ["anchor", "drag", "curvature"],
|
|
6570
|
+
description: "Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.",
|
|
6571
|
+
params: {
|
|
6572
|
+
type: "object",
|
|
6573
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 }, x: { type: "number" }, y: { type: "number" }, curvature: { type: "boolean" } },
|
|
6574
|
+
required: ["nodeId", "subpath", "index", "x", "y"]
|
|
6575
|
+
},
|
|
6576
|
+
undoable: true,
|
|
6577
|
+
run: (_ctx, params) => {
|
|
6578
|
+
check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
|
|
6579
|
+
return { ok: true };
|
|
6580
|
+
}
|
|
6581
|
+
},
|
|
6582
|
+
{
|
|
6583
|
+
...base,
|
|
6584
|
+
id: `${prefix}bendSegment`,
|
|
6585
|
+
label: "Bend Segment\u2026",
|
|
6586
|
+
keywords: ["curve", "segment", "drag"],
|
|
6587
|
+
description: "Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).",
|
|
6588
|
+
params: {
|
|
6589
|
+
type: "object",
|
|
6590
|
+
properties: {
|
|
6591
|
+
nodeId: { type: "string" },
|
|
6592
|
+
subpath: { type: "integer", minimum: 0 },
|
|
6593
|
+
segment: { type: "integer", minimum: 0 },
|
|
6594
|
+
x: { type: "number" },
|
|
6595
|
+
y: { type: "number" },
|
|
6596
|
+
t: { type: "number", minimum: 0, maximum: 1 }
|
|
6597
|
+
},
|
|
6598
|
+
required: ["nodeId", "subpath", "segment", "x", "y"]
|
|
6599
|
+
},
|
|
6600
|
+
undoable: true,
|
|
6601
|
+
run: (_ctx, params) => {
|
|
6602
|
+
check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
|
|
6603
|
+
return { ok: true };
|
|
6604
|
+
}
|
|
6605
|
+
},
|
|
6606
|
+
{
|
|
6607
|
+
...base,
|
|
6608
|
+
id: `${prefix}join`,
|
|
6609
|
+
label: "Join",
|
|
6610
|
+
shortcut: "Mod+J",
|
|
6611
|
+
keywords: ["connect", "close", "merge", "endpoints"],
|
|
6612
|
+
description: "Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.",
|
|
6613
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
6614
|
+
undoable: true,
|
|
6615
|
+
run: (_ctx, params) => {
|
|
6616
|
+
check(controller.joinPaths({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
|
|
6617
|
+
return { selection: controller.getState().selection };
|
|
6618
|
+
}
|
|
6619
|
+
},
|
|
6620
|
+
{
|
|
6621
|
+
...base,
|
|
6622
|
+
id: `${prefix}average`,
|
|
6623
|
+
label: "Average Points\u2026",
|
|
6624
|
+
shortcut: "Mod+Alt+J",
|
|
6625
|
+
keywords: ["align", "points", "anchors", "collapse"],
|
|
6626
|
+
description: "Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).",
|
|
6627
|
+
params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
|
|
6628
|
+
undoable: true,
|
|
6629
|
+
run: (_ctx, params) => {
|
|
6630
|
+
check(controller.averageAnchors(params?.axis ?? "both", anchorsOf(params?.anchors)));
|
|
6631
|
+
return { ok: true };
|
|
6632
|
+
}
|
|
6633
|
+
},
|
|
6634
|
+
{
|
|
6635
|
+
...base,
|
|
6636
|
+
id: `${prefix}removeRedundantPoints`,
|
|
6637
|
+
label: "Remove Redundant Points",
|
|
6638
|
+
keywords: ["simplify", "clean up", "anchors", "collinear"],
|
|
6639
|
+
description: "Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).",
|
|
6640
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
6641
|
+
enabled: enabledFor,
|
|
6642
|
+
undoable: true,
|
|
6643
|
+
run: (_ctx, params) => {
|
|
6644
|
+
check(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
|
|
6645
|
+
return { ok: true };
|
|
6646
|
+
}
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
...base,
|
|
6650
|
+
id: `${prefix}smooth`,
|
|
6651
|
+
label: "Smooth Path",
|
|
6652
|
+
keywords: ["round", "soften", "handles", "curves"],
|
|
6653
|
+
description: "Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0\u20131, default 1).",
|
|
6654
|
+
params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
|
|
6655
|
+
undoable: true,
|
|
6656
|
+
run: (_ctx, params) => {
|
|
6657
|
+
check(controller.smoothPaths({ ids: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.anchors }));
|
|
6658
|
+
return { ok: true };
|
|
6659
|
+
}
|
|
6660
|
+
},
|
|
6661
|
+
{
|
|
6662
|
+
...base,
|
|
6663
|
+
id: `${prefix}cut`,
|
|
6664
|
+
label: "Cut Path (Scissors)\u2026",
|
|
6665
|
+
keywords: ["scissors", "split", "open", "break"],
|
|
6666
|
+
description: "Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.",
|
|
6667
|
+
params: { type: "object", properties: { nodeId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, anchors: ANCHORS } },
|
|
6668
|
+
undoable: true,
|
|
6669
|
+
run: (_ctx, params) => {
|
|
6670
|
+
if (params?.nodeId !== void 0) {
|
|
6671
|
+
if (!controller.getNode(params.nodeId)) fail(`There is no node "${params.nodeId}".`);
|
|
6672
|
+
if (params.x === void 0 || params.y === void 0) fail("Pass x and y with nodeId.");
|
|
6673
|
+
const created = controller.cutPathAt(params.nodeId, { x: params.x, y: params.y });
|
|
6674
|
+
if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
|
|
6675
|
+
return { ids: created };
|
|
6676
|
+
}
|
|
6677
|
+
check(controller.cutAtAnchors(anchorsOf(params?.anchors)));
|
|
6678
|
+
return { ids: controller.getState().selection };
|
|
6679
|
+
}
|
|
6680
|
+
},
|
|
6681
|
+
{
|
|
6682
|
+
...base,
|
|
6683
|
+
id: `${prefix}knife`,
|
|
6684
|
+
label: "Knife\u2026",
|
|
6685
|
+
keywords: ["slice", "cut", "split", "divide"],
|
|
6686
|
+
description: "Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.",
|
|
6687
|
+
params: { type: "object", properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ["points"] },
|
|
6688
|
+
undoable: true,
|
|
6689
|
+
run: (_ctx, params) => {
|
|
6690
|
+
const created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : void 0 });
|
|
6691
|
+
if (!created.length) fail("The knife did not cross any closed path from edge to edge.");
|
|
6692
|
+
return { ids: created };
|
|
6693
|
+
}
|
|
6694
|
+
},
|
|
6695
|
+
{
|
|
6696
|
+
...base,
|
|
6697
|
+
id: `${prefix}erase`,
|
|
6698
|
+
label: "Erase\u2026",
|
|
6699
|
+
keywords: ["eraser", "rub out", "remove"],
|
|
6700
|
+
description: "Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.",
|
|
6701
|
+
params: { type: "object", properties: { points: POINTS, size: { type: "number", exclusiveMinimum: 0 }, ids: IDS }, required: ["points"] },
|
|
6702
|
+
undoable: true,
|
|
6703
|
+
run: (_ctx, params) => {
|
|
6704
|
+
const changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : void 0 });
|
|
6705
|
+
if (!changed.length) fail("The eraser did not touch any path.");
|
|
6706
|
+
return { ids: changed.filter((id) => controller.getNode(id)) };
|
|
6707
|
+
}
|
|
6708
|
+
},
|
|
6709
|
+
{
|
|
6710
|
+
...base,
|
|
6711
|
+
id: `${prefix}setEditing`,
|
|
6712
|
+
label: "Path Editing Options\u2026",
|
|
6713
|
+
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
6714
|
+
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
|
|
6715
|
+
params: {
|
|
6716
|
+
type: "object",
|
|
6717
|
+
properties: {
|
|
6718
|
+
keepShapeOnDelete: { type: "boolean" },
|
|
6719
|
+
curvatureMoves: { type: "boolean" },
|
|
6720
|
+
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
6721
|
+
recognizeShapes: { type: "boolean" },
|
|
6722
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
6723
|
+
}
|
|
6724
|
+
},
|
|
6725
|
+
run: (_ctx, params) => {
|
|
6726
|
+
const patch = {};
|
|
6727
|
+
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
6728
|
+
controller.setEditing(patch);
|
|
6729
|
+
return { editing: controller.getState().editing };
|
|
6730
|
+
}
|
|
6731
|
+
},
|
|
4249
6732
|
{
|
|
4250
6733
|
...base,
|
|
4251
6734
|
id: `${prefix}setTool`,
|
|
@@ -4284,13 +6767,26 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4284
6767
|
];
|
|
4285
6768
|
}
|
|
4286
6769
|
export {
|
|
6770
|
+
DEFAULT_VECTOR_PATH_EDIT,
|
|
4287
6771
|
DEFAULT_VECTOR_SNAP,
|
|
4288
6772
|
IDENTITY_MATRIX,
|
|
4289
|
-
KAPPA,
|
|
6773
|
+
KAPPA2 as KAPPA,
|
|
6774
|
+
LazyMouse,
|
|
6775
|
+
VECTOR_ARROWHEADS,
|
|
6776
|
+
VECTOR_ARROWHEAD_LABELS,
|
|
4290
6777
|
VECTOR_CLIPBOARD_TYPE,
|
|
4291
6778
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
6779
|
+
VECTOR_CORNER_KINDS,
|
|
6780
|
+
VECTOR_CORNER_KIND_LABELS,
|
|
6781
|
+
VECTOR_CORNER_WIDGET_INSET,
|
|
6782
|
+
VECTOR_EDIT_TOGGLES,
|
|
6783
|
+
VECTOR_INK_MAX_CORNERS,
|
|
4292
6784
|
VECTOR_OVERLAY_THEME,
|
|
6785
|
+
VECTOR_PATH_ACTIONS,
|
|
6786
|
+
VECTOR_PENCIL_STRING_PX,
|
|
4293
6787
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
6788
|
+
VECTOR_STROKE_ALIGN_LABELS,
|
|
6789
|
+
VECTOR_STYLE_KEYS,
|
|
4294
6790
|
VECTOR_TOOLS,
|
|
4295
6791
|
VECTOR_TOOL_GLYPHS,
|
|
4296
6792
|
VECTOR_TOOL_SHORTCUTS,
|
|
@@ -4299,6 +6795,11 @@ export {
|
|
|
4299
6795
|
VectorSnapTargets,
|
|
4300
6796
|
applyMatrix,
|
|
4301
6797
|
applyMatrixLinear,
|
|
6798
|
+
applyVectorCornersAttribute,
|
|
6799
|
+
arrowheadInset,
|
|
6800
|
+
arrowheadPath,
|
|
6801
|
+
arrowheadWeight,
|
|
6802
|
+
bendSegment,
|
|
4302
6803
|
bezierToSubpaths,
|
|
4303
6804
|
boundsFromPoints,
|
|
4304
6805
|
boundsIntersect,
|
|
@@ -4309,14 +6810,27 @@ export {
|
|
|
4309
6810
|
collectPathNodes,
|
|
4310
6811
|
constrainToLeave,
|
|
4311
6812
|
convertAnchor,
|
|
6813
|
+
cornerBisectorPoint,
|
|
6814
|
+
cornerRadiusChange,
|
|
6815
|
+
cornerSetback,
|
|
4312
6816
|
createVectorCommands,
|
|
6817
|
+
curvatureMoveAnchor,
|
|
6818
|
+
cutSubpathAt,
|
|
4313
6819
|
decomposeRigid,
|
|
6820
|
+
effectiveStrokeAlign,
|
|
4314
6821
|
ellipseSubpath,
|
|
6822
|
+
eraseFromFill,
|
|
6823
|
+
eraseFromOutline,
|
|
6824
|
+
eraserRegion,
|
|
6825
|
+
expandVectorCorners,
|
|
6826
|
+
expandVectorCornersOf,
|
|
4315
6827
|
findVectorNode,
|
|
6828
|
+
fitCornerRadius,
|
|
4316
6829
|
getAnchor,
|
|
4317
6830
|
getAnchorWorld,
|
|
4318
6831
|
getEditablePathIds,
|
|
4319
6832
|
getTransformHandlePositions,
|
|
6833
|
+
getVectorCornerWidgets,
|
|
4320
6834
|
getVectorOverlayRenderData,
|
|
4321
6835
|
getVectorSceneRenderData,
|
|
4322
6836
|
getVectorSelectionBoxGeometry,
|
|
@@ -4325,31 +6839,50 @@ export {
|
|
|
4325
6839
|
getVisibleAnchors,
|
|
4326
6840
|
getVisibleHandles,
|
|
4327
6841
|
hasPaint,
|
|
6842
|
+
hasStrokeExtras,
|
|
6843
|
+
hasVectorCorners,
|
|
4328
6844
|
hitTestVectorNode,
|
|
4329
6845
|
hitTestVectorNodes,
|
|
6846
|
+
hitTestVectorStroke,
|
|
4330
6847
|
inferAnchorType,
|
|
4331
6848
|
insertAnchor,
|
|
4332
6849
|
invertMatrix,
|
|
6850
|
+
isClosedBezierShape,
|
|
4333
6851
|
isIdentityMatrix,
|
|
4334
6852
|
isVectorTextTarget,
|
|
6853
|
+
joinSubpaths,
|
|
6854
|
+
knifePath,
|
|
4335
6855
|
lineSubpath,
|
|
4336
6856
|
locateVectorNode,
|
|
4337
6857
|
matrixRotation,
|
|
4338
6858
|
moveAnchorHandle,
|
|
4339
6859
|
multiplyMatrices,
|
|
4340
6860
|
nearestSegment,
|
|
6861
|
+
nextVectorCornerKind,
|
|
4341
6862
|
nodeMatrix,
|
|
4342
6863
|
parseSvgTransform,
|
|
4343
6864
|
parseVectorSvg,
|
|
4344
6865
|
polygonSubpath,
|
|
6866
|
+
polylineBounds,
|
|
6867
|
+
recognizeInkShape,
|
|
4345
6868
|
rectSubpath,
|
|
6869
|
+
removeAnchorKeepingShape,
|
|
4346
6870
|
removeAnchors,
|
|
6871
|
+
removeAnchorsKeepingShape,
|
|
6872
|
+
removeRedundantPoints,
|
|
4347
6873
|
reverseSubpath,
|
|
4348
6874
|
rotateMatrix,
|
|
4349
6875
|
roundSubpaths,
|
|
4350
6876
|
scaleFor,
|
|
4351
6877
|
scaleMatrix,
|
|
6878
|
+
setAnchorCorner,
|
|
6879
|
+
setSubpathCorners,
|
|
6880
|
+
simplifyPolyline,
|
|
6881
|
+
smoothSubpath,
|
|
6882
|
+
splitIntoFaces,
|
|
6883
|
+
splitSubpathAt,
|
|
4352
6884
|
starSubpath,
|
|
6885
|
+
subpathPointAt,
|
|
4353
6886
|
subpathSegment,
|
|
4354
6887
|
subpathSegmentCount,
|
|
4355
6888
|
subpathsToBezier,
|
|
@@ -4360,7 +6893,11 @@ export {
|
|
|
4360
6893
|
translateAnchor,
|
|
4361
6894
|
translateMatrix,
|
|
4362
6895
|
unionBounds,
|
|
6896
|
+
vectorCornerAt,
|
|
6897
|
+
vectorCorners,
|
|
6898
|
+
vectorCornersAttribute,
|
|
4363
6899
|
vectorDocumentToSvg,
|
|
6900
|
+
vectorInkShapeName,
|
|
4364
6901
|
vectorNodeBounds,
|
|
4365
6902
|
vectorNodeRotation,
|
|
4366
6903
|
vectorNodeToBezier,
|
|
@@ -4370,6 +6907,11 @@ export {
|
|
|
4370
6907
|
vectorPathData,
|
|
4371
6908
|
vectorPathWorldBezier,
|
|
4372
6909
|
vectorResizeCursor,
|
|
6910
|
+
vectorStrokeOutline,
|
|
6911
|
+
vectorStrokeOutlinePath,
|
|
6912
|
+
vectorStrokeParts,
|
|
6913
|
+
vectorStrokeReach,
|
|
6914
|
+
vectorStrokeSvgParts,
|
|
4373
6915
|
vectorToolLabel,
|
|
4374
6916
|
vectorWorldMatrix,
|
|
4375
6917
|
walkVectorNodes
|