@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.js
CHANGED
|
@@ -4,21 +4,33 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
bezierPathBounds,
|
|
6
6
|
bezierPathToSvg,
|
|
7
|
+
closeContour,
|
|
8
|
+
contourSignedArea,
|
|
9
|
+
contoursToPath,
|
|
7
10
|
distanceToBezierPath,
|
|
11
|
+
fitCubicWithTangents,
|
|
8
12
|
fitCubics,
|
|
9
13
|
formatNumber,
|
|
10
14
|
gradientToSvgDef,
|
|
15
|
+
normalizeBezierPath,
|
|
16
|
+
outlineStroke,
|
|
11
17
|
pathBoolean,
|
|
18
|
+
pathToContours,
|
|
12
19
|
pointInBezierPath,
|
|
20
|
+
segmentBounds,
|
|
21
|
+
segmentDerivative,
|
|
22
|
+
segmentIntersections,
|
|
13
23
|
segmentNearest,
|
|
14
24
|
segmentPoint,
|
|
15
25
|
splitSegment,
|
|
26
|
+
subSegment,
|
|
16
27
|
toBezierPath,
|
|
17
|
-
transformBezierPath
|
|
18
|
-
|
|
28
|
+
transformBezierPath,
|
|
29
|
+
windingNumber
|
|
30
|
+
} from "../chunk-EGPIVUEF.js";
|
|
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
|
|
@@ -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 (!leave?.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 _VectorSnapTargets {
|
|
933
|
+
points = [];
|
|
934
|
+
xs = [];
|
|
935
|
+
ys = [];
|
|
936
|
+
segments = [];
|
|
937
|
+
/** Bounds of the objects a moved box spaces itself evenly with. */
|
|
938
|
+
boxes = [];
|
|
939
|
+
/** Side lengths of the other objects (in their own frames), which a resize may match. */
|
|
940
|
+
sizes = [];
|
|
941
|
+
constructor() {
|
|
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 = 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) => !!moving.get(id)?.has(`${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", f?.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 (leave?.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 (leave?.angles?.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 = segmentNearest(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(segmentPoint(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 segmentIntersections(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: options.axes?.x !== false, y: options.axes?.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: bx?.d ?? 0, dy: by?.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: hx?.v ?? null, ty: hy?.v ?? null, s: { hx, hy, on: null } });
|
|
1420
|
+
tries.push({ tx: hx?.v ?? null, ty: null, s: { hx, hy: null, on: null } });
|
|
1421
|
+
tries.push({ tx: null, ty: hy?.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: tx ?? p.x, y: 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
|
+
};
|
|
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 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 = segmentPoint(seg, t);
|
|
1572
|
+
return cond([p[0] - b[0], p[1] - b[1]], segmentDerivative(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 = segmentPoint(seg, t);
|
|
1579
|
+
const d = segmentDerivative(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 = segmentPoint(seg, t);
|
|
1607
|
+
const dv = segmentDerivative(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(segmentPoint(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 fitCubicWithTangents(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] = segmentPoint(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: 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: 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 = p.in ?? p;
|
|
1913
|
+
const hout = 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 segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/vector/pathCut.ts
|
|
2009
|
+
var loopArea = (l) => contourSignedArea(l.segs);
|
|
2010
|
+
function loopSample(l) {
|
|
2011
|
+
const k = l.chord.findIndex((c) => !c);
|
|
2012
|
+
return segmentPoint(l.segs[k >= 0 ? k : 0], 0.5);
|
|
2013
|
+
}
|
|
2014
|
+
var contains2 = (l, p) => windingNumber([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 contoursToPath(face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
2031
|
+
}
|
|
2032
|
+
function facesOf(path, fillRule) {
|
|
2033
|
+
const normalized = normalizeBezierPath(path, fillRule);
|
|
2034
|
+
const loops = pathToContours(normalized).map((c) => {
|
|
2035
|
+
const segs = closeContour(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 = segmentPoint(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() : subSegment(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 = bezierPathBounds(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 = segmentBounds(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 windingNumber(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 outlineStroke(line2, { width, cap: "round", join: "round" });
|
|
2233
|
+
}
|
|
2234
|
+
function eraseFromFill(path, fillRule, region) {
|
|
2235
|
+
const overlap = pathBoolean("intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2236
|
+
if (!pathToContours(overlap).some((c) => Math.abs(contourSignedArea(closeContour(c, true))) > 1e-9)) return null;
|
|
2237
|
+
const rest = pathBoolean("difference", [{ path, fillRule }, { path: region }]).path;
|
|
2238
|
+
return splitIntoFaces(rest);
|
|
2239
|
+
}
|
|
2240
|
+
function eraseFromOutline(sp, region) {
|
|
2241
|
+
const regionSegs = pathToContours(region).flatMap((c) => closeContour(c, true));
|
|
2242
|
+
if (!regionSegs.length) return null;
|
|
2243
|
+
const rb = bezierPathBounds(region);
|
|
2244
|
+
const count = subpathSegmentCount(sp);
|
|
2245
|
+
if (!rb || count === 0) return null;
|
|
2246
|
+
const removed = (p) => pointInBezierPath(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 = segmentBounds(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 = segmentBounds(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 segmentIntersections(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(segmentPoint(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 = subSegment(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 = class {
|
|
2350
|
+
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
2351
|
+
constructor(length2) {
|
|
2352
|
+
this.length = length2;
|
|
2353
|
+
}
|
|
2354
|
+
length;
|
|
2355
|
+
pos = null;
|
|
2356
|
+
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
|
+
};
|
|
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 = 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",
|
|
@@ -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,6 +2777,28 @@ 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)}`;
|
|
@@ -1057,11 +2813,17 @@ var VectorEditorController = class {
|
|
|
1057
2813
|
defaults;
|
|
1058
2814
|
viewport;
|
|
1059
2815
|
snap;
|
|
2816
|
+
editing;
|
|
1060
2817
|
hitRadius;
|
|
1061
2818
|
hover = null;
|
|
1062
2819
|
pointer = null;
|
|
1063
2820
|
drag = null;
|
|
1064
2821
|
interaction = null;
|
|
2822
|
+
guides = null;
|
|
2823
|
+
/** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
|
|
2824
|
+
snappedToObject = false;
|
|
2825
|
+
/** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
|
|
2826
|
+
hoverTargets = null;
|
|
1065
2827
|
spaceHeld = false;
|
|
1066
2828
|
clipboard = null;
|
|
1067
2829
|
createId;
|
|
@@ -1079,7 +2841,8 @@ var VectorEditorController = class {
|
|
|
1079
2841
|
this.tool = options.tool ?? "select";
|
|
1080
2842
|
this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
|
|
1081
2843
|
this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
|
|
1082
|
-
this.snap = {
|
|
2844
|
+
this.snap = { ...DEFAULT_VECTOR_SNAP, ...options.snap ?? {} };
|
|
2845
|
+
this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...options.editing ?? {} };
|
|
1083
2846
|
this.hitRadius = options.hitRadius ?? 6;
|
|
1084
2847
|
this.createId = options.createId ?? defaultCreateId;
|
|
1085
2848
|
this.onChange = options.onChange;
|
|
@@ -1128,6 +2891,8 @@ var VectorEditorController = 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(),
|
|
@@ -1312,6 +3077,7 @@ var VectorEditorController = 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 {
|
|
|
1324
3090
|
}
|
|
1325
3091
|
setSnap(snap) {
|
|
1326
3092
|
this.snap = { ...this.snap, ...snap };
|
|
3093
|
+
if (!this.snap.guides || !this.snap.objects) this.guides = null;
|
|
3094
|
+
this.notify();
|
|
3095
|
+
}
|
|
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;
|
|
1327
3102
|
this.notify();
|
|
1328
3103
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
return { x: Math.round(p.x / g) * g, y: Math.round(p.y / g) * g };
|
|
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 leave?.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 = 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 ? this.getNode(this.pen.nodeId)?.subpaths[this.pen.subpath] : void 0;
|
|
3158
|
+
if (this.pen && sp && this.hover?.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(this.pen?.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
|
|
@@ -1936,12 +3766,47 @@ var VectorEditorController = 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 = 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 (node?.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 (node?.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 = node?.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
|
}
|
|
@@ -1984,15 +3849,24 @@ var VectorEditorController = class {
|
|
|
1984
3849
|
});
|
|
1985
3850
|
return ref;
|
|
1986
3851
|
}
|
|
1987
|
-
/**
|
|
1988
|
-
|
|
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 = {}) {
|
|
1989
3858
|
if (!refs.length) return { ok: false, message: "Select points to delete." };
|
|
3859
|
+
const keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;
|
|
1990
3860
|
this.change("Delete Points", () => {
|
|
1991
3861
|
const nodes = mapTree(this.doc.nodes, (n) => {
|
|
1992
|
-
if (n.type !== "path") return n;
|
|
3862
|
+
if (n.type !== "path" || n.locked) return n;
|
|
1993
3863
|
const mine = refs.filter((r) => r.nodeId === n.id);
|
|
1994
3864
|
if (!mine.length) return n;
|
|
1995
|
-
const subpaths = n.subpaths.map((sp, s) =>
|
|
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);
|
|
1996
3870
|
return subpaths.length ? { ...n, subpaths } : null;
|
|
1997
3871
|
});
|
|
1998
3872
|
this.setDoc(nodes);
|
|
@@ -2030,6 +3904,269 @@ var VectorEditorController = class {
|
|
|
2030
3904
|
});
|
|
2031
3905
|
return { ok: true };
|
|
2032
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 = 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, 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 = 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 = options.refs ?? (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null);
|
|
4016
|
+
const ids = 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, 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) => 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) {
|
|
4037
|
+
const node = this.getNode(nodeId);
|
|
4038
|
+
if (node?.type !== "path" || node.locked) return null;
|
|
4039
|
+
const local = this.toLocal(nodeId, point);
|
|
4040
|
+
const hit = nearestSegment(node, local);
|
|
4041
|
+
if (!hit) return null;
|
|
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();
|
|
4055
|
+
});
|
|
4056
|
+
return ids;
|
|
4057
|
+
}
|
|
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, [...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;
|
|
4076
|
+
this.anchors = [];
|
|
4077
|
+
this.dropStaleRefs();
|
|
4078
|
+
});
|
|
4079
|
+
return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
|
|
4080
|
+
}
|
|
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);
|
|
4086
|
+
});
|
|
4087
|
+
const visible = (p) => !blocked.has(p.id);
|
|
4088
|
+
if (ids?.length || this.selection.length) return this.pathTargets(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, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);
|
|
4095
|
+
if (b && boundsIntersect(b, box)) out.push(n);
|
|
4096
|
+
});
|
|
4097
|
+
return out;
|
|
4098
|
+
}
|
|
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), 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;
|
|
4125
|
+
this.anchors = [];
|
|
4126
|
+
this.dropStaleRefs();
|
|
4127
|
+
});
|
|
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 = 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 = bezierPathBounds(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 = transformBezierPath(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), 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) => 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;
|
|
4169
|
+
}
|
|
2033
4170
|
/** Ends the pen path being drawn (a lone anchor is removed). */
|
|
2034
4171
|
finishPath() {
|
|
2035
4172
|
if (!this.pen) return;
|
|
@@ -2222,10 +4359,38 @@ var VectorEditorController = 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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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
|
}
|
|
@@ -2379,9 +4555,9 @@ var VectorEditorController = 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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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 {
|
|
|
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";
|
|
@@ -2571,7 +4783,9 @@ var VectorEditorController = class {
|
|
|
2571
4783
|
case "shape": {
|
|
2572
4784
|
const node = this.getNode(drag.nodeId);
|
|
2573
4785
|
if (node?.type !== "path") return;
|
|
2574
|
-
|
|
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 {
|
|
|
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
4824
|
if (node?.type !== "path") return;
|
|
4825
|
+
if (!moved) {
|
|
4826
|
+
if (string && this.interaction?.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 {
|
|
|
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: res?.x ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
|
|
4863
|
+
dy: res?.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 {
|
|
|
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);
|
|
@@ -2674,6 +4965,13 @@ var VectorEditorController = class {
|
|
|
2674
4965
|
this.notify(true);
|
|
2675
4966
|
return;
|
|
2676
4967
|
}
|
|
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
|
+
}
|
|
2677
4975
|
const fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
|
|
2678
4976
|
const anchors = [];
|
|
2679
4977
|
for (const c of fitted) {
|
|
@@ -2691,9 +4989,10 @@ var VectorEditorController = 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 {
|
|
|
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;
|
|
@@ -2778,6 +5078,8 @@ var VectorEditorController = 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;
|
|
@@ -2858,6 +5160,22 @@ var VectorEditorController = class {
|
|
|
2858
5160
|
}
|
|
2859
5161
|
}
|
|
2860
5162
|
};
|
|
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;
|
|
@@ -3017,12 +5335,41 @@ function getVectorOverlayRenderData(state) {
|
|
|
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 (it?.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 (it?.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" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
|
|
5363
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
|
|
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
|
|
@@ -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
|
};
|
|
@@ -3106,6 +5480,14 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3106
5480
|
return list2;
|
|
3107
5481
|
};
|
|
3108
5482
|
const enabledFor = (_, params) => params?.ids?.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 (node?.type !== "path" || !node.subpaths[r.subpath]?.anchors[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 = [];
|
|
@@ -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(params?.anchors), { keepShape: params?.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: params?.ids ? ids(params) : void 0, tolerance: params?.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(params?.axis ?? "both", anchorsOf(params?.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: params?.ids ? ids(params) : void 0, tolerance: params?.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: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.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 (params?.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(params?.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 (params?.[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`,
|
|
@@ -3382,18 +5944,28 @@ function createVectorCommands(controller, options = {}) {
|
|
|
3382
5944
|
];
|
|
3383
5945
|
}
|
|
3384
5946
|
export {
|
|
5947
|
+
DEFAULT_VECTOR_PATH_EDIT,
|
|
5948
|
+
DEFAULT_VECTOR_SNAP,
|
|
3385
5949
|
IDENTITY_MATRIX,
|
|
3386
5950
|
KAPPA,
|
|
5951
|
+
LazyMouse,
|
|
3387
5952
|
VECTOR_CLIPBOARD_TYPE,
|
|
5953
|
+
VECTOR_CONSTRUCTION_ANGLES,
|
|
5954
|
+
VECTOR_EDIT_TOGGLES,
|
|
5955
|
+
VECTOR_INK_MAX_CORNERS,
|
|
3388
5956
|
VECTOR_OVERLAY_THEME,
|
|
5957
|
+
VECTOR_PATH_ACTIONS,
|
|
5958
|
+
VECTOR_PENCIL_STRING_PX,
|
|
3389
5959
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
3390
5960
|
VECTOR_TOOLS,
|
|
3391
5961
|
VECTOR_TOOL_GLYPHS,
|
|
3392
5962
|
VECTOR_TOOL_SHORTCUTS,
|
|
3393
5963
|
VECTOR_TRANSFORM_HANDLES,
|
|
3394
5964
|
VectorEditorController,
|
|
5965
|
+
VectorSnapTargets,
|
|
3395
5966
|
applyMatrix,
|
|
3396
5967
|
applyMatrixLinear,
|
|
5968
|
+
bendSegment,
|
|
3397
5969
|
bezierToSubpaths,
|
|
3398
5970
|
boundsFromPoints,
|
|
3399
5971
|
boundsIntersect,
|
|
@@ -3402,10 +5974,16 @@ export {
|
|
|
3402
5974
|
cloneVectorDocument,
|
|
3403
5975
|
cloneVectorNode,
|
|
3404
5976
|
collectPathNodes,
|
|
5977
|
+
constrainToLeave,
|
|
3405
5978
|
convertAnchor,
|
|
3406
5979
|
createVectorCommands,
|
|
5980
|
+
curvatureMoveAnchor,
|
|
5981
|
+
cutSubpathAt,
|
|
3407
5982
|
decomposeRigid,
|
|
3408
5983
|
ellipseSubpath,
|
|
5984
|
+
eraseFromFill,
|
|
5985
|
+
eraseFromOutline,
|
|
5986
|
+
eraserRegion,
|
|
3409
5987
|
findVectorNode,
|
|
3410
5988
|
getAnchor,
|
|
3411
5989
|
getAnchorWorld,
|
|
@@ -3426,6 +6004,8 @@ export {
|
|
|
3426
6004
|
invertMatrix,
|
|
3427
6005
|
isIdentityMatrix,
|
|
3428
6006
|
isVectorTextTarget,
|
|
6007
|
+
joinSubpaths,
|
|
6008
|
+
knifePath,
|
|
3429
6009
|
lineSubpath,
|
|
3430
6010
|
locateVectorNode,
|
|
3431
6011
|
matrixRotation,
|
|
@@ -3436,14 +6016,24 @@ export {
|
|
|
3436
6016
|
parseSvgTransform,
|
|
3437
6017
|
parseVectorSvg,
|
|
3438
6018
|
polygonSubpath,
|
|
6019
|
+
polylineBounds,
|
|
6020
|
+
recognizeInkShape,
|
|
3439
6021
|
rectSubpath,
|
|
6022
|
+
removeAnchorKeepingShape,
|
|
3440
6023
|
removeAnchors,
|
|
6024
|
+
removeAnchorsKeepingShape,
|
|
6025
|
+
removeRedundantPoints,
|
|
3441
6026
|
reverseSubpath,
|
|
3442
6027
|
rotateMatrix,
|
|
3443
6028
|
roundSubpaths,
|
|
3444
6029
|
scaleFor,
|
|
3445
6030
|
scaleMatrix,
|
|
6031
|
+
simplifyPolyline,
|
|
6032
|
+
smoothSubpath,
|
|
6033
|
+
splitIntoFaces,
|
|
6034
|
+
splitSubpathAt,
|
|
3446
6035
|
starSubpath,
|
|
6036
|
+
subpathPointAt,
|
|
3447
6037
|
subpathSegment,
|
|
3448
6038
|
subpathSegmentCount,
|
|
3449
6039
|
subpathsToBezier,
|
|
@@ -3455,6 +6045,7 @@ export {
|
|
|
3455
6045
|
translateMatrix,
|
|
3456
6046
|
unionBounds,
|
|
3457
6047
|
vectorDocumentToSvg,
|
|
6048
|
+
vectorInkShapeName,
|
|
3458
6049
|
vectorNodeBounds,
|
|
3459
6050
|
vectorNodeRotation,
|
|
3460
6051
|
vectorNodeToBezier,
|