@pixerate/editor 0.36.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 +10 -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 +1301 -284
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +381 -8
- package/dist/vector/index.d.ts +381 -8
- package/dist/vector/index.js +1100 -83
- 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,13 +10,16 @@ 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,
|
|
@@ -31,12 +34,12 @@ import {
|
|
|
31
34
|
transformBezierPath,
|
|
32
35
|
trimSegments,
|
|
33
36
|
windingNumber
|
|
34
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-HPEKB352.js";
|
|
35
38
|
|
|
36
39
|
// src/vector/types.ts
|
|
37
40
|
var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
|
|
38
41
|
var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
|
|
39
|
-
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
|
|
42
|
+
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser", "width"];
|
|
40
43
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
41
44
|
|
|
42
45
|
// src/vector/corners.ts
|
|
@@ -245,6 +248,535 @@ function setSubpathCorners(sp, radius, kind) {
|
|
|
245
248
|
return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
|
|
246
249
|
}
|
|
247
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
|
+
|
|
248
780
|
// src/vector/stroke.ts
|
|
249
781
|
var KAPPA = 0.5522847498307936;
|
|
250
782
|
var NOTCH = 0.7;
|
|
@@ -256,8 +788,21 @@ function effectiveStrokeAlign(style, closedShape) {
|
|
|
256
788
|
const a = style.strokeAlign ?? "center";
|
|
257
789
|
return closedShape ? a : "center";
|
|
258
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
|
+
}
|
|
259
796
|
function hasStrokeExtras(style) {
|
|
260
|
-
return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow;
|
|
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 } : {} });
|
|
261
806
|
}
|
|
262
807
|
var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
|
|
263
808
|
function arrowheadWeight(style, end) {
|
|
@@ -347,13 +892,13 @@ function endOf(segs, atStart) {
|
|
|
347
892
|
const last = segs[segs.length - 1];
|
|
348
893
|
return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
|
|
349
894
|
}
|
|
350
|
-
function pointAlong(segs, fromStart,
|
|
895
|
+
function pointAlong(segs, fromStart, dist4) {
|
|
351
896
|
const order = fromStart ? segs : [...segs].reverse();
|
|
352
897
|
let acc = 0;
|
|
353
898
|
for (const s of order) {
|
|
354
899
|
const l = segLen(s);
|
|
355
|
-
if (acc + l >=
|
|
356
|
-
const want = fromStart ?
|
|
900
|
+
if (acc + l >= dist4) {
|
|
901
|
+
const want = fromStart ? dist4 - acc : l - (dist4 - acc);
|
|
357
902
|
const pts = trimSegments([s], 0, want);
|
|
358
903
|
const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
|
|
359
904
|
return toVec(p);
|
|
@@ -410,12 +955,7 @@ function vectorStrokeParts(path, style) {
|
|
|
410
955
|
}
|
|
411
956
|
function vectorStrokeOutlinePath(path, style) {
|
|
412
957
|
const parts = vectorStrokeParts(path, style);
|
|
413
|
-
const stroke =
|
|
414
|
-
width: parts.drawWidth,
|
|
415
|
-
join: style.strokeLinejoin ?? "miter",
|
|
416
|
-
cap: style.strokeLinecap ?? "butt",
|
|
417
|
-
...style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {}
|
|
418
|
-
});
|
|
958
|
+
const stroke = vectorStrokeLineArea(parts, style);
|
|
419
959
|
const fill = { path, fillRule: style.fillRule ?? "nonzero" };
|
|
420
960
|
let area = stroke;
|
|
421
961
|
if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
|
|
@@ -431,15 +971,32 @@ function hitTestVectorStroke(path, p, style, tolerance = 0) {
|
|
|
431
971
|
if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
|
|
432
972
|
}
|
|
433
973
|
if (!parts.line.length) return false;
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
437
994
|
if (d <= tolerance) return true;
|
|
438
995
|
const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
|
|
439
996
|
return parts.align === "inside" ? inside : !inside;
|
|
440
997
|
}
|
|
441
998
|
function vectorStrokeReach(style) {
|
|
442
|
-
const width = style.strokeWidth ?? 1;
|
|
999
|
+
const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
|
|
443
1000
|
const align = style.strokeAlign ?? "center";
|
|
444
1001
|
let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
|
|
445
1002
|
if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
|
|
@@ -749,8 +1306,10 @@ function bakeIntoGeometry(node, m) {
|
|
|
749
1306
|
if (isIdentityMatrix(m)) return node;
|
|
750
1307
|
if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
|
|
751
1308
|
const path = node;
|
|
1309
|
+
const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
|
|
752
1310
|
return {
|
|
753
1311
|
...path,
|
|
1312
|
+
...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
|
|
754
1313
|
subpaths: path.subpaths.map((sp) => ({
|
|
755
1314
|
closed: sp.closed,
|
|
756
1315
|
anchors: sp.anchors.map((a) => {
|
|
@@ -980,11 +1539,20 @@ function vectorPaintToSvg(paint, gradientId) {
|
|
|
980
1539
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
981
1540
|
return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
|
|
982
1541
|
}
|
|
1542
|
+
var partsCache = /* @__PURE__ */ new WeakMap();
|
|
983
1543
|
function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
984
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) {
|
|
985
1552
|
const path = subpathsToBezier(node.subpaths);
|
|
986
1553
|
const parts = vectorStrokeParts(path, node);
|
|
987
|
-
|
|
1554
|
+
const profiled = !!strokeWidthProfileOf(node);
|
|
1555
|
+
if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
|
|
988
1556
|
let clip = null;
|
|
989
1557
|
if (parts.align !== "center") {
|
|
990
1558
|
const b = bezierPathBounds(path);
|
|
@@ -992,7 +1560,26 @@ function vectorStrokeSvgParts(node, clipId, precision = 3) {
|
|
|
992
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 };
|
|
993
1561
|
clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
|
|
994
1562
|
}
|
|
995
|
-
return {
|
|
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
|
+
);
|
|
996
1583
|
}
|
|
997
1584
|
var round2 = (n) => Math.round(n * 100) / 100;
|
|
998
1585
|
function gradientMarkup(def) {
|
|
@@ -1051,6 +1638,13 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1051
1638
|
if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
|
|
1052
1639
|
if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
|
|
1053
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
|
+
}
|
|
1054
1648
|
lines.push(`${indent(depth)}<g${common(node)}${data}>`);
|
|
1055
1649
|
lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
|
|
1056
1650
|
if (options.strokes === "outline") {
|
|
@@ -1071,7 +1665,8 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
1071
1665
|
clip = ` mask="url(#${id})"`;
|
|
1072
1666
|
}
|
|
1073
1667
|
}
|
|
1074
|
-
lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.
|
|
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}/>`);
|
|
1075
1670
|
for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
|
|
1076
1671
|
}
|
|
1077
1672
|
lines.push(`${indent(depth)}</g>`);
|
|
@@ -1334,6 +1929,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1334
1929
|
return n > 0 ? n : void 0;
|
|
1335
1930
|
};
|
|
1336
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;
|
|
1337
1937
|
const node = {
|
|
1338
1938
|
id: idFor(g),
|
|
1339
1939
|
type: "path",
|
|
@@ -1349,7 +1949,11 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
1349
1949
|
...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
|
|
1350
1950
|
...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
|
|
1351
1951
|
...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
|
|
1352
|
-
...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {}
|
|
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 } : {}
|
|
1353
1957
|
};
|
|
1354
1958
|
return placed(node, m);
|
|
1355
1959
|
};
|
|
@@ -1669,7 +2273,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1669
2273
|
*/
|
|
1670
2274
|
snapFrom(p, leave, tol) {
|
|
1671
2275
|
const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
|
|
1672
|
-
const
|
|
2276
|
+
const at2 = (q, kind, lines2 = []) => ({
|
|
1673
2277
|
point: q,
|
|
1674
2278
|
kind,
|
|
1675
2279
|
x: true,
|
|
@@ -1683,16 +2287,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1683
2287
|
return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
|
|
1684
2288
|
}
|
|
1685
2289
|
const point = this.nearestPoint(p, tol);
|
|
1686
|
-
if (point) return
|
|
2290
|
+
if (point) return at2(point.p, point.kind);
|
|
1687
2291
|
const near2 = this.segmentsNear(p, tol);
|
|
1688
|
-
const
|
|
1689
|
-
if (
|
|
2292
|
+
const crossing2 = this.crossing(near2, p, tol);
|
|
2293
|
+
if (crossing2) return at2(crossing2, "intersect");
|
|
1690
2294
|
if (leave && leave.curves !== false) {
|
|
1691
2295
|
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1692
|
-
if (foot) return
|
|
2296
|
+
if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1693
2297
|
}
|
|
1694
2298
|
const on = this.onSegment(near2);
|
|
1695
|
-
if (on) return
|
|
2299
|
+
if (on) return at2(on, "path");
|
|
1696
2300
|
if (leave?.angles?.length) {
|
|
1697
2301
|
const c = construction(leave, p, tol);
|
|
1698
2302
|
if (c) {
|
|
@@ -1739,7 +2343,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1739
2343
|
* the nearest target line crosses it, at most twice `tol` away: where it lands and the
|
|
1740
2344
|
* target point that line comes from.
|
|
1741
2345
|
*/
|
|
1742
|
-
slide(
|
|
2346
|
+
slide(at2, dir, q, tol, ray) {
|
|
1743
2347
|
const len2 = dir.x * dir.x + dir.y * dir.y;
|
|
1744
2348
|
if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
|
|
1745
2349
|
const reach = 2 * tol;
|
|
@@ -1747,10 +2351,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1747
2351
|
for (const vertical of [true, false]) {
|
|
1748
2352
|
const d = vertical ? dir.x : dir.y;
|
|
1749
2353
|
if (Math.abs(d) <= 1e-9) continue;
|
|
1750
|
-
const a = vertical ?
|
|
2354
|
+
const a = vertical ? at2.x : at2.y;
|
|
1751
2355
|
for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
|
|
1752
2356
|
const s = (l.v - a) / d;
|
|
1753
|
-
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 };
|
|
1754
2358
|
const moved = distance(hit, q);
|
|
1755
2359
|
if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
|
|
1756
2360
|
}
|
|
@@ -1898,11 +2502,11 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1898
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;
|
|
1899
2503
|
if (!pick) return null;
|
|
1900
2504
|
const { start, repeats } = pick;
|
|
1901
|
-
const
|
|
2505
|
+
const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
1902
2506
|
const end = start + len2;
|
|
1903
2507
|
const gaps = [];
|
|
1904
|
-
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b,
|
|
1905
|
-
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)]);
|
|
1906
2510
|
if (repeats !== null) {
|
|
1907
2511
|
for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
|
|
1908
2512
|
}
|
|
@@ -2331,9 +2935,9 @@ function curvatureMoveAnchor(sp, index, q) {
|
|
|
2331
2935
|
anchors[index] = moved;
|
|
2332
2936
|
return { anchors, closed: sp.closed };
|
|
2333
2937
|
}
|
|
2334
|
-
function cutSubpathAt(sp,
|
|
2938
|
+
function cutSubpathAt(sp, at2) {
|
|
2335
2939
|
const n = sp.anchors.length;
|
|
2336
|
-
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);
|
|
2337
2941
|
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
2338
2942
|
const first = cuts[0];
|
|
2339
2943
|
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
@@ -2364,7 +2968,7 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
|
2364
2968
|
return cutSubpathAt(subpath, [index]);
|
|
2365
2969
|
}
|
|
2366
2970
|
var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
2367
|
-
var
|
|
2971
|
+
var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
2368
2972
|
function reverse(sp) {
|
|
2369
2973
|
return {
|
|
2370
2974
|
closed: sp.closed,
|
|
@@ -2391,7 +2995,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
2391
2995
|
const sp = open[0];
|
|
2392
2996
|
const first = sp.anchors[0];
|
|
2393
2997
|
const last = sp.anchors[sp.anchors.length - 1];
|
|
2394
|
-
if (sp.anchors.length > 2 &&
|
|
2998
|
+
if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
|
|
2395
2999
|
sp.anchors.pop();
|
|
2396
3000
|
sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
|
|
2397
3001
|
} else {
|
|
@@ -2407,7 +3011,7 @@ function joinSubpaths(subpaths, tolerance) {
|
|
|
2407
3011
|
for (let j = i + 1; j < open.length; j++) {
|
|
2408
3012
|
for (const ei of [false, true]) {
|
|
2409
3013
|
for (const ej of [false, true]) {
|
|
2410
|
-
const d =
|
|
3014
|
+
const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
|
|
2411
3015
|
if (d < best.d) best = { d, i, j, ei, ej };
|
|
2412
3016
|
}
|
|
2413
3017
|
}
|
|
@@ -2447,8 +3051,8 @@ function smoothSubpath(sp, amount = 1, only) {
|
|
|
2447
3051
|
const p = orig[i];
|
|
2448
3052
|
const dir = unit2(next.x - prev.x, next.y - prev.y);
|
|
2449
3053
|
if (!dir) continue;
|
|
2450
|
-
const li =
|
|
2451
|
-
const lo =
|
|
3054
|
+
const li = dist2(p, prev) / 3;
|
|
3055
|
+
const lo = dist2(p, next) / 3;
|
|
2452
3056
|
const hin = p.in ?? p;
|
|
2453
3057
|
const hout = p.out ?? p;
|
|
2454
3058
|
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
@@ -2475,12 +3079,12 @@ function segsOf(sp) {
|
|
|
2475
3079
|
function segsToSubpath(segs, closed, types) {
|
|
2476
3080
|
if (!segs.length) return null;
|
|
2477
3081
|
const anchors = [];
|
|
2478
|
-
const
|
|
2479
|
-
anchors.push(
|
|
3082
|
+
const at2 = (p) => ({ x: p[0], y: p[1] });
|
|
3083
|
+
anchors.push(at2(segs[0].c[0]));
|
|
2480
3084
|
for (const s of segs) {
|
|
2481
3085
|
const end = s.c[s.c.length - 1];
|
|
2482
3086
|
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
2483
|
-
const next =
|
|
3087
|
+
const next = at2(end);
|
|
2484
3088
|
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
2485
3089
|
anchors.push(next);
|
|
2486
3090
|
}
|
|
@@ -2506,8 +3110,8 @@ function mergePair(a, b, tol) {
|
|
|
2506
3110
|
if (len2 < 1e-12) return null;
|
|
2507
3111
|
const mx = a3[0] - a0[0];
|
|
2508
3112
|
const my = a3[1] - a0[1];
|
|
2509
|
-
const
|
|
2510
|
-
return Math.abs((mx * cy - my * cx) / len2) <= tol &&
|
|
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;
|
|
2511
3115
|
}
|
|
2512
3116
|
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
2513
3117
|
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
@@ -2939,7 +3543,7 @@ function vectorInkShapeName(shape) {
|
|
|
2939
3543
|
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2940
3544
|
}
|
|
2941
3545
|
}
|
|
2942
|
-
var
|
|
3546
|
+
var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2943
3547
|
function bbox(pts) {
|
|
2944
3548
|
if (!pts.length) return [0, 0, 0, 0];
|
|
2945
3549
|
let [x0, y0] = pts[0];
|
|
@@ -2957,7 +3561,7 @@ function segDistance(a, b, p) {
|
|
|
2957
3561
|
const dy = b[1] - a[1];
|
|
2958
3562
|
const len2 = dx * dx + dy * dy;
|
|
2959
3563
|
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2960
|
-
return
|
|
3564
|
+
return dist3([a[0] + t * dx, a[1] + t * dy], p);
|
|
2961
3565
|
}
|
|
2962
3566
|
function polygonDistance(c, p) {
|
|
2963
3567
|
let best = Infinity;
|
|
@@ -3019,7 +3623,7 @@ function corners(p, diag) {
|
|
|
3019
3623
|
let far = 0;
|
|
3020
3624
|
let farD = 0;
|
|
3021
3625
|
p.forEach((q, i) => {
|
|
3022
|
-
const d =
|
|
3626
|
+
const d = dist3(start, q);
|
|
3023
3627
|
if (d > farD) {
|
|
3024
3628
|
farD = d;
|
|
3025
3629
|
far = i;
|
|
@@ -3046,7 +3650,7 @@ function corners(p, diag) {
|
|
|
3046
3650
|
}
|
|
3047
3651
|
let short = -1;
|
|
3048
3652
|
for (let j = 0; j < n; j++) {
|
|
3049
|
-
if (
|
|
3653
|
+
if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
3050
3654
|
short = j;
|
|
3051
3655
|
break;
|
|
3052
3656
|
}
|
|
@@ -3086,8 +3690,8 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3086
3690
|
const diag = Math.hypot(w, h);
|
|
3087
3691
|
if (size < minSize) return null;
|
|
3088
3692
|
let length2 = 0;
|
|
3089
|
-
for (let i = 1; i < p.length; i++) length2 +=
|
|
3090
|
-
const gap =
|
|
3693
|
+
for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
|
|
3694
|
+
const gap = dist3(first, last);
|
|
3091
3695
|
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
3092
3696
|
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
3093
3697
|
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
@@ -3113,7 +3717,14 @@ function recognizeInkShape(points, options = {}) {
|
|
|
3113
3717
|
|
|
3114
3718
|
// src/vector/controller.ts
|
|
3115
3719
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
3116
|
-
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
|
+
};
|
|
3117
3728
|
var VECTOR_PENCIL_STRING_PX = 100;
|
|
3118
3729
|
var VECTOR_STYLE_KEYS = [
|
|
3119
3730
|
"fill",
|
|
@@ -3128,7 +3739,8 @@ var VECTOR_STYLE_KEYS = [
|
|
|
3128
3739
|
"endArrow",
|
|
3129
3740
|
"arrowAlign",
|
|
3130
3741
|
"startArrowScale",
|
|
3131
|
-
"endArrowScale"
|
|
3742
|
+
"endArrowScale",
|
|
3743
|
+
"strokeWidthProfile"
|
|
3132
3744
|
];
|
|
3133
3745
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
3134
3746
|
var DEFAULT_DEFAULTS = {
|
|
@@ -3190,9 +3802,9 @@ function getVisibleHandles(state) {
|
|
|
3190
3802
|
return out;
|
|
3191
3803
|
}
|
|
3192
3804
|
if (state.tool !== "direct") return out;
|
|
3193
|
-
const
|
|
3805
|
+
const editable2 = new Set(getEditablePathIds(state));
|
|
3194
3806
|
for (const ref of state.selectedAnchors) {
|
|
3195
|
-
if (!
|
|
3807
|
+
if (!editable2.has(ref.nodeId)) continue;
|
|
3196
3808
|
const node = findVectorNode(state.document.nodes, ref.nodeId);
|
|
3197
3809
|
const sp = node.subpaths[ref.subpath];
|
|
3198
3810
|
if (!sp) continue;
|
|
@@ -3229,6 +3841,56 @@ function getVectorCornerWidgets(state) {
|
|
|
3229
3841
|
}
|
|
3230
3842
|
return out;
|
|
3231
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
|
+
}
|
|
3232
3894
|
function getAnchor(doc, ref) {
|
|
3233
3895
|
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3234
3896
|
return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
|
|
@@ -3299,9 +3961,10 @@ var TOOL_LABELS = {
|
|
|
3299
3961
|
line: "Line",
|
|
3300
3962
|
scissors: "Scissors",
|
|
3301
3963
|
knife: "Knife",
|
|
3302
|
-
eraser: "Eraser"
|
|
3964
|
+
eraser: "Eraser",
|
|
3965
|
+
width: "Width"
|
|
3303
3966
|
};
|
|
3304
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
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" };
|
|
3305
3968
|
function vectorToolLabel(tool) {
|
|
3306
3969
|
return TOOL_LABELS[tool];
|
|
3307
3970
|
}
|
|
@@ -3317,7 +3980,8 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
3317
3980
|
line: "L",
|
|
3318
3981
|
scissors: "C",
|
|
3319
3982
|
knife: "K",
|
|
3320
|
-
eraser: "E"
|
|
3983
|
+
eraser: "E",
|
|
3984
|
+
width: "W"
|
|
3321
3985
|
};
|
|
3322
3986
|
var VECTOR_TOOL_GLYPHS = {
|
|
3323
3987
|
select: "\u2196",
|
|
@@ -3331,7 +3995,8 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
3331
3995
|
line: "\u2571",
|
|
3332
3996
|
scissors: "\u2702",
|
|
3333
3997
|
knife: "\u27CB",
|
|
3334
|
-
eraser: "\u232B"
|
|
3998
|
+
eraser: "\u232B",
|
|
3999
|
+
width: "\u21F9"
|
|
3335
4000
|
};
|
|
3336
4001
|
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
3337
4002
|
var BOOLEAN_OPS = {
|
|
@@ -3385,6 +4050,7 @@ var VectorEditorController = class {
|
|
|
3385
4050
|
doc;
|
|
3386
4051
|
selection = [];
|
|
3387
4052
|
anchors = [];
|
|
4053
|
+
widthPoint = null;
|
|
3388
4054
|
editingId = null;
|
|
3389
4055
|
pen = null;
|
|
3390
4056
|
tool;
|
|
@@ -3463,6 +4129,7 @@ var VectorEditorController = class {
|
|
|
3463
4129
|
tool: this.tool,
|
|
3464
4130
|
selection: this.selection,
|
|
3465
4131
|
selectedAnchors: this.anchors,
|
|
4132
|
+
selectedWidthPoint: this.widthPoint,
|
|
3466
4133
|
editingId: this.editingId,
|
|
3467
4134
|
selectionBounds: this.boundsOf(this.selection),
|
|
3468
4135
|
selectionFrame: this.frameOf(this.selectedRoots()),
|
|
@@ -3531,6 +4198,10 @@ var VectorEditorController = class {
|
|
|
3531
4198
|
return h?.nodeId ? "move" : "default";
|
|
3532
4199
|
case "pen":
|
|
3533
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";
|
|
3534
4205
|
default:
|
|
3535
4206
|
return "crosshair";
|
|
3536
4207
|
}
|
|
@@ -3656,6 +4327,7 @@ var VectorEditorController = class {
|
|
|
3656
4327
|
this.tool = tool;
|
|
3657
4328
|
this.hover = null;
|
|
3658
4329
|
this.guides = null;
|
|
4330
|
+
this.widthPoint = null;
|
|
3659
4331
|
if (tool !== "direct") {
|
|
3660
4332
|
this.anchors = [];
|
|
3661
4333
|
this.editingId = null;
|
|
@@ -3887,6 +4559,7 @@ var VectorEditorController = class {
|
|
|
3887
4559
|
if (d.fillRule) out.fillRule = d.fillRule;
|
|
3888
4560
|
if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
|
|
3889
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 }));
|
|
3890
4563
|
return out;
|
|
3891
4564
|
}
|
|
3892
4565
|
/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
|
|
@@ -3907,10 +4580,39 @@ var VectorEditorController = class {
|
|
|
3907
4580
|
}
|
|
3908
4581
|
if (!targets.size) return { ok: false, message: "Nothing is selected." };
|
|
3909
4582
|
this.change(options.label ?? "Change Style", () => {
|
|
3910
|
-
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));
|
|
3911
4584
|
});
|
|
3912
4585
|
return { ok: true };
|
|
3913
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
|
+
}
|
|
3914
4616
|
/**
|
|
3915
4617
|
* Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
|
|
3916
4618
|
* anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
|
|
@@ -3981,6 +4683,7 @@ var VectorEditorController = class {
|
|
|
3981
4683
|
arrowAlign: _al,
|
|
3982
4684
|
startArrowScale: _ss,
|
|
3983
4685
|
endArrowScale: _es,
|
|
4686
|
+
strokeWidthProfile: _wp,
|
|
3984
4687
|
...rest
|
|
3985
4688
|
} = cloneVectorNode(p);
|
|
3986
4689
|
const id = this.newId();
|
|
@@ -4282,7 +4985,7 @@ var VectorEditorController = class {
|
|
|
4282
4985
|
if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
|
|
4283
4986
|
const set = new Set(groups.map((g) => g.id));
|
|
4284
4987
|
this.change("Ungroup", () => {
|
|
4285
|
-
const
|
|
4988
|
+
const flatten2 = (list) => {
|
|
4286
4989
|
const out = [];
|
|
4287
4990
|
let changed = false;
|
|
4288
4991
|
for (const n of list) {
|
|
@@ -4294,14 +4997,14 @@ var VectorEditorController = class {
|
|
|
4294
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)
|
|
4295
4998
|
);
|
|
4296
4999
|
} else if (n.type === "group") {
|
|
4297
|
-
const children =
|
|
5000
|
+
const children = flatten2(n.children);
|
|
4298
5001
|
if (children !== n.children) changed = true;
|
|
4299
5002
|
out.push(children !== n.children ? { ...n, children } : n);
|
|
4300
5003
|
} else out.push(n);
|
|
4301
5004
|
}
|
|
4302
5005
|
return changed ? out : list;
|
|
4303
5006
|
};
|
|
4304
|
-
this.setDoc(
|
|
5007
|
+
this.setDoc(flatten2(this.doc.nodes));
|
|
4305
5008
|
this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
|
|
4306
5009
|
});
|
|
4307
5010
|
return { ok: true };
|
|
@@ -4571,7 +5274,12 @@ var VectorEditorController = class {
|
|
|
4571
5274
|
const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
|
|
4572
5275
|
if (!set.size) return { ok: false, message: "Select a path." };
|
|
4573
5276
|
this.change("Reverse Path", () => {
|
|
4574
|
-
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
|
+
);
|
|
4575
5283
|
this.anchors = [];
|
|
4576
5284
|
});
|
|
4577
5285
|
return { ok: true };
|
|
@@ -4652,9 +5360,9 @@ var VectorEditorController = class {
|
|
|
4652
5360
|
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
4653
5361
|
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
4654
5362
|
this.change("Average", () => {
|
|
4655
|
-
for (const { ref, at } of points) {
|
|
4656
|
-
const dx = axis === "horizontal" ? 0 : cx -
|
|
4657
|
-
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;
|
|
4658
5366
|
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
4659
5367
|
}
|
|
4660
5368
|
});
|
|
@@ -4738,8 +5446,8 @@ var VectorEditorController = class {
|
|
|
4738
5446
|
const node = this.getNode(id);
|
|
4739
5447
|
if (node.locked) continue;
|
|
4740
5448
|
const ids = this.applyCuts(node, (sp, i) => {
|
|
4741
|
-
const
|
|
4742
|
-
const pieces =
|
|
5449
|
+
const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
5450
|
+
const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
|
|
4743
5451
|
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4744
5452
|
});
|
|
4745
5453
|
if (ids) created.push(...ids);
|
|
@@ -5053,10 +5761,131 @@ var VectorEditorController = class {
|
|
|
5053
5761
|
const seg = this.nearestOutline(p);
|
|
5054
5762
|
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
5055
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
|
+
}
|
|
5056
5770
|
default:
|
|
5057
5771
|
return null;
|
|
5058
5772
|
}
|
|
5059
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
|
+
}
|
|
5060
5889
|
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
5061
5890
|
nearestOutline(p) {
|
|
5062
5891
|
let best = null;
|
|
@@ -5104,7 +5933,9 @@ var VectorEditorController = class {
|
|
|
5104
5933
|
case "pen":
|
|
5105
5934
|
return this.downPen(screen, p, input);
|
|
5106
5935
|
case "pencil":
|
|
5107
|
-
return this.downPencil(p);
|
|
5936
|
+
return this.downPencil(p, input);
|
|
5937
|
+
case "width":
|
|
5938
|
+
return this.downWidth(screen, p, input);
|
|
5108
5939
|
case "scissors": {
|
|
5109
5940
|
const seg = this.nearestOutline(p);
|
|
5110
5941
|
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
@@ -5297,7 +6128,7 @@ var VectorEditorController = class {
|
|
|
5297
6128
|
this.interaction = { type: "pen", nodeId: node.id };
|
|
5298
6129
|
this.notify(true);
|
|
5299
6130
|
}
|
|
5300
|
-
downPencil(p) {
|
|
6131
|
+
downPencil(p, input = {}) {
|
|
5301
6132
|
const before = this.snapshot();
|
|
5302
6133
|
const node = {
|
|
5303
6134
|
id: this.newId(),
|
|
@@ -5311,7 +6142,11 @@ var VectorEditorController = class {
|
|
|
5311
6142
|
this.setDoc([...this.doc.nodes, node]);
|
|
5312
6143
|
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
5313
6144
|
lazy.push([p.x, p.y]);
|
|
5314
|
-
|
|
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 };
|
|
5315
6150
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
5316
6151
|
this.notify(true);
|
|
5317
6152
|
}
|
|
@@ -5520,12 +6355,36 @@ var VectorEditorController = class {
|
|
|
5520
6355
|
this.notify(true);
|
|
5521
6356
|
return;
|
|
5522
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
|
+
}
|
|
5523
6380
|
case "pencil": {
|
|
5524
6381
|
const last = drag.points[drag.points.length - 1];
|
|
5525
6382
|
let moved = false;
|
|
6383
|
+
if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
|
|
5526
6384
|
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
5527
6385
|
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
5528
6386
|
drag.points.push(q);
|
|
6387
|
+
drag.pressures?.push(drag.pressure);
|
|
5529
6388
|
moved = true;
|
|
5530
6389
|
}
|
|
5531
6390
|
const string = drag.lazy.string();
|
|
@@ -5606,7 +6465,7 @@ var VectorEditorController = class {
|
|
|
5606
6465
|
}
|
|
5607
6466
|
/** Primary button released. */
|
|
5608
6467
|
pointerUp(screen, input = {}) {
|
|
5609
|
-
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 });
|
|
5610
6469
|
const drag = this.drag;
|
|
5611
6470
|
this.drag = null;
|
|
5612
6471
|
this.interaction = null;
|
|
@@ -5658,6 +6517,15 @@ var VectorEditorController = class {
|
|
|
5658
6517
|
this.notify();
|
|
5659
6518
|
return;
|
|
5660
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
|
+
}
|
|
5661
6529
|
case "stroke": {
|
|
5662
6530
|
this.notify();
|
|
5663
6531
|
if (drag.tool === "knife") this.knife(drag.points);
|
|
@@ -5688,7 +6556,10 @@ var VectorEditorController = class {
|
|
|
5688
6556
|
this.notify(true);
|
|
5689
6557
|
return;
|
|
5690
6558
|
}
|
|
5691
|
-
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
|
+
}
|
|
5692
6563
|
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
5693
6564
|
if (shape) {
|
|
5694
6565
|
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
@@ -5703,7 +6574,8 @@ var VectorEditorController = class {
|
|
|
5703
6574
|
anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
|
|
5704
6575
|
}
|
|
5705
6576
|
const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
|
|
5706
|
-
|
|
6577
|
+
const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
|
|
6578
|
+
this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
|
|
5707
6579
|
this.selection = [drag.nodeId];
|
|
5708
6580
|
return this.commit("Draw", drag.before);
|
|
5709
6581
|
}
|
|
@@ -5825,7 +6697,10 @@ var VectorEditorController = class {
|
|
|
5825
6697
|
switch (key) {
|
|
5826
6698
|
case "Escape":
|
|
5827
6699
|
if (this.drag) this.cancelDrag();
|
|
5828
|
-
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();
|
|
5829
6704
|
else if (this.tool === "direct" && this.anchors.length) {
|
|
5830
6705
|
this.anchors = [];
|
|
5831
6706
|
this.notify();
|
|
@@ -5846,6 +6721,7 @@ var VectorEditorController = class {
|
|
|
5846
6721
|
return false;
|
|
5847
6722
|
case "Delete":
|
|
5848
6723
|
case "Backspace":
|
|
6724
|
+
if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
|
|
5849
6725
|
if (this.pen) {
|
|
5850
6726
|
const node = this.getNode(this.pen.nodeId);
|
|
5851
6727
|
const sp = node.subpaths[this.pen.subpath];
|
|
@@ -5927,6 +6803,31 @@ function scaleFor(handle, b, p, mods = {}) {
|
|
|
5927
6803
|
}
|
|
5928
6804
|
return { sx, sy, origin };
|
|
5929
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
|
+
}
|
|
5930
6831
|
|
|
5931
6832
|
// src/vector/render.ts
|
|
5932
6833
|
var fmt = (n) => Math.round(n * 100) / 100;
|
|
@@ -6091,7 +6992,21 @@ function getVectorOverlayRenderData(state) {
|
|
|
6091
6992
|
});
|
|
6092
6993
|
if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
|
|
6093
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;
|
|
6094
|
-
|
|
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 };
|
|
6095
7010
|
}
|
|
6096
7011
|
var VECTOR_OVERLAY_THEME = {
|
|
6097
7012
|
accent: "#0ea5e9",
|
|
@@ -6105,13 +7020,23 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
6105
7020
|
anchorSize: 7,
|
|
6106
7021
|
handleRadius: 3.5,
|
|
6107
7022
|
/** Radius of a live corner widget (a ring with a dot). */
|
|
6108
|
-
cornerWidgetRadius: 4
|
|
7023
|
+
cornerWidgetRadius: 4,
|
|
7024
|
+
/** Half the diagonal of a width point's diamond. */
|
|
7025
|
+
widthPointSize: 5
|
|
6109
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
|
+
}
|
|
6110
7030
|
function toVectorModifiers(event) {
|
|
6111
7031
|
return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
|
|
6112
7032
|
}
|
|
6113
7033
|
function toVectorPointerInput(event) {
|
|
6114
|
-
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
|
+
};
|
|
6115
7040
|
}
|
|
6116
7041
|
function toVectorKeyInput(event) {
|
|
6117
7042
|
return { key: event.key, ...toVectorModifiers(event) };
|
|
@@ -6160,6 +7085,8 @@ function getVectorSelectionStyle(state) {
|
|
|
6160
7085
|
arrowAlign: first.arrowAlign ?? "extend",
|
|
6161
7086
|
startArrowScale: first.startArrowScale ?? 1,
|
|
6162
7087
|
endArrowScale: first.endArrowScale ?? 1,
|
|
7088
|
+
strokeWidthProfile: profileOf(first),
|
|
7089
|
+
widthProfile: widthProfilePresetId(profileOf(first)),
|
|
6163
7090
|
cornerRadius: c0.radius,
|
|
6164
7091
|
cornerKind: c0.kind,
|
|
6165
7092
|
corners: corners2.length,
|
|
@@ -6175,12 +7102,17 @@ function getVectorSelectionStyle(state) {
|
|
|
6175
7102
|
arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
|
|
6176
7103
|
startArrowScale: !same((p) => p.startArrowScale ?? 1),
|
|
6177
7104
|
endArrowScale: !same((p) => p.endArrowScale ?? 1),
|
|
7105
|
+
widthProfile: !same(profileOf),
|
|
6178
7106
|
cornerRadius: corners2.some((c) => c.radius !== c0.radius),
|
|
6179
7107
|
cornerKind: corners2.some((c) => c.kind !== c0.kind)
|
|
6180
7108
|
},
|
|
6181
7109
|
pathIds: paths.map((p) => p.id)
|
|
6182
7110
|
};
|
|
6183
7111
|
}
|
|
7112
|
+
function profileOf(p) {
|
|
7113
|
+
const points = sanitizeWidthProfile(p.strokeWidthProfile);
|
|
7114
|
+
return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
|
|
7115
|
+
}
|
|
6184
7116
|
var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
|
|
6185
7117
|
var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
|
|
6186
7118
|
var VECTOR_ARROWHEAD_LABELS = {
|
|
@@ -6232,6 +7164,19 @@ var POINTS = {
|
|
|
6232
7164
|
minItems: 1,
|
|
6233
7165
|
description: "Polyline [[x, y], \u2026] in document units."
|
|
6234
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
|
+
};
|
|
6235
7180
|
var PAINT = {
|
|
6236
7181
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
6237
7182
|
};
|
|
@@ -6248,7 +7193,8 @@ var STYLE_PROPS = {
|
|
|
6248
7193
|
endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
|
|
6249
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." },
|
|
6250
7195
|
startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
|
|
6251
|
-
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." }
|
|
7196
|
+
endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
|
|
7197
|
+
strokeWidthProfile: WIDTH_POINTS
|
|
6252
7198
|
};
|
|
6253
7199
|
var pickStyle = (params) => {
|
|
6254
7200
|
const out = {};
|
|
@@ -6405,6 +7351,44 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6405
7351
|
return { ok: true };
|
|
6406
7352
|
}
|
|
6407
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
|
+
},
|
|
6408
7392
|
{
|
|
6409
7393
|
...base,
|
|
6410
7394
|
id: `${prefix}outlineStroke`,
|
|
@@ -6711,7 +7695,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6711
7695
|
id: `${prefix}setEditing`,
|
|
6712
7696
|
label: "Path Editing Options\u2026",
|
|
6713
7697
|
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
6714
|
-
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `
|
|
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`).",
|
|
6715
7699
|
params: {
|
|
6716
7700
|
type: "object",
|
|
6717
7701
|
properties: {
|
|
@@ -6719,12 +7703,13 @@ function createVectorCommands(controller, options = {}) {
|
|
|
6719
7703
|
curvatureMoves: { type: "boolean" },
|
|
6720
7704
|
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
6721
7705
|
recognizeShapes: { type: "boolean" },
|
|
6722
|
-
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
7706
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 },
|
|
7707
|
+
pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
|
|
6723
7708
|
}
|
|
6724
7709
|
},
|
|
6725
7710
|
run: (_ctx, params) => {
|
|
6726
7711
|
const patch = {};
|
|
6727
|
-
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];
|
|
6728
7713
|
controller.setEditing(patch);
|
|
6729
7714
|
return { editing: controller.getState().editing };
|
|
6730
7715
|
}
|
|
@@ -6784,6 +7769,7 @@ export {
|
|
|
6784
7769
|
VECTOR_OVERLAY_THEME,
|
|
6785
7770
|
VECTOR_PATH_ACTIONS,
|
|
6786
7771
|
VECTOR_PENCIL_STRING_PX,
|
|
7772
|
+
VECTOR_PRESSURE_NOMINAL,
|
|
6787
7773
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
6788
7774
|
VECTOR_STROKE_ALIGN_LABELS,
|
|
6789
7775
|
VECTOR_STYLE_KEYS,
|
|
@@ -6791,6 +7777,11 @@ export {
|
|
|
6791
7777
|
VECTOR_TOOL_GLYPHS,
|
|
6792
7778
|
VECTOR_TOOL_SHORTCUTS,
|
|
6793
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,
|
|
6794
7785
|
VectorEditorController,
|
|
6795
7786
|
VectorSnapTargets,
|
|
6796
7787
|
applyMatrix,
|
|
@@ -6816,6 +7807,7 @@ export {
|
|
|
6816
7807
|
createVectorCommands,
|
|
6817
7808
|
curvatureMoveAnchor,
|
|
6818
7809
|
cutSubpathAt,
|
|
7810
|
+
dashIntervals,
|
|
6819
7811
|
decomposeRigid,
|
|
6820
7812
|
effectiveStrokeAlign,
|
|
6821
7813
|
ellipseSubpath,
|
|
@@ -6826,6 +7818,7 @@ export {
|
|
|
6826
7818
|
expandVectorCornersOf,
|
|
6827
7819
|
findVectorNode,
|
|
6828
7820
|
fitCornerRadius,
|
|
7821
|
+
generateWidthProfile,
|
|
6829
7822
|
getAnchor,
|
|
6830
7823
|
getAnchorWorld,
|
|
6831
7824
|
getEditablePathIds,
|
|
@@ -6836,6 +7829,7 @@ export {
|
|
|
6836
7829
|
getVectorSelectionBoxGeometry,
|
|
6837
7830
|
getVectorSelectionFrame,
|
|
6838
7831
|
getVectorSelectionStyle,
|
|
7832
|
+
getVectorWidthHandles,
|
|
6839
7833
|
getVisibleAnchors,
|
|
6840
7834
|
getVisibleHandles,
|
|
6841
7835
|
hasPaint,
|
|
@@ -6846,46 +7840,58 @@ export {
|
|
|
6846
7840
|
hitTestVectorStroke,
|
|
6847
7841
|
inferAnchorType,
|
|
6848
7842
|
insertAnchor,
|
|
7843
|
+
insertWidthPoint,
|
|
6849
7844
|
invertMatrix,
|
|
6850
7845
|
isClosedBezierShape,
|
|
6851
7846
|
isIdentityMatrix,
|
|
7847
|
+
isUniformWidthProfile,
|
|
6852
7848
|
isVectorTextTarget,
|
|
6853
7849
|
joinSubpaths,
|
|
6854
7850
|
knifePath,
|
|
6855
7851
|
lineSubpath,
|
|
6856
7852
|
locateVectorNode,
|
|
6857
7853
|
matrixRotation,
|
|
7854
|
+
mirrorWidthProfile,
|
|
6858
7855
|
moveAnchorHandle,
|
|
6859
7856
|
multiplyMatrices,
|
|
6860
7857
|
nearestSegment,
|
|
7858
|
+
nearestStrokePosition,
|
|
6861
7859
|
nextVectorCornerKind,
|
|
6862
7860
|
nodeMatrix,
|
|
6863
7861
|
parseSvgTransform,
|
|
6864
7862
|
parseVectorSvg,
|
|
6865
7863
|
polygonSubpath,
|
|
6866
7864
|
polylineBounds,
|
|
7865
|
+
pressureWidthProfile,
|
|
6867
7866
|
recognizeInkShape,
|
|
6868
7867
|
rectSubpath,
|
|
6869
7868
|
removeAnchorKeepingShape,
|
|
6870
7869
|
removeAnchors,
|
|
6871
7870
|
removeAnchorsKeepingShape,
|
|
6872
7871
|
removeRedundantPoints,
|
|
7872
|
+
removeWidthPoints,
|
|
6873
7873
|
reverseSubpath,
|
|
7874
|
+
reverseWidthProfile,
|
|
6874
7875
|
rotateMatrix,
|
|
6875
7876
|
roundSubpaths,
|
|
7877
|
+
sanitizeWidthProfile,
|
|
6876
7878
|
scaleFor,
|
|
6877
7879
|
scaleMatrix,
|
|
6878
7880
|
setAnchorCorner,
|
|
6879
7881
|
setSubpathCorners,
|
|
7882
|
+
setWidthPoint,
|
|
6880
7883
|
simplifyPolyline,
|
|
6881
7884
|
smoothSubpath,
|
|
6882
7885
|
splitIntoFaces,
|
|
6883
7886
|
splitSubpathAt,
|
|
6884
7887
|
starSubpath,
|
|
7888
|
+
strokePositionPoint,
|
|
7889
|
+
strokeWidthProfileOf,
|
|
6885
7890
|
subpathPointAt,
|
|
6886
7891
|
subpathSegment,
|
|
6887
7892
|
subpathSegmentCount,
|
|
6888
7893
|
subpathsToBezier,
|
|
7894
|
+
taperWaveFactor,
|
|
6889
7895
|
toVectorKeyInput,
|
|
6890
7896
|
toVectorModifiers,
|
|
6891
7897
|
toVectorPointerInput,
|
|
@@ -6893,9 +7899,12 @@ export {
|
|
|
6893
7899
|
translateAnchor,
|
|
6894
7900
|
translateMatrix,
|
|
6895
7901
|
unionBounds,
|
|
7902
|
+
variableStrokeOutline,
|
|
7903
|
+
variableStrokeRings,
|
|
6896
7904
|
vectorCornerAt,
|
|
6897
7905
|
vectorCorners,
|
|
6898
7906
|
vectorCornersAttribute,
|
|
7907
|
+
vectorDiamondPath,
|
|
6899
7908
|
vectorDocumentToSvg,
|
|
6900
7909
|
vectorInkShapeName,
|
|
6901
7910
|
vectorNodeBounds,
|
|
@@ -6907,13 +7916,21 @@ export {
|
|
|
6907
7916
|
vectorPathData,
|
|
6908
7917
|
vectorPathWorldBezier,
|
|
6909
7918
|
vectorResizeCursor,
|
|
7919
|
+
vectorStrokeLineArea,
|
|
6910
7920
|
vectorStrokeOutline,
|
|
6911
7921
|
vectorStrokeOutlinePath,
|
|
6912
7922
|
vectorStrokeParts,
|
|
6913
7923
|
vectorStrokeReach,
|
|
6914
7924
|
vectorStrokeSvgParts,
|
|
7925
|
+
vectorStrokeWidthGeometry,
|
|
6915
7926
|
vectorToolLabel,
|
|
6916
7927
|
vectorWorldMatrix,
|
|
6917
|
-
walkVectorNodes
|
|
7928
|
+
walkVectorNodes,
|
|
7929
|
+
widthProfileAt,
|
|
7930
|
+
widthProfileFromAttribute,
|
|
7931
|
+
widthProfileMaxScale,
|
|
7932
|
+
widthProfilePreset,
|
|
7933
|
+
widthProfilePresetId,
|
|
7934
|
+
widthProfileToAttribute
|
|
6918
7935
|
};
|
|
6919
7936
|
//# sourceMappingURL=index.js.map
|