Files
moteva/frontend/patches/leafer-x-path-editor@1.1.3.patch
T

1964 lines
137 KiB
Diff
Raw Normal View History

diff --git a/dist/index.cjs b/dist/index.cjs
index f7a34bfc04a4661ee493c1e8fcb6c0356cdcf530..0fea36da222e9fe024a14b3a4d46b8a6c50a59da 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -36,12 +36,15 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
const { M, L, C, Q, Z } = core.PathCommandMap;
function pathData2Point(path) {
const pointData = [];
+ let subpathStartIndex = -1;
for (let i = 0; i < path.length; i++) {
const point = {};
if (path[i] === M) {
point.x = path[++i];
point.y = path[++i];
point.type = 'start';
+ point.move = true;
+ subpathStartIndex = pointData.length;
}
else if (path[i] === L) {
point.x = path[++i];
@@ -62,63 +65,84 @@ function pathData2Point(path) {
point.y = path[++i];
}
else if (path[i] === Z) {
- point.x = pointData[0].x;
- point.y = pointData[0].y;
+ const start = pointData[subpathStartIndex];
+ const end = pointData[pointData.length - 1];
+ if (!start || !end)
+ continue;
+ if (pointData.length - 1 > subpathStartIndex &&
+ start.x === end.x &&
+ start.y === end.y) {
+ pointData.pop();
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
+ }
+ }
+ pointData[pointData.length - 1].closed = true;
+ continue;
}
pointData.push(point);
}
- const head = pointData[0];
- const tail = pointData[pointData.length - 1];
- if (tail.type !== 'end') {
- if (head.x === tail.x && head.y === tail.y) {
- pointData.pop();
- if (tail.x2 !== undefined && tail.x2 !== undefined) {
- pointData[pointData.length - 1].x2 = tail.x2;
- pointData[pointData.length - 1].y2 = tail.y2;
- }
- if (tail.x1 !== undefined && tail.y1 !== undefined) {
- pointData[0].x1 = tail.x1;
- pointData[0].y1 = tail.y1;
+ for (let endIndex = pointData.length - 1; endIndex > 0;) {
+ let startIndex = endIndex;
+ while (startIndex > 0 && !pointData[startIndex].move)
+ startIndex--;
+ const start = pointData[startIndex];
+ const end = pointData[endIndex];
+ if (endIndex > startIndex &&
+ !end.closed &&
+ start.x === end.x &&
+ start.y === end.y) {
+ pointData.splice(endIndex, 1);
+ const newEnd = pointData[endIndex - 1];
+ newEnd.closed = true;
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
}
}
+ endIndex = startIndex - 1;
}
return pointData;
}
function point2PathData(points) {
const pathData = [];
- const getNext = (index) => {
- if (index === points.length - 1) {
- return points[0];
+ if (!points.length)
+ return pathData;
+ const pushSegment = (prev, next) => {
+ const { x, y, x1, y1 } = next;
+ if (prev.x2 === undefined &&
+ prev.y2 === undefined &&
+ x1 === undefined &&
+ y1 === undefined) {
+ pathData.push(L, x, y);
+ }
+ else if (prev.x2 !== undefined &&
+ prev.y2 !== undefined &&
+ x1 !== undefined &&
+ y1 !== undefined) {
+ pathData.push(C, prev.x2, prev.y2, x1, y1, x, y);
+ }
+ else if (prev.x2 !== undefined && prev.y2 !== undefined) {
+ pathData.push(Q, prev.x2, prev.y2, x, y);
+ }
+ else if (x1 !== undefined && y1 !== undefined) {
+ pathData.push(Q, x1, y1, x, y);
}
- return points[index + 1];
};
- pathData.push(M, points[0].x, points[0].y);
+ let subpathStart = points[0];
for (let i = 0; i < points.length; i++) {
- const prev = points[i];
- const next = getNext(i);
- const { type, x, y, x1, y1 } = next;
- if (type === 'end') {
- continue;
+ const point = points[i];
+ if (i === 0 || point.move || point.type === 'start') {
+ pathData.push(M, point.x, point.y);
+ subpathStart = point;
}
else {
- if (prev.x2 === undefined &&
- prev.y2 === undefined &&
- x1 === undefined &&
- y1 === undefined) {
- pathData.push(L, x, y);
- }
- else if (prev.x2 !== undefined &&
- prev.y2 !== undefined &&
- x1 !== undefined &&
- y1 !== undefined) {
- pathData.push(C, prev.x2 || 0, prev.y2 || 0, x1, y1, x, y);
- }
- else if (prev.x2 !== undefined && prev.y2 !== undefined) {
- pathData.push(Q, prev.x2 || 0, prev.y2 || 0, x, y);
- }
- else if (x1 !== undefined && y1 !== undefined) {
- pathData.push(Q, x1, y1, x, y);
- }
+ pushSegment(points[i - 1], point);
+ }
+ if (point.closed) {
+ pushSegment(point, subpathStart);
+ pathData.push(Z);
}
}
return pathData;
@@ -138,6 +162,134 @@ function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
+const hasPoint = (x, y) => x !== undefined && y !== undefined;
+const lerp = (from, to, t) => ({
+ x: from.x + (to.x - from.x) * t,
+ y: from.y + (to.y - from.y) * t,
+});
+const distanceSquared = (left, right) => Math.pow(left.x - right.x, 2) + Math.pow(left.y - right.y, 2);
+function getSegmentPoints(points, segment) {
+ const from = points[segment.fromIndex];
+ const to = points[segment.toIndex];
+ const fromCoordinate = { x: from.x, y: from.y };
+ const toCoordinate = { x: to.x, y: to.y };
+ const outgoing = hasPoint(from.x2, from.y2)
+ ? { x: from.x2, y: from.y2 }
+ : undefined;
+ const incoming = hasPoint(to.x1, to.y1)
+ ? { x: to.x1, y: to.y1 }
+ : undefined;
+ return { from, to, fromCoordinate, toCoordinate, outgoing, incoming };
+}
+function getPathSegmentPoint(points, segment, t) {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ return lerp(lerp(left, center, t), lerp(center, right, t), t);
+ }
+ const control = outgoing || incoming;
+ if (control) {
+ return lerp(lerp(fromCoordinate, control, t), lerp(control, toCoordinate, t), t);
+ }
+ return lerp(fromCoordinate, toCoordinate, t);
+}
+function getClosestSegmentLocation(points, segment, target) {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (!outgoing && !incoming) {
+ const dx = toCoordinate.x - fromCoordinate.x;
+ const dy = toCoordinate.y - fromCoordinate.y;
+ const lengthSquared = dx * dx + dy * dy;
+ const t = lengthSquared
+ ? Math.max(0, Math.min(1, ((target.x - fromCoordinate.x) * dx + (target.y - fromCoordinate.y) * dy) / lengthSquared))
+ : 0;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+ }
+ const samples = 40;
+ let bestT = 0;
+ let bestDistance = Number.POSITIVE_INFINITY;
+ for (let index = 0; index <= samples; index++) {
+ const t = index / samples;
+ const distance = distanceSquared(getPathSegmentPoint(points, segment, t), target);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestT = t;
+ }
+ }
+ let left = Math.max(0, bestT - 1 / samples);
+ let right = Math.min(1, bestT + 1 / samples);
+ for (let index = 0; index < 12; index++) {
+ const leftThird = left + (right - left) / 3;
+ const rightThird = right - (right - left) / 3;
+ if (distanceSquared(getPathSegmentPoint(points, segment, leftThird), target) <=
+ distanceSquared(getPathSegmentPoint(points, segment, rightThird), target)) {
+ right = rightThird;
+ }
+ else {
+ left = leftThird;
+ }
+ }
+ const t = (left + right) / 2;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+}
+function splitPathSegment(points, segment, rawT) {
+ const nextPoints = points.map((point) => (Object.assign({}, point)));
+ const t = Math.max(0.001, Math.min(0.999, rawT));
+ const { from, to, fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(nextPoints, segment);
+ let newPoint;
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ const leftCenter = lerp(left, center, t);
+ const rightCenter = lerp(center, right, t);
+ const point = lerp(leftCenter, rightCenter, t);
+ from.x2 = left.x;
+ from.y2 = left.y;
+ to.x1 = right.x;
+ to.y1 = right.y;
+ newPoint = Object.assign(Object.assign({}, point), { x1: leftCenter.x, y1: leftCenter.y, x2: rightCenter.x, y2: rightCenter.y, mode: 'no-mirror' });
+ }
+ else if (outgoing || incoming) {
+ const control = outgoing || incoming;
+ const left = lerp(fromCoordinate, control, t);
+ const right = lerp(control, toCoordinate, t);
+ const point = lerp(left, right, t);
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ newPoint = Object.assign(Object.assign({}, point), { x1: left.x, y1: left.y, x2: right.x, y2: right.y, mode: 'no-mirror' });
+ }
+ else {
+ newPoint = getPathSegmentPoint(nextPoints, segment, t);
+ }
+ if (segment.closing) {
+ from.closed = false;
+ newPoint.closed = true;
+ }
+ nextPoints.splice(segment.insertIndex, 0, newPoint);
+ return nextPoints;
+}
+function deletePathSegment(points, segment) {
+ const nextPoints = points.map((point) => (Object.assign({}, point)));
+ const from = nextPoints[segment.fromIndex];
+ const to = nextPoints[segment.toIndex];
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ if (segment.closing) {
+ from.closed = false;
+ }
+ else {
+ to.move = true;
+ to.type = 'start';
+ }
+ return nextPoints;
+}
+
class PathEditorEvent extends core.Event {
constructor(type, data) {
super(type);
@@ -195,11 +347,8 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
}),
this.controlLineBox.on_(core.PointerEvent.ENTER, this.handleControlLineEnter.bind(this)),
this.controlLineBox.on_(core.PointerEvent.LEAVE, this.handleControlLineLeave.bind(this)),
- this.controlLineBox.on_(core.PointerEvent.TAP, (e) => {
- if (e.target.data.isAnchor) {
- this.addPointAfter(e.target.data.index, e.target.x, e.target.y);
- }
- }),
+ this.controlLineBox.on_(core.PointerEvent.MOVE, this.handleControlLineMove.bind(this)),
+ this.controlLineBox.on_(core.PointerEvent.TAP, this.handleControlLineTap.bind(this)),
];
}
isCtrl() {
@@ -210,12 +359,13 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
isDel() {
if (this.downKey.length !== 1)
return false;
- return this.downKey.some((val) => [8].includes(val));
+ return this.downKey.some((val) => [8, 46].includes(val));
}
addKeyEventListener() {
const handleKeyDownEvent = (e) => {
this.downKey.push(e.keyCode);
if (this.isDel()) {
+ e.preventDefault();
this.handleDeletePoint();
}
};
@@ -276,50 +426,127 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
};
}
onUnload() {
+ this.clearHoverAnchor();
this.closeInnerEditor();
this.removeKeyEventListener();
this.editBox.remove(this.view);
}
handleDeletePoint() {
+ if (this.selectControlLine) {
+ this.points = deletePathSegment(this.points, this.selectControlLine);
+ this.selectControlLine = undefined;
+ this.reDraw();
+ return;
+ }
if (this.selectPointIdx === undefined)
return;
this.points.splice(this.selectPointIdx, 1);
this.selectPointIdx = undefined;
this.reDraw();
}
+ isSameSegment(left, right) {
+ return !!(left &&
+ right &&
+ left.fromIndex === right.fromIndex &&
+ left.toIndex === right.toIndex &&
+ left.insertIndex === right.insertIndex &&
+ !!left.closing === !!right.closing);
+ }
+ clearRemoveAnchorTimer() {
+ if (this.removeAnchorTimer)
+ clearTimeout(this.removeAnchorTimer);
+ this.removeAnchorTimer = undefined;
+ }
+ clearHoverAnchor() {
+ var _a;
+ this.clearRemoveAnchorTimer();
+ if ((_a = this.hoverAnchorPoint) === null || _a === void 0 ? void 0 : _a.parent) {
+ this.controlLineBox.remove(this.hoverAnchorPoint);
+ }
+ this.hoverAnchorPoint = undefined;
+ }
+ scheduleHoverAnchorRemoval() {
+ this.clearRemoveAnchorTimer();
+ const anchorPoint = this.hoverAnchorPoint;
+ this.removeAnchorTimer = setTimeout(() => {
+ if (this.hoverAnchorPoint === anchorPoint)
+ this.clearHoverAnchor();
+ }, 80);
+ }
+ updateHoverAnchor(controlLine, pointer) {
+ const segment = controlLine.data.segment;
+ const location = pointer && this.isSameSegment(segment, this.selectControlLine)
+ ? getClosestSegmentLocation(this.points, segment, pointer)
+ : { point: getPathSegmentPoint(this.points, segment, 0.5), t: 0.5 };
+ const size = this.pointRadius * 1.6;
+ if (!this.hoverAnchorPoint) {
+ this.hoverAnchorPoint = new core.Ellipse(Object.assign(Object.assign({}, this.pointStyle), { width: size, height: size, offsetX: -size / 2, offsetY: -size / 2, fill: 'white', stroke: '#2193FF', strokeWidth: 2, hitRadius: 4, cursor: 'copy', data: { isAnchor: true } }));
+ this.controlLineBox.add(this.hoverAnchorPoint);
+ }
+ this.hoverAnchorPoint.set({ x: location.point.x, y: location.point.y });
+ this.hoverAnchorPoint.data.segment = segment;
+ this.hoverAnchorPoint.data.t = location.t;
+ }
handleControlLineEnter(e) {
- e.target.state = 'hover';
+ this.clearRemoveAnchorTimer();
+ if (e.target.data.isAnchor)
+ return;
+ if (!e.target.data.isControlLine)
+ return;
+ const segment = e.target.data.segment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : 'hover';
this.hoverControlLine = e.target;
- const { isStraight, start, end } = e.target.data;
- if (isStraight) {
- const centerX = (start.x + end.x) / 2;
- const centerY = (start.y + end.y) / 2;
- const anchorPoint = new core.Ellipse(Object.assign({ x: centerX, y: centerY, width: this.pointRadius, height: this.pointRadius, offsetX: -this.pointRadius, offsetY: -this.pointRadius, cursor: 'pointer', data: {
- isAnchor: true,
- index: e.target.data.index,
- } }, this.pointStyle));
- this.controlLineBox.add(anchorPoint);
- e.target.data.anchorPoint = anchorPoint;
- }
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
}
handleControlLineLeave(e) {
- if (this.hoverControlLine) {
- this.hoverControlLine.state = undefined;
- }
- if (e.target.data.anchorPoint) {
- this.controlLineBox.remove(e.target.data.anchorPoint);
+ if (e.target.data.isControlLine) {
+ const segment = e.target.data.segment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined;
+ if (this.hoverControlLine === e.target)
+ this.hoverControlLine = undefined;
}
- else if (e.target.data.isAnchor) {
- this.controlLineBox.remove(e.target);
+ this.scheduleHoverAnchorRemoval();
+ }
+ handleControlLineMove(e) {
+ if (!e.target.data.isControlLine)
+ return;
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
+ }
+ handleControlLineTap(e) {
+ if (e.target.data.isAnchor) {
+ const segment = e.target.data.segment;
+ this.points = splitPathSegment(this.points, segment, e.target.data.t);
+ this.selectControlLine = undefined;
+ this.selectPointIdx = segment.insertIndex;
+ this.clearHoverAnchor();
+ this.reDraw();
+ return;
}
+ if (!e.target.data.isControlLine)
+ return;
+ this.handleSelectPoint();
+ this.selectControlLine = e.target.data.segment;
+ this.updateControl();
+ this.updateControlLines();
}
updateControlLines() {
+ this.clearHoverAnchor();
const controlLines = [];
- for (let i = 0; i < this.points.length; i++) {
- const currentPoint = this.points[i];
- const prevPoint = i === 0 ? this.points[this.points.length - 1] : this.points[i - 1];
+ const addControlLine = (fromIndex, toIndex, insertIndex, closing = false) => {
+ const currentPoint = this.points[toIndex];
+ const prevPoint = this.points[fromIndex];
const { x, y, x1, y1 } = currentPoint;
const { x: prevX, y: prevY, x2: prevX2, y2: prevY2 } = prevPoint;
+ const segment = {
+ fromIndex,
+ toIndex,
+ insertIndex,
+ closing,
+ };
let path = ``;
if (x1 !== undefined &&
y1 !== undefined &&
@@ -339,34 +566,44 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
const controlLine = new core.Path({
path,
stroke: 'transparent',
- strokeWidth: 2,
+ strokeWidth: 1.5,
+ hitRadius: 7,
+ hitStroke: 'all',
+ cursor: 'pointer',
states: {
hover: {
- stroke: 'red',
+ stroke: '#2193FF',
+ strokeWidth: 2,
+ },
+ selected: {
+ stroke: '#EF4444',
+ strokeWidth: 2,
},
},
+ state: this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined,
editable: false,
});
- if (path.indexOf('L') > -1) {
- controlLine.data.isStraight = true;
- controlLine.data.start = { x: prevX, y: prevY };
- controlLine.data.end = { x, y };
- controlLine.data.index = i;
- }
+ controlLine.data.isControlLine = true;
+ controlLine.data.segment = segment;
controlLines.push(controlLine);
+ };
+ let subpathStartIndex = 0;
+ for (let i = 0; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ if (i === 0 || currentPoint.move || currentPoint.type === 'start') {
+ subpathStartIndex = i;
+ }
+ else {
+ addControlLine(i - 1, i, i);
+ }
+ if (currentPoint.closed) {
+ addControlLine(i, subpathStartIndex, i + 1, true);
+ }
}
this.controlLineBox.set({ children: controlLines });
}
- addPointAfter(index, x, y) {
- const newPoint = { x, y };
- this.points.splice(index, 0, newPoint);
- if (index < this.selectPointIdx) {
- this.selectPointIdx++;
- }
- this.handleSelectPoint();
- this.selectPointIdx = index;
- this.reDraw();
- }
calculateMirrorMode(points) {
return points.map((point) => {
const { x, y, x1, y1, x2, y2 } = point;
@@ -649,7 +886,9 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
handlePointDown(e) {
const { innerId } = e.target;
const pointIdx = this.pointIdxMap.get(innerId);
+ this.selectControlLine = undefined;
this.handleSelectPoint(pointIdx.index);
+ this.updateControlLines();
}
handlePointDrag(e) {
const { moveX, moveY } = e;
@@ -686,8 +925,10 @@ let SVGPathEditor = class SVGPathEditor extends editor.InnerEditor {
if (prevSelectPoint) {
prevSelectPoint.state = 'unSelect';
}
- if (index === undefined)
+ if (index === undefined) {
+ this.selectPointIdx = undefined;
return;
+ }
this.selectPointIdx = index;
const selectPoint = this.pointsBox.children[index];
selectPoint.state = 'select';
@@ -894,3 +1135,6 @@ SVGPathEditor = __decorate([
core.Path.setEditInner('SVGPathEditor');
exports.PathEditorEvent = PathEditorEvent;
+exports.deletePathSegment = deletePathSegment;
+exports.getClosestSegmentLocation = getClosestSegmentLocation;
+exports.splitPathSegment = splitPathSegment;
diff --git a/dist/index.esm.js b/dist/index.esm.js
index e5a9ddf630b97cef3408f471f63c8d7107417da3..86a377d386b266f3ddac5606ffc84a18c10a0dc4 100644
--- a/dist/index.esm.js
+++ b/dist/index.esm.js
@@ -34,12 +34,15 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
const { M, L, C, Q, Z } = PathCommandMap;
function pathData2Point(path) {
const pointData = [];
+ let subpathStartIndex = -1;
for (let i = 0; i < path.length; i++) {
const point = {};
if (path[i] === M) {
point.x = path[++i];
point.y = path[++i];
point.type = 'start';
+ point.move = true;
+ subpathStartIndex = pointData.length;
}
else if (path[i] === L) {
point.x = path[++i];
@@ -60,63 +63,84 @@ function pathData2Point(path) {
point.y = path[++i];
}
else if (path[i] === Z) {
- point.x = pointData[0].x;
- point.y = pointData[0].y;
+ const start = pointData[subpathStartIndex];
+ const end = pointData[pointData.length - 1];
+ if (!start || !end)
+ continue;
+ if (pointData.length - 1 > subpathStartIndex &&
+ start.x === end.x &&
+ start.y === end.y) {
+ pointData.pop();
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
+ }
+ }
+ pointData[pointData.length - 1].closed = true;
+ continue;
}
pointData.push(point);
}
- const head = pointData[0];
- const tail = pointData[pointData.length - 1];
- if (tail.type !== 'end') {
- if (head.x === tail.x && head.y === tail.y) {
- pointData.pop();
- if (tail.x2 !== undefined && tail.x2 !== undefined) {
- pointData[pointData.length - 1].x2 = tail.x2;
- pointData[pointData.length - 1].y2 = tail.y2;
- }
- if (tail.x1 !== undefined && tail.y1 !== undefined) {
- pointData[0].x1 = tail.x1;
- pointData[0].y1 = tail.y1;
+ for (let endIndex = pointData.length - 1; endIndex > 0;) {
+ let startIndex = endIndex;
+ while (startIndex > 0 && !pointData[startIndex].move)
+ startIndex--;
+ const start = pointData[startIndex];
+ const end = pointData[endIndex];
+ if (endIndex > startIndex &&
+ !end.closed &&
+ start.x === end.x &&
+ start.y === end.y) {
+ pointData.splice(endIndex, 1);
+ const newEnd = pointData[endIndex - 1];
+ newEnd.closed = true;
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
}
}
+ endIndex = startIndex - 1;
}
return pointData;
}
function point2PathData(points) {
const pathData = [];
- const getNext = (index) => {
- if (index === points.length - 1) {
- return points[0];
+ if (!points.length)
+ return pathData;
+ const pushSegment = (prev, next) => {
+ const { x, y, x1, y1 } = next;
+ if (prev.x2 === undefined &&
+ prev.y2 === undefined &&
+ x1 === undefined &&
+ y1 === undefined) {
+ pathData.push(L, x, y);
+ }
+ else if (prev.x2 !== undefined &&
+ prev.y2 !== undefined &&
+ x1 !== undefined &&
+ y1 !== undefined) {
+ pathData.push(C, prev.x2, prev.y2, x1, y1, x, y);
+ }
+ else if (prev.x2 !== undefined && prev.y2 !== undefined) {
+ pathData.push(Q, prev.x2, prev.y2, x, y);
+ }
+ else if (x1 !== undefined && y1 !== undefined) {
+ pathData.push(Q, x1, y1, x, y);
}
- return points[index + 1];
};
- pathData.push(M, points[0].x, points[0].y);
+ let subpathStart = points[0];
for (let i = 0; i < points.length; i++) {
- const prev = points[i];
- const next = getNext(i);
- const { type, x, y, x1, y1 } = next;
- if (type === 'end') {
- continue;
+ const point = points[i];
+ if (i === 0 || point.move || point.type === 'start') {
+ pathData.push(M, point.x, point.y);
+ subpathStart = point;
}
else {
- if (prev.x2 === undefined &&
- prev.y2 === undefined &&
- x1 === undefined &&
- y1 === undefined) {
- pathData.push(L, x, y);
- }
- else if (prev.x2 !== undefined &&
- prev.y2 !== undefined &&
- x1 !== undefined &&
- y1 !== undefined) {
- pathData.push(C, prev.x2 || 0, prev.y2 || 0, x1, y1, x, y);
- }
- else if (prev.x2 !== undefined && prev.y2 !== undefined) {
- pathData.push(Q, prev.x2 || 0, prev.y2 || 0, x, y);
- }
- else if (x1 !== undefined && y1 !== undefined) {
- pathData.push(Q, x1, y1, x, y);
- }
+ pushSegment(points[i - 1], point);
+ }
+ if (point.closed) {
+ pushSegment(point, subpathStart);
+ pathData.push(Z);
}
}
return pathData;
@@ -136,6 +160,134 @@ function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
+const hasPoint = (x, y) => x !== undefined && y !== undefined;
+const lerp = (from, to, t) => ({
+ x: from.x + (to.x - from.x) * t,
+ y: from.y + (to.y - from.y) * t,
+});
+const distanceSquared = (left, right) => Math.pow(left.x - right.x, 2) + Math.pow(left.y - right.y, 2);
+function getSegmentPoints(points, segment) {
+ const from = points[segment.fromIndex];
+ const to = points[segment.toIndex];
+ const fromCoordinate = { x: from.x, y: from.y };
+ const toCoordinate = { x: to.x, y: to.y };
+ const outgoing = hasPoint(from.x2, from.y2)
+ ? { x: from.x2, y: from.y2 }
+ : undefined;
+ const incoming = hasPoint(to.x1, to.y1)
+ ? { x: to.x1, y: to.y1 }
+ : undefined;
+ return { from, to, fromCoordinate, toCoordinate, outgoing, incoming };
+}
+function getPathSegmentPoint(points, segment, t) {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ return lerp(lerp(left, center, t), lerp(center, right, t), t);
+ }
+ const control = outgoing || incoming;
+ if (control) {
+ return lerp(lerp(fromCoordinate, control, t), lerp(control, toCoordinate, t), t);
+ }
+ return lerp(fromCoordinate, toCoordinate, t);
+}
+function getClosestSegmentLocation(points, segment, target) {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (!outgoing && !incoming) {
+ const dx = toCoordinate.x - fromCoordinate.x;
+ const dy = toCoordinate.y - fromCoordinate.y;
+ const lengthSquared = dx * dx + dy * dy;
+ const t = lengthSquared
+ ? Math.max(0, Math.min(1, ((target.x - fromCoordinate.x) * dx + (target.y - fromCoordinate.y) * dy) / lengthSquared))
+ : 0;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+ }
+ const samples = 40;
+ let bestT = 0;
+ let bestDistance = Number.POSITIVE_INFINITY;
+ for (let index = 0; index <= samples; index++) {
+ const t = index / samples;
+ const distance = distanceSquared(getPathSegmentPoint(points, segment, t), target);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestT = t;
+ }
+ }
+ let left = Math.max(0, bestT - 1 / samples);
+ let right = Math.min(1, bestT + 1 / samples);
+ for (let index = 0; index < 12; index++) {
+ const leftThird = left + (right - left) / 3;
+ const rightThird = right - (right - left) / 3;
+ if (distanceSquared(getPathSegmentPoint(points, segment, leftThird), target) <=
+ distanceSquared(getPathSegmentPoint(points, segment, rightThird), target)) {
+ right = rightThird;
+ }
+ else {
+ left = leftThird;
+ }
+ }
+ const t = (left + right) / 2;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+}
+function splitPathSegment(points, segment, rawT) {
+ const nextPoints = points.map((point) => (Object.assign({}, point)));
+ const t = Math.max(0.001, Math.min(0.999, rawT));
+ const { from, to, fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(nextPoints, segment);
+ let newPoint;
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ const leftCenter = lerp(left, center, t);
+ const rightCenter = lerp(center, right, t);
+ const point = lerp(leftCenter, rightCenter, t);
+ from.x2 = left.x;
+ from.y2 = left.y;
+ to.x1 = right.x;
+ to.y1 = right.y;
+ newPoint = Object.assign(Object.assign({}, point), { x1: leftCenter.x, y1: leftCenter.y, x2: rightCenter.x, y2: rightCenter.y, mode: 'no-mirror' });
+ }
+ else if (outgoing || incoming) {
+ const control = outgoing || incoming;
+ const left = lerp(fromCoordinate, control, t);
+ const right = lerp(control, toCoordinate, t);
+ const point = lerp(left, right, t);
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ newPoint = Object.assign(Object.assign({}, point), { x1: left.x, y1: left.y, x2: right.x, y2: right.y, mode: 'no-mirror' });
+ }
+ else {
+ newPoint = getPathSegmentPoint(nextPoints, segment, t);
+ }
+ if (segment.closing) {
+ from.closed = false;
+ newPoint.closed = true;
+ }
+ nextPoints.splice(segment.insertIndex, 0, newPoint);
+ return nextPoints;
+}
+function deletePathSegment(points, segment) {
+ const nextPoints = points.map((point) => (Object.assign({}, point)));
+ const from = nextPoints[segment.fromIndex];
+ const to = nextPoints[segment.toIndex];
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ if (segment.closing) {
+ from.closed = false;
+ }
+ else {
+ to.move = true;
+ to.type = 'start';
+ }
+ return nextPoints;
+}
+
class PathEditorEvent extends Event {
constructor(type, data) {
super(type);
@@ -193,11 +345,8 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
}),
this.controlLineBox.on_(PointerEvent.ENTER, this.handleControlLineEnter.bind(this)),
this.controlLineBox.on_(PointerEvent.LEAVE, this.handleControlLineLeave.bind(this)),
- this.controlLineBox.on_(PointerEvent.TAP, (e) => {
- if (e.target.data.isAnchor) {
- this.addPointAfter(e.target.data.index, e.target.x, e.target.y);
- }
- }),
+ this.controlLineBox.on_(PointerEvent.MOVE, this.handleControlLineMove.bind(this)),
+ this.controlLineBox.on_(PointerEvent.TAP, this.handleControlLineTap.bind(this)),
];
}
isCtrl() {
@@ -208,12 +357,13 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
isDel() {
if (this.downKey.length !== 1)
return false;
- return this.downKey.some((val) => [8].includes(val));
+ return this.downKey.some((val) => [8, 46].includes(val));
}
addKeyEventListener() {
const handleKeyDownEvent = (e) => {
this.downKey.push(e.keyCode);
if (this.isDel()) {
+ e.preventDefault();
this.handleDeletePoint();
}
};
@@ -274,50 +424,127 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
};
}
onUnload() {
+ this.clearHoverAnchor();
this.closeInnerEditor();
this.removeKeyEventListener();
this.editBox.remove(this.view);
}
handleDeletePoint() {
+ if (this.selectControlLine) {
+ this.points = deletePathSegment(this.points, this.selectControlLine);
+ this.selectControlLine = undefined;
+ this.reDraw();
+ return;
+ }
if (this.selectPointIdx === undefined)
return;
this.points.splice(this.selectPointIdx, 1);
this.selectPointIdx = undefined;
this.reDraw();
}
+ isSameSegment(left, right) {
+ return !!(left &&
+ right &&
+ left.fromIndex === right.fromIndex &&
+ left.toIndex === right.toIndex &&
+ left.insertIndex === right.insertIndex &&
+ !!left.closing === !!right.closing);
+ }
+ clearRemoveAnchorTimer() {
+ if (this.removeAnchorTimer)
+ clearTimeout(this.removeAnchorTimer);
+ this.removeAnchorTimer = undefined;
+ }
+ clearHoverAnchor() {
+ var _a;
+ this.clearRemoveAnchorTimer();
+ if ((_a = this.hoverAnchorPoint) === null || _a === void 0 ? void 0 : _a.parent) {
+ this.controlLineBox.remove(this.hoverAnchorPoint);
+ }
+ this.hoverAnchorPoint = undefined;
+ }
+ scheduleHoverAnchorRemoval() {
+ this.clearRemoveAnchorTimer();
+ const anchorPoint = this.hoverAnchorPoint;
+ this.removeAnchorTimer = setTimeout(() => {
+ if (this.hoverAnchorPoint === anchorPoint)
+ this.clearHoverAnchor();
+ }, 80);
+ }
+ updateHoverAnchor(controlLine, pointer) {
+ const segment = controlLine.data.segment;
+ const location = pointer && this.isSameSegment(segment, this.selectControlLine)
+ ? getClosestSegmentLocation(this.points, segment, pointer)
+ : { point: getPathSegmentPoint(this.points, segment, 0.5), t: 0.5 };
+ const size = this.pointRadius * 1.6;
+ if (!this.hoverAnchorPoint) {
+ this.hoverAnchorPoint = new Ellipse(Object.assign(Object.assign({}, this.pointStyle), { width: size, height: size, offsetX: -size / 2, offsetY: -size / 2, fill: 'white', stroke: '#2193FF', strokeWidth: 2, hitRadius: 4, cursor: 'copy', data: { isAnchor: true } }));
+ this.controlLineBox.add(this.hoverAnchorPoint);
+ }
+ this.hoverAnchorPoint.set({ x: location.point.x, y: location.point.y });
+ this.hoverAnchorPoint.data.segment = segment;
+ this.hoverAnchorPoint.data.t = location.t;
+ }
handleControlLineEnter(e) {
- e.target.state = 'hover';
+ this.clearRemoveAnchorTimer();
+ if (e.target.data.isAnchor)
+ return;
+ if (!e.target.data.isControlLine)
+ return;
+ const segment = e.target.data.segment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : 'hover';
this.hoverControlLine = e.target;
- const { isStraight, start, end } = e.target.data;
- if (isStraight) {
- const centerX = (start.x + end.x) / 2;
- const centerY = (start.y + end.y) / 2;
- const anchorPoint = new Ellipse(Object.assign({ x: centerX, y: centerY, width: this.pointRadius, height: this.pointRadius, offsetX: -this.pointRadius, offsetY: -this.pointRadius, cursor: 'pointer', data: {
- isAnchor: true,
- index: e.target.data.index,
- } }, this.pointStyle));
- this.controlLineBox.add(anchorPoint);
- e.target.data.anchorPoint = anchorPoint;
- }
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
}
handleControlLineLeave(e) {
- if (this.hoverControlLine) {
- this.hoverControlLine.state = undefined;
- }
- if (e.target.data.anchorPoint) {
- this.controlLineBox.remove(e.target.data.anchorPoint);
+ if (e.target.data.isControlLine) {
+ const segment = e.target.data.segment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined;
+ if (this.hoverControlLine === e.target)
+ this.hoverControlLine = undefined;
}
- else if (e.target.data.isAnchor) {
- this.controlLineBox.remove(e.target);
+ this.scheduleHoverAnchorRemoval();
+ }
+ handleControlLineMove(e) {
+ if (!e.target.data.isControlLine)
+ return;
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
+ }
+ handleControlLineTap(e) {
+ if (e.target.data.isAnchor) {
+ const segment = e.target.data.segment;
+ this.points = splitPathSegment(this.points, segment, e.target.data.t);
+ this.selectControlLine = undefined;
+ this.selectPointIdx = segment.insertIndex;
+ this.clearHoverAnchor();
+ this.reDraw();
+ return;
}
+ if (!e.target.data.isControlLine)
+ return;
+ this.handleSelectPoint();
+ this.selectControlLine = e.target.data.segment;
+ this.updateControl();
+ this.updateControlLines();
}
updateControlLines() {
+ this.clearHoverAnchor();
const controlLines = [];
- for (let i = 0; i < this.points.length; i++) {
- const currentPoint = this.points[i];
- const prevPoint = i === 0 ? this.points[this.points.length - 1] : this.points[i - 1];
+ const addControlLine = (fromIndex, toIndex, insertIndex, closing = false) => {
+ const currentPoint = this.points[toIndex];
+ const prevPoint = this.points[fromIndex];
const { x, y, x1, y1 } = currentPoint;
const { x: prevX, y: prevY, x2: prevX2, y2: prevY2 } = prevPoint;
+ const segment = {
+ fromIndex,
+ toIndex,
+ insertIndex,
+ closing,
+ };
let path = ``;
if (x1 !== undefined &&
y1 !== undefined &&
@@ -337,34 +564,44 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
const controlLine = new Path({
path,
stroke: 'transparent',
- strokeWidth: 2,
+ strokeWidth: 1.5,
+ hitRadius: 7,
+ hitStroke: 'all',
+ cursor: 'pointer',
states: {
hover: {
- stroke: 'red',
+ stroke: '#2193FF',
+ strokeWidth: 2,
+ },
+ selected: {
+ stroke: '#EF4444',
+ strokeWidth: 2,
},
},
+ state: this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined,
editable: false,
});
- if (path.indexOf('L') > -1) {
- controlLine.data.isStraight = true;
- controlLine.data.start = { x: prevX, y: prevY };
- controlLine.data.end = { x, y };
- controlLine.data.index = i;
- }
+ controlLine.data.isControlLine = true;
+ controlLine.data.segment = segment;
controlLines.push(controlLine);
+ };
+ let subpathStartIndex = 0;
+ for (let i = 0; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ if (i === 0 || currentPoint.move || currentPoint.type === 'start') {
+ subpathStartIndex = i;
+ }
+ else {
+ addControlLine(i - 1, i, i);
+ }
+ if (currentPoint.closed) {
+ addControlLine(i, subpathStartIndex, i + 1, true);
+ }
}
this.controlLineBox.set({ children: controlLines });
}
- addPointAfter(index, x, y) {
- const newPoint = { x, y };
- this.points.splice(index, 0, newPoint);
- if (index < this.selectPointIdx) {
- this.selectPointIdx++;
- }
- this.handleSelectPoint();
- this.selectPointIdx = index;
- this.reDraw();
- }
calculateMirrorMode(points) {
return points.map((point) => {
const { x, y, x1, y1, x2, y2 } = point;
@@ -647,7 +884,9 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
handlePointDown(e) {
const { innerId } = e.target;
const pointIdx = this.pointIdxMap.get(innerId);
+ this.selectControlLine = undefined;
this.handleSelectPoint(pointIdx.index);
+ this.updateControlLines();
}
handlePointDrag(e) {
const { moveX, moveY } = e;
@@ -684,8 +923,10 @@ let SVGPathEditor = class SVGPathEditor extends InnerEditor {
if (prevSelectPoint) {
prevSelectPoint.state = 'unSelect';
}
- if (index === undefined)
+ if (index === undefined) {
+ this.selectPointIdx = undefined;
return;
+ }
this.selectPointIdx = index;
const selectPoint = this.pointsBox.children[index];
selectPoint.state = 'select';
@@ -891,4 +1132,4 @@ SVGPathEditor = __decorate([
], SVGPathEditor);
Path.setEditInner('SVGPathEditor');
-export { PathEditorEvent };
+export { PathEditorEvent, deletePathSegment, getClosestSegmentLocation, splitPathSegment };
diff --git a/dist/index.esm.min.js b/dist/index.esm.min.js
index 718545713f40aa629b9da0aa7d1077bf998325e1..9bddb2e87b923c7864cd4dcfc17c9d751ad5a6bf 100644
--- a/dist/index.esm.min.js
+++ b/dist/index.esm.min.js
@@ -1 +1 @@
-import{PathCommandMap as t,Event as i,Path as e,Box as o,DragEvent as s,PointerEvent as n,Ellipse as r}from"@leafer-ui/core";import{registerInnerEditor as h,InnerEditor as a}from"@leafer-in/editor";import"@leafer-in/state";"function"==typeof SuppressedError&&SuppressedError;const{M:d,L:l,C:c,Q:x,Z:p}=t;function y(t){const i=[];i.push(d,t[0].x,t[0].y);for(let o=0;o<t.length;o++){const s=t[o],n=(e=o)===t.length-1?t[0]:t[e+1],{type:r,x:h,y:a,x1:d,y1:p}=n;"end"!==r&&(void 0===s.x2&&void 0===s.y2&&void 0===d&&void 0===p?i.push(l,h,a):void 0!==s.x2&&void 0!==s.y2&&void 0!==d&&void 0!==p?i.push(c,s.x2||0,s.y2||0,d,p,h,a):void 0!==s.x2&&void 0!==s.y2?i.push(x,s.x2||0,s.y2||0,h,a):void 0!==d&&void 0!==p&&i.push(x,d,p,h,a))}var e;return i}function g(t,i=1){return Number.isInteger(t)?t:Number(t.toFixed(i))}function u(t,i,e,o){return Math.sqrt(Math.pow(e-t,2)+Math.pow(o-i,2))}function f(t,i,e,o,s,n){return Math.abs((t-e)*(n-o)-(i-o)*(s-e))}function v(t,i,e){return Math.max(i,Math.min(e,t))}class b extends i{constructor(t,i){super(t),i&&Object.assign(this,i)}}b.CHANGE="pathEditor.change";let m=class extends a{get tag(){return"SVGPathEditor"}constructor(t){super(t),this.strokeBox=new o,this.pointsBox=new o,this.controlsBox=new o,this.controlAbsorbBox=new o,this.controlLineBox=new o,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new o,this.controlsBox=new o,this.strokeBox=new o,this.controlAbsorbBox=new o,this.controlLineBox=new o,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(s.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(n.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(n.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(s.DRAG,this.handleControlDrag.bind(this)),this.controlsBox.on_(s.END,this.handleControlDragEnd.bind(this)),this.controlsBox.on_(n.DOWN,this.handleControlDown.bind(this)),this.controlsBox.on_(n.UP,this.handleControlUp.bind(this)),this.editor.app.on_(n.DOUBLE_TAP,(t=>{t.target!==this.editTarget&&this.editor.closeInnerEditor()})),this.controlLineBox.on_(n.ENTER,this.handleControlLineEnter.bind(this)),this.controlLineBox.on_(n.LEAVE,this.handleControlLineLeave.bind(this)),this.controlLineBox.on_(n.TAP,(t=>{t.target.data.isAnchor&&this.addPointAfter(t.target.data.index,t.target.x,t.target.y)}))]}isCtrl(){return 1===this.downKey.length&&this.downKey.some((t=>[17,91].includes(t)))}isDel(){return 1===this.downKey.length&&this.downKey.some((t=>[8].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&this.handleDeletePoint()};document.addEventListener("keydown",t);const i=t=>{this.downKey=this.downKey.filter((i=>t.keyCode!==i))};document.addEventListener("keyup",i),this.keyEvents.push({type:"keydown",event:t}),this.keyEvents.push({type:"keyup",event:i})}removeKeyEventListener(){this.keyEvents.forEach((({type:t,event:i})=>{document.removeEventListener(t,i)})),this.keyEvents=[]}onLoad(){var t;this.calculatePointStyle(this.editTarget.worldTransform),this.points=this.innerTransformPoints(function(t){const i=[];for(let e=0;e<t.length;e++){const o={};t[e]===d?(o.x=t[++e],o.y=t[++e],o.type="start"):t[e]===l?(o.x=t[++e],o.y=t[++e]):t[e]===c?(i[i.length-1].x2=t[++e],i[i.length-1].y2=t[++e],o.x1=t[++e],o.y1=t[++e],o.x=t[++e],o.y=t[++e]):t[e]===x?(i[i.length-1].x2=t[++e],i[i.length-1].y2=t[++e],o.x=t[++e],o.y=t[++e]):t[e]===p&&(o.x=i[0].x,o.y=i[0].y),i.push(o)}const e=i[0],o=i[i.length-1];return"end"!==o.type&&e.x===o.x&&e.y===o.y&&(i.pop(),void 0!==o.x2&&void 0!==o.x2&&(i[i.length-1].x2=o.x2,i[i.length-1].y2=o.y2),void 0!==o.x1&&void 0!==o.y1&&(i[0].x1=o.x1,i[0].y1=o.y1)),i}(this.editTarget.getPath())),this.points=this.calculateMirrorMode
+import{PathCommandMap as t,Event as e,Path as o,Box as i,DragEvent as n,PointerEvent as s,Ellipse as r}from"@leafer-ui/core";import{registerInnerEditor as h,InnerEditor as a}from"@leafer-in/editor";import"@leafer-in/state";"function"==typeof SuppressedError&&SuppressedError;const{M:l,L:d,C:c,Q:x,Z:g}=t;function y(t){const e=[];if(!t.length)return e;const o=(t,o)=>{const{x:i,y:n,x1:s,y1:r}=o;void 0===t.x2&&void 0===t.y2&&void 0===s&&void 0===r?e.push(d,i,n):void 0!==t.x2&&void 0!==t.y2&&void 0!==s&&void 0!==r?e.push(c,t.x2,t.y2,s,r,i,n):void 0!==t.x2&&void 0!==t.y2?e.push(x,t.x2,t.y2,i,n):void 0!==s&&void 0!==r&&e.push(x,s,r,i,n)};let i=t[0];for(let n=0;n<t.length;n++){const s=t[n];0===n||s.move||"start"===s.type?(e.push(l,s.x,s.y),i=s):o(t[n-1],s),s.closed&&(o(s,i),e.push(g))}return e}function p(t,e=1){return Number.isInteger(t)?t:Number(t.toFixed(e))}function u(t,e,o,i){return Math.sqrt(Math.pow(o-t,2)+Math.pow(i-e,2))}function v(t,e,o,i,n,s){return Math.abs((t-o)*(s-i)-(e-i)*(n-o))}function f(t,e,o){return Math.max(e,Math.min(o,t))}const m=(t,e)=>void 0!==t&&void 0!==e,b=(t,e,o)=>({x:t.x+(e.x-t.x)*o,y:t.y+(e.y-t.y)*o}),P=(t,e)=>Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2);function w(t,e){const o=t[e.fromIndex],i=t[e.toIndex];return{from:o,to:i,fromCoordinate:{x:o.x,y:o.y},toCoordinate:{x:i.x,y:i.y},outgoing:m(o.x2,o.y2)?{x:o.x2,y:o.y2}:void 0,incoming:m(i.x1,i.y1)?{x:i.x1,y:i.y1}:void 0}}function C(t,e,o){const{fromCoordinate:i,toCoordinate:n,outgoing:s,incoming:r}=w(t,e);if(s&&r){const t=b(i,s,o),e=b(s,r,o),h=b(r,n,o);return b(b(t,e,o),b(e,h,o),o)}const h=s||r;return h?b(b(i,h,o),b(h,n,o),o):b(i,n,o)}function M(t,e,o){const{fromCoordinate:i,toCoordinate:n,outgoing:s,incoming:r}=w(t,e);if(!s&&!r){const s=n.x-i.x,r=n.y-i.y,h=s*s+r*r,a=h?Math.max(0,Math.min(1,((o.x-i.x)*s+(o.y-i.y)*r)/h)):0;return{point:C(t,e,a),t:a}}let h=0,a=Number.POSITIVE_INFINITY;for(let i=0;i<=40;i++){const n=i/40,s=P(C(t,e,n),o);s<a&&(a=s,h=n)}let l=Math.max(0,h-1/40),d=Math.min(1,h+1/40);for(let i=0;i<12;i++){const i=l+(d-l)/3,n=d-(d-l)/3;P(C(t,e,i),o)<=P(C(t,e,n),o)?d=n:l=i}const c=(l+d)/2;return{point:C(t,e,c),t:c}}function A(t,e,o){const i=t.map((t=>Object.assign({},t))),n=Math.max(.001,Math.min(.999,o)),{from:s,to:r,fromCoordinate:h,toCoordinate:a,outgoing:l,incoming:d}=w(i,e);let c;if(l&&d){const t=b(h,l,n),e=b(l,d,n),o=b(d,a,n),i=b(t,e,n),x=b(e,o,n),g=b(i,x,n);s.x2=t.x,s.y2=t.y,r.x1=o.x,r.y1=o.y,c=Object.assign(Object.assign({},g),{x1:i.x,y1:i.y,x2:x.x,y2:x.y,mode:"no-mirror"})}else if(l||d){const t=l||d,e=b(h,t,n),o=b(t,a,n),i=b(e,o,n);s.x2=void 0,s.y2=void 0,r.x1=void 0,r.y1=void 0,c=Object.assign(Object.assign({},i),{x1:e.x,y1:e.y,x2:o.x,y2:o.y,mode:"no-mirror"})}else c=C(i,e,n);return e.closing&&(s.closed=!1,c.closed=!0),i.splice(e.insertIndex,0,c),i}function L(t,e){const o=t.map((t=>Object.assign({},t))),i=o[e.fromIndex],n=o[e.toIndex];return i.x2=void 0,i.y2=void 0,n.x1=void 0,n.y1=void 0,e.closing?i.closed=!1:(n.move=!0,n.type="start"),o}class I extends e{constructor(t,e){super(t),e&&Object.assign(this,e)}}I.CHANGE="pathEditor.change";let T=class extends a{get tag(){return"SVGPathEditor"}constructor(t){super(t),this.strokeBox=new i,this.pointsBox=new i,this.controlsBox=new i,this.controlAbsorbBox=new i,this.controlLineBox=new i,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new i,this.controlsBox=new i,this.strokeBox=new i,this.controlAbsorbBox=new i,this.controlLineBox=new i,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(n.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(s.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(s.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(n.DRAG,th
diff --git a/dist/index.min.cjs b/dist/index.min.cjs
index c29e650918870792070a761d7b42eab336118d51..ded0aa5eefb57249516e2af3ec3c22e04e01e0f0 100644
--- a/dist/index.min.cjs
+++ b/dist/index.min.cjs
@@ -1 +1 @@
-"use strict";var t=require("@leafer-ui/core"),e=require("@leafer-in/editor");require("@leafer-in/state"),"function"==typeof SuppressedError&&SuppressedError;const{M:i,L:o,C:s,Q:n,Z:r}=t.PathCommandMap;function h(t){const e=[];e.push(i,t[0].x,t[0].y);for(let i=0;i<t.length;i++){const h=t[i],a=(r=i)===t.length-1?t[0]:t[r+1],{type:d,x:l,y:c,x1:x,y1:p}=a;"end"!==d&&(void 0===h.x2&&void 0===h.y2&&void 0===x&&void 0===p?e.push(o,l,c):void 0!==h.x2&&void 0!==h.y2&&void 0!==x&&void 0!==p?e.push(s,h.x2||0,h.y2||0,x,p,l,c):void 0!==h.x2&&void 0!==h.y2?e.push(n,h.x2||0,h.y2||0,l,c):void 0!==x&&void 0!==p&&e.push(n,x,p,l,c))}var r;return e}function a(t,e=1){return Number.isInteger(t)?t:Number(t.toFixed(e))}function d(t,e,i,o){return Math.sqrt(Math.pow(i-t,2)+Math.pow(o-e,2))}function l(t,e,i,o,s,n){return Math.abs((t-i)*(n-o)-(e-o)*(s-i))}function c(t,e,i){return Math.max(e,Math.min(i,t))}class x extends t.Event{constructor(t,e){super(t),e&&Object.assign(this,e)}}x.CHANGE="pathEditor.change";let p=class extends e.InnerEditor{get tag(){return"SVGPathEditor"}constructor(e){super(e),this.strokeBox=new t.Box,this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.strokeBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(t.DragEvent.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(t.PointerEvent.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(t.PointerEvent.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(t.DragEvent.DRAG,this.handleControlDrag.bind(this)),this.controlsBox.on_(t.DragEvent.END,this.handleControlDragEnd.bind(this)),this.controlsBox.on_(t.PointerEvent.DOWN,this.handleControlDown.bind(this)),this.controlsBox.on_(t.PointerEvent.UP,this.handleControlUp.bind(this)),this.editor.app.on_(t.PointerEvent.DOUBLE_TAP,(t=>{t.target!==this.editTarget&&this.editor.closeInnerEditor()})),this.controlLineBox.on_(t.PointerEvent.ENTER,this.handleControlLineEnter.bind(this)),this.controlLineBox.on_(t.PointerEvent.LEAVE,this.handleControlLineLeave.bind(this)),this.controlLineBox.on_(t.PointerEvent.TAP,(t=>{t.target.data.isAnchor&&this.addPointAfter(t.target.data.index,t.target.x,t.target.y)}))]}isCtrl(){return 1===this.downKey.length&&this.downKey.some((t=>[17,91].includes(t)))}isDel(){return 1===this.downKey.length&&this.downKey.some((t=>[8].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&this.handleDeletePoint()};document.addEventListener("keydown",t);const e=t=>{this.downKey=this.downKey.filter((e=>t.keyCode!==e))};document.addEventListener("keyup",e),this.keyEvents.push({type:"keydown",event:t}),this.keyEvents.push({type:"keyup",event:e})}removeKeyEventListener(){this.keyEvents.forEach((({type:t,event:e})=>{document.removeEventListener(t,e)})),this.keyEvents=[]}onLoad(){var t;this.calculatePointStyle(this.editTarget.worldTransform),this.points=this.innerTransformPoints(function(t){const e=[];for(let h=0;h<t.length;h++){const a={};t[h]===i?(a.x=t[++h],a.y=t[++h],a.type="start"):t[h]===o?(a.x=t[++h],a.y=t[++h]):t[h]===s?(e[e.length-1].x2=t[++h],e[e.length-1].y2=t[++h],a.x1=t[++h],a.y1=t[++h],a.x=t[++h],a.y=t[++h]):t[h]===n?(e[e.length-1].x2=t[++h],e[e.length-1].y2=t[++h],a.x=t[++h],a.y=t[++h]):t[h]===r&&(a.x=e[0].x,a.y=e[0].y),e.push(a)}const h=e[0],a=e[e.length-1];return"end"!==a.type&&h.x===a.x&&h.y===a.y&&(e.pop(),void 0!==a.x2&&void 0!==a.x2&&(e[e.length-1].x2=a.x2,e[e.length-1].y2=a.y2),void 0!==a.x1&&void 0!==a.y1&&(e[0].x
+"use strict";var t=require("@leafer-ui/core"),e=require("@leafer-in/editor");require("@leafer-in/state"),"function"==typeof SuppressedError&&SuppressedError;const{M:o,L:i,C:n,Q:s,Z:r}=t.PathCommandMap;function h(t){const e=[];if(!t.length)return e;const h=(t,o)=>{const{x:r,y:h,x1:a,y1:l}=o;void 0===t.x2&&void 0===t.y2&&void 0===a&&void 0===l?e.push(i,r,h):void 0!==t.x2&&void 0!==t.y2&&void 0!==a&&void 0!==l?e.push(n,t.x2,t.y2,a,l,r,h):void 0!==t.x2&&void 0!==t.y2?e.push(s,t.x2,t.y2,r,h):void 0!==a&&void 0!==l&&e.push(s,a,l,r,h)};let a=t[0];for(let i=0;i<t.length;i++){const n=t[i];0===i||n.move||"start"===n.type?(e.push(o,n.x,n.y),a=n):h(t[i-1],n),n.closed&&(h(n,a),e.push(r))}return e}function a(t,e=1){return Number.isInteger(t)?t:Number(t.toFixed(e))}function l(t,e,o,i){return Math.sqrt(Math.pow(o-t,2)+Math.pow(i-e,2))}function d(t,e,o,i,n,s){return Math.abs((t-o)*(s-i)-(e-i)*(n-o))}function c(t,e,o){return Math.max(e,Math.min(o,t))}const x=(t,e)=>void 0!==t&&void 0!==e,g=(t,e,o)=>({x:t.x+(e.x-t.x)*o,y:t.y+(e.y-t.y)*o}),p=(t,e)=>Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2);function v(t,e){const o=t[e.fromIndex],i=t[e.toIndex];return{from:o,to:i,fromCoordinate:{x:o.x,y:o.y},toCoordinate:{x:i.x,y:i.y},outgoing:x(o.x2,o.y2)?{x:o.x2,y:o.y2}:void 0,incoming:x(i.x1,i.y1)?{x:i.x1,y:i.y1}:void 0}}function y(t,e,o){const{fromCoordinate:i,toCoordinate:n,outgoing:s,incoming:r}=v(t,e);if(s&&r){const t=g(i,s,o),e=g(s,r,o),h=g(r,n,o);return g(g(t,e,o),g(e,h,o),o)}const h=s||r;return h?g(g(i,h,o),g(h,n,o),o):g(i,n,o)}function u(t,e,o){const{fromCoordinate:i,toCoordinate:n,outgoing:s,incoming:r}=v(t,e);if(!s&&!r){const s=n.x-i.x,r=n.y-i.y,h=s*s+r*r,a=h?Math.max(0,Math.min(1,((o.x-i.x)*s+(o.y-i.y)*r)/h)):0;return{point:y(t,e,a),t:a}}let h=0,a=Number.POSITIVE_INFINITY;for(let i=0;i<=40;i++){const n=i/40,s=p(y(t,e,n),o);s<a&&(a=s,h=n)}let l=Math.max(0,h-1/40),d=Math.min(1,h+1/40);for(let i=0;i<12;i++){const i=l+(d-l)/3,n=d-(d-l)/3;p(y(t,e,i),o)<=p(y(t,e,n),o)?d=n:l=i}const c=(l+d)/2;return{point:y(t,e,c),t:c}}function f(t,e,o){const i=t.map((t=>Object.assign({},t))),n=Math.max(.001,Math.min(.999,o)),{from:s,to:r,fromCoordinate:h,toCoordinate:a,outgoing:l,incoming:d}=v(i,e);let c;if(l&&d){const t=g(h,l,n),e=g(l,d,n),o=g(d,a,n),i=g(t,e,n),x=g(e,o,n),p=g(i,x,n);s.x2=t.x,s.y2=t.y,r.x1=o.x,r.y1=o.y,c=Object.assign(Object.assign({},p),{x1:i.x,y1:i.y,x2:x.x,y2:x.y,mode:"no-mirror"})}else if(l||d){const t=l||d,e=g(h,t,n),o=g(t,a,n),i=g(e,o,n);s.x2=void 0,s.y2=void 0,r.x1=void 0,r.y1=void 0,c=Object.assign(Object.assign({},i),{x1:e.x,y1:e.y,x2:o.x,y2:o.y,mode:"no-mirror"})}else c=y(i,e,n);return e.closing&&(s.closed=!1,c.closed=!0),i.splice(e.insertIndex,0,c),i}function m(t,e){const o=t.map((t=>Object.assign({},t))),i=o[e.fromIndex],n=o[e.toIndex];return i.x2=void 0,i.y2=void 0,n.x1=void 0,n.y1=void 0,e.closing?i.closed=!1:(n.move=!0,n.type="start"),o}class b extends t.Event{constructor(t,e){super(t),e&&Object.assign(this,e)}}b.CHANGE="pathEditor.change";let P=class extends e.InnerEditor{get tag(){return"SVGPathEditor"}constructor(e){super(e),this.strokeBox=new t.Box,this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.strokeBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(t.DragEvent.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(t.PointerEvent.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(t.PointerEvent.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(t.DragEvent.DRAG,t
diff --git a/src/index.ts b/src/index.ts
index 18bd15470c59fcf04cfc7d2c80e3029c25dc8b6e..b690a1e1b14b4378bffe4031c6f9f314d9ac40b7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,9 @@
import './svg-path-editor';
export { PathEditorEvent } from './event';
+export {
+ deletePathSegment,
+ getClosestSegmentLocation,
+ splitPathSegment,
+} from './segment';
+export type { IPathSegment } from './type';
diff --git a/src/segment.ts b/src/segment.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8830cebdedd6ed2dcff8c39e1d38db371ff846d7
--- /dev/null
+++ b/src/segment.ts
@@ -0,0 +1,186 @@
+import { IPathSegment, IPoint } from './type';
+
+interface ICoordinate {
+ x: number;
+ y: number;
+}
+
+export interface IClosestSegmentLocation {
+ point: ICoordinate;
+ t: number;
+}
+
+const hasPoint = (x?: number, y?: number): x is number =>
+ x !== undefined && y !== undefined;
+
+const lerp = (from: ICoordinate, to: ICoordinate, t: number) => ({
+ x: from.x + (to.x - from.x) * t,
+ y: from.y + (to.y - from.y) * t,
+});
+
+const distanceSquared = (left: ICoordinate, right: ICoordinate) =>
+ Math.pow(left.x - right.x, 2) + Math.pow(left.y - right.y, 2);
+
+function getSegmentPoints(points: IPoint[], segment: IPathSegment) {
+ const from = points[segment.fromIndex] as Required<Pick<IPoint, 'x' | 'y'>> & IPoint;
+ const to = points[segment.toIndex] as Required<Pick<IPoint, 'x' | 'y'>> & IPoint;
+ const fromCoordinate = { x: from.x, y: from.y };
+ const toCoordinate = { x: to.x, y: to.y };
+ const outgoing = hasPoint(from.x2, from.y2)
+ ? { x: from.x2, y: from.y2 as number }
+ : undefined;
+ const incoming = hasPoint(to.x1, to.y1)
+ ? { x: to.x1, y: to.y1 as number }
+ : undefined;
+
+ return { from, to, fromCoordinate, toCoordinate, outgoing, incoming };
+}
+
+export function getPathSegmentPoint(
+ points: IPoint[],
+ segment: IPathSegment,
+ t: number
+) {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ return lerp(lerp(left, center, t), lerp(center, right, t), t);
+ }
+
+ const control = outgoing || incoming;
+ if (control) {
+ return lerp(lerp(fromCoordinate, control, t), lerp(control, toCoordinate, t), t);
+ }
+
+ return lerp(fromCoordinate, toCoordinate, t);
+}
+
+export function getClosestSegmentLocation(
+ points: IPoint[],
+ segment: IPathSegment,
+ target: ICoordinate
+): IClosestSegmentLocation {
+ const { fromCoordinate, toCoordinate, outgoing, incoming } = getSegmentPoints(points, segment);
+ if (!outgoing && !incoming) {
+ const dx = toCoordinate.x - fromCoordinate.x;
+ const dy = toCoordinate.y - fromCoordinate.y;
+ const lengthSquared = dx * dx + dy * dy;
+ const t = lengthSquared
+ ? Math.max(0, Math.min(1, ((target.x - fromCoordinate.x) * dx + (target.y - fromCoordinate.y) * dy) / lengthSquared))
+ : 0;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+ }
+
+ const samples = 40;
+ let bestT = 0;
+ let bestDistance = Number.POSITIVE_INFINITY;
+ for (let index = 0; index <= samples; index++) {
+ const t = index / samples;
+ const distance = distanceSquared(
+ getPathSegmentPoint(points, segment, t),
+ target
+ );
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestT = t;
+ }
+ }
+
+ let left = Math.max(0, bestT - 1 / samples);
+ let right = Math.min(1, bestT + 1 / samples);
+ for (let index = 0; index < 12; index++) {
+ const leftThird = left + (right - left) / 3;
+ const rightThird = right - (right - left) / 3;
+ if (
+ distanceSquared(getPathSegmentPoint(points, segment, leftThird), target) <=
+ distanceSquared(getPathSegmentPoint(points, segment, rightThird), target)
+ ) {
+ right = rightThird;
+ } else {
+ left = leftThird;
+ }
+ }
+ const t = (left + right) / 2;
+ return { point: getPathSegmentPoint(points, segment, t), t };
+}
+
+export function splitPathSegment(
+ points: IPoint[],
+ segment: IPathSegment,
+ rawT: number
+) {
+ const nextPoints = points.map((point) => ({ ...point }));
+ const t = Math.max(0.001, Math.min(0.999, rawT));
+ const { from, to, fromCoordinate, toCoordinate, outgoing, incoming } =
+ getSegmentPoints(nextPoints, segment);
+ let newPoint: IPoint;
+
+ if (outgoing && incoming) {
+ const left = lerp(fromCoordinate, outgoing, t);
+ const center = lerp(outgoing, incoming, t);
+ const right = lerp(incoming, toCoordinate, t);
+ const leftCenter = lerp(left, center, t);
+ const rightCenter = lerp(center, right, t);
+ const point = lerp(leftCenter, rightCenter, t);
+
+ from.x2 = left.x;
+ from.y2 = left.y;
+ to.x1 = right.x;
+ to.y1 = right.y;
+ newPoint = {
+ ...point,
+ x1: leftCenter.x,
+ y1: leftCenter.y,
+ x2: rightCenter.x,
+ y2: rightCenter.y,
+ mode: 'no-mirror',
+ };
+ } else if (outgoing || incoming) {
+ const control = outgoing || (incoming as ICoordinate);
+ const left = lerp(fromCoordinate, control, t);
+ const right = lerp(control, toCoordinate, t);
+ const point = lerp(left, right, t);
+
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ newPoint = {
+ ...point,
+ x1: left.x,
+ y1: left.y,
+ x2: right.x,
+ y2: right.y,
+ mode: 'no-mirror',
+ };
+ } else {
+ newPoint = getPathSegmentPoint(nextPoints, segment, t);
+ }
+
+ if (segment.closing) {
+ from.closed = false;
+ newPoint.closed = true;
+ }
+ nextPoints.splice(segment.insertIndex, 0, newPoint);
+ return nextPoints;
+}
+
+export function deletePathSegment(points: IPoint[], segment: IPathSegment) {
+ const nextPoints = points.map((point) => ({ ...point }));
+ const from = nextPoints[segment.fromIndex];
+ const to = nextPoints[segment.toIndex];
+
+ from.x2 = undefined;
+ from.y2 = undefined;
+ to.x1 = undefined;
+ to.y1 = undefined;
+ if (segment.closing) {
+ from.closed = false;
+ } else {
+ to.move = true;
+ to.type = 'start';
+ }
+ return nextPoints;
+}
diff --git a/src/state.d.ts b/src/state.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a04676e7fe99d024976c053c0bff0a3ef889e5b
--- /dev/null
+++ b/src/state.d.ts
@@ -0,0 +1 @@
+declare module '@leafer-in/state';
diff --git a/src/svg-path-editor.ts b/src/svg-path-editor.ts
index 97c706aefa54897e26c67fe929e0697e2e65ec18..d5d3829d7461b17e859fc5601501228b20228b2f 100644
--- a/src/svg-path-editor.ts
+++ b/src/svg-path-editor.ts
@@ -17,7 +17,13 @@ import {
point2PathData,
toFixed,
} from './utils';
-import { AnyObject, IPoint, MirrorMode, PointIdx } from './type';
+import { AnyObject, IPathSegment, IPoint, MirrorMode, PointIdx } from './type';
+import {
+ deletePathSegment,
+ getClosestSegmentLocation,
+ getPathSegmentPoint,
+ splitPathSegment,
+} from './segment';
import { PathEditorEvent } from './event';
import '@leafer-in/state';
@@ -62,6 +68,14 @@ export class SVGPathEditor extends InnerEditor {
// 当前 hover 的控制线
private hoverControlLine?: Path;
+ // 当前选中的线段
+ private selectControlLine?: IPathSegment;
+
+ // hover 线段上的待添加锚点
+ private hoverAnchorPoint?: Ellipse;
+
+ private removeAnchorTimer?: ReturnType<typeof setTimeout>;
+
// 当前选中的 控制点
private selectControlPoint?: Ellipse;
@@ -142,11 +156,14 @@ export class SVGPathEditor extends InnerEditor {
PointerEvent.LEAVE,
this.handleControlLineLeave.bind(this)
),
- this.controlLineBox.on_(PointerEvent.TAP, (e: any) => {
- if (e.target.data.isAnchor) {
- this.addPointAfter(e.target.data.index, e.target.x, e.target.y);
- }
- }),
+ this.controlLineBox.on_(
+ PointerEvent.MOVE,
+ this.handleControlLineMove.bind(this)
+ ),
+ this.controlLineBox.on_(
+ PointerEvent.TAP,
+ this.handleControlLineTap.bind(this)
+ ),
];
}
@@ -159,13 +176,14 @@ export class SVGPathEditor extends InnerEditor {
// 是否按下了 删除键
private isDel() {
if (this.downKey.length !== 1) return false;
- return this.downKey.some((val) => [8].includes(val));
+ return this.downKey.some((val) => [8, 46].includes(val));
}
private addKeyEventListener() {
const handleKeyDownEvent = (e: any) => {
this.downKey.push(e.keyCode);
if (this.isDel()) {
+ e.preventDefault();
this.handleDeletePoint();
}
};
@@ -259,56 +277,101 @@ export class SVGPathEditor extends InnerEditor {
}
public onUnload(): void {
+ this.clearHoverAnchor();
this.closeInnerEditor();
this.removeKeyEventListener();
// 4. 卸载控制点
this.editBox.remove(this.view);
}
- /**
- * 删除选中顶点
- *
- * @memberof SVGPathEditor
- */
+ /** 删除选中的线段或顶点 */
handleDeletePoint() {
+ if (this.selectControlLine) {
+ this.points = deletePathSegment(this.points, this.selectControlLine);
+ this.selectControlLine = undefined;
+ this.reDraw();
+ return;
+ }
if (this.selectPointIdx === undefined) return;
this.points.splice(this.selectPointIdx, 1);
this.selectPointIdx = undefined;
this.reDraw();
}
- /**
- * 处理控制线的 hover 事件
- *
- * @param {*} e
- * @memberof SVGPathEditor
- */
- handleControlLineEnter(e: any) {
- e.target.state = 'hover';
- this.hoverControlLine = e.target;
+ private isSameSegment(left?: IPathSegment, right?: IPathSegment) {
+ return !!(
+ left &&
+ right &&
+ left.fromIndex === right.fromIndex &&
+ left.toIndex === right.toIndex &&
+ left.insertIndex === right.insertIndex &&
+ !!left.closing === !!right.closing
+ );
+ }
- const { isStraight, start, end } = e.target.data;
- if (isStraight) {
- const centerX = (start.x + end.x) / 2;
- const centerY = (start.y + end.y) / 2;
- const anchorPoint = new Ellipse({
- x: centerX,
- y: centerY,
- width: this.pointRadius,
- height: this.pointRadius,
- offsetX: -this.pointRadius,
- offsetY: -this.pointRadius,
- cursor: 'pointer',
- data: {
- isAnchor: true,
- index: e.target.data.index,
- },
+ private clearRemoveAnchorTimer() {
+ if (this.removeAnchorTimer) clearTimeout(this.removeAnchorTimer);
+ this.removeAnchorTimer = undefined;
+ }
+
+ private clearHoverAnchor() {
+ this.clearRemoveAnchorTimer();
+ if (this.hoverAnchorPoint?.parent) {
+ this.controlLineBox.remove(this.hoverAnchorPoint);
+ }
+ this.hoverAnchorPoint = undefined;
+ }
+
+ private scheduleHoverAnchorRemoval() {
+ this.clearRemoveAnchorTimer();
+ const anchorPoint = this.hoverAnchorPoint;
+ this.removeAnchorTimer = setTimeout(() => {
+ if (this.hoverAnchorPoint === anchorPoint) this.clearHoverAnchor();
+ }, 80);
+ }
+
+ private updateHoverAnchor(
+ controlLine: Path,
+ pointer?: { x: number; y: number }
+ ) {
+ const segment = controlLine.data.segment as IPathSegment;
+ const location =
+ pointer && this.isSameSegment(segment, this.selectControlLine)
+ ? getClosestSegmentLocation(this.points, segment, pointer)
+ : { point: getPathSegmentPoint(this.points, segment, 0.5), t: 0.5 };
+ const size = this.pointRadius * 1.6;
+ if (!this.hoverAnchorPoint) {
+ this.hoverAnchorPoint = new Ellipse({
...this.pointStyle,
+ width: size,
+ height: size,
+ offsetX: -size / 2,
+ offsetY: -size / 2,
+ fill: 'white',
+ stroke: '#2193FF',
+ strokeWidth: 2,
+ hitRadius: 4,
+ cursor: 'copy',
+ data: { isAnchor: true },
});
-
- this.controlLineBox.add(anchorPoint);
- e.target.data.anchorPoint = anchorPoint;
+ this.controlLineBox.add(this.hoverAnchorPoint);
}
+ this.hoverAnchorPoint.set({ x: location.point.x, y: location.point.y });
+ this.hoverAnchorPoint.data.segment = segment;
+ this.hoverAnchorPoint.data.t = location.t;
+ }
+
+ handleControlLineEnter(e: any) {
+ this.clearRemoveAnchorTimer();
+ if (e.target.data.isAnchor) return;
+ if (!e.target.data.isControlLine) return;
+
+ const segment = e.target.data.segment as IPathSegment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : 'hover';
+ this.hoverControlLine = e.target;
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
}
/**
@@ -318,15 +381,37 @@ export class SVGPathEditor extends InnerEditor {
* @memberof SVGPathEditor
*/
handleControlLineLeave(e: any) {
- if (this.hoverControlLine) {
- this.hoverControlLine.state = undefined;
+ if (e.target.data.isControlLine) {
+ const segment = e.target.data.segment as IPathSegment;
+ e.target.state = this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined;
+ if (this.hoverControlLine === e.target) this.hoverControlLine = undefined;
}
+ this.scheduleHoverAnchorRemoval();
+ }
+
+ handleControlLineMove(e: any) {
+ if (!e.target.data.isControlLine) return;
+ this.updateHoverAnchor(e.target, e.getInnerPoint(this.controlLineBox));
+ }
- if (e.target.data.anchorPoint) {
- this.controlLineBox.remove(e.target.data.anchorPoint);
- } else if (e.target.data.isAnchor) {
- this.controlLineBox.remove(e.target);
+ handleControlLineTap(e: any) {
+ if (e.target.data.isAnchor) {
+ const segment = e.target.data.segment as IPathSegment;
+ this.points = splitPathSegment(this.points, segment, e.target.data.t);
+ this.selectControlLine = undefined;
+ this.selectPointIdx = segment.insertIndex;
+ this.clearHoverAnchor();
+ this.reDraw();
+ return;
}
+ if (!e.target.data.isControlLine) return;
+
+ this.handleSelectPoint();
+ this.selectControlLine = e.target.data.segment as IPathSegment;
+ this.updateControl();
+ this.updateControlLines();
}
/**
@@ -335,13 +420,24 @@ export class SVGPathEditor extends InnerEditor {
* @memberof SVGPathEditor
*/
updateControlLines() {
+ this.clearHoverAnchor();
const controlLines: Path[] = [];
- for (let i = 0; i < this.points.length; i++) {
- const currentPoint = this.points[i];
- const prevPoint =
- i === 0 ? this.points[this.points.length - 1] : this.points[i - 1];
+ const addControlLine = (
+ fromIndex: number,
+ toIndex: number,
+ insertIndex: number,
+ closing = false
+ ) => {
+ const currentPoint = this.points[toIndex];
+ const prevPoint = this.points[fromIndex];
const { x, y, x1, y1 } = currentPoint;
const { x: prevX, y: prevY, x2: prevX2, y2: prevY2 } = prevPoint;
+ const segment: IPathSegment = {
+ fromIndex,
+ toIndex,
+ insertIndex,
+ closing,
+ };
let path = ``;
if (
@@ -362,46 +458,45 @@ export class SVGPathEditor extends InnerEditor {
const controlLine = new Path({
path,
stroke: 'transparent',
- strokeWidth: 2,
+ strokeWidth: 1.5,
+ hitRadius: 7,
+ hitStroke: 'all',
+ cursor: 'pointer',
states: {
hover: {
- stroke: 'red',
+ stroke: '#2193FF',
+ strokeWidth: 2,
+ },
+ selected: {
+ stroke: '#EF4444',
+ strokeWidth: 2,
},
},
+ state: this.isSameSegment(segment, this.selectControlLine)
+ ? 'selected'
+ : undefined,
editable: false,
});
+ controlLine.data.isControlLine = true;
+ controlLine.data.segment = segment;
+ controlLines.push(controlLine);
+ };
- if (path.indexOf('L') > -1) {
- controlLine.data.isStraight = true; // 直线
- controlLine.data.start = { x: prevX, y: prevY };
- controlLine.data.end = { x, y };
- controlLine.data.index = i;
+ let subpathStartIndex = 0;
+ for (let i = 0; i < this.points.length; i++) {
+ const currentPoint = this.points[i];
+ if (i === 0 || currentPoint.move || currentPoint.type === 'start') {
+ subpathStartIndex = i;
+ } else {
+ addControlLine(i - 1, i, i);
+ }
+ if (currentPoint.closed) {
+ addControlLine(i, subpathStartIndex, i + 1, true);
}
-
- controlLines.push(controlLine);
}
this.controlLineBox.set({ children: controlLines });
}
- /**
- * 在指定位置后添加顶点
- *
- * @param {number} index
- * @param {number} x
- * @param {number} y
- * @memberof SVGPathEditor
- */
- addPointAfter(index: number, x: number, y: number) {
- const newPoint: IPoint = { x, y };
- this.points.splice(index, 0, newPoint);
- if (index < this.selectPointIdx) {
- this.selectPointIdx++;
- }
- this.handleSelectPoint();
- this.selectPointIdx = index;
- this.reDraw();
- }
-
/**
* 计算初始数据的控制点模式
*
@@ -856,7 +951,9 @@ export class SVGPathEditor extends InnerEditor {
handlePointDown(e: any) {
const { innerId } = e.target;
const pointIdx = this.pointIdxMap.get(innerId);
+ this.selectControlLine = undefined;
this.handleSelectPoint(pointIdx.index);
+ this.updateControlLines();
}
/**
@@ -921,7 +1018,10 @@ export class SVGPathEditor extends InnerEditor {
if (prevSelectPoint) {
prevSelectPoint.state = 'unSelect';
}
- if (index === undefined) return;
+ if (index === undefined) {
+ this.selectPointIdx = undefined;
+ return;
+ }
this.selectPointIdx = index;
const selectPoint = this.pointsBox.children[index];
selectPoint.state = 'select';
diff --git a/src/type.ts b/src/type.ts
index 82e098c25333dc0f6b9fbf1c1d4f4cde6098daff..8d55fe561be3397a294e6def9964219bfe1cd8b8 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -19,6 +19,17 @@ export interface IPoint {
mode?: MirrorMode;
// 顶点类型
type?: PointType;
+ // 是否是一个子路径的起点
+ move?: boolean;
+ // 是否从当前点闭合到子路径起点
+ closed?: boolean;
+}
+
+export interface IPathSegment {
+ fromIndex: number;
+ toIndex: number;
+ insertIndex: number;
+ closing?: boolean;
}
export type AnyObject = Record<string, any>;
diff --git a/src/utils.ts b/src/utils.ts
index 32d06923d6df2db0f8f79c89e0ec06f383e265b3..faa9b7f9aeab03b9542e67622705ffadb82f5798 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -67,6 +67,7 @@ export function pathData2CommandObject(
*/
export function pathData2Point(path: IPathCommandData) {
const pointData: IPoint[] = [];
+ let subpathStartIndex = -1;
for (let i = 0; i < path.length; i++) {
const point: IPoint = {} as IPoint;
@@ -74,6 +75,8 @@ export function pathData2Point(path: IPathCommandData) {
point.x = path[++i];
point.y = path[++i];
point.type = 'start';
+ point.move = true;
+ subpathStartIndex = pointData.length;
} else if (path[i] === L) {
point.x = path[++i];
point.y = path[++i];
@@ -90,29 +93,47 @@ export function pathData2Point(path: IPathCommandData) {
point.x = path[++i];
point.y = path[++i];
} else if (path[i] === Z) {
- point.x = pointData[0].x;
- point.y = pointData[0].y;
+ const start = pointData[subpathStartIndex];
+ const end = pointData[pointData.length - 1];
+ if (!start || !end) continue;
+
+ if (
+ pointData.length - 1 > subpathStartIndex &&
+ start.x === end.x &&
+ start.y === end.y
+ ) {
+ pointData.pop();
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
+ }
+ }
+ pointData[pointData.length - 1].closed = true;
+ continue;
}
pointData.push(point);
}
- const head = pointData[0];
- const tail = pointData[pointData.length - 1];
- if (tail.type !== 'end') {
- // 如果起点和终点重合, 则删除一个重复点, 并将他的控制点赋值给前后两个点
- if (head.x === tail.x && head.y === tail.y) {
- pointData.pop();
-
- if (tail.x2 !== undefined && tail.x2 !== undefined) {
- pointData[pointData.length - 1].x2 = tail.x2;
- pointData[pointData.length - 1].y2 = tail.y2;
- }
-
- if (tail.x1 !== undefined && tail.y1 !== undefined) {
- pointData[0].x1 = tail.x1;
- pointData[0].y1 = tail.y1;
+ for (let endIndex = pointData.length - 1; endIndex > 0; ) {
+ let startIndex = endIndex;
+ while (startIndex > 0 && !pointData[startIndex].move) startIndex--;
+ const start = pointData[startIndex];
+ const end = pointData[endIndex];
+ if (
+ endIndex > startIndex &&
+ !end.closed &&
+ start.x === end.x &&
+ start.y === end.y
+ ) {
+ pointData.splice(endIndex, 1);
+ const newEnd = pointData[endIndex - 1];
+ newEnd.closed = true;
+ if (end.x1 !== undefined && end.y1 !== undefined) {
+ start.x1 = end.x1;
+ start.y1 = end.y1;
}
}
+ endIndex = startIndex - 1;
}
return pointData;
@@ -127,44 +148,44 @@ export function pathData2Point(path: IPathCommandData) {
*/
export function point2PathData(points: IPoint[]) {
const pathData: IPathCommandData = [];
+ if (!points.length) return pathData;
- const getNext = (index: number) => {
- if (index === points.length - 1) {
- return points[0];
+ const pushSegment = (prev: IPoint, next: IPoint) => {
+ const { x, y, x1, y1 } = next;
+ if (
+ prev.x2 === undefined &&
+ prev.y2 === undefined &&
+ x1 === undefined &&
+ y1 === undefined
+ ) {
+ pathData.push(L, x, y);
+ } else if (
+ prev.x2 !== undefined &&
+ prev.y2 !== undefined &&
+ x1 !== undefined &&
+ y1 !== undefined
+ ) {
+ pathData.push(C, prev.x2, prev.y2, x1, y1, x, y);
+ } else if (prev.x2 !== undefined && prev.y2 !== undefined) {
+ pathData.push(Q, prev.x2, prev.y2, x, y);
+ } else if (x1 !== undefined && y1 !== undefined) {
+ pathData.push(Q, x1, y1, x, y);
}
- return points[index + 1];
};
- pathData.push(M, points[0].x, points[0].y);
-
+ let subpathStart = points[0];
for (let i = 0; i < points.length; i++) {
- const prev = points[i];
- const next = getNext(i);
-
- const { type, x, y, x1, y1 } = next;
-
- if (type === 'end') {
- continue;
+ const point = points[i];
+ if (i === 0 || point.move || point.type === 'start') {
+ pathData.push(M, point.x, point.y);
+ subpathStart = point;
} else {
- if (
- prev.x2 === undefined &&
- prev.y2 === undefined &&
- x1 === undefined &&
- y1 === undefined
- ) {
- pathData.push(L, x, y);
- } else if (
- prev.x2 !== undefined &&
- prev.y2 !== undefined &&
- x1 !== undefined &&
- y1 !== undefined
- ) {
- pathData.push(C, prev.x2 || 0, prev.y2 || 0, x1, y1, x, y);
- } else if (prev.x2 !== undefined && prev.y2 !== undefined) {
- pathData.push(Q, prev.x2 || 0, prev.y2 || 0, x, y);
- } else if (x1 !== undefined && y1 !== undefined) {
- pathData.push(Q, x1, y1, x, y);
- }
+ pushSegment(points[i - 1], point);
+ }
+
+ if (point.closed) {
+ pushSegment(point, subpathStart);
+ pathData.push(Z);
}
}
diff --git a/types/index.d.ts b/types/index.d.ts
index 2e48d779073af5567503a4379a84c89cfe9e4946..1d84def482b40fd3f9f301861629537445d8b84b 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1,6 +1,27 @@
import { IEventTarget, IUI } from '@leafer-ui/interface';
import { Event } from '@leafer-ui/core';
+type PointType = 'start' | 'end';
+type MirrorMode = 'no-mirror' | 'mirror-angle' | 'mirror-angle-length';
+interface IPoint {
+ x?: number;
+ y?: number;
+ x1?: number;
+ y1?: number;
+ x2?: number;
+ y2?: number;
+ mode?: MirrorMode;
+ type?: PointType;
+ move?: boolean;
+ closed?: boolean;
+}
+interface IPathSegment {
+ fromIndex: number;
+ toIndex: number;
+ insertIndex: number;
+ closing?: boolean;
+}
+
interface IPathEditorEvent {
readonly target?: IUI;
readonly value?: IUI | IUI[];
@@ -14,4 +35,38 @@ declare class PathEditorEvent extends Event {
constructor(type: string, data?: IPathEditorEvent);
}
-export { PathEditorEvent };
+interface ICoordinate {
+ x: number;
+ y: number;
+}
+interface IClosestSegmentLocation {
+ point: ICoordinate;
+ t: number;
+}
+declare function getClosestSegmentLocation(points: IPoint[], segment: IPathSegment, target: ICoordinate): IClosestSegmentLocation;
+declare function splitPathSegment(points: IPoint[], segment: IPathSegment, rawT: number): {
+ x?: number;
+ y?: number;
+ x1?: number;
+ y1?: number;
+ x2?: number;
+ y2?: number;
+ mode?: MirrorMode;
+ type?: "start" | "end";
+ move?: boolean;
+ closed?: boolean;
+}[];
+declare function deletePathSegment(points: IPoint[], segment: IPathSegment): {
+ x?: number;
+ y?: number;
+ x1?: number;
+ y1?: number;
+ x2?: number;
+ y2?: number;
+ mode?: MirrorMode;
+ type?: "start" | "end";
+ move?: boolean;
+ closed?: boolean;
+}[];
+
+export { type IPathSegment, PathEditorEvent, deletePathSegment, getClosestSegmentLocation, splitPathSegment };