@pixerate/editor 0.35.0 → 0.37.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 +17 -0
- package/dist/canvas/index.cjs +25 -25
- package/dist/canvas/index.d.cts +3 -217
- package/dist/canvas/index.d.ts +3 -217
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-BGNPT5PE.cjs → chunk-GVB7ZPLZ.cjs} +24 -3
- package/dist/chunk-GVB7ZPLZ.cjs.map +1 -0
- package/dist/{chunk-EGPIVUEF.js → chunk-HPEKB352.js} +23 -2
- package/dist/chunk-HPEKB352.js.map +1 -0
- package/dist/{bezier-D8bsO-L0.d.cts → stroke-CWjFKal4.d.cts} +215 -1
- package/dist/{bezier-BvqVmWMg.d.ts → stroke-ysgaPpAo.d.ts} +215 -1
- package/dist/vector/index.cjs +2262 -388
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +704 -25
- package/dist/vector/index.d.ts +704 -25
- package/dist/vector/index.js +2107 -233
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BGNPT5PE.cjs.map +0 -1
- package/dist/chunk-EGPIVUEF.js.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -10,37 +10,1011 @@ import {
|
|
|
10
10
|
distanceToBezierPath,
|
|
11
11
|
fitCubicWithTangents,
|
|
12
12
|
fitCubics,
|
|
13
|
+
flattenChain,
|
|
13
14
|
formatNumber,
|
|
14
15
|
gradientToSvgDef,
|
|
16
|
+
isStraightCubic,
|
|
15
17
|
normalizeBezierPath,
|
|
16
18
|
outlineStroke,
|
|
17
19
|
pathBoolean,
|
|
18
20
|
pathToContours,
|
|
19
21
|
pointInBezierPath,
|
|
22
|
+
polygonBoolean,
|
|
20
23
|
segmentBounds,
|
|
21
24
|
segmentDerivative,
|
|
22
25
|
segmentIntersections,
|
|
26
|
+
segmentLength,
|
|
23
27
|
segmentNearest,
|
|
28
|
+
segmentParamAtLength,
|
|
24
29
|
segmentPoint,
|
|
30
|
+
segmentTangent,
|
|
25
31
|
splitSegment,
|
|
26
32
|
subSegment,
|
|
27
33
|
toBezierPath,
|
|
28
34
|
transformBezierPath,
|
|
35
|
+
trimSegments,
|
|
29
36
|
windingNumber
|
|
30
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-HPEKB352.js";
|
|
31
38
|
|
|
32
39
|
// src/vector/types.ts
|
|
33
|
-
var
|
|
40
|
+
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
41
|
+
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
42
|
+
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser", "width"];
|
|
34
43
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
35
44
|
|
|
36
|
-
// src/vector/
|
|
45
|
+
// src/vector/corners.ts
|
|
37
46
|
var EPS = 1e-9;
|
|
47
|
+
var MIN_SINE = 1e-6;
|
|
48
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
49
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
50
|
+
var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
|
|
51
|
+
var dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
52
|
+
var cross = (a, b) => a.x * b.y - a.y * b.x;
|
|
53
|
+
var len = (a) => Math.hypot(a.x, a.y);
|
|
54
|
+
var pt = (p) => [p.x, p.y];
|
|
55
|
+
var vec = (p) => ({ x: p[0], y: p[1] });
|
|
56
|
+
var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
|
|
57
|
+
var Side = class {
|
|
58
|
+
curve;
|
|
59
|
+
straight;
|
|
60
|
+
length;
|
|
61
|
+
constructor(a, h1, h2, b) {
|
|
62
|
+
this.straight = onAnchor(a, h1) && onAnchor(b, h2);
|
|
63
|
+
this.curve = this.straight ? [pt(a), pt(b)] : [pt(a), pt(h1 ?? a), pt(h2 ?? b), pt(b)];
|
|
64
|
+
this.length = this.straight ? Math.hypot(b.x - a.x, b.y - a.y) : segmentLength(this.curve);
|
|
65
|
+
}
|
|
66
|
+
/** Unit direction the side leaves the corner in (towards its first control point off the corner). */
|
|
67
|
+
direction() {
|
|
68
|
+
const p0 = vec(this.curve[0]);
|
|
69
|
+
for (const q of this.curve.slice(1)) {
|
|
70
|
+
const d = sub(vec(q), p0);
|
|
71
|
+
const l = len(d);
|
|
72
|
+
if (l > EPS) return mul(d, 1 / l);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/** The point `s` along the side from the corner: its parameter, position and the direction the side runs on in there. */
|
|
77
|
+
at(s, u) {
|
|
78
|
+
if (this.straight) {
|
|
79
|
+
const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
|
|
80
|
+
return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
|
|
81
|
+
}
|
|
82
|
+
const t = segmentParamAtLength(this.curve, s, this.length);
|
|
83
|
+
const d = vec(segmentDerivative(this.curve, t));
|
|
84
|
+
const l = len(d);
|
|
85
|
+
return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function cornerOf(sp, s, i) {
|
|
89
|
+
const n = sp.anchors.length;
|
|
90
|
+
if (n < 2 || !sp.closed && (i === 0 || i === n - 1)) return null;
|
|
91
|
+
if (n === 2 && sp.closed) return null;
|
|
92
|
+
const a = sp.anchors[i];
|
|
93
|
+
if (a.type === "smooth" || a.type === "symmetric") return null;
|
|
94
|
+
const prev = sp.anchors[(i - 1 + n) % n];
|
|
95
|
+
const next = sp.anchors[(i + 1) % n];
|
|
96
|
+
const sideU = new Side(a, a.in, prev.out, prev);
|
|
97
|
+
const sideV = new Side(a, a.out, next.in, next);
|
|
98
|
+
const u = sideU.direction();
|
|
99
|
+
const v2 = sideV.direction();
|
|
100
|
+
if (!u || !v2) return null;
|
|
101
|
+
const lu = sideU.length;
|
|
102
|
+
const lv = sideV.length;
|
|
103
|
+
if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
|
|
104
|
+
const cos = Math.max(-1, Math.min(1, dot(u, v2)));
|
|
105
|
+
const sin = Math.abs(cross(u, v2));
|
|
106
|
+
if (sin <= MIN_SINE) return null;
|
|
107
|
+
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 };
|
|
108
|
+
}
|
|
109
|
+
var strip = ({ sideU: _u, sideV: _v, ...info }) => info;
|
|
110
|
+
function vectorCornerAt(sp, index, subpath = 0) {
|
|
111
|
+
const c = cornerOf(sp, subpath, index);
|
|
112
|
+
return c ? strip(c) : null;
|
|
113
|
+
}
|
|
114
|
+
function vectorCorners(subpaths) {
|
|
115
|
+
const out = [];
|
|
116
|
+
subpaths.forEach((sp, s) => {
|
|
117
|
+
for (let i = 0; i < sp.anchors.length; i++) {
|
|
118
|
+
const c = cornerOf(sp, s, i);
|
|
119
|
+
if (c) out.push(strip(c));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
|
|
125
|
+
var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
|
|
126
|
+
var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
|
|
127
|
+
var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
|
|
128
|
+
function arcHandle(cos) {
|
|
129
|
+
const c = Math.max(-1, Math.min(1, cos));
|
|
130
|
+
const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
|
|
131
|
+
const cosHalf = Math.sqrt(Math.max(0, (1 + c) / 2));
|
|
132
|
+
return 4 / 3 * sinHalf / (1 + cosHalf);
|
|
133
|
+
}
|
|
134
|
+
function squareTo(a, b) {
|
|
135
|
+
const w = sub(b, mul(a, dot(a, b)));
|
|
136
|
+
const l = len(w);
|
|
137
|
+
return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
|
|
138
|
+
}
|
|
139
|
+
function cutCorner(c, radius, kind) {
|
|
140
|
+
const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
|
|
141
|
+
if (!(s > EPS)) return null;
|
|
142
|
+
const a = c.sideU.at(s, c.u);
|
|
143
|
+
const b = c.sideV.at(s, c.v);
|
|
144
|
+
if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
|
|
145
|
+
if (kind === "inverted") {
|
|
146
|
+
const h = s * arcHandle(dot(a.dir, b.dir));
|
|
147
|
+
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 };
|
|
148
|
+
}
|
|
149
|
+
const d1 = mul(a.dir, -1);
|
|
150
|
+
const d2 = mul(b.dir, -1);
|
|
151
|
+
const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
|
|
152
|
+
const chord = len(sub(b.p, a.p));
|
|
153
|
+
const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
|
|
154
|
+
if (!Number.isFinite(k)) return null;
|
|
155
|
+
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 };
|
|
156
|
+
}
|
|
157
|
+
var radiusOf = (a) => {
|
|
158
|
+
const r = a.corner?.radius;
|
|
159
|
+
return typeof r === "number" && r > 0 && Number.isFinite(r) ? r : 0;
|
|
160
|
+
};
|
|
161
|
+
function hasVectorCorners(subpaths) {
|
|
162
|
+
return subpaths.some((sp) => sp.anchors.some((a) => radiusOf(a) > 0));
|
|
163
|
+
}
|
|
164
|
+
function expandVectorCornersOf(sp, subpath = 0) {
|
|
165
|
+
const n = sp.anchors.length;
|
|
166
|
+
const cuts = sp.anchors.map((a, i) => {
|
|
167
|
+
const r = radiusOf(a);
|
|
168
|
+
if (!r) return null;
|
|
169
|
+
const c = cornerOf(sp, subpath, i);
|
|
170
|
+
if (!c) return null;
|
|
171
|
+
const fitted = fitCornerRadius(c, r);
|
|
172
|
+
return fitted > EPS ? cutCorner(c, fitted, a.corner?.kind ?? "round") : null;
|
|
173
|
+
});
|
|
174
|
+
if (!cuts.some(Boolean)) return { subpath: { closed: sp.closed, anchors: sp.anchors.map(plain) }, sources: sp.anchors.map((_, i) => i) };
|
|
175
|
+
const trimmed = sp.anchors.map((a, j) => {
|
|
176
|
+
const k = (j + 1) % n;
|
|
177
|
+
if (k === 0 && !sp.closed) return void 0;
|
|
178
|
+
const b = sp.anchors[k];
|
|
179
|
+
const from = cuts[j];
|
|
180
|
+
const to = cuts[k];
|
|
181
|
+
if (!from && !to) return void 0;
|
|
182
|
+
if (onAnchor(a, a.out) && onAnchor(b, b.in)) return { out: null, in: null };
|
|
183
|
+
const s0 = from ? from.tv : 0;
|
|
184
|
+
const s1 = to ? 1 - to.tu : 1;
|
|
185
|
+
const seg = [pt(a), pt(a.out ?? a), pt(b.in ?? b), pt(b)];
|
|
186
|
+
const part = subSegment(seg, s0, Math.max(s0, s1));
|
|
187
|
+
return { out: vec(part[1]), in: vec(part[2]) };
|
|
188
|
+
});
|
|
189
|
+
const anchors = [];
|
|
190
|
+
const sources = [];
|
|
191
|
+
let closing = null;
|
|
192
|
+
const handleIn = (i, a) => {
|
|
193
|
+
const t = trimmed[(i - 1 + n) % n];
|
|
194
|
+
return t ? t.in : a.in ?? null;
|
|
195
|
+
};
|
|
196
|
+
const handleOut = (i, a) => {
|
|
197
|
+
const t = trimmed[i];
|
|
198
|
+
return t ? t.out : a.out ?? null;
|
|
199
|
+
};
|
|
200
|
+
sp.anchors.forEach((a, i) => {
|
|
201
|
+
const c = cuts[i];
|
|
202
|
+
if (!c) {
|
|
203
|
+
anchors.push(withHandles({ x: a.x, y: a.y, ...a.type ? { type: a.type } : {} }, handleIn(i, a), handleOut(i, a)));
|
|
204
|
+
sources.push(i);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const start = withHandles({ x: c.start.x, y: c.start.y }, handleIn(i, a), c.out);
|
|
208
|
+
const end = withHandles({ x: c.end.x, y: c.end.y }, c.in, handleOut(i, a));
|
|
209
|
+
if (i === 0 && sp.closed) closing = start;
|
|
210
|
+
else {
|
|
211
|
+
anchors.push(start);
|
|
212
|
+
sources.push(i);
|
|
213
|
+
}
|
|
214
|
+
anchors.push(end);
|
|
215
|
+
sources.push(i);
|
|
216
|
+
});
|
|
217
|
+
if (closing) {
|
|
218
|
+
anchors.push(closing);
|
|
219
|
+
sources.push(0);
|
|
220
|
+
}
|
|
221
|
+
return { subpath: { closed: sp.closed, anchors }, sources };
|
|
222
|
+
}
|
|
223
|
+
function withHandles(a, hin, hout) {
|
|
224
|
+
if (hin && !onAnchor(a, hin)) a.in = { x: hin.x, y: hin.y };
|
|
225
|
+
if (hout && !onAnchor(a, hout)) a.out = { x: hout.x, y: hout.y };
|
|
226
|
+
return a;
|
|
227
|
+
}
|
|
228
|
+
function plain(a) {
|
|
229
|
+
if (!("corner" in a)) return a;
|
|
230
|
+
const { corner: _c, ...rest } = a;
|
|
231
|
+
return rest;
|
|
232
|
+
}
|
|
233
|
+
function expandVectorCorners(subpaths) {
|
|
234
|
+
if (!hasVectorCorners(subpaths)) return subpaths;
|
|
235
|
+
return subpaths.map((sp, s) => sp.anchors.some((a) => radiusOf(a) > 0) ? expandVectorCornersOf(sp, s).subpath : sp);
|
|
236
|
+
}
|
|
237
|
+
function setAnchorCorner(a, radius, kind) {
|
|
238
|
+
const { corner, ...rest } = a;
|
|
239
|
+
const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
|
|
240
|
+
const k = kind ?? corner?.kind;
|
|
241
|
+
if (r <= 0 && !kind) return rest;
|
|
242
|
+
return { ...rest, corner: { radius: r, ...k && k !== "round" ? { kind: k } : {} } };
|
|
243
|
+
}
|
|
244
|
+
function nextVectorCornerKind(kind) {
|
|
245
|
+
return kind === "inverted" ? "chamfer" : kind === "chamfer" ? "round" : "inverted";
|
|
246
|
+
}
|
|
247
|
+
function setSubpathCorners(sp, radius, kind) {
|
|
248
|
+
return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/vector/width.ts
|
|
252
|
+
var VECTOR_WIDTH_PROFILE_MAX_POINTS = 64;
|
|
253
|
+
var VECTOR_WIDTH_PROFILE_MAX_SCALE = 1e3;
|
|
254
|
+
var VECTOR_WIDTH_SAME_T = 1e-6;
|
|
255
|
+
var sym = (t, s) => ({ t, left: s, right: s });
|
|
256
|
+
var VECTOR_WIDTH_PRESETS = [
|
|
257
|
+
{ id: "uniform", label: "Uniform", points: [] },
|
|
258
|
+
{ id: "taperStart", label: "Taper start", points: [sym(0, 0), sym(1, 1)] },
|
|
259
|
+
{ id: "taperEnd", label: "Taper end", points: [sym(0, 1), sym(1, 0)] },
|
|
260
|
+
{ id: "taperBoth", label: "Taper both", points: [sym(0, 0), sym(0.5, 1), sym(1, 0)] },
|
|
261
|
+
// Full width at the ends, swelling to twice the width in the middle.
|
|
262
|
+
{ id: "bulge", label: "Bulge", points: [sym(0, 1), sym(0.5, 2), sym(1, 1)] },
|
|
263
|
+
// Full width at the ends, pinched to a quarter in the middle.
|
|
264
|
+
{ id: "pinch", label: "Pinch", points: [sym(0, 1), sym(0.5, 0.25), sym(1, 1)] },
|
|
265
|
+
// A round head a fifth of the way along, then a long taper to the end.
|
|
266
|
+
{ id: "teardrop", label: "Teardrop", points: [sym(0, 0), sym(0.2, 1), sym(1, 0)] },
|
|
267
|
+
// Two swells between narrow necks.
|
|
268
|
+
{ id: "wave", label: "Wave", points: [sym(0, 0.3), sym(0.25, 1), sym(0.5, 0.3), sym(0.75, 1), sym(1, 0.3)] },
|
|
269
|
+
// A pen stroke: a quick press, a slow lift.
|
|
270
|
+
{ id: "pressure", label: "Pressure", points: [sym(0, 0.15), sym(0.12, 0.85), sym(0.45, 1), sym(0.8, 0.7), sym(1, 0.05)] }
|
|
271
|
+
];
|
|
272
|
+
var VECTOR_WIDTH_PRESET_IDS = VECTOR_WIDTH_PRESETS.map((p) => p.id);
|
|
273
|
+
var clean = (v2, max) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(Math.max(v2, 0), max) : null;
|
|
274
|
+
function sanitizeWidthProfile(points) {
|
|
275
|
+
if (!Array.isArray(points)) return [];
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const p of points) {
|
|
278
|
+
if (!p || typeof p !== "object") continue;
|
|
279
|
+
const t = clean(p.t, 1);
|
|
280
|
+
const l = clean(p.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
281
|
+
const r = clean(p.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
282
|
+
if (t === null || l === null && r === null) continue;
|
|
283
|
+
out.push({ t, left: l ?? r, right: r ?? l });
|
|
284
|
+
if (out.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) break;
|
|
285
|
+
}
|
|
286
|
+
return out.map((p, i) => [p, i]).sort((a, b) => a[0].t - b[0].t || a[1] - b[1]).map(([p]) => p);
|
|
287
|
+
}
|
|
288
|
+
function isUniformWidthProfile(points) {
|
|
289
|
+
return !points?.length || points.every((p) => p.left === 1 && p.right === 1);
|
|
290
|
+
}
|
|
291
|
+
function secant(pts, i, k) {
|
|
292
|
+
const a = pts[i];
|
|
293
|
+
const b = pts[i + 1];
|
|
294
|
+
return a && b && b.t - a.t > 1e-12 ? (b[k] - a[k]) / (b.t - a.t) : 0;
|
|
295
|
+
}
|
|
296
|
+
function tangent(pts, i, k) {
|
|
297
|
+
const last = pts.length - 1;
|
|
298
|
+
if (i <= 0) return secant(pts, 0, k);
|
|
299
|
+
if (i >= last) return secant(pts, last - 1, k);
|
|
300
|
+
const d0 = secant(pts, i - 1, k);
|
|
301
|
+
const d1 = secant(pts, i, k);
|
|
302
|
+
return d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
|
|
303
|
+
}
|
|
304
|
+
function interp(pts, t, k) {
|
|
305
|
+
const first = pts[0];
|
|
306
|
+
const last = pts[pts.length - 1];
|
|
307
|
+
if (!(t > first.t)) return first[k];
|
|
308
|
+
if (t >= last.t) return last[k];
|
|
309
|
+
let i = 0;
|
|
310
|
+
while (i + 1 < pts.length && pts[i + 1].t <= t) i++;
|
|
311
|
+
const a = pts[i];
|
|
312
|
+
const b = pts[i + 1];
|
|
313
|
+
const h = b.t - a.t;
|
|
314
|
+
if (h <= 1e-12) return b[k];
|
|
315
|
+
const u = (t - a.t) / h;
|
|
316
|
+
const u2 = u * u;
|
|
317
|
+
const u3 = u2 * u;
|
|
318
|
+
const w = (2 * u3 - 3 * u2 + 1) * a[k] + (u3 - 2 * u2 + u) * h * tangent(pts, i, k) + (3 * u2 - 2 * u3) * b[k] + (u3 - u2) * h * tangent(pts, i + 1, k);
|
|
319
|
+
return Math.max(0, w);
|
|
320
|
+
}
|
|
321
|
+
function widthProfileAt(points, t) {
|
|
322
|
+
if (!points?.length) return { left: 1, right: 1 };
|
|
323
|
+
return { left: interp(points, t, "left"), right: interp(points, t, "right") };
|
|
324
|
+
}
|
|
325
|
+
function widthProfileMaxScale(points) {
|
|
326
|
+
if (!points?.length) return 1;
|
|
327
|
+
return points.reduce((m, p) => Math.max(m, p.left, p.right), 0);
|
|
328
|
+
}
|
|
329
|
+
function widthProfilePreset(id) {
|
|
330
|
+
const p = VECTOR_WIDTH_PRESETS.find((q) => q.id === id);
|
|
331
|
+
return p ? p.points.map((q) => ({ ...q })) : null;
|
|
332
|
+
}
|
|
333
|
+
function widthProfilePresetId(points) {
|
|
334
|
+
if (isUniformWidthProfile(points)) return "uniform";
|
|
335
|
+
const same = (a, b) => a.length === b.length && a.every((p, i) => Math.abs(p.t - b[i].t) < 1e-9 && Math.abs(p.left - b[i].left) < 1e-9 && Math.abs(p.right - b[i].right) < 1e-9);
|
|
336
|
+
return VECTOR_WIDTH_PRESETS.find((p) => p.points.length && same(points, p.points))?.id ?? "custom";
|
|
337
|
+
}
|
|
338
|
+
function reverseWidthProfile(points) {
|
|
339
|
+
return points.map((p) => ({ t: 1 - p.t, left: p.right, right: p.left })).reverse();
|
|
340
|
+
}
|
|
341
|
+
function mirrorWidthProfile(points) {
|
|
342
|
+
return points.map((p) => ({ t: p.t, left: p.right, right: p.left }));
|
|
343
|
+
}
|
|
344
|
+
function editable(points) {
|
|
345
|
+
return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
|
|
346
|
+
}
|
|
347
|
+
function insertWidthPoint(points, t, sides = {}, options = {}) {
|
|
348
|
+
const pts = editable(points);
|
|
349
|
+
const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
|
|
350
|
+
const here = widthProfileAt(points?.length ? points : null, tt);
|
|
351
|
+
const p = {
|
|
352
|
+
t: tt,
|
|
353
|
+
left: clean(sides.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
|
|
354
|
+
right: clean(sides.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
|
|
355
|
+
};
|
|
356
|
+
const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
|
|
357
|
+
if (same >= 0 && !options.step) {
|
|
358
|
+
pts[same] = { ...p, t: pts[same].t };
|
|
359
|
+
return { points: pts, index: same };
|
|
360
|
+
}
|
|
361
|
+
let i = 0;
|
|
362
|
+
while (i < pts.length && pts[i].t <= tt) i++;
|
|
363
|
+
if (pts.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { points: pts, index: Math.max(0, i - 1) };
|
|
364
|
+
pts.splice(i, 0, p);
|
|
365
|
+
return { points: pts, index: i };
|
|
366
|
+
}
|
|
367
|
+
function setWidthPoint(points, index, patch) {
|
|
368
|
+
const pts = points.map((p2) => ({ ...p2 }));
|
|
369
|
+
const p = pts[index];
|
|
370
|
+
if (!p) return pts;
|
|
371
|
+
if (typeof patch.t === "number" && Number.isFinite(patch.t)) {
|
|
372
|
+
const lo = index > 0 ? pts[index - 1].t : 0;
|
|
373
|
+
const hi = index < pts.length - 1 ? pts[index + 1].t : 1;
|
|
374
|
+
p.t = Math.min(Math.max(patch.t, lo), Math.max(hi, lo));
|
|
375
|
+
}
|
|
376
|
+
const l = clean(patch.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
377
|
+
const r = clean(patch.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
|
|
378
|
+
if (l !== null) p.left = l;
|
|
379
|
+
if (r !== null) p.right = r;
|
|
380
|
+
return pts;
|
|
381
|
+
}
|
|
382
|
+
function removeWidthPoints(points, indices) {
|
|
383
|
+
const drop = new Set(indices);
|
|
384
|
+
return points.filter((_, i) => !drop.has(i)).map((p) => ({ ...p }));
|
|
385
|
+
}
|
|
386
|
+
function ease(u, e) {
|
|
387
|
+
const x = Math.min(Math.max(u, 0), 1);
|
|
388
|
+
const round = Math.sqrt(Math.max(0, 1 - (1 - x) * (1 - x)));
|
|
389
|
+
return x + (round - x) * Math.min(Math.max(e, 0), 1);
|
|
390
|
+
}
|
|
391
|
+
function taperWaveFactor(t, taper, wave) {
|
|
392
|
+
let k = 1;
|
|
393
|
+
if (taper) {
|
|
394
|
+
let ls = Math.max(0, taper.startLength ?? 0);
|
|
395
|
+
let le = Math.max(0, taper.endLength ?? 0);
|
|
396
|
+
if (ls + le > 1) {
|
|
397
|
+
const f = 1 / (ls + le);
|
|
398
|
+
ls *= f;
|
|
399
|
+
le *= f;
|
|
400
|
+
}
|
|
401
|
+
const sw = Math.min(Math.max(taper.startWidth ?? 0, 0), 1);
|
|
402
|
+
const ew = Math.min(Math.max(taper.endWidth ?? 0, 0), 1);
|
|
403
|
+
if (ls > 0 && t < ls) k = Math.min(k, sw + (1 - sw) * ease(t / ls, taper.startEase ?? 0));
|
|
404
|
+
if (le > 0 && t > 1 - le) k = Math.min(k, ew + (1 - ew) * ease((1 - t) / le, taper.endEase ?? 0));
|
|
405
|
+
}
|
|
406
|
+
if (wave) {
|
|
407
|
+
const cycles = wave.cycles ?? 4;
|
|
408
|
+
const amount = Math.min(Math.max(wave.amount ?? 0.5, 0), 1);
|
|
409
|
+
if (cycles > 0 && amount > 0) {
|
|
410
|
+
const ph = 2 * Math.PI * t * cycles + (wave.phase ?? 0) * Math.PI / 180;
|
|
411
|
+
k *= 1 - amount * (0.5 - 0.5 * Math.cos(ph));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return k;
|
|
415
|
+
}
|
|
416
|
+
function generateWidthProfile(options) {
|
|
417
|
+
const cycles = options.wave ? Math.max(0, options.wave.cycles ?? 4) : 0;
|
|
418
|
+
const n = Math.max(8, Math.min(VECTOR_WIDTH_PROFILE_MAX_POINTS * 4, options.samples ?? Math.ceil(32 + cycles * 16)));
|
|
419
|
+
const raw = [];
|
|
420
|
+
for (let i = 0; i <= n; i++) raw.push([i / n, taperWaveFactor(i / n, options.taper, options.wave)]);
|
|
421
|
+
const ts = [options.taper?.startLength, options.taper?.endLength !== void 0 ? 1 - options.taper.endLength : void 0];
|
|
422
|
+
for (const t of ts) if (typeof t === "number" && t > 0 && t < 1) raw.push([t, taperWaveFactor(t, options.taper, options.wave)]);
|
|
423
|
+
raw.sort((a, b) => a[0] - b[0]);
|
|
424
|
+
const kept = simplifySamples(raw, options.tolerance ?? 0.01, VECTOR_WIDTH_PROFILE_MAX_POINTS);
|
|
425
|
+
return kept.map(([t, k]) => sym(t, k));
|
|
426
|
+
}
|
|
427
|
+
function simplifySamples(samples, tolerance, max) {
|
|
428
|
+
if (samples.length <= 2) return samples.slice();
|
|
429
|
+
const keep = new Array(samples.length).fill(false);
|
|
430
|
+
keep[0] = keep[samples.length - 1] = true;
|
|
431
|
+
const stack = [[0, samples.length - 1]];
|
|
432
|
+
while (stack.length) {
|
|
433
|
+
const [a, b] = stack.pop();
|
|
434
|
+
const [ta, va] = samples[a];
|
|
435
|
+
const [tb, vb] = samples[b];
|
|
436
|
+
let worst = -1;
|
|
437
|
+
let err = tolerance;
|
|
438
|
+
for (let i = a + 1; i < b; i++) {
|
|
439
|
+
const [t, v2] = samples[i];
|
|
440
|
+
const lin = tb - ta > 1e-12 ? va + (vb - va) * (t - ta) / (tb - ta) : va;
|
|
441
|
+
const e = Math.abs(v2 - lin);
|
|
442
|
+
if (e > err) {
|
|
443
|
+
err = e;
|
|
444
|
+
worst = i;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (worst >= 0) {
|
|
448
|
+
keep[worst] = true;
|
|
449
|
+
stack.push([a, worst], [worst, b]);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
let out = samples.filter((_, i) => keep[i]);
|
|
453
|
+
while (out.length > max) out = out.filter((_, i) => i === 0 || i === out.length - 1 || i % 2 === 0);
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
var VECTOR_PRESSURE_NOMINAL = 0.5;
|
|
457
|
+
function pressureWidthProfile(samples, options = {}) {
|
|
458
|
+
const raw = samples.filter((s) => Number.isFinite(s.t) && Number.isFinite(s.pressure)).map((s) => [Math.min(Math.max(s.t, 0), 1), Math.max(0.05, Math.min(1, Math.max(0, s.pressure)) / VECTOR_PRESSURE_NOMINAL)]);
|
|
459
|
+
if (raw.length < 2) return [];
|
|
460
|
+
if (raw.every((s) => Math.abs(s[1] - raw[0][1]) < 1e-6)) return [];
|
|
461
|
+
return simplifySamples(raw, options.tolerance ?? 0.02, VECTOR_WIDTH_PROFILE_MAX_POINTS).map(([t, k]) => sym(t, Math.round(k * 1e3) / 1e3));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/vector/varstroke.ts
|
|
465
|
+
var SAMPLES_PER_SUBPATH = 128;
|
|
466
|
+
var MAX_DASHES = 5e3;
|
|
467
|
+
var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
|
|
468
|
+
var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
|
|
469
|
+
var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
|
|
470
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
471
|
+
var unitOf = (v2) => {
|
|
472
|
+
const l = Math.hypot(v2[0], v2[1]);
|
|
473
|
+
return l > 0 ? [v2[0] / l, v2[1] / l] : [1, 0];
|
|
474
|
+
};
|
|
475
|
+
var leftNormal = (d) => [d[1], -d[0]];
|
|
476
|
+
function flatten(segments, closed, tol) {
|
|
477
|
+
const pts = [];
|
|
478
|
+
const corner = [];
|
|
479
|
+
const push = (p, isCorner) => {
|
|
480
|
+
const last = pts[pts.length - 1];
|
|
481
|
+
if (last && dist(last, p) <= 1e-9) {
|
|
482
|
+
corner[corner.length - 1] ||= isCorner;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
pts.push(p);
|
|
486
|
+
corner.push(isCorner);
|
|
487
|
+
};
|
|
488
|
+
for (const seg of segments) {
|
|
489
|
+
const flat = flattenChain([seg], tol);
|
|
490
|
+
if (dist(seg[0], seg[seg.length - 1]) <= 1e-12 && flat.length > 1) flat.push(seg[seg.length - 1]);
|
|
491
|
+
flat.forEach((p, i) => push(p, i === 0 || i === flat.length - 1));
|
|
492
|
+
}
|
|
493
|
+
if (closed && pts.length > 2 && dist(pts[0], pts[pts.length - 1]) > 1e-9) push(pts[0], true);
|
|
494
|
+
if (pts.length < 2) return null;
|
|
495
|
+
let total = 0;
|
|
496
|
+
for (let i = 1; i < pts.length; i++) total += dist(pts[i - 1], pts[i]);
|
|
497
|
+
if (!(total > 0)) return null;
|
|
498
|
+
const step = Math.max(total / SAMPLES_PER_SUBPATH, tol);
|
|
499
|
+
const fine = [pts[0]];
|
|
500
|
+
const fineCorner = [corner[0]];
|
|
501
|
+
for (let i = 1; i < pts.length; i++) {
|
|
502
|
+
const n = Math.max(1, Math.ceil(dist(pts[i - 1], pts[i]) / step));
|
|
503
|
+
for (let k = 1; k <= n; k++) {
|
|
504
|
+
fine.push(k === n ? pts[i] : lerp(pts[i - 1], pts[i], k / n));
|
|
505
|
+
fineCorner.push(k === n ? corner[i] : false);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const s = [0];
|
|
509
|
+
for (let i = 1; i < fine.length; i++) s.push(s[i - 1] + dist(fine[i - 1], fine[i]));
|
|
510
|
+
return { pts: fine, s, corner: fineCorner, closed };
|
|
511
|
+
}
|
|
512
|
+
function dashIntervals(pattern, offset, len2) {
|
|
513
|
+
let pat = pattern.map((v2) => Number.isFinite(v2) ? Math.max(0, v2) : 0);
|
|
514
|
+
if (pat.length % 2 === 1) pat = [...pat, ...pat];
|
|
515
|
+
const period = pat.reduce((a, b) => a + b, 0);
|
|
516
|
+
if (pat.length < 2 || !(period > 1e-9) || !(len2 > 0)) return [[0, len2]];
|
|
517
|
+
let pos = ((Number.isFinite(offset) ? offset : 0) % period + period) % period;
|
|
518
|
+
let idx = 0;
|
|
519
|
+
while (pos >= pat[idx]) {
|
|
520
|
+
pos -= pat[idx];
|
|
521
|
+
idx = (idx + 1) % pat.length;
|
|
522
|
+
}
|
|
523
|
+
const out = [];
|
|
524
|
+
let s = 0;
|
|
525
|
+
let guard = 0;
|
|
526
|
+
while (s < len2 && guard++ < MAX_DASHES * pat.length) {
|
|
527
|
+
const e = Math.min(s + pat[idx] - pos, len2);
|
|
528
|
+
if (idx % 2 === 0 && e >= s) out.push([s, e]);
|
|
529
|
+
s += pat[idx] - pos;
|
|
530
|
+
pos = 0;
|
|
531
|
+
idx = (idx + 1) % pat.length;
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
function at(poly, t) {
|
|
536
|
+
let i = 1;
|
|
537
|
+
while (i < poly.pts.length - 1 && poly.s[i] <= t) i++;
|
|
538
|
+
const a = poly.s[i - 1];
|
|
539
|
+
const b = poly.s[i];
|
|
540
|
+
const u = b > a ? Math.min(Math.max((t - a) / (b - a), 0), 1) : 0;
|
|
541
|
+
return [lerp(poly.pts[i - 1], poly.pts[i], u), i];
|
|
542
|
+
}
|
|
543
|
+
function arcPoints(c, a0, sweep, r0, r1, tol) {
|
|
544
|
+
const r = Math.max(r0, r1);
|
|
545
|
+
if (!(r > tol * 0.5) || !Number.isFinite(sweep)) return [];
|
|
546
|
+
const stepAngle = 2 * Math.acos(Math.min(Math.max(1 - tol / r, -1), 1));
|
|
547
|
+
const n = Math.min(128, Math.max(2, Math.ceil(Math.abs(sweep) / Math.max(stepAngle, 1e-3))));
|
|
548
|
+
const out = [];
|
|
549
|
+
for (let k = 1; k < n; k++) {
|
|
550
|
+
const f = k / n;
|
|
551
|
+
const a = a0 + sweep * f;
|
|
552
|
+
const rr = r0 + (r1 - r0) * f;
|
|
553
|
+
out.push([c[0] + Math.cos(a) * rr, c[1] + Math.sin(a) * rr]);
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
function crossing(a0, a1, b0, b1) {
|
|
558
|
+
const r = sub2(a1, a0);
|
|
559
|
+
const q = sub2(b1, b0);
|
|
560
|
+
const den = r[0] * q[1] - r[1] * q[0];
|
|
561
|
+
if (Math.abs(den) < 1e-12) return null;
|
|
562
|
+
const w = sub2(b0, a0);
|
|
563
|
+
const t = (w[0] * q[1] - w[1] * q[0]) / den;
|
|
564
|
+
const u = (w[0] * r[1] - w[1] * r[0]) / den;
|
|
565
|
+
return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
|
|
566
|
+
}
|
|
567
|
+
function variableStrokeRings(path, options) {
|
|
568
|
+
const half = options.width / 2;
|
|
569
|
+
if (!(half > 0) || !Number.isFinite(half)) return [];
|
|
570
|
+
const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
|
|
571
|
+
const join = options.join ?? "miter";
|
|
572
|
+
const cap = options.cap ?? "butt";
|
|
573
|
+
const miterLimit = options.miterLimit && options.miterLimit >= 1 ? options.miterLimit : 4;
|
|
574
|
+
const rings = [];
|
|
575
|
+
const push = (out, p) => {
|
|
576
|
+
const last = out[out.length - 1];
|
|
577
|
+
if (!last || dist(last, p) > 1e-12) out.push(p);
|
|
578
|
+
};
|
|
579
|
+
for (const contour of pathToContours(path)) {
|
|
580
|
+
if (!contour.segments.length) continue;
|
|
581
|
+
const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
|
|
582
|
+
if (!poly) continue;
|
|
583
|
+
const len2 = poly.s[poly.s.length - 1];
|
|
584
|
+
const sides = (s) => {
|
|
585
|
+
const w = widthProfileAt(options.profile, s / len2);
|
|
586
|
+
return [w.left * half, w.right * half];
|
|
587
|
+
};
|
|
588
|
+
const intervals = options.dash?.some((v2) => v2 > 0) ? dashIntervals(options.dash, options.dashOffset ?? 0, len2) : [[0, len2]];
|
|
589
|
+
const whole = intervals.length === 1 && intervals[0][0] <= 0 && intervals[0][1] >= len2;
|
|
590
|
+
for (const [a, b] of intervals) {
|
|
591
|
+
const [pa, ia] = at(poly, a);
|
|
592
|
+
const [pb, ib] = at(poly, b);
|
|
593
|
+
const pts = [{ p: pa, s: a, corner: poly.corner[0] && a <= 0 }];
|
|
594
|
+
for (let i = ia; i < ib; i++) pts.push({ p: poly.pts[i], s: poly.s[i], corner: poly.corner[i] });
|
|
595
|
+
pts.push({ p: pb, s: b, corner: false });
|
|
596
|
+
const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
|
|
597
|
+
if (run.length < 2) {
|
|
598
|
+
const [l, r] = sides(a);
|
|
599
|
+
const w2 = (l + r) / 2;
|
|
600
|
+
if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const closed = whole && poly.closed;
|
|
604
|
+
const m = run.length - 1;
|
|
605
|
+
const dirs = [];
|
|
606
|
+
for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
|
|
607
|
+
const normals = dirs.map(leftNormal);
|
|
608
|
+
const w = run.map((q) => sides(q.s));
|
|
609
|
+
const side = (sigma) => {
|
|
610
|
+
const k = sigma > 0 ? 0 : 1;
|
|
611
|
+
const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
|
|
612
|
+
const E = (i) => along(run[i + 1].p, normals[i], sigma * w[i + 1][k]);
|
|
613
|
+
const out = [S(0)];
|
|
614
|
+
for (let i = 0; i < m; i++) {
|
|
615
|
+
const seam = i === m - 1;
|
|
616
|
+
if (seam && !closed) {
|
|
617
|
+
push(out, E(i));
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
const j = seam ? 0 : i + 1;
|
|
621
|
+
const p = run[i + 1].p;
|
|
622
|
+
const d0 = dirs[i];
|
|
623
|
+
const d1 = dirs[j];
|
|
624
|
+
const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
|
|
625
|
+
const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
|
|
626
|
+
const r0 = w[i + 1][k];
|
|
627
|
+
const r1 = w[j][k];
|
|
628
|
+
const e = E(i);
|
|
629
|
+
const st = S(j);
|
|
630
|
+
if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
|
|
631
|
+
push(out, e);
|
|
632
|
+
if (!seam) push(out, st);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const uturn = Math.abs(cross2) < 1e-12;
|
|
636
|
+
const outer = uturn ? sigma > 0 : cross2 * sigma > 0;
|
|
637
|
+
if (!outer) {
|
|
638
|
+
const x = seam ? null : crossing(out[out.length - 1], e, st, E(j));
|
|
639
|
+
if (x) push(out, x);
|
|
640
|
+
else {
|
|
641
|
+
push(out, e);
|
|
642
|
+
push(out, p);
|
|
643
|
+
if (!seam) push(out, st);
|
|
644
|
+
}
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
push(out, e);
|
|
648
|
+
const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
|
|
649
|
+
const r = (r0 + r1) / 2;
|
|
650
|
+
const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
|
|
651
|
+
if (r > 1e-9) {
|
|
652
|
+
if (kind === "round") {
|
|
653
|
+
if (r * (1 - Math.cos(Math.abs(theta) / 2)) > tol * 0.25) {
|
|
654
|
+
for (const q of arcPoints(p, Math.atan2(e[1] - p[1], e[0] - p[0]), theta, r0, r1, tol)) push(out, q);
|
|
655
|
+
}
|
|
656
|
+
} else if (kind === "miter") {
|
|
657
|
+
const halfCos = Math.cos(theta / 2);
|
|
658
|
+
const bis = [normals[i][0] + normals[j][0], normals[i][1] + normals[j][1]];
|
|
659
|
+
const bl = Math.hypot(bis[0], bis[1]);
|
|
660
|
+
if (halfCos > 1e-6 && 1 / halfCos <= miterLimit && bl > 1e-9) push(out, along(p, bis, sigma * r / halfCos / bl));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!seam) push(out, st);
|
|
664
|
+
}
|
|
665
|
+
if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
|
|
666
|
+
return out;
|
|
667
|
+
};
|
|
668
|
+
const left = side(1);
|
|
669
|
+
const right = side(-1);
|
|
670
|
+
if (closed) {
|
|
671
|
+
rings.push(left, right.reverse());
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const capPoints = (atStart) => {
|
|
675
|
+
if (cap === "butt") return [];
|
|
676
|
+
const i = atStart ? 0 : m;
|
|
677
|
+
const d = atStart ? dirs[0] : dirs[m - 1];
|
|
678
|
+
const [l, r] = w[i];
|
|
679
|
+
if (!(l + r > 1e-9)) return [];
|
|
680
|
+
const p = run[i].p;
|
|
681
|
+
const n = leftNormal(d);
|
|
682
|
+
const L = along(p, n, l);
|
|
683
|
+
const R = along(p, n, -r);
|
|
684
|
+
if (cap === "round") {
|
|
685
|
+
const a0 = Math.atan2(d[1], d[0]) + (atStart ? Math.PI / 2 : -Math.PI / 2);
|
|
686
|
+
return arcPoints(p, a0, Math.PI, atStart ? r : l, atStart ? l : r, tol);
|
|
687
|
+
}
|
|
688
|
+
const ext = (l + r) / 2;
|
|
689
|
+
const o = atStart ? [-d[0], -d[1]] : d;
|
|
690
|
+
return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
|
|
691
|
+
};
|
|
692
|
+
const ring = [];
|
|
693
|
+
for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
|
|
694
|
+
if (ring.length > 2) rings.push(ring);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return rings;
|
|
698
|
+
}
|
|
699
|
+
function variableStrokeOutline(path, options) {
|
|
700
|
+
const pieces = variableStrokeRings(path, options);
|
|
701
|
+
if (!pieces.length) return [];
|
|
702
|
+
const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
|
|
703
|
+
let polygons;
|
|
704
|
+
try {
|
|
705
|
+
polygons = polygonBoolean("union", [{ rings: pieces, fillRule: "nonzero" }]);
|
|
706
|
+
} catch {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
const out = [];
|
|
710
|
+
for (const polygon2 of polygons) {
|
|
711
|
+
for (const ring of polygon2) {
|
|
712
|
+
if (ring.length < 3) continue;
|
|
713
|
+
const cubics = fitCubics(ring, tol, { closed: true, cornerAngleDeg: 25 });
|
|
714
|
+
if (!cubics.length) continue;
|
|
715
|
+
out.push({ type: "M", x: cubics[0].p0[0], y: cubics[0].p0[1] });
|
|
716
|
+
cubics.forEach((c, i) => {
|
|
717
|
+
if (isStraightCubic(c, tol * 0.02)) {
|
|
718
|
+
if (i < cubics.length - 1) out.push({ type: "L", x: c.p3[0], y: c.p3[1] });
|
|
719
|
+
} else out.push({ type: "C", x1: c.p1[0], y1: c.p1[1], x2: c.p2[0], y2: c.p2[1], x: c.p3[0], y: c.p3[1] });
|
|
720
|
+
});
|
|
721
|
+
out.push({ type: "Z" });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
function nearestStrokePosition(path, p, only) {
|
|
727
|
+
let best = null;
|
|
728
|
+
pathToContours(path).forEach((c, ci) => {
|
|
729
|
+
if (!c.segments.length || only !== void 0 && ci !== only) return;
|
|
730
|
+
const lengths = c.segments.map((s) => segmentLength(s));
|
|
731
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
732
|
+
if (!(total > 0)) return;
|
|
733
|
+
let acc = 0;
|
|
734
|
+
c.segments.forEach((seg, si) => {
|
|
735
|
+
const near2 = segmentNearest(seg, p);
|
|
736
|
+
if (!best || near2.distance < best.distance) {
|
|
737
|
+
const s = acc + segmentLength(seg, 0, near2.t);
|
|
738
|
+
const tan = segmentTangent(seg, near2.t);
|
|
739
|
+
const pt2 = seg.length === 2 ? lerp(seg[0], seg[1], near2.t) : evalSeg(seg, near2.t);
|
|
740
|
+
const n = leftNormal([tan[0], tan[1]]);
|
|
741
|
+
const side = (p[0] - pt2[0]) * n[0] + (p[1] - pt2[1]) * n[1] >= 0 ? "left" : "right";
|
|
742
|
+
best = { contour: ci, t: Math.min(Math.max(s / total, 0), 1), distance: near2.distance, side, point: pt2, tangent: [tan[0], tan[1]], length: total };
|
|
743
|
+
}
|
|
744
|
+
acc += lengths[si];
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
return best;
|
|
748
|
+
}
|
|
749
|
+
function evalSeg(seg, t) {
|
|
750
|
+
let pts = seg.map((q) => [q[0], q[1]]);
|
|
751
|
+
while (pts.length > 1) pts = pts.slice(1).map((q, i) => lerp(pts[i], q, t));
|
|
752
|
+
return pts[0];
|
|
753
|
+
}
|
|
754
|
+
function strokePositionPoint(path, index, t) {
|
|
755
|
+
const c = pathToContours(path)[index];
|
|
756
|
+
if (!c?.segments.length) return null;
|
|
757
|
+
const lengths = c.segments.map((s) => segmentLength(s));
|
|
758
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
759
|
+
let want = Math.min(Math.max(t, 0), 1) * total;
|
|
760
|
+
for (let i = 0; i < c.segments.length; i++) {
|
|
761
|
+
const seg = c.segments[i];
|
|
762
|
+
if (want <= lengths[i] + 1e-12 || i === c.segments.length - 1) {
|
|
763
|
+
let lo = 0;
|
|
764
|
+
let hi = 1;
|
|
765
|
+
const target = Math.min(want, lengths[i]);
|
|
766
|
+
for (let k = 0; k < 40; k++) {
|
|
767
|
+
const mid = (lo + hi) / 2;
|
|
768
|
+
if (segmentLength(seg, 0, mid) < target) lo = mid;
|
|
769
|
+
else hi = mid;
|
|
770
|
+
}
|
|
771
|
+
const u = (lo + hi) / 2;
|
|
772
|
+
const tan = segmentTangent(seg, u);
|
|
773
|
+
return { point: evalSeg(seg, u), tangent: [tan[0], tan[1]] };
|
|
774
|
+
}
|
|
775
|
+
want -= lengths[i];
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/vector/stroke.ts
|
|
781
|
+
var KAPPA = 0.5522847498307936;
|
|
782
|
+
var NOTCH = 0.7;
|
|
783
|
+
function isClosedBezierShape(path) {
|
|
784
|
+
const contours = pathToContours(path).filter((c) => c.segments.length > 0);
|
|
785
|
+
return contours.length > 0 && contours.every((c) => c.closed);
|
|
786
|
+
}
|
|
787
|
+
function effectiveStrokeAlign(style, closedShape) {
|
|
788
|
+
const a = style.strokeAlign ?? "center";
|
|
789
|
+
return closedShape ? a : "center";
|
|
790
|
+
}
|
|
791
|
+
function strokeWidthProfileOf(style) {
|
|
792
|
+
if (!style.strokeWidthProfile?.length) return null;
|
|
793
|
+
const p = sanitizeWidthProfile(style.strokeWidthProfile);
|
|
794
|
+
return isUniformWidthProfile(p) ? null : p;
|
|
795
|
+
}
|
|
796
|
+
function hasStrokeExtras(style) {
|
|
797
|
+
return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow || !!strokeWidthProfileOf(style);
|
|
798
|
+
}
|
|
799
|
+
function vectorStrokeLineArea(parts, style, tolerance) {
|
|
800
|
+
const profile = strokeWidthProfileOf(style);
|
|
801
|
+
const dash = style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {};
|
|
802
|
+
const join = style.strokeLinejoin ?? "miter";
|
|
803
|
+
const cap = style.strokeLinecap ?? "butt";
|
|
804
|
+
if (profile) return variableStrokeOutline(parts.line, { width: parts.drawWidth, profile, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
|
|
805
|
+
return outlineStroke(parts.line, { width: parts.drawWidth, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
|
|
806
|
+
}
|
|
807
|
+
var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
|
|
808
|
+
function arrowheadWeight(style, end) {
|
|
809
|
+
const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
|
|
810
|
+
const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
811
|
+
return Math.max(0.25, (style.strokeWidth ?? 1) * s);
|
|
812
|
+
}
|
|
813
|
+
function arrowheadInset(kind, hw, cap) {
|
|
814
|
+
const l = 4 * hw;
|
|
815
|
+
const solid = (back) => Math.max(back / 2, cap);
|
|
816
|
+
switch (kind) {
|
|
817
|
+
case "triangle":
|
|
818
|
+
case "circle":
|
|
819
|
+
case "square":
|
|
820
|
+
return solid(l);
|
|
821
|
+
case "arrow":
|
|
822
|
+
return solid(NOTCH * l);
|
|
823
|
+
case "bar":
|
|
824
|
+
return solid(0.75 * hw);
|
|
825
|
+
case "open":
|
|
826
|
+
return Math.max(hw * Math.sqrt(l * l + l * l / 4) / (l / 2), cap);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
var v = (x, y) => ({ x, y });
|
|
830
|
+
var plus = (a, b, k = 1) => ({ x: a.x + b.x * k, y: a.y + b.y * k });
|
|
831
|
+
var unit = (a) => {
|
|
832
|
+
const l = Math.hypot(a.x, a.y);
|
|
833
|
+
return l > 0 ? { x: a.x / l, y: a.y / l } : { x: 1, y: 0 };
|
|
834
|
+
};
|
|
835
|
+
function polygon(pts) {
|
|
836
|
+
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" }];
|
|
837
|
+
}
|
|
838
|
+
function circle(c, r) {
|
|
839
|
+
const k = r * KAPPA;
|
|
840
|
+
const { x, y } = c;
|
|
841
|
+
return [
|
|
842
|
+
{ type: "M", x: x + r, y },
|
|
843
|
+
{ type: "C", x1: x + r, y1: y + k, x2: x + k, y2: y + r, x, y: y + r },
|
|
844
|
+
{ type: "C", x1: x - k, y1: y + r, x2: x - r, y2: y + k, x: x - r, y },
|
|
845
|
+
{ type: "C", x1: x - r, y1: y - k, x2: x - k, y2: y - r, x, y: y - r },
|
|
846
|
+
{ type: "C", x1: x + k, y1: y - r, x2: x + r, y2: y - k, x: x + r, y },
|
|
847
|
+
{ type: "Z" }
|
|
848
|
+
];
|
|
849
|
+
}
|
|
850
|
+
function arrowheadPath(kind, tip, dir, hw) {
|
|
851
|
+
const n = v(-dir.y, dir.x);
|
|
852
|
+
const l = 4 * hw;
|
|
853
|
+
const half = l / 2;
|
|
854
|
+
const base = plus(tip, dir, -l);
|
|
855
|
+
switch (kind) {
|
|
856
|
+
case "triangle":
|
|
857
|
+
return polygon([tip, plus(base, n, half), plus(base, n, -half)]);
|
|
858
|
+
case "arrow":
|
|
859
|
+
return polygon([tip, plus(base, n, half), plus(tip, dir, -NOTCH * l), plus(base, n, -half)]);
|
|
860
|
+
case "open": {
|
|
861
|
+
const t = hw;
|
|
862
|
+
const a = plus(base, n, half);
|
|
863
|
+
const b = plus(base, n, -half);
|
|
864
|
+
const ua = unit(plus(a, tip, -1));
|
|
865
|
+
const ub = unit(plus(b, tip, -1));
|
|
866
|
+
const na = v(-ua.y, ua.x);
|
|
867
|
+
const nb = v(ub.y, -ub.x);
|
|
868
|
+
const inner = plus(tip, dir, -(t * Math.hypot(a.x - tip.x, a.y - tip.y)) / half);
|
|
869
|
+
return polygon([tip, a, plus(a, na, t), inner, plus(b, nb, t), b]);
|
|
870
|
+
}
|
|
871
|
+
case "circle":
|
|
872
|
+
return circle(plus(tip, dir, -half), half);
|
|
873
|
+
case "square":
|
|
874
|
+
return polygon([plus(tip, n, half), plus(base, n, half), plus(base, n, -half), plus(tip, n, -half)]);
|
|
875
|
+
case "bar": {
|
|
876
|
+
const t = 0.75 * hw;
|
|
877
|
+
return polygon([plus(tip, n, half), plus(plus(tip, n, half), dir, -t), plus(plus(tip, n, -half), dir, -t), plus(tip, n, -half)]);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
var toVec = (p) => ({ x: p[0], y: p[1] });
|
|
882
|
+
var segLen = (s) => segmentLength(s);
|
|
883
|
+
function endOf(segs, atStart) {
|
|
884
|
+
const long = (s2) => segLen(s2) > 1e-9;
|
|
885
|
+
if (atStart) {
|
|
886
|
+
const s2 = segs.find(long) ?? segs[0];
|
|
887
|
+
const t2 = segmentTangent(s2, 0);
|
|
888
|
+
return { p: toVec(segs[0][0]), out: v(-t2[0], -t2[1]) };
|
|
889
|
+
}
|
|
890
|
+
const s = [...segs].reverse().find(long) ?? segs[segs.length - 1];
|
|
891
|
+
const t = segmentTangent(s, 1);
|
|
892
|
+
const last = segs[segs.length - 1];
|
|
893
|
+
return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
|
|
894
|
+
}
|
|
895
|
+
function pointAlong(segs, fromStart, dist4) {
|
|
896
|
+
const order = fromStart ? segs : [...segs].reverse();
|
|
897
|
+
let acc = 0;
|
|
898
|
+
for (const s of order) {
|
|
899
|
+
const l = segLen(s);
|
|
900
|
+
if (acc + l >= dist4) {
|
|
901
|
+
const want = fromStart ? dist4 - acc : l - (dist4 - acc);
|
|
902
|
+
const pts = trimSegments([s], 0, want);
|
|
903
|
+
const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
|
|
904
|
+
return toVec(p);
|
|
905
|
+
}
|
|
906
|
+
acc += l;
|
|
907
|
+
}
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
function vectorStrokeParts(path, style) {
|
|
911
|
+
const width = style.strokeWidth ?? 1;
|
|
912
|
+
const contours = pathToContours(path).filter((c) => c.segments.length > 0);
|
|
913
|
+
const closedShape = contours.length > 0 && contours.every((c) => c.closed);
|
|
914
|
+
const align = effectiveStrokeAlign(style, closedShape);
|
|
915
|
+
const drawWidth = align === "center" ? width : width * 2;
|
|
916
|
+
const heads = [];
|
|
917
|
+
if (!contours.length || !style.startArrow && !style.endArrow) return { align, line: path, drawWidth, heads };
|
|
918
|
+
const first = contours[0];
|
|
919
|
+
const last = contours[contours.length - 1];
|
|
920
|
+
const cap = capExtent(style, width);
|
|
921
|
+
const tipMode = style.arrowAlign === "tip";
|
|
922
|
+
const trims = contours.map(() => [0, 0]);
|
|
923
|
+
const place = (end, contour, ci) => {
|
|
924
|
+
const kind = end === "start" ? style.startArrow : style.endArrow;
|
|
925
|
+
if (!kind || contour.closed) return;
|
|
926
|
+
const hw = arrowheadWeight(style, end);
|
|
927
|
+
const inset = arrowheadInset(kind, hw, cap);
|
|
928
|
+
const atStart = end === "start";
|
|
929
|
+
const { p, out } = endOf(contour.segments, atStart);
|
|
930
|
+
let tip;
|
|
931
|
+
let dir;
|
|
932
|
+
if (tipMode) {
|
|
933
|
+
const q = pointAlong(contour.segments, atStart, inset);
|
|
934
|
+
const d = q ? v(p.x - q.x, p.y - q.y) : null;
|
|
935
|
+
dir = d && Math.hypot(d.x, d.y) > 1e-9 ? unit(d) : out;
|
|
936
|
+
tip = p;
|
|
937
|
+
trims[ci][atStart ? 0 : 1] = inset;
|
|
938
|
+
} else {
|
|
939
|
+
tip = plus(p, out, inset);
|
|
940
|
+
dir = out;
|
|
941
|
+
}
|
|
942
|
+
heads.push({ end, kind, tip, dir, inset, path: arrowheadPath(kind, tip, dir, hw) });
|
|
943
|
+
};
|
|
944
|
+
place("start", first, 0);
|
|
945
|
+
place("end", last, contours.length - 1);
|
|
946
|
+
if (!tipMode || !heads.length) return { align, line: path, drawWidth, heads };
|
|
947
|
+
const trimmed = contours.map((c, i) => {
|
|
948
|
+
const [s0, s1] = trims[i];
|
|
949
|
+
if (!s0 && !s1) return c;
|
|
950
|
+
const lengths = c.segments.map(segLen);
|
|
951
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
952
|
+
return { closed: false, segments: total - s1 > s0 ? trimSegments(c.segments, s0, total - s1, lengths) : [] };
|
|
953
|
+
});
|
|
954
|
+
return { align, line: contoursToPath(trimmed), drawWidth, heads };
|
|
955
|
+
}
|
|
956
|
+
function vectorStrokeOutlinePath(path, style) {
|
|
957
|
+
const parts = vectorStrokeParts(path, style);
|
|
958
|
+
const stroke = vectorStrokeLineArea(parts, style);
|
|
959
|
+
const fill = { path, fillRule: style.fillRule ?? "nonzero" };
|
|
960
|
+
let area = stroke;
|
|
961
|
+
if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
|
|
962
|
+
else if (parts.align === "outside") area = pathBoolean("difference", [stroke, fill]).path;
|
|
963
|
+
if (!parts.heads.length) return area;
|
|
964
|
+
return pathBoolean("union", [area, ...parts.heads.map((h) => h.path)]).path;
|
|
965
|
+
}
|
|
966
|
+
function hitTestVectorStroke(path, p, style, tolerance = 0) {
|
|
967
|
+
const parts = vectorStrokeParts(path, style);
|
|
968
|
+
const point = [p.x, p.y];
|
|
969
|
+
const width = style.strokeWidth ?? 1;
|
|
970
|
+
for (const h of parts.heads) {
|
|
971
|
+
if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
|
|
972
|
+
}
|
|
973
|
+
if (!parts.line.length) return false;
|
|
974
|
+
const profile = strokeWidthProfileOf(style);
|
|
975
|
+
let d;
|
|
976
|
+
let reach = width / 2;
|
|
977
|
+
if (profile) {
|
|
978
|
+
const near2 = nearestStrokePosition(parts.line, point);
|
|
979
|
+
if (!near2) return false;
|
|
980
|
+
d = near2.distance;
|
|
981
|
+
const w = widthProfileAt(profile, near2.t);
|
|
982
|
+
reach = width / 2 * (near2.side === "left" ? w.left : w.right);
|
|
983
|
+
if (parts.align !== "center") reach *= 2;
|
|
984
|
+
const atEnd = near2.t <= 1e-9 || near2.t >= 1 - 1e-9;
|
|
985
|
+
if (atEnd && (style.strokeLinecap === "round" || style.strokeLinecap === "square") && d > reach) {
|
|
986
|
+
reach = Math.max(reach, width / 2 * (w.left + w.right) / 2);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
d = distanceToBezierPath(parts.line, point);
|
|
990
|
+
if (parts.align !== "center") reach = width;
|
|
991
|
+
}
|
|
992
|
+
if (parts.align === "center") return d <= reach + tolerance;
|
|
993
|
+
if (d > reach + tolerance) return false;
|
|
994
|
+
if (d <= tolerance) return true;
|
|
995
|
+
const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
|
|
996
|
+
return parts.align === "inside" ? inside : !inside;
|
|
997
|
+
}
|
|
998
|
+
function vectorStrokeReach(style) {
|
|
999
|
+
const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
|
|
1000
|
+
const align = style.strokeAlign ?? "center";
|
|
1001
|
+
let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
|
|
1002
|
+
if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
|
|
1003
|
+
for (const end of ["start", "end"]) {
|
|
1004
|
+
if (end === "start" ? style.startArrow : style.endArrow) reach = Math.max(reach, 8 * arrowheadWeight(style, end) + width);
|
|
1005
|
+
}
|
|
1006
|
+
return reach;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/vector/geometry.ts
|
|
1010
|
+
var EPS2 = 1e-9;
|
|
38
1011
|
var cloneAnchor = (a) => ({
|
|
39
1012
|
x: a.x,
|
|
40
1013
|
y: a.y,
|
|
41
1014
|
...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
|
|
42
1015
|
...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
|
|
43
|
-
...a.type && a.type !== "corner" ? { type: a.type } : {}
|
|
1016
|
+
...a.type && a.type !== "corner" ? { type: a.type } : {},
|
|
1017
|
+
...a.corner ? { corner: { ...a.corner } } : {}
|
|
44
1018
|
});
|
|
45
1019
|
var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
|
|
46
1020
|
function cloneVectorNode(node) {
|
|
@@ -58,7 +1032,7 @@ function hasPaint(paint) {
|
|
|
58
1032
|
return true;
|
|
59
1033
|
}
|
|
60
1034
|
var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
|
|
61
|
-
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) <
|
|
1035
|
+
var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
|
|
62
1036
|
function subpathSegmentCount(sp) {
|
|
63
1037
|
const n = sp.anchors.length;
|
|
64
1038
|
if (n < 2) return 0;
|
|
@@ -72,9 +1046,9 @@ function subpathSegment(sp, i) {
|
|
|
72
1046
|
const c2 = handleOf(b, "in");
|
|
73
1047
|
return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
|
|
74
1048
|
}
|
|
75
|
-
function subpathsToBezier(subpaths) {
|
|
1049
|
+
function subpathsToBezier(subpaths, options = {}) {
|
|
76
1050
|
const out = [];
|
|
77
|
-
for (const sp of subpaths) {
|
|
1051
|
+
for (const sp of options.corners === false ? subpaths : expandVectorCorners(subpaths)) {
|
|
78
1052
|
if (sp.anchors.length === 0) continue;
|
|
79
1053
|
const first = sp.anchors[0];
|
|
80
1054
|
out.push({ type: "M", x: first.x, y: first.y });
|
|
@@ -104,9 +1078,9 @@ function inferAnchorType(a) {
|
|
|
104
1078
|
const oy = a.out.y - a.y;
|
|
105
1079
|
const li = Math.hypot(ix, iy);
|
|
106
1080
|
const lo = Math.hypot(ox, oy);
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
if (Math.abs(
|
|
1081
|
+
const cross2 = (ix * oy - iy * ox) / (li * lo);
|
|
1082
|
+
const dot2 = (ix * ox + iy * oy) / (li * lo);
|
|
1083
|
+
if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
|
|
110
1084
|
return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
|
|
111
1085
|
}
|
|
112
1086
|
function bezierToSubpaths(input) {
|
|
@@ -249,8 +1223,12 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
|
249
1223
|
if (path.length === 0) return false;
|
|
250
1224
|
const point = [p.x, p.y];
|
|
251
1225
|
if (hasPaint(node.fill) && pointInBezierPath(path, point, node.fillRule ?? "nonzero")) return true;
|
|
252
|
-
|
|
253
|
-
return distanceToBezierPath(path, point) <=
|
|
1226
|
+
if (hasPaint(node.stroke)) return hitTestVectorStroke(path, p, node, tolerance);
|
|
1227
|
+
return distanceToBezierPath(path, point) <= tolerance;
|
|
1228
|
+
}
|
|
1229
|
+
function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
|
|
1230
|
+
if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
|
|
1231
|
+
return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
|
|
254
1232
|
}
|
|
255
1233
|
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
256
1234
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -268,8 +1246,8 @@ function nearestSegment(node, p) {
|
|
|
268
1246
|
const seg = subpathSegment(sp, i);
|
|
269
1247
|
const hit = segmentNearest(seg, [p.x, p.y]);
|
|
270
1248
|
if (!best || hit.distance < best.distance) {
|
|
271
|
-
const
|
|
272
|
-
best = { subpath: s, segment: i, t: hit.t, point: { x:
|
|
1249
|
+
const pt2 = segmentPoint(seg, hit.t);
|
|
1250
|
+
best = { subpath: s, segment: i, t: hit.t, point: { x: pt2[0], y: pt2[1] }, distance: hit.distance };
|
|
273
1251
|
}
|
|
274
1252
|
}
|
|
275
1253
|
});
|
|
@@ -303,8 +1281,8 @@ function invertMatrix(m) {
|
|
|
303
1281
|
if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
|
|
304
1282
|
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
305
1283
|
}
|
|
306
|
-
function applyMatrixLinear(m,
|
|
307
|
-
return { x: m[0] *
|
|
1284
|
+
function applyMatrixLinear(m, v2) {
|
|
1285
|
+
return { x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y };
|
|
308
1286
|
}
|
|
309
1287
|
function matrixRotation(m) {
|
|
310
1288
|
const [a, b, c, d] = m;
|
|
@@ -328,8 +1306,10 @@ function bakeIntoGeometry(node, m) {
|
|
|
328
1306
|
if (isIdentityMatrix(m)) return node;
|
|
329
1307
|
if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
|
|
330
1308
|
const path = node;
|
|
1309
|
+
const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
|
|
331
1310
|
return {
|
|
332
1311
|
...path,
|
|
1312
|
+
...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
|
|
333
1313
|
subpaths: path.subpaths.map((sp) => ({
|
|
334
1314
|
closed: sp.closed,
|
|
335
1315
|
anchors: sp.anchors.map((a) => {
|
|
@@ -338,6 +1318,7 @@ function bakeIntoGeometry(node, m) {
|
|
|
338
1318
|
if (a.in) next.in = applyMatrix(m, a.in);
|
|
339
1319
|
if (a.out) next.out = applyMatrix(m, a.out);
|
|
340
1320
|
if (a.type) next.type = a.type;
|
|
1321
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
341
1322
|
return next;
|
|
342
1323
|
})
|
|
343
1324
|
}))
|
|
@@ -389,12 +1370,12 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
|
|
|
389
1370
|
if (breakHandles) delete next.type;
|
|
390
1371
|
const dx = pos.x - a.x;
|
|
391
1372
|
const dy = pos.y - a.y;
|
|
392
|
-
const
|
|
1373
|
+
const len2 = Math.hypot(dx, dy);
|
|
393
1374
|
if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
|
|
394
|
-
else if (type === "smooth" &&
|
|
1375
|
+
else if (type === "smooth" && len2 > EPS2) {
|
|
395
1376
|
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 /
|
|
1377
|
+
const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
|
|
1378
|
+
next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
|
|
398
1379
|
}
|
|
399
1380
|
if (isOnAnchor(next, next.in)) delete next.in;
|
|
400
1381
|
if (isOnAnchor(next, next.out)) delete next.out;
|
|
@@ -421,15 +1402,15 @@ function convertAnchor(sp, index, type) {
|
|
|
421
1402
|
else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
|
|
422
1403
|
else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
|
|
423
1404
|
else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
|
|
424
|
-
if (!dir || Math.hypot(dir.x, dir.y) <
|
|
1405
|
+
if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
|
|
425
1406
|
const dl = Math.hypot(dir.x, dir.y);
|
|
426
1407
|
const ux = dir.x / dl;
|
|
427
1408
|
const uy = dir.y / dl;
|
|
428
1409
|
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
1410
|
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
1411
|
if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
|
|
431
|
-
if (lenIn >
|
|
432
|
-
if (lenOut >
|
|
1412
|
+
if (lenIn > EPS2) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
|
|
1413
|
+
if (lenOut > EPS2) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
|
|
433
1414
|
a.type = type;
|
|
434
1415
|
return { anchors, closed: sp.closed };
|
|
435
1416
|
}
|
|
@@ -465,37 +1446,24 @@ function reverseSubpath(sp) {
|
|
|
465
1446
|
if (a.out) next.in = { ...a.out };
|
|
466
1447
|
if (a.in) next.out = { ...a.in };
|
|
467
1448
|
if (a.type) next.type = a.type;
|
|
1449
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
468
1450
|
return next;
|
|
469
1451
|
});
|
|
470
1452
|
return { anchors, closed: sp.closed };
|
|
471
1453
|
}
|
|
472
|
-
var
|
|
473
|
-
function rectSubpath(b, radius = 0) {
|
|
1454
|
+
var KAPPA2 = 0.5522847498307936;
|
|
1455
|
+
function rectSubpath(b, radius = 0, kind) {
|
|
474
1456
|
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 };
|
|
1457
|
+
const sp = { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
|
|
1458
|
+
return radius > 0 ? setSubpathCorners(sp, radius, kind) : sp;
|
|
491
1459
|
}
|
|
492
1460
|
function ellipseSubpath(b) {
|
|
493
1461
|
const rx = b.width / 2;
|
|
494
1462
|
const ry = b.height / 2;
|
|
495
1463
|
const cx = b.x + rx;
|
|
496
1464
|
const cy = b.y + ry;
|
|
497
|
-
const kx = rx *
|
|
498
|
-
const ky = ry *
|
|
1465
|
+
const kx = rx * KAPPA2;
|
|
1466
|
+
const ky = ry * KAPPA2;
|
|
499
1467
|
return {
|
|
500
1468
|
closed: true,
|
|
501
1469
|
anchors: [
|
|
@@ -543,15 +1511,80 @@ function roundSubpaths(subpaths, precision = 3) {
|
|
|
543
1511
|
}
|
|
544
1512
|
|
|
545
1513
|
// src/vector/svg.ts
|
|
1514
|
+
function vectorCornersAttribute(subpaths, precision = 3) {
|
|
1515
|
+
const out = [];
|
|
1516
|
+
subpaths.forEach(
|
|
1517
|
+
(sp, s) => sp.anchors.forEach((a, i) => {
|
|
1518
|
+
if (!a.corner || !(a.corner.radius > 0)) return;
|
|
1519
|
+
out.push(`${s}.${i}:${formatNumber(a.corner.radius, precision)}${a.corner.kind && a.corner.kind !== "round" ? `:${a.corner.kind}` : ""}`);
|
|
1520
|
+
})
|
|
1521
|
+
);
|
|
1522
|
+
return out.join(" ");
|
|
1523
|
+
}
|
|
1524
|
+
function applyVectorCornersAttribute(subpaths, value) {
|
|
1525
|
+
for (const item of value.trim().split(/\s+/)) {
|
|
1526
|
+
const m = /^(\d+)\.(\d+):([\d.eE+-]+)(?::([a-z]+))?$/.exec(item);
|
|
1527
|
+
if (!m) continue;
|
|
1528
|
+
const a = subpaths[+m[1]]?.anchors[+m[2]];
|
|
1529
|
+
const radius = parseFloat(m[3]);
|
|
1530
|
+
if (!a || !(radius > 0)) continue;
|
|
1531
|
+
const kind = VECTOR_CORNER_KINDS.includes(m[4]) ? m[4] : void 0;
|
|
1532
|
+
a.corner = { radius, ...kind && kind !== "round" ? { kind } : {} };
|
|
1533
|
+
}
|
|
1534
|
+
return subpaths;
|
|
1535
|
+
}
|
|
546
1536
|
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
547
1537
|
function vectorPaintToSvg(paint, gradientId) {
|
|
548
1538
|
if (!hasPaint(paint)) return { value: "none", gradient: null };
|
|
549
1539
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
550
1540
|
return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
|
|
551
1541
|
}
|
|
1542
|
+
var partsCache = /* @__PURE__ */ new WeakMap();
|
|
1543
|
+
function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
1544
|
+
if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
|
|
1545
|
+
const hit = partsCache.get(node);
|
|
1546
|
+
if (hit && hit.clipId === clipId && hit.precision === precision) return hit.parts;
|
|
1547
|
+
const parts = computeStrokeSvgParts(node, clipId, precision);
|
|
1548
|
+
partsCache.set(node, { clipId, precision, parts });
|
|
1549
|
+
return parts;
|
|
1550
|
+
}
|
|
1551
|
+
function computeStrokeSvgParts(node, clipId, precision) {
|
|
1552
|
+
const path = subpathsToBezier(node.subpaths);
|
|
1553
|
+
const parts = vectorStrokeParts(path, node);
|
|
1554
|
+
const profiled = !!strokeWidthProfileOf(node);
|
|
1555
|
+
if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
|
|
1556
|
+
let clip = null;
|
|
1557
|
+
if (parts.align !== "center") {
|
|
1558
|
+
const b = bezierPathBounds(path);
|
|
1559
|
+
const pad = vectorStrokeReach(node) + 1;
|
|
1560
|
+
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 };
|
|
1561
|
+
clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
align: parts.align,
|
|
1565
|
+
d: bezierPathToSvg(parts.line, { precision }),
|
|
1566
|
+
width: parts.drawWidth,
|
|
1567
|
+
...profiled && (node.strokeWidth ?? 1) > 0 ? { outline: bezierPathToSvg(vectorStrokeLineArea(parts, node), { precision }) } : {},
|
|
1568
|
+
clip,
|
|
1569
|
+
heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision }))
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function widthProfileToAttribute(points, precision = 4) {
|
|
1573
|
+
return points.map((p) => [p.t, p.left, p.right].map((n) => formatNumber(n, precision)).join(",")).join(";");
|
|
1574
|
+
}
|
|
1575
|
+
function widthProfileFromAttribute(value) {
|
|
1576
|
+
if (!value) return [];
|
|
1577
|
+
return sanitizeWidthProfile(
|
|
1578
|
+
value.split(";").map((part) => {
|
|
1579
|
+
const [t, left, right] = part.split(",").map((n) => parseFloat(n));
|
|
1580
|
+
return { t, left, right: Number.isFinite(right) ? right : left };
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
552
1585
|
function gradientMarkup(def) {
|
|
553
1586
|
const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
|
|
554
|
-
const attrs = Object.entries(def.attrs).map(([k,
|
|
1587
|
+
const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
|
|
555
1588
|
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
1589
|
return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
|
|
557
1590
|
}
|
|
@@ -582,15 +1615,61 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
582
1615
|
const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
|
|
583
1616
|
if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
|
|
584
1617
|
if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (node.
|
|
592
|
-
|
|
593
|
-
|
|
1618
|
+
const d = vectorPathData(node, precision);
|
|
1619
|
+
const corners2 = vectorCornersAttribute(node.subpaths, precision);
|
|
1620
|
+
const live = corners2 ? ` data-vector-d="${bezierPathToSvg(subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
|
|
1621
|
+
const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
|
|
1622
|
+
const lineAttrs = (width) => {
|
|
1623
|
+
let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
|
|
1624
|
+
if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
|
|
1625
|
+
if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
|
|
1626
|
+
if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
|
|
1627
|
+
return out2;
|
|
1628
|
+
};
|
|
1629
|
+
const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
|
|
1630
|
+
if (!parts) {
|
|
1631
|
+
lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(node.strokeWidth ?? 1) : ""}${live}/>`);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
let data = ` data-vector-path="" data-vector-stroke-width="${num(node.strokeWidth ?? 1)}"`;
|
|
1635
|
+
if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
|
|
1636
|
+
if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
|
|
1637
|
+
if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
|
|
1638
|
+
if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
|
|
1639
|
+
if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
|
|
1640
|
+
if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
|
|
1641
|
+
const profile = strokeWidthProfileOf(node);
|
|
1642
|
+
if (profile) {
|
|
1643
|
+
data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
|
|
1644
|
+
if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
|
|
1645
|
+
if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
|
|
1646
|
+
if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
|
|
1647
|
+
}
|
|
1648
|
+
lines.push(`${indent(depth)}<g${common(node)}${data}>`);
|
|
1649
|
+
lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
|
|
1650
|
+
if (options.strokes === "outline") {
|
|
1651
|
+
const area = bezierPathToSvg(vectorStrokeOutlinePath(subpathsToBezier(node.subpaths), node), { precision });
|
|
1652
|
+
lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${area}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1653
|
+
} else {
|
|
1654
|
+
let clip = "";
|
|
1655
|
+
if (parts.clip) {
|
|
1656
|
+
const rule = node.fillRule === "evenodd" ? "evenodd" : "nonzero";
|
|
1657
|
+
const id = escapeAttr(parts.clip.id);
|
|
1658
|
+
if (parts.clip.kind === "clip") {
|
|
1659
|
+
defs.push(`<clipPath id="${id}"><path d="${d}" clip-rule="${rule}"/></clipPath>`);
|
|
1660
|
+
clip = ` clip-path="url(#${id})"`;
|
|
1661
|
+
} else {
|
|
1662
|
+
const b = parts.clip.bounds;
|
|
1663
|
+
const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
|
|
1664
|
+
defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
|
|
1665
|
+
clip = ` mask="url(#${id})"`;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (parts.outline !== void 0) lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${parts.outline}" fill="${escapeAttr(stroke.value)}"${clip}/>`);
|
|
1669
|
+
else lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
|
|
1670
|
+
for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1671
|
+
}
|
|
1672
|
+
lines.push(`${indent(depth)}</g>`);
|
|
594
1673
|
};
|
|
595
1674
|
for (const node of doc.nodes) emit(node, 1);
|
|
596
1675
|
const w = num(doc.width);
|
|
@@ -765,11 +1844,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
765
1844
|
const rawGradients = /* @__PURE__ */ new Map();
|
|
766
1845
|
collectGradients(root, rawGradients);
|
|
767
1846
|
const paint = (value) => {
|
|
768
|
-
const
|
|
769
|
-
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(
|
|
1847
|
+
const v2 = value.trim();
|
|
1848
|
+
const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v2);
|
|
770
1849
|
if (url) return resolveGradient(url[1], rawGradients) ?? null;
|
|
771
|
-
if (
|
|
772
|
-
return
|
|
1850
|
+
if (v2 === "none" || v2 === "") return null;
|
|
1851
|
+
return v2;
|
|
773
1852
|
};
|
|
774
1853
|
const viewBox = numberList(root.attrs.viewBox);
|
|
775
1854
|
const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
|
|
@@ -779,8 +1858,13 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
779
1858
|
const shapeOf = (el) => {
|
|
780
1859
|
const a = el.attrs;
|
|
781
1860
|
switch (el.name) {
|
|
782
|
-
case "path":
|
|
1861
|
+
case "path": {
|
|
1862
|
+
if (a["data-vector-d"] && a["data-vector-corners"]) {
|
|
1863
|
+
const sharp = bezierToSubpaths(a["data-vector-d"]);
|
|
1864
|
+
if (sharp.length) return applyVectorCornersAttribute(sharp, a["data-vector-corners"]);
|
|
1865
|
+
}
|
|
783
1866
|
return a.d ? bezierToSubpaths(a.d) : null;
|
|
1867
|
+
}
|
|
784
1868
|
case "rect": {
|
|
785
1869
|
const w = length(a.width);
|
|
786
1870
|
const h = length(a.height);
|
|
@@ -814,6 +1898,65 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
814
1898
|
}
|
|
815
1899
|
return null;
|
|
816
1900
|
};
|
|
1901
|
+
const strokeStyle = (s) => ({
|
|
1902
|
+
...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
|
|
1903
|
+
...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
|
|
1904
|
+
...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
|
|
1905
|
+
...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
|
|
1906
|
+
...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
|
|
1907
|
+
});
|
|
1908
|
+
const placed = (node, m) => {
|
|
1909
|
+
if (m === IDENTITY_MATRIX) return node;
|
|
1910
|
+
const next = transformVectorNode(node, m);
|
|
1911
|
+
const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
1912
|
+
if (hasPaint(next.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) next.strokeWidth = (next.strokeWidth ?? 1) * strokeScale;
|
|
1913
|
+
return next;
|
|
1914
|
+
};
|
|
1915
|
+
const mergedPath = (g, style, m, meta) => {
|
|
1916
|
+
const part = (name) => g.children.find((c) => c.name === "path" && c.attrs["data-vector-part"] === name);
|
|
1917
|
+
const fillEl = part("fill");
|
|
1918
|
+
const subpaths = fillEl ? shapeOf(fillEl) : null;
|
|
1919
|
+
if (!fillEl || !subpaths?.length) return null;
|
|
1920
|
+
const a = g.attrs;
|
|
1921
|
+
const fs = readStyle(fillEl, style);
|
|
1922
|
+
const strokeEl = part("stroke");
|
|
1923
|
+
const outlineEl = part("stroke-outline");
|
|
1924
|
+
const ss = strokeEl ? readStyle(strokeEl, style) : null;
|
|
1925
|
+
const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
|
|
1926
|
+
const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
|
|
1927
|
+
const scale = (v2) => {
|
|
1928
|
+
const n = v2 === void 0 ? NaN : parseFloat(v2);
|
|
1929
|
+
return n > 0 ? n : void 0;
|
|
1930
|
+
};
|
|
1931
|
+
const align = a["data-vector-stroke-align"];
|
|
1932
|
+
const profile = widthProfileFromAttribute(a["data-vector-width-profile"]);
|
|
1933
|
+
const kept = (key, values) => values.includes(a[key]) ? a[key] : void 0;
|
|
1934
|
+
const keptCap = kept("data-vector-stroke-linecap", ["round", "square"]);
|
|
1935
|
+
const keptJoin = kept("data-vector-stroke-linejoin", ["round", "bevel"]);
|
|
1936
|
+
const keptDash = a["data-vector-stroke-dasharray"] ? numberList(a["data-vector-stroke-dasharray"]) : void 0;
|
|
1937
|
+
const node = {
|
|
1938
|
+
id: idFor(g),
|
|
1939
|
+
type: "path",
|
|
1940
|
+
...meta,
|
|
1941
|
+
subpaths,
|
|
1942
|
+
fill: paint(fs.fill),
|
|
1943
|
+
stroke: ss ? paint(ss.stroke) : outlineEl ? paint(readStyle(outlineEl, style).fill) : null,
|
|
1944
|
+
...ss ? strokeStyle(ss) : {},
|
|
1945
|
+
...fs.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
|
|
1946
|
+
strokeWidth: width2,
|
|
1947
|
+
...align === "inside" || align === "outside" ? { strokeAlign: align } : {},
|
|
1948
|
+
...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
|
|
1949
|
+
...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
|
|
1950
|
+
...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
|
|
1951
|
+
...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
|
|
1952
|
+
...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
|
|
1953
|
+
...profile.length ? { strokeWidthProfile: profile } : {},
|
|
1954
|
+
...keptCap ? { strokeLinecap: keptCap } : {},
|
|
1955
|
+
...keptJoin ? { strokeLinejoin: keptJoin } : {},
|
|
1956
|
+
...keptDash?.length ? { strokeDasharray: keptDash } : {}
|
|
1957
|
+
};
|
|
1958
|
+
return placed(node, m);
|
|
1959
|
+
};
|
|
817
1960
|
const visit = (el, style, matrix) => {
|
|
818
1961
|
const out = [];
|
|
819
1962
|
for (const child of el.children) {
|
|
@@ -827,6 +1970,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
827
1970
|
...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
|
|
828
1971
|
...hidden ? { visible: false } : {}
|
|
829
1972
|
};
|
|
1973
|
+
if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
|
|
1974
|
+
const node2 = mergedPath(child, s, m, meta);
|
|
1975
|
+
if (node2) out.push(node2);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
830
1978
|
if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
|
|
831
1979
|
const children = visit(child, s, m);
|
|
832
1980
|
if (!children.length) continue;
|
|
@@ -836,24 +1984,18 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
836
1984
|
}
|
|
837
1985
|
const subpaths = shapeOf(child);
|
|
838
1986
|
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
|
-
}
|
|
1987
|
+
const node = placed(
|
|
1988
|
+
{
|
|
1989
|
+
id: idFor(child),
|
|
1990
|
+
type: "path",
|
|
1991
|
+
...meta,
|
|
1992
|
+
subpaths,
|
|
1993
|
+
fill: child.name === "line" ? null : paint(s.fill),
|
|
1994
|
+
stroke: paint(s.stroke),
|
|
1995
|
+
...strokeStyle(s)
|
|
1996
|
+
},
|
|
1997
|
+
m
|
|
1998
|
+
);
|
|
857
1999
|
out.push(node);
|
|
858
2000
|
}
|
|
859
2001
|
return out;
|
|
@@ -876,9 +2018,9 @@ function constrainToLeave(leave, p) {
|
|
|
876
2018
|
return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
|
|
877
2019
|
}
|
|
878
2020
|
default: {
|
|
879
|
-
const
|
|
2021
|
+
const len2 = Math.hypot(dx, dy);
|
|
880
2022
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
881
|
-
return { x: leave.at.x + Math.cos(a) *
|
|
2023
|
+
return { x: leave.at.x + Math.cos(a) * len2, y: leave.at.y + Math.sin(a) * len2 };
|
|
882
2024
|
}
|
|
883
2025
|
}
|
|
884
2026
|
}
|
|
@@ -887,24 +2029,24 @@ var CROSSING_PAIRS = 32;
|
|
|
887
2029
|
var CURVE_SAMPLES = 48;
|
|
888
2030
|
var BISECT = 50;
|
|
889
2031
|
var EQUAL = 1e-6;
|
|
890
|
-
function bound(sorted, key,
|
|
2032
|
+
function bound(sorted, key, v2, strict) {
|
|
891
2033
|
let lo = 0;
|
|
892
2034
|
let hi = sorted.length;
|
|
893
2035
|
while (lo < hi) {
|
|
894
2036
|
const mid = lo + hi >>> 1;
|
|
895
2037
|
const k = key(sorted[mid]);
|
|
896
|
-
if (strict ? k <=
|
|
2038
|
+
if (strict ? k <= v2 : k < v2) lo = mid + 1;
|
|
897
2039
|
else hi = mid;
|
|
898
2040
|
}
|
|
899
2041
|
return lo;
|
|
900
2042
|
}
|
|
901
|
-
function within(sorted, key,
|
|
902
|
-
return sorted.slice(bound(sorted, key,
|
|
2043
|
+
function within(sorted, key, v2, tol) {
|
|
2044
|
+
return sorted.slice(bound(sorted, key, v2 - tol, false), bound(sorted, key, v2 + tol, true));
|
|
903
2045
|
}
|
|
904
|
-
var linesNear = (lines,
|
|
2046
|
+
var linesNear = (lines, v2, tol) => within(lines, (l) => l.v, v2, tol);
|
|
905
2047
|
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
906
2048
|
var toPoint = (p) => [p.x, p.y];
|
|
907
|
-
var
|
|
2049
|
+
var toVec2 = (p) => ({ x: p[0], y: p[1] });
|
|
908
2050
|
var LABELS = {
|
|
909
2051
|
anchor: "anchor",
|
|
910
2052
|
centre: "centre",
|
|
@@ -1022,7 +2164,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1022
2164
|
const count = subpathSegmentCount(sp);
|
|
1023
2165
|
for (let i = 0; i < count && budget > 0; i++) {
|
|
1024
2166
|
if (moving(i) || moving((i + 1) % n)) continue;
|
|
1025
|
-
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m,
|
|
2167
|
+
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec2(q))));
|
|
1026
2168
|
let minX = Infinity;
|
|
1027
2169
|
let minY = Infinity;
|
|
1028
2170
|
let maxX = -Infinity;
|
|
@@ -1131,7 +2273,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1131
2273
|
*/
|
|
1132
2274
|
snapFrom(p, leave, tol) {
|
|
1133
2275
|
const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
|
|
1134
|
-
const
|
|
2276
|
+
const at2 = (q, kind, lines2 = []) => ({
|
|
1135
2277
|
point: q,
|
|
1136
2278
|
kind,
|
|
1137
2279
|
x: true,
|
|
@@ -1145,16 +2287,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1145
2287
|
return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
|
|
1146
2288
|
}
|
|
1147
2289
|
const point = this.nearestPoint(p, tol);
|
|
1148
|
-
if (point) return
|
|
2290
|
+
if (point) return at2(point.p, point.kind);
|
|
1149
2291
|
const near2 = this.segmentsNear(p, tol);
|
|
1150
|
-
const
|
|
1151
|
-
if (
|
|
2292
|
+
const crossing2 = this.crossing(near2, p, tol);
|
|
2293
|
+
if (crossing2) return at2(crossing2, "intersect");
|
|
1152
2294
|
if (leave && leave.curves !== false) {
|
|
1153
2295
|
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1154
|
-
if (foot) return
|
|
2296
|
+
if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1155
2297
|
}
|
|
1156
2298
|
const on = this.onSegment(near2);
|
|
1157
|
-
if (on) return
|
|
2299
|
+
if (on) return at2(on, "path");
|
|
1158
2300
|
if (leave?.angles?.length) {
|
|
1159
2301
|
const c = construction(leave, p, tol);
|
|
1160
2302
|
if (c) {
|
|
@@ -1166,9 +2308,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1166
2308
|
return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
|
|
1167
2309
|
}
|
|
1168
2310
|
}
|
|
1169
|
-
const nearest = (lines2,
|
|
2311
|
+
const nearest = (lines2, v2) => {
|
|
1170
2312
|
let best = null;
|
|
1171
|
-
for (const l of linesNear(lines2,
|
|
2313
|
+
for (const l of linesNear(lines2, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
|
|
1172
2314
|
return best;
|
|
1173
2315
|
};
|
|
1174
2316
|
const bx = nearest(this.xs, p.x);
|
|
@@ -1201,7 +2343,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1201
2343
|
* the nearest target line crosses it, at most twice `tol` away: where it lands and the
|
|
1202
2344
|
* target point that line comes from.
|
|
1203
2345
|
*/
|
|
1204
|
-
slide(
|
|
2346
|
+
slide(at2, dir, q, tol, ray) {
|
|
1205
2347
|
const len2 = dir.x * dir.x + dir.y * dir.y;
|
|
1206
2348
|
if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
|
|
1207
2349
|
const reach = 2 * tol;
|
|
@@ -1209,10 +2351,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1209
2351
|
for (const vertical of [true, false]) {
|
|
1210
2352
|
const d = vertical ? dir.x : dir.y;
|
|
1211
2353
|
if (Math.abs(d) <= 1e-9) continue;
|
|
1212
|
-
const a = vertical ?
|
|
2354
|
+
const a = vertical ? at2.x : at2.y;
|
|
1213
2355
|
for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
|
|
1214
2356
|
const s = (l.v - a) / d;
|
|
1215
|
-
const hit = vertical ? { x: l.v, y:
|
|
2357
|
+
const hit = vertical ? { x: l.v, y: at2.y + dir.y * s } : { x: at2.x + dir.x * s, y: l.v };
|
|
1216
2358
|
const moved = distance(hit, q);
|
|
1217
2359
|
if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
|
|
1218
2360
|
}
|
|
@@ -1228,16 +2370,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1228
2370
|
/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
|
|
1229
2371
|
segmentsNear(p, tol) {
|
|
1230
2372
|
const out = [];
|
|
1231
|
-
const
|
|
2373
|
+
const pt2 = toPoint(p);
|
|
1232
2374
|
for (const s of this.segments) {
|
|
1233
2375
|
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,
|
|
2376
|
+
const hit = segmentNearest(s.seg, pt2);
|
|
1235
2377
|
if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
|
|
1236
2378
|
}
|
|
1237
2379
|
return out.sort((a, b) => a.distance - b.distance);
|
|
1238
2380
|
}
|
|
1239
2381
|
onSegment(near2) {
|
|
1240
|
-
return near2.length ?
|
|
2382
|
+
return near2.length ? toVec2(segmentPoint(near2[0].seg, near2[0].t)) : null;
|
|
1241
2383
|
}
|
|
1242
2384
|
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1243
2385
|
crossing(near2, p, tol) {
|
|
@@ -1247,7 +2389,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1247
2389
|
for (let i = 0; i < list.length; i++) {
|
|
1248
2390
|
for (let j = i + 1; j < list.length; j++) {
|
|
1249
2391
|
for (const hit of segmentIntersections(list[i].seg, list[j].seg)) {
|
|
1250
|
-
const q =
|
|
2392
|
+
const q = toVec2(hit.point);
|
|
1251
2393
|
const d = distance(q, p);
|
|
1252
2394
|
if (d <= tol && d < bestD) {
|
|
1253
2395
|
best = q;
|
|
@@ -1288,7 +2430,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1288
2430
|
const axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };
|
|
1289
2431
|
const nearest = (lines, values) => {
|
|
1290
2432
|
let best = null;
|
|
1291
|
-
for (const
|
|
2433
|
+
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
2434
|
return best;
|
|
1293
2435
|
};
|
|
1294
2436
|
const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
|
|
@@ -1331,7 +2473,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1331
2473
|
evenSpacing(r, horizontal, tol) {
|
|
1332
2474
|
const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
|
|
1333
2475
|
const [r0, r1] = span(r, horizontal);
|
|
1334
|
-
const
|
|
2476
|
+
const len2 = r1 - r0;
|
|
1335
2477
|
const [c0, c1] = span(r, !horizontal);
|
|
1336
2478
|
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
2479
|
if (!row.length) return null;
|
|
@@ -1351,20 +2493,20 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1351
2493
|
if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
|
|
1352
2494
|
}
|
|
1353
2495
|
const starts = [];
|
|
1354
|
-
if (before && after && after.a0 - before.a1 >
|
|
2496
|
+
if (before && after && after.a0 - before.a1 > len2) starts.push({ start: (before.a1 + after.a0 - len2) / 2, repeats: null });
|
|
1355
2497
|
for (const { g } of pairs) {
|
|
1356
2498
|
if (before) starts.push({ start: before.a1 + g, repeats: g });
|
|
1357
|
-
if (after) starts.push({ start: after.a0 - g -
|
|
2499
|
+
if (after) starts.push({ start: after.a0 - g - len2, repeats: g });
|
|
1358
2500
|
}
|
|
1359
2501
|
let pick = null;
|
|
1360
2502
|
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
2503
|
if (!pick) return null;
|
|
1362
2504
|
const { start, repeats } = pick;
|
|
1363
|
-
const
|
|
1364
|
-
const end = start +
|
|
2505
|
+
const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
2506
|
+
const end = start + len2;
|
|
1365
2507
|
const gaps = [];
|
|
1366
|
-
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b,
|
|
1367
|
-
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b,
|
|
2508
|
+
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at2)]);
|
|
2509
|
+
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at2)]);
|
|
1368
2510
|
if (repeats !== null) {
|
|
1369
2511
|
for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
|
|
1370
2512
|
}
|
|
@@ -1372,9 +2514,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1372
2514
|
}
|
|
1373
2515
|
/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
|
|
1374
2516
|
gapMarks([u0, u1, c], horizontal, tick, out) {
|
|
1375
|
-
const
|
|
1376
|
-
out.lines.push(line("spacing",
|
|
1377
|
-
out.labels.push(this.label(
|
|
2517
|
+
const pt2 = (u, v2) => horizontal ? { x: u, y: v2 } : { x: v2, y: u };
|
|
2518
|
+
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)));
|
|
2519
|
+
out.labels.push(this.label(pt2((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
|
|
1378
2520
|
}
|
|
1379
2521
|
/** The other objects' side length nearest `length`, within `tol`, or null. */
|
|
1380
2522
|
matchSize(length2, tol) {
|
|
@@ -1401,9 +2543,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1401
2543
|
const hp = handlePos(handle, frame);
|
|
1402
2544
|
const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
|
|
1403
2545
|
const valid = (n, s) => Number.isFinite(n) && n * s > 0;
|
|
1404
|
-
const nearest = (
|
|
2546
|
+
const nearest = (v2, lines) => {
|
|
1405
2547
|
let best = null;
|
|
1406
|
-
for (const l of linesNear(lines,
|
|
2548
|
+
for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
|
|
1407
2549
|
return best;
|
|
1408
2550
|
};
|
|
1409
2551
|
let nx = sx;
|
|
@@ -1454,9 +2596,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1454
2596
|
};
|
|
1455
2597
|
for (const h of [handle, ...ends]) {
|
|
1456
2598
|
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 *
|
|
2599
|
+
const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
|
|
2600
|
+
const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
|
|
2601
|
+
const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
|
|
1460
2602
|
const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
|
|
1461
2603
|
const l2 = dir.x * dir.x + dir.y * dir.y;
|
|
1462
2604
|
const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
|
|
@@ -1489,16 +2631,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1489
2631
|
const sized = [];
|
|
1490
2632
|
if (!proportional) {
|
|
1491
2633
|
if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
|
|
1492
|
-
const
|
|
1493
|
-
if (
|
|
1494
|
-
out.sx = Math.sign(out.sx || 1) * (
|
|
2634
|
+
const len2 = this.matchSize(Math.abs(out.sx) * frame.width, tol);
|
|
2635
|
+
if (len2 !== null && len2 > 0) {
|
|
2636
|
+
out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
|
|
1495
2637
|
sized.push("x");
|
|
1496
2638
|
}
|
|
1497
2639
|
}
|
|
1498
2640
|
if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
|
|
1499
|
-
const
|
|
1500
|
-
if (
|
|
1501
|
-
out.sy = Math.sign(out.sy || 1) * (
|
|
2641
|
+
const len2 = this.matchSize(Math.abs(out.sy) * frame.height, tol);
|
|
2642
|
+
if (len2 !== null && len2 > 0) {
|
|
2643
|
+
out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
|
|
1502
2644
|
sized.push("y");
|
|
1503
2645
|
}
|
|
1504
2646
|
}
|
|
@@ -1549,14 +2691,14 @@ function line(kind, a, b) {
|
|
|
1549
2691
|
return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
|
|
1550
2692
|
}
|
|
1551
2693
|
function construction(leave, p, tol) {
|
|
1552
|
-
const
|
|
1553
|
-
if (Math.hypot(
|
|
2694
|
+
const v2 = { x: p.x - leave.at.x, y: p.y - leave.at.y };
|
|
2695
|
+
if (Math.hypot(v2.x, v2.y) <= tol) return null;
|
|
1554
2696
|
let best = null;
|
|
1555
2697
|
for (const a of leave.angles ?? []) {
|
|
1556
2698
|
const r = a * Math.PI / 180;
|
|
1557
2699
|
const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
|
|
1558
2700
|
const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
|
|
1559
|
-
const k =
|
|
2701
|
+
const k = v2.x * dir.x + v2.y * dir.y;
|
|
1560
2702
|
const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
|
|
1561
2703
|
const d = distance(point, p);
|
|
1562
2704
|
if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
|
|
@@ -1581,7 +2723,7 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1581
2723
|
vals.push(cond(r, d));
|
|
1582
2724
|
scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
|
|
1583
2725
|
}
|
|
1584
|
-
if (!(scale > 0) || vals.every((
|
|
2726
|
+
if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
|
|
1585
2727
|
const out = [];
|
|
1586
2728
|
for (let i = 0; i < CURVE_SAMPLES; i++) {
|
|
1587
2729
|
const ga = vals[i];
|
|
@@ -1614,8 +2756,8 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1614
2756
|
}
|
|
1615
2757
|
|
|
1616
2758
|
// src/vector/pathEdit.ts
|
|
1617
|
-
var
|
|
1618
|
-
var
|
|
2759
|
+
var EPS3 = 1e-9;
|
|
2760
|
+
var unit2 = (x, y) => {
|
|
1619
2761
|
const l = Math.hypot(x, y);
|
|
1620
2762
|
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1621
2763
|
};
|
|
@@ -1628,14 +2770,14 @@ function cubicOf(sp, i) {
|
|
|
1628
2770
|
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
1629
2771
|
function startTangent(c) {
|
|
1630
2772
|
for (let k = 1; k < 4; k++) {
|
|
1631
|
-
const u =
|
|
2773
|
+
const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
1632
2774
|
if (u) return u;
|
|
1633
2775
|
}
|
|
1634
2776
|
return [1, 0];
|
|
1635
2777
|
}
|
|
1636
2778
|
function endTangent(c) {
|
|
1637
2779
|
for (let k = 2; k >= 0; k--) {
|
|
1638
|
-
const u =
|
|
2780
|
+
const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
1639
2781
|
if (u) return u;
|
|
1640
2782
|
}
|
|
1641
2783
|
return [1, 0];
|
|
@@ -1643,11 +2785,11 @@ function endTangent(c) {
|
|
|
1643
2785
|
function sample(c, n, out, withStart) {
|
|
1644
2786
|
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
|
|
1645
2787
|
}
|
|
1646
|
-
var
|
|
2788
|
+
var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
|
|
1647
2789
|
function tidy(a, keepType = true) {
|
|
1648
2790
|
const next = cloneAnchor(a);
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
2791
|
+
if (onAnchor2(next, next.in)) delete next.in;
|
|
2792
|
+
if (onAnchor2(next, next.out)) delete next.out;
|
|
1651
2793
|
const shown = inferAnchorType(next);
|
|
1652
2794
|
const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
|
|
1653
2795
|
if (type === "corner") delete next.type;
|
|
@@ -1755,8 +2897,8 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
1755
2897
|
const a = sp.anchors[index];
|
|
1756
2898
|
if (!a) return { anchors, closed: sp.closed };
|
|
1757
2899
|
const own = [
|
|
1758
|
-
a.in && !
|
|
1759
|
-
a.out && !
|
|
2900
|
+
a.in && !onAnchor2(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
|
|
2901
|
+
a.out && !onAnchor2(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
|
|
1760
2902
|
];
|
|
1761
2903
|
const ends = [null, null];
|
|
1762
2904
|
neighbours(sp, index).forEach((j, side) => {
|
|
@@ -1775,15 +2917,16 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
1775
2917
|
if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
|
|
1776
2918
|
if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
|
|
1777
2919
|
if (a.type) moved.type = a.type;
|
|
2920
|
+
if (a.corner) moved.corner = { ...a.corner };
|
|
1778
2921
|
const [prev, next] = ends;
|
|
1779
2922
|
if (a.type && a.type !== "corner" && prev && next) {
|
|
1780
2923
|
const dx = next.x - prev.x;
|
|
1781
2924
|
const dy = next.y - prev.y;
|
|
1782
2925
|
const l = Math.hypot(dx, dy);
|
|
1783
2926
|
if (l > 1e-9) {
|
|
1784
|
-
const
|
|
1785
|
-
let li =
|
|
1786
|
-
let lo =
|
|
2927
|
+
const len2 = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
|
|
2928
|
+
let li = len2(own[0], prev);
|
|
2929
|
+
let lo = len2(own[1], next);
|
|
1787
2930
|
if (a.type === "symmetric") li = lo = (li + lo) / 2;
|
|
1788
2931
|
moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
|
|
1789
2932
|
moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
|
|
@@ -1792,9 +2935,9 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
1792
2935
|
anchors[index] = moved;
|
|
1793
2936
|
return { anchors, closed: sp.closed };
|
|
1794
2937
|
}
|
|
1795
|
-
function cutSubpathAt(sp,
|
|
2938
|
+
function cutSubpathAt(sp, at2) {
|
|
1796
2939
|
const n = sp.anchors.length;
|
|
1797
|
-
const cuts = [...new Set(
|
|
2940
|
+
const cuts = [...new Set(at2)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
|
|
1798
2941
|
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
1799
2942
|
const first = cuts[0];
|
|
1800
2943
|
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
@@ -1824,8 +2967,8 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
|
1824
2967
|
const { subpath, index } = insertAnchor(sp, segment, t);
|
|
1825
2968
|
return cutSubpathAt(subpath, [index]);
|
|
1826
2969
|
}
|
|
1827
|
-
var
|
|
1828
|
-
var
|
|
2970
|
+
var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
2971
|
+
var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
1829
2972
|
function reverse(sp) {
|
|
1830
2973
|
return {
|
|
1831
2974
|
closed: sp.closed,
|
|
@@ -1834,6 +2977,7 @@ function reverse(sp) {
|
|
|
1834
2977
|
if (a.out) next.in = { ...a.out };
|
|
1835
2978
|
if (a.in) next.out = { ...a.in };
|
|
1836
2979
|
if (a.type) next.type = a.type;
|
|
2980
|
+
if (a.corner) next.corner = { ...a.corner };
|
|
1837
2981
|
return next;
|
|
1838
2982
|
})
|
|
1839
2983
|
};
|
|
@@ -1851,7 +2995,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
1851
2995
|
const sp = open[0];
|
|
1852
2996
|
const first = sp.anchors[0];
|
|
1853
2997
|
const last = sp.anchors[sp.anchors.length - 1];
|
|
1854
|
-
if (sp.anchors.length > 2 &&
|
|
2998
|
+
if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
|
|
1855
2999
|
sp.anchors.pop();
|
|
1856
3000
|
sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
|
|
1857
3001
|
} else {
|
|
@@ -1867,7 +3011,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
1867
3011
|
for (let j = i + 1; j < open.length; j++) {
|
|
1868
3012
|
for (const ei of [false, true]) {
|
|
1869
3013
|
for (const ej of [false, true]) {
|
|
1870
|
-
const d =
|
|
3014
|
+
const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
|
|
1871
3015
|
if (d < best.d) best = { d, i, j, ei, ej };
|
|
1872
3016
|
}
|
|
1873
3017
|
}
|
|
@@ -1905,10 +3049,10 @@ function smoothSubpath(sp, amount = 1, only) {
|
|
|
1905
3049
|
const prev = orig[(i + n - 1) % n];
|
|
1906
3050
|
const next = orig[(i + 1) % n];
|
|
1907
3051
|
const p = orig[i];
|
|
1908
|
-
const dir =
|
|
3052
|
+
const dir = unit2(next.x - prev.x, next.y - prev.y);
|
|
1909
3053
|
if (!dir) continue;
|
|
1910
|
-
const li =
|
|
1911
|
-
const lo =
|
|
3054
|
+
const li = dist2(p, prev) / 3;
|
|
3055
|
+
const lo = dist2(p, next) / 3;
|
|
1912
3056
|
const hin = p.in ?? p;
|
|
1913
3057
|
const hout = p.out ?? p;
|
|
1914
3058
|
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
@@ -1935,12 +3079,12 @@ function segsOf(sp) {
|
|
|
1935
3079
|
function segsToSubpath(segs, closed, types) {
|
|
1936
3080
|
if (!segs.length) return null;
|
|
1937
3081
|
const anchors = [];
|
|
1938
|
-
const
|
|
1939
|
-
anchors.push(
|
|
3082
|
+
const at2 = (p) => ({ x: p[0], y: p[1] });
|
|
3083
|
+
anchors.push(at2(segs[0].c[0]));
|
|
1940
3084
|
for (const s of segs) {
|
|
1941
3085
|
const end = s.c[s.c.length - 1];
|
|
1942
3086
|
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
1943
|
-
const next =
|
|
3087
|
+
const next = at2(end);
|
|
1944
3088
|
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
1945
3089
|
anchors.push(next);
|
|
1946
3090
|
}
|
|
@@ -1962,12 +3106,12 @@ function mergePair(a, b, tol) {
|
|
|
1962
3106
|
if (a.line && b.line) {
|
|
1963
3107
|
const cx = b3[0] - a0[0];
|
|
1964
3108
|
const cy = b3[1] - a0[1];
|
|
1965
|
-
const
|
|
1966
|
-
if (
|
|
3109
|
+
const len2 = Math.hypot(cx, cy);
|
|
3110
|
+
if (len2 < 1e-12) return null;
|
|
1967
3111
|
const mx = a3[0] - a0[0];
|
|
1968
3112
|
const my = a3[1] - a0[1];
|
|
1969
|
-
const
|
|
1970
|
-
return Math.abs((mx * cy - my * cx) /
|
|
3113
|
+
const along2 = (mx * cx + my * cy) / len2;
|
|
3114
|
+
return Math.abs((mx * cy - my * cx) / len2) <= tol && along2 >= 0 && along2 <= len2 ? { c: [a0, b3], line: true } : null;
|
|
1971
3115
|
}
|
|
1972
3116
|
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
1973
3117
|
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
@@ -2091,7 +3235,7 @@ function lineHits(p, q, seg) {
|
|
|
2091
3235
|
const dy = q[1] - p[1];
|
|
2092
3236
|
const len2 = dx * dx + dy * dy;
|
|
2093
3237
|
if (len2 < 1e-24) return [];
|
|
2094
|
-
const h = (
|
|
3238
|
+
const h = (pt2) => (pt2[0] - p[0]) * -dy + (pt2[1] - p[1]) * dx;
|
|
2095
3239
|
let coeffs;
|
|
2096
3240
|
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
2097
3241
|
else {
|
|
@@ -2099,7 +3243,7 @@ function lineHits(p, q, seg) {
|
|
|
2099
3243
|
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
2100
3244
|
}
|
|
2101
3245
|
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((
|
|
3246
|
+
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
|
|
2103
3247
|
const out = [];
|
|
2104
3248
|
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
2105
3249
|
const point = segmentPoint(seg, t);
|
|
@@ -2326,12 +3470,12 @@ function simplifyPolyline(points, tolerance) {
|
|
|
2326
3470
|
const [ax, ay] = points[a];
|
|
2327
3471
|
const dx = points[b][0] - ax;
|
|
2328
3472
|
const dy = points[b][1] - ay;
|
|
2329
|
-
const
|
|
3473
|
+
const len2 = Math.hypot(dx, dy);
|
|
2330
3474
|
let far = -1;
|
|
2331
3475
|
let farD = 0;
|
|
2332
3476
|
for (let i = a + 1; i < b; i++) {
|
|
2333
3477
|
const [px, py] = points[i];
|
|
2334
|
-
const d =
|
|
3478
|
+
const d = len2 < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len2;
|
|
2335
3479
|
if (d > farD) {
|
|
2336
3480
|
farD = d;
|
|
2337
3481
|
far = i;
|
|
@@ -2399,7 +3543,7 @@ function vectorInkShapeName(shape) {
|
|
|
2399
3543
|
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2400
3544
|
}
|
|
2401
3545
|
}
|
|
2402
|
-
var
|
|
3546
|
+
var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2403
3547
|
function bbox(pts) {
|
|
2404
3548
|
if (!pts.length) return [0, 0, 0, 0];
|
|
2405
3549
|
let [x0, y0] = pts[0];
|
|
@@ -2417,7 +3561,7 @@ function segDistance(a, b, p) {
|
|
|
2417
3561
|
const dy = b[1] - a[1];
|
|
2418
3562
|
const len2 = dx * dx + dy * dy;
|
|
2419
3563
|
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2420
|
-
return
|
|
3564
|
+
return dist3([a[0] + t * dx, a[1] + t * dy], p);
|
|
2421
3565
|
}
|
|
2422
3566
|
function polygonDistance(c, p) {
|
|
2423
3567
|
let best = Infinity;
|
|
@@ -2443,11 +3587,11 @@ function snapSquare(x, y, w, h) {
|
|
|
2443
3587
|
}
|
|
2444
3588
|
function turning(a, b, c) {
|
|
2445
3589
|
const u = [b[0] - a[0], b[1] - a[1]];
|
|
2446
|
-
const
|
|
3590
|
+
const v2 = [c[0] - b[0], c[1] - b[1]];
|
|
2447
3591
|
const lu = Math.hypot(u[0], u[1]);
|
|
2448
|
-
const lv = Math.hypot(
|
|
3592
|
+
const lv = Math.hypot(v2[0], v2[1]);
|
|
2449
3593
|
if (lu <= 0 || lv <= 0) return 0;
|
|
2450
|
-
return Math.acos(Math.min(1, Math.max(-1, (u[0] *
|
|
3594
|
+
return Math.acos(Math.min(1, Math.max(-1, (u[0] * v2[0] + u[1] * v2[1]) / (lu * lv)))) * 180 / Math.PI;
|
|
2451
3595
|
}
|
|
2452
3596
|
function rdp(p, eps) {
|
|
2453
3597
|
const n = p.length;
|
|
@@ -2479,7 +3623,7 @@ function corners(p, diag) {
|
|
|
2479
3623
|
let far = 0;
|
|
2480
3624
|
let farD = 0;
|
|
2481
3625
|
p.forEach((q, i) => {
|
|
2482
|
-
const d =
|
|
3626
|
+
const d = dist3(start, q);
|
|
2483
3627
|
if (d > farD) {
|
|
2484
3628
|
farD = d;
|
|
2485
3629
|
far = i;
|
|
@@ -2506,7 +3650,7 @@ function corners(p, diag) {
|
|
|
2506
3650
|
}
|
|
2507
3651
|
let short = -1;
|
|
2508
3652
|
for (let j = 0; j < n; j++) {
|
|
2509
|
-
if (
|
|
3653
|
+
if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
2510
3654
|
short = j;
|
|
2511
3655
|
break;
|
|
2512
3656
|
}
|
|
@@ -2546,8 +3690,8 @@ function recognizeInkShape(points, options = {}) {
|
|
|
2546
3690
|
const diag = Math.hypot(w, h);
|
|
2547
3691
|
if (size < minSize) return null;
|
|
2548
3692
|
let length2 = 0;
|
|
2549
|
-
for (let i = 1; i < p.length; i++) length2 +=
|
|
2550
|
-
const gap =
|
|
3693
|
+
for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
|
|
3694
|
+
const gap = dist3(first, last);
|
|
2551
3695
|
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
2552
3696
|
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
2553
3697
|
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
@@ -2573,8 +3717,31 @@ function recognizeInkShape(points, options = {}) {
|
|
|
2573
3717
|
|
|
2574
3718
|
// src/vector/controller.ts
|
|
2575
3719
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
2576
|
-
var DEFAULT_VECTOR_PATH_EDIT = {
|
|
3720
|
+
var DEFAULT_VECTOR_PATH_EDIT = {
|
|
3721
|
+
keepShapeOnDelete: true,
|
|
3722
|
+
curvatureMoves: false,
|
|
3723
|
+
pencilSmoothing: 0,
|
|
3724
|
+
recognizeShapes: false,
|
|
3725
|
+
eraserSize: 10,
|
|
3726
|
+
pencilPressure: "pen"
|
|
3727
|
+
};
|
|
2577
3728
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3729
|
+
var VECTOR_STYLE_KEYS = [
|
|
3730
|
+
"fill",
|
|
3731
|
+
"stroke",
|
|
3732
|
+
"strokeWidth",
|
|
3733
|
+
"strokeLinecap",
|
|
3734
|
+
"strokeLinejoin",
|
|
3735
|
+
"strokeDasharray",
|
|
3736
|
+
"fillRule",
|
|
3737
|
+
"strokeAlign",
|
|
3738
|
+
"startArrow",
|
|
3739
|
+
"endArrow",
|
|
3740
|
+
"arrowAlign",
|
|
3741
|
+
"startArrowScale",
|
|
3742
|
+
"endArrowScale",
|
|
3743
|
+
"strokeWidthProfile"
|
|
3744
|
+
];
|
|
2578
3745
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
2579
3746
|
var DEFAULT_DEFAULTS = {
|
|
2580
3747
|
fill: "#d4d4d8",
|
|
@@ -2635,9 +3802,9 @@ function getVisibleHandles(state) {
|
|
|
2635
3802
|
return out;
|
|
2636
3803
|
}
|
|
2637
3804
|
if (state.tool !== "direct") return out;
|
|
2638
|
-
const
|
|
3805
|
+
const editable2 = new Set(getEditablePathIds(state));
|
|
2639
3806
|
for (const ref of state.selectedAnchors) {
|
|
2640
|
-
if (!
|
|
3807
|
+
if (!editable2.has(ref.nodeId)) continue;
|
|
2641
3808
|
const node = findVectorNode(state.document.nodes, ref.nodeId);
|
|
2642
3809
|
const sp = node.subpaths[ref.subpath];
|
|
2643
3810
|
if (!sp) continue;
|
|
@@ -2651,6 +3818,79 @@ function getVisibleHandles(state) {
|
|
|
2651
3818
|
}
|
|
2652
3819
|
return out;
|
|
2653
3820
|
}
|
|
3821
|
+
var VECTOR_CORNER_WIDGET_INSET = 10;
|
|
3822
|
+
function getVectorCornerWidgets(state) {
|
|
3823
|
+
if (state.tool !== "direct" || state.pen) return [];
|
|
3824
|
+
const zoom = state.viewport.zoom;
|
|
3825
|
+
const out = [];
|
|
3826
|
+
for (const id of getEditablePathIds(state)) {
|
|
3827
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
3828
|
+
const picked = state.selectedAnchors.filter((r) => r.nodeId === id);
|
|
3829
|
+
const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
|
|
3830
|
+
for (const c of vectorCorners(node.subpaths)) {
|
|
3831
|
+
if (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;
|
|
3832
|
+
const side = Math.min(c.lu, c.lv);
|
|
3833
|
+
if (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;
|
|
3834
|
+
const a = node.subpaths[c.subpath].anchors[c.index];
|
|
3835
|
+
const radius = a.corner?.radius ?? 0;
|
|
3836
|
+
const drawn = fitCornerRadius(c, radius);
|
|
3837
|
+
const k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);
|
|
3838
|
+
const p = applyMatrix(m, cornerBisectorPoint(c, k));
|
|
3839
|
+
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 });
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return out;
|
|
3843
|
+
}
|
|
3844
|
+
function vectorStrokeWidthGeometry(node) {
|
|
3845
|
+
if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;
|
|
3846
|
+
const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
|
|
3847
|
+
return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
|
|
3848
|
+
}
|
|
3849
|
+
function getVectorWidthHandles(state) {
|
|
3850
|
+
if (state.tool !== "width") return [];
|
|
3851
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3852
|
+
for (const id of state.selection) {
|
|
3853
|
+
const n = findVectorNode(state.document.nodes, id);
|
|
3854
|
+
if (n) for (const p of collectPathNodes(n)) ids.add(p.id);
|
|
3855
|
+
}
|
|
3856
|
+
if (state.hover?.nodeId) ids.add(state.hover.nodeId);
|
|
3857
|
+
if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
|
|
3858
|
+
if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
|
|
3859
|
+
const out = [];
|
|
3860
|
+
for (const id of ids) {
|
|
3861
|
+
const node = findVectorNode(state.document.nodes, id);
|
|
3862
|
+
if (node?.type !== "path" || node.visible === false || !node.strokeWidthProfile?.length) continue;
|
|
3863
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
3864
|
+
if (!g) continue;
|
|
3865
|
+
const profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
3866
|
+
const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
|
|
3867
|
+
const contours = node.subpaths.length;
|
|
3868
|
+
for (let sp = 0; sp < contours; sp++) {
|
|
3869
|
+
profile.forEach((w, index) => {
|
|
3870
|
+
const at2 = strokePositionPoint(g.line, sp, w.t);
|
|
3871
|
+
if (!at2) return;
|
|
3872
|
+
const n = { x: at2.tangent[1], y: -at2.tangent[0] };
|
|
3873
|
+
const c = { x: at2.point[0], y: at2.point[1] };
|
|
3874
|
+
const p = applyMatrix(m, c);
|
|
3875
|
+
const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
|
|
3876
|
+
const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
|
|
3877
|
+
const sel = state.selectedWidthPoint;
|
|
3878
|
+
out.push({
|
|
3879
|
+
ref: { nodeId: id, index, subpath: sp },
|
|
3880
|
+
x: p.x,
|
|
3881
|
+
y: p.y,
|
|
3882
|
+
left: l,
|
|
3883
|
+
right: r,
|
|
3884
|
+
t: w.t,
|
|
3885
|
+
leftScale: w.left,
|
|
3886
|
+
rightScale: w.right,
|
|
3887
|
+
selected: !!sel && sel.nodeId === id && sel.index === index
|
|
3888
|
+
});
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
return out;
|
|
3893
|
+
}
|
|
2654
3894
|
function getAnchor(doc, ref) {
|
|
2655
3895
|
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
2656
3896
|
return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
|
|
@@ -2721,9 +3961,10 @@ var TOOL_LABELS = {
|
|
|
2721
3961
|
line: "Line",
|
|
2722
3962
|
scissors: "Scissors",
|
|
2723
3963
|
knife: "Knife",
|
|
2724
|
-
eraser: "Eraser"
|
|
3964
|
+
eraser: "Eraser",
|
|
3965
|
+
width: "Width"
|
|
2725
3966
|
};
|
|
2726
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
3967
|
+
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser", w: "width" };
|
|
2727
3968
|
function vectorToolLabel(tool) {
|
|
2728
3969
|
return TOOL_LABELS[tool];
|
|
2729
3970
|
}
|
|
@@ -2739,7 +3980,8 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
2739
3980
|
line: "L",
|
|
2740
3981
|
scissors: "C",
|
|
2741
3982
|
knife: "K",
|
|
2742
|
-
eraser: "E"
|
|
3983
|
+
eraser: "E",
|
|
3984
|
+
width: "W"
|
|
2743
3985
|
};
|
|
2744
3986
|
var VECTOR_TOOL_GLYPHS = {
|
|
2745
3987
|
select: "\u2196",
|
|
@@ -2753,7 +3995,8 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
2753
3995
|
line: "\u2571",
|
|
2754
3996
|
scissors: "\u2702",
|
|
2755
3997
|
knife: "\u27CB",
|
|
2756
|
-
eraser: "\u232B"
|
|
3998
|
+
eraser: "\u232B",
|
|
3999
|
+
width: "\u21F9"
|
|
2757
4000
|
};
|
|
2758
4001
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
2759
4002
|
var BOOLEAN_OPS = {
|
|
@@ -2807,6 +4050,7 @@ var VectorEditorController = class {
|
|
|
2807
4050
|
doc;
|
|
2808
4051
|
selection = [];
|
|
2809
4052
|
anchors = [];
|
|
4053
|
+
widthPoint = null;
|
|
2810
4054
|
editingId = null;
|
|
2811
4055
|
pen = null;
|
|
2812
4056
|
tool;
|
|
@@ -2885,6 +4129,7 @@ var VectorEditorController = class {
|
|
|
2885
4129
|
tool: this.tool,
|
|
2886
4130
|
selection: this.selection,
|
|
2887
4131
|
selectedAnchors: this.anchors,
|
|
4132
|
+
selectedWidthPoint: this.widthPoint,
|
|
2888
4133
|
editingId: this.editingId,
|
|
2889
4134
|
selectionBounds: this.boundsOf(this.selection),
|
|
2890
4135
|
selectionFrame: this.frameOf(this.selectedRoots()),
|
|
@@ -2948,11 +4193,15 @@ var VectorEditorController = class {
|
|
|
2948
4193
|
if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
|
|
2949
4194
|
return h?.nodeId ? "move" : "default";
|
|
2950
4195
|
case "direct":
|
|
2951
|
-
if (h?.anchor || h?.handle) return "pointer";
|
|
4196
|
+
if (h?.anchor || h?.handle || h?.corner) return "pointer";
|
|
2952
4197
|
if (h?.segment) return "copy";
|
|
2953
4198
|
return h?.nodeId ? "move" : "default";
|
|
2954
4199
|
case "pen":
|
|
2955
4200
|
return h?.anchor ? "pointer" : "crosshair";
|
|
4201
|
+
case "width":
|
|
4202
|
+
if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
|
|
4203
|
+
if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
|
|
4204
|
+
return h?.strokeSpot ? "copy" : "default";
|
|
2956
4205
|
default:
|
|
2957
4206
|
return "crosshair";
|
|
2958
4207
|
}
|
|
@@ -3033,12 +4282,12 @@ var VectorEditorController = class {
|
|
|
3033
4282
|
// Viewport
|
|
3034
4283
|
// -------------------------------------------------------------------------
|
|
3035
4284
|
screenToDocument(p) {
|
|
3036
|
-
const
|
|
3037
|
-
return { x: (p.x -
|
|
4285
|
+
const v2 = this.viewport;
|
|
4286
|
+
return { x: (p.x - v2.x) / v2.zoom, y: (p.y - v2.y) / v2.zoom };
|
|
3038
4287
|
}
|
|
3039
4288
|
documentToScreen(p) {
|
|
3040
|
-
const
|
|
3041
|
-
return { x: p.x *
|
|
4289
|
+
const v2 = this.viewport;
|
|
4290
|
+
return { x: p.x * v2.zoom + v2.x, y: p.y * v2.zoom + v2.y };
|
|
3042
4291
|
}
|
|
3043
4292
|
setViewport(viewport) {
|
|
3044
4293
|
const next = { ...this.viewport, ...viewport };
|
|
@@ -3078,6 +4327,7 @@ var VectorEditorController = class {
|
|
|
3078
4327
|
this.tool = tool;
|
|
3079
4328
|
this.hover = null;
|
|
3080
4329
|
this.guides = null;
|
|
4330
|
+
this.widthPoint = null;
|
|
3081
4331
|
if (tool !== "direct") {
|
|
3082
4332
|
this.anchors = [];
|
|
3083
4333
|
this.editingId = null;
|
|
@@ -3105,9 +4355,9 @@ var VectorEditorController = class {
|
|
|
3105
4355
|
toggleSnapping(on = !this.snap.objects) {
|
|
3106
4356
|
this.setSnap({ objects: on });
|
|
3107
4357
|
}
|
|
3108
|
-
gridValue(
|
|
4358
|
+
gridValue(v2) {
|
|
3109
4359
|
const g = this.snap.grid;
|
|
3110
|
-
return g > 0 ? Math.round(
|
|
4360
|
+
return g > 0 ? Math.round(v2 / g) * g : v2;
|
|
3111
4361
|
}
|
|
3112
4362
|
snapPoint(p) {
|
|
3113
4363
|
return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
|
|
@@ -3263,7 +4513,7 @@ var VectorEditorController = class {
|
|
|
3263
4513
|
let subpath;
|
|
3264
4514
|
switch (input.kind) {
|
|
3265
4515
|
case "rect":
|
|
3266
|
-
subpath = rectSubpath(b
|
|
4516
|
+
subpath = rectSubpath(b);
|
|
3267
4517
|
break;
|
|
3268
4518
|
case "ellipse":
|
|
3269
4519
|
subpath = ellipseSubpath(b);
|
|
@@ -3278,10 +4528,13 @@ var VectorEditorController = class {
|
|
|
3278
4528
|
subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
|
|
3279
4529
|
break;
|
|
3280
4530
|
}
|
|
4531
|
+
const radius = input.cornerRadius ?? d.cornerRadius;
|
|
4532
|
+
const kind = input.cornerKind ?? d.cornerKind;
|
|
4533
|
+
if ((input.kind === "rect" || input.kind === "polygon" || input.kind === "star") && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);
|
|
3281
4534
|
const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
3282
4535
|
const style = this.styleFor(input.kind === "line");
|
|
3283
4536
|
const own = {};
|
|
3284
|
-
for (const key of
|
|
4537
|
+
for (const key of VECTOR_STYLE_KEYS) {
|
|
3285
4538
|
if (input[key] !== void 0) own[key] = input[key];
|
|
3286
4539
|
}
|
|
3287
4540
|
return { id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
|
|
@@ -3304,6 +4557,9 @@ var VectorEditorController = class {
|
|
|
3304
4557
|
if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
|
|
3305
4558
|
if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
|
|
3306
4559
|
if (d.fillRule) out.fillRule = d.fillRule;
|
|
4560
|
+
if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
|
|
4561
|
+
for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
|
|
4562
|
+
if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
|
|
3307
4563
|
return out;
|
|
3308
4564
|
}
|
|
3309
4565
|
/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
|
|
@@ -3324,10 +4580,129 @@ var VectorEditorController = class {
|
|
|
3324
4580
|
}
|
|
3325
4581
|
if (!targets.size) return { ok: false, message: "Nothing is selected." };
|
|
3326
4582
|
this.change(options.label ?? "Change Style", () => {
|
|
3327
|
-
this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ?
|
|
4583
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? withStyle(n, style) : n));
|
|
4584
|
+
});
|
|
4585
|
+
return { ok: true };
|
|
4586
|
+
}
|
|
4587
|
+
/**
|
|
4588
|
+
* Sets the variable-width profile of the selected (or given) paths: a preset id
|
|
4589
|
+
* (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for
|
|
4590
|
+
* an unknown preset.
|
|
4591
|
+
*/
|
|
4592
|
+
setWidthProfile(profile, options = {}) {
|
|
4593
|
+
let points;
|
|
4594
|
+
if (profile === null) points = [];
|
|
4595
|
+
else if (typeof profile === "string") {
|
|
4596
|
+
const preset = widthProfilePreset(profile);
|
|
4597
|
+
if (!preset) return { ok: false, message: `There is no width profile "${profile}".` };
|
|
4598
|
+
points = preset;
|
|
4599
|
+
} else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);
|
|
4600
|
+
else {
|
|
4601
|
+
const p = profile;
|
|
4602
|
+
points = p.taper || p.wave ? generateWidthProfile(p) : [];
|
|
4603
|
+
}
|
|
4604
|
+
this.widthPoint = null;
|
|
4605
|
+
return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
|
|
4606
|
+
}
|
|
4607
|
+
/** Removes one width point of a path's profile (the width tool's Delete). */
|
|
4608
|
+
removeWidthPoint(ref) {
|
|
4609
|
+
const node = this.getNode(ref.nodeId);
|
|
4610
|
+
if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
|
|
4611
|
+
const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
|
|
4612
|
+
this.widthPoint = null;
|
|
4613
|
+
this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
|
|
4614
|
+
return { ok: true };
|
|
4615
|
+
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
4618
|
+
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
4619
|
+
* corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.
|
|
4620
|
+
*/
|
|
4621
|
+
setCornerRadius(radius, options = {}) {
|
|
4622
|
+
if (!Number.isFinite(radius) || radius < 0) return { ok: false, message: "The radius must be a number of at least 0." };
|
|
4623
|
+
const refs = this.cornerTargets(options);
|
|
4624
|
+
if (!refs.length) return { ok: false, message: "Nothing is selected." };
|
|
4625
|
+
this.change(radius > 0 ? "Corner Radius" : "Sharpen Corners", () => {
|
|
4626
|
+
this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind));
|
|
4627
|
+
});
|
|
4628
|
+
return { ok: true };
|
|
4629
|
+
}
|
|
4630
|
+
/** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */
|
|
4631
|
+
setCornerKind(kind, options = {}) {
|
|
4632
|
+
const refs = this.cornerTargets(options);
|
|
4633
|
+
if (!refs.length) return { ok: false, message: "Nothing is selected." };
|
|
4634
|
+
this.change("Corner Kind", () => {
|
|
4635
|
+
this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
|
|
3328
4636
|
});
|
|
3329
4637
|
return { ok: true };
|
|
3330
4638
|
}
|
|
4639
|
+
/** Anchors a corner change applies to (see `setCornerRadius`). */
|
|
4640
|
+
cornerTargets(options) {
|
|
4641
|
+
if (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));
|
|
4642
|
+
if (!options.ids && this.tool === "direct" && this.anchors.length) return this.anchors.slice();
|
|
4643
|
+
const out = [];
|
|
4644
|
+
for (const id of options.ids ?? this.selection) {
|
|
4645
|
+
const node = this.getNode(id);
|
|
4646
|
+
if (!node) continue;
|
|
4647
|
+
for (const p of collectPathNodes(node)) {
|
|
4648
|
+
if (p.locked) continue;
|
|
4649
|
+
p.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
return out;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the
|
|
4656
|
+
* area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy
|
|
4657
|
+
* of the path that keeps the fill (left out when there is none). Returns the new paths' ids.
|
|
4658
|
+
*/
|
|
4659
|
+
outlineStroke(options = {}) {
|
|
4660
|
+
const roots = options.ids ?? this.selectedRoots();
|
|
4661
|
+
const paths = [];
|
|
4662
|
+
for (const id of roots) {
|
|
4663
|
+
const node = this.getNode(id);
|
|
4664
|
+
if (node) {
|
|
4665
|
+
for (const p of collectPathNodes(node)) if (!p.locked && hasPaint(p.stroke) && (p.strokeWidth ?? 1) > 0) paths.push(p);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
if (!paths.length) return [];
|
|
4669
|
+
const created = [];
|
|
4670
|
+
const replace = /* @__PURE__ */ new Map();
|
|
4671
|
+
for (const p of paths) {
|
|
4672
|
+
const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
|
|
4673
|
+
if (!outline.length) continue;
|
|
4674
|
+
const {
|
|
4675
|
+
stroke,
|
|
4676
|
+
strokeWidth: _w,
|
|
4677
|
+
strokeLinecap: _c,
|
|
4678
|
+
strokeLinejoin: _j,
|
|
4679
|
+
strokeDasharray: _d,
|
|
4680
|
+
strokeAlign: _a,
|
|
4681
|
+
startArrow: _s,
|
|
4682
|
+
endArrow: _e,
|
|
4683
|
+
arrowAlign: _al,
|
|
4684
|
+
startArrowScale: _ss,
|
|
4685
|
+
endArrowScale: _es,
|
|
4686
|
+
strokeWidthProfile: _wp,
|
|
4687
|
+
...rest
|
|
4688
|
+
} = cloneVectorNode(p);
|
|
4689
|
+
const id = this.newId();
|
|
4690
|
+
const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
|
|
4691
|
+
const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
|
|
4692
|
+
created.push(id);
|
|
4693
|
+
replace.set(p.id, list);
|
|
4694
|
+
}
|
|
4695
|
+
if (!created.length) return [];
|
|
4696
|
+
this.change("Outline Stroke", () => {
|
|
4697
|
+
let nodes = this.doc.nodes;
|
|
4698
|
+
for (const [id, list] of replace) nodes = spliceTree(nodes, id, list);
|
|
4699
|
+
this.setDoc(nodes);
|
|
4700
|
+
this.selection = created;
|
|
4701
|
+
this.anchors = [];
|
|
4702
|
+
this.editingId = null;
|
|
4703
|
+
});
|
|
4704
|
+
return created;
|
|
4705
|
+
}
|
|
3331
4706
|
rename(id, name) {
|
|
3332
4707
|
return this.updateNode(id, { name }, { label: "Rename" });
|
|
3333
4708
|
}
|
|
@@ -3610,7 +4985,7 @@ var VectorEditorController = class {
|
|
|
3610
4985
|
if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
|
|
3611
4986
|
const set = new Set(groups.map((g) => g.id));
|
|
3612
4987
|
this.change("Ungroup", () => {
|
|
3613
|
-
const
|
|
4988
|
+
const flatten2 = (list) => {
|
|
3614
4989
|
const out = [];
|
|
3615
4990
|
let changed = false;
|
|
3616
4991
|
for (const n of list) {
|
|
@@ -3622,14 +4997,14 @@ var VectorEditorController = class {
|
|
|
3622
4997
|
...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
|
|
3623
4998
|
);
|
|
3624
4999
|
} else if (n.type === "group") {
|
|
3625
|
-
const children =
|
|
5000
|
+
const children = flatten2(n.children);
|
|
3626
5001
|
if (children !== n.children) changed = true;
|
|
3627
5002
|
out.push(children !== n.children ? { ...n, children } : n);
|
|
3628
5003
|
} else out.push(n);
|
|
3629
5004
|
}
|
|
3630
5005
|
return changed ? out : list;
|
|
3631
5006
|
};
|
|
3632
|
-
this.setDoc(
|
|
5007
|
+
this.setDoc(flatten2(this.doc.nodes));
|
|
3633
5008
|
this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
|
|
3634
5009
|
});
|
|
3635
5010
|
return { ok: true };
|
|
@@ -3899,7 +5274,12 @@ var VectorEditorController = class {
|
|
|
3899
5274
|
const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
|
|
3900
5275
|
if (!set.size) return { ok: false, message: "Select a path." };
|
|
3901
5276
|
this.change("Reverse Path", () => {
|
|
3902
|
-
this.setDoc(
|
|
5277
|
+
this.setDoc(
|
|
5278
|
+
mapTree(
|
|
5279
|
+
this.doc.nodes,
|
|
5280
|
+
(n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {} } : n
|
|
5281
|
+
)
|
|
5282
|
+
);
|
|
3903
5283
|
this.anchors = [];
|
|
3904
5284
|
});
|
|
3905
5285
|
return { ok: true };
|
|
@@ -3930,7 +5310,7 @@ var VectorEditorController = class {
|
|
|
3930
5310
|
const map = (p) => applyMatrix(m, p);
|
|
3931
5311
|
return from.subpaths.map((sp) => ({
|
|
3932
5312
|
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 } : {} }))
|
|
5313
|
+
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
5314
|
}));
|
|
3935
5315
|
}
|
|
3936
5316
|
/**
|
|
@@ -3980,9 +5360,9 @@ var VectorEditorController = class {
|
|
|
3980
5360
|
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
3981
5361
|
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
3982
5362
|
this.change("Average", () => {
|
|
3983
|
-
for (const { ref, at } of points) {
|
|
3984
|
-
const dx = axis === "horizontal" ? 0 : cx -
|
|
3985
|
-
const dy = axis === "vertical" ? 0 : cy -
|
|
5363
|
+
for (const { ref, at: at2 } of points) {
|
|
5364
|
+
const dx = axis === "horizontal" ? 0 : cx - at2.x;
|
|
5365
|
+
const dy = axis === "vertical" ? 0 : cy - at2.y;
|
|
3986
5366
|
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
3987
5367
|
}
|
|
3988
5368
|
});
|
|
@@ -4066,8 +5446,8 @@ var VectorEditorController = class {
|
|
|
4066
5446
|
const node = this.getNode(id);
|
|
4067
5447
|
if (node.locked) continue;
|
|
4068
5448
|
const ids = this.applyCuts(node, (sp, i) => {
|
|
4069
|
-
const
|
|
4070
|
-
const pieces =
|
|
5449
|
+
const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
5450
|
+
const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
|
|
4071
5451
|
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4072
5452
|
});
|
|
4073
5453
|
if (ids) created.push(...ids);
|
|
@@ -4285,6 +5665,22 @@ var VectorEditorController = class {
|
|
|
4285
5665
|
}
|
|
4286
5666
|
return null;
|
|
4287
5667
|
}
|
|
5668
|
+
cornerView() {
|
|
5669
|
+
return { ...this.view(), viewport: this.viewport };
|
|
5670
|
+
}
|
|
5671
|
+
hitCornerWidget(screen) {
|
|
5672
|
+
let best = null;
|
|
5673
|
+
let bestD = Infinity;
|
|
5674
|
+
for (const w of getVectorCornerWidgets(this.cornerView())) {
|
|
5675
|
+
const s = this.documentToScreen(w);
|
|
5676
|
+
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
5677
|
+
if (d <= this.hitRadius && d < bestD) {
|
|
5678
|
+
best = w;
|
|
5679
|
+
bestD = d;
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
return best;
|
|
5683
|
+
}
|
|
4288
5684
|
hitAnchor(screen, refs) {
|
|
4289
5685
|
let best = null;
|
|
4290
5686
|
let bestD = Infinity;
|
|
@@ -4343,6 +5739,8 @@ var VectorEditorController = class {
|
|
|
4343
5739
|
if (handle) return { nodeId: handle.nodeId, handle };
|
|
4344
5740
|
const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
|
|
4345
5741
|
if (anchor) return { nodeId: anchor.nodeId, anchor };
|
|
5742
|
+
const widget = this.hitCornerWidget(screen);
|
|
5743
|
+
if (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };
|
|
4346
5744
|
for (const id of getEditablePathIds(this.view())) {
|
|
4347
5745
|
const node = this.getNode(id);
|
|
4348
5746
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -4363,10 +5761,131 @@ var VectorEditorController = class {
|
|
|
4363
5761
|
const seg = this.nearestOutline(p);
|
|
4364
5762
|
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
4365
5763
|
}
|
|
5764
|
+
case "width": {
|
|
5765
|
+
const hit = this.hitWidthHandle(screen);
|
|
5766
|
+
if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
|
|
5767
|
+
const spot = this.strokeSpotAt(p);
|
|
5768
|
+
return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
|
|
5769
|
+
}
|
|
4366
5770
|
default:
|
|
4367
5771
|
return null;
|
|
4368
5772
|
}
|
|
4369
5773
|
}
|
|
5774
|
+
widthView() {
|
|
5775
|
+
return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
|
|
5776
|
+
}
|
|
5777
|
+
/** The width point centre or side end under `screen` (width tool). */
|
|
5778
|
+
hitWidthHandle(screen) {
|
|
5779
|
+
let best = null;
|
|
5780
|
+
let bestD = Infinity;
|
|
5781
|
+
for (const h of getVectorWidthHandles(this.widthView())) {
|
|
5782
|
+
for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
|
|
5783
|
+
const s = this.documentToScreen(q);
|
|
5784
|
+
const d = Math.hypot(s.x - screen.x, s.y - screen.y);
|
|
5785
|
+
if (d <= this.hitRadius && d < bestD - 1e-9) {
|
|
5786
|
+
best = { ref: { ...h.ref, part } };
|
|
5787
|
+
bestD = d;
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
return best;
|
|
5792
|
+
}
|
|
5793
|
+
/** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */
|
|
5794
|
+
strokeSpotAt(p) {
|
|
5795
|
+
let best = null;
|
|
5796
|
+
const tol = this.tolerance();
|
|
5797
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
5798
|
+
if (n.type !== "path" || n.visible === false || n.locked) return;
|
|
5799
|
+
if (hiddenOrLocked(this.doc.nodes, n.id)) return;
|
|
5800
|
+
const g = vectorStrokeWidthGeometry(n);
|
|
5801
|
+
if (!g) return;
|
|
5802
|
+
const m = this.worldOf(n.id);
|
|
5803
|
+
const local = applyMatrix(invertMatrix(m), p);
|
|
5804
|
+
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
5805
|
+
if (!near2) return;
|
|
5806
|
+
const profile = strokeWidthProfileOf(n);
|
|
5807
|
+
const w = widthProfileAt(profile, near2.t);
|
|
5808
|
+
const reach = Math.max(near2.side === "left" ? w.left : w.right, 0) * g.unit + tol;
|
|
5809
|
+
if (near2.distance > reach || near2.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
|
|
5810
|
+
if (best && near2.distance >= best.distance) return;
|
|
5811
|
+
const nx = near2.tangent[1];
|
|
5812
|
+
const ny = -near2.tangent[0];
|
|
5813
|
+
const [cx, cy] = near2.point;
|
|
5814
|
+
best = {
|
|
5815
|
+
nodeId: n.id,
|
|
5816
|
+
subpath: near2.contour,
|
|
5817
|
+
t: near2.t,
|
|
5818
|
+
point: applyMatrix(m, { x: cx, y: cy }),
|
|
5819
|
+
left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
|
|
5820
|
+
right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
|
|
5821
|
+
distance: near2.distance
|
|
5822
|
+
};
|
|
5823
|
+
});
|
|
5824
|
+
if (!best) return null;
|
|
5825
|
+
const { distance: _d, ...spot } = best;
|
|
5826
|
+
return spot;
|
|
5827
|
+
}
|
|
5828
|
+
downWidth(screen, p, input) {
|
|
5829
|
+
const hit = this.hitWidthHandle(screen);
|
|
5830
|
+
const spot = hit ? null : this.strokeSpotAt(p);
|
|
5831
|
+
const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
|
|
5832
|
+
const node = nodeId ? this.getNode(nodeId) : void 0;
|
|
5833
|
+
const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
|
|
5834
|
+
if (!node || node.type !== "path" || !g) {
|
|
5835
|
+
if (this.widthPoint) {
|
|
5836
|
+
this.widthPoint = null;
|
|
5837
|
+
this.notify();
|
|
5838
|
+
}
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5841
|
+
const before = this.snapshot();
|
|
5842
|
+
const local = this.toLocal(node.id, p);
|
|
5843
|
+
let profile;
|
|
5844
|
+
let ref;
|
|
5845
|
+
let mode = "width";
|
|
5846
|
+
let side = "both";
|
|
5847
|
+
let t;
|
|
5848
|
+
let subpath;
|
|
5849
|
+
if (hit) {
|
|
5850
|
+
profile = sanitizeWidthProfile(node.strokeWidthProfile);
|
|
5851
|
+
ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
|
|
5852
|
+
t = profile[ref.index]?.t ?? 0;
|
|
5853
|
+
subpath = hit.ref.subpath;
|
|
5854
|
+
if (hit.ref.part === "center") mode = "move";
|
|
5855
|
+
else if (input.alt) side = hit.ref.part;
|
|
5856
|
+
} else {
|
|
5857
|
+
const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
|
|
5858
|
+
const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
|
|
5859
|
+
profile = inserted.points;
|
|
5860
|
+
subpath = near2.contour;
|
|
5861
|
+
t = near2.t;
|
|
5862
|
+
ref = { nodeId: node.id, index: inserted.index, subpath };
|
|
5863
|
+
if (input.alt) side = near2.side;
|
|
5864
|
+
}
|
|
5865
|
+
const at2 = strokePositionPoint(g.line, subpath, t);
|
|
5866
|
+
if (!at2) return;
|
|
5867
|
+
const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
5868
|
+
this.doc = base;
|
|
5869
|
+
this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
|
|
5870
|
+
this.drag = {
|
|
5871
|
+
kind: "width",
|
|
5872
|
+
ref,
|
|
5873
|
+
mode,
|
|
5874
|
+
side,
|
|
5875
|
+
origin: { x: at2.point[0], y: at2.point[1] },
|
|
5876
|
+
normal: { x: at2.tangent[1], y: -at2.tangent[0] },
|
|
5877
|
+
unit: g.unit,
|
|
5878
|
+
profile,
|
|
5879
|
+
before,
|
|
5880
|
+
base,
|
|
5881
|
+
start: p,
|
|
5882
|
+
moved: false,
|
|
5883
|
+
created: !hit
|
|
5884
|
+
};
|
|
5885
|
+
const w = profile[ref.index];
|
|
5886
|
+
this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
|
|
5887
|
+
this.notify(!hit);
|
|
5888
|
+
}
|
|
4370
5889
|
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
4371
5890
|
nearestOutline(p) {
|
|
4372
5891
|
let best = null;
|
|
@@ -4414,7 +5933,9 @@ var VectorEditorController = class {
|
|
|
4414
5933
|
case "pen":
|
|
4415
5934
|
return this.downPen(screen, p, input);
|
|
4416
5935
|
case "pencil":
|
|
4417
|
-
return this.downPencil(p);
|
|
5936
|
+
return this.downPencil(p, input);
|
|
5937
|
+
case "width":
|
|
5938
|
+
return this.downWidth(screen, p, input);
|
|
4418
5939
|
case "scissors": {
|
|
4419
5940
|
const seg = this.nearestOutline(p);
|
|
4420
5941
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -4500,6 +6021,13 @@ var VectorEditorController = class {
|
|
|
4500
6021
|
this.notify();
|
|
4501
6022
|
return;
|
|
4502
6023
|
}
|
|
6024
|
+
const widget = this.hitCornerWidget(screen);
|
|
6025
|
+
if (widget) {
|
|
6026
|
+
const refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);
|
|
6027
|
+
this.drag = { kind: "corner", widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };
|
|
6028
|
+
this.notify();
|
|
6029
|
+
return;
|
|
6030
|
+
}
|
|
4503
6031
|
for (const id of getEditablePathIds(this.view())) {
|
|
4504
6032
|
const node = this.getNode(id);
|
|
4505
6033
|
const seg = nearestSegment(node, this.toLocal(id, p));
|
|
@@ -4557,10 +6085,10 @@ var VectorEditorController = class {
|
|
|
4557
6085
|
}
|
|
4558
6086
|
const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
|
|
4559
6087
|
const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
|
|
4560
|
-
let
|
|
4561
|
-
|
|
6088
|
+
let pt3 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
|
|
6089
|
+
pt3 = this.toLocal(pen.nodeId, pt3);
|
|
4562
6090
|
const node2 = this.getNode(pen.nodeId);
|
|
4563
|
-
const anchors = [...sp.anchors, { x:
|
|
6091
|
+
const anchors = [...sp.anchors, { x: pt3.x, y: pt3.y }];
|
|
4564
6092
|
this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
|
|
4565
6093
|
const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
|
|
4566
6094
|
this.anchors = [ref2];
|
|
@@ -4588,8 +6116,8 @@ var VectorEditorController = class {
|
|
|
4588
6116
|
this.notify(this.doc !== before.doc);
|
|
4589
6117
|
return;
|
|
4590
6118
|
}
|
|
4591
|
-
const
|
|
4592
|
-
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x:
|
|
6119
|
+
const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
6120
|
+
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
|
|
4593
6121
|
this.setDoc([...this.doc.nodes, node]);
|
|
4594
6122
|
this.pen = { nodeId: node.id, subpath: 0 };
|
|
4595
6123
|
this.selection = [node.id];
|
|
@@ -4600,7 +6128,7 @@ var VectorEditorController = class {
|
|
|
4600
6128
|
this.interaction = { type: "pen", nodeId: node.id };
|
|
4601
6129
|
this.notify(true);
|
|
4602
6130
|
}
|
|
4603
|
-
downPencil(p) {
|
|
6131
|
+
downPencil(p, input = {}) {
|
|
4604
6132
|
const before = this.snapshot();
|
|
4605
6133
|
const node = {
|
|
4606
6134
|
id: this.newId(),
|
|
@@ -4614,7 +6142,11 @@ var VectorEditorController = class {
|
|
|
4614
6142
|
this.setDoc([...this.doc.nodes, node]);
|
|
4615
6143
|
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
4616
6144
|
lazy.push([p.x, p.y]);
|
|
4617
|
-
|
|
6145
|
+
const mode = this.editing.pencilPressure;
|
|
6146
|
+
const hasPressure = typeof input.pressure === "number" && Number.isFinite(input.pressure);
|
|
6147
|
+
const record = hasPressure && (mode === "all" || mode === "pen" && input.pointerType === "pen");
|
|
6148
|
+
const pressure = hasPressure ? input.pressure : 0.5;
|
|
6149
|
+
this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };
|
|
4618
6150
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
4619
6151
|
this.notify(true);
|
|
4620
6152
|
}
|
|
@@ -4737,13 +6269,25 @@ var VectorEditorController = class {
|
|
|
4737
6269
|
case "handle": {
|
|
4738
6270
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
4739
6271
|
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,
|
|
6272
|
+
const pt2 = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
|
|
6273
|
+
const local = this.toLocal(drag.ref.nodeId, pt2, drag.base);
|
|
4742
6274
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
|
|
4743
6275
|
this.interaction = { type: "handle", ref: drag.ref };
|
|
4744
6276
|
this.notify(true);
|
|
4745
6277
|
return;
|
|
4746
6278
|
}
|
|
6279
|
+
case "corner": {
|
|
6280
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;
|
|
6281
|
+
drag.moved = true;
|
|
6282
|
+
const c = drag.widget.corner;
|
|
6283
|
+
const inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));
|
|
6284
|
+
const d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });
|
|
6285
|
+
const radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));
|
|
6286
|
+
this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));
|
|
6287
|
+
this.interaction = { type: "corner", ref: drag.widget.ref, radius };
|
|
6288
|
+
this.notify(true);
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
4747
6291
|
case "bend": {
|
|
4748
6292
|
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
4749
6293
|
drag.moved = true;
|
|
@@ -4769,10 +6313,10 @@ var VectorEditorController = class {
|
|
|
4769
6313
|
return;
|
|
4770
6314
|
}
|
|
4771
6315
|
this.guides = null;
|
|
4772
|
-
let
|
|
4773
|
-
if (input.shift)
|
|
6316
|
+
let pt2 = p;
|
|
6317
|
+
if (input.shift) pt2 = constrain45(anchor, pt2);
|
|
4774
6318
|
const side = drag.closing ? "in" : "out";
|
|
4775
|
-
const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x -
|
|
6319
|
+
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
6320
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
|
|
4777
6321
|
if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
|
|
4778
6322
|
return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
|
|
@@ -4811,12 +6355,36 @@ var VectorEditorController = class {
|
|
|
4811
6355
|
this.notify(true);
|
|
4812
6356
|
return;
|
|
4813
6357
|
}
|
|
6358
|
+
case "width": {
|
|
6359
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
6360
|
+
drag.moved = true;
|
|
6361
|
+
const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
|
|
6362
|
+
const local = this.toLocal(node.id, p, drag.base);
|
|
6363
|
+
let profile;
|
|
6364
|
+
if (drag.mode === "move") {
|
|
6365
|
+
const g = vectorStrokeWidthGeometry(node);
|
|
6366
|
+
const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
|
|
6367
|
+
if (!near2) return;
|
|
6368
|
+
profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
|
|
6369
|
+
} else {
|
|
6370
|
+
const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
|
|
6371
|
+
const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
|
|
6372
|
+
profile = setWidthPoint(drag.profile, drag.ref.index, patch);
|
|
6373
|
+
}
|
|
6374
|
+
this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
|
|
6375
|
+
const w = profile[drag.ref.index];
|
|
6376
|
+
this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
|
|
6377
|
+
this.notify(true);
|
|
6378
|
+
return;
|
|
6379
|
+
}
|
|
4814
6380
|
case "pencil": {
|
|
4815
6381
|
const last = drag.points[drag.points.length - 1];
|
|
4816
6382
|
let moved = false;
|
|
6383
|
+
if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
|
|
4817
6384
|
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
4818
6385
|
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
4819
6386
|
drag.points.push(q);
|
|
6387
|
+
drag.pressures?.push(drag.pressure);
|
|
4820
6388
|
moved = true;
|
|
4821
6389
|
}
|
|
4822
6390
|
const string = drag.lazy.string();
|
|
@@ -4885,19 +6453,19 @@ var VectorEditorController = class {
|
|
|
4885
6453
|
const guides = { lines: [], labels: [] };
|
|
4886
6454
|
for (const axis of ["x", "y"]) {
|
|
4887
6455
|
const d = end[axis] - start[axis];
|
|
4888
|
-
const
|
|
4889
|
-
if (
|
|
4890
|
-
out[axis] = start[axis] + Math.sign(d) *
|
|
6456
|
+
const len2 = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
|
|
6457
|
+
if (len2 === null || len2 <= 0) continue;
|
|
6458
|
+
out[axis] = start[axis] + Math.sign(d) * len2 / k;
|
|
4891
6459
|
const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
|
|
4892
6460
|
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(
|
|
6461
|
+
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
6462
|
}
|
|
4895
6463
|
this.setGuides(guides);
|
|
4896
6464
|
return out;
|
|
4897
6465
|
}
|
|
4898
6466
|
/** Primary button released. */
|
|
4899
6467
|
pointerUp(screen, input = {}) {
|
|
4900
|
-
if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
|
|
6468
|
+
if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, { ...input, pressure: void 0 });
|
|
4901
6469
|
const drag = this.drag;
|
|
4902
6470
|
this.drag = null;
|
|
4903
6471
|
this.interaction = null;
|
|
@@ -4935,6 +6503,29 @@ var VectorEditorController = class {
|
|
|
4935
6503
|
return;
|
|
4936
6504
|
}
|
|
4937
6505
|
return this.commit("Bend Segment", drag.before);
|
|
6506
|
+
case "corner": {
|
|
6507
|
+
if (drag.moved) return this.commit("Corner Radius", drag.before);
|
|
6508
|
+
if (drag.alt) {
|
|
6509
|
+
const kind = nextVectorCornerKind(drag.widget.kind);
|
|
6510
|
+
this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
|
|
6511
|
+
return this.commit("Corner Kind", drag.before);
|
|
6512
|
+
}
|
|
6513
|
+
const ref = drag.widget.ref;
|
|
6514
|
+
const has = this.anchors.some((a) => sameRef(a, ref));
|
|
6515
|
+
this.editingId = ref.nodeId;
|
|
6516
|
+
this.anchors = drag.shift ? has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref] : [ref];
|
|
6517
|
+
this.notify();
|
|
6518
|
+
return;
|
|
6519
|
+
}
|
|
6520
|
+
case "width": {
|
|
6521
|
+
if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
|
|
6522
|
+
if (drag.created) {
|
|
6523
|
+
this.doc = drag.before.doc;
|
|
6524
|
+
this.widthPoint = null;
|
|
6525
|
+
this.notify(true);
|
|
6526
|
+
} else this.notify();
|
|
6527
|
+
return;
|
|
6528
|
+
}
|
|
4938
6529
|
case "stroke": {
|
|
4939
6530
|
this.notify();
|
|
4940
6531
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -4965,7 +6556,10 @@ var VectorEditorController = class {
|
|
|
4965
6556
|
this.notify(true);
|
|
4966
6557
|
return;
|
|
4967
6558
|
}
|
|
4968
|
-
for (const q of drag.lazy.finish())
|
|
6559
|
+
for (const q of drag.lazy.finish()) {
|
|
6560
|
+
drag.points.push(q);
|
|
6561
|
+
drag.pressures?.push(drag.pressure);
|
|
6562
|
+
}
|
|
4969
6563
|
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
4970
6564
|
if (shape) {
|
|
4971
6565
|
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
@@ -4980,7 +6574,8 @@ var VectorEditorController = class {
|
|
|
4980
6574
|
anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
|
|
4981
6575
|
}
|
|
4982
6576
|
const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
|
|
4983
|
-
|
|
6577
|
+
const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
|
|
6578
|
+
this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
|
|
4984
6579
|
this.selection = [drag.nodeId];
|
|
4985
6580
|
return this.commit("Draw", drag.before);
|
|
4986
6581
|
}
|
|
@@ -5102,7 +6697,10 @@ var VectorEditorController = class {
|
|
|
5102
6697
|
switch (key) {
|
|
5103
6698
|
case "Escape":
|
|
5104
6699
|
if (this.drag) this.cancelDrag();
|
|
5105
|
-
else if (this.
|
|
6700
|
+
else if (this.tool === "width" && this.widthPoint) {
|
|
6701
|
+
this.widthPoint = null;
|
|
6702
|
+
this.notify();
|
|
6703
|
+
} else if (this.pen) this.finishPath();
|
|
5106
6704
|
else if (this.tool === "direct" && this.anchors.length) {
|
|
5107
6705
|
this.anchors = [];
|
|
5108
6706
|
this.notify();
|
|
@@ -5123,6 +6721,7 @@ var VectorEditorController = class {
|
|
|
5123
6721
|
return false;
|
|
5124
6722
|
case "Delete":
|
|
5125
6723
|
case "Backspace":
|
|
6724
|
+
if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
|
|
5126
6725
|
if (this.pen) {
|
|
5127
6726
|
const node = this.getNode(this.pen.nodeId);
|
|
5128
6727
|
const sp = node.subpaths[this.pen.subpath];
|
|
@@ -5179,9 +6778,9 @@ function inkShapeSubpath(shape) {
|
|
|
5179
6778
|
function constrain45(origin, p) {
|
|
5180
6779
|
const dx = p.x - origin.x;
|
|
5181
6780
|
const dy = p.y - origin.y;
|
|
5182
|
-
const
|
|
6781
|
+
const len2 = Math.hypot(dx, dy);
|
|
5183
6782
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
5184
|
-
return { x: origin.x + Math.cos(a) *
|
|
6783
|
+
return { x: origin.x + Math.cos(a) * len2, y: origin.y + Math.sin(a) * len2 };
|
|
5185
6784
|
}
|
|
5186
6785
|
function scaleFor(handle, b, p, mods = {}) {
|
|
5187
6786
|
const west = handle.includes("w");
|
|
@@ -5204,6 +6803,31 @@ function scaleFor(handle, b, p, mods = {}) {
|
|
|
5204
6803
|
}
|
|
5205
6804
|
return { sx, sy, origin };
|
|
5206
6805
|
}
|
|
6806
|
+
function withStyle(node, style) {
|
|
6807
|
+
const next = { ...node, ...style };
|
|
6808
|
+
if ("strokeWidthProfile" in style) {
|
|
6809
|
+
const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
|
|
6810
|
+
if (p.length) next.strokeWidthProfile = p;
|
|
6811
|
+
else delete next.strokeWidthProfile;
|
|
6812
|
+
}
|
|
6813
|
+
return next;
|
|
6814
|
+
}
|
|
6815
|
+
function hiddenOrLocked(nodes, id) {
|
|
6816
|
+
const loc = locateVectorNode(nodes, id);
|
|
6817
|
+
if (!loc) return false;
|
|
6818
|
+
const n = loc.list[loc.index];
|
|
6819
|
+
if (n.locked || n.visible === false) return true;
|
|
6820
|
+
return loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;
|
|
6821
|
+
}
|
|
6822
|
+
function pencilPressureProfile(points, pressures) {
|
|
6823
|
+
const n = Math.min(points.length, pressures.length);
|
|
6824
|
+
if (n < 2) return [];
|
|
6825
|
+
const s = [0];
|
|
6826
|
+
for (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));
|
|
6827
|
+
const total = s[n - 1];
|
|
6828
|
+
if (!(total > 0)) return [];
|
|
6829
|
+
return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
|
|
6830
|
+
}
|
|
5207
6831
|
|
|
5208
6832
|
// src/vector/render.ts
|
|
5209
6833
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
@@ -5218,10 +6842,12 @@ function renderNode(node, defs, idPrefix) {
|
|
|
5218
6842
|
const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
|
|
5219
6843
|
if (fill.gradient) defs.push(fill.gradient);
|
|
5220
6844
|
if (stroke.gradient) defs.push(stroke.gradient);
|
|
6845
|
+
const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
|
|
5221
6846
|
return {
|
|
5222
6847
|
kind: "path",
|
|
5223
6848
|
id: node.id,
|
|
5224
6849
|
...node.transform ? { transform: fmtMatrix(node.transform) } : {},
|
|
6850
|
+
...strokeParts ? { strokeParts } : {},
|
|
5225
6851
|
d: vectorPathData(node),
|
|
5226
6852
|
fill: fill.value,
|
|
5227
6853
|
stroke: stroke.value,
|
|
@@ -5237,9 +6863,9 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
5237
6863
|
const defs = [];
|
|
5238
6864
|
const prefix = options.idPrefix ?? "ve-";
|
|
5239
6865
|
const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
|
|
5240
|
-
const
|
|
6866
|
+
const v2 = state.viewport;
|
|
5241
6867
|
return {
|
|
5242
|
-
transform: `translate(${fmt(
|
|
6868
|
+
transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
|
|
5243
6869
|
width: state.document.width,
|
|
5244
6870
|
height: state.document.height,
|
|
5245
6871
|
...state.document.background ? { background: state.document.background } : {},
|
|
@@ -5249,8 +6875,8 @@ function getVectorSceneRenderData(state, options = {}) {
|
|
|
5249
6875
|
}
|
|
5250
6876
|
var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
|
|
5251
6877
|
function screenOutline(state, node) {
|
|
5252
|
-
const
|
|
5253
|
-
const view = [
|
|
6878
|
+
const v2 = state.viewport;
|
|
6879
|
+
const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
|
|
5254
6880
|
const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
|
|
5255
6881
|
return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
|
|
5256
6882
|
}
|
|
@@ -5359,8 +6985,28 @@ function getVectorOverlayRenderData(state) {
|
|
|
5359
6985
|
const b = toScreen(state, it.string[1]);
|
|
5360
6986
|
pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
|
|
5361
6987
|
}
|
|
6988
|
+
const dragged = it?.type === "corner" ? it.ref : null;
|
|
6989
|
+
const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
|
|
6990
|
+
const s = toScreen(state, w);
|
|
6991
|
+
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 };
|
|
6992
|
+
});
|
|
6993
|
+
if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
|
|
5362
6994
|
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
|
-
|
|
6995
|
+
const sc = (p) => {
|
|
6996
|
+
const q = toScreen(state, p);
|
|
6997
|
+
return { x: fmt(q.x), y: fmt(q.y) };
|
|
6998
|
+
};
|
|
6999
|
+
const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
|
|
7000
|
+
const widthPoints = getVectorWidthHandles(state).map((h) => {
|
|
7001
|
+
const c = sc(h);
|
|
7002
|
+
const hw = state.hover?.widthPoint;
|
|
7003
|
+
const hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;
|
|
7004
|
+
return { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === "width" && sameWidth(it.ref, h.ref) };
|
|
7005
|
+
});
|
|
7006
|
+
const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
|
|
7007
|
+
const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
|
|
7008
|
+
if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
|
|
7009
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot };
|
|
5364
7010
|
}
|
|
5365
7011
|
var VECTOR_OVERLAY_THEME = {
|
|
5366
7012
|
accent: "#0ea5e9",
|
|
@@ -5372,13 +7018,25 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
5372
7018
|
eraserFill: "rgba(239, 68, 68, 0.25)",
|
|
5373
7019
|
transformHandleSize: 8,
|
|
5374
7020
|
anchorSize: 7,
|
|
5375
|
-
handleRadius: 3.5
|
|
7021
|
+
handleRadius: 3.5,
|
|
7022
|
+
/** Radius of a live corner widget (a ring with a dot). */
|
|
7023
|
+
cornerWidgetRadius: 4,
|
|
7024
|
+
/** Half the diagonal of a width point's diamond. */
|
|
7025
|
+
widthPointSize: 5
|
|
5376
7026
|
};
|
|
7027
|
+
function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
|
|
7028
|
+
return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
|
|
7029
|
+
}
|
|
5377
7030
|
function toVectorModifiers(event) {
|
|
5378
7031
|
return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
|
|
5379
7032
|
}
|
|
5380
7033
|
function toVectorPointerInput(event) {
|
|
5381
|
-
return {
|
|
7034
|
+
return {
|
|
7035
|
+
...toVectorModifiers(event),
|
|
7036
|
+
button: event.button,
|
|
7037
|
+
...typeof event.pressure === "number" ? { pressure: event.pressure } : {},
|
|
7038
|
+
...event.pointerType ? { pointerType: event.pointerType } : {}
|
|
7039
|
+
};
|
|
5382
7040
|
}
|
|
5383
7041
|
function toVectorKeyInput(event) {
|
|
5384
7042
|
return { key: event.key, ...toVectorModifiers(event) };
|
|
@@ -5396,18 +7054,75 @@ function getVectorSelectionStyle(state) {
|
|
|
5396
7054
|
const node = findVectorNode(state.document.nodes, id);
|
|
5397
7055
|
if (node) paths.push(...collect(node));
|
|
5398
7056
|
}
|
|
7057
|
+
const anchorMode = state.tool === "direct" && !!state.selectedAnchors?.length;
|
|
7058
|
+
if (anchorMode) {
|
|
7059
|
+
for (const r of state.selectedAnchors) {
|
|
7060
|
+
if (paths.some((p) => p.id === r.nodeId)) continue;
|
|
7061
|
+
const node = findVectorNode(state.document.nodes, r.nodeId);
|
|
7062
|
+
if (node?.type === "path") paths.push(node);
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
5399
7065
|
if (!paths.length) return null;
|
|
5400
7066
|
const first = paths[0];
|
|
5401
7067
|
const same = (get) => paths.every((p) => JSON.stringify(get(p) ?? null) === JSON.stringify(get(first) ?? null));
|
|
7068
|
+
const corners2 = [];
|
|
7069
|
+
for (const p of paths) {
|
|
7070
|
+
for (const c of vectorCorners(p.subpaths)) {
|
|
7071
|
+
if (anchorMode && !state.selectedAnchors.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;
|
|
7072
|
+
const corner = p.subpaths[c.subpath].anchors[c.index].corner;
|
|
7073
|
+
corners2.push({ radius: corner?.radius ?? 0, kind: corner?.kind ?? "round" });
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
const c0 = corners2[0] ?? { radius: 0, kind: "round" };
|
|
5402
7077
|
return {
|
|
5403
7078
|
fill: first.fill ?? null,
|
|
5404
7079
|
stroke: first.stroke ?? null,
|
|
5405
7080
|
strokeWidth: first.strokeWidth ?? 1,
|
|
5406
7081
|
opacity: first.opacity ?? 1,
|
|
5407
|
-
|
|
7082
|
+
strokeAlign: first.strokeAlign ?? "center",
|
|
7083
|
+
startArrow: first.startArrow ?? null,
|
|
7084
|
+
endArrow: first.endArrow ?? null,
|
|
7085
|
+
arrowAlign: first.arrowAlign ?? "extend",
|
|
7086
|
+
startArrowScale: first.startArrowScale ?? 1,
|
|
7087
|
+
endArrowScale: first.endArrowScale ?? 1,
|
|
7088
|
+
strokeWidthProfile: profileOf(first),
|
|
7089
|
+
widthProfile: widthProfilePresetId(profileOf(first)),
|
|
7090
|
+
cornerRadius: c0.radius,
|
|
7091
|
+
cornerKind: c0.kind,
|
|
7092
|
+
corners: corners2.length,
|
|
7093
|
+
hasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),
|
|
7094
|
+
mixed: {
|
|
7095
|
+
fill: !same((p) => p.fill),
|
|
7096
|
+
stroke: !same((p) => p.stroke),
|
|
7097
|
+
strokeWidth: !same((p) => p.strokeWidth ?? 1),
|
|
7098
|
+
opacity: !same((p) => p.opacity ?? 1),
|
|
7099
|
+
strokeAlign: !same((p) => p.strokeAlign ?? "center"),
|
|
7100
|
+
startArrow: !same((p) => p.startArrow ?? null),
|
|
7101
|
+
endArrow: !same((p) => p.endArrow ?? null),
|
|
7102
|
+
arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
|
|
7103
|
+
startArrowScale: !same((p) => p.startArrowScale ?? 1),
|
|
7104
|
+
endArrowScale: !same((p) => p.endArrowScale ?? 1),
|
|
7105
|
+
widthProfile: !same(profileOf),
|
|
7106
|
+
cornerRadius: corners2.some((c) => c.radius !== c0.radius),
|
|
7107
|
+
cornerKind: corners2.some((c) => c.kind !== c0.kind)
|
|
7108
|
+
},
|
|
5408
7109
|
pathIds: paths.map((p) => p.id)
|
|
5409
7110
|
};
|
|
5410
7111
|
}
|
|
7112
|
+
function profileOf(p) {
|
|
7113
|
+
const points = sanitizeWidthProfile(p.strokeWidthProfile);
|
|
7114
|
+
return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
|
|
7115
|
+
}
|
|
7116
|
+
var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
|
|
7117
|
+
var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
|
|
7118
|
+
var VECTOR_ARROWHEAD_LABELS = {
|
|
7119
|
+
triangle: "Triangle",
|
|
7120
|
+
arrow: "Arrow",
|
|
7121
|
+
open: "Open arrow",
|
|
7122
|
+
circle: "Circle",
|
|
7123
|
+
square: "Square",
|
|
7124
|
+
bar: "Bar"
|
|
7125
|
+
};
|
|
5411
7126
|
function vectorPaintToHex(paint) {
|
|
5412
7127
|
if (typeof paint !== "string") return null;
|
|
5413
7128
|
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
|
|
@@ -5420,7 +7135,8 @@ var VECTOR_PATH_ACTIONS = [
|
|
|
5420
7135
|
{ id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
|
|
5421
7136
|
{ id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
|
|
5422
7137
|
{ 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() }
|
|
7138
|
+
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
|
|
7139
|
+
{ id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
|
|
5424
7140
|
];
|
|
5425
7141
|
var VECTOR_EDIT_TOGGLES = [
|
|
5426
7142
|
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
@@ -5448,6 +7164,19 @@ var POINTS = {
|
|
|
5448
7164
|
minItems: 1,
|
|
5449
7165
|
description: "Polyline [[x, y], \u2026] in document units."
|
|
5450
7166
|
};
|
|
7167
|
+
var WIDTH_POINTS = {
|
|
7168
|
+
type: ["array", "null"],
|
|
7169
|
+
items: {
|
|
7170
|
+
type: "object",
|
|
7171
|
+
properties: {
|
|
7172
|
+
t: { type: "number", minimum: 0, maximum: 1, description: "Position along each subpath by length (0 start, 1 end)." },
|
|
7173
|
+
left: { type: "number", minimum: 0, description: "Left side as a multiple of half the stroke width (1 = uniform)." },
|
|
7174
|
+
right: { type: "number", minimum: 0, description: "Right side (looking along the path, y down)." }
|
|
7175
|
+
},
|
|
7176
|
+
required: ["t", "left", "right"]
|
|
7177
|
+
},
|
|
7178
|
+
description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
|
|
7179
|
+
};
|
|
5451
7180
|
var PAINT = {
|
|
5452
7181
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
5453
7182
|
};
|
|
@@ -5458,7 +7187,14 @@ var STYLE_PROPS = {
|
|
|
5458
7187
|
strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
|
|
5459
7188
|
strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
|
|
5460
7189
|
strokeDasharray: { type: "array", items: { type: "number" } },
|
|
5461
|
-
fillRule: { type: "string", enum: ["nonzero", "evenodd"] }
|
|
7190
|
+
fillRule: { type: "string", enum: ["nonzero", "evenodd"] },
|
|
7191
|
+
strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred)." },
|
|
7192
|
+
startArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the start of an open path, or null for none." },
|
|
7193
|
+
endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
|
|
7194
|
+
arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
|
|
7195
|
+
startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
|
|
7196
|
+
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
|
|
7197
|
+
strokeWidthProfile: WIDTH_POINTS
|
|
5462
7198
|
};
|
|
5463
7199
|
var pickStyle = (params) => {
|
|
5464
7200
|
const out = {};
|
|
@@ -5551,7 +7287,8 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5551
7287
|
y: { type: "number" },
|
|
5552
7288
|
width: { type: "number", description: "For a line: the x offset of its end." },
|
|
5553
7289
|
height: { type: "number", description: "For a line: the y offset of its end." },
|
|
5554
|
-
cornerRadius: { type: "number", minimum: 0 },
|
|
7290
|
+
cornerRadius: { type: "number", minimum: 0, description: "Live corner radius of a rect, polygon or star." },
|
|
7291
|
+
cornerKind: { type: "string", enum: VECTOR_CORNER_KINDS },
|
|
5555
7292
|
sides: { type: "integer", minimum: 3 },
|
|
5556
7293
|
points: { type: "integer", minimum: 3 },
|
|
5557
7294
|
innerRatio: { type: "number", minimum: 0, maximum: 1 },
|
|
@@ -5597,6 +7334,76 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5597
7334
|
return { ok: true };
|
|
5598
7335
|
}
|
|
5599
7336
|
},
|
|
7337
|
+
{
|
|
7338
|
+
...base,
|
|
7339
|
+
id: `${prefix}setCorners`,
|
|
7340
|
+
label: "Corner Radius\u2026",
|
|
7341
|
+
keywords: ["round", "rounded", "corners", "radius", "fillet", "chamfer", "live corners"],
|
|
7342
|
+
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.",
|
|
7343
|
+
params: { type: "object", properties: { radius: { type: "number", minimum: 0 }, kind: { type: "string", enum: VECTOR_CORNER_KINDS }, ids: IDS, anchors: ANCHORS } },
|
|
7344
|
+
undoable: true,
|
|
7345
|
+
run: (_ctx, params) => {
|
|
7346
|
+
const refs = params?.anchors ? anchorsOf(params.anchors) : void 0;
|
|
7347
|
+
const target = { refs, ids: params?.ids ? ids(params) : void 0 };
|
|
7348
|
+
if (params?.radius !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));
|
|
7349
|
+
else if (params?.kind) check(controller.setCornerKind(params.kind, target));
|
|
7350
|
+
else fail("Pass `radius` and / or `kind`.");
|
|
7351
|
+
return { ok: true };
|
|
7352
|
+
}
|
|
7353
|
+
},
|
|
7354
|
+
{
|
|
7355
|
+
...base,
|
|
7356
|
+
id: `${prefix}setWidthProfile`,
|
|
7357
|
+
label: "Width Profile\u2026",
|
|
7358
|
+
keywords: ["variable width", "taper", "tapered", "pressure", "profile", "brush", "calligraphy", "bulge", "thick", "thin"],
|
|
7359
|
+
description: "Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0\u20131, startEase / endEase 0\u20131) and / or `wave` (amount 0\u20131, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.",
|
|
7360
|
+
params: {
|
|
7361
|
+
type: "object",
|
|
7362
|
+
properties: {
|
|
7363
|
+
ids: IDS,
|
|
7364
|
+
preset: { type: "string", enum: VECTOR_WIDTH_PRESET_IDS },
|
|
7365
|
+
points: WIDTH_POINTS,
|
|
7366
|
+
taper: {
|
|
7367
|
+
type: "object",
|
|
7368
|
+
properties: {
|
|
7369
|
+
startLength: { type: "number", minimum: 0, maximum: 1 },
|
|
7370
|
+
endLength: { type: "number", minimum: 0, maximum: 1 },
|
|
7371
|
+
startWidth: { type: "number", minimum: 0, maximum: 1 },
|
|
7372
|
+
endWidth: { type: "number", minimum: 0, maximum: 1 },
|
|
7373
|
+
startEase: { type: "number", minimum: 0, maximum: 1 },
|
|
7374
|
+
endEase: { type: "number", minimum: 0, maximum: 1 }
|
|
7375
|
+
}
|
|
7376
|
+
},
|
|
7377
|
+
wave: { type: "object", properties: { amount: { type: "number", minimum: 0, maximum: 1 }, cycles: { type: "number", exclusiveMinimum: 0 }, phase: { type: "number" } } }
|
|
7378
|
+
}
|
|
7379
|
+
},
|
|
7380
|
+
enabled: enabledFor,
|
|
7381
|
+
undoable: true,
|
|
7382
|
+
run: (_ctx, params) => {
|
|
7383
|
+
const target = { ids: ids(params) };
|
|
7384
|
+
if (params?.preset) check(controller.setWidthProfile(params.preset, target));
|
|
7385
|
+
else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
|
|
7386
|
+
else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
|
|
7387
|
+
else fail("Pass `preset`, `points`, `taper` or `wave`.");
|
|
7388
|
+
const node = controller.getNode(target.ids[0]);
|
|
7389
|
+
return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
|
|
7390
|
+
}
|
|
7391
|
+
},
|
|
7392
|
+
{
|
|
7393
|
+
...base,
|
|
7394
|
+
id: `${prefix}outlineStroke`,
|
|
7395
|
+
label: "Outline Stroke",
|
|
7396
|
+
keywords: ["expand", "stroke to path", "outline", "convert stroke"],
|
|
7397
|
+
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.",
|
|
7398
|
+
params: { type: "object", properties: { ids: IDS } },
|
|
7399
|
+
enabled: enabledFor,
|
|
7400
|
+
undoable: true,
|
|
7401
|
+
run: (_ctx, params) => {
|
|
7402
|
+
const created = controller.outlineStroke({ ids: params?.ids ? ids(params) : void 0 });
|
|
7403
|
+
if (!created.length) fail("No selected path has a stroke.");
|
|
7404
|
+
return { ids: created };
|
|
7405
|
+
}
|
|
7406
|
+
},
|
|
5600
7407
|
{
|
|
5601
7408
|
...base,
|
|
5602
7409
|
id: `${prefix}transform`,
|
|
@@ -5888,7 +7695,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5888
7695
|
id: `${prefix}setEditing`,
|
|
5889
7696
|
label: "Path Editing Options\u2026",
|
|
5890
7697
|
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
5891
|
-
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `
|
|
7698
|
+
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).",
|
|
5892
7699
|
params: {
|
|
5893
7700
|
type: "object",
|
|
5894
7701
|
properties: {
|
|
@@ -5896,12 +7703,13 @@ function createVectorCommands(controller, options = {}) {
|
|
|
5896
7703
|
curvatureMoves: { type: "boolean" },
|
|
5897
7704
|
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
5898
7705
|
recognizeShapes: { type: "boolean" },
|
|
5899
|
-
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
7706
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 },
|
|
7707
|
+
pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
|
|
5900
7708
|
}
|
|
5901
7709
|
},
|
|
5902
7710
|
run: (_ctx, params) => {
|
|
5903
7711
|
const patch = {};
|
|
5904
|
-
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
7712
|
+
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
5905
7713
|
controller.setEditing(patch);
|
|
5906
7714
|
return { editing: controller.getState().editing };
|
|
5907
7715
|
}
|
|
@@ -5947,24 +7755,41 @@ export {
|
|
|
5947
7755
|
DEFAULT_VECTOR_PATH_EDIT,
|
|
5948
7756
|
DEFAULT_VECTOR_SNAP,
|
|
5949
7757
|
IDENTITY_MATRIX,
|
|
5950
|
-
KAPPA,
|
|
7758
|
+
KAPPA2 as KAPPA,
|
|
5951
7759
|
LazyMouse,
|
|
7760
|
+
VECTOR_ARROWHEADS,
|
|
7761
|
+
VECTOR_ARROWHEAD_LABELS,
|
|
5952
7762
|
VECTOR_CLIPBOARD_TYPE,
|
|
5953
7763
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
7764
|
+
VECTOR_CORNER_KINDS,
|
|
7765
|
+
VECTOR_CORNER_KIND_LABELS,
|
|
7766
|
+
VECTOR_CORNER_WIDGET_INSET,
|
|
5954
7767
|
VECTOR_EDIT_TOGGLES,
|
|
5955
7768
|
VECTOR_INK_MAX_CORNERS,
|
|
5956
7769
|
VECTOR_OVERLAY_THEME,
|
|
5957
7770
|
VECTOR_PATH_ACTIONS,
|
|
5958
7771
|
VECTOR_PENCIL_STRING_PX,
|
|
7772
|
+
VECTOR_PRESSURE_NOMINAL,
|
|
5959
7773
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
7774
|
+
VECTOR_STROKE_ALIGN_LABELS,
|
|
7775
|
+
VECTOR_STYLE_KEYS,
|
|
5960
7776
|
VECTOR_TOOLS,
|
|
5961
7777
|
VECTOR_TOOL_GLYPHS,
|
|
5962
7778
|
VECTOR_TOOL_SHORTCUTS,
|
|
5963
7779
|
VECTOR_TRANSFORM_HANDLES,
|
|
7780
|
+
VECTOR_WIDTH_PRESETS,
|
|
7781
|
+
VECTOR_WIDTH_PRESET_IDS,
|
|
7782
|
+
VECTOR_WIDTH_PROFILE_MAX_POINTS,
|
|
7783
|
+
VECTOR_WIDTH_PROFILE_MAX_SCALE,
|
|
7784
|
+
VECTOR_WIDTH_SAME_T,
|
|
5964
7785
|
VectorEditorController,
|
|
5965
7786
|
VectorSnapTargets,
|
|
5966
7787
|
applyMatrix,
|
|
5967
7788
|
applyMatrixLinear,
|
|
7789
|
+
applyVectorCornersAttribute,
|
|
7790
|
+
arrowheadInset,
|
|
7791
|
+
arrowheadPath,
|
|
7792
|
+
arrowheadWeight,
|
|
5968
7793
|
bendSegment,
|
|
5969
7794
|
bezierToSubpaths,
|
|
5970
7795
|
boundsFromPoints,
|
|
@@ -5976,67 +7801,97 @@ export {
|
|
|
5976
7801
|
collectPathNodes,
|
|
5977
7802
|
constrainToLeave,
|
|
5978
7803
|
convertAnchor,
|
|
7804
|
+
cornerBisectorPoint,
|
|
7805
|
+
cornerRadiusChange,
|
|
7806
|
+
cornerSetback,
|
|
5979
7807
|
createVectorCommands,
|
|
5980
7808
|
curvatureMoveAnchor,
|
|
5981
7809
|
cutSubpathAt,
|
|
7810
|
+
dashIntervals,
|
|
5982
7811
|
decomposeRigid,
|
|
7812
|
+
effectiveStrokeAlign,
|
|
5983
7813
|
ellipseSubpath,
|
|
5984
7814
|
eraseFromFill,
|
|
5985
7815
|
eraseFromOutline,
|
|
5986
7816
|
eraserRegion,
|
|
7817
|
+
expandVectorCorners,
|
|
7818
|
+
expandVectorCornersOf,
|
|
5987
7819
|
findVectorNode,
|
|
7820
|
+
fitCornerRadius,
|
|
7821
|
+
generateWidthProfile,
|
|
5988
7822
|
getAnchor,
|
|
5989
7823
|
getAnchorWorld,
|
|
5990
7824
|
getEditablePathIds,
|
|
5991
7825
|
getTransformHandlePositions,
|
|
7826
|
+
getVectorCornerWidgets,
|
|
5992
7827
|
getVectorOverlayRenderData,
|
|
5993
7828
|
getVectorSceneRenderData,
|
|
5994
7829
|
getVectorSelectionBoxGeometry,
|
|
5995
7830
|
getVectorSelectionFrame,
|
|
5996
7831
|
getVectorSelectionStyle,
|
|
7832
|
+
getVectorWidthHandles,
|
|
5997
7833
|
getVisibleAnchors,
|
|
5998
7834
|
getVisibleHandles,
|
|
5999
7835
|
hasPaint,
|
|
7836
|
+
hasStrokeExtras,
|
|
7837
|
+
hasVectorCorners,
|
|
6000
7838
|
hitTestVectorNode,
|
|
6001
7839
|
hitTestVectorNodes,
|
|
7840
|
+
hitTestVectorStroke,
|
|
6002
7841
|
inferAnchorType,
|
|
6003
7842
|
insertAnchor,
|
|
7843
|
+
insertWidthPoint,
|
|
6004
7844
|
invertMatrix,
|
|
7845
|
+
isClosedBezierShape,
|
|
6005
7846
|
isIdentityMatrix,
|
|
7847
|
+
isUniformWidthProfile,
|
|
6006
7848
|
isVectorTextTarget,
|
|
6007
7849
|
joinSubpaths,
|
|
6008
7850
|
knifePath,
|
|
6009
7851
|
lineSubpath,
|
|
6010
7852
|
locateVectorNode,
|
|
6011
7853
|
matrixRotation,
|
|
7854
|
+
mirrorWidthProfile,
|
|
6012
7855
|
moveAnchorHandle,
|
|
6013
7856
|
multiplyMatrices,
|
|
6014
7857
|
nearestSegment,
|
|
7858
|
+
nearestStrokePosition,
|
|
7859
|
+
nextVectorCornerKind,
|
|
6015
7860
|
nodeMatrix,
|
|
6016
7861
|
parseSvgTransform,
|
|
6017
7862
|
parseVectorSvg,
|
|
6018
7863
|
polygonSubpath,
|
|
6019
7864
|
polylineBounds,
|
|
7865
|
+
pressureWidthProfile,
|
|
6020
7866
|
recognizeInkShape,
|
|
6021
7867
|
rectSubpath,
|
|
6022
7868
|
removeAnchorKeepingShape,
|
|
6023
7869
|
removeAnchors,
|
|
6024
7870
|
removeAnchorsKeepingShape,
|
|
6025
7871
|
removeRedundantPoints,
|
|
7872
|
+
removeWidthPoints,
|
|
6026
7873
|
reverseSubpath,
|
|
7874
|
+
reverseWidthProfile,
|
|
6027
7875
|
rotateMatrix,
|
|
6028
7876
|
roundSubpaths,
|
|
7877
|
+
sanitizeWidthProfile,
|
|
6029
7878
|
scaleFor,
|
|
6030
7879
|
scaleMatrix,
|
|
7880
|
+
setAnchorCorner,
|
|
7881
|
+
setSubpathCorners,
|
|
7882
|
+
setWidthPoint,
|
|
6031
7883
|
simplifyPolyline,
|
|
6032
7884
|
smoothSubpath,
|
|
6033
7885
|
splitIntoFaces,
|
|
6034
7886
|
splitSubpathAt,
|
|
6035
7887
|
starSubpath,
|
|
7888
|
+
strokePositionPoint,
|
|
7889
|
+
strokeWidthProfileOf,
|
|
6036
7890
|
subpathPointAt,
|
|
6037
7891
|
subpathSegment,
|
|
6038
7892
|
subpathSegmentCount,
|
|
6039
7893
|
subpathsToBezier,
|
|
7894
|
+
taperWaveFactor,
|
|
6040
7895
|
toVectorKeyInput,
|
|
6041
7896
|
toVectorModifiers,
|
|
6042
7897
|
toVectorPointerInput,
|
|
@@ -6044,6 +7899,12 @@ export {
|
|
|
6044
7899
|
translateAnchor,
|
|
6045
7900
|
translateMatrix,
|
|
6046
7901
|
unionBounds,
|
|
7902
|
+
variableStrokeOutline,
|
|
7903
|
+
variableStrokeRings,
|
|
7904
|
+
vectorCornerAt,
|
|
7905
|
+
vectorCorners,
|
|
7906
|
+
vectorCornersAttribute,
|
|
7907
|
+
vectorDiamondPath,
|
|
6047
7908
|
vectorDocumentToSvg,
|
|
6048
7909
|
vectorInkShapeName,
|
|
6049
7910
|
vectorNodeBounds,
|
|
@@ -6055,8 +7916,21 @@ export {
|
|
|
6055
7916
|
vectorPathData,
|
|
6056
7917
|
vectorPathWorldBezier,
|
|
6057
7918
|
vectorResizeCursor,
|
|
7919
|
+
vectorStrokeLineArea,
|
|
7920
|
+
vectorStrokeOutline,
|
|
7921
|
+
vectorStrokeOutlinePath,
|
|
7922
|
+
vectorStrokeParts,
|
|
7923
|
+
vectorStrokeReach,
|
|
7924
|
+
vectorStrokeSvgParts,
|
|
7925
|
+
vectorStrokeWidthGeometry,
|
|
6058
7926
|
vectorToolLabel,
|
|
6059
7927
|
vectorWorldMatrix,
|
|
6060
|
-
walkVectorNodes
|
|
7928
|
+
walkVectorNodes,
|
|
7929
|
+
widthProfileAt,
|
|
7930
|
+
widthProfileFromAttribute,
|
|
7931
|
+
widthProfileMaxScale,
|
|
7932
|
+
widthProfilePreset,
|
|
7933
|
+
widthProfilePresetId,
|
|
7934
|
+
widthProfileToAttribute
|
|
6061
7935
|
};
|
|
6062
7936
|
//# sourceMappingURL=index.js.map
|