@pixerate/editor 0.33.0 → 0.35.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 +22 -0
- package/dist/{bezier-DJPYiVbA.d.ts → bezier-BvqVmWMg.d.ts} +1 -1
- package/dist/{bezier-6KPx3w7Z.d.cts → bezier-D8bsO-L0.d.cts} +1 -1
- package/dist/canvas/index.cjs +179 -622
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +60 -3
- package/dist/canvas/index.d.ts +60 -3
- package/dist/canvas/index.js +37 -480
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-M4XE4UMT.cjs → chunk-BGNPT5PE.cjs} +464 -5
- package/dist/chunk-BGNPT5PE.cjs.map +1 -0
- package/dist/{chunk-GQTXV2EQ.js → chunk-EGPIVUEF.js} +470 -11
- package/dist/chunk-EGPIVUEF.js.map +1 -0
- package/dist/vector/index.cjs +2776 -185
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +638 -10
- package/dist/vector/index.d.ts +638 -10
- package/dist/vector/index.js +2644 -53
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GQTXV2EQ.js.map +0 -1
- package/dist/chunk-M4XE4UMT.cjs.map +0 -1
package/dist/vector/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;
|
|
2
2
|
|
|
3
3
|
var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
|
|
4
4
|
|
|
@@ -15,10 +15,22 @@ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
var _chunkBGNPT5PEcjs = require('../chunk-BGNPT5PE.cjs');
|
|
19
31
|
|
|
20
32
|
// src/vector/types.ts
|
|
21
|
-
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
|
|
33
|
+
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
|
|
22
34
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
23
35
|
|
|
24
36
|
// src/vector/geometry.ts
|
|
@@ -82,7 +94,7 @@ function vectorNodeToBezier(node) {
|
|
|
82
94
|
}
|
|
83
95
|
function vectorPathData(input, precision = 3) {
|
|
84
96
|
const subpaths = Array.isArray(input) ? input : input.subpaths;
|
|
85
|
-
return
|
|
97
|
+
return _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
|
|
86
98
|
}
|
|
87
99
|
function inferAnchorType(a) {
|
|
88
100
|
if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
|
|
@@ -98,7 +110,7 @@ function inferAnchorType(a) {
|
|
|
98
110
|
return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
|
|
99
111
|
}
|
|
100
112
|
function bezierToSubpaths(input) {
|
|
101
|
-
const path =
|
|
113
|
+
const path = _chunkBGNPT5PEcjs.toBezierPath.call(void 0, input);
|
|
102
114
|
const out = [];
|
|
103
115
|
let current = null;
|
|
104
116
|
const last = () => current.anchors[current.anchors.length - 1];
|
|
@@ -195,14 +207,14 @@ function nodeMatrix(node) {
|
|
|
195
207
|
function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
|
|
196
208
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
197
209
|
const path = subpathsToBezier(node.subpaths);
|
|
198
|
-
return isIdentityMatrix(m) ? path :
|
|
210
|
+
return isIdentityMatrix(m) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, m);
|
|
199
211
|
}
|
|
200
212
|
function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
|
|
201
213
|
if (node.type === "group") {
|
|
202
214
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
203
215
|
return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
|
|
204
216
|
}
|
|
205
|
-
const b =
|
|
217
|
+
const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
|
|
206
218
|
if (!b) return null;
|
|
207
219
|
return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
|
|
208
220
|
}
|
|
@@ -236,9 +248,9 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
|
236
248
|
const path = vectorPathWorldBezier(node, parent);
|
|
237
249
|
if (path.length === 0) return false;
|
|
238
250
|
const point = [p.x, p.y];
|
|
239
|
-
if (hasPaint(node.fill) &&
|
|
251
|
+
if (hasPaint(node.fill) && _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
|
|
240
252
|
const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
|
|
241
|
-
return
|
|
253
|
+
return _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
|
|
242
254
|
}
|
|
243
255
|
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
244
256
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -254,9 +266,9 @@ function nearestSegment(node, p) {
|
|
|
254
266
|
const count = subpathSegmentCount(sp);
|
|
255
267
|
for (let i = 0; i < count; i++) {
|
|
256
268
|
const seg = subpathSegment(sp, i);
|
|
257
|
-
const hit =
|
|
269
|
+
const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
|
|
258
270
|
if (!best || hit.distance < best.distance) {
|
|
259
|
-
const pt =
|
|
271
|
+
const pt = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, hit.t);
|
|
260
272
|
best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
|
|
261
273
|
}
|
|
262
274
|
}
|
|
@@ -427,7 +439,7 @@ function insertAnchor(sp, segment, t) {
|
|
|
427
439
|
const ia = segment;
|
|
428
440
|
const ib = (segment + 1) % n;
|
|
429
441
|
const seg = subpathSegment(sp, segment);
|
|
430
|
-
const [left, right] =
|
|
442
|
+
const [left, right] = _chunkBGNPT5PEcjs.splitSegment.call(void 0, seg, t);
|
|
431
443
|
const mid = left[left.length - 1];
|
|
432
444
|
const anchor = { x: mid[0], y: mid[1] };
|
|
433
445
|
if (seg.length === 4) {
|
|
@@ -535,7 +547,7 @@ var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """)
|
|
|
535
547
|
function vectorPaintToSvg(paint, gradientId) {
|
|
536
548
|
if (!hasPaint(paint)) return { value: "none", gradient: null };
|
|
537
549
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
538
|
-
return { value: `url(#${gradientId})`, gradient:
|
|
550
|
+
return { value: `url(#${gradientId})`, gradient: _chunkBGNPT5PEcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
|
|
539
551
|
}
|
|
540
552
|
function gradientMarkup(def) {
|
|
541
553
|
const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
|
|
@@ -549,11 +561,11 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
549
561
|
const pretty = options.pretty !== false;
|
|
550
562
|
const defs = [];
|
|
551
563
|
const lines = [];
|
|
552
|
-
const num = (n) =>
|
|
564
|
+
const num = (n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, precision);
|
|
553
565
|
const indent = (depth) => pretty ? " ".repeat(depth) : "";
|
|
554
566
|
const common = (node) => {
|
|
555
567
|
let out2 = ` id="${escapeAttr(node.id)}"`;
|
|
556
|
-
if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) =>
|
|
568
|
+
if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
|
|
557
569
|
if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
|
|
558
570
|
if (node.visible === false) out2 += ` display="none"`;
|
|
559
571
|
return out2;
|
|
@@ -850,7 +862,1719 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
850
862
|
return { width, height, nodes };
|
|
851
863
|
}
|
|
852
864
|
|
|
865
|
+
// src/vector/snap.ts
|
|
866
|
+
var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
|
|
867
|
+
function constrainToLeave(leave, p) {
|
|
868
|
+
if (!_optionalChain([leave, 'optionalAccess', _8 => _8.constrain])) return p;
|
|
869
|
+
const dx = p.x - leave.at.x;
|
|
870
|
+
const dy = p.y - leave.at.y;
|
|
871
|
+
switch (leave.constrain) {
|
|
872
|
+
case "axis":
|
|
873
|
+
return Math.abs(dx) > Math.abs(dy) ? { x: p.x, y: leave.at.y } : { x: leave.at.x, y: p.y };
|
|
874
|
+
case "square": {
|
|
875
|
+
const s = Math.max(Math.abs(dx), Math.abs(dy));
|
|
876
|
+
return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
|
|
877
|
+
}
|
|
878
|
+
default: {
|
|
879
|
+
const len = Math.hypot(dx, dy);
|
|
880
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
881
|
+
return { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
var BUDGET = 2e4;
|
|
886
|
+
var CROSSING_PAIRS = 32;
|
|
887
|
+
var CURVE_SAMPLES = 48;
|
|
888
|
+
var BISECT = 50;
|
|
889
|
+
var EQUAL = 1e-6;
|
|
890
|
+
function bound(sorted, key, v, strict) {
|
|
891
|
+
let lo = 0;
|
|
892
|
+
let hi = sorted.length;
|
|
893
|
+
while (lo < hi) {
|
|
894
|
+
const mid = lo + hi >>> 1;
|
|
895
|
+
const k = key(sorted[mid]);
|
|
896
|
+
if (strict ? k <= v : k < v) lo = mid + 1;
|
|
897
|
+
else hi = mid;
|
|
898
|
+
}
|
|
899
|
+
return lo;
|
|
900
|
+
}
|
|
901
|
+
function within(sorted, key, v, tol) {
|
|
902
|
+
return sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));
|
|
903
|
+
}
|
|
904
|
+
var linesNear = (lines, v, tol) => within(lines, (l) => l.v, v, tol);
|
|
905
|
+
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
906
|
+
var toPoint = (p) => [p.x, p.y];
|
|
907
|
+
var toVec = (p) => ({ x: p[0], y: p[1] });
|
|
908
|
+
var LABELS = {
|
|
909
|
+
anchor: "anchor",
|
|
910
|
+
centre: "centre",
|
|
911
|
+
intersect: "intersect",
|
|
912
|
+
perpendicular: "perpendicular",
|
|
913
|
+
tangent: "tangent",
|
|
914
|
+
path: "path",
|
|
915
|
+
construction: "angle",
|
|
916
|
+
align: "align",
|
|
917
|
+
spacing: "spacing",
|
|
918
|
+
size: "size"
|
|
919
|
+
};
|
|
920
|
+
var fmtLength = (n) => String(Math.round(n * 100) / 100);
|
|
921
|
+
function nodeFrame(node, parent, aabb) {
|
|
922
|
+
const angle = matrixRotation(multiplyMatrices(nodeMatrix(node), parent));
|
|
923
|
+
const b = angle ? vectorNodeBounds(node, multiplyMatrices(parent, rotateMatrix(-angle))) : aabb !== void 0 ? aabb : vectorNodeBounds(node, parent);
|
|
924
|
+
if (!b) return null;
|
|
925
|
+
const c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
|
|
926
|
+
return { width: b.width, height: b.height, centre: angle ? applyMatrix(rotateMatrix(angle), c) : c };
|
|
927
|
+
}
|
|
928
|
+
function contains(node, ids) {
|
|
929
|
+
if (ids.has(node.id)) return true;
|
|
930
|
+
return node.type === "group" && node.children.some((c) => contains(c, ids));
|
|
931
|
+
}
|
|
932
|
+
var VectorSnapTargets = (_class = class _VectorSnapTargets {
|
|
933
|
+
__init() {this.points = []}
|
|
934
|
+
__init2() {this.xs = []}
|
|
935
|
+
__init3() {this.ys = []}
|
|
936
|
+
__init4() {this.segments = []}
|
|
937
|
+
/** Bounds of the objects a moved box spaces itself evenly with. */
|
|
938
|
+
__init5() {this.boxes = []}
|
|
939
|
+
/** Side lengths of the other objects (in their own frames), which a resize may match. */
|
|
940
|
+
__init6() {this.sizes = []}
|
|
941
|
+
constructor() {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);
|
|
942
|
+
}
|
|
943
|
+
/** How many targets of each sort were gathered. */
|
|
944
|
+
get counts() {
|
|
945
|
+
return { points: this.points.length, lines: this.xs.length + this.ys.length, segments: this.segments.length, boxes: this.boxes.length };
|
|
946
|
+
}
|
|
947
|
+
/** Targets for moving nodes `ids`: the other art's anchors, bounds and centres, the artboard, and boxes to space evenly with. */
|
|
948
|
+
static forMove(doc, ids) {
|
|
949
|
+
return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
|
|
950
|
+
}
|
|
951
|
+
/** Targets for resizing nodes `ids`: as for a move, plus the other objects' sizes. */
|
|
952
|
+
static forResize(doc, ids) {
|
|
953
|
+
return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Targets for a drawn point (pen, shape tools): the art but `exclude`, its anchors (as
|
|
957
|
+
* points and in line), centres, edges and segments. The anchors and segments of `exclude`
|
|
958
|
+
* (the path being drawn, whose bounds grow with it) stay targets.
|
|
959
|
+
*/
|
|
960
|
+
static forDrawing(doc, exclude = []) {
|
|
961
|
+
const skip = new Set(exclude);
|
|
962
|
+
const t = _VectorSnapTargets.collect(doc, skip, "drawing", false);
|
|
963
|
+
t.pushPaths(doc, skip, () => false, true);
|
|
964
|
+
return t.index();
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Targets for dragging anchors: the other art with its segments, and the anchors and
|
|
968
|
+
* segments that stay put on the paths being reshaped (their bounds move along, so they are
|
|
969
|
+
* no targets). The moving anchors are left out.
|
|
970
|
+
*/
|
|
971
|
+
static forAnchorDrag(doc, anchors) {
|
|
972
|
+
const moving = /* @__PURE__ */ new Map();
|
|
973
|
+
for (const r of anchors) {
|
|
974
|
+
const set = _nullishCoalesce(moving.get(r.nodeId), () => ( /* @__PURE__ */ new Set()));
|
|
975
|
+
set.add(`${r.subpath}:${r.index}`);
|
|
976
|
+
moving.set(r.nodeId, set);
|
|
977
|
+
}
|
|
978
|
+
const t = _VectorSnapTargets.collect(doc, new Set(moving.keys()), "segments", false);
|
|
979
|
+
t.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!_optionalChain([moving, 'access', _9 => _9.get, 'call', _10 => _10(id), 'optionalAccess', _11 => _11.has, 'call', _12 => _12(`${s}:${i}`)]), true);
|
|
980
|
+
return t.index();
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Targets for dragging a handle of path `nodeId`: the other art and that path's anchors
|
|
984
|
+
* (its bounds and the segments through the handle move with it, so they would chase it).
|
|
985
|
+
*/
|
|
986
|
+
static forHandle(doc, nodeId) {
|
|
987
|
+
const skip = /* @__PURE__ */ new Set([nodeId]);
|
|
988
|
+
const t = _VectorSnapTargets.collect(doc, skip, "bounds", false);
|
|
989
|
+
t.pushPaths(doc, skip, () => false, false);
|
|
990
|
+
return t.index();
|
|
991
|
+
}
|
|
992
|
+
/** The anchors of the paths `ids` as targets (on them and in line), and their segments when `segments`. */
|
|
993
|
+
pushPaths(doc, ids, moving, segments) {
|
|
994
|
+
let anchors = BUDGET;
|
|
995
|
+
let segmentBudget = segments ? BUDGET : 0;
|
|
996
|
+
const visit = (list, parent) => {
|
|
997
|
+
for (const n of list) {
|
|
998
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
999
|
+
if (n.type === "group") visit(n.children, m);
|
|
1000
|
+
else if (ids.has(n.id) && n.visible !== false) {
|
|
1001
|
+
n.subpaths.forEach((sp, s) => {
|
|
1002
|
+
sp.anchors.forEach((a, i) => {
|
|
1003
|
+
if (anchors <= 0 || moving(n.id, s, i)) return;
|
|
1004
|
+
this.pushAnchor(applyMatrix(m, a));
|
|
1005
|
+
anchors--;
|
|
1006
|
+
});
|
|
1007
|
+
segmentBudget = this.pushSegments(sp, m, (i) => moving(n.id, s, i), segmentBudget);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
visit(doc.nodes, IDENTITY_MATRIX);
|
|
1013
|
+
}
|
|
1014
|
+
pushAnchor(p) {
|
|
1015
|
+
this.points.push({ p, kind: "anchor" });
|
|
1016
|
+
this.xs.push({ v: p.x, from: p, kind: "anchor" });
|
|
1017
|
+
this.ys.push({ v: p.y, from: p, kind: "anchor" });
|
|
1018
|
+
}
|
|
1019
|
+
/** The segments of `sp` (mapped by `m`) that stay put while the anchors `moving` accepts move. */
|
|
1020
|
+
pushSegments(sp, m, moving, budget) {
|
|
1021
|
+
const n = sp.anchors.length;
|
|
1022
|
+
const count = subpathSegmentCount(sp);
|
|
1023
|
+
for (let i = 0; i < count && budget > 0; i++) {
|
|
1024
|
+
if (moving(i) || moving((i + 1) % n)) continue;
|
|
1025
|
+
const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));
|
|
1026
|
+
let minX = Infinity;
|
|
1027
|
+
let minY = Infinity;
|
|
1028
|
+
let maxX = -Infinity;
|
|
1029
|
+
let maxY = -Infinity;
|
|
1030
|
+
for (const [x, y] of seg) {
|
|
1031
|
+
minX = Math.min(minX, x);
|
|
1032
|
+
minY = Math.min(minY, y);
|
|
1033
|
+
maxX = Math.max(maxX, x);
|
|
1034
|
+
maxY = Math.max(maxY, y);
|
|
1035
|
+
}
|
|
1036
|
+
this.segments.push({ seg, minX, minY, maxX, maxY });
|
|
1037
|
+
budget--;
|
|
1038
|
+
}
|
|
1039
|
+
return budget;
|
|
1040
|
+
}
|
|
1041
|
+
pushRect(r, kind, centre) {
|
|
1042
|
+
const corners2 = [
|
|
1043
|
+
{ x: r.x, y: r.y },
|
|
1044
|
+
{ x: r.x + r.width, y: r.y },
|
|
1045
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
1046
|
+
{ x: r.x, y: r.y + r.height }
|
|
1047
|
+
];
|
|
1048
|
+
for (const p of corners2) {
|
|
1049
|
+
this.xs.push({ v: p.x, from: p, kind });
|
|
1050
|
+
this.ys.push({ v: p.y, from: p, kind });
|
|
1051
|
+
}
|
|
1052
|
+
if (centre) {
|
|
1053
|
+
this.points.push({ p: centre, kind: "centre" });
|
|
1054
|
+
this.xs.push({ v: centre.x, from: centre, kind: "centre" });
|
|
1055
|
+
this.ys.push({ v: centre.y, from: centre, kind: "centre" });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/** Sorts the targets for `within` (stable: of targets alike, the first gathered wins). */
|
|
1059
|
+
index() {
|
|
1060
|
+
this.points.sort((a, b) => a.p.x - b.p.x);
|
|
1061
|
+
this.xs.sort((a, b) => a.v - b.v);
|
|
1062
|
+
this.ys.sort((a, b) => a.v - b.v);
|
|
1063
|
+
return this;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Gathers targets from the artboard and every visible node but `exclude` (and what is inside
|
|
1067
|
+
* those): each leaf's anchors (also as lines for `drawing`), its segments (`segments`,
|
|
1068
|
+
* `drawing`), its bounds' corners as lines and its centre; and the top-level objects' boxes and
|
|
1069
|
+
* sizes (inside a group holding an excluded node, that group's other members).
|
|
1070
|
+
*/
|
|
1071
|
+
static collect(doc, exclude, gather, sort = true) {
|
|
1072
|
+
const self = new _VectorSnapTargets();
|
|
1073
|
+
const artboard = { x: 0, y: 0, width: doc.width, height: doc.height };
|
|
1074
|
+
self.pushRect(artboard, "artboard", { x: doc.width / 2, y: doc.height / 2 });
|
|
1075
|
+
let anchors = BUDGET;
|
|
1076
|
+
let segments = BUDGET;
|
|
1077
|
+
const visit = (list, parent) => {
|
|
1078
|
+
for (const n of list) {
|
|
1079
|
+
if (exclude.has(n.id) || n.visible === false) continue;
|
|
1080
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
1081
|
+
if (n.type === "group") {
|
|
1082
|
+
visit(n.children, m);
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (anchors <= 0) continue;
|
|
1086
|
+
for (const sp of n.subpaths) {
|
|
1087
|
+
for (const a of sp.anchors) {
|
|
1088
|
+
if (anchors-- <= 0) break;
|
|
1089
|
+
const p = applyMatrix(m, a);
|
|
1090
|
+
if (gather === "drawing") self.pushAnchor(p);
|
|
1091
|
+
else self.points.push({ p, kind: "anchor" });
|
|
1092
|
+
}
|
|
1093
|
+
if (gather !== "bounds") segments = self.pushSegments(sp, m, () => false, segments);
|
|
1094
|
+
}
|
|
1095
|
+
const b = vectorNodeBounds(n, parent);
|
|
1096
|
+
const f = nodeFrame(n, parent, b);
|
|
1097
|
+
if (b) self.pushRect(b, "edge", _nullishCoalesce(_optionalChain([f, 'optionalAccess', _13 => _13.centre]), () => ( null)));
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
visit(doc.nodes, IDENTITY_MATRIX);
|
|
1101
|
+
const objects = (list, parent) => {
|
|
1102
|
+
for (const n of list) {
|
|
1103
|
+
if (exclude.has(n.id) || n.visible === false || self.boxes.length >= BUDGET) continue;
|
|
1104
|
+
if (n.type === "group" && contains(n, exclude)) {
|
|
1105
|
+
objects(n.children, multiplyMatrices(nodeMatrix(n), parent));
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const b = vectorNodeBounds(n, parent);
|
|
1109
|
+
if (b) self.boxes.push(b);
|
|
1110
|
+
const f = nodeFrame(n, parent, b);
|
|
1111
|
+
if (f) self.sizes.push(f.width, f.height);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
objects(doc.nodes, IDENTITY_MATRIX);
|
|
1115
|
+
return sort ? self.index() : self;
|
|
1116
|
+
}
|
|
1117
|
+
// -------------------------------------------------------------------------
|
|
1118
|
+
// Points
|
|
1119
|
+
// -------------------------------------------------------------------------
|
|
1120
|
+
/** Snaps a single point (see `snapFrom`). */
|
|
1121
|
+
snapPoint(p, tol) {
|
|
1122
|
+
return this.snapFrom(p, null, tol);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Snaps a point `p`, drawn or dragged from `leave` when given. With `leave.constrain` it keeps
|
|
1126
|
+
* to its angle round `leave.at`, sliding along that way into line with the nearest target.
|
|
1127
|
+
* Otherwise it lands, in order, on an anchor or centre, an intersection of two paths, the
|
|
1128
|
+
* perpendicular foot or a tangent point from `leave.at`, a path, a construction guide
|
|
1129
|
+
* through `leave.at` (where a target's line crosses it if one does nearby), or else lines up
|
|
1130
|
+
* with targets on x and y.
|
|
1131
|
+
*/
|
|
1132
|
+
snapFrom(p, leave, tol) {
|
|
1133
|
+
const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
|
|
1134
|
+
const at = (q, kind, lines2 = []) => ({
|
|
1135
|
+
point: q,
|
|
1136
|
+
kind,
|
|
1137
|
+
x: true,
|
|
1138
|
+
y: true,
|
|
1139
|
+
lines: lines2,
|
|
1140
|
+
labels: [this.label(q, kind)]
|
|
1141
|
+
});
|
|
1142
|
+
if (_optionalChain([leave, 'optionalAccess', _14 => _14.constrain])) {
|
|
1143
|
+
const q = constrainToLeave(leave, p);
|
|
1144
|
+
const hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);
|
|
1145
|
+
return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
|
|
1146
|
+
}
|
|
1147
|
+
const point = this.nearestPoint(p, tol);
|
|
1148
|
+
if (point) return at(point.p, point.kind);
|
|
1149
|
+
const near2 = this.segmentsNear(p, tol);
|
|
1150
|
+
const crossing = this.crossing(near2, p, tol);
|
|
1151
|
+
if (crossing) return at(crossing, "intersect");
|
|
1152
|
+
if (leave && leave.curves !== false) {
|
|
1153
|
+
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1154
|
+
if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1155
|
+
}
|
|
1156
|
+
const on = this.onSegment(near2);
|
|
1157
|
+
if (on) return at(on, "path");
|
|
1158
|
+
if (_optionalChain([leave, 'optionalAccess', _15 => _15.angles, 'optionalAccess', _16 => _16.length])) {
|
|
1159
|
+
const c = construction(leave, p, tol);
|
|
1160
|
+
if (c) {
|
|
1161
|
+
const hit = this.slide(leave.at, c.dir, c.point, tol, false);
|
|
1162
|
+
if (hit) {
|
|
1163
|
+
const lined = this.linedUp(hit.point, hit.from);
|
|
1164
|
+
return { point: hit.point, x: true, y: true, kind: lined.kind, lines: [line("construction", leave.at, hit.point), ...lined.lines], labels: lined.labels };
|
|
1165
|
+
}
|
|
1166
|
+
return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const nearest = (lines2, v) => {
|
|
1170
|
+
let best = null;
|
|
1171
|
+
for (const l of linesNear(lines2, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
|
|
1172
|
+
return best;
|
|
1173
|
+
};
|
|
1174
|
+
const bx = nearest(this.xs, p.x);
|
|
1175
|
+
const by = nearest(this.ys, p.y);
|
|
1176
|
+
if (!bx && !by) return none(p);
|
|
1177
|
+
const out = { x: bx ? bx.v : p.x, y: by ? by.v : p.y };
|
|
1178
|
+
const lines = [];
|
|
1179
|
+
if (bx) lines.push(line("align", bx.from, { x: out.x, y: out.y }));
|
|
1180
|
+
if (by) lines.push(line("align", by.from, { x: out.x, y: out.y }));
|
|
1181
|
+
return { point: out, kind: "align", x: !!bx, y: !!by, lines, labels: [this.label(out, "align")] };
|
|
1182
|
+
}
|
|
1183
|
+
label(p, kind, text = LABELS[kind]) {
|
|
1184
|
+
return { kind, x: p.x, y: p.y, text };
|
|
1185
|
+
}
|
|
1186
|
+
/** The anchor or centre nearest `p`, within `tol`. */
|
|
1187
|
+
nearestPoint(p, tol) {
|
|
1188
|
+
let best = null;
|
|
1189
|
+
let bestD = Infinity;
|
|
1190
|
+
for (const t of within(this.points, (e) => e.p.x, p.x, tol)) {
|
|
1191
|
+
const d = distance(t.p, p);
|
|
1192
|
+
if (d <= tol && d < bestD) {
|
|
1193
|
+
best = t;
|
|
1194
|
+
bestD = d;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return best;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Slides `q`, on the line through `at` along `dir` (beyond `at` only for a `ray`), to where
|
|
1201
|
+
* the nearest target line crosses it, at most twice `tol` away: where it lands and the
|
|
1202
|
+
* target point that line comes from.
|
|
1203
|
+
*/
|
|
1204
|
+
slide(at, dir, q, tol, ray) {
|
|
1205
|
+
const len2 = dir.x * dir.x + dir.y * dir.y;
|
|
1206
|
+
if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
|
|
1207
|
+
const reach = 2 * tol;
|
|
1208
|
+
let best = null;
|
|
1209
|
+
for (const vertical of [true, false]) {
|
|
1210
|
+
const d = vertical ? dir.x : dir.y;
|
|
1211
|
+
if (Math.abs(d) <= 1e-9) continue;
|
|
1212
|
+
const a = vertical ? at.x : at.y;
|
|
1213
|
+
for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
|
|
1214
|
+
const s = (l.v - a) / d;
|
|
1215
|
+
const hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };
|
|
1216
|
+
const moved = distance(hit, q);
|
|
1217
|
+
if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return best && { point: best.point, from: best.from };
|
|
1221
|
+
}
|
|
1222
|
+
/** The guides of a point `q` slid into line with the target at `from`: the line and "align", or the target's own label when `q` sits on it. */
|
|
1223
|
+
linedUp(q, from) {
|
|
1224
|
+
const on = this.nearestPoint(q, 1e-9);
|
|
1225
|
+
if (on) return { kind: on.kind, lines: [], labels: [this.label(on.p, on.kind)] };
|
|
1226
|
+
return { kind: "align", lines: [line("align", from, q)], labels: [this.label(q, "align")] };
|
|
1227
|
+
}
|
|
1228
|
+
/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
|
|
1229
|
+
segmentsNear(p, tol) {
|
|
1230
|
+
const out = [];
|
|
1231
|
+
const pt = toPoint(p);
|
|
1232
|
+
for (const s of this.segments) {
|
|
1233
|
+
if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
|
|
1234
|
+
const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, s.seg, pt);
|
|
1235
|
+
if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
|
|
1236
|
+
}
|
|
1237
|
+
return out.sort((a, b) => a.distance - b.distance);
|
|
1238
|
+
}
|
|
1239
|
+
onSegment(near2) {
|
|
1240
|
+
return near2.length ? toVec(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, near2[0].seg, near2[0].t)) : null;
|
|
1241
|
+
}
|
|
1242
|
+
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1243
|
+
crossing(near2, p, tol) {
|
|
1244
|
+
const list = near2.slice(0, CROSSING_PAIRS);
|
|
1245
|
+
let best = null;
|
|
1246
|
+
let bestD = Infinity;
|
|
1247
|
+
for (let i = 0; i < list.length; i++) {
|
|
1248
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
1249
|
+
for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
|
|
1250
|
+
const q = toVec(hit.point);
|
|
1251
|
+
const d = distance(q, p);
|
|
1252
|
+
if (d <= tol && d < bestD) {
|
|
1253
|
+
best = q;
|
|
1254
|
+
bestD = d;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return best;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* The foot of a perpendicular, or a tangent point, from `base` on a target segment near `p`
|
|
1263
|
+
* (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
|
|
1264
|
+
* bracketed by sign changes of samples and narrowed by bisection.
|
|
1265
|
+
*/
|
|
1266
|
+
curvePoint(near2, base, p, tol) {
|
|
1267
|
+
let best = null;
|
|
1268
|
+
for (const { seg } of near2) {
|
|
1269
|
+
for (const kind of ["perpendicular", "tangent"]) {
|
|
1270
|
+
if (kind === "tangent" && seg.length === 2) continue;
|
|
1271
|
+
for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
|
|
1272
|
+
const d = distance(q, p);
|
|
1273
|
+
if (d <= tol && (!best || d < best.d)) best = { point: q, kind, d };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return best && { point: best.point, kind: best.kind };
|
|
1278
|
+
}
|
|
1279
|
+
// -------------------------------------------------------------------------
|
|
1280
|
+
// Boxes
|
|
1281
|
+
// -------------------------------------------------------------------------
|
|
1282
|
+
/**
|
|
1283
|
+
* Snaps a moving box `r` (the moved selection's bounds): its edge or centre nearest a target
|
|
1284
|
+
* line on each axis (`axes` limits which may move). With `spacing`, an axis that lines up
|
|
1285
|
+
* with nothing spaces the box evenly in its row or column. Returns the correction and guides.
|
|
1286
|
+
*/
|
|
1287
|
+
snapRect(r, tol, options = {}) {
|
|
1288
|
+
const axes = { x: _optionalChain([options, 'access', _17 => _17.axes, 'optionalAccess', _18 => _18.x]) !== false, y: _optionalChain([options, 'access', _19 => _19.axes, 'optionalAccess', _20 => _20.y]) !== false };
|
|
1289
|
+
const nearest = (lines, values) => {
|
|
1290
|
+
let best = null;
|
|
1291
|
+
for (const v of values) for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.d)) best = { d: l.v - v, from: l.from };
|
|
1292
|
+
return best;
|
|
1293
|
+
};
|
|
1294
|
+
const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
|
|
1295
|
+
const by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;
|
|
1296
|
+
const out = { dx: _nullishCoalesce(_optionalChain([bx, 'optionalAccess', _21 => _21.d]), () => ( 0)), dy: _nullishCoalesce(_optionalChain([by, 'optionalAccess', _22 => _22.d]), () => ( 0)), x: !!bx, y: !!by, lines: [], labels: [] };
|
|
1297
|
+
const done = { ...r, x: r.x + out.dx, y: r.y + out.dy };
|
|
1298
|
+
if (bx) {
|
|
1299
|
+
const x = bx.from.x;
|
|
1300
|
+
out.lines.push({ kind: "align", x1: x, y1: Math.min(bx.from.y, done.y), x2: x, y2: Math.max(bx.from.y, done.y + done.height) });
|
|
1301
|
+
}
|
|
1302
|
+
if (by) {
|
|
1303
|
+
const y = by.from.y;
|
|
1304
|
+
out.lines.push({ kind: "align", x1: Math.min(by.from.x, done.x), y1: y, x2: Math.max(by.from.x, done.x + done.width), y2: y });
|
|
1305
|
+
}
|
|
1306
|
+
if (options.spacing && this.boxes.length) {
|
|
1307
|
+
for (const horizontal of [true, false]) {
|
|
1308
|
+
if (horizontal ? out.x || !axes.x : out.y || !axes.y) continue;
|
|
1309
|
+
const moved = { ...r, x: r.x + out.dx, y: r.y + out.dy };
|
|
1310
|
+
const even = this.evenSpacing(moved, horizontal, tol);
|
|
1311
|
+
if (!even) continue;
|
|
1312
|
+
if (horizontal) {
|
|
1313
|
+
out.dx += even.shift;
|
|
1314
|
+
out.x = true;
|
|
1315
|
+
} else {
|
|
1316
|
+
out.dy += even.shift;
|
|
1317
|
+
out.y = true;
|
|
1318
|
+
}
|
|
1319
|
+
for (const gap of even.gaps) this.gapMarks(gap, horizontal, tol, out);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (bx || by) out.labels.unshift(this.label({ x: r.x + out.dx + r.width / 2, y: r.y + out.dy + r.height / 2 }, "align"));
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Equal spacing along x (`horizontal`) or y for the moved box `r`, among the objects of its
|
|
1327
|
+
* row (column): the shift within `tol` that centres it between its neighbours, or leaves it
|
|
1328
|
+
* as far from the neighbour before or after it as two other objects of the row are apart;
|
|
1329
|
+
* and the gaps that are equal then, as (start, end, where across).
|
|
1330
|
+
*/
|
|
1331
|
+
evenSpacing(r, horizontal, tol) {
|
|
1332
|
+
const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
|
|
1333
|
+
const [r0, r1] = span(r, horizontal);
|
|
1334
|
+
const len = r1 - r0;
|
|
1335
|
+
const [c0, c1] = span(r, !horizontal);
|
|
1336
|
+
const row = this.boxes.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] })).filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol)).sort((a, b) => a.a0 - b.a0);
|
|
1337
|
+
if (!row.length) return null;
|
|
1338
|
+
const mid = (a, b) => (Math.max(span(a, !horizontal)[0], span(b, !horizontal)[0]) + Math.min(span(a, !horizontal)[1], span(b, !horizontal)[1])) / 2;
|
|
1339
|
+
const pairs = [];
|
|
1340
|
+
for (const p of row) {
|
|
1341
|
+
const next = bound(row, (e) => e.a0, p.a1, false);
|
|
1342
|
+
const q = row[next];
|
|
1343
|
+
if (!q) continue;
|
|
1344
|
+
const straddles = p.a1 <= r0 + tol && q.a0 >= r1 - tol;
|
|
1345
|
+
if (q.a0 > p.a1 && !straddles) pairs.push({ g: q.a0 - p.a1, gap: [p.a1, q.a0, mid(p.b, q.b)] });
|
|
1346
|
+
}
|
|
1347
|
+
let before = null;
|
|
1348
|
+
let after = null;
|
|
1349
|
+
for (const e of row) {
|
|
1350
|
+
if (e.a1 <= r0 + tol && (!before || e.a1 > before.a1)) before = e;
|
|
1351
|
+
if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
|
|
1352
|
+
}
|
|
1353
|
+
const starts = [];
|
|
1354
|
+
if (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });
|
|
1355
|
+
for (const { g } of pairs) {
|
|
1356
|
+
if (before) starts.push({ start: before.a1 + g, repeats: g });
|
|
1357
|
+
if (after) starts.push({ start: after.a0 - g - len, repeats: g });
|
|
1358
|
+
}
|
|
1359
|
+
let pick = null;
|
|
1360
|
+
for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
|
|
1361
|
+
if (!pick) return null;
|
|
1362
|
+
const { start, repeats } = pick;
|
|
1363
|
+
const at = horizontal ? { ...r, x: start } : { ...r, y: start };
|
|
1364
|
+
const end = start + len;
|
|
1365
|
+
const gaps = [];
|
|
1366
|
+
if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
|
|
1367
|
+
if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
|
|
1368
|
+
if (repeats !== null) {
|
|
1369
|
+
for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
|
|
1370
|
+
}
|
|
1371
|
+
return { shift: start - r0, gaps };
|
|
1372
|
+
}
|
|
1373
|
+
/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
|
|
1374
|
+
gapMarks([u0, u1, c], horizontal, tick, out) {
|
|
1375
|
+
const pt = (u, v) => horizontal ? { x: u, y: v } : { x: v, y: u };
|
|
1376
|
+
out.lines.push(line("spacing", pt(u0, c), pt(u1, c)), line("spacing", pt(u0, c - tick), pt(u0, c + tick)), line("spacing", pt(u1, c - tick), pt(u1, c + tick)));
|
|
1377
|
+
out.labels.push(this.label(pt((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
|
|
1378
|
+
}
|
|
1379
|
+
/** The other objects' side length nearest `length`, within `tol`, or null. */
|
|
1380
|
+
matchSize(length2, tol) {
|
|
1381
|
+
let best = null;
|
|
1382
|
+
for (const s of this.sizes) if (Math.abs(s - length2) <= tol && (best === null || Math.abs(s - length2) < Math.abs(best - length2))) best = s;
|
|
1383
|
+
return best;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Snaps a selection-box resize. `sx / sy` are the scale `scaleFor` gave for dragging
|
|
1387
|
+
* `handle` of `frame` (in the box's own, possibly rotated frame), `mods.shift` keeps the
|
|
1388
|
+
* proportions and `mods.alt` scales about the centre. The handle lands on an anchor or
|
|
1389
|
+
* centre within `tol`, else the corner or the side it moves lines up with the nearest
|
|
1390
|
+
* target, whatever the box's angle; a handle with one way to go (a side, or a proportional
|
|
1391
|
+
* corner) slides along it until one of them is on a target. An axis that snaps to nothing
|
|
1392
|
+
* may match another object's width or height instead.
|
|
1393
|
+
*/
|
|
1394
|
+
snapScale(frame, handle, sx, sy, mods, tol) {
|
|
1395
|
+
const toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;
|
|
1396
|
+
const toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;
|
|
1397
|
+
const corner = handle.length === 2;
|
|
1398
|
+
const proportional = !!mods.shift;
|
|
1399
|
+
const centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
|
|
1400
|
+
const origin = mods.alt ? centre : handlePos(opposite(handle), frame);
|
|
1401
|
+
const hp = handlePos(handle, frame);
|
|
1402
|
+
const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
|
|
1403
|
+
const valid = (n, s) => Number.isFinite(n) && n * s > 0;
|
|
1404
|
+
const nearest = (v, lines) => {
|
|
1405
|
+
let best = null;
|
|
1406
|
+
for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
|
|
1407
|
+
return best;
|
|
1408
|
+
};
|
|
1409
|
+
let nx = sx;
|
|
1410
|
+
let ny = sy;
|
|
1411
|
+
let snap = { hx: null, hy: null, on: null };
|
|
1412
|
+
if (corner && !proportional) {
|
|
1413
|
+
const p = applyMatrix(toDoc, { x: origin.x + d0.x * sx, y: origin.y + d0.y * sy });
|
|
1414
|
+
const hx = nearest(p.x, this.xs);
|
|
1415
|
+
const hy = nearest(p.y, this.ys);
|
|
1416
|
+
const on = this.nearestPoint(p, tol);
|
|
1417
|
+
const tries = [];
|
|
1418
|
+
if (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });
|
|
1419
|
+
tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _23 => _23.v]), () => ( null)), ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _24 => _24.v]), () => ( null)), s: { hx, hy, on: null } });
|
|
1420
|
+
tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _25 => _25.v]), () => ( null)), ty: null, s: { hx, hy: null, on: null } });
|
|
1421
|
+
tries.push({ tx: null, ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _26 => _26.v]), () => ( null)), s: { hx: null, hy, on: null } });
|
|
1422
|
+
for (const { tx, ty, s } of tries) {
|
|
1423
|
+
if (tx === null && ty === null) continue;
|
|
1424
|
+
const q = applyMatrix(toLocal, { x: _nullishCoalesce(tx, () => ( p.x)), y: _nullishCoalesce(ty, () => ( p.y)) });
|
|
1425
|
+
let cx = (q.x - origin.x) / d0.x;
|
|
1426
|
+
let cy = (q.y - origin.y) / d0.y;
|
|
1427
|
+
if (!frame.angle) {
|
|
1428
|
+
if (tx === null) cx = sx;
|
|
1429
|
+
if (ty === null) cy = sy;
|
|
1430
|
+
}
|
|
1431
|
+
if (valid(cx, sx) && valid(cy, sy)) {
|
|
1432
|
+
nx = cx;
|
|
1433
|
+
ny = cy;
|
|
1434
|
+
snap = s;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
} else {
|
|
1439
|
+
const movesX2 = handle === "e" || handle === "w";
|
|
1440
|
+
let e;
|
|
1441
|
+
let c;
|
|
1442
|
+
let s0;
|
|
1443
|
+
if (corner) [e, c, s0] = [{ x: Math.sign(sx), y: Math.sign(sy) }, { x: 0, y: 0 }, Math.abs(sx)];
|
|
1444
|
+
else if (movesX2) [e, c, s0] = proportional ? [{ x: 1, y: Math.sign(sx) }, { x: 0, y: 0 }, sx] : [{ x: 1, y: 0 }, { x: 0, y: 1 }, sx];
|
|
1445
|
+
else [e, c, s0] = proportional ? [{ x: Math.sign(sy), y: 1 }, { x: 0, y: 0 }, sy] : [{ x: 0, y: 1 }, { x: 1, y: 0 }, sy];
|
|
1446
|
+
const i = RING.indexOf(handle);
|
|
1447
|
+
const ends = corner ? [] : [RING[(i + 1) % 8], RING[(i + 7) % 8]];
|
|
1448
|
+
const travel = Math.hypot(e.x * d0.x, e.y * d0.y);
|
|
1449
|
+
let best = null;
|
|
1450
|
+
const consider = (s, inLine, found2) => {
|
|
1451
|
+
const moved = Math.abs(s - s0) * travel;
|
|
1452
|
+
if (!valid(s, s0) || moved > 2 * tol) return;
|
|
1453
|
+
if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
|
|
1454
|
+
};
|
|
1455
|
+
for (const h of [handle, ...ends]) {
|
|
1456
|
+
const hv = handlePos(h, frame);
|
|
1457
|
+
const v = { x: hv.x - origin.x, y: hv.y - origin.y };
|
|
1458
|
+
const base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });
|
|
1459
|
+
const dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });
|
|
1460
|
+
const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
|
|
1461
|
+
const l2 = dir.x * dir.x + dir.y * dir.y;
|
|
1462
|
+
const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
|
|
1463
|
+
if (on) consider(((on.p.x - base.x) * dir.x + (on.p.y - base.y) * dir.y) / l2, false, { hx: null, hy: null, on });
|
|
1464
|
+
if (Math.abs(dir.x) > 1e-9) {
|
|
1465
|
+
const hit = nearest(p.x, this.xs);
|
|
1466
|
+
if (hit) consider((hit.v - base.x) / dir.x, true, { hx: hit, hy: null, on: null });
|
|
1467
|
+
}
|
|
1468
|
+
if (Math.abs(dir.y) > 1e-9) {
|
|
1469
|
+
const hit = nearest(p.y, this.ys);
|
|
1470
|
+
if (hit) consider((hit.v - base.y) / dir.y, true, { hx: null, hy: hit, on: null });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
const found = best;
|
|
1474
|
+
if (found) {
|
|
1475
|
+
nx = c.x + found.s * e.x;
|
|
1476
|
+
ny = c.y + found.s * e.y;
|
|
1477
|
+
snap = found.snap;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
const out = { sx: nx, sy: ny, snapped: false, lines: [], labels: [] };
|
|
1481
|
+
const handleAt = () => applyMatrix(toDoc, { x: origin.x + d0.x * out.sx, y: origin.y + d0.y * out.sy });
|
|
1482
|
+
if (snap.on) {
|
|
1483
|
+
out.snapped = true;
|
|
1484
|
+
out.labels.push(this.label(snap.on.p, snap.on.kind));
|
|
1485
|
+
return out;
|
|
1486
|
+
}
|
|
1487
|
+
const movesX = handle !== "n" && handle !== "s";
|
|
1488
|
+
const movesY = handle !== "e" && handle !== "w";
|
|
1489
|
+
const sized = [];
|
|
1490
|
+
if (!proportional) {
|
|
1491
|
+
if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
|
|
1492
|
+
const len = this.matchSize(Math.abs(out.sx) * frame.width, tol);
|
|
1493
|
+
if (len !== null && len > 0) {
|
|
1494
|
+
out.sx = Math.sign(out.sx || 1) * (len / frame.width);
|
|
1495
|
+
sized.push("x");
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
|
|
1499
|
+
const len = this.matchSize(Math.abs(out.sy) * frame.height, tol);
|
|
1500
|
+
if (len !== null && len > 0) {
|
|
1501
|
+
out.sy = Math.sign(out.sy || 1) * (len / frame.height);
|
|
1502
|
+
sized.push("y");
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const x0 = origin.x + (frame.x - origin.x) * out.sx;
|
|
1507
|
+
const x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;
|
|
1508
|
+
const y0 = origin.y + (frame.y - origin.y) * out.sy;
|
|
1509
|
+
const y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;
|
|
1510
|
+
const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
|
|
1511
|
+
const page = [
|
|
1512
|
+
{ x: box.x0, y: box.y0 },
|
|
1513
|
+
{ x: box.x1, y: box.y0 },
|
|
1514
|
+
{ x: box.x1, y: box.y1 },
|
|
1515
|
+
{ x: box.x0, y: box.y1 }
|
|
1516
|
+
].map((q) => applyMatrix(toDoc, q));
|
|
1517
|
+
const pminY = Math.min(...page.map((q) => q.y));
|
|
1518
|
+
const pmaxY = Math.max(...page.map((q) => q.y));
|
|
1519
|
+
const pminX = Math.min(...page.map((q) => q.x));
|
|
1520
|
+
const pmaxX = Math.max(...page.map((q) => q.x));
|
|
1521
|
+
if (snap.hx) out.lines.push({ kind: "align", x1: snap.hx.v, y1: Math.min(snap.hx.from.y, pminY), x2: snap.hx.v, y2: Math.max(snap.hx.from.y, pmaxY) });
|
|
1522
|
+
if (snap.hy) out.lines.push({ kind: "align", x1: Math.min(snap.hy.from.x, pminX), y1: snap.hy.v, x2: Math.max(snap.hy.from.x, pmaxX), y2: snap.hy.v });
|
|
1523
|
+
if (snap.hx || snap.hy) {
|
|
1524
|
+
out.snapped = true;
|
|
1525
|
+
out.labels.push(this.label(handleAt(), "align"));
|
|
1526
|
+
}
|
|
1527
|
+
for (const axis of sized) {
|
|
1528
|
+
out.snapped = true;
|
|
1529
|
+
const off = tol * 2;
|
|
1530
|
+
const [a, b] = axis === "x" ? [{ x: box.x0, y: box.y1 + off }, { x: box.x1, y: box.y1 + off }] : [{ x: box.x1 + off, y: box.y0 }, { x: box.x1 + off, y: box.y1 }];
|
|
1531
|
+
const pa = applyMatrix(toDoc, a);
|
|
1532
|
+
const pb = applyMatrix(toDoc, b);
|
|
1533
|
+
out.lines.push(line("size", pa, pb));
|
|
1534
|
+
out.labels.push(this.label({ x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 }, "size", `${axis === "x" ? "W" : "H"} ${fmtLength(axis === "x" ? box.x1 - box.x0 : box.y1 - box.y0)}`));
|
|
1535
|
+
}
|
|
1536
|
+
return out;
|
|
1537
|
+
}
|
|
1538
|
+
}, _class);
|
|
1539
|
+
var RING = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
1540
|
+
function opposite(h) {
|
|
1541
|
+
return RING[(RING.indexOf(h) + 4) % 8];
|
|
1542
|
+
}
|
|
1543
|
+
function handlePos(h, r) {
|
|
1544
|
+
const x = h.includes("w") ? r.x : h.includes("e") ? r.x + r.width : r.x + r.width / 2;
|
|
1545
|
+
const y = h.startsWith("n") ? r.y : h.startsWith("s") ? r.y + r.height : r.y + r.height / 2;
|
|
1546
|
+
return { x, y };
|
|
1547
|
+
}
|
|
1548
|
+
function line(kind, a, b) {
|
|
1549
|
+
return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
|
|
1550
|
+
}
|
|
1551
|
+
function construction(leave, p, tol) {
|
|
1552
|
+
const v = { x: p.x - leave.at.x, y: p.y - leave.at.y };
|
|
1553
|
+
if (Math.hypot(v.x, v.y) <= tol) return null;
|
|
1554
|
+
let best = null;
|
|
1555
|
+
for (const a of _nullishCoalesce(leave.angles, () => ( []))) {
|
|
1556
|
+
const r = a * Math.PI / 180;
|
|
1557
|
+
const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
|
|
1558
|
+
const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
|
|
1559
|
+
const k = v.x * dir.x + v.y * dir.y;
|
|
1560
|
+
const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
|
|
1561
|
+
const d = distance(point, p);
|
|
1562
|
+
if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
|
|
1563
|
+
}
|
|
1564
|
+
return best && { dir: best.dir, point: best.point };
|
|
1565
|
+
}
|
|
1566
|
+
var dotCondition = (r, d) => r[0] * d[0] + r[1] * d[1];
|
|
1567
|
+
var crossCondition = (r, d) => r[0] * d[1] - r[1] * d[0];
|
|
1568
|
+
function solveOnSegment(seg, base, cond) {
|
|
1569
|
+
const b = [base.x, base.y];
|
|
1570
|
+
const g = (t) => {
|
|
1571
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1572
|
+
return cond([p[0] - b[0], p[1] - b[1]], _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t));
|
|
1573
|
+
};
|
|
1574
|
+
const vals = [];
|
|
1575
|
+
let scale = 0;
|
|
1576
|
+
for (let i = 0; i <= CURVE_SAMPLES; i++) {
|
|
1577
|
+
const t = i / CURVE_SAMPLES;
|
|
1578
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1579
|
+
const d = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
|
|
1580
|
+
const r = [p[0] - b[0], p[1] - b[1]];
|
|
1581
|
+
vals.push(cond(r, d));
|
|
1582
|
+
scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
|
|
1583
|
+
}
|
|
1584
|
+
if (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];
|
|
1585
|
+
const out = [];
|
|
1586
|
+
for (let i = 0; i < CURVE_SAMPLES; i++) {
|
|
1587
|
+
const ga = vals[i];
|
|
1588
|
+
const gb = vals[i + 1];
|
|
1589
|
+
if (!(Number.isFinite(ga) && Number.isFinite(gb)) || ga > 0 === gb > 0 && ga !== 0) continue;
|
|
1590
|
+
let lo = i / CURVE_SAMPLES;
|
|
1591
|
+
let hi = (i + 1) / CURVE_SAMPLES;
|
|
1592
|
+
let flo = ga;
|
|
1593
|
+
for (let k = 0; k < BISECT; k++) {
|
|
1594
|
+
const m = (lo + hi) / 2;
|
|
1595
|
+
const fm = g(m);
|
|
1596
|
+
if (fm === 0) {
|
|
1597
|
+
lo = hi = m;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
if (fm > 0 === flo > 0) {
|
|
1601
|
+
lo = m;
|
|
1602
|
+
flo = fm;
|
|
1603
|
+
} else hi = m;
|
|
1604
|
+
}
|
|
1605
|
+
const t = (lo + hi) / 2;
|
|
1606
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1607
|
+
const dv = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
|
|
1608
|
+
const r = Math.hypot(p[0] - b[0], p[1] - b[1]);
|
|
1609
|
+
const dl = Math.hypot(dv[0], dv[1]);
|
|
1610
|
+
if (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;
|
|
1611
|
+
if (!out.some((q) => Math.hypot(q.x - p[0], q.y - p[1]) <= 1e-9 * (1 + r))) out.push({ x: p[0], y: p[1] });
|
|
1612
|
+
}
|
|
1613
|
+
return out;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/vector/pathEdit.ts
|
|
1617
|
+
var EPS2 = 1e-9;
|
|
1618
|
+
var unit = (x, y) => {
|
|
1619
|
+
const l = Math.hypot(x, y);
|
|
1620
|
+
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1621
|
+
};
|
|
1622
|
+
function cubicOf(sp, i) {
|
|
1623
|
+
const s = subpathSegment(sp, i);
|
|
1624
|
+
if (s.length === 4) return s;
|
|
1625
|
+
const [a, b] = s;
|
|
1626
|
+
return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
|
|
1627
|
+
}
|
|
1628
|
+
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
1629
|
+
function startTangent(c) {
|
|
1630
|
+
for (let k = 1; k < 4; k++) {
|
|
1631
|
+
const u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
1632
|
+
if (u) return u;
|
|
1633
|
+
}
|
|
1634
|
+
return [1, 0];
|
|
1635
|
+
}
|
|
1636
|
+
function endTangent(c) {
|
|
1637
|
+
for (let k = 2; k >= 0; k--) {
|
|
1638
|
+
const u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
1639
|
+
if (u) return u;
|
|
1640
|
+
}
|
|
1641
|
+
return [1, 0];
|
|
1642
|
+
}
|
|
1643
|
+
function sample(c, n, out, withStart) {
|
|
1644
|
+
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, c, i / n));
|
|
1645
|
+
}
|
|
1646
|
+
var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
|
|
1647
|
+
function tidy(a, keepType = true) {
|
|
1648
|
+
const next = cloneAnchor(a);
|
|
1649
|
+
if (onAnchor(next, next.in)) delete next.in;
|
|
1650
|
+
if (onAnchor(next, next.out)) delete next.out;
|
|
1651
|
+
const shown = inferAnchorType(next);
|
|
1652
|
+
const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
|
|
1653
|
+
if (type === "corner") delete next.type;
|
|
1654
|
+
else next.type = type;
|
|
1655
|
+
return next;
|
|
1656
|
+
}
|
|
1657
|
+
function neighbours(sp, i) {
|
|
1658
|
+
const n = sp.anchors.length;
|
|
1659
|
+
const wrap = sp.closed && n > 1;
|
|
1660
|
+
return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
|
|
1661
|
+
}
|
|
1662
|
+
function goldenMin(f, a, b) {
|
|
1663
|
+
const R = 0.618033988749895;
|
|
1664
|
+
let x1 = b - R * (b - a);
|
|
1665
|
+
let x2 = a + R * (b - a);
|
|
1666
|
+
let f1 = f(x1);
|
|
1667
|
+
let f2 = f(x2);
|
|
1668
|
+
for (let i = 0; i < 40; i++) {
|
|
1669
|
+
if (f1.error < f2.error || Number.isNaN(f2.error)) {
|
|
1670
|
+
b = x2;
|
|
1671
|
+
x2 = x1;
|
|
1672
|
+
f2 = f1;
|
|
1673
|
+
x1 = b - R * (b - a);
|
|
1674
|
+
f1 = f(x1);
|
|
1675
|
+
} else {
|
|
1676
|
+
a = x1;
|
|
1677
|
+
x1 = x2;
|
|
1678
|
+
f1 = f2;
|
|
1679
|
+
x2 = a + R * (b - a);
|
|
1680
|
+
f2 = f(x2);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return f1.error <= f2.error ? f1 : f2;
|
|
1684
|
+
}
|
|
1685
|
+
function fitAcross(pts, N, t0, t1) {
|
|
1686
|
+
const fit = (k) => {
|
|
1687
|
+
const u = [];
|
|
1688
|
+
for (let i = 0; i <= N; i++) u.push(k * i / N);
|
|
1689
|
+
for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
|
|
1690
|
+
return _chunkBGNPT5PEcjs.fitCubicWithTangents.call(void 0, pts, u, t0, t1);
|
|
1691
|
+
};
|
|
1692
|
+
return goldenMin(fit, 0, 1);
|
|
1693
|
+
}
|
|
1694
|
+
function removeAnchorKeepingShape(sp, index) {
|
|
1695
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1696
|
+
const n = anchors.length;
|
|
1697
|
+
if (index < 0 || index >= n) return { anchors, closed: sp.closed };
|
|
1698
|
+
if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
|
|
1699
|
+
const pi = (index + n - 1) % n;
|
|
1700
|
+
const ni = (index + 1) % n;
|
|
1701
|
+
if (!(isLine(sp, pi) && isLine(sp, index))) {
|
|
1702
|
+
const left = cubicOf(sp, pi);
|
|
1703
|
+
const right = cubicOf(sp, index);
|
|
1704
|
+
const N = 16;
|
|
1705
|
+
const pts = [];
|
|
1706
|
+
sample(left, N, pts, true);
|
|
1707
|
+
sample(right, N, pts, false);
|
|
1708
|
+
const t0 = startTangent(left);
|
|
1709
|
+
const t1 = endTangent(right);
|
|
1710
|
+
const { curve } = fitAcross(pts, N, t0, t1);
|
|
1711
|
+
if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
|
|
1712
|
+
anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
|
|
1713
|
+
anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
anchors.splice(index, 1);
|
|
1718
|
+
let closed = sp.closed;
|
|
1719
|
+
if (!closed && anchors.length) {
|
|
1720
|
+
anchors[0] = tidy({ ...anchors[0], in: null });
|
|
1721
|
+
const last = anchors.length - 1;
|
|
1722
|
+
anchors[last] = tidy({ ...anchors[last], out: null });
|
|
1723
|
+
}
|
|
1724
|
+
if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
|
|
1725
|
+
return { anchors, closed };
|
|
1726
|
+
}
|
|
1727
|
+
function removeAnchorsKeepingShape(sp, indices) {
|
|
1728
|
+
const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
|
|
1729
|
+
return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
|
|
1730
|
+
}
|
|
1731
|
+
function bendSegment(sp, segment, t, p) {
|
|
1732
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1733
|
+
const n = anchors.length;
|
|
1734
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
|
|
1735
|
+
const tc = Math.min(0.95, Math.max(0.05, t));
|
|
1736
|
+
const ia = segment;
|
|
1737
|
+
const ib = (segment + 1) % n;
|
|
1738
|
+
const a = anchors[ia];
|
|
1739
|
+
const b = anchors[ib];
|
|
1740
|
+
const k = 3 * tc * (1 - tc);
|
|
1741
|
+
const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
|
|
1742
|
+
const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
|
|
1743
|
+
const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
|
|
1744
|
+
const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
|
|
1745
|
+
anchors[ia] = moveAnchorHandle(a, "out", c1);
|
|
1746
|
+
anchors[ib] = moveAnchorHandle(b, "in", c2);
|
|
1747
|
+
return { anchors, closed: sp.closed };
|
|
1748
|
+
}
|
|
1749
|
+
function subpathPointAt(sp, segment, t) {
|
|
1750
|
+
const [x, y] = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, subpathSegment(sp, segment), t);
|
|
1751
|
+
return { x, y };
|
|
1752
|
+
}
|
|
1753
|
+
function curvatureMoveAnchor(sp, index, q) {
|
|
1754
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1755
|
+
const a = sp.anchors[index];
|
|
1756
|
+
if (!a) return { anchors, closed: sp.closed };
|
|
1757
|
+
const own = [
|
|
1758
|
+
a.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
|
|
1759
|
+
a.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
|
|
1760
|
+
];
|
|
1761
|
+
const ends = [null, null];
|
|
1762
|
+
neighbours(sp, index).forEach((j, side) => {
|
|
1763
|
+
if (j < 0 || j === index) return;
|
|
1764
|
+
const b = anchors[j];
|
|
1765
|
+
const old = Math.hypot(a.x - b.x, a.y - b.y);
|
|
1766
|
+
const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
|
|
1767
|
+
const key = side === 0 ? "out" : "in";
|
|
1768
|
+
const h = b[key];
|
|
1769
|
+
if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
|
|
1770
|
+
const o = own[side];
|
|
1771
|
+
if (o) own[side] = { x: o.x * s, y: o.y * s };
|
|
1772
|
+
ends[side] = { x: b.x, y: b.y };
|
|
1773
|
+
});
|
|
1774
|
+
const moved = { x: q.x, y: q.y };
|
|
1775
|
+
if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
|
|
1776
|
+
if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
|
|
1777
|
+
if (a.type) moved.type = a.type;
|
|
1778
|
+
const [prev, next] = ends;
|
|
1779
|
+
if (a.type && a.type !== "corner" && prev && next) {
|
|
1780
|
+
const dx = next.x - prev.x;
|
|
1781
|
+
const dy = next.y - prev.y;
|
|
1782
|
+
const l = Math.hypot(dx, dy);
|
|
1783
|
+
if (l > 1e-9) {
|
|
1784
|
+
const len = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
|
|
1785
|
+
let li = len(own[0], prev);
|
|
1786
|
+
let lo = len(own[1], next);
|
|
1787
|
+
if (a.type === "symmetric") li = lo = (li + lo) / 2;
|
|
1788
|
+
moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
|
|
1789
|
+
moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
anchors[index] = moved;
|
|
1793
|
+
return { anchors, closed: sp.closed };
|
|
1794
|
+
}
|
|
1795
|
+
function cutSubpathAt(sp, at) {
|
|
1796
|
+
const n = sp.anchors.length;
|
|
1797
|
+
const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
|
|
1798
|
+
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
1799
|
+
const first = cuts[0];
|
|
1800
|
+
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
1801
|
+
const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
|
|
1802
|
+
const pieces = [];
|
|
1803
|
+
let cur = [];
|
|
1804
|
+
run.forEach((a, i) => {
|
|
1805
|
+
if (!cutSet.has(i)) {
|
|
1806
|
+
cur.push(cloneAnchor(a));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (cur.length) {
|
|
1810
|
+
cur.push(tidy({ ...a, out: null }, false));
|
|
1811
|
+
pieces.push({ anchors: cur, closed: false });
|
|
1812
|
+
cur = [];
|
|
1813
|
+
}
|
|
1814
|
+
if (i < n) cur.push(tidy({ ...a, in: null }, false));
|
|
1815
|
+
});
|
|
1816
|
+
if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
|
|
1817
|
+
return pieces;
|
|
1818
|
+
}
|
|
1819
|
+
function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
1820
|
+
const n = sp.anchors.length;
|
|
1821
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
1822
|
+
if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
|
|
1823
|
+
if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
|
|
1824
|
+
const { subpath, index } = insertAnchor(sp, segment, t);
|
|
1825
|
+
return cutSubpathAt(subpath, [index]);
|
|
1826
|
+
}
|
|
1827
|
+
var endOf = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
1828
|
+
var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
1829
|
+
function reverse(sp) {
|
|
1830
|
+
return {
|
|
1831
|
+
closed: sp.closed,
|
|
1832
|
+
anchors: sp.anchors.slice().reverse().map((a) => {
|
|
1833
|
+
const next = { x: a.x, y: a.y };
|
|
1834
|
+
if (a.out) next.in = { ...a.out };
|
|
1835
|
+
if (a.in) next.out = { ...a.in };
|
|
1836
|
+
if (a.type) next.type = a.type;
|
|
1837
|
+
return next;
|
|
1838
|
+
})
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function joinSubpaths(subpaths, tolerance) {
|
|
1842
|
+
const closed = [];
|
|
1843
|
+
let open = [];
|
|
1844
|
+
for (const sp of subpaths) {
|
|
1845
|
+
const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
1846
|
+
if (sp.closed || sp.anchors.length < 2) {
|
|
1847
|
+
if (sp.anchors.length) closed.push(copy);
|
|
1848
|
+
} else open.push(copy);
|
|
1849
|
+
}
|
|
1850
|
+
if (open.length === 1) {
|
|
1851
|
+
const sp = open[0];
|
|
1852
|
+
const first = sp.anchors[0];
|
|
1853
|
+
const last = sp.anchors[sp.anchors.length - 1];
|
|
1854
|
+
if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
|
|
1855
|
+
sp.anchors.pop();
|
|
1856
|
+
sp.anchors[0] = tidy({ ...first, in: _nullishCoalesce(last.in, () => ( null)) }, false);
|
|
1857
|
+
} else {
|
|
1858
|
+
sp.anchors[0] = tidy({ ...first, in: null }, false);
|
|
1859
|
+
sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
|
|
1860
|
+
}
|
|
1861
|
+
sp.closed = true;
|
|
1862
|
+
return [...closed, sp];
|
|
1863
|
+
}
|
|
1864
|
+
while (open.length > 1) {
|
|
1865
|
+
let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
|
|
1866
|
+
for (let i = 0; i < open.length; i++) {
|
|
1867
|
+
for (let j = i + 1; j < open.length; j++) {
|
|
1868
|
+
for (const ei of [false, true]) {
|
|
1869
|
+
for (const ej of [false, true]) {
|
|
1870
|
+
const d = dist(endOf(open[i], ei), endOf(open[j], ej));
|
|
1871
|
+
if (d < best.d) best = { d, i, j, ei, ej };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
let a = open[best.i];
|
|
1877
|
+
let b = open[best.j];
|
|
1878
|
+
open = open.filter((_, k) => k !== best.j);
|
|
1879
|
+
if (!best.ei) a = reverse(a);
|
|
1880
|
+
if (best.ej) b = reverse(b);
|
|
1881
|
+
const anchors = a.anchors;
|
|
1882
|
+
if (best.d <= tolerance) {
|
|
1883
|
+
const [head, ...rest] = b.anchors;
|
|
1884
|
+
const l = anchors[anchors.length - 1];
|
|
1885
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: _nullishCoalesce(head.out, () => ( null)) }, false);
|
|
1886
|
+
anchors.push(...rest);
|
|
1887
|
+
} else {
|
|
1888
|
+
const l = anchors[anchors.length - 1];
|
|
1889
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
|
|
1890
|
+
anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
|
|
1891
|
+
}
|
|
1892
|
+
open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
|
|
1893
|
+
}
|
|
1894
|
+
return [...closed, ...open];
|
|
1895
|
+
}
|
|
1896
|
+
function smoothSubpath(sp, amount = 1, only) {
|
|
1897
|
+
const t = Math.min(1, Math.max(0, amount));
|
|
1898
|
+
const orig = sp.anchors;
|
|
1899
|
+
const n = orig.length;
|
|
1900
|
+
const anchors = orig.map(cloneAnchor);
|
|
1901
|
+
if (n < 3 || t === 0) return { anchors, closed: sp.closed };
|
|
1902
|
+
for (let i = 0; i < n; i++) {
|
|
1903
|
+
if (!sp.closed && (i === 0 || i === n - 1)) continue;
|
|
1904
|
+
if (only && !only.has(i)) continue;
|
|
1905
|
+
const prev = orig[(i + n - 1) % n];
|
|
1906
|
+
const next = orig[(i + 1) % n];
|
|
1907
|
+
const p = orig[i];
|
|
1908
|
+
const dir = unit(next.x - prev.x, next.y - prev.y);
|
|
1909
|
+
if (!dir) continue;
|
|
1910
|
+
const li = dist(p, prev) / 3;
|
|
1911
|
+
const lo = dist(p, next) / 3;
|
|
1912
|
+
const hin = _nullishCoalesce(p.in, () => ( p));
|
|
1913
|
+
const hout = _nullishCoalesce(p.out, () => ( p));
|
|
1914
|
+
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
1915
|
+
anchors[i] = tidy(
|
|
1916
|
+
{
|
|
1917
|
+
x: p.x,
|
|
1918
|
+
y: p.y,
|
|
1919
|
+
in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
|
|
1920
|
+
out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
|
|
1921
|
+
},
|
|
1922
|
+
false
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
return { anchors, closed: sp.closed };
|
|
1926
|
+
}
|
|
1927
|
+
function segsOf(sp) {
|
|
1928
|
+
const out = [];
|
|
1929
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
1930
|
+
const s = subpathSegment(sp, i);
|
|
1931
|
+
out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
|
|
1932
|
+
}
|
|
1933
|
+
return out;
|
|
1934
|
+
}
|
|
1935
|
+
function segsToSubpath(segs, closed, types) {
|
|
1936
|
+
if (!segs.length) return null;
|
|
1937
|
+
const anchors = [];
|
|
1938
|
+
const at = (p) => ({ x: p[0], y: p[1] });
|
|
1939
|
+
anchors.push(at(segs[0].c[0]));
|
|
1940
|
+
for (const s of segs) {
|
|
1941
|
+
const end = s.c[s.c.length - 1];
|
|
1942
|
+
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
1943
|
+
const next = at(end);
|
|
1944
|
+
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
1945
|
+
anchors.push(next);
|
|
1946
|
+
}
|
|
1947
|
+
if (closed) {
|
|
1948
|
+
const last = anchors.pop();
|
|
1949
|
+
if (last.in) anchors[0].in = last.in;
|
|
1950
|
+
}
|
|
1951
|
+
const typed = anchors.map((a) => {
|
|
1952
|
+
const type = types.get(`${a.x},${a.y}`);
|
|
1953
|
+
return tidy(type ? { ...a, type } : a, !!type);
|
|
1954
|
+
});
|
|
1955
|
+
return { anchors: typed, closed };
|
|
1956
|
+
}
|
|
1957
|
+
var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
|
|
1958
|
+
function mergePair(a, b, tol) {
|
|
1959
|
+
const a0 = a.c[0];
|
|
1960
|
+
const a3 = a.c[a.c.length - 1];
|
|
1961
|
+
const b3 = b.c[b.c.length - 1];
|
|
1962
|
+
if (a.line && b.line) {
|
|
1963
|
+
const cx = b3[0] - a0[0];
|
|
1964
|
+
const cy = b3[1] - a0[1];
|
|
1965
|
+
const len = Math.hypot(cx, cy);
|
|
1966
|
+
if (len < 1e-12) return null;
|
|
1967
|
+
const mx = a3[0] - a0[0];
|
|
1968
|
+
const my = a3[1] - a0[1];
|
|
1969
|
+
const along = (mx * cx + my * cy) / len;
|
|
1970
|
+
return Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;
|
|
1971
|
+
}
|
|
1972
|
+
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
1973
|
+
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
1974
|
+
const ta = endTangent(ca);
|
|
1975
|
+
const tb = startTangent(cb);
|
|
1976
|
+
if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
|
|
1977
|
+
const pts = [];
|
|
1978
|
+
sample(ca, 16, pts, true);
|
|
1979
|
+
sample(cb, 16, pts, false);
|
|
1980
|
+
const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
|
|
1981
|
+
return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
|
|
1982
|
+
}
|
|
1983
|
+
function removeRedundantPoints(sp, tolerance) {
|
|
1984
|
+
const tol = Math.max(tolerance, 1e-9);
|
|
1985
|
+
const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
|
|
1986
|
+
const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
|
|
1987
|
+
if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
1988
|
+
const minSegs = sp.closed ? 2 : 1;
|
|
1989
|
+
let changed = true;
|
|
1990
|
+
while (changed && segs.length > minSegs) {
|
|
1991
|
+
changed = false;
|
|
1992
|
+
const n = segs.length;
|
|
1993
|
+
const joints = sp.closed ? n : n - 1;
|
|
1994
|
+
for (let j = 0; j < joints; j++) {
|
|
1995
|
+
const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
|
|
1996
|
+
const m = mergePair(segs[i0], segs[i1], tol);
|
|
1997
|
+
if (!m) continue;
|
|
1998
|
+
segs[i0] = m;
|
|
1999
|
+
segs.splice(i1, 1);
|
|
2000
|
+
if (sp.closed && i1 === 0) segs.unshift(segs.pop());
|
|
2001
|
+
changed = true;
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
return _nullishCoalesce(segsToSubpath(segs, sp.closed, types), () => ( { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/vector/pathCut.ts
|
|
2009
|
+
var loopArea = (l) => _chunkBGNPT5PEcjs.contourSignedArea.call(void 0, l.segs);
|
|
2010
|
+
function loopSample(l) {
|
|
2011
|
+
const k = l.chord.findIndex((c) => !c);
|
|
2012
|
+
return _chunkBGNPT5PEcjs.segmentPoint.call(void 0, l.segs[k >= 0 ? k : 0], 0.5);
|
|
2013
|
+
}
|
|
2014
|
+
var contains2 = (l, p) => _chunkBGNPT5PEcjs.windingNumber.call(void 0, [l.segs], p) !== 0;
|
|
2015
|
+
function groupFaces(loops) {
|
|
2016
|
+
const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
2017
|
+
if (!usable.length) return [];
|
|
2018
|
+
const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
2019
|
+
const sign = Math.sign(loopArea(largest));
|
|
2020
|
+
const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
2021
|
+
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
2022
|
+
for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
2023
|
+
const p = loopSample(hole);
|
|
2024
|
+
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
2025
|
+
if (parent) faces.get(parent).loops.push(hole);
|
|
2026
|
+
}
|
|
2027
|
+
return outers.reverse().map((o) => faces.get(o));
|
|
2028
|
+
}
|
|
2029
|
+
function faceToPath(face) {
|
|
2030
|
+
return _chunkBGNPT5PEcjs.contoursToPath.call(void 0, face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
2031
|
+
}
|
|
2032
|
+
function facesOf(path, fillRule) {
|
|
2033
|
+
const normalized = _chunkBGNPT5PEcjs.normalizeBezierPath.call(void 0, path, fillRule);
|
|
2034
|
+
const loops = _chunkBGNPT5PEcjs.pathToContours.call(void 0, normalized).map((c) => {
|
|
2035
|
+
const segs = _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true).map((s) => s.slice());
|
|
2036
|
+
return { segs, chord: segs.map(() => false) };
|
|
2037
|
+
});
|
|
2038
|
+
return groupFaces(loops);
|
|
2039
|
+
}
|
|
2040
|
+
function splitIntoFaces(path, fillRule = "nonzero") {
|
|
2041
|
+
return facesOf(path, fillRule).map(faceToPath);
|
|
2042
|
+
}
|
|
2043
|
+
function rootsInUnit(a, b, c, d, scale) {
|
|
2044
|
+
const f = (t) => ((a * t + b) * t + c) * t + d;
|
|
2045
|
+
const tol = 1e-12 * Math.max(1, scale);
|
|
2046
|
+
const cuts = [0];
|
|
2047
|
+
const qa = 3 * a;
|
|
2048
|
+
const qb = 2 * b;
|
|
2049
|
+
if (Math.abs(qa) > 1e-18) {
|
|
2050
|
+
const disc = qb * qb - 4 * qa * c;
|
|
2051
|
+
if (disc >= 0) {
|
|
2052
|
+
const r = Math.sqrt(disc);
|
|
2053
|
+
for (const t of [(-qb - r) / (2 * qa), (-qb + r) / (2 * qa)].sort((x, y) => x - y)) if (t > 0 && t < 1) cuts.push(t);
|
|
2054
|
+
}
|
|
2055
|
+
} else if (Math.abs(qb) > 1e-18) {
|
|
2056
|
+
const t = -c / qb;
|
|
2057
|
+
if (t > 0 && t < 1) cuts.push(t);
|
|
2058
|
+
}
|
|
2059
|
+
cuts.push(1);
|
|
2060
|
+
const roots = [];
|
|
2061
|
+
const push = (t) => {
|
|
2062
|
+
if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
|
|
2063
|
+
};
|
|
2064
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
2065
|
+
let lo = cuts[i];
|
|
2066
|
+
let hi = cuts[i + 1];
|
|
2067
|
+
let flo = f(lo);
|
|
2068
|
+
const fhi = f(hi);
|
|
2069
|
+
if (Math.abs(flo) <= tol) push(lo);
|
|
2070
|
+
if (Math.abs(fhi) <= tol) push(hi);
|
|
2071
|
+
if (flo * fhi >= 0) continue;
|
|
2072
|
+
for (let k = 0; k < 80; k++) {
|
|
2073
|
+
const m = (lo + hi) / 2;
|
|
2074
|
+
const fm = f(m);
|
|
2075
|
+
if (fm === 0) {
|
|
2076
|
+
lo = hi = m;
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
if (fm * flo < 0) hi = m;
|
|
2080
|
+
else {
|
|
2081
|
+
lo = m;
|
|
2082
|
+
flo = fm;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
push((lo + hi) / 2);
|
|
2086
|
+
}
|
|
2087
|
+
return roots.sort((x, y) => x - y);
|
|
2088
|
+
}
|
|
2089
|
+
function lineHits(p, q, seg) {
|
|
2090
|
+
const dx = q[0] - p[0];
|
|
2091
|
+
const dy = q[1] - p[1];
|
|
2092
|
+
const len2 = dx * dx + dy * dy;
|
|
2093
|
+
if (len2 < 1e-24) return [];
|
|
2094
|
+
const h = (pt) => (pt[0] - p[0]) * -dy + (pt[1] - p[1]) * dx;
|
|
2095
|
+
let coeffs;
|
|
2096
|
+
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
2097
|
+
else {
|
|
2098
|
+
const [h0, h1, h2, h3] = seg.map(h);
|
|
2099
|
+
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
2100
|
+
}
|
|
2101
|
+
if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
|
|
2102
|
+
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt) => Math.abs(pt[0]) + Math.abs(pt[1])));
|
|
2103
|
+
const out = [];
|
|
2104
|
+
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
2105
|
+
const point = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
2106
|
+
const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
|
|
2107
|
+
if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
|
|
2108
|
+
}
|
|
2109
|
+
return out;
|
|
2110
|
+
}
|
|
2111
|
+
var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
|
|
2112
|
+
var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
|
|
2113
|
+
function arc(l, a, b, eps) {
|
|
2114
|
+
const n = l.segs.length;
|
|
2115
|
+
const segs = [];
|
|
2116
|
+
const chord = [];
|
|
2117
|
+
const push = (k, t0, t1) => {
|
|
2118
|
+
const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : _chunkBGNPT5PEcjs.subSegment.call(void 0, l.segs[k], t0, t1);
|
|
2119
|
+
if (!degenerate(piece, eps)) {
|
|
2120
|
+
segs.push(piece);
|
|
2121
|
+
chord.push(l.chord[k]);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
if (a.seg === b.seg && b.t >= a.t + 1e-12) {
|
|
2125
|
+
push(a.seg, a.t, b.t);
|
|
2126
|
+
return { segs, chord };
|
|
2127
|
+
}
|
|
2128
|
+
push(a.seg, a.t, 1);
|
|
2129
|
+
for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
|
|
2130
|
+
push(b.seg, 0, b.t);
|
|
2131
|
+
return { segs, chord };
|
|
2132
|
+
}
|
|
2133
|
+
function polySegs(points, eps) {
|
|
2134
|
+
const out = [];
|
|
2135
|
+
for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
function knifePath(path, fillRule, knife) {
|
|
2139
|
+
const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2140
|
+
if (pts.length < 2) return null;
|
|
2141
|
+
let faces = facesOf(path, fillRule);
|
|
2142
|
+
if (!faces.length) return null;
|
|
2143
|
+
const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, path);
|
|
2144
|
+
const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
|
|
2145
|
+
const eps = size * 1e-9;
|
|
2146
|
+
let cursor = -Infinity;
|
|
2147
|
+
let cuts = 0;
|
|
2148
|
+
for (let guard = 0; guard < 512; guard++) {
|
|
2149
|
+
let best = null;
|
|
2150
|
+
faces.forEach((face2, fi2) => {
|
|
2151
|
+
const hits = [];
|
|
2152
|
+
for (let m = 0; m < pts.length - 1; m++) {
|
|
2153
|
+
if (m + 1 < cursor - 1e-9) continue;
|
|
2154
|
+
const kb = { minX: Math.min(pts[m][0], pts[m + 1][0]), maxX: Math.max(pts[m][0], pts[m + 1][0]), minY: Math.min(pts[m][1], pts[m + 1][1]), maxY: Math.max(pts[m][1], pts[m + 1][1]) };
|
|
2155
|
+
face2.loops.forEach(
|
|
2156
|
+
(l, li) => l.segs.forEach((seg, si) => {
|
|
2157
|
+
const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
|
|
2158
|
+
if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
|
|
2159
|
+
for (const h of lineHits(pts[m], pts[m + 1], seg)) {
|
|
2160
|
+
const s = m + h.s;
|
|
2161
|
+
if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
|
|
2162
|
+
}
|
|
2163
|
+
})
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
hits.sort((x, y) => x.s - y.s);
|
|
2167
|
+
const unique = hits.filter((h, i) => i === 0 || h.s - hits[i - 1].s > 1e-9 || !near(h.point, hits[i - 1].point, eps * 10));
|
|
2168
|
+
const inside = (s) => {
|
|
2169
|
+
const m = Math.min(pts.length - 2, Math.floor(s));
|
|
2170
|
+
const u = s - m;
|
|
2171
|
+
const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
|
|
2172
|
+
return _chunkBGNPT5PEcjs.windingNumber.call(void 0, face2.loops.map((l) => l.segs), q) !== 0;
|
|
2173
|
+
};
|
|
2174
|
+
for (let i = 0; i < unique.length - 1; i++) {
|
|
2175
|
+
const from2 = unique[i];
|
|
2176
|
+
const to2 = unique[i + 1];
|
|
2177
|
+
if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
|
|
2178
|
+
if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
if (!best) break;
|
|
2183
|
+
const { face: fi, from, to } = best;
|
|
2184
|
+
const face = faces[fi];
|
|
2185
|
+
const corners2 = [from.point];
|
|
2186
|
+
for (let m = Math.floor(from.s) + 1; m <= to.s - 1e-12 && m < pts.length; m++) if (m > from.s) corners2.push(pts[m]);
|
|
2187
|
+
corners2.push(to.point);
|
|
2188
|
+
const forward = polySegs(corners2, eps);
|
|
2189
|
+
const back = polySegs(corners2.slice().reverse(), eps);
|
|
2190
|
+
const la = face.loops[from.loop];
|
|
2191
|
+
const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
|
|
2192
|
+
if (from.loop === to.loop) {
|
|
2193
|
+
const ab = arc(la, from, to, eps);
|
|
2194
|
+
const ba = arc(la, to, from, eps);
|
|
2195
|
+
loops.push(
|
|
2196
|
+
{ segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
|
|
2197
|
+
{ segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
|
|
2198
|
+
);
|
|
2199
|
+
} else {
|
|
2200
|
+
const lb = face.loops[to.loop];
|
|
2201
|
+
const roundA = arc(la, from, from, eps);
|
|
2202
|
+
const roundB = arc(lb, to, to, eps);
|
|
2203
|
+
loops.push({
|
|
2204
|
+
segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
|
|
2205
|
+
chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
|
|
2209
|
+
cursor = to.s;
|
|
2210
|
+
cuts++;
|
|
2211
|
+
}
|
|
2212
|
+
return cuts ? faces.map(faceToPath) : null;
|
|
2213
|
+
}
|
|
2214
|
+
function eraserRegion(points, width) {
|
|
2215
|
+
const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2216
|
+
const r = width / 2;
|
|
2217
|
+
if (!pts.length || !(r > 0)) return [];
|
|
2218
|
+
const moved = pts.some((p) => !near(p, pts[0], 1e-9));
|
|
2219
|
+
if (!moved) {
|
|
2220
|
+
const [cx, cy] = pts[0];
|
|
2221
|
+
const k = r * 0.5522847498307936;
|
|
2222
|
+
return [
|
|
2223
|
+
{ type: "M", x: cx, y: cy - r },
|
|
2224
|
+
{ type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
|
|
2225
|
+
{ type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
|
|
2226
|
+
{ type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
|
|
2227
|
+
{ type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
|
|
2228
|
+
{ type: "Z" }
|
|
2229
|
+
];
|
|
2230
|
+
}
|
|
2231
|
+
const line2 = [{ type: "M", x: pts[0][0], y: pts[0][1] }, ...pts.slice(1).map((p) => ({ type: "L", x: p[0], y: p[1] }))];
|
|
2232
|
+
return _chunkBGNPT5PEcjs.outlineStroke.call(void 0, line2, { width, cap: "round", join: "round" });
|
|
2233
|
+
}
|
|
2234
|
+
function eraseFromFill(path, fillRule, region) {
|
|
2235
|
+
const overlap = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2236
|
+
if (!_chunkBGNPT5PEcjs.pathToContours.call(void 0, overlap).some((c) => Math.abs(_chunkBGNPT5PEcjs.contourSignedArea.call(void 0, _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true))) > 1e-9)) return null;
|
|
2237
|
+
const rest = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "difference", [{ path, fillRule }, { path: region }]).path;
|
|
2238
|
+
return splitIntoFaces(rest);
|
|
2239
|
+
}
|
|
2240
|
+
function eraseFromOutline(sp, region) {
|
|
2241
|
+
const regionSegs = _chunkBGNPT5PEcjs.pathToContours.call(void 0, region).flatMap((c) => _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true));
|
|
2242
|
+
if (!regionSegs.length) return null;
|
|
2243
|
+
const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
|
|
2244
|
+
const count = subpathSegmentCount(sp);
|
|
2245
|
+
if (!rb || count === 0) return null;
|
|
2246
|
+
const removed = (p) => _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, region, p, "nonzero");
|
|
2247
|
+
const runs = [];
|
|
2248
|
+
let cur = [];
|
|
2249
|
+
let any = false;
|
|
2250
|
+
for (let i = 0; i < count; i++) {
|
|
2251
|
+
const seg = subpathSegment(sp, i);
|
|
2252
|
+
const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
|
|
2253
|
+
const ts = [0, 1];
|
|
2254
|
+
if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
|
|
2255
|
+
for (const r of regionSegs) {
|
|
2256
|
+
const b = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, r);
|
|
2257
|
+
if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
|
|
2258
|
+
for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, seg, r)) if (hit.ta > 1e-9 && hit.ta < 1 - 1e-9) ts.push(hit.ta);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
ts.sort((a, b) => a - b);
|
|
2262
|
+
for (let k = 0; k < ts.length - 1; k++) {
|
|
2263
|
+
const t0 = ts[k];
|
|
2264
|
+
const t1 = ts[k + 1];
|
|
2265
|
+
if (t1 - t0 < 1e-9) continue;
|
|
2266
|
+
if (removed(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, (t0 + t1) / 2))) {
|
|
2267
|
+
any = true;
|
|
2268
|
+
if (cur.length) runs.push(cur);
|
|
2269
|
+
cur = [];
|
|
2270
|
+
} else cur.push({ seg: i, t0, t1 });
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (cur.length) runs.push(cur);
|
|
2274
|
+
if (!any) return null;
|
|
2275
|
+
if (sp.closed && runs.length >= 2) {
|
|
2276
|
+
const first = runs[0][0];
|
|
2277
|
+
const lastRun = runs[runs.length - 1];
|
|
2278
|
+
const last = lastRun[lastRun.length - 1];
|
|
2279
|
+
if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
|
|
2280
|
+
}
|
|
2281
|
+
const out = [];
|
|
2282
|
+
for (const run of runs) {
|
|
2283
|
+
const anchors = [];
|
|
2284
|
+
for (const { seg, t0, t1 } of run) {
|
|
2285
|
+
const c = _chunkBGNPT5PEcjs.subSegment.call(void 0, subpathSegment(sp, seg), t0, t1);
|
|
2286
|
+
const line2 = c.length === 2;
|
|
2287
|
+
const start = c[0];
|
|
2288
|
+
const end = c[c.length - 1];
|
|
2289
|
+
if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
|
|
2290
|
+
if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
|
|
2291
|
+
const next = { x: end[0], y: end[1] };
|
|
2292
|
+
if (!line2) next.in = { x: c[2][0], y: c[2][1] };
|
|
2293
|
+
anchors.push(next);
|
|
2294
|
+
}
|
|
2295
|
+
if (anchors.length < 2) continue;
|
|
2296
|
+
for (const a of anchors) {
|
|
2297
|
+
const type = inferAnchorType(a);
|
|
2298
|
+
if (type !== "corner") a.type = type;
|
|
2299
|
+
}
|
|
2300
|
+
out.push({ anchors, closed: false });
|
|
2301
|
+
}
|
|
2302
|
+
return out;
|
|
2303
|
+
}
|
|
2304
|
+
function polylineBounds(points, pad = 0) {
|
|
2305
|
+
if (!points.length) return null;
|
|
2306
|
+
let minX = Infinity;
|
|
2307
|
+
let minY = Infinity;
|
|
2308
|
+
let maxX = -Infinity;
|
|
2309
|
+
let maxY = -Infinity;
|
|
2310
|
+
for (const [x, y] of points) {
|
|
2311
|
+
minX = Math.min(minX, x);
|
|
2312
|
+
minY = Math.min(minY, y);
|
|
2313
|
+
maxX = Math.max(maxX, x);
|
|
2314
|
+
maxY = Math.max(maxY, y);
|
|
2315
|
+
}
|
|
2316
|
+
return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
|
|
2317
|
+
}
|
|
2318
|
+
function simplifyPolyline(points, tolerance) {
|
|
2319
|
+
const n = points.length;
|
|
2320
|
+
if (n < 3) return points.slice();
|
|
2321
|
+
const keep = new Array(n).fill(false);
|
|
2322
|
+
keep[0] = keep[n - 1] = true;
|
|
2323
|
+
const stack = [[0, n - 1]];
|
|
2324
|
+
while (stack.length) {
|
|
2325
|
+
const [a, b] = stack.pop();
|
|
2326
|
+
const [ax, ay] = points[a];
|
|
2327
|
+
const dx = points[b][0] - ax;
|
|
2328
|
+
const dy = points[b][1] - ay;
|
|
2329
|
+
const len = Math.hypot(dx, dy);
|
|
2330
|
+
let far = -1;
|
|
2331
|
+
let farD = 0;
|
|
2332
|
+
for (let i = a + 1; i < b; i++) {
|
|
2333
|
+
const [px, py] = points[i];
|
|
2334
|
+
const d = len < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len;
|
|
2335
|
+
if (d > farD) {
|
|
2336
|
+
farD = d;
|
|
2337
|
+
far = i;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (far >= 0 && farD > tolerance) {
|
|
2341
|
+
keep[far] = true;
|
|
2342
|
+
stack.push([a, far], [far, b]);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return points.filter((_, i) => keep[i]);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// src/vector/ink.ts
|
|
2349
|
+
var LazyMouse = (_class2 = class {
|
|
2350
|
+
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
2351
|
+
constructor(length2) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);
|
|
2352
|
+
this.length = length2;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
__init7() {this.pos = null}
|
|
2356
|
+
__init8() {this.last = null}
|
|
2357
|
+
push(p) {
|
|
2358
|
+
this.last = p;
|
|
2359
|
+
if (!(this.length > 0)) return [p];
|
|
2360
|
+
const pos = this.pos;
|
|
2361
|
+
if (!pos) {
|
|
2362
|
+
this.pos = p;
|
|
2363
|
+
return [p];
|
|
2364
|
+
}
|
|
2365
|
+
const dx = p[0] - pos[0];
|
|
2366
|
+
const dy = p[1] - pos[1];
|
|
2367
|
+
const d = Math.hypot(dx, dy);
|
|
2368
|
+
if (d <= this.length) return [];
|
|
2369
|
+
const k = (d - this.length) / d;
|
|
2370
|
+
const next = [pos[0] + dx * k, pos[1] + dy * k];
|
|
2371
|
+
this.pos = next;
|
|
2372
|
+
return [next];
|
|
2373
|
+
}
|
|
2374
|
+
/** The string from the pen to the pointer while they are apart, else null. */
|
|
2375
|
+
string() {
|
|
2376
|
+
const { pos, last } = this;
|
|
2377
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return null;
|
|
2378
|
+
return [pos, last];
|
|
2379
|
+
}
|
|
2380
|
+
/** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */
|
|
2381
|
+
finish() {
|
|
2382
|
+
const { pos, last } = this;
|
|
2383
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return [];
|
|
2384
|
+
this.pos = last;
|
|
2385
|
+
return [last];
|
|
2386
|
+
}
|
|
2387
|
+
}, _class2);
|
|
2388
|
+
var VECTOR_INK_MAX_CORNERS = 8;
|
|
2389
|
+
var MAX_POINTS = 1e5;
|
|
2390
|
+
function vectorInkShapeName(shape) {
|
|
2391
|
+
switch (shape.kind) {
|
|
2392
|
+
case "line":
|
|
2393
|
+
return "line";
|
|
2394
|
+
case "rect":
|
|
2395
|
+
return shape.width === shape.height ? "square" : "rectangle";
|
|
2396
|
+
case "ellipse":
|
|
2397
|
+
return shape.width === shape.height ? "circle" : "ellipse";
|
|
2398
|
+
case "polygon":
|
|
2399
|
+
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2403
|
+
function bbox(pts) {
|
|
2404
|
+
if (!pts.length) return [0, 0, 0, 0];
|
|
2405
|
+
let [x0, y0] = pts[0];
|
|
2406
|
+
let [x1, y1] = pts[0];
|
|
2407
|
+
for (const [x, y] of pts) {
|
|
2408
|
+
x0 = Math.min(x0, x);
|
|
2409
|
+
y0 = Math.min(y0, y);
|
|
2410
|
+
x1 = Math.max(x1, x);
|
|
2411
|
+
y1 = Math.max(y1, y);
|
|
2412
|
+
}
|
|
2413
|
+
return [x0, y0, x1 - x0, y1 - y0];
|
|
2414
|
+
}
|
|
2415
|
+
function segDistance(a, b, p) {
|
|
2416
|
+
const dx = b[0] - a[0];
|
|
2417
|
+
const dy = b[1] - a[1];
|
|
2418
|
+
const len2 = dx * dx + dy * dy;
|
|
2419
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2420
|
+
return dist2([a[0] + t * dx, a[1] + t * dy], p);
|
|
2421
|
+
}
|
|
2422
|
+
function polygonDistance(c, p) {
|
|
2423
|
+
let best = Infinity;
|
|
2424
|
+
for (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));
|
|
2425
|
+
return best;
|
|
2426
|
+
}
|
|
2427
|
+
function ellipseRms(p, x0, y0, w, h) {
|
|
2428
|
+
const rx = w / 2;
|
|
2429
|
+
const ry = h / 2;
|
|
2430
|
+
if (rx <= 0 || ry <= 0 || !p.length) return Infinity;
|
|
2431
|
+
const cx = x0 + rx;
|
|
2432
|
+
const cy = y0 + ry;
|
|
2433
|
+
let sum = 0;
|
|
2434
|
+
for (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;
|
|
2435
|
+
return Math.sqrt(sum / p.length);
|
|
2436
|
+
}
|
|
2437
|
+
function snapSquare(x, y, w, h) {
|
|
2438
|
+
if (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {
|
|
2439
|
+
const s = (w + h) / 2;
|
|
2440
|
+
return [x + (w - s) / 2, y + (h - s) / 2, s, s];
|
|
2441
|
+
}
|
|
2442
|
+
return [x, y, w, h];
|
|
2443
|
+
}
|
|
2444
|
+
function turning(a, b, c) {
|
|
2445
|
+
const u = [b[0] - a[0], b[1] - a[1]];
|
|
2446
|
+
const v = [c[0] - b[0], c[1] - b[1]];
|
|
2447
|
+
const lu = Math.hypot(u[0], u[1]);
|
|
2448
|
+
const lv = Math.hypot(v[0], v[1]);
|
|
2449
|
+
if (lu <= 0 || lv <= 0) return 0;
|
|
2450
|
+
return Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180 / Math.PI;
|
|
2451
|
+
}
|
|
2452
|
+
function rdp(p, eps) {
|
|
2453
|
+
const n = p.length;
|
|
2454
|
+
if (n < 3) return p.slice();
|
|
2455
|
+
const keep = new Array(n).fill(false);
|
|
2456
|
+
keep[0] = keep[n - 1] = true;
|
|
2457
|
+
const stack = [[0, n - 1]];
|
|
2458
|
+
while (stack.length) {
|
|
2459
|
+
const [a, b] = stack.pop();
|
|
2460
|
+
let far = a;
|
|
2461
|
+
let farD = 0;
|
|
2462
|
+
for (let i = a + 1; i < b; i++) {
|
|
2463
|
+
const d = segDistance(p[a], p[b], p[i]);
|
|
2464
|
+
if (d > farD) {
|
|
2465
|
+
farD = d;
|
|
2466
|
+
far = i;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (farD > eps) {
|
|
2470
|
+
keep[far] = true;
|
|
2471
|
+
stack.push([a, far], [far, b]);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return p.filter((_, i) => keep[i]);
|
|
2475
|
+
}
|
|
2476
|
+
function corners(p, diag) {
|
|
2477
|
+
const eps = 0.05 * diag;
|
|
2478
|
+
const start = p[0];
|
|
2479
|
+
let far = 0;
|
|
2480
|
+
let farD = 0;
|
|
2481
|
+
p.forEach((q, i) => {
|
|
2482
|
+
const d = dist2(start, q);
|
|
2483
|
+
if (d > farD) {
|
|
2484
|
+
farD = d;
|
|
2485
|
+
far = i;
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
const a = p.slice(0, far);
|
|
2489
|
+
const b = p.slice(far);
|
|
2490
|
+
const ring = rdp([...a, ...b.slice(0, 1)], eps);
|
|
2491
|
+
const back = rdp([...b, start], eps);
|
|
2492
|
+
ring.pop();
|
|
2493
|
+
back.pop();
|
|
2494
|
+
ring.push(...back);
|
|
2495
|
+
while (ring.length > 3) {
|
|
2496
|
+
const n = ring.length;
|
|
2497
|
+
const turn = (i) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);
|
|
2498
|
+
let flat = 0;
|
|
2499
|
+
let flatT = Infinity;
|
|
2500
|
+
for (let i = 0; i < n; i++) {
|
|
2501
|
+
const t = turn(i);
|
|
2502
|
+
if (t < flatT) {
|
|
2503
|
+
flatT = t;
|
|
2504
|
+
flat = i;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
let short = -1;
|
|
2508
|
+
for (let j = 0; j < n; j++) {
|
|
2509
|
+
if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
2510
|
+
short = j;
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
if (flatT < 30) ring.splice(flat, 1);
|
|
2515
|
+
else if (short >= 0) {
|
|
2516
|
+
const k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;
|
|
2517
|
+
ring.splice(k, 1);
|
|
2518
|
+
} else break;
|
|
2519
|
+
}
|
|
2520
|
+
return ring;
|
|
2521
|
+
}
|
|
2522
|
+
function upright(c) {
|
|
2523
|
+
const n = c.length;
|
|
2524
|
+
const dirs = c.map((a, i) => {
|
|
2525
|
+
const b = c[(i + 1) % n];
|
|
2526
|
+
const deg = (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI % 180 + 180) % 180;
|
|
2527
|
+
if (deg <= 15 || deg >= 165) return "across";
|
|
2528
|
+
if (deg >= 75 && deg <= 105) return "down";
|
|
2529
|
+
return null;
|
|
2530
|
+
});
|
|
2531
|
+
return dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);
|
|
2532
|
+
}
|
|
2533
|
+
function recognizeInkShape(points, options = {}) {
|
|
2534
|
+
const minSize = _nullishCoalesce(options.minSize, () => ( 6));
|
|
2535
|
+
const p = [];
|
|
2536
|
+
for (const q of points.slice(0, MAX_POINTS)) {
|
|
2537
|
+
if (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;
|
|
2538
|
+
const last2 = p[p.length - 1];
|
|
2539
|
+
if (!last2 || last2[0] !== q[0] || last2[1] !== q[1]) p.push([q[0], q[1]]);
|
|
2540
|
+
}
|
|
2541
|
+
if (p.length < 2) return null;
|
|
2542
|
+
const first = p[0];
|
|
2543
|
+
const last = p[p.length - 1];
|
|
2544
|
+
const [x0, y0, w, h] = bbox(p);
|
|
2545
|
+
const size = Math.max(w, h);
|
|
2546
|
+
const diag = Math.hypot(w, h);
|
|
2547
|
+
if (size < minSize) return null;
|
|
2548
|
+
let length2 = 0;
|
|
2549
|
+
for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
|
|
2550
|
+
const gap = dist2(first, last);
|
|
2551
|
+
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
2552
|
+
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
2553
|
+
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
2554
|
+
}
|
|
2555
|
+
if (gap > 0.2 * size || length2 < 2 * size) return null;
|
|
2556
|
+
const rms = ellipseRms(p, x0, y0, w, h);
|
|
2557
|
+
const roundish = w > 0.2 * size && h > 0.2 * size;
|
|
2558
|
+
const ellipse = () => {
|
|
2559
|
+
const [x, y, sw, sh] = snapSquare(x0, y0, w, h);
|
|
2560
|
+
return { kind: "ellipse", x, y, width: sw, height: sh };
|
|
2561
|
+
};
|
|
2562
|
+
if (roundish && rms < 0.06) return ellipse();
|
|
2563
|
+
const c = corners(p, diag);
|
|
2564
|
+
const fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);
|
|
2565
|
+
if (fits && c.length === 4 && upright(c)) {
|
|
2566
|
+
const [bx, by, bw, bh] = bbox(c);
|
|
2567
|
+
const [x, y, sw, sh] = snapSquare(bx, by, bw, bh);
|
|
2568
|
+
return { kind: "rect", x, y, width: sw, height: sh };
|
|
2569
|
+
}
|
|
2570
|
+
if (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: "polygon", points: c };
|
|
2571
|
+
return roundish && rms < 0.1 ? ellipse() : null;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
853
2574
|
// src/vector/controller.ts
|
|
2575
|
+
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
2576
|
+
var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
|
|
2577
|
+
var VECTOR_PENCIL_STRING_PX = 100;
|
|
854
2578
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
855
2579
|
var DEFAULT_DEFAULTS = {
|
|
856
2580
|
fill: "#d4d4d8",
|
|
@@ -878,7 +2602,7 @@ function getEditablePathIds(state) {
|
|
|
878
2602
|
}
|
|
879
2603
|
}
|
|
880
2604
|
}
|
|
881
|
-
return [...ids].filter((id) => _optionalChain([findVectorNode, 'call',
|
|
2605
|
+
return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _27 => _27(state.document.nodes, id), 'optionalAccess', _28 => _28.type]) === "path");
|
|
882
2606
|
}
|
|
883
2607
|
function getVisibleAnchors(state) {
|
|
884
2608
|
const out = [];
|
|
@@ -893,7 +2617,7 @@ function getVisibleHandles(state) {
|
|
|
893
2617
|
const seen = /* @__PURE__ */ new Set();
|
|
894
2618
|
const push = (node, s, i, handle) => {
|
|
895
2619
|
const sp = node.subpaths[s];
|
|
896
|
-
const a = _optionalChain([sp, 'optionalAccess',
|
|
2620
|
+
const a = _optionalChain([sp, 'optionalAccess', _29 => _29.anchors, 'access', _30 => _30[i]]);
|
|
897
2621
|
if (!a || !a[handle]) return;
|
|
898
2622
|
const key = `${node.id}:${s}:${i}:${handle}`;
|
|
899
2623
|
if (seen.has(key)) return;
|
|
@@ -902,8 +2626,8 @@ function getVisibleHandles(state) {
|
|
|
902
2626
|
};
|
|
903
2627
|
if (state.tool === "pen" && state.pen) {
|
|
904
2628
|
const node = findVectorNode(state.document.nodes, state.pen.nodeId);
|
|
905
|
-
const sp = _optionalChain([node, 'optionalAccess',
|
|
906
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
2629
|
+
const sp = _optionalChain([node, 'optionalAccess', _31 => _31.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
|
|
2630
|
+
if (_optionalChain([node, 'optionalAccess', _32 => _32.type]) === "path" && sp) {
|
|
907
2631
|
const last = sp.anchors.length - 1;
|
|
908
2632
|
push(node, state.pen.subpath, last, "in");
|
|
909
2633
|
push(node, state.pen.subpath, last, "out");
|
|
@@ -929,7 +2653,7 @@ function getVisibleHandles(state) {
|
|
|
929
2653
|
}
|
|
930
2654
|
function getAnchor(doc, ref) {
|
|
931
2655
|
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
932
|
-
return _optionalChain([node, 'optionalAccess',
|
|
2656
|
+
return _optionalChain([node, 'optionalAccess', _33 => _33.type]) === "path" ? _optionalChain([node, 'access', _34 => _34.subpaths, 'access', _35 => _35[ref.subpath], 'optionalAccess', _36 => _36.anchors, 'access', _37 => _37[ref.index]]) : void 0;
|
|
933
2657
|
}
|
|
934
2658
|
function getAnchorWorld(doc, ref) {
|
|
935
2659
|
const a = getAnchor(doc, ref);
|
|
@@ -969,14 +2693,14 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
|
|
|
969
2693
|
const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
|
|
970
2694
|
const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
|
|
971
2695
|
const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
|
|
972
|
-
const
|
|
2696
|
+
const corners2 = [
|
|
973
2697
|
{ x: box.x, y: box.y },
|
|
974
2698
|
{ x: box.x + box.width, y: box.y },
|
|
975
2699
|
{ x: box.x + box.width, y: box.y + box.height },
|
|
976
2700
|
{ x: box.x, y: box.y + box.height }
|
|
977
2701
|
].map(toScreen2);
|
|
978
2702
|
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
979
|
-
return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners };
|
|
2703
|
+
return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
|
|
980
2704
|
}
|
|
981
2705
|
var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
|
|
982
2706
|
var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
|
|
@@ -994,9 +2718,12 @@ var TOOL_LABELS = {
|
|
|
994
2718
|
ellipse: "Ellipse",
|
|
995
2719
|
polygon: "Polygon",
|
|
996
2720
|
star: "Star",
|
|
997
|
-
line: "Line"
|
|
2721
|
+
line: "Line",
|
|
2722
|
+
scissors: "Scissors",
|
|
2723
|
+
knife: "Knife",
|
|
2724
|
+
eraser: "Eraser"
|
|
998
2725
|
};
|
|
999
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse",
|
|
2726
|
+
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
1000
2727
|
function vectorToolLabel(tool) {
|
|
1001
2728
|
return TOOL_LABELS[tool];
|
|
1002
2729
|
}
|
|
@@ -1009,7 +2736,10 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
1009
2736
|
ellipse: "O",
|
|
1010
2737
|
polygon: "Y",
|
|
1011
2738
|
star: "S",
|
|
1012
|
-
line: "L"
|
|
2739
|
+
line: "L",
|
|
2740
|
+
scissors: "C",
|
|
2741
|
+
knife: "K",
|
|
2742
|
+
eraser: "E"
|
|
1013
2743
|
};
|
|
1014
2744
|
var VECTOR_TOOL_GLYPHS = {
|
|
1015
2745
|
select: "\u2196",
|
|
@@ -1020,8 +2750,12 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
1020
2750
|
ellipse: "\u25EF",
|
|
1021
2751
|
polygon: "\u2B21",
|
|
1022
2752
|
star: "\u2606",
|
|
1023
|
-
line: "\u2571"
|
|
2753
|
+
line: "\u2571",
|
|
2754
|
+
scissors: "\u2702",
|
|
2755
|
+
knife: "\u27CB",
|
|
2756
|
+
eraser: "\u232B"
|
|
1024
2757
|
};
|
|
2758
|
+
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
1025
2759
|
var BOOLEAN_OPS = {
|
|
1026
2760
|
union: "union",
|
|
1027
2761
|
subtract: "difference",
|
|
@@ -1043,43 +2777,72 @@ function mapTree(nodes, fn) {
|
|
|
1043
2777
|
}
|
|
1044
2778
|
return changed ? out : nodes;
|
|
1045
2779
|
}
|
|
2780
|
+
function spliceTree(nodes, id, replacements) {
|
|
2781
|
+
let changed = false;
|
|
2782
|
+
const out = [];
|
|
2783
|
+
for (const node of nodes) {
|
|
2784
|
+
if (node.id === id) {
|
|
2785
|
+
changed = true;
|
|
2786
|
+
out.push(...replacements);
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (node.type === "group") {
|
|
2790
|
+
const children = spliceTree(node.children, id, replacements);
|
|
2791
|
+
if (children !== node.children) {
|
|
2792
|
+
changed = true;
|
|
2793
|
+
out.push({ ...node, children });
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
out.push(node);
|
|
2798
|
+
}
|
|
2799
|
+
return changed ? out : nodes;
|
|
2800
|
+
}
|
|
2801
|
+
var toPoint2 = (p) => [p.x, p.y];
|
|
1046
2802
|
var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
1047
2803
|
var idCounter = 0;
|
|
1048
2804
|
var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
|
|
1049
|
-
var VectorEditorController = (
|
|
2805
|
+
var VectorEditorController = (_class3 = class {
|
|
2806
|
+
|
|
1050
2807
|
|
|
2808
|
+
__init9() {this.selection = []}
|
|
2809
|
+
__init10() {this.anchors = []}
|
|
2810
|
+
__init11() {this.editingId = null}
|
|
2811
|
+
__init12() {this.pen = null}
|
|
1051
2812
|
|
|
1052
|
-
__init() {this.selection = []}
|
|
1053
|
-
__init2() {this.anchors = []}
|
|
1054
|
-
__init3() {this.editingId = null}
|
|
1055
|
-
__init4() {this.pen = null}
|
|
1056
2813
|
|
|
1057
2814
|
|
|
1058
2815
|
|
|
1059
2816
|
|
|
1060
2817
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
2818
|
+
__init13() {this.hover = null}
|
|
2819
|
+
__init14() {this.pointer = null}
|
|
2820
|
+
__init15() {this.drag = null}
|
|
2821
|
+
__init16() {this.interaction = null}
|
|
2822
|
+
__init17() {this.guides = null}
|
|
2823
|
+
/** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
|
|
2824
|
+
__init18() {this.snappedToObject = false}
|
|
2825
|
+
/** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
|
|
2826
|
+
__init19() {this.hoverTargets = null}
|
|
2827
|
+
__init20() {this.spaceHeld = false}
|
|
2828
|
+
__init21() {this.clipboard = null}
|
|
1067
2829
|
|
|
1068
2830
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
2831
|
+
__init22() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2832
|
+
__init23() {this.cleanups = []}
|
|
2833
|
+
__init24() {this.applying = false}
|
|
1072
2834
|
/** Set while a commit records a change that was already applied, so `restore` still reports it. */
|
|
1073
|
-
|
|
1074
|
-
|
|
2835
|
+
__init25() {this.committing = false}
|
|
2836
|
+
__init26() {this.revision = 0}
|
|
1075
2837
|
|
|
1076
|
-
constructor(options = {}) {;
|
|
2838
|
+
constructor(options = {}) {;_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);_class3.prototype.__init18.call(this);_class3.prototype.__init19.call(this);_class3.prototype.__init20.call(this);_class3.prototype.__init21.call(this);_class3.prototype.__init22.call(this);_class3.prototype.__init23.call(this);_class3.prototype.__init24.call(this);_class3.prototype.__init25.call(this);_class3.prototype.__init26.call(this);_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);
|
|
1077
2839
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
1078
2840
|
this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
|
|
1079
2841
|
this.tool = _nullishCoalesce(options.tool, () => ( "select"));
|
|
1080
2842
|
this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
|
|
1081
2843
|
this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
|
|
1082
|
-
this.snap = {
|
|
2844
|
+
this.snap = { ...DEFAULT_VECTOR_SNAP, ..._nullishCoalesce(options.snap, () => ( {})) };
|
|
2845
|
+
this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ..._nullishCoalesce(options.editing, () => ( {})) };
|
|
1083
2846
|
this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
|
|
1084
2847
|
this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
|
|
1085
2848
|
this.onChange = options.onChange;
|
|
@@ -1101,9 +2864,9 @@ var VectorEditorController = (_class = class {
|
|
|
1101
2864
|
// State
|
|
1102
2865
|
// -------------------------------------------------------------------------
|
|
1103
2866
|
/** The current snapshot (stable until the next change). */
|
|
1104
|
-
|
|
2867
|
+
__init27() {this.getState = () => this.state}
|
|
1105
2868
|
/** Calls `listener` after every change. Returns an unsubscribe function. */
|
|
1106
|
-
|
|
2869
|
+
__init28() {this.subscribe = (listener) => {
|
|
1107
2870
|
this.listeners.add(listener);
|
|
1108
2871
|
return () => {
|
|
1109
2872
|
this.listeners.delete(listener);
|
|
@@ -1128,6 +2891,8 @@ var VectorEditorController = (_class = class {
|
|
|
1128
2891
|
viewport: this.viewport,
|
|
1129
2892
|
defaults: this.defaults,
|
|
1130
2893
|
snap: this.snap,
|
|
2894
|
+
editing: this.editing,
|
|
2895
|
+
guides: this.guides,
|
|
1131
2896
|
hitRadius: this.hitRadius,
|
|
1132
2897
|
hover: this.hover,
|
|
1133
2898
|
cursor: this.cursorFor(),
|
|
@@ -1144,7 +2909,7 @@ var VectorEditorController = (_class = class {
|
|
|
1144
2909
|
notify(docChanged = false) {
|
|
1145
2910
|
this.revision++;
|
|
1146
2911
|
this.state = this.computeState();
|
|
1147
|
-
if (docChanged) _optionalChain([this, 'access',
|
|
2912
|
+
if (docChanged) _optionalChain([this, 'access', _38 => _38.onChange, 'optionalCall', _39 => _39(this.doc)]);
|
|
1148
2913
|
const state = this.state;
|
|
1149
2914
|
this.listeners.forEach((listener) => {
|
|
1150
2915
|
try {
|
|
@@ -1174,20 +2939,20 @@ var VectorEditorController = (_class = class {
|
|
|
1174
2939
|
return applyMatrix(invertMatrix(this.worldOf(id, doc)), p);
|
|
1175
2940
|
}
|
|
1176
2941
|
cursorFor() {
|
|
1177
|
-
if (_optionalChain([this, 'access',
|
|
1178
|
-
if (_optionalChain([this, 'access',
|
|
1179
|
-
if (_optionalChain([this, 'access',
|
|
2942
|
+
if (_optionalChain([this, 'access', _40 => _40.drag, 'optionalAccess', _41 => _41.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
|
|
2943
|
+
if (_optionalChain([this, 'access', _42 => _42.drag, 'optionalAccess', _43 => _43.kind]) === "rotate") return "grabbing";
|
|
2944
|
+
if (_optionalChain([this, 'access', _44 => _44.drag, 'optionalAccess', _45 => _45.kind]) === "move") return "move";
|
|
1180
2945
|
const h = this.hover;
|
|
1181
2946
|
switch (this.tool) {
|
|
1182
2947
|
case "select":
|
|
1183
|
-
if (_optionalChain([h, 'optionalAccess',
|
|
1184
|
-
return _optionalChain([h, 'optionalAccess',
|
|
2948
|
+
if (_optionalChain([h, 'optionalAccess', _46 => _46.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _47 => _47.frameOf, 'call', _48 => _48(this.selectedRoots()), 'optionalAccess', _49 => _49.angle]), () => ( 0)));
|
|
2949
|
+
return _optionalChain([h, 'optionalAccess', _50 => _50.nodeId]) ? "move" : "default";
|
|
1185
2950
|
case "direct":
|
|
1186
|
-
if (_optionalChain([h, 'optionalAccess',
|
|
1187
|
-
if (_optionalChain([h, 'optionalAccess',
|
|
1188
|
-
return _optionalChain([h, 'optionalAccess',
|
|
2951
|
+
if (_optionalChain([h, 'optionalAccess', _51 => _51.anchor]) || _optionalChain([h, 'optionalAccess', _52 => _52.handle])) return "pointer";
|
|
2952
|
+
if (_optionalChain([h, 'optionalAccess', _53 => _53.segment])) return "copy";
|
|
2953
|
+
return _optionalChain([h, 'optionalAccess', _54 => _54.nodeId]) ? "move" : "default";
|
|
1189
2954
|
case "pen":
|
|
1190
|
-
return _optionalChain([h, 'optionalAccess',
|
|
2955
|
+
return _optionalChain([h, 'optionalAccess', _55 => _55.anchor]) ? "pointer" : "crosshair";
|
|
1191
2956
|
default:
|
|
1192
2957
|
return "crosshair";
|
|
1193
2958
|
}
|
|
@@ -1207,7 +2972,7 @@ var VectorEditorController = (_class = class {
|
|
|
1207
2972
|
this.selection = s.selection.filter((id) => this.getNode(id));
|
|
1208
2973
|
this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
|
|
1209
2974
|
this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
|
|
1210
|
-
this.pen = s.pen && _optionalChain([this, 'access',
|
|
2975
|
+
this.pen = s.pen && _optionalChain([this, 'access', _56 => _56.getNode, 'call', _57 => _57(s.pen.nodeId), 'optionalAccess', _58 => _58.subpaths, 'optionalAccess', _59 => _59[s.pen.subpath]]) ? s.pen : null;
|
|
1211
2976
|
} finally {
|
|
1212
2977
|
this.applying = false;
|
|
1213
2978
|
}
|
|
@@ -1312,6 +3077,7 @@ var VectorEditorController = (_class = class {
|
|
|
1312
3077
|
this.cancelDrag();
|
|
1313
3078
|
this.tool = tool;
|
|
1314
3079
|
this.hover = null;
|
|
3080
|
+
this.guides = null;
|
|
1315
3081
|
if (tool !== "direct") {
|
|
1316
3082
|
this.anchors = [];
|
|
1317
3083
|
this.editingId = null;
|
|
@@ -1324,18 +3090,82 @@ var VectorEditorController = (_class = class {
|
|
|
1324
3090
|
}
|
|
1325
3091
|
setSnap(snap) {
|
|
1326
3092
|
this.snap = { ...this.snap, ...snap };
|
|
3093
|
+
if (!this.snap.guides || !this.snap.objects) this.guides = null;
|
|
1327
3094
|
this.notify();
|
|
1328
3095
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
3096
|
+
/** Changes how deleting, dragging anchors, the pencil and the eraser behave. */
|
|
3097
|
+
setEditing(editing) {
|
|
3098
|
+
const next = { ...this.editing, ...editing };
|
|
3099
|
+
next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
|
|
3100
|
+
if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
|
|
3101
|
+
this.editing = next;
|
|
3102
|
+
this.notify();
|
|
3103
|
+
}
|
|
3104
|
+
/** Turns object snapping on or off (the toolbar's snapping toggle). */
|
|
3105
|
+
toggleSnapping(on = !this.snap.objects) {
|
|
3106
|
+
this.setSnap({ objects: on });
|
|
1333
3107
|
}
|
|
1334
|
-
|
|
3108
|
+
gridValue(v) {
|
|
1335
3109
|
const g = this.snap.grid;
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
3110
|
+
return g > 0 ? Math.round(v / g) * g : v;
|
|
3111
|
+
}
|
|
3112
|
+
snapPoint(p) {
|
|
3113
|
+
return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
|
|
3114
|
+
}
|
|
3115
|
+
/** The pull distance in document units. */
|
|
3116
|
+
snapTolerance() {
|
|
3117
|
+
return this.snap.threshold / this.viewport.zoom;
|
|
3118
|
+
}
|
|
3119
|
+
setGuides(guides) {
|
|
3120
|
+
this.guides = guides && this.snap.guides && (guides.lines.length || guides.labels.length) ? guides : null;
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Where a drawn or dragged point `p` goes: onto the smart guide `targets` (gathered by
|
|
3124
|
+
* `gather` when missing), else onto the grid, kept to `leave`'s constraint. Cmd / Ctrl
|
|
3125
|
+
* bypasses snapping.
|
|
3126
|
+
*/
|
|
3127
|
+
snapDrawn(p, leave, input, targets) {
|
|
3128
|
+
this.snappedToObject = false;
|
|
3129
|
+
if (input.mod) {
|
|
3130
|
+
this.setGuides(null);
|
|
3131
|
+
return constrainToLeave(leave, p);
|
|
3132
|
+
}
|
|
3133
|
+
if (this.snap.objects) {
|
|
3134
|
+
const withAngles = leave ? { ...leave, angles: this.snap.angles } : null;
|
|
3135
|
+
const res = targets().snapFrom(p, withAngles, this.snapTolerance());
|
|
3136
|
+
if (res.kind) {
|
|
3137
|
+
this.snappedToObject = true;
|
|
3138
|
+
this.setGuides(res);
|
|
3139
|
+
return _optionalChain([leave, 'optionalAccess', _60 => _60.constrain]) ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
this.setGuides(null);
|
|
3143
|
+
return constrainToLeave(leave, this.snapPoint(p));
|
|
3144
|
+
}
|
|
3145
|
+
/** Drawing targets for the pen and shape tools, reused while the document stays the same. */
|
|
3146
|
+
drawingTargets(exclude) {
|
|
3147
|
+
const key = _nullishCoalesce(exclude, () => ( ""));
|
|
3148
|
+
const cached = this.hoverTargets;
|
|
3149
|
+
if (cached && cached.doc === this.doc && cached.exclude === key) return cached.targets;
|
|
3150
|
+
const targets = VectorSnapTargets.forDrawing(this.doc, exclude ? [exclude] : []);
|
|
3151
|
+
this.hoverTargets = { doc: this.doc, exclude: key, targets };
|
|
3152
|
+
return targets;
|
|
3153
|
+
}
|
|
3154
|
+
/** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */
|
|
3155
|
+
hoverSnap(p, input) {
|
|
3156
|
+
if (this.tool === "pen") {
|
|
3157
|
+
const sp = this.pen ? _optionalChain([this, 'access', _61 => _61.getNode, 'call', _62 => _62(this.pen.nodeId), 'optionalAccess', _63 => _63.subpaths, 'access', _64 => _64[this.pen.subpath]]) : void 0;
|
|
3158
|
+
if (this.pen && sp && _optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.anchor])) {
|
|
3159
|
+
this.setGuides(null);
|
|
3160
|
+
return p;
|
|
3161
|
+
}
|
|
3162
|
+
const last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
|
|
3163
|
+
const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
|
|
3164
|
+
return this.snapDrawn(p, leave, input, () => this.drawingTargets(_nullishCoalesce(_optionalChain([this, 'access', _67 => _67.pen, 'optionalAccess', _68 => _68.nodeId]), () => ( null))));
|
|
3165
|
+
}
|
|
3166
|
+
if (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
3167
|
+
this.setGuides(null);
|
|
3168
|
+
return p;
|
|
1339
3169
|
}
|
|
1340
3170
|
// -------------------------------------------------------------------------
|
|
1341
3171
|
// Document
|
|
@@ -1566,10 +3396,10 @@ var VectorEditorController = (_class = class {
|
|
|
1566
3396
|
return this.selection.filter((id) => {
|
|
1567
3397
|
let covered = false;
|
|
1568
3398
|
const loc = locateVectorNode(this.doc.nodes, id);
|
|
1569
|
-
let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess',
|
|
3399
|
+
let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _69 => _69.parent]), () => ( null));
|
|
1570
3400
|
while (parent) {
|
|
1571
3401
|
if (this.selection.includes(parent.id)) covered = true;
|
|
1572
|
-
parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call',
|
|
3402
|
+
parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _70 => _70(this.doc.nodes, parent.id), 'optionalAccess', _71 => _71.parent]), () => ( null));
|
|
1573
3403
|
}
|
|
1574
3404
|
return !covered;
|
|
1575
3405
|
});
|
|
@@ -1752,8 +3582,8 @@ var VectorEditorController = (_class = class {
|
|
|
1752
3582
|
if (!ids.length) return null;
|
|
1753
3583
|
const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
|
|
1754
3584
|
if (locs.some((l) => !l)) return null;
|
|
1755
|
-
const parent = _nullishCoalesce(_optionalChain([locs, 'access',
|
|
1756
|
-
if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access',
|
|
3585
|
+
const parent = _nullishCoalesce(_optionalChain([locs, 'access', _72 => _72[0], 'access', _73 => _73.parent, 'optionalAccess', _74 => _74.id]), () => ( null));
|
|
3586
|
+
if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _75 => _75.parent, 'optionalAccess', _76 => _76.id]), () => ( null))) !== parent)) return null;
|
|
1757
3587
|
const id = this.newId();
|
|
1758
3588
|
this.change("Group", () => {
|
|
1759
3589
|
const set = new Set(ids);
|
|
@@ -1776,7 +3606,7 @@ var VectorEditorController = (_class = class {
|
|
|
1776
3606
|
}
|
|
1777
3607
|
/** Replaces selected groups with their children. */
|
|
1778
3608
|
ungroup(options = {}) {
|
|
1779
|
-
const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess',
|
|
3609
|
+
const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _77 => _77.type]) === "group");
|
|
1780
3610
|
if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
|
|
1781
3611
|
const set = new Set(groups.map((g) => g.id));
|
|
1782
3612
|
this.change("Ungroup", () => {
|
|
@@ -1836,7 +3666,7 @@ var VectorEditorController = (_class = class {
|
|
|
1836
3666
|
});
|
|
1837
3667
|
const paths = [...new Set(order)].map((id2) => this.getNode(id2));
|
|
1838
3668
|
if (paths.length < 2) return null;
|
|
1839
|
-
const result =
|
|
3669
|
+
const result = _chunkBGNPT5PEcjs.pathBoolean.call(void 0,
|
|
1840
3670
|
BOOLEAN_OPS[op],
|
|
1841
3671
|
paths.map((p) => ({
|
|
1842
3672
|
path: vectorPathWorldBezier(
|
|
@@ -1886,11 +3716,11 @@ var VectorEditorController = (_class = class {
|
|
|
1886
3716
|
/** A document-space path mapped into the parent space of node `id`. */
|
|
1887
3717
|
intoParentSpace(id, path) {
|
|
1888
3718
|
const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
|
|
1889
|
-
return isRoot(parent) ? path :
|
|
3719
|
+
return isRoot(parent) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
|
|
1890
3720
|
}
|
|
1891
3721
|
/** Splits selected compound paths into one path per subpath. */
|
|
1892
3722
|
breakApart(options = {}) {
|
|
1893
|
-
const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess',
|
|
3723
|
+
const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _78 => _78.type]) === "path" && n.subpaths.length > 1);
|
|
1894
3724
|
if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
|
|
1895
3725
|
const created = [];
|
|
1896
3726
|
const set = new Set(paths.map((p) => p.id));
|
|
@@ -1936,12 +3766,47 @@ var VectorEditorController = (_class = class {
|
|
|
1936
3766
|
return translateAnchor(a, d.x, d.y);
|
|
1937
3767
|
});
|
|
1938
3768
|
}
|
|
1939
|
-
/**
|
|
1940
|
-
|
|
3769
|
+
/**
|
|
3770
|
+
* Moves one anchor to a document position. Its handles follow; with `curvature` (default:
|
|
3771
|
+
* the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.
|
|
3772
|
+
*/
|
|
3773
|
+
moveAnchor(ref, to, options = {}) {
|
|
1941
3774
|
const a = getAnchorWorld(this.doc, ref);
|
|
1942
3775
|
if (!a) return { ok: false, message: "There is no such anchor." };
|
|
3776
|
+
const curvature = _nullishCoalesce(options.curvature, () => ( this.editing.curvatureMoves));
|
|
1943
3777
|
this.change("Move Point", () => {
|
|
1944
|
-
this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
3778
|
+
this.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
3779
|
+
});
|
|
3780
|
+
return { ok: true };
|
|
3781
|
+
}
|
|
3782
|
+
/** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */
|
|
3783
|
+
curvatureMoved(doc, ref, to) {
|
|
3784
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3785
|
+
if (_optionalChain([node, 'optionalAccess', _79 => _79.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
3786
|
+
const q = this.toLocal(ref.nodeId, to, doc);
|
|
3787
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp);
|
|
3788
|
+
const nodes = mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n);
|
|
3789
|
+
return { ...doc, nodes };
|
|
3790
|
+
}
|
|
3791
|
+
/** `doc` with segment `ref` bent through document point `p` at parameter `t`. */
|
|
3792
|
+
bent(doc, ref, t, p) {
|
|
3793
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3794
|
+
if (_optionalChain([node, 'optionalAccess', _80 => _80.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
3795
|
+
const q = this.toLocal(ref.nodeId, p, doc);
|
|
3796
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? bendSegment(sp, ref.segment, t, q) : sp);
|
|
3797
|
+
return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n) };
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Bends a segment so the curve passes through document point `to` at parameter `t`
|
|
3801
|
+
* (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.
|
|
3802
|
+
*/
|
|
3803
|
+
bendSegment(ref, to, t = 0.5) {
|
|
3804
|
+
const node = this.getNode(ref.nodeId);
|
|
3805
|
+
const sp = _optionalChain([node, 'optionalAccess', _81 => _81.type]) === "path" ? node.subpaths[ref.subpath] : void 0;
|
|
3806
|
+
if (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: "There is no such segment." };
|
|
3807
|
+
if (node.locked) return { ok: false, message: "The path is locked." };
|
|
3808
|
+
this.change("Bend Segment", () => {
|
|
3809
|
+
this.doc = this.bent(this.doc, ref, t, to);
|
|
1945
3810
|
});
|
|
1946
3811
|
return { ok: true };
|
|
1947
3812
|
}
|
|
@@ -1969,72 +3834,344 @@ var VectorEditorController = (_class = class {
|
|
|
1969
3834
|
});
|
|
1970
3835
|
return { ok: true };
|
|
1971
3836
|
}
|
|
1972
|
-
/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
|
|
1973
|
-
insertAnchorAt(nodeId, point) {
|
|
3837
|
+
/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
|
|
3838
|
+
insertAnchorAt(nodeId, point) {
|
|
3839
|
+
const node = this.getNode(nodeId);
|
|
3840
|
+
if (_optionalChain([node, 'optionalAccess', _82 => _82.type]) !== "path") return null;
|
|
3841
|
+
const hit = nearestSegment(node, this.toLocal(nodeId, point));
|
|
3842
|
+
if (!hit) return null;
|
|
3843
|
+
const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
|
|
3844
|
+
const ref = { nodeId, subpath: hit.subpath, index };
|
|
3845
|
+
this.change("Add Point", () => {
|
|
3846
|
+
this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
|
|
3847
|
+
this.editingId = nodeId;
|
|
3848
|
+
this.anchors = [ref];
|
|
3849
|
+
});
|
|
3850
|
+
return ref;
|
|
3851
|
+
}
|
|
3852
|
+
/**
|
|
3853
|
+
* Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go
|
|
3854
|
+
* too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'
|
|
3855
|
+
* handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.
|
|
3856
|
+
*/
|
|
3857
|
+
deleteAnchors(refs = this.anchors, options = {}) {
|
|
3858
|
+
if (!refs.length) return { ok: false, message: "Select points to delete." };
|
|
3859
|
+
const keepShape = _nullishCoalesce(options.keepShape, () => ( this.editing.keepShapeOnDelete));
|
|
3860
|
+
this.change("Delete Points", () => {
|
|
3861
|
+
const nodes = mapTree(this.doc.nodes, (n) => {
|
|
3862
|
+
if (n.type !== "path" || n.locked) return n;
|
|
3863
|
+
const mine = refs.filter((r) => r.nodeId === n.id);
|
|
3864
|
+
if (!mine.length) return n;
|
|
3865
|
+
const subpaths = n.subpaths.map((sp, s) => {
|
|
3866
|
+
const indices = mine.filter((r) => r.subpath === s).map((r) => r.index);
|
|
3867
|
+
if (!indices.length) return sp;
|
|
3868
|
+
return keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);
|
|
3869
|
+
}).filter((sp) => sp.anchors.length > 1);
|
|
3870
|
+
return subpaths.length ? { ...n, subpaths } : null;
|
|
3871
|
+
});
|
|
3872
|
+
this.setDoc(nodes);
|
|
3873
|
+
this.anchors = [];
|
|
3874
|
+
this.selection = this.selection.filter((id) => this.getNode(id));
|
|
3875
|
+
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
3876
|
+
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
3877
|
+
});
|
|
3878
|
+
return { ok: true };
|
|
3879
|
+
}
|
|
3880
|
+
/** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
|
|
3881
|
+
setClosed(closed, nodeId) {
|
|
3882
|
+
const targets = nodeId ? (() => {
|
|
3883
|
+
const n = this.getNode(nodeId);
|
|
3884
|
+
return _optionalChain([n, 'optionalAccess', _83 => _83.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
|
|
3885
|
+
})() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
|
|
3886
|
+
if (!targets.length) return { ok: false, message: "Select a path or points." };
|
|
3887
|
+
this.change(closed ? "Close Path" : "Open Path", () => {
|
|
3888
|
+
this.setDoc(
|
|
3889
|
+
mapTree(
|
|
3890
|
+
this.doc.nodes,
|
|
3891
|
+
(n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
|
|
3892
|
+
)
|
|
3893
|
+
);
|
|
3894
|
+
});
|
|
3895
|
+
return { ok: true };
|
|
3896
|
+
}
|
|
3897
|
+
/** Reverses the direction of every subpath of the selected paths. */
|
|
3898
|
+
reversePaths(ids = this.selection) {
|
|
3899
|
+
const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
|
|
3900
|
+
if (!set.size) return { ok: false, message: "Select a path." };
|
|
3901
|
+
this.change("Reverse Path", () => {
|
|
3902
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
|
|
3903
|
+
this.anchors = [];
|
|
3904
|
+
});
|
|
3905
|
+
return { ok: true };
|
|
3906
|
+
}
|
|
3907
|
+
// -------------------------------------------------------------------------
|
|
3908
|
+
// Path clean-up, Scissors, Knife and Eraser
|
|
3909
|
+
// -------------------------------------------------------------------------
|
|
3910
|
+
/** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */
|
|
3911
|
+
pathTargets(ids) {
|
|
3912
|
+
const roots = _nullishCoalesce(ids, () => ( (this.tool === "direct" && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection)));
|
|
3913
|
+
const out = /* @__PURE__ */ new Map();
|
|
3914
|
+
for (const id of roots) {
|
|
3915
|
+
const node = this.getNode(id);
|
|
3916
|
+
if (node && !node.locked) {
|
|
3917
|
+
for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
const order = [];
|
|
3921
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
3922
|
+
if (out.has(n.id)) order.push(n.id);
|
|
3923
|
+
});
|
|
3924
|
+
return order.map((id) => out.get(id));
|
|
3925
|
+
}
|
|
3926
|
+
/** Subpaths of node `from` mapped into node `to`'s own space. */
|
|
3927
|
+
subpathsIn(from, to) {
|
|
3928
|
+
const m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));
|
|
3929
|
+
if (isRoot(m)) return from.subpaths;
|
|
3930
|
+
const map = (p) => applyMatrix(m, p);
|
|
3931
|
+
return from.subpaths.map((sp) => ({
|
|
3932
|
+
closed: sp.closed,
|
|
3933
|
+
anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {} }))
|
|
3934
|
+
}));
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and
|
|
3938
|
+
* place, the others go just above it with the same style). No pieces removes the path.
|
|
3939
|
+
* Returns the ids of the paths now standing in for it.
|
|
3940
|
+
*/
|
|
3941
|
+
replaceWithPieces(node, pieces) {
|
|
3942
|
+
const nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }));
|
|
3943
|
+
this.setDoc(spliceTree(this.doc.nodes, node.id, nodes));
|
|
3944
|
+
return nodes.map((n) => n.id);
|
|
3945
|
+
}
|
|
3946
|
+
/** Clears anchor selections, editing and pen state that no longer point at anything. */
|
|
3947
|
+
dropStaleRefs() {
|
|
3948
|
+
this.selection = this.selection.filter((id) => this.getNode(id));
|
|
3949
|
+
this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
|
|
3950
|
+
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
3951
|
+
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Join: the open subpaths of the selected (or given) paths become one open path, joined
|
|
3955
|
+
* nearest endpoints first (ends within `tolerance` document units merge into one anchor,
|
|
3956
|
+
* others get a straight segment); a single open subpath is closed. The result replaces the
|
|
3957
|
+
* lowest path, in its space and style. Default tolerance: the pick distance.
|
|
3958
|
+
*/
|
|
3959
|
+
joinPaths(options = {}) {
|
|
3960
|
+
const paths = this.pathTargets(options.ids);
|
|
3961
|
+
if (!paths.length) return { ok: false, message: "Select paths to join." };
|
|
3962
|
+
const base = paths[0];
|
|
3963
|
+
const all = paths.flatMap((p) => this.subpathsIn(p, base.id));
|
|
3964
|
+
if (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: "There are no open paths to join." };
|
|
3965
|
+
const subpaths = joinSubpaths(all, _nullishCoalesce(options.tolerance, () => ( this.tolerance())));
|
|
3966
|
+
const drop = new Set(paths.slice(1).map((p) => p.id));
|
|
3967
|
+
this.change("Join", () => {
|
|
3968
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n));
|
|
3969
|
+
this.selection = [base.id];
|
|
3970
|
+
this.anchors = [];
|
|
3971
|
+
if (this.editingId && drop.has(this.editingId)) this.editingId = base.id;
|
|
3972
|
+
this.dropStaleRefs();
|
|
3973
|
+
});
|
|
3974
|
+
return { ok: true };
|
|
3975
|
+
}
|
|
3976
|
+
/** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */
|
|
3977
|
+
averageAnchors(axis = "both", refs = this.anchors) {
|
|
3978
|
+
const points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p) => !!p.at);
|
|
3979
|
+
if (points.length < 2) return { ok: false, message: "Select at least two points to average." };
|
|
3980
|
+
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
3981
|
+
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
3982
|
+
this.change("Average", () => {
|
|
3983
|
+
for (const { ref, at } of points) {
|
|
3984
|
+
const dx = axis === "horizontal" ? 0 : cx - at.x;
|
|
3985
|
+
const dy = axis === "vertical" ? 0 : cy - at.y;
|
|
3986
|
+
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
return { ok: true };
|
|
3990
|
+
}
|
|
3991
|
+
/** Applies `fn` to every subpath of the target paths as one undo step. */
|
|
3992
|
+
editSubpaths(label, ids, fn) {
|
|
3993
|
+
const paths = this.pathTargets(ids);
|
|
3994
|
+
if (!paths.length) return { ok: false, message: "Select a path." };
|
|
3995
|
+
const set = new Set(paths.map((p) => p.id));
|
|
3996
|
+
this.change(label, () => {
|
|
3997
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n));
|
|
3998
|
+
this.dropStaleRefs();
|
|
3999
|
+
});
|
|
4000
|
+
return { ok: true };
|
|
4001
|
+
}
|
|
4002
|
+
/**
|
|
4003
|
+
* Remove Redundant Points: drops anchors of the selected (or given) paths that can go
|
|
4004
|
+
* without moving the outline more than `tolerance` document units (default 0.5).
|
|
4005
|
+
*/
|
|
4006
|
+
removeRedundantPoints(options = {}) {
|
|
4007
|
+
const tolerance = _nullishCoalesce(options.tolerance, () => ( 0.5));
|
|
4008
|
+
return this.editSubpaths("Remove Redundant Points", options.ids, (sp) => removeRedundantPoints(sp, tolerance));
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct
|
|
4012
|
+
* tool and points selected, only those points change.
|
|
4013
|
+
*/
|
|
4014
|
+
smoothPaths(options = {}) {
|
|
4015
|
+
const refs = _nullishCoalesce(options.refs, () => ( (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null)));
|
|
4016
|
+
const ids = _nullishCoalesce(options.ids, () => ( (refs ? [...new Set(refs.map((r) => r.nodeId))] : void 0)));
|
|
4017
|
+
return this.editSubpaths("Smooth", ids, (sp, node, i) => {
|
|
4018
|
+
const only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : void 0;
|
|
4019
|
+
return only && !only.size ? sp : smoothSubpath(sp, _nullishCoalesce(options.amount, () => ( 1)), only);
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
/** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */
|
|
4023
|
+
applyCuts(node, cut) {
|
|
4024
|
+
const results = node.subpaths.map((sp, i) => cut(sp, i));
|
|
4025
|
+
if (results.every((r) => !r)) return null;
|
|
4026
|
+
const subpaths = node.subpaths.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
|
|
4027
|
+
if (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));
|
|
4028
|
+
this.replaceNode(node.id, { ...node, subpaths });
|
|
4029
|
+
return [node.id];
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an
|
|
4033
|
+
* anchor when within the pick distance of one). A closed subpath opens there; an open one
|
|
4034
|
+
* splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.
|
|
4035
|
+
*/
|
|
4036
|
+
cutPathAt(nodeId, point) {
|
|
1974
4037
|
const node = this.getNode(nodeId);
|
|
1975
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
1976
|
-
const
|
|
4038
|
+
if (_optionalChain([node, 'optionalAccess', _84 => _84.type]) !== "path" || node.locked) return null;
|
|
4039
|
+
const local = this.toLocal(nodeId, point);
|
|
4040
|
+
const hit = nearestSegment(node, local);
|
|
1977
4041
|
if (!hit) return null;
|
|
1978
|
-
const
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
4042
|
+
const sp = node.subpaths[hit.subpath];
|
|
4043
|
+
const n = sp.anchors.length;
|
|
4044
|
+
const near2 = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());
|
|
4045
|
+
if (!sp.closed && n > 1 && (near2 === 0 || near2 === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
|
|
4046
|
+
let ids = null;
|
|
4047
|
+
this.change("Cut Path", () => {
|
|
4048
|
+
ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near2 !== void 0 ? cutSubpathAt(s, [near2]) : splitSubpathAt(s, hit.segment, hit.t));
|
|
4049
|
+
if (ids) {
|
|
4050
|
+
this.selection = ids;
|
|
4051
|
+
this.anchors = [];
|
|
4052
|
+
if (this.editingId === nodeId) this.editingId = null;
|
|
4053
|
+
}
|
|
4054
|
+
this.dropStaleRefs();
|
|
1984
4055
|
});
|
|
1985
|
-
return
|
|
4056
|
+
return ids;
|
|
1986
4057
|
}
|
|
1987
|
-
/**
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
this.
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
4058
|
+
/** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */
|
|
4059
|
+
cutAtAnchors(refs = this.anchors) {
|
|
4060
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4061
|
+
for (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [..._nullishCoalesce(byNode.get(r.nodeId), () => ( [])), r]);
|
|
4062
|
+
if (!byNode.size) return { ok: false, message: "Select points to cut at." };
|
|
4063
|
+
const created = [];
|
|
4064
|
+
this.change("Cut Path", () => {
|
|
4065
|
+
for (const [id, list] of byNode) {
|
|
4066
|
+
const node = this.getNode(id);
|
|
4067
|
+
if (node.locked) continue;
|
|
4068
|
+
const ids = this.applyCuts(node, (sp, i) => {
|
|
4069
|
+
const at = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
4070
|
+
const pieces = at.length ? cutSubpathAt(sp, at) : null;
|
|
4071
|
+
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4072
|
+
});
|
|
4073
|
+
if (ids) created.push(...ids);
|
|
4074
|
+
}
|
|
4075
|
+
if (created.length) this.selection = created;
|
|
1999
4076
|
this.anchors = [];
|
|
2000
|
-
this.
|
|
2001
|
-
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
2002
|
-
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
4077
|
+
this.dropStaleRefs();
|
|
2003
4078
|
});
|
|
2004
|
-
return { ok: true };
|
|
4079
|
+
return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
|
|
2005
4080
|
}
|
|
2006
|
-
/**
|
|
2007
|
-
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
})() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
|
|
2012
|
-
if (!targets.length) return { ok: false, message: "Select a path or points." };
|
|
2013
|
-
this.change(closed ? "Close Path" : "Open Path", () => {
|
|
2014
|
-
this.setDoc(
|
|
2015
|
-
mapTree(
|
|
2016
|
-
this.doc.nodes,
|
|
2017
|
-
(n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
|
|
2018
|
-
)
|
|
2019
|
-
);
|
|
4081
|
+
/** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */
|
|
4082
|
+
strokeTargets(points, ids, pad) {
|
|
4083
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
4084
|
+
walkVectorNodes(this.doc.nodes, (n, parent) => {
|
|
4085
|
+
if (n.visible === false || n.locked || parent && blocked.has(parent.id)) blocked.add(n.id);
|
|
2020
4086
|
});
|
|
2021
|
-
|
|
4087
|
+
const visible = (p) => !blocked.has(p.id);
|
|
4088
|
+
if (_optionalChain([ids, 'optionalAccess', _85 => _85.length]) || this.selection.length) return this.pathTargets(_nullishCoalesce(ids, () => ( this.selection))).filter(visible);
|
|
4089
|
+
const box = polylineBounds(points.map(toPoint2), pad);
|
|
4090
|
+
if (!box) return [];
|
|
4091
|
+
const out = [];
|
|
4092
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
4093
|
+
if (n.type !== "path" || !visible(n)) return;
|
|
4094
|
+
const b = vectorNodeBounds(n, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, n.id), () => ( IDENTITY_MATRIX)));
|
|
4095
|
+
if (b && boundsIntersect(b, box)) out.push(n);
|
|
4096
|
+
});
|
|
4097
|
+
return out;
|
|
2022
4098
|
}
|
|
2023
|
-
/**
|
|
2024
|
-
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
4099
|
+
/** A stroke's points in node `id`'s own space. */
|
|
4100
|
+
localPoints(id, points) {
|
|
4101
|
+
const inv = invertMatrix(this.worldOf(id));
|
|
4102
|
+
return points.map((p) => toPoint2(applyMatrix(inv, p)));
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses
|
|
4106
|
+
* them from edge to edge, with no gap between the pieces. It cuts the given paths, the
|
|
4107
|
+
* selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty
|
|
4108
|
+
* when nothing was cut).
|
|
4109
|
+
*/
|
|
4110
|
+
knife(points, options = {}) {
|
|
4111
|
+
const pts = simplifyPolyline(points.map(toPoint2), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));
|
|
4112
|
+
if (pts.length < 2) return [];
|
|
4113
|
+
const plan = [];
|
|
4114
|
+
for (const node of this.strokeTargets(pts, options.ids, 0)) {
|
|
4115
|
+
const closed = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4116
|
+
if (!closed.length || closed.some((sp) => !sp.closed)) continue;
|
|
4117
|
+
const result = knifePath(subpathsToBezier(closed), _nullishCoalesce(node.fillRule, () => ( "nonzero")), this.localPoints(node.id, pts));
|
|
4118
|
+
if (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });
|
|
4119
|
+
}
|
|
4120
|
+
if (!plan.length) return [];
|
|
4121
|
+
const created = [];
|
|
4122
|
+
this.change("Knife", () => {
|
|
4123
|
+
for (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));
|
|
4124
|
+
this.selection = created;
|
|
2029
4125
|
this.anchors = [];
|
|
4126
|
+
this.dropStaleRefs();
|
|
2030
4127
|
});
|
|
2031
|
-
return
|
|
4128
|
+
return created;
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)
|
|
4132
|
+
* along `points` (document space). Filled closed paths lose that area (pieces that come
|
|
4133
|
+
* apart become separate paths); outlines are cut where they pass under it. It erases from
|
|
4134
|
+
* the given paths, the selection, or (with neither) every path it reaches. Returns the ids
|
|
4135
|
+
* of the paths it changed or created.
|
|
4136
|
+
*/
|
|
4137
|
+
erase(points, options = {}) {
|
|
4138
|
+
const width = _nullishCoalesce(options.size, () => ( this.editing.eraserSize));
|
|
4139
|
+
if (!(width > 0) || !points.length) return [];
|
|
4140
|
+
const pts = simplifyPolyline(points.map(toPoint2), Math.min(width * 0.05, 0.25 / this.viewport.zoom));
|
|
4141
|
+
const region = eraserRegion(pts, width);
|
|
4142
|
+
if (!region.length) return [];
|
|
4143
|
+
const plan = [];
|
|
4144
|
+
const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
|
|
4145
|
+
const reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;
|
|
4146
|
+
for (const node of this.strokeTargets(reach, options.ids, 0)) {
|
|
4147
|
+
const local = _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, region, invertMatrix(this.worldOf(node.id)));
|
|
4148
|
+
const live = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4149
|
+
if (!live.length) continue;
|
|
4150
|
+
if (hasPaint(node.fill) && live.every((sp) => sp.closed)) {
|
|
4151
|
+
const faces = eraseFromFill(subpathsToBezier(live), _nullishCoalesce(node.fillRule, () => ( "nonzero")), local);
|
|
4152
|
+
if (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
const results = live.map((sp) => eraseFromOutline(sp, local));
|
|
4156
|
+
if (results.every((r) => !r)) continue;
|
|
4157
|
+
const subpaths = live.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
|
|
4158
|
+
plan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });
|
|
4159
|
+
}
|
|
4160
|
+
if (!plan.length) return [];
|
|
4161
|
+
const changed = [];
|
|
4162
|
+
this.change("Erase", () => {
|
|
4163
|
+
for (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));
|
|
4164
|
+
this.anchors = [];
|
|
4165
|
+
this.dropStaleRefs();
|
|
4166
|
+
if (this.selection.length || options.ids) this.selection = [.../* @__PURE__ */ new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];
|
|
4167
|
+
});
|
|
4168
|
+
return changed;
|
|
2032
4169
|
}
|
|
2033
4170
|
/** Ends the pen path being drawn (a lone anchor is removed). */
|
|
2034
4171
|
finishPath() {
|
|
2035
4172
|
if (!this.pen) return;
|
|
2036
4173
|
const node = this.getNode(this.pen.nodeId);
|
|
2037
|
-
const sp = _optionalChain([node, 'optionalAccess',
|
|
4174
|
+
const sp = _optionalChain([node, 'optionalAccess', _86 => _86.subpaths, 'access', _87 => _87[this.pen.subpath]]);
|
|
2038
4175
|
const pen = this.pen;
|
|
2039
4176
|
this.pen = null;
|
|
2040
4177
|
this.pointer = null;
|
|
@@ -2078,7 +4215,7 @@ var VectorEditorController = (_class = class {
|
|
|
2078
4215
|
if (trimmed.startsWith("{")) {
|
|
2079
4216
|
try {
|
|
2080
4217
|
const data = JSON.parse(trimmed);
|
|
2081
|
-
if (_optionalChain([data, 'optionalAccess',
|
|
4218
|
+
if (_optionalChain([data, 'optionalAccess', _88 => _88.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
|
|
2082
4219
|
} catch (e2) {
|
|
2083
4220
|
nodes = null;
|
|
2084
4221
|
}
|
|
@@ -2090,7 +4227,7 @@ var VectorEditorController = (_class = class {
|
|
|
2090
4227
|
}
|
|
2091
4228
|
}
|
|
2092
4229
|
} else nodes = this.clipboard;
|
|
2093
|
-
if (!_optionalChain([nodes, 'optionalAccess',
|
|
4230
|
+
if (!_optionalChain([nodes, 'optionalAccess', _89 => _89.length])) return [];
|
|
2094
4231
|
const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
|
|
2095
4232
|
const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
|
|
2096
4233
|
this.change("Paste", () => {
|
|
@@ -2141,7 +4278,7 @@ var VectorEditorController = (_class = class {
|
|
|
2141
4278
|
hitHandle(screen) {
|
|
2142
4279
|
for (const ref of getVisibleHandles(this.view()).reverse()) {
|
|
2143
4280
|
const a = getAnchorWorld(this.doc, ref);
|
|
2144
|
-
const h = _optionalChain([a, 'optionalAccess',
|
|
4281
|
+
const h = _optionalChain([a, 'optionalAccess', _90 => _90[ref.handle]]);
|
|
2145
4282
|
if (!h) continue;
|
|
2146
4283
|
const s = this.documentToScreen(h);
|
|
2147
4284
|
if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
|
|
@@ -2222,10 +4359,38 @@ var VectorEditorController = (_class = class {
|
|
|
2222
4359
|
const anchor = this.hitAnchor(screen, refs);
|
|
2223
4360
|
return anchor ? { nodeId: anchor.nodeId, anchor } : null;
|
|
2224
4361
|
}
|
|
4362
|
+
case "scissors": {
|
|
4363
|
+
const seg = this.nearestOutline(p);
|
|
4364
|
+
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
4365
|
+
}
|
|
2225
4366
|
default:
|
|
2226
4367
|
return null;
|
|
2227
4368
|
}
|
|
2228
4369
|
}
|
|
4370
|
+
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
4371
|
+
nearestOutline(p) {
|
|
4372
|
+
let best = null;
|
|
4373
|
+
const tol = this.tolerance();
|
|
4374
|
+
const visit = (list, parent) => {
|
|
4375
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
4376
|
+
const n = list[i];
|
|
4377
|
+
if (n.visible === false || n.locked) continue;
|
|
4378
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
4379
|
+
if (n.type === "group") {
|
|
4380
|
+
visit(n.children, m);
|
|
4381
|
+
continue;
|
|
4382
|
+
}
|
|
4383
|
+
const hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));
|
|
4384
|
+
if (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {
|
|
4385
|
+
best = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
};
|
|
4389
|
+
visit(this.doc.nodes, IDENTITY_MATRIX);
|
|
4390
|
+
if (!best) return null;
|
|
4391
|
+
const { distance: _d, ...seg } = best;
|
|
4392
|
+
return seg;
|
|
4393
|
+
}
|
|
2229
4394
|
sameHover(a, b) {
|
|
2230
4395
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
2231
4396
|
}
|
|
@@ -2250,8 +4415,19 @@ var VectorEditorController = (_class = class {
|
|
|
2250
4415
|
return this.downPen(screen, p, input);
|
|
2251
4416
|
case "pencil":
|
|
2252
4417
|
return this.downPencil(p);
|
|
4418
|
+
case "scissors": {
|
|
4419
|
+
const seg = this.nearestOutline(p);
|
|
4420
|
+
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
case "knife":
|
|
4424
|
+
case "eraser":
|
|
4425
|
+
this.drag = { kind: "stroke", tool: this.tool, points: [p], before: this.snapshot() };
|
|
4426
|
+
this.interaction = { type: "stroke", tool: this.tool, points: [p], width: this.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
4427
|
+
this.notify();
|
|
4428
|
+
return;
|
|
2253
4429
|
default:
|
|
2254
|
-
return this.downShape(p);
|
|
4430
|
+
return this.downShape(p, input);
|
|
2255
4431
|
}
|
|
2256
4432
|
}
|
|
2257
4433
|
downSelect(screen, p, input) {
|
|
@@ -2320,7 +4496,7 @@ var VectorEditorController = (_class = class {
|
|
|
2320
4496
|
return;
|
|
2321
4497
|
}
|
|
2322
4498
|
} else if (!selected) this.anchors = [anchor];
|
|
2323
|
-
this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null };
|
|
4499
|
+
this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null, lead: anchor };
|
|
2324
4500
|
this.notify();
|
|
2325
4501
|
return;
|
|
2326
4502
|
}
|
|
@@ -2336,7 +4512,7 @@ var VectorEditorController = (_class = class {
|
|
|
2336
4512
|
const ref = { nodeId: id, subpath: seg.subpath, index };
|
|
2337
4513
|
this.anchors = [ref];
|
|
2338
4514
|
this.editingId = id;
|
|
2339
|
-
this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null };
|
|
4515
|
+
this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null, lead: ref };
|
|
2340
4516
|
this.notify(true);
|
|
2341
4517
|
return;
|
|
2342
4518
|
}
|
|
@@ -2344,7 +4520,7 @@ var VectorEditorController = (_class = class {
|
|
|
2344
4520
|
const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
|
|
2345
4521
|
this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
|
|
2346
4522
|
this.editingId = id;
|
|
2347
|
-
this.drag = { kind: "
|
|
4523
|
+
this.drag = { kind: "bend", ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };
|
|
2348
4524
|
this.notify();
|
|
2349
4525
|
return;
|
|
2350
4526
|
}
|
|
@@ -2354,7 +4530,7 @@ var VectorEditorController = (_class = class {
|
|
|
2354
4530
|
if (!input.shift) this.selection = [path.id];
|
|
2355
4531
|
else if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];
|
|
2356
4532
|
this.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));
|
|
2357
|
-
this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
|
|
4533
|
+
this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };
|
|
2358
4534
|
this.notify();
|
|
2359
4535
|
return;
|
|
2360
4536
|
}
|
|
@@ -2367,7 +4543,7 @@ var VectorEditorController = (_class = class {
|
|
|
2367
4543
|
}
|
|
2368
4544
|
downPen(screen, p, input) {
|
|
2369
4545
|
const before = this.snapshot();
|
|
2370
|
-
const sp = this.pen ? _optionalChain([this, 'access',
|
|
4546
|
+
const sp = this.pen ? _optionalChain([this, 'access', _91 => _91.getNode, 'call', _92 => _92(this.pen.nodeId), 'optionalAccess', _93 => _93.subpaths, 'access', _94 => _94[this.pen.subpath]]) : void 0;
|
|
2371
4547
|
if (this.pen && sp) {
|
|
2372
4548
|
const pen = this.pen;
|
|
2373
4549
|
const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
|
|
@@ -2379,9 +4555,9 @@ var VectorEditorController = (_class = class {
|
|
|
2379
4555
|
this.notify(true);
|
|
2380
4556
|
return;
|
|
2381
4557
|
}
|
|
2382
|
-
let pt2 = this.snapPoint(p);
|
|
2383
4558
|
const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
|
|
2384
|
-
|
|
4559
|
+
const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
|
|
4560
|
+
let pt2 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
|
|
2385
4561
|
pt2 = this.toLocal(pen.nodeId, pt2);
|
|
2386
4562
|
const node2 = this.getNode(pen.nodeId);
|
|
2387
4563
|
const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
|
|
@@ -2412,7 +4588,7 @@ var VectorEditorController = (_class = class {
|
|
|
2412
4588
|
this.notify(this.doc !== before.doc);
|
|
2413
4589
|
return;
|
|
2414
4590
|
}
|
|
2415
|
-
const pt = this.
|
|
4591
|
+
const pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
2416
4592
|
const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
|
|
2417
4593
|
this.setDoc([...this.doc.nodes, node]);
|
|
2418
4594
|
this.pen = { nodeId: node.id, subpath: 0 };
|
|
@@ -2436,17 +4612,20 @@ var VectorEditorController = (_class = class {
|
|
|
2436
4612
|
subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
|
|
2437
4613
|
};
|
|
2438
4614
|
this.setDoc([...this.doc.nodes, node]);
|
|
2439
|
-
|
|
4615
|
+
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
4616
|
+
lazy.push([p.x, p.y]);
|
|
4617
|
+
this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
|
|
2440
4618
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
2441
4619
|
this.notify(true);
|
|
2442
4620
|
}
|
|
2443
|
-
downShape(p) {
|
|
4621
|
+
downShape(p, input) {
|
|
2444
4622
|
const before = this.snapshot();
|
|
2445
|
-
const
|
|
4623
|
+
const targets = this.drawingTargets(null);
|
|
4624
|
+
const start = this.snapDrawn(p, null, input, () => targets);
|
|
2446
4625
|
const kind = this.tool;
|
|
2447
4626
|
const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
|
|
2448
4627
|
this.setDoc([...this.doc.nodes, node]);
|
|
2449
|
-
this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before };
|
|
4628
|
+
this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
|
|
2450
4629
|
this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
|
|
2451
4630
|
this.notify(true);
|
|
2452
4631
|
}
|
|
@@ -2457,9 +4636,11 @@ var VectorEditorController = (_class = class {
|
|
|
2457
4636
|
if (!drag) {
|
|
2458
4637
|
const hover = this.computeHover(screen);
|
|
2459
4638
|
const pointerMatters = this.tool === "pen" && !!this.pen;
|
|
2460
|
-
|
|
4639
|
+
const guides = this.guides;
|
|
2461
4640
|
this.hover = hover;
|
|
2462
|
-
|
|
4641
|
+
const pointer = this.hoverSnap(p, input);
|
|
4642
|
+
if (this.sameHover(hover, this.state.hover) && !pointerMatters && JSON.stringify(guides) === JSON.stringify(this.guides)) return;
|
|
4643
|
+
this.pointer = pointer;
|
|
2463
4644
|
this.notify();
|
|
2464
4645
|
return;
|
|
2465
4646
|
}
|
|
@@ -2480,8 +4661,9 @@ var VectorEditorController = (_class = class {
|
|
|
2480
4661
|
const created = [];
|
|
2481
4662
|
drag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);
|
|
2482
4663
|
this.selection = created;
|
|
4664
|
+
drag.targets = void 0;
|
|
2483
4665
|
}
|
|
2484
|
-
({ dx, dy } = this.
|
|
4666
|
+
({ dx, dy } = this.snapMove(drag, dx, dy, input));
|
|
2485
4667
|
this.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));
|
|
2486
4668
|
this.interaction = { type: "move", dx, dy };
|
|
2487
4669
|
this.notify(true);
|
|
@@ -2490,7 +4672,13 @@ var VectorEditorController = (_class = class {
|
|
|
2490
4672
|
case "scale": {
|
|
2491
4673
|
const angle = drag.frame.angle;
|
|
2492
4674
|
const q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;
|
|
2493
|
-
|
|
4675
|
+
let { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);
|
|
4676
|
+
if (this.snap.objects && !input.mod && drag.handle !== "rotate") {
|
|
4677
|
+
drag.targets ??= VectorSnapTargets.forResize(drag.base, this.selectedRoots());
|
|
4678
|
+
const res = drag.targets.snapScale(drag.frame, drag.handle, sx, sy, input, this.snapTolerance());
|
|
4679
|
+
({ sx, sy } = res);
|
|
4680
|
+
this.setGuides(res);
|
|
4681
|
+
} else this.setGuides(null);
|
|
2494
4682
|
const ids = new Set(this.selectedRoots());
|
|
2495
4683
|
this.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));
|
|
2496
4684
|
this.interaction = { type: "scale", handle: drag.handle, sx, sy };
|
|
@@ -2531,24 +4719,47 @@ var VectorEditorController = (_class = class {
|
|
|
2531
4719
|
let dy = p.y - drag.start.y;
|
|
2532
4720
|
if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
|
|
2533
4721
|
drag.moved = true;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
4722
|
+
const lead = this.leadAnchor(drag);
|
|
4723
|
+
if (lead) {
|
|
4724
|
+
const leave = input.shift ? { at: lead, constrain: "axis", curves: false } : null;
|
|
4725
|
+
const q = this.snapDrawn({ x: lead.x + dx, y: lead.y + dy }, leave, input, () => drag.targets ??= VectorSnapTargets.forAnchorDrag(drag.base, this.anchors));
|
|
4726
|
+
dx = q.x - lead.x;
|
|
4727
|
+
dy = q.y - lead.y;
|
|
4728
|
+
} else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
|
|
4729
|
+
const single = this.anchors.length === 1 ? this.anchors[0] : null;
|
|
4730
|
+
const from = single ? getAnchorWorld(drag.base, single) : void 0;
|
|
4731
|
+
if (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });
|
|
4732
|
+
else this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
|
|
2538
4733
|
this.interaction = { type: "anchors", dx, dy };
|
|
2539
4734
|
this.notify(true);
|
|
2540
4735
|
return;
|
|
2541
4736
|
}
|
|
2542
4737
|
case "handle": {
|
|
2543
4738
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
2544
|
-
|
|
2545
|
-
|
|
4739
|
+
const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
|
|
4740
|
+
const pt = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
|
|
2546
4741
|
const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
|
|
2547
4742
|
this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
|
|
2548
4743
|
this.interaction = { type: "handle", ref: drag.ref };
|
|
2549
4744
|
this.notify(true);
|
|
2550
4745
|
return;
|
|
2551
4746
|
}
|
|
4747
|
+
case "bend": {
|
|
4748
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
4749
|
+
drag.moved = true;
|
|
4750
|
+
this.doc = this.bent(drag.base, drag.ref, drag.t, p);
|
|
4751
|
+
this.interaction = { type: "bend", ref: drag.ref };
|
|
4752
|
+
this.notify(true);
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
case "stroke": {
|
|
4756
|
+
const last = drag.points[drag.points.length - 1];
|
|
4757
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
4758
|
+
drag.points.push(p);
|
|
4759
|
+
this.interaction = { type: "stroke", tool: drag.tool, points: drag.points.slice(), width: drag.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
4760
|
+
this.notify();
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
2552
4763
|
case "pull": {
|
|
2553
4764
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
2554
4765
|
if (!anchor) return;
|
|
@@ -2557,6 +4768,7 @@ var VectorEditorController = (_class = class {
|
|
|
2557
4768
|
this.notify(true);
|
|
2558
4769
|
return;
|
|
2559
4770
|
}
|
|
4771
|
+
this.guides = null;
|
|
2560
4772
|
let pt = p;
|
|
2561
4773
|
if (input.shift) pt = constrain45(anchor, pt);
|
|
2562
4774
|
const side = drag.closing ? "in" : "out";
|
|
@@ -2570,8 +4782,10 @@ var VectorEditorController = (_class = class {
|
|
|
2570
4782
|
}
|
|
2571
4783
|
case "shape": {
|
|
2572
4784
|
const node = this.getNode(drag.nodeId);
|
|
2573
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
2574
|
-
|
|
4785
|
+
if (_optionalChain([node, 'optionalAccess', _95 => _95.type]) !== "path") return;
|
|
4786
|
+
const leave = { at: drag.start, constrain: input.shift ? drag.tool === "line" ? "angle" : "square" : null, curves: false };
|
|
4787
|
+
let end = this.snapDrawn(p, leave, input, () => drag.targets ??= this.drawingTargets(null));
|
|
4788
|
+
if (!this.snappedToObject && drag.tool !== "line" && !input.shift && !input.mod && this.snap.objects && drag.targets) end = this.matchShapeSize(drag.targets, drag.start, end, input.alt);
|
|
2575
4789
|
let start = drag.start;
|
|
2576
4790
|
let bounds;
|
|
2577
4791
|
if (drag.tool === "line") {
|
|
@@ -2599,10 +4813,22 @@ var VectorEditorController = (_class = class {
|
|
|
2599
4813
|
}
|
|
2600
4814
|
case "pencil": {
|
|
2601
4815
|
const last = drag.points[drag.points.length - 1];
|
|
2602
|
-
|
|
2603
|
-
drag.
|
|
4816
|
+
let moved = false;
|
|
4817
|
+
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
4818
|
+
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
4819
|
+
drag.points.push(q);
|
|
4820
|
+
moved = true;
|
|
4821
|
+
}
|
|
4822
|
+
const string = drag.lazy.string();
|
|
2604
4823
|
const node = this.getNode(drag.nodeId);
|
|
2605
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
4824
|
+
if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) !== "path") return;
|
|
4825
|
+
if (!moved) {
|
|
4826
|
+
if (string && _optionalChain([this, 'access', _97 => _97.interaction, 'optionalAccess', _98 => _98.type]) === "draw") {
|
|
4827
|
+
this.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };
|
|
4828
|
+
this.notify();
|
|
4829
|
+
}
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
2606
4832
|
this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
|
|
2607
4833
|
const xs = drag.points.map((q) => q[0]);
|
|
2608
4834
|
const ys = drag.points.map((q) => q[1]);
|
|
@@ -2610,19 +4836,72 @@ var VectorEditorController = (_class = class {
|
|
|
2610
4836
|
type: "draw",
|
|
2611
4837
|
tool: "pencil",
|
|
2612
4838
|
nodeId: drag.nodeId,
|
|
2613
|
-
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }
|
|
4839
|
+
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },
|
|
4840
|
+
...string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] } : {}
|
|
2614
4841
|
};
|
|
2615
4842
|
this.notify(true);
|
|
2616
4843
|
return;
|
|
2617
4844
|
}
|
|
2618
4845
|
}
|
|
2619
4846
|
}
|
|
4847
|
+
/** Snaps a move of the selection by (dx, dy): its bounds line up with targets or space evenly, else go to the grid. */
|
|
4848
|
+
snapMove(drag, dx, dy, input) {
|
|
4849
|
+
const origin = drag.origin;
|
|
4850
|
+
if (input.mod || !origin) {
|
|
4851
|
+
this.setGuides(null);
|
|
4852
|
+
return { dx, dy };
|
|
4853
|
+
}
|
|
4854
|
+
const axes = { x: !input.shift || dx !== 0, y: !input.shift || dy !== 0 };
|
|
4855
|
+
let res = null;
|
|
4856
|
+
if (this.snap.objects) {
|
|
4857
|
+
drag.targets ??= VectorSnapTargets.forMove(drag.base, this.selectedRoots());
|
|
4858
|
+
res = drag.targets.snapRect({ ...origin, x: origin.x + dx, y: origin.y + dy }, this.snapTolerance(), { spacing: true, axes });
|
|
4859
|
+
}
|
|
4860
|
+
this.setGuides(res);
|
|
4861
|
+
return {
|
|
4862
|
+
dx: _optionalChain([res, 'optionalAccess', _99 => _99.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
|
|
4863
|
+
dy: _optionalChain([res, 'optionalAccess', _100 => _100.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
|
|
4864
|
+
};
|
|
4865
|
+
}
|
|
4866
|
+
/** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */
|
|
4867
|
+
leadAnchor(drag) {
|
|
4868
|
+
const ref = drag.lead && this.anchors.some((a) => sameRef(a, drag.lead)) ? drag.lead : null;
|
|
4869
|
+
if (ref) {
|
|
4870
|
+
const a = getAnchorWorld(drag.base, ref);
|
|
4871
|
+
return a ? { x: a.x, y: a.y } : null;
|
|
4872
|
+
}
|
|
4873
|
+
let best = null;
|
|
4874
|
+
for (const r of this.anchors) {
|
|
4875
|
+
const a = getAnchorWorld(drag.base, r);
|
|
4876
|
+
if (a && (!best || Math.hypot(a.x - drag.start.x, a.y - drag.start.y) < Math.hypot(best.x - drag.start.x, best.y - drag.start.y))) best = { x: a.x, y: a.y };
|
|
4877
|
+
}
|
|
4878
|
+
return best;
|
|
4879
|
+
}
|
|
4880
|
+
/** A shape's dragged corner moved so its width or height matches another object's, when one is within reach. */
|
|
4881
|
+
matchShapeSize(targets, start, end, fromCentre = false) {
|
|
4882
|
+
const k = fromCentre ? 2 : 1;
|
|
4883
|
+
const tol = this.snapTolerance() * k;
|
|
4884
|
+
const out = { ...end };
|
|
4885
|
+
const guides = { lines: [], labels: [] };
|
|
4886
|
+
for (const axis of ["x", "y"]) {
|
|
4887
|
+
const d = end[axis] - start[axis];
|
|
4888
|
+
const len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
|
|
4889
|
+
if (len === null || len <= 0) continue;
|
|
4890
|
+
out[axis] = start[axis] + Math.sign(d) * len / k;
|
|
4891
|
+
const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
|
|
4892
|
+
guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
|
|
4893
|
+
guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(len * 100) / 100}` });
|
|
4894
|
+
}
|
|
4895
|
+
this.setGuides(guides);
|
|
4896
|
+
return out;
|
|
4897
|
+
}
|
|
2620
4898
|
/** Primary button released. */
|
|
2621
4899
|
pointerUp(screen, input = {}) {
|
|
2622
4900
|
if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
|
|
2623
4901
|
const drag = this.drag;
|
|
2624
4902
|
this.drag = null;
|
|
2625
4903
|
this.interaction = null;
|
|
4904
|
+
this.guides = null;
|
|
2626
4905
|
if (!drag) return;
|
|
2627
4906
|
switch (drag.kind) {
|
|
2628
4907
|
case "pan":
|
|
@@ -2650,6 +4929,18 @@ var VectorEditorController = (_class = class {
|
|
|
2650
4929
|
return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
|
|
2651
4930
|
case "handle":
|
|
2652
4931
|
return this.commit("Move Handle", drag.before);
|
|
4932
|
+
case "bend":
|
|
4933
|
+
if (!drag.moved) {
|
|
4934
|
+
this.notify();
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4937
|
+
return this.commit("Bend Segment", drag.before);
|
|
4938
|
+
case "stroke": {
|
|
4939
|
+
this.notify();
|
|
4940
|
+
if (drag.tool === "knife") this.knife(drag.points);
|
|
4941
|
+
else this.erase(drag.points);
|
|
4942
|
+
return;
|
|
4943
|
+
}
|
|
2653
4944
|
case "pull":
|
|
2654
4945
|
if (drag.closing) this.pen = null;
|
|
2655
4946
|
return this.commit(drag.label, drag.before);
|
|
@@ -2669,12 +4960,19 @@ var VectorEditorController = (_class = class {
|
|
|
2669
4960
|
}
|
|
2670
4961
|
case "pencil": {
|
|
2671
4962
|
const node = this.getNode(drag.nodeId);
|
|
2672
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
4963
|
+
if (_optionalChain([node, 'optionalAccess', _101 => _101.type]) !== "path" || drag.points.length < 2) {
|
|
2673
4964
|
this.doc = drag.before.doc;
|
|
2674
4965
|
this.notify(true);
|
|
2675
4966
|
return;
|
|
2676
4967
|
}
|
|
2677
|
-
const
|
|
4968
|
+
for (const q of drag.lazy.finish()) drag.points.push(q);
|
|
4969
|
+
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
4970
|
+
if (shape) {
|
|
4971
|
+
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
4972
|
+
this.selection = [drag.nodeId];
|
|
4973
|
+
return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
|
|
4974
|
+
}
|
|
4975
|
+
const fitted = _chunkBGNPT5PEcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
|
|
2678
4976
|
const anchors = [];
|
|
2679
4977
|
for (const c of fitted) {
|
|
2680
4978
|
if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
|
|
@@ -2691,9 +4989,10 @@ var VectorEditorController = (_class = class {
|
|
|
2691
4989
|
/** The pointer left the canvas. */
|
|
2692
4990
|
pointerLeave() {
|
|
2693
4991
|
if (this.drag) return;
|
|
2694
|
-
if (!this.hover && !this.pointer) return;
|
|
4992
|
+
if (!this.hover && !this.pointer && !this.guides) return;
|
|
2695
4993
|
this.hover = null;
|
|
2696
4994
|
this.pointer = null;
|
|
4995
|
+
this.guides = null;
|
|
2697
4996
|
this.notify();
|
|
2698
4997
|
}
|
|
2699
4998
|
/** Abandons the drag in progress (Escape, lost capture). */
|
|
@@ -2702,6 +5001,7 @@ var VectorEditorController = (_class = class {
|
|
|
2702
5001
|
if (!drag) return;
|
|
2703
5002
|
this.drag = null;
|
|
2704
5003
|
this.interaction = null;
|
|
5004
|
+
this.guides = null;
|
|
2705
5005
|
if (drag.kind === "pan") {
|
|
2706
5006
|
this.notify();
|
|
2707
5007
|
return;
|
|
@@ -2734,7 +5034,7 @@ var VectorEditorController = (_class = class {
|
|
|
2734
5034
|
this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
|
|
2735
5035
|
return;
|
|
2736
5036
|
}
|
|
2737
|
-
const id = _nullishCoalesce(_optionalChain([this, 'access',
|
|
5037
|
+
const id = _nullishCoalesce(_optionalChain([this, 'access', _102 => _102.hover, 'optionalAccess', _103 => _103.segment, 'optionalAccess', _104 => _104.nodeId]), () => ( this.editingId));
|
|
2738
5038
|
if (id) {
|
|
2739
5039
|
const node = this.getNode(id);
|
|
2740
5040
|
const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
|
|
@@ -2778,6 +5078,8 @@ var VectorEditorController = (_class = class {
|
|
|
2778
5078
|
case "g":
|
|
2779
5079
|
input.shift ? this.ungroup() : this.group();
|
|
2780
5080
|
return true;
|
|
5081
|
+
case "j":
|
|
5082
|
+
return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
|
|
2781
5083
|
case "]":
|
|
2782
5084
|
this.arrange(input.alt ? "front" : "forward");
|
|
2783
5085
|
return true;
|
|
@@ -2812,7 +5114,7 @@ var VectorEditorController = (_class = class {
|
|
|
2812
5114
|
this.finishPath();
|
|
2813
5115
|
return true;
|
|
2814
5116
|
}
|
|
2815
|
-
if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access',
|
|
5117
|
+
if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _105 => _105.getNode, 'call', _106 => _106(this.selection[0]), 'optionalAccess', _107 => _107.type]) === "path") {
|
|
2816
5118
|
this.tool = "direct";
|
|
2817
5119
|
this.editingId = this.selection[0];
|
|
2818
5120
|
this.notify();
|
|
@@ -2857,7 +5159,23 @@ var VectorEditorController = (_class = class {
|
|
|
2857
5159
|
this.notify();
|
|
2858
5160
|
}
|
|
2859
5161
|
}
|
|
2860
|
-
},
|
|
5162
|
+
}, _class3);
|
|
5163
|
+
function inkShapeLabel(shape) {
|
|
5164
|
+
const name = vectorInkShapeName(shape);
|
|
5165
|
+
return name[0].toUpperCase() + name.slice(1);
|
|
5166
|
+
}
|
|
5167
|
+
function inkShapeSubpath(shape) {
|
|
5168
|
+
switch (shape.kind) {
|
|
5169
|
+
case "line":
|
|
5170
|
+
return lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });
|
|
5171
|
+
case "rect":
|
|
5172
|
+
return rectSubpath(shape);
|
|
5173
|
+
case "ellipse":
|
|
5174
|
+
return ellipseSubpath(shape);
|
|
5175
|
+
case "polygon":
|
|
5176
|
+
return { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
2861
5179
|
function constrain45(origin, p) {
|
|
2862
5180
|
const dx = p.x - origin.x;
|
|
2863
5181
|
const dy = p.y - origin.y;
|
|
@@ -2910,7 +5228,7 @@ function renderNode(node, defs, idPrefix) {
|
|
|
2910
5228
|
strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
|
|
2911
5229
|
...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
|
|
2912
5230
|
...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
|
|
2913
|
-
..._optionalChain([node, 'access',
|
|
5231
|
+
..._optionalChain([node, 'access', _108 => _108.strokeDasharray, 'optionalAccess', _109 => _109.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
|
|
2914
5232
|
fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
|
|
2915
5233
|
opacity: _nullishCoalesce(node.opacity, () => ( 1))
|
|
2916
5234
|
};
|
|
@@ -2934,7 +5252,7 @@ function screenOutline(state, node) {
|
|
|
2934
5252
|
const v = state.viewport;
|
|
2935
5253
|
const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
|
|
2936
5254
|
const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
|
|
2937
|
-
return collectWithMatrix(node, parent).map(({ path, parent: m }) =>
|
|
5255
|
+
return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
|
|
2938
5256
|
}
|
|
2939
5257
|
function collectWithMatrix(node, parent) {
|
|
2940
5258
|
if (node.type === "path") return [{ path: node, parent }];
|
|
@@ -2959,9 +5277,9 @@ function getVectorOverlayRenderData(state) {
|
|
|
2959
5277
|
};
|
|
2960
5278
|
for (const id of state.selection) outline(id, true);
|
|
2961
5279
|
if (state.editingId) outline(state.editingId, true);
|
|
2962
|
-
if (_optionalChain([state, 'access',
|
|
5280
|
+
if (_optionalChain([state, 'access', _110 => _110.hover, 'optionalAccess', _111 => _111.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
|
|
2963
5281
|
let selectionBox = null;
|
|
2964
|
-
if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access',
|
|
5282
|
+
if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _112 => _112.interaction, 'optionalAccess', _113 => _113.type]) !== "marquee") {
|
|
2965
5283
|
const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
|
|
2966
5284
|
selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
|
|
2967
5285
|
}
|
|
@@ -2971,9 +5289,9 @@ function getVectorOverlayRenderData(state) {
|
|
|
2971
5289
|
const a = getAnchorWorld(doc, ref);
|
|
2972
5290
|
const s = toScreen(state, a);
|
|
2973
5291
|
const closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;
|
|
2974
|
-
return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access',
|
|
5292
|
+
return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _114 => _114.hover, 'optionalAccess', _115 => _115.anchor]), ref), closes };
|
|
2975
5293
|
});
|
|
2976
|
-
if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access',
|
|
5294
|
+
if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _116 => _116.hover, 'optionalAccess', _117 => _117.anchor])) {
|
|
2977
5295
|
const a = getAnchorWorld(doc, state.hover.anchor);
|
|
2978
5296
|
if (a) {
|
|
2979
5297
|
const s = toScreen(state, a);
|
|
@@ -2982,15 +5300,15 @@ function getVectorOverlayRenderData(state) {
|
|
|
2982
5300
|
}
|
|
2983
5301
|
const handles = getVisibleHandles(state).flatMap((ref) => {
|
|
2984
5302
|
const a = getAnchorWorld(doc, ref);
|
|
2985
|
-
const h = _optionalChain([a, 'optionalAccess',
|
|
5303
|
+
const h = _optionalChain([a, 'optionalAccess', _118 => _118[ref.handle]]);
|
|
2986
5304
|
if (!a || !h) return [];
|
|
2987
5305
|
const s = toScreen(state, h);
|
|
2988
5306
|
const sa = toScreen(state, a);
|
|
2989
|
-
const hovered = !!_optionalChain([state, 'access',
|
|
5307
|
+
const hovered = !!_optionalChain([state, 'access', _119 => _119.hover, 'optionalAccess', _120 => _120.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
|
|
2990
5308
|
return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
|
|
2991
5309
|
});
|
|
2992
5310
|
let marquee = null;
|
|
2993
|
-
if (_optionalChain([state, 'access',
|
|
5311
|
+
if (_optionalChain([state, 'access', _121 => _121.interaction, 'optionalAccess', _122 => _122.type]) === "marquee") {
|
|
2994
5312
|
const b = state.interaction.bounds;
|
|
2995
5313
|
const tl = toScreen(state, b);
|
|
2996
5314
|
marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
|
|
@@ -2998,7 +5316,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
2998
5316
|
let penPreview = null;
|
|
2999
5317
|
if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
|
|
3000
5318
|
const node = findVectorNode(doc.nodes, state.pen.nodeId);
|
|
3001
|
-
const sp = _optionalChain([node, 'optionalAccess',
|
|
5319
|
+
const sp = _optionalChain([node, 'optionalAccess', _123 => _123.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
|
|
3002
5320
|
const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
|
|
3003
5321
|
if (last) {
|
|
3004
5322
|
const a = toScreen(state, last);
|
|
@@ -3011,18 +5329,47 @@ function getVectorOverlayRenderData(state) {
|
|
|
3011
5329
|
const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
|
|
3012
5330
|
let status = null;
|
|
3013
5331
|
const it = state.interaction;
|
|
3014
|
-
if (_optionalChain([it, 'optionalAccess',
|
|
3015
|
-
else if (_optionalChain([it, 'optionalAccess',
|
|
3016
|
-
else if (_optionalChain([it, 'optionalAccess',
|
|
5332
|
+
if (_optionalChain([it, 'optionalAccess', _124 => _124.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
|
|
5333
|
+
else if (_optionalChain([it, 'optionalAccess', _125 => _125.type]) === "move" || _optionalChain([it, 'optionalAccess', _126 => _126.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
|
|
5334
|
+
else if (_optionalChain([it, 'optionalAccess', _127 => _127.type]) === "scale" || _optionalChain([it, 'optionalAccess', _128 => _128.type]) === "draw") {
|
|
3017
5335
|
const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
|
|
3018
5336
|
if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
|
|
3019
5337
|
}
|
|
3020
|
-
|
|
5338
|
+
const guides = { lines: [], labels: [] };
|
|
5339
|
+
if (state.guides) {
|
|
5340
|
+
for (const l of state.guides.lines) {
|
|
5341
|
+
const a = toScreen(state, { x: l.x1, y: l.y1 });
|
|
5342
|
+
const b = toScreen(state, { x: l.x2, y: l.y2 });
|
|
5343
|
+
guides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });
|
|
5344
|
+
}
|
|
5345
|
+
for (const l of state.guides.labels) {
|
|
5346
|
+
const p = toScreen(state, l);
|
|
5347
|
+
guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
let toolStroke = null;
|
|
5351
|
+
if (_optionalChain([it, 'optionalAccess', _129 => _129.type]) === "stroke") {
|
|
5352
|
+
const pts = it.points.map((q) => toScreen(state, q));
|
|
5353
|
+
const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
|
|
5354
|
+
toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
|
|
5355
|
+
}
|
|
5356
|
+
let pencilString = null;
|
|
5357
|
+
if (_optionalChain([it, 'optionalAccess', _130 => _130.type]) === "draw" && it.string) {
|
|
5358
|
+
const a = toScreen(state, it.string[0]);
|
|
5359
|
+
const b = toScreen(state, it.string[1]);
|
|
5360
|
+
pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
|
|
5361
|
+
}
|
|
5362
|
+
const cutPoint = state.tool === "scissors" && _optionalChain([state, 'access', _131 => _131.hover, 'optionalAccess', _132 => _132.segment]) && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
|
|
5363
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
|
|
3021
5364
|
}
|
|
3022
5365
|
var VECTOR_OVERLAY_THEME = {
|
|
3023
5366
|
accent: "#0ea5e9",
|
|
5367
|
+
/** Smart guide lines and labels. */
|
|
5368
|
+
guide: "#ff3dfc",
|
|
3024
5369
|
handleFill: "#ffffff",
|
|
3025
5370
|
marqueeFill: "rgba(14, 165, 233, 0.08)",
|
|
5371
|
+
/** The eraser's swath while it is dragged. */
|
|
5372
|
+
eraserFill: "rgba(239, 68, 68, 0.25)",
|
|
3026
5373
|
transformHandleSize: 8,
|
|
3027
5374
|
anchorSize: 7,
|
|
3028
5375
|
handleRadius: 3.5
|
|
@@ -3041,7 +5388,7 @@ function isVectorTextTarget(target) {
|
|
|
3041
5388
|
if (!el || typeof el !== "object") return false;
|
|
3042
5389
|
const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
|
|
3043
5390
|
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
3044
|
-
return !!el.isContentEditable || _optionalChain([el, 'access',
|
|
5391
|
+
return !!el.isContentEditable || _optionalChain([el, 'access', _133 => _133.getAttribute, 'optionalCall', _134 => _134("contenteditable")]) === "true";
|
|
3045
5392
|
}
|
|
3046
5393
|
function getVectorSelectionStyle(state) {
|
|
3047
5394
|
const paths = [];
|
|
@@ -3068,12 +5415,39 @@ function vectorPaintToHex(paint) {
|
|
|
3068
5415
|
const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
3069
5416
|
return `#${hex.toLowerCase()}`;
|
|
3070
5417
|
}
|
|
5418
|
+
var VECTOR_PATH_ACTIONS = [
|
|
5419
|
+
{ id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
|
|
5420
|
+
{ id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
|
|
5421
|
+
{ id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
|
|
5422
|
+
{ id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
|
|
5423
|
+
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() }
|
|
5424
|
+
];
|
|
5425
|
+
var VECTOR_EDIT_TOGGLES = [
|
|
5426
|
+
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
5427
|
+
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
5428
|
+
{ key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
|
|
5429
|
+
];
|
|
3071
5430
|
|
|
3072
5431
|
// src/vector/commands.ts
|
|
3073
5432
|
var fail = (message) => {
|
|
3074
5433
|
throw new Error(message);
|
|
3075
5434
|
};
|
|
3076
5435
|
var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
|
|
5436
|
+
var ANCHORS = {
|
|
5437
|
+
type: "array",
|
|
5438
|
+
items: {
|
|
5439
|
+
type: "object",
|
|
5440
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 } },
|
|
5441
|
+
required: ["nodeId", "subpath", "index"]
|
|
5442
|
+
},
|
|
5443
|
+
description: "Anchors as { nodeId, subpath, index } (default: the selected points)."
|
|
5444
|
+
};
|
|
5445
|
+
var POINTS = {
|
|
5446
|
+
type: "array",
|
|
5447
|
+
items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
|
|
5448
|
+
minItems: 1,
|
|
5449
|
+
description: "Polyline [[x, y], \u2026] in document units."
|
|
5450
|
+
};
|
|
3077
5451
|
var PAINT = {
|
|
3078
5452
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
3079
5453
|
};
|
|
@@ -3100,12 +5474,20 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3100
5474
|
const base = { group, ...options.scope ? { scope: options.scope } : {} };
|
|
3101
5475
|
const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
|
|
3102
5476
|
const ids = (params) => {
|
|
3103
|
-
const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess',
|
|
5477
|
+
const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _135 => _135.ids]), () => ( controller.getState().selection));
|
|
3104
5478
|
for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
|
|
3105
5479
|
if (!list2.length) fail("Nothing is selected; pass ids.");
|
|
3106
5480
|
return list2;
|
|
3107
5481
|
};
|
|
3108
|
-
const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess',
|
|
5482
|
+
const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _136 => _136.ids, 'optionalAccess', _137 => _137.length]) ? true : hasSelection();
|
|
5483
|
+
const anchorsOf = (refs) => {
|
|
5484
|
+
if (!refs) return [...controller.getState().selectedAnchors];
|
|
5485
|
+
for (const r of refs) {
|
|
5486
|
+
const node = controller.getNode(r.nodeId);
|
|
5487
|
+
if (_optionalChain([node, 'optionalAccess', _138 => _138.type]) !== "path" || !_optionalChain([node, 'access', _139 => _139.subpaths, 'access', _140 => _140[r.subpath], 'optionalAccess', _141 => _141.anchors, 'access', _142 => _142[r.index]])) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of "${r.nodeId}".`);
|
|
5488
|
+
}
|
|
5489
|
+
return refs;
|
|
5490
|
+
};
|
|
3109
5491
|
const list = () => {
|
|
3110
5492
|
const state = controller.getState();
|
|
3111
5493
|
const out = [];
|
|
@@ -3192,7 +5574,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3192
5574
|
run: (_ctx, params) => {
|
|
3193
5575
|
const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
|
|
3194
5576
|
const node = controller.getNode(id);
|
|
3195
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
5577
|
+
if (_optionalChain([node, 'optionalAccess', _143 => _143.type]) === "path" && !node.subpaths.length) {
|
|
3196
5578
|
controller.undo();
|
|
3197
5579
|
fail("The path data has no points.");
|
|
3198
5580
|
}
|
|
@@ -3325,7 +5707,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3325
5707
|
enabled: enabledFor,
|
|
3326
5708
|
undoable: true,
|
|
3327
5709
|
run: (_ctx, params) => {
|
|
3328
|
-
const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess',
|
|
5710
|
+
const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _144 => _144.name]) });
|
|
3329
5711
|
if (!id) fail("Only nodes that share a parent can be grouped.");
|
|
3330
5712
|
return { id };
|
|
3331
5713
|
}
|
|
@@ -3344,6 +5726,186 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3344
5726
|
return { selection: controller.getState().selection };
|
|
3345
5727
|
}
|
|
3346
5728
|
},
|
|
5729
|
+
{
|
|
5730
|
+
...base,
|
|
5731
|
+
id: `${prefix}deleteAnchors`,
|
|
5732
|
+
label: "Delete Points",
|
|
5733
|
+
keywords: ["remove", "anchor", "point", "node"],
|
|
5734
|
+
description: "Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.",
|
|
5735
|
+
params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
|
|
5736
|
+
undoable: true,
|
|
5737
|
+
run: (_ctx, params) => {
|
|
5738
|
+
check(controller.deleteAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _145 => _145.anchors])), { keepShape: _optionalChain([params, 'optionalAccess', _146 => _146.keepShape]) }));
|
|
5739
|
+
return { ok: true };
|
|
5740
|
+
}
|
|
5741
|
+
},
|
|
5742
|
+
{
|
|
5743
|
+
...base,
|
|
5744
|
+
id: `${prefix}moveAnchor`,
|
|
5745
|
+
label: "Move Point\u2026",
|
|
5746
|
+
keywords: ["anchor", "drag", "curvature"],
|
|
5747
|
+
description: "Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.",
|
|
5748
|
+
params: {
|
|
5749
|
+
type: "object",
|
|
5750
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 }, x: { type: "number" }, y: { type: "number" }, curvature: { type: "boolean" } },
|
|
5751
|
+
required: ["nodeId", "subpath", "index", "x", "y"]
|
|
5752
|
+
},
|
|
5753
|
+
undoable: true,
|
|
5754
|
+
run: (_ctx, params) => {
|
|
5755
|
+
check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
|
|
5756
|
+
return { ok: true };
|
|
5757
|
+
}
|
|
5758
|
+
},
|
|
5759
|
+
{
|
|
5760
|
+
...base,
|
|
5761
|
+
id: `${prefix}bendSegment`,
|
|
5762
|
+
label: "Bend Segment\u2026",
|
|
5763
|
+
keywords: ["curve", "segment", "drag"],
|
|
5764
|
+
description: "Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).",
|
|
5765
|
+
params: {
|
|
5766
|
+
type: "object",
|
|
5767
|
+
properties: {
|
|
5768
|
+
nodeId: { type: "string" },
|
|
5769
|
+
subpath: { type: "integer", minimum: 0 },
|
|
5770
|
+
segment: { type: "integer", minimum: 0 },
|
|
5771
|
+
x: { type: "number" },
|
|
5772
|
+
y: { type: "number" },
|
|
5773
|
+
t: { type: "number", minimum: 0, maximum: 1 }
|
|
5774
|
+
},
|
|
5775
|
+
required: ["nodeId", "subpath", "segment", "x", "y"]
|
|
5776
|
+
},
|
|
5777
|
+
undoable: true,
|
|
5778
|
+
run: (_ctx, params) => {
|
|
5779
|
+
check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
|
|
5780
|
+
return { ok: true };
|
|
5781
|
+
}
|
|
5782
|
+
},
|
|
5783
|
+
{
|
|
5784
|
+
...base,
|
|
5785
|
+
id: `${prefix}join`,
|
|
5786
|
+
label: "Join",
|
|
5787
|
+
shortcut: "Mod+J",
|
|
5788
|
+
keywords: ["connect", "close", "merge", "endpoints"],
|
|
5789
|
+
description: "Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.",
|
|
5790
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
5791
|
+
undoable: true,
|
|
5792
|
+
run: (_ctx, params) => {
|
|
5793
|
+
check(controller.joinPaths({ ids: _optionalChain([params, 'optionalAccess', _147 => _147.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _148 => _148.tolerance]) }));
|
|
5794
|
+
return { selection: controller.getState().selection };
|
|
5795
|
+
}
|
|
5796
|
+
},
|
|
5797
|
+
{
|
|
5798
|
+
...base,
|
|
5799
|
+
id: `${prefix}average`,
|
|
5800
|
+
label: "Average Points\u2026",
|
|
5801
|
+
shortcut: "Mod+Alt+J",
|
|
5802
|
+
keywords: ["align", "points", "anchors", "collapse"],
|
|
5803
|
+
description: "Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).",
|
|
5804
|
+
params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
|
|
5805
|
+
undoable: true,
|
|
5806
|
+
run: (_ctx, params) => {
|
|
5807
|
+
check(controller.averageAnchors(_nullishCoalesce(_optionalChain([params, 'optionalAccess', _149 => _149.axis]), () => ( "both")), anchorsOf(_optionalChain([params, 'optionalAccess', _150 => _150.anchors]))));
|
|
5808
|
+
return { ok: true };
|
|
5809
|
+
}
|
|
5810
|
+
},
|
|
5811
|
+
{
|
|
5812
|
+
...base,
|
|
5813
|
+
id: `${prefix}removeRedundantPoints`,
|
|
5814
|
+
label: "Remove Redundant Points",
|
|
5815
|
+
keywords: ["simplify", "clean up", "anchors", "collinear"],
|
|
5816
|
+
description: "Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).",
|
|
5817
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
5818
|
+
enabled: enabledFor,
|
|
5819
|
+
undoable: true,
|
|
5820
|
+
run: (_ctx, params) => {
|
|
5821
|
+
check(controller.removeRedundantPoints({ ids: _optionalChain([params, 'optionalAccess', _151 => _151.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _152 => _152.tolerance]) }));
|
|
5822
|
+
return { ok: true };
|
|
5823
|
+
}
|
|
5824
|
+
},
|
|
5825
|
+
{
|
|
5826
|
+
...base,
|
|
5827
|
+
id: `${prefix}smooth`,
|
|
5828
|
+
label: "Smooth Path",
|
|
5829
|
+
keywords: ["round", "soften", "handles", "curves"],
|
|
5830
|
+
description: "Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0\u20131, default 1).",
|
|
5831
|
+
params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
|
|
5832
|
+
undoable: true,
|
|
5833
|
+
run: (_ctx, params) => {
|
|
5834
|
+
check(controller.smoothPaths({ ids: _optionalChain([params, 'optionalAccess', _153 => _153.ids]) ? ids(params) : void 0, amount: _optionalChain([params, 'optionalAccess', _154 => _154.amount]), refs: _optionalChain([params, 'optionalAccess', _155 => _155.anchors]) }));
|
|
5835
|
+
return { ok: true };
|
|
5836
|
+
}
|
|
5837
|
+
},
|
|
5838
|
+
{
|
|
5839
|
+
...base,
|
|
5840
|
+
id: `${prefix}cut`,
|
|
5841
|
+
label: "Cut Path (Scissors)\u2026",
|
|
5842
|
+
keywords: ["scissors", "split", "open", "break"],
|
|
5843
|
+
description: "Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.",
|
|
5844
|
+
params: { type: "object", properties: { nodeId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, anchors: ANCHORS } },
|
|
5845
|
+
undoable: true,
|
|
5846
|
+
run: (_ctx, params) => {
|
|
5847
|
+
if (_optionalChain([params, 'optionalAccess', _156 => _156.nodeId]) !== void 0) {
|
|
5848
|
+
if (!controller.getNode(params.nodeId)) fail(`There is no node "${params.nodeId}".`);
|
|
5849
|
+
if (params.x === void 0 || params.y === void 0) fail("Pass x and y with nodeId.");
|
|
5850
|
+
const created = controller.cutPathAt(params.nodeId, { x: params.x, y: params.y });
|
|
5851
|
+
if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
|
|
5852
|
+
return { ids: created };
|
|
5853
|
+
}
|
|
5854
|
+
check(controller.cutAtAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _157 => _157.anchors]))));
|
|
5855
|
+
return { ids: controller.getState().selection };
|
|
5856
|
+
}
|
|
5857
|
+
},
|
|
5858
|
+
{
|
|
5859
|
+
...base,
|
|
5860
|
+
id: `${prefix}knife`,
|
|
5861
|
+
label: "Knife\u2026",
|
|
5862
|
+
keywords: ["slice", "cut", "split", "divide"],
|
|
5863
|
+
description: "Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.",
|
|
5864
|
+
params: { type: "object", properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ["points"] },
|
|
5865
|
+
undoable: true,
|
|
5866
|
+
run: (_ctx, params) => {
|
|
5867
|
+
const created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : void 0 });
|
|
5868
|
+
if (!created.length) fail("The knife did not cross any closed path from edge to edge.");
|
|
5869
|
+
return { ids: created };
|
|
5870
|
+
}
|
|
5871
|
+
},
|
|
5872
|
+
{
|
|
5873
|
+
...base,
|
|
5874
|
+
id: `${prefix}erase`,
|
|
5875
|
+
label: "Erase\u2026",
|
|
5876
|
+
keywords: ["eraser", "rub out", "remove"],
|
|
5877
|
+
description: "Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.",
|
|
5878
|
+
params: { type: "object", properties: { points: POINTS, size: { type: "number", exclusiveMinimum: 0 }, ids: IDS }, required: ["points"] },
|
|
5879
|
+
undoable: true,
|
|
5880
|
+
run: (_ctx, params) => {
|
|
5881
|
+
const changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : void 0 });
|
|
5882
|
+
if (!changed.length) fail("The eraser did not touch any path.");
|
|
5883
|
+
return { ids: changed.filter((id) => controller.getNode(id)) };
|
|
5884
|
+
}
|
|
5885
|
+
},
|
|
5886
|
+
{
|
|
5887
|
+
...base,
|
|
5888
|
+
id: `${prefix}setEditing`,
|
|
5889
|
+
label: "Path Editing Options\u2026",
|
|
5890
|
+
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
5891
|
+
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
|
|
5892
|
+
params: {
|
|
5893
|
+
type: "object",
|
|
5894
|
+
properties: {
|
|
5895
|
+
keepShapeOnDelete: { type: "boolean" },
|
|
5896
|
+
curvatureMoves: { type: "boolean" },
|
|
5897
|
+
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
5898
|
+
recognizeShapes: { type: "boolean" },
|
|
5899
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
5900
|
+
}
|
|
5901
|
+
},
|
|
5902
|
+
run: (_ctx, params) => {
|
|
5903
|
+
const patch = {};
|
|
5904
|
+
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (_optionalChain([params, 'optionalAccess', _158 => _158[key]]) !== void 0) patch[key] = params[key];
|
|
5905
|
+
controller.setEditing(patch);
|
|
5906
|
+
return { editing: controller.getState().editing };
|
|
5907
|
+
}
|
|
5908
|
+
},
|
|
3347
5909
|
{
|
|
3348
5910
|
...base,
|
|
3349
5911
|
id: `${prefix}setTool`,
|
|
@@ -3364,7 +5926,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3364
5926
|
keywords: ["download", "save", "markup"],
|
|
3365
5927
|
description: "Returns the document as SVG markup.",
|
|
3366
5928
|
params: { type: "object", properties: { precision: { type: "integer", minimum: 0, maximum: 8 } } },
|
|
3367
|
-
run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess',
|
|
5929
|
+
run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _159 => _159.precision]) }) })
|
|
3368
5930
|
},
|
|
3369
5931
|
{
|
|
3370
5932
|
...base,
|
|
@@ -3467,5 +6029,34 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3467
6029
|
|
|
3468
6030
|
|
|
3469
6031
|
|
|
3470
|
-
|
|
6032
|
+
|
|
6033
|
+
|
|
6034
|
+
|
|
6035
|
+
|
|
6036
|
+
|
|
6037
|
+
|
|
6038
|
+
|
|
6039
|
+
|
|
6040
|
+
|
|
6041
|
+
|
|
6042
|
+
|
|
6043
|
+
|
|
6044
|
+
|
|
6045
|
+
|
|
6046
|
+
|
|
6047
|
+
|
|
6048
|
+
|
|
6049
|
+
|
|
6050
|
+
|
|
6051
|
+
|
|
6052
|
+
|
|
6053
|
+
|
|
6054
|
+
|
|
6055
|
+
|
|
6056
|
+
|
|
6057
|
+
|
|
6058
|
+
|
|
6059
|
+
|
|
6060
|
+
|
|
6061
|
+
exports.DEFAULT_VECTOR_PATH_EDIT = DEFAULT_VECTOR_PATH_EDIT; exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.LazyMouse = LazyMouse; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; exports.VECTOR_EDIT_TOGGLES = VECTOR_EDIT_TOGGLES; exports.VECTOR_INK_MAX_CORNERS = VECTOR_INK_MAX_CORNERS; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; exports.VECTOR_PATH_ACTIONS = VECTOR_PATH_ACTIONS; exports.VECTOR_PENCIL_STRING_PX = VECTOR_PENCIL_STRING_PX; exports.VECTOR_ROTATE_HANDLE_OFFSET = VECTOR_ROTATE_HANDLE_OFFSET; exports.VECTOR_TOOLS = VECTOR_TOOLS; exports.VECTOR_TOOL_GLYPHS = VECTOR_TOOL_GLYPHS; exports.VECTOR_TOOL_SHORTCUTS = VECTOR_TOOL_SHORTCUTS; exports.VECTOR_TRANSFORM_HANDLES = VECTOR_TRANSFORM_HANDLES; exports.VectorEditorController = VectorEditorController; exports.VectorSnapTargets = VectorSnapTargets; exports.applyMatrix = applyMatrix; exports.applyMatrixLinear = applyMatrixLinear; exports.bendSegment = bendSegment; exports.bezierToSubpaths = bezierToSubpaths; exports.boundsFromPoints = boundsFromPoints; exports.boundsIntersect = boundsIntersect; exports.cloneAnchor = cloneAnchor; exports.cloneSubpath = cloneSubpath; exports.cloneVectorDocument = cloneVectorDocument; exports.cloneVectorNode = cloneVectorNode; exports.collectPathNodes = collectPathNodes; exports.constrainToLeave = constrainToLeave; exports.convertAnchor = convertAnchor; exports.createVectorCommands = createVectorCommands; exports.curvatureMoveAnchor = curvatureMoveAnchor; exports.cutSubpathAt = cutSubpathAt; exports.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; exports.eraseFromFill = eraseFromFill; exports.eraseFromOutline = eraseFromOutline; exports.eraserRegion = eraserRegion; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; exports.getVectorSelectionBoxGeometry = getVectorSelectionBoxGeometry; exports.getVectorSelectionFrame = getVectorSelectionFrame; exports.getVectorSelectionStyle = getVectorSelectionStyle; exports.getVisibleAnchors = getVisibleAnchors; exports.getVisibleHandles = getVisibleHandles; exports.hasPaint = hasPaint; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isIdentityMatrix = isIdentityMatrix; exports.isVectorTextTarget = isVectorTextTarget; exports.joinSubpaths = joinSubpaths; exports.knifePath = knifePath; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.matrixRotation = matrixRotation; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; exports.nodeMatrix = nodeMatrix; exports.parseSvgTransform = parseSvgTransform; exports.parseVectorSvg = parseVectorSvg; exports.polygonSubpath = polygonSubpath; exports.polylineBounds = polylineBounds; exports.recognizeInkShape = recognizeInkShape; exports.rectSubpath = rectSubpath; exports.removeAnchorKeepingShape = removeAnchorKeepingShape; exports.removeAnchors = removeAnchors; exports.removeAnchorsKeepingShape = removeAnchorsKeepingShape; exports.removeRedundantPoints = removeRedundantPoints; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.simplifyPolyline = simplifyPolyline; exports.smoothSubpath = smoothSubpath; exports.splitIntoFaces = splitIntoFaces; exports.splitSubpathAt = splitSubpathAt; exports.starSubpath = starSubpath; exports.subpathPointAt = subpathPointAt; exports.subpathSegment = subpathSegment; exports.subpathSegmentCount = subpathSegmentCount; exports.subpathsToBezier = subpathsToBezier; exports.toVectorKeyInput = toVectorKeyInput; exports.toVectorModifiers = toVectorModifiers; exports.toVectorPointerInput = toVectorPointerInput; exports.transformVectorNode = transformVectorNode; exports.translateAnchor = translateAnchor; exports.translateMatrix = translateMatrix; exports.unionBounds = unionBounds; exports.vectorDocumentToSvg = vectorDocumentToSvg; exports.vectorInkShapeName = vectorInkShapeName; exports.vectorNodeBounds = vectorNodeBounds; exports.vectorNodeRotation = vectorNodeRotation; exports.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorParentMatrix = vectorParentMatrix; exports.vectorPathData = vectorPathData; exports.vectorPathWorldBezier = vectorPathWorldBezier; exports.vectorResizeCursor = vectorResizeCursor; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
|
|
3471
6062
|
//# sourceMappingURL=index.cjs.map
|