@pixerate/editor 0.35.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 +7 -0
- package/dist/vector/index.cjs +1118 -261
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +326 -20
- package/dist/vector/index.d.ts +326 -20
- package/dist/vector/index.js +1032 -175
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
package/dist/vector/index.js
CHANGED
|
@@ -20,27 +20,444 @@ import {
|
|
|
20
20
|
segmentBounds,
|
|
21
21
|
segmentDerivative,
|
|
22
22
|
segmentIntersections,
|
|
23
|
+
segmentLength,
|
|
23
24
|
segmentNearest,
|
|
25
|
+
segmentParamAtLength,
|
|
24
26
|
segmentPoint,
|
|
27
|
+
segmentTangent,
|
|
25
28
|
splitSegment,
|
|
26
29
|
subSegment,
|
|
27
30
|
toBezierPath,
|
|
28
31
|
transformBezierPath,
|
|
32
|
+
trimSegments,
|
|
29
33
|
windingNumber
|
|
30
34
|
} from "../chunk-EGPIVUEF.js";
|
|
31
35
|
|
|
32
36
|
// src/vector/types.ts
|
|
37
|
+
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
38
|
+
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
33
39
|
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
|
|
34
40
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
35
41
|
|
|
36
|
-
// src/vector/
|
|
42
|
+
// src/vector/corners.ts
|
|
37
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;
|
|
38
454
|
var cloneAnchor = (a) => ({
|
|
39
455
|
x: a.x,
|
|
40
456
|
y: a.y,
|
|
41
457
|
...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
|
|
42
458
|
...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
|
|
43
|
-
...a.type && a.type !== "corner" ? { type: a.type } : {}
|
|
459
|
+
...a.type && a.type !== "corner" ? { type: a.type } : {},
|
|
460
|
+
...a.corner ? { corner: { ...a.corner } } : {}
|
|
44
461
|
});
|
|
45
462
|
var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
|
|
46
463
|
function cloneVectorNode(node) {
|
|
@@ -58,7 +475,7 @@ function hasPaint(paint) {
|
|
|
58
475
|
return true;
|
|
59
476
|
}
|
|
60
477
|
var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
|
|
61
|
-
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;
|
|
62
479
|
function subpathSegmentCount(sp) {
|
|
63
480
|
const n = sp.anchors.length;
|
|
64
481
|
if (n < 2) return 0;
|
|
@@ -72,9 +489,9 @@ function subpathSegment(sp, i) {
|
|
|
72
489
|
const c2 = handleOf(b, "in");
|
|
73
490
|
return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
|
|
74
491
|
}
|
|
75
|
-
function subpathsToBezier(subpaths) {
|
|
492
|
+
function subpathsToBezier(subpaths, options = {}) {
|
|
76
493
|
const out = [];
|
|
77
|
-
for (const sp of subpaths) {
|
|
494
|
+
for (const sp of options.corners === false ? subpaths : expandVectorCorners(subpaths)) {
|
|
78
495
|
if (sp.anchors.length === 0) continue;
|
|
79
496
|
const first = sp.anchors[0];
|
|
80
497
|
out.push({ type: "M", x: first.x, y: first.y });
|
|
@@ -104,9 +521,9 @@ function inferAnchorType(a) {
|
|
|
104
521
|
const oy = a.out.y - a.y;
|
|
105
522
|
const li = Math.hypot(ix, iy);
|
|
106
523
|
const lo = Math.hypot(ox, oy);
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
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";
|
|
110
527
|
return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
|
|
111
528
|
}
|
|
112
529
|
function bezierToSubpaths(input) {
|
|
@@ -249,8 +666,12 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
|
249
666
|
if (path.length === 0) return false;
|
|
250
667
|
const point = [p.x, p.y];
|
|
251
668
|
if (hasPaint(node.fill) && pointInBezierPath(path, point, node.fillRule ?? "nonzero")) return true;
|
|
252
|
-
|
|
253
|
-
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);
|
|
254
675
|
}
|
|
255
676
|
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
256
677
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -268,8 +689,8 @@ function nearestSegment(node, p) {
|
|
|
268
689
|
const seg = subpathSegment(sp, i);
|
|
269
690
|
const hit = segmentNearest(seg, [p.x, p.y]);
|
|
270
691
|
if (!best || hit.distance < best.distance) {
|
|
271
|
-
const
|
|
272
|
-
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 };
|
|
273
694
|
}
|
|
274
695
|
}
|
|
275
696
|
});
|
|
@@ -303,8 +724,8 @@ function invertMatrix(m) {
|
|
|
303
724
|
if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
|
|
304
725
|
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
305
726
|
}
|
|
306
|
-
function applyMatrixLinear(m,
|
|
307
|
-
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 };
|
|
308
729
|
}
|
|
309
730
|
function matrixRotation(m) {
|
|
310
731
|
const [a, b, c, d] = m;
|
|
@@ -338,6 +759,7 @@ function bakeIntoGeometry(node, m) {
|
|
|
338
759
|
if (a.in) next.in = applyMatrix(m, a.in);
|
|
339
760
|
if (a.out) next.out = applyMatrix(m, a.out);
|
|
340
761
|
if (a.type) next.type = a.type;
|
|
762
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
341
763
|
return next;
|
|
342
764
|
})
|
|
343
765
|
}))
|
|
@@ -389,12 +811,12 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
|
|
|
389
811
|
if (breakHandles) delete next.type;
|
|
390
812
|
const dx = pos.x - a.x;
|
|
391
813
|
const dy = pos.y - a.y;
|
|
392
|
-
const
|
|
814
|
+
const len2 = Math.hypot(dx, dy);
|
|
393
815
|
if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
|
|
394
|
-
else if (type === "smooth" &&
|
|
816
|
+
else if (type === "smooth" && len2 > EPS2) {
|
|
395
817
|
const o = a[other];
|
|
396
|
-
const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) :
|
|
397
|
-
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 };
|
|
398
820
|
}
|
|
399
821
|
if (isOnAnchor(next, next.in)) delete next.in;
|
|
400
822
|
if (isOnAnchor(next, next.out)) delete next.out;
|
|
@@ -421,15 +843,15 @@ function convertAnchor(sp, index, type) {
|
|
|
421
843
|
else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
|
|
422
844
|
else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
|
|
423
845
|
else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
|
|
424
|
-
if (!dir || Math.hypot(dir.x, dir.y) <
|
|
846
|
+
if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
|
|
425
847
|
const dl = Math.hypot(dir.x, dir.y);
|
|
426
848
|
const ux = dir.x / dl;
|
|
427
849
|
const uy = dir.y / dl;
|
|
428
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;
|
|
429
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;
|
|
430
852
|
if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
|
|
431
|
-
if (lenIn >
|
|
432
|
-
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 };
|
|
433
855
|
a.type = type;
|
|
434
856
|
return { anchors, closed: sp.closed };
|
|
435
857
|
}
|
|
@@ -465,37 +887,24 @@ function reverseSubpath(sp) {
|
|
|
465
887
|
if (a.out) next.in = { ...a.out };
|
|
466
888
|
if (a.in) next.out = { ...a.in };
|
|
467
889
|
if (a.type) next.type = a.type;
|
|
890
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
468
891
|
return next;
|
|
469
892
|
});
|
|
470
893
|
return { anchors, closed: sp.closed };
|
|
471
894
|
}
|
|
472
|
-
var
|
|
473
|
-
function rectSubpath(b, radius = 0) {
|
|
895
|
+
var KAPPA2 = 0.5522847498307936;
|
|
896
|
+
function rectSubpath(b, radius = 0, kind) {
|
|
474
897
|
const { x, y, width: w, height: h } = b;
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
return { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
|
|
478
|
-
}
|
|
479
|
-
const k = r * KAPPA;
|
|
480
|
-
const anchors = [
|
|
481
|
-
{ x: x + r, y, in: { x: x + r - k, y } },
|
|
482
|
-
{ x: x + w - r, y, out: { x: x + w - r + k, y } },
|
|
483
|
-
{ x: x + w, y: y + r, in: { x: x + w, y: y + r - k } },
|
|
484
|
-
{ x: x + w, y: y + h - r, out: { x: x + w, y: y + h - r + k } },
|
|
485
|
-
{ x: x + w - r, y: y + h, in: { x: x + w - r + k, y: y + h } },
|
|
486
|
-
{ x: x + r, y: y + h, out: { x: x + r - k, y: y + h } },
|
|
487
|
-
{ x, y: y + h - r, in: { x, y: y + h - r + k } },
|
|
488
|
-
{ x, y: y + r, out: { x, y: y + r - k } }
|
|
489
|
-
];
|
|
490
|
-
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;
|
|
491
900
|
}
|
|
492
901
|
function ellipseSubpath(b) {
|
|
493
902
|
const rx = b.width / 2;
|
|
494
903
|
const ry = b.height / 2;
|
|
495
904
|
const cx = b.x + rx;
|
|
496
905
|
const cy = b.y + ry;
|
|
497
|
-
const kx = rx *
|
|
498
|
-
const ky = ry *
|
|
906
|
+
const kx = rx * KAPPA2;
|
|
907
|
+
const ky = ry * KAPPA2;
|
|
499
908
|
return {
|
|
500
909
|
closed: true,
|
|
501
910
|
anchors: [
|
|
@@ -543,15 +952,52 @@ function roundSubpaths(subpaths, precision = 3) {
|
|
|
543
952
|
}
|
|
544
953
|
|
|
545
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
|
+
}
|
|
546
977
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
547
978
|
function vectorPaintToSvg(paint, gradientId) {
|
|
548
979
|
if (!hasPaint(paint)) return { value: "none", gradient: null };
|
|
549
980
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
550
981
|
return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
|
|
551
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;
|
|
552
998
|
function gradientMarkup(def) {
|
|
553
999
|
const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
|
|
554
|
-
const attrs = Object.entries(def.attrs).map(([k,
|
|
1000
|
+
const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
|
|
555
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("");
|
|
556
1002
|
return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
|
|
557
1003
|
}
|
|
@@ -582,15 +1028,53 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
582
1028
|
const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
|
|
583
1029
|
if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
|
|
584
1030
|
if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (node.
|
|
592
|
-
|
|
593
|
-
|
|
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>`);
|
|
594
1078
|
};
|
|
595
1079
|
for (const node of doc.nodes) emit(node, 1);
|
|
596
1080
|
const w = num(doc.width);
|
|
@@ -765,11 +1249,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
765
1249
|
const rawGradients = /* @__PURE__ */ new Map();
|
|
766
1250
|
collectGradients(root, rawGradients);
|
|
767
1251
|
const paint = (value) => {
|
|
768
|
-
const
|
|
769
|
-
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(
|
|
1252
|
+
const v2 = value.trim();
|
|
1253
|
+
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v2);
|
|
770
1254
|
if (url) return resolveGradient(url[1], rawGradients) ?? null;
|
|
771
|
-
if (
|
|
772
|
-
return
|
|
1255
|
+
if (v2 === "none" || v2 === "") return null;
|
|
1256
|
+
return v2;
|
|
773
1257
|
};
|
|
774
1258
|
const viewBox = numberList(root.attrs.viewBox);
|
|
775
1259
|
const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
|
|
@@ -779,8 +1263,13 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
779
1263
|
const shapeOf = (el) => {
|
|
780
1264
|
const a = el.attrs;
|
|
781
1265
|
switch (el.name) {
|
|
782
|
-
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
|
+
}
|
|
783
1271
|
return a.d ? bezierToSubpaths(a.d) : null;
|
|
1272
|
+
}
|
|
784
1273
|
case "rect": {
|
|
785
1274
|
const w = length(a.width);
|
|
786
1275
|
const h = length(a.height);
|
|
@@ -814,6 +1303,56 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
814
1303
|
}
|
|
815
1304
|
return null;
|
|
816
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
|
+
};
|
|
817
1356
|
const visit = (el, style, matrix) => {
|
|
818
1357
|
const out = [];
|
|
819
1358
|
for (const child of el.children) {
|
|
@@ -827,6 +1366,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
827
1366
|
...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
|
|
828
1367
|
...hidden ? { visible: false } : {}
|
|
829
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
|
+
}
|
|
830
1374
|
if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
|
|
831
1375
|
const children = visit(child, s, m);
|
|
832
1376
|
if (!children.length) continue;
|
|
@@ -836,24 +1380,18 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
836
1380
|
}
|
|
837
1381
|
const subpaths = shapeOf(child);
|
|
838
1382
|
if (!subpaths || !subpaths.length) continue;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
};
|
|
852
|
-
if (m !== IDENTITY_MATRIX) {
|
|
853
|
-
node = transformVectorNode(node, m);
|
|
854
|
-
const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
855
|
-
if (hasPaint(node.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) node.strokeWidth = (node.strokeWidth ?? 1) * strokeScale;
|
|
856
|
-
}
|
|
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
|
+
);
|
|
857
1395
|
out.push(node);
|
|
858
1396
|
}
|
|
859
1397
|
return out;
|
|
@@ -876,9 +1414,9 @@ function constrainToLeave(leave, p) {
|
|
|
876
1414
|
return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
|
|
877
1415
|
}
|
|
878
1416
|
default: {
|
|
879
|
-
const
|
|
1417
|
+
const len2 = Math.hypot(dx, dy);
|
|
880
1418
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
881
|
-
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 };
|
|
882
1420
|
}
|
|
883
1421
|
}
|
|
884
1422
|
}
|
|
@@ -887,24 +1425,24 @@ var CROSSING_PAIRS = 32;
|
|
|
887
1425
|
var CURVE_SAMPLES = 48;
|
|
888
1426
|
var BISECT = 50;
|
|
889
1427
|
var EQUAL = 1e-6;
|
|
890
|
-
function bound(sorted, key,
|
|
1428
|
+
function bound(sorted, key, v2, strict) {
|
|
891
1429
|
let lo = 0;
|
|
892
1430
|
let hi = sorted.length;
|
|
893
1431
|
while (lo < hi) {
|
|
894
1432
|
const mid = lo + hi >>> 1;
|
|
895
1433
|
const k = key(sorted[mid]);
|
|
896
|
-
if (strict ? k <=
|
|
1434
|
+
if (strict ? k <= v2 : k < v2) lo = mid + 1;
|
|
897
1435
|
else hi = mid;
|
|
898
1436
|
}
|
|
899
1437
|
return lo;
|
|
900
1438
|
}
|
|
901
|
-
function within(sorted, key,
|
|
902
|
-
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));
|
|
903
1441
|
}
|
|
904
|
-
var linesNear = (lines,
|
|
1442
|
+
var linesNear = (lines, v2, tol) => within(lines, (l) => l.v, v2, tol);
|
|
905
1443
|
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
906
1444
|
var toPoint = (p) => [p.x, p.y];
|
|
907
|
-
var
|
|
1445
|
+
var toVec2 = (p) => ({ x: p[0], y: p[1] });
|
|
908
1446
|
var LABELS = {
|
|
909
1447
|
anchor: "anchor",
|
|
910
1448
|
centre: "centre",
|
|
@@ -1022,7 +1560,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1022
1560
|
const count = subpathSegmentCount(sp);
|
|
1023
1561
|
for (let i = 0; i < count && budget > 0; i++) {
|
|
1024
1562
|
if (moving(i) || moving((i + 1) % n)) continue;
|
|
1025
|
-
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m,
|
|
1563
|
+
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec2(q))));
|
|
1026
1564
|
let minX = Infinity;
|
|
1027
1565
|
let minY = Infinity;
|
|
1028
1566
|
let maxX = -Infinity;
|
|
@@ -1166,9 +1704,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1166
1704
|
return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
|
|
1167
1705
|
}
|
|
1168
1706
|
}
|
|
1169
|
-
const nearest = (lines2,
|
|
1707
|
+
const nearest = (lines2, v2) => {
|
|
1170
1708
|
let best = null;
|
|
1171
|
-
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;
|
|
1172
1710
|
return best;
|
|
1173
1711
|
};
|
|
1174
1712
|
const bx = nearest(this.xs, p.x);
|
|
@@ -1228,16 +1766,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1228
1766
|
/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
|
|
1229
1767
|
segmentsNear(p, tol) {
|
|
1230
1768
|
const out = [];
|
|
1231
|
-
const
|
|
1769
|
+
const pt2 = toPoint(p);
|
|
1232
1770
|
for (const s of this.segments) {
|
|
1233
1771
|
if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
|
|
1234
|
-
const hit = segmentNearest(s.seg,
|
|
1772
|
+
const hit = segmentNearest(s.seg, pt2);
|
|
1235
1773
|
if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
|
|
1236
1774
|
}
|
|
1237
1775
|
return out.sort((a, b) => a.distance - b.distance);
|
|
1238
1776
|
}
|
|
1239
1777
|
onSegment(near2) {
|
|
1240
|
-
return near2.length ?
|
|
1778
|
+
return near2.length ? toVec2(segmentPoint(near2[0].seg, near2[0].t)) : null;
|
|
1241
1779
|
}
|
|
1242
1780
|
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1243
1781
|
crossing(near2, p, tol) {
|
|
@@ -1247,7 +1785,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1247
1785
|
for (let i = 0; i < list.length; i++) {
|
|
1248
1786
|
for (let j = i + 1; j < list.length; j++) {
|
|
1249
1787
|
for (const hit of segmentIntersections(list[i].seg, list[j].seg)) {
|
|
1250
|
-
const q =
|
|
1788
|
+
const q = toVec2(hit.point);
|
|
1251
1789
|
const d = distance(q, p);
|
|
1252
1790
|
if (d <= tol && d < bestD) {
|
|
1253
1791
|
best = q;
|
|
@@ -1288,7 +1826,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1288
1826
|
const axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };
|
|
1289
1827
|
const nearest = (lines, values) => {
|
|
1290
1828
|
let best = null;
|
|
1291
|
-
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 };
|
|
1292
1830
|
return best;
|
|
1293
1831
|
};
|
|
1294
1832
|
const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
|
|
@@ -1331,7 +1869,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1331
1869
|
evenSpacing(r, horizontal, tol) {
|
|
1332
1870
|
const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
|
|
1333
1871
|
const [r0, r1] = span(r, horizontal);
|
|
1334
|
-
const
|
|
1872
|
+
const len2 = r1 - r0;
|
|
1335
1873
|
const [c0, c1] = span(r, !horizontal);
|
|
1336
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);
|
|
1337
1875
|
if (!row.length) return null;
|
|
@@ -1351,17 +1889,17 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1351
1889
|
if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
|
|
1352
1890
|
}
|
|
1353
1891
|
const starts = [];
|
|
1354
|
-
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 });
|
|
1355
1893
|
for (const { g } of pairs) {
|
|
1356
1894
|
if (before) starts.push({ start: before.a1 + g, repeats: g });
|
|
1357
|
-
if (after) starts.push({ start: after.a0 - g -
|
|
1895
|
+
if (after) starts.push({ start: after.a0 - g - len2, repeats: g });
|
|
1358
1896
|
}
|
|
1359
1897
|
let pick = null;
|
|
1360
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;
|
|
1361
1899
|
if (!pick) return null;
|
|
1362
1900
|
const { start, repeats } = pick;
|
|
1363
1901
|
const at = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
1364
|
-
const end = start +
|
|
1902
|
+
const end = start + len2;
|
|
1365
1903
|
const gaps = [];
|
|
1366
1904
|
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
|
|
1367
1905
|
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
|
|
@@ -1372,9 +1910,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1372
1910
|
}
|
|
1373
1911
|
/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
|
|
1374
1912
|
gapMarks([u0, u1, c], horizontal, tick, out) {
|
|
1375
|
-
const
|
|
1376
|
-
out.lines.push(line("spacing",
|
|
1377
|
-
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)));
|
|
1378
1916
|
}
|
|
1379
1917
|
/** The other objects' side length nearest `length`, within `tol`, or null. */
|
|
1380
1918
|
matchSize(length2, tol) {
|
|
@@ -1401,9 +1939,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1401
1939
|
const hp = handlePos(handle, frame);
|
|
1402
1940
|
const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
|
|
1403
1941
|
const valid = (n, s) => Number.isFinite(n) && n * s > 0;
|
|
1404
|
-
const nearest = (
|
|
1942
|
+
const nearest = (v2, lines) => {
|
|
1405
1943
|
let best = null;
|
|
1406
|
-
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;
|
|
1407
1945
|
return best;
|
|
1408
1946
|
};
|
|
1409
1947
|
let nx = sx;
|
|
@@ -1454,9 +1992,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1454
1992
|
};
|
|
1455
1993
|
for (const h of [handle, ...ends]) {
|
|
1456
1994
|
const hv = handlePos(h, frame);
|
|
1457
|
-
const
|
|
1458
|
-
const base = applyMatrix(toDoc, { x: origin.x + c.x *
|
|
1459
|
-
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 });
|
|
1460
1998
|
const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
|
|
1461
1999
|
const l2 = dir.x * dir.x + dir.y * dir.y;
|
|
1462
2000
|
const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
|
|
@@ -1489,16 +2027,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1489
2027
|
const sized = [];
|
|
1490
2028
|
if (!proportional) {
|
|
1491
2029
|
if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
|
|
1492
|
-
const
|
|
1493
|
-
if (
|
|
1494
|
-
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);
|
|
1495
2033
|
sized.push("x");
|
|
1496
2034
|
}
|
|
1497
2035
|
}
|
|
1498
2036
|
if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
|
|
1499
|
-
const
|
|
1500
|
-
if (
|
|
1501
|
-
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);
|
|
1502
2040
|
sized.push("y");
|
|
1503
2041
|
}
|
|
1504
2042
|
}
|
|
@@ -1549,14 +2087,14 @@ function line(kind, a, b) {
|
|
|
1549
2087
|
return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
|
|
1550
2088
|
}
|
|
1551
2089
|
function construction(leave, p, tol) {
|
|
1552
|
-
const
|
|
1553
|
-
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;
|
|
1554
2092
|
let best = null;
|
|
1555
2093
|
for (const a of leave.angles ?? []) {
|
|
1556
2094
|
const r = a * Math.PI / 180;
|
|
1557
2095
|
const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
|
|
1558
2096
|
const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
|
|
1559
|
-
const k =
|
|
2097
|
+
const k = v2.x * dir.x + v2.y * dir.y;
|
|
1560
2098
|
const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
|
|
1561
2099
|
const d = distance(point, p);
|
|
1562
2100
|
if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
|
|
@@ -1581,7 +2119,7 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1581
2119
|
vals.push(cond(r, d));
|
|
1582
2120
|
scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
|
|
1583
2121
|
}
|
|
1584
|
-
if (!(scale > 0) || vals.every((
|
|
2122
|
+
if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
|
|
1585
2123
|
const out = [];
|
|
1586
2124
|
for (let i = 0; i < CURVE_SAMPLES; i++) {
|
|
1587
2125
|
const ga = vals[i];
|
|
@@ -1614,8 +2152,8 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1614
2152
|
}
|
|
1615
2153
|
|
|
1616
2154
|
// src/vector/pathEdit.ts
|
|
1617
|
-
var
|
|
1618
|
-
var
|
|
2155
|
+
var EPS3 = 1e-9;
|
|
2156
|
+
var unit2 = (x, y) => {
|
|
1619
2157
|
const l = Math.hypot(x, y);
|
|
1620
2158
|
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1621
2159
|
};
|
|
@@ -1628,14 +2166,14 @@ function cubicOf(sp, i) {
|
|
|
1628
2166
|
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
1629
2167
|
function startTangent(c) {
|
|
1630
2168
|
for (let k = 1; k < 4; k++) {
|
|
1631
|
-
const u =
|
|
2169
|
+
const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
1632
2170
|
if (u) return u;
|
|
1633
2171
|
}
|
|
1634
2172
|
return [1, 0];
|
|
1635
2173
|
}
|
|
1636
2174
|
function endTangent(c) {
|
|
1637
2175
|
for (let k = 2; k >= 0; k--) {
|
|
1638
|
-
const u =
|
|
2176
|
+
const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
1639
2177
|
if (u) return u;
|
|
1640
2178
|
}
|
|
1641
2179
|
return [1, 0];
|
|
@@ -1643,11 +2181,11 @@ function endTangent(c) {
|
|
|
1643
2181
|
function sample(c, n, out, withStart) {
|
|
1644
2182
|
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
|
|
1645
2183
|
}
|
|
1646
|
-
var
|
|
2184
|
+
var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
|
|
1647
2185
|
function tidy(a, keepType = true) {
|
|
1648
2186
|
const next = cloneAnchor(a);
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
2187
|
+
if (onAnchor2(next, next.in)) delete next.in;
|
|
2188
|
+
if (onAnchor2(next, next.out)) delete next.out;
|
|
1651
2189
|
const shown = inferAnchorType(next);
|
|
1652
2190
|
const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
|
|
1653
2191
|
if (type === "corner") delete next.type;
|
|
@@ -1755,8 +2293,8 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
1755
2293
|
const a = sp.anchors[index];
|
|
1756
2294
|
if (!a) return { anchors, closed: sp.closed };
|
|
1757
2295
|
const own = [
|
|
1758
|
-
a.in && !
|
|
1759
|
-
a.out && !
|
|
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
|
|
1760
2298
|
];
|
|
1761
2299
|
const ends = [null, null];
|
|
1762
2300
|
neighbours(sp, index).forEach((j, side) => {
|
|
@@ -1775,15 +2313,16 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
1775
2313
|
if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
|
|
1776
2314
|
if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
|
|
1777
2315
|
if (a.type) moved.type = a.type;
|
|
2316
|
+
if (a.corner) moved.corner = { ...a.corner };
|
|
1778
2317
|
const [prev, next] = ends;
|
|
1779
2318
|
if (a.type && a.type !== "corner" && prev && next) {
|
|
1780
2319
|
const dx = next.x - prev.x;
|
|
1781
2320
|
const dy = next.y - prev.y;
|
|
1782
2321
|
const l = Math.hypot(dx, dy);
|
|
1783
2322
|
if (l > 1e-9) {
|
|
1784
|
-
const
|
|
1785
|
-
let li =
|
|
1786
|
-
let lo =
|
|
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);
|
|
1787
2326
|
if (a.type === "symmetric") li = lo = (li + lo) / 2;
|
|
1788
2327
|
moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
|
|
1789
2328
|
moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
|
|
@@ -1824,7 +2363,7 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
|
1824
2363
|
const { subpath, index } = insertAnchor(sp, segment, t);
|
|
1825
2364
|
return cutSubpathAt(subpath, [index]);
|
|
1826
2365
|
}
|
|
1827
|
-
var
|
|
2366
|
+
var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
1828
2367
|
var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
1829
2368
|
function reverse(sp) {
|
|
1830
2369
|
return {
|
|
@@ -1834,6 +2373,7 @@ function reverse(sp) {
|
|
|
1834
2373
|
if (a.out) next.in = { ...a.out };
|
|
1835
2374
|
if (a.in) next.out = { ...a.in };
|
|
1836
2375
|
if (a.type) next.type = a.type;
|
|
2376
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
1837
2377
|
return next;
|
|
1838
2378
|
})
|
|
1839
2379
|
};
|
|
@@ -1867,7 +2407,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
1867
2407
|
for (let j = i + 1; j < open.length; j++) {
|
|
1868
2408
|
for (const ei of [false, true]) {
|
|
1869
2409
|
for (const ej of [false, true]) {
|
|
1870
|
-
const d = dist(
|
|
2410
|
+
const d = dist(endOf2(open[i], ei), endOf2(open[j], ej));
|
|
1871
2411
|
if (d < best.d) best = { d, i, j, ei, ej };
|
|
1872
2412
|
}
|
|
1873
2413
|
}
|
|
@@ -1905,7 +2445,7 @@ function smoothSubpath(sp, amount = 1, only) {
|
|
|
1905
2445
|
const prev = orig[(i + n - 1) % n];
|
|
1906
2446
|
const next = orig[(i + 1) % n];
|
|
1907
2447
|
const p = orig[i];
|
|
1908
|
-
const dir =
|
|
2448
|
+
const dir = unit2(next.x - prev.x, next.y - prev.y);
|
|
1909
2449
|
if (!dir) continue;
|
|
1910
2450
|
const li = dist(p, prev) / 3;
|
|
1911
2451
|
const lo = dist(p, next) / 3;
|
|
@@ -1962,12 +2502,12 @@ function mergePair(a, b, tol) {
|
|
|
1962
2502
|
if (a.line && b.line) {
|
|
1963
2503
|
const cx = b3[0] - a0[0];
|
|
1964
2504
|
const cy = b3[1] - a0[1];
|
|
1965
|
-
const
|
|
1966
|
-
if (
|
|
2505
|
+
const len2 = Math.hypot(cx, cy);
|
|
2506
|
+
if (len2 < 1e-12) return null;
|
|
1967
2507
|
const mx = a3[0] - a0[0];
|
|
1968
2508
|
const my = a3[1] - a0[1];
|
|
1969
|
-
const along = (mx * cx + my * cy) /
|
|
1970
|
-
return Math.abs((mx * cy - my * cx) /
|
|
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;
|
|
1971
2511
|
}
|
|
1972
2512
|
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
1973
2513
|
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
@@ -2091,7 +2631,7 @@ function lineHits(p, q, seg) {
|
|
|
2091
2631
|
const dy = q[1] - p[1];
|
|
2092
2632
|
const len2 = dx * dx + dy * dy;
|
|
2093
2633
|
if (len2 < 1e-24) return [];
|
|
2094
|
-
const h = (
|
|
2634
|
+
const h = (pt2) => (pt2[0] - p[0]) * -dy + (pt2[1] - p[1]) * dx;
|
|
2095
2635
|
let coeffs;
|
|
2096
2636
|
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
2097
2637
|
else {
|
|
@@ -2099,7 +2639,7 @@ function lineHits(p, q, seg) {
|
|
|
2099
2639
|
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
2100
2640
|
}
|
|
2101
2641
|
if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
|
|
2102
|
-
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((
|
|
2642
|
+
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
|
|
2103
2643
|
const out = [];
|
|
2104
2644
|
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
2105
2645
|
const point = segmentPoint(seg, t);
|
|
@@ -2326,12 +2866,12 @@ function simplifyPolyline(points, tolerance) {
|
|
|
2326
2866
|
const [ax, ay] = points[a];
|
|
2327
2867
|
const dx = points[b][0] - ax;
|
|
2328
2868
|
const dy = points[b][1] - ay;
|
|
2329
|
-
const
|
|
2869
|
+
const len2 = Math.hypot(dx, dy);
|
|
2330
2870
|
let far = -1;
|
|
2331
2871
|
let farD = 0;
|
|
2332
2872
|
for (let i = a + 1; i < b; i++) {
|
|
2333
2873
|
const [px, py] = points[i];
|
|
2334
|
-
const d =
|
|
2874
|
+
const d = len2 < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len2;
|
|
2335
2875
|
if (d > farD) {
|
|
2336
2876
|
farD = d;
|
|
2337
2877
|
far = i;
|
|
@@ -2443,11 +2983,11 @@ function snapSquare(x, y, w, h) {
|
|
|
2443
2983
|
}
|
|
2444
2984
|
function turning(a, b, c) {
|
|
2445
2985
|
const u = [b[0] - a[0], b[1] - a[1]];
|
|
2446
|
-
const
|
|
2986
|
+
const v2 = [c[0] - b[0], c[1] - b[1]];
|
|
2447
2987
|
const lu = Math.hypot(u[0], u[1]);
|
|
2448
|
-
const lv = Math.hypot(
|
|
2988
|
+
const lv = Math.hypot(v2[0], v2[1]);
|
|
2449
2989
|
if (lu <= 0 || lv <= 0) return 0;
|
|
2450
|
-
return Math.acos(Math.min(1, Math.max(-1, (u[0] *
|
|
2990
|
+
return Math.acos(Math.min(1, Math.max(-1, (u[0] * v2[0] + u[1] * v2[1]) / (lu * lv)))) * 180 / Math.PI;
|
|
2451
2991
|
}
|
|
2452
2992
|
function rdp(p, eps) {
|
|
2453
2993
|
const n = p.length;
|
|
@@ -2575,6 +3115,21 @@ function recognizeInkShape(points, options = {}) {
|
|
|
2575
3115
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
2576
3116
|
var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
|
|
2577
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
|
+
];
|
|
2578
3133
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
2579
3134
|
var DEFAULT_DEFAULTS = {
|
|
2580
3135
|
fill: "#d4d4d8",
|
|
@@ -2651,6 +3206,29 @@ function getVisibleHandles(state) {
|
|
|
2651
3206
|
}
|
|
2652
3207
|
return out;
|
|
2653
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
|
+
}
|
|
2654
3232
|
function getAnchor(doc, ref) {
|
|
2655
3233
|
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
2656
3234
|
return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
|
|
@@ -2948,7 +3526,7 @@ var VectorEditorController = class {
|
|
|
2948
3526
|
if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
|
|
2949
3527
|
return h?.nodeId ? "move" : "default";
|
|
2950
3528
|
case "direct":
|
|
2951
|
-
if (h?.anchor || h?.handle) return "pointer";
|
|
3529
|
+
if (h?.anchor || h?.handle || h?.corner) return "pointer";
|
|
2952
3530
|
if (h?.segment) return "copy";
|
|
2953
3531
|
return h?.nodeId ? "move" : "default";
|
|
2954
3532
|
case "pen":
|
|
@@ -3033,12 +3611,12 @@ var VectorEditorController = class {
|
|
|
3033
3611
|
// Viewport
|
|
3034
3612
|
// -------------------------------------------------------------------------
|
|
3035
3613
|
screenToDocument(p) {
|
|
3036
|
-
const
|
|
3037
|
-
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 };
|
|
3038
3616
|
}
|
|
3039
3617
|
documentToScreen(p) {
|
|
3040
|
-
const
|
|
3041
|
-
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 };
|
|
3042
3620
|
}
|
|
3043
3621
|
setViewport(viewport) {
|
|
3044
3622
|
const next = { ...this.viewport, ...viewport };
|
|
@@ -3105,9 +3683,9 @@ var VectorEditorController = class {
|
|
|
3105
3683
|
toggleSnapping(on = !this.snap.objects) {
|
|
3106
3684
|
this.setSnap({ objects: on });
|
|
3107
3685
|
}
|
|
3108
|
-
gridValue(
|
|
3686
|
+
gridValue(v2) {
|
|
3109
3687
|
const g = this.snap.grid;
|
|
3110
|
-
return g > 0 ? Math.round(
|
|
3688
|
+
return g > 0 ? Math.round(v2 / g) * g : v2;
|
|
3111
3689
|
}
|
|
3112
3690
|
snapPoint(p) {
|
|
3113
3691
|
return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
|
|
@@ -3263,7 +3841,7 @@ var VectorEditorController = class {
|
|
|
3263
3841
|
let subpath;
|
|
3264
3842
|
switch (input.kind) {
|
|
3265
3843
|
case "rect":
|
|
3266
|
-
subpath = rectSubpath(b
|
|
3844
|
+
subpath = rectSubpath(b);
|
|
3267
3845
|
break;
|
|
3268
3846
|
case "ellipse":
|
|
3269
3847
|
subpath = ellipseSubpath(b);
|
|
@@ -3278,10 +3856,13 @@ var VectorEditorController = class {
|
|
|
3278
3856
|
subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
|
|
3279
3857
|
break;
|
|
3280
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);
|
|
3281
3862
|
const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
3282
3863
|
const style = this.styleFor(input.kind === "line");
|
|
3283
3864
|
const own = {};
|
|
3284
|
-
for (const key of
|
|
3865
|
+
for (const key of VECTOR_STYLE_KEYS) {
|
|
3285
3866
|
if (input[key] !== void 0) own[key] = input[key];
|
|
3286
3867
|
}
|
|
3287
3868
|
return { id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
|
|
@@ -3304,6 +3885,8 @@ var VectorEditorController = class {
|
|
|
3304
3885
|
if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
|
|
3305
3886
|
if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
|
|
3306
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];
|
|
3307
3890
|
return out;
|
|
3308
3891
|
}
|
|
3309
3892
|
/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
|
|
@@ -3328,6 +3911,95 @@ var VectorEditorController = class {
|
|
|
3328
3911
|
});
|
|
3329
3912
|
return { ok: true };
|
|
3330
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
|
+
}
|
|
3331
4003
|
rename(id, name) {
|
|
3332
4004
|
return this.updateNode(id, { name }, { label: "Rename" });
|
|
3333
4005
|
}
|
|
@@ -3930,7 +4602,7 @@ var VectorEditorController = class {
|
|
|
3930
4602
|
const map = (p) => applyMatrix(m, p);
|
|
3931
4603
|
return from.subpaths.map((sp) => ({
|
|
3932
4604
|
closed: sp.closed,
|
|
3933
|
-
anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {} }))
|
|
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 } } : {} }))
|
|
3934
4606
|
}));
|
|
3935
4607
|
}
|
|
3936
4608
|
/**
|
|
@@ -4285,6 +4957,22 @@ var VectorEditorController = class {
|
|
|
4285
4957
|
}
|
|
4286
4958
|
return null;
|
|
4287
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
|
+
}
|
|
4288
4976
|
hitAnchor(screen, refs) {
|
|
4289
4977
|
let best = null;
|
|
4290
4978
|
let bestD = Infinity;
|
|
@@ -4343,6 +5031,8 @@ var VectorEditorController = class {
|
|
|
4343
5031
|
if (handle) return { nodeId: handle.nodeId, handle };
|
|
4344
5032
|
const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
|
|
4345
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 };
|
|
4346
5036
|
for (const id of getEditablePathIds(this.view())) {
|
|
4347
5037
|
const node = this.getNode(id);
|
|
4348
5038
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -4500,6 +5190,13 @@ var VectorEditorController = class {
|
|
|
4500
5190
|
this.notify();
|
|
4501
5191
|
return;
|
|
4502
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
|
+
}
|
|
4503
5200
|
for (const id of getEditablePathIds(this.view())) {
|
|
4504
5201
|
const node = this.getNode(id);
|
|
4505
5202
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -4557,10 +5254,10 @@ var VectorEditorController = class {
|
|
|
4557
5254
|
}
|
|
4558
5255
|
const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
|
|
4559
5256
|
const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
|
|
4560
|
-
let
|
|
4561
|
-
|
|
5257
|
+
let pt3 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
|
|
5258
|
+
pt3 = this.toLocal(pen.nodeId, pt3);
|
|
4562
5259
|
const node2 = this.getNode(pen.nodeId);
|
|
4563
|
-
const anchors = [...sp.anchors, { x:
|
|
5260
|
+
const anchors = [...sp.anchors, { x: pt3.x, y: pt3.y }];
|
|
4564
5261
|
this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
|
|
4565
5262
|
const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
|
|
4566
5263
|
this.anchors = [ref2];
|
|
@@ -4588,8 +5285,8 @@ var VectorEditorController = class {
|
|
|
4588
5285
|
this.notify(this.doc !== before.doc);
|
|
4589
5286
|
return;
|
|
4590
5287
|
}
|
|
4591
|
-
const
|
|
4592
|
-
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 }] };
|
|
4593
5290
|
this.setDoc([...this.doc.nodes, node]);
|
|
4594
5291
|
this.pen = { nodeId: node.id, subpath: 0 };
|
|
4595
5292
|
this.selection = [node.id];
|
|
@@ -4737,13 +5434,25 @@ var VectorEditorController = class {
|
|
|
4737
5434
|
case "handle": {
|
|
4738
5435
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
4739
5436
|
const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
|
|
4740
|
-
const
|
|
4741
|
-
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);
|
|
4742
5439
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
|
|
4743
5440
|
this.interaction = { type: "handle", ref: drag.ref };
|
|
4744
5441
|
this.notify(true);
|
|
4745
5442
|
return;
|
|
4746
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
|
+
}
|
|
4747
5456
|
case "bend": {
|
|
4748
5457
|
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
4749
5458
|
drag.moved = true;
|
|
@@ -4769,10 +5478,10 @@ var VectorEditorController = class {
|
|
|
4769
5478
|
return;
|
|
4770
5479
|
}
|
|
4771
5480
|
this.guides = null;
|
|
4772
|
-
let
|
|
4773
|
-
if (input.shift)
|
|
5481
|
+
let pt2 = p;
|
|
5482
|
+
if (input.shift) pt2 = constrain45(anchor, pt2);
|
|
4774
5483
|
const side = drag.closing ? "in" : "out";
|
|
4775
|
-
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);
|
|
4776
5485
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
|
|
4777
5486
|
if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
|
|
4778
5487
|
return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
|
|
@@ -4885,12 +5594,12 @@ var VectorEditorController = class {
|
|
|
4885
5594
|
const guides = { lines: [], labels: [] };
|
|
4886
5595
|
for (const axis of ["x", "y"]) {
|
|
4887
5596
|
const d = end[axis] - start[axis];
|
|
4888
|
-
const
|
|
4889
|
-
if (
|
|
4890
|
-
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;
|
|
4891
5600
|
const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
|
|
4892
5601
|
guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
|
|
4893
|
-
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}` });
|
|
4894
5603
|
}
|
|
4895
5604
|
this.setGuides(guides);
|
|
4896
5605
|
return out;
|
|
@@ -4935,6 +5644,20 @@ var VectorEditorController = class {
|
|
|
4935
5644
|
return;
|
|
4936
5645
|
}
|
|
4937
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
|
+
}
|
|
4938
5661
|
case "stroke": {
|
|
4939
5662
|
this.notify();
|
|
4940
5663
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -5179,9 +5902,9 @@ function inkShapeSubpath(shape) {
|
|
|
5179
5902
|
function constrain45(origin, p) {
|
|
5180
5903
|
const dx = p.x - origin.x;
|
|
5181
5904
|
const dy = p.y - origin.y;
|
|
5182
|
-
const
|
|
5905
|
+
const len2 = Math.hypot(dx, dy);
|
|
5183
5906
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
5184
|
-
return { x: origin.x + Math.cos(a) *
|
|
5907
|
+
return { x: origin.x + Math.cos(a) * len2, y: origin.y + Math.sin(a) * len2 };
|
|
5185
5908
|
}
|
|
5186
5909
|
function scaleFor(handle, b, p, mods = {}) {
|
|
5187
5910
|
const west = handle.includes("w");
|
|
@@ -5218,10 +5941,12 @@ function renderNode(node, defs, idPrefix) {
|
|
|
5218
5941
|
const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
|
|
5219
5942
|
if (fill.gradient) defs.push(fill.gradient);
|
|
5220
5943
|
if (stroke.gradient) defs.push(stroke.gradient);
|
|
5944
|
+
const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
|
|
5221
5945
|
return {
|
|
5222
5946
|
kind: "path",
|
|
5223
5947
|
id: node.id,
|
|
5224
5948
|
...node.transform ? { transform: fmtMatrix(node.transform) } : {},
|
|
5949
|
+
...strokeParts ? { strokeParts } : {},
|
|
5225
5950
|
d: vectorPathData(node),
|
|
5226
5951
|
fill: fill.value,
|
|
5227
5952
|
stroke: stroke.value,
|
|
@@ -5237,9 +5962,9 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
5237
5962
|
const defs = [];
|
|
5238
5963
|
const prefix = options.idPrefix ?? "ve-";
|
|
5239
5964
|
const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
|
|
5240
|
-
const
|
|
5965
|
+
const v2 = state.viewport;
|
|
5241
5966
|
return {
|
|
5242
|
-
transform: `translate(${fmt(
|
|
5967
|
+
transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
|
|
5243
5968
|
width: state.document.width,
|
|
5244
5969
|
height: state.document.height,
|
|
5245
5970
|
...state.document.background ? { background: state.document.background } : {},
|
|
@@ -5249,8 +5974,8 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
5249
5974
|
}
|
|
5250
5975
|
var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
|
|
5251
5976
|
function screenOutline(state, node) {
|
|
5252
|
-
const
|
|
5253
|
-
const view = [
|
|
5977
|
+
const v2 = state.viewport;
|
|
5978
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
5254
5979
|
const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
|
|
5255
5980
|
return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
|
|
5256
5981
|
}
|
|
@@ -5359,8 +6084,14 @@ function getVectorOverlayRenderData(state) {
|
|
|
5359
6084
|
const b = toScreen(state, it.string[1]);
|
|
5360
6085
|
pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
|
|
5361
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)}`;
|
|
5362
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;
|
|
5363
|
-
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
|
|
6094
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets };
|
|
5364
6095
|
}
|
|
5365
6096
|
var VECTOR_OVERLAY_THEME = {
|
|
5366
6097
|
accent: "#0ea5e9",
|
|
@@ -5372,7 +6103,9 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
5372
6103
|
eraserFill: "rgba(239, 68, 68, 0.25)",
|
|
5373
6104
|
transformHandleSize: 8,
|
|
5374
6105
|
anchorSize: 7,
|
|
5375
|
-
handleRadius: 3.5
|
|
6106
|
+
handleRadius: 3.5,
|
|
6107
|
+
/** Radius of a live corner widget (a ring with a dot). */
|
|
6108
|
+
cornerWidgetRadius: 4
|
|
5376
6109
|
};
|
|
5377
6110
|
function toVectorModifiers(event) {
|
|
5378
6111
|
return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
|
|
@@ -5396,18 +6129,68 @@ function getVectorSelectionStyle(state) {
|
|
|
5396
6129
|
const node = findVectorNode(state.document.nodes, id);
|
|
5397
6130
|
if (node) paths.push(...collect(node));
|
|
5398
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
|
+
}
|
|
5399
6140
|
if (!paths.length) return null;
|
|
5400
6141
|
const first = paths[0];
|
|
5401
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" };
|
|
5402
6152
|
return {
|
|
5403
6153
|
fill: first.fill ?? null,
|
|
5404
6154
|
stroke: first.stroke ?? null,
|
|
5405
6155
|
strokeWidth: first.strokeWidth ?? 1,
|
|
5406
6156
|
opacity: first.opacity ?? 1,
|
|
5407
|
-
|
|
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
|
+
},
|
|
5408
6181
|
pathIds: paths.map((p) => p.id)
|
|
5409
6182
|
};
|
|
5410
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
|
+
};
|
|
5411
6194
|
function vectorPaintToHex(paint) {
|
|
5412
6195
|
if (typeof paint !== "string") return null;
|
|
5413
6196
|
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
|
|
@@ -5420,7 +6203,8 @@ var VECTOR_PATH_ACTIONS = [
|
|
|
5420
6203
|
{ id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
|
|
5421
6204
|
{ id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
|
|
5422
6205
|
{ id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
|
|
5423
|
-
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() }
|
|
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() }
|
|
5424
6208
|
];
|
|
5425
6209
|
var VECTOR_EDIT_TOGGLES = [
|
|
5426
6210
|
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
@@ -5458,7 +6242,13 @@ var STYLE_PROPS = {
|
|
|
5458
6242
|
strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
|
|
5459
6243
|
strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
|
|
5460
6244
|
strokeDasharray: { type: "array", items: { type: "number" } },
|
|
5461
|
-
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)." }
|
|
5462
6252
|
};
|
|
5463
6253
|
var pickStyle = (params) => {
|
|
5464
6254
|
const out = {};
|
|
@@ -5551,7 +6341,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5551
6341
|
y: { type: "number" },
|
|
5552
6342
|
width: { type: "number", description: "For a line: the x offset of its end." },
|
|
5553
6343
|
height: { type: "number", description: "For a line: the y offset of its end." },
|
|
5554
|
-
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 },
|
|
5555
6346
|
sides: { type: "integer", minimum: 3 },
|
|
5556
6347
|
points: { type: "integer", minimum: 3 },
|
|
5557
6348
|
innerRatio: { type: "number", minimum: 0, maximum: 1 },
|
|
@@ -5597,6 +6388,38 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5597
6388
|
return { ok: true };
|
|
5598
6389
|
}
|
|
5599
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
|
+
},
|
|
5600
6423
|
{
|
|
5601
6424
|
...base,
|
|
5602
6425
|
id: `${prefix}transform`,
|
|
@@ -5947,16 +6770,23 @@ export {
|
|
|
5947
6770
|
DEFAULT_VECTOR_PATH_EDIT,
|
|
5948
6771
|
DEFAULT_VECTOR_SNAP,
|
|
5949
6772
|
IDENTITY_MATRIX,
|
|
5950
|
-
KAPPA,
|
|
6773
|
+
KAPPA2 as KAPPA,
|
|
5951
6774
|
LazyMouse,
|
|
6775
|
+
VECTOR_ARROWHEADS,
|
|
6776
|
+
VECTOR_ARROWHEAD_LABELS,
|
|
5952
6777
|
VECTOR_CLIPBOARD_TYPE,
|
|
5953
6778
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
6779
|
+
VECTOR_CORNER_KINDS,
|
|
6780
|
+
VECTOR_CORNER_KIND_LABELS,
|
|
6781
|
+
VECTOR_CORNER_WIDGET_INSET,
|
|
5954
6782
|
VECTOR_EDIT_TOGGLES,
|
|
5955
6783
|
VECTOR_INK_MAX_CORNERS,
|
|
5956
6784
|
VECTOR_OVERLAY_THEME,
|
|
5957
6785
|
VECTOR_PATH_ACTIONS,
|
|
5958
6786
|
VECTOR_PENCIL_STRING_PX,
|
|
5959
6787
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
6788
|
+
VECTOR_STROKE_ALIGN_LABELS,
|
|
6789
|
+
VECTOR_STYLE_KEYS,
|
|
5960
6790
|
VECTOR_TOOLS,
|
|
5961
6791
|
VECTOR_TOOL_GLYPHS,
|
|
5962
6792
|
VECTOR_TOOL_SHORTCUTS,
|
|
@@ -5965,6 +6795,10 @@ export {
|
|
|
5965
6795
|
VectorSnapTargets,
|
|
5966
6796
|
applyMatrix,
|
|
5967
6797
|
applyMatrixLinear,
|
|
6798
|
+
applyVectorCornersAttribute,
|
|
6799
|
+
arrowheadInset,
|
|
6800
|
+
arrowheadPath,
|
|
6801
|
+
arrowheadWeight,
|
|
5968
6802
|
bendSegment,
|
|
5969
6803
|
bezierToSubpaths,
|
|
5970
6804
|
boundsFromPoints,
|
|
@@ -5976,19 +6810,27 @@ export {
|
|
|
5976
6810
|
collectPathNodes,
|
|
5977
6811
|
constrainToLeave,
|
|
5978
6812
|
convertAnchor,
|
|
6813
|
+
cornerBisectorPoint,
|
|
6814
|
+
cornerRadiusChange,
|
|
6815
|
+
cornerSetback,
|
|
5979
6816
|
createVectorCommands,
|
|
5980
6817
|
curvatureMoveAnchor,
|
|
5981
6818
|
cutSubpathAt,
|
|
5982
6819
|
decomposeRigid,
|
|
6820
|
+
effectiveStrokeAlign,
|
|
5983
6821
|
ellipseSubpath,
|
|
5984
6822
|
eraseFromFill,
|
|
5985
6823
|
eraseFromOutline,
|
|
5986
6824
|
eraserRegion,
|
|
6825
|
+
expandVectorCorners,
|
|
6826
|
+
expandVectorCornersOf,
|
|
5987
6827
|
findVectorNode,
|
|
6828
|
+
fitCornerRadius,
|
|
5988
6829
|
getAnchor,
|
|
5989
6830
|
getAnchorWorld,
|
|
5990
6831
|
getEditablePathIds,
|
|
5991
6832
|
getTransformHandlePositions,
|
|
6833
|
+
getVectorCornerWidgets,
|
|
5992
6834
|
getVectorOverlayRenderData,
|
|
5993
6835
|
getVectorSceneRenderData,
|
|
5994
6836
|
getVectorSelectionBoxGeometry,
|
|
@@ -5997,11 +6839,15 @@ export {
|
|
|
5997
6839
|
getVisibleAnchors,
|
|
5998
6840
|
getVisibleHandles,
|
|
5999
6841
|
hasPaint,
|
|
6842
|
+
hasStrokeExtras,
|
|
6843
|
+
hasVectorCorners,
|
|
6000
6844
|
hitTestVectorNode,
|
|
6001
6845
|
hitTestVectorNodes,
|
|
6846
|
+
hitTestVectorStroke,
|
|
6002
6847
|
inferAnchorType,
|
|
6003
6848
|
insertAnchor,
|
|
6004
6849
|
invertMatrix,
|
|
6850
|
+
isClosedBezierShape,
|
|
6005
6851
|
isIdentityMatrix,
|
|
6006
6852
|
isVectorTextTarget,
|
|
6007
6853
|
joinSubpaths,
|
|
@@ -6012,6 +6858,7 @@ export {
|
|
|
6012
6858
|
moveAnchorHandle,
|
|
6013
6859
|
multiplyMatrices,
|
|
6014
6860
|
nearestSegment,
|
|
6861
|
+
nextVectorCornerKind,
|
|
6015
6862
|
nodeMatrix,
|
|
6016
6863
|
parseSvgTransform,
|
|
6017
6864
|
parseVectorSvg,
|
|
@@ -6028,6 +6875,8 @@ export {
|
|
|
6028
6875
|
roundSubpaths,
|
|
6029
6876
|
scaleFor,
|
|
6030
6877
|
scaleMatrix,
|
|
6878
|
+
setAnchorCorner,
|
|
6879
|
+
setSubpathCorners,
|
|
6031
6880
|
simplifyPolyline,
|
|
6032
6881
|
smoothSubpath,
|
|
6033
6882
|
splitIntoFaces,
|
|
@@ -6044,6 +6893,9 @@ export {
|
|
|
6044
6893
|
translateAnchor,
|
|
6045
6894
|
translateMatrix,
|
|
6046
6895
|
unionBounds,
|
|
6896
|
+
vectorCornerAt,
|
|
6897
|
+
vectorCorners,
|
|
6898
|
+
vectorCornersAttribute,
|
|
6047
6899
|
vectorDocumentToSvg,
|
|
6048
6900
|
vectorInkShapeName,
|
|
6049
6901
|
vectorNodeBounds,
|
|
@@ -6055,6 +6907,11 @@ export {
|
|
|
6055
6907
|
vectorPathData,
|
|
6056
6908
|
vectorPathWorldBezier,
|
|
6057
6909
|
vectorResizeCursor,
|
|
6910
|
+
vectorStrokeOutline,
|
|
6911
|
+
vectorStrokeOutlinePath,
|
|
6912
|
+
vectorStrokeParts,
|
|
6913
|
+
vectorStrokeReach,
|
|
6914
|
+
vectorStrokeSvgParts,
|
|
6058
6915
|
vectorToolLabel,
|
|
6059
6916
|
vectorWorldMatrix,
|
|
6060
6917
|
walkVectorNodes
|