d424b56076
- Implemented CanvasPathEditor component for editing paths on the canvas. - Created editablePath module to handle path data manipulation and conversion to SVG. - Introduced pathPen module for managing path drawing with a pen tool, including anchor management and path data generation. - Added tests for editablePath and pathPen functionalities to ensure correctness of path editing and drawing behavior.
1964 lines
137 KiB
Diff
1964 lines
137 KiB
Diff
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(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const i=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:i})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:i}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,i)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:i}=this.editTarget,{scaleX:e,scaleY:o}=i;this.calculatePointStyle(i),!this.transform||e===this.transform.worldTransform.scaleX&&o===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},i),boxBounds:Object.assign({},t)}}onUnload(){this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}handleControlLineEnter(t){t.target.state="hover",this.hoverControlLine=t.target;const{isStraight:i,start:e,end:o}=t.target.data;if(i){const i=(e.x+o.x)/2,s=(e.y+o.y)/2,n=new r(Object.assign({x:i,y:s,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius,offsetY:-this.pointRadius,cursor:"pointer",data:{isAnchor:!0,index:t.target.data.index}},this.pointStyle));this.controlLineBox.add(n),t.target.data.anchorPoint=n}}handleControlLineLeave(t){this.hoverControlLine&&(this.hoverControlLine.state=void 0),t.target.data.anchorPoint?this.controlLineBox.remove(t.target.data.anchorPoint):t.target.data.isAnchor&&this.controlLineBox.remove(t.target)}updateControlLines(){const t=[];for(let i=0;i<this.points.length;i++){const o=this.points[i],s=0===i?this.points[this.points.length-1]:this.points[i-1],{x:n,y:r,x1:h,y1:a}=o,{x:d,y:l,x2:c,y2:x}=s;let p="";p=void 0!==h&&void 0!==a&&void 0!==c&&void 0!==x?`M ${d} ${l} C ${c} ${x} ${h} ${a} ${n} ${r}`:void 0!==c&&void 0!==x?`M ${d} ${l} Q ${c} ${x} ${n} ${r}`:void 0!==h&&void 0!==a?`M ${d} ${l} Q ${h} ${a} ${n} ${r}`:`M ${d} ${l} L ${n} ${r}`;const y=new e({path:p,stroke:"transparent",strokeWidth:2,states:{hover:{stroke:"red"}},editable:!1});p.indexOf("L")>-1&&(y.data.isStraight=!0,y.data.start={x:d,y:l},y.data.end={x:n,y:r},y.data.index=i),t.push(y)}this.controlLineBox.set({children:t})}addPointAfter(t,i,e){const o={x:i,y:e};this.points.splice(t,0,o),t<this.selectPointIdx&&this.selectPointIdx++,this.handleSelectPoint(),this.selectPointIdx=t,this.reDraw()}calculateMirrorMode(t){return t.map((t=>{const{x:i,y:e,x1:o,y1:s,x2:n,y2:r}=t;if(void 0!==o&&void 0!==s&&null!=n&&void 0!==r){let h="no-mirror";if(0===f(i,e,o,s,n,r)){h="mirror-angle";g(u(i,e,o,s),1)===g(u(i,e,n,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:i,scaleY:e}=t;if(i===e){const t=v(6*i,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=v(1.4*i,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:i}=t.target,e=this.pointIdxMap.get(i),o=this.points[e.index];if(void 0!==o.x1&&void 0!==o.y1||void 0!==o.x2&&void 0!==o.y2)o.x1=void 0,o.x2=void 0,o.y1=void 0,o.y2=void 0;else{const[t,i,s,n]=this.getDefaultControls(this.points[e.leftIdx],this.points[e.index],this.points[e.rightIdx]);o.x1=t,o.y1=i,o.x2=s,o.y2=n,o.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,i,e){const o=.6*u(e.x,e.y,t.x,t.y),s=e.x-t.x,n=e.y-t.y,r=Math.sqrt(s*s+n*n),h=s/r,a=n/r,d=o/2,l={x:i.x+d*h,y:i.y+d*a},c={x:i.x-d*h,y:i.y-d*a};return[c.x,c.y,l.x,l.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(t){const{isLeft:i,isRight:o}=t.target.data;if("Ellipse"!==t.target.tag)return;const{moveX:s,moveY:n}=t,{innerId:h,x:a,y:d}=t.target,{x:l,y:c}=this.getTransformMovePosition(s,n),x=a+l,p=d+c;t.target.set({x:x,y:p});const y=this.controlMap.get(h);if(y){this.isCtrl()&&(y.mode="no-mirror"),i?(y.x1=x,y.y1=p):o&&(y.x2=x,y.y2=p);const{x:t,y:s,x1:n,y1:h,x2:a,y2:d,mode:g}=y;if(void 0!==n&&void 0!==h&&void 0!==a&&void 0!==d&&"mirror-angle-length"!==g){if("mirror-angle"!==g){const e=120*v(u(t,s,n,h)+u(t,s,a,d),40,800)/150;if(f(t,s,n,h,a,d)<=e){let e,o,r,l;this.controlAbsorbStatus.isMirrorAngle=!0,i?(e=a,o=d,r=n,l=h):(e=n,o=h,r=a,l=d);const{x:c,y:x}=this.getMirrorPoint(e,o,t,s,r,l);i?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===g){const e=u(t,s,n,h),o=u(t,s,a,d),r=this.pointRadius/2;Math.abs(e-o)<=r?(i?(y.x1=t-(a-t),y.y1=s-(d-s)):(y.x2=t-(n-t),y.y2=s-(h-s)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)i?(y.x2-=l,y.y2-=c):o&&(y.x1-=l,y.y1-=c);else if("mirror-angle"===y.mode){let t=y.x1,e=y.y1,s=y.x2,n=y.y2;o&&(t=y.x2,e=y.y2,s=y.x1,n=y.y1);const{x:r,y:h}=y,{x:a,y:d}=this.getMirrorPoint(t,e,r,h,s,n);i?(y.x2=a,y.y2=d):(y.x1=a,y.y1=d)}let b,m,w=[];if(i?(b=y.x1,m=y.y1):(b=y.x2,m=y.y2),this.controlAbsorbStatus.isMirrorAngle){const i=new e({path:`M ${t} ${s} L ${b} ${m}`,stroke:"red",strokeWidth:1,editable:!1});w.push(i)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const t=new r({x:b,y:m,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});t.fill="red",w.push(t)}this.controlAbsorbBox.set({children:[...w]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,i,e,o,s,n){const r=u(s,n,e,o),h=t-e,a=i-o,d=Math.sqrt(h*h+a*a);return{x:e-h/d*r,y:o-a/d*r}}handleControlDragEnd(t){const{innerId:i}=t.target,e=this.controlMap.get(i);if(e){const{x1:t,y1:i,x2:o,y2:s}=e;void 0!==t&&void 0!==i&&void 0!==o&&void 0!==s&&(this.controlAbsorbStatus.isMirrorAngle&&(e.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===e.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(e.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:i}=t.target,e=this.pointIdxMap.get(i);this.handleSelectPoint(e.index)}handlePointDrag(t){const{moveX:i,moveY:e}=t,{innerId:o}=t.target;let{x:s,y:n}=t.target;const{x:r,y:h}=this.getTransformMovePosition(i,e),a=s+r,d=n+h;t.target.set({x:a,y:d});const l=this.pointIdxMap.get(o);if(l){const{index:t}=l,i=this.points[t];i.x=a,i.y=d,void 0!==i.x1&&(i.x1+=r),void 0!==i.y1&&(i.y1+=h),void 0!==i.x2&&(i.x2+=r),void 0!==i.y2&&(i.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const i=this.pointsBox.children[this.selectPointIdx];if(i&&(i.state="unSelect"),void 0===t)return;this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,i,e){i=i||this.editTarget.worldTransform,e=e||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=i;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=e;return t.map((t=>{const i=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==i[t]&&(i[t]=(i[t]-n)*o)})),["y","y1","y2"].forEach((t=>{void 0!==i[t]&&(i[t]=(i[t]-r)*s)})),i}))}outerTransformPoints(t,i,e){i=i||this.editTarget.worldTransform,e=e||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=i;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=e;return t.map((t=>{const i=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==i[t]&&(i[t]=g(i[t]/o+n))})),["y","y1","y2"].forEach((t=>{void 0!==i[t]&&(i[t]=g(i[t]/s+r))})),i}))}getTransformMovePosition(t,i){const{worldTransform:e}=this.transform;let{a:o,b:s,c:n,d:r,scaleX:h,scaleY:a}=e;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*o+(i/=a)*s,y:t*n+i*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const i=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=y(this.outerTransformPoints(this.points)),this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:i})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:y(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let t,i,e;const o=new Map(this.pointIdxMap),s=this.selectPointIdx,n=this.points.map(((n,h)=>{const{x:a=0,y:d=0,type:l}=n;if("end"===l)return null;const c=new r(Object.assign({x:a,y:d,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:s===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:i,rightIdx:h};return t||(t=x),o.set(c.innerId,x),e&&(e.rightIdx=h),e=x,i=h,c})).filter(Boolean);t.leftIdx=i,e.rightIdx=t.index,this.pointIdxMap=o,this.pointsBox.set({children:n})}drawStroke(){const t=new e({stroke:"#2193FF",strokeWidth:1,path:y(this.points),editable:!1});this.strokeBox.set({children:[t]})}createControl(t){if(!t)return[];const{x:i,y:o,x1:s,x2:n,y1:h,y2:a}=t;let d,l;void 0!==s&&void 0!==h&&(d=new r(Object.assign(Object.assign({x:s,y:h},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,t)),void 0!==n&&void 0!==a&&(l=new r(Object.assign(Object.assign({x:n,y:a},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,t));let c,x="";return d&&l?x=`M ${s} ${h} L ${i} ${o} L ${n} ${a}`:d?x=`M ${s} ${h} L ${i} ${o}`:l&&(x=`M ${i} ${o} L ${n} ${a}`),x&&(c=new e({path:x,stroke:"#2193FF",strokeWidth:1,editable:!1})),[c,d,l].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],i=this.points[this.selectPointIdx-1],e=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const o=this.createControl(i),s=this.createControl(t),n=this.createControl(e);this.controlsBox.set({children:[...o,...s,...n]})}};m=function(t,i,e,o){var s,n=arguments.length,r=n<3?i:null===o?o=Object.getOwnPropertyDescriptor(i,e):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,i,e,o);else for(var h=t.length-1;h>=0;h--)(s=t[h])&&(r=(n<3?s(r):n>3?s(i,e,r):s(i,e))||r);return n>3&&r&&Object.defineProperty(i,e,r),r}([h()],m),e.setEditInner("SVGPathEditor");export{b as PathEditorEvent};
|
|
+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,this.handleControlDrag.bind(this)),this.controlsBox.on_(n.END,this.handleControlDragEnd.bind(this)),this.controlsBox.on_(s.DOWN,this.handleControlDown.bind(this)),this.controlsBox.on_(s.UP,this.handleControlUp.bind(this)),this.editor.app.on_(s.DOUBLE_TAP,(t=>{t.target!==this.editTarget&&this.editor.closeInnerEditor()})),this.controlLineBox.on_(s.ENTER,this.handleControlLineEnter.bind(this)),this.controlLineBox.on_(s.LEAVE,this.handleControlLineLeave.bind(this)),this.controlLineBox.on_(s.MOVE,this.handleControlLineMove.bind(this)),this.controlLineBox.on_(s.TAP,this.handleControlLineTap.bind(this))]}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,46].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&(t.preventDefault(),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=[];let o=-1;for(let i=0;i<t.length;i++){const n={};if(t[i]===l)n.x=t[++i],n.y=t[++i],n.type="start",n.move=!0,o=e.length;else if(t[i]===d)n.x=t[++i],n.y=t[++i];else if(t[i]===c)e[e.length-1].x2=t[++i],e[e.length-1].y2=t[++i],n.x1=t[++i],n.y1=t[++i],n.x=t[++i],n.y=t[++i];else if(t[i]===x)e[e.length-1].x2=t[++i],e[e.length-1].y2=t[++i],n.x=t[++i],n.y=t[++i];else if(t[i]===g){const t=e[o],i=e[e.length-1];if(!t||!i)continue;e.length-1>o&&t.x===i.x&&t.y===i.y&&(e.pop(),void 0!==i.x1&&void 0!==i.y1&&(t.x1=i.x1,t.y1=i.y1)),e[e.length-1].closed=!0;continue}e.push(n)}for(let t=e.length-1;t>0;){let o=t;for(;o>0&&!e[o].move;)o--;const i=e[o],n=e[t];t>o&&!n.closed&&i.x===n.x&&i.y===n.y&&(e.splice(t,1),e[t-1].closed=!0,void 0!==n.x1&&void 0!==n.y1&&(i.x1=n.x1,i.y1=n.y1)),t=o-1}return e}(this.editTarget.getPath())),this.points=this.calculateMirrorMode(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const e=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new I(I.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:e}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,e)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:e}=this.editTarget,{scaleX:o,scaleY:i}=e;this.calculatePointStyle(e),!this.transform||o===this.transform.worldTransform.scaleX&&i===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},e),boxBounds:Object.assign({},t)}}onUnload(){this.clearHoverAnchor(),this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){if(this.selectControlLine)return this.points=L(this.points,this.selectControlLine),this.selectControlLine=void 0,void this.reDraw();void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}isSameSegment(t,e){return!(!t||!e||t.fromIndex!==e.fromIndex||t.toIndex!==e.toIndex||t.insertIndex!==e.insertIndex||!!t.closing!=!!e.closing)}clearRemoveAnchorTimer(){this.removeAnchorTimer&&clearTimeout(this.removeAnchorTimer),this.removeAnchorTimer=void 0}clearHoverAnchor(){var t;this.clearRemoveAnchorTimer(),(null===(t=this.hoverAnchorPoint)||void 0===t?void 0:t.parent)&&this.controlLineBox.remove(this.hoverAnchorPoint),this.hoverAnchorPoint=void 0}scheduleHoverAnchorRemoval(){this.clearRemoveAnchorTimer();const t=this.hoverAnchorPoint;this.removeAnchorTimer=setTimeout((()=>{this.hoverAnchorPoint===t&&this.clearHoverAnchor()}),80)}updateHoverAnchor(t,e){const o=t.data.segment,i=e&&this.isSameSegment(o,this.selectControlLine)?M(this.points,o,e):{point:C(this.points,o,.5),t:.5},n=1.6*this.pointRadius;this.hoverAnchorPoint||(this.hoverAnchorPoint=new r(Object.assign(Object.assign({},this.pointStyle),{width:n,height:n,offsetX:-n/2,offsetY:-n/2,fill:"white",stroke:"#2193FF",strokeWidth:2,hitRadius:4,cursor:"copy",data:{isAnchor:!0}})),this.controlLineBox.add(this.hoverAnchorPoint)),this.hoverAnchorPoint.set({x:i.point.x,y:i.point.y}),this.hoverAnchorPoint.data.segment=o,this.hoverAnchorPoint.data.t=i.t}handleControlLineEnter(t){if(this.clearRemoveAnchorTimer(),t.target.data.isAnchor)return;if(!t.target.data.isControlLine)return;const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":"hover",this.hoverControlLine=t.target,this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineLeave(t){if(t.target.data.isControlLine){const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":void 0,this.hoverControlLine===t.target&&(this.hoverControlLine=void 0)}this.scheduleHoverAnchorRemoval()}handleControlLineMove(t){t.target.data.isControlLine&&this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineTap(t){if(t.target.data.isAnchor){const e=t.target.data.segment;return this.points=A(this.points,e,t.target.data.t),this.selectControlLine=void 0,this.selectPointIdx=e.insertIndex,this.clearHoverAnchor(),void this.reDraw()}t.target.data.isControlLine&&(this.handleSelectPoint(),this.selectControlLine=t.target.data.segment,this.updateControl(),this.updateControlLines())}updateControlLines(){this.clearHoverAnchor();const t=[],e=(e,i,n,s=!1)=>{const r=this.points[i],h=this.points[e],{x:a,y:l,x1:d,y1:c}=r,{x:x,y:g,x2:y,y2:p}=h,u={fromIndex:e,toIndex:i,insertIndex:n,closing:s};let v="";v=void 0!==d&&void 0!==c&&void 0!==y&&void 0!==p?`M ${x} ${g} C ${y} ${p} ${d} ${c} ${a} ${l}`:void 0!==y&&void 0!==p?`M ${x} ${g} Q ${y} ${p} ${a} ${l}`:void 0!==d&&void 0!==c?`M ${x} ${g} Q ${d} ${c} ${a} ${l}`:`M ${x} ${g} L ${a} ${l}`;const f=new o({path:v,stroke:"transparent",strokeWidth:1.5,hitRadius:7,hitStroke:"all",cursor:"pointer",states:{hover:{stroke:"#2193FF",strokeWidth:2},selected:{stroke:"#EF4444",strokeWidth:2}},state:this.isSameSegment(u,this.selectControlLine)?"selected":void 0,editable:!1});f.data.isControlLine=!0,f.data.segment=u,t.push(f)};let i=0;for(let t=0;t<this.points.length;t++){const o=this.points[t];0===t||o.move||"start"===o.type?i=t:e(t-1,t,t),o.closed&&e(t,i,t+1,!0)}this.controlLineBox.set({children:t})}calculateMirrorMode(t){return t.map((t=>{const{x:e,y:o,x1:i,y1:n,x2:s,y2:r}=t;if(void 0!==i&&void 0!==n&&null!=s&&void 0!==r){let h="no-mirror";if(0===v(e,o,i,n,s,r)){h="mirror-angle";p(u(e,o,i,n),1)===p(u(e,o,s,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:o}=t;if(e===o){const t=f(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=f(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,o=this.pointIdxMap.get(e),i=this.points[o.index];if(void 0!==i.x1&&void 0!==i.y1||void 0!==i.x2&&void 0!==i.y2)i.x1=void 0,i.x2=void 0,i.y1=void 0,i.y2=void 0;else{const[t,e,n,s]=this.getDefaultControls(this.points[o.leftIdx],this.points[o.index],this.points[o.rightIdx]);i.x1=t,i.y1=e,i.x2=n,i.y2=s,i.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,o){const i=.6*u(o.x,o.y,t.x,t.y),n=o.x-t.x,s=o.y-t.y,r=Math.sqrt(n*n+s*s),h=n/r,a=s/r,l=i/2,d={x:e.x+l*h,y:e.y+l*a},c={x:e.x-l*h,y:e.y-l*a};return[c.x,c.y,d.x,d.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(t){const{isLeft:e,isRight:i}=t.target.data;if("Ellipse"!==t.target.tag)return;const{moveX:n,moveY:s}=t,{innerId:h,x:a,y:l}=t.target,{x:d,y:c}=this.getTransformMovePosition(n,s),x=a+d,g=l+c;t.target.set({x:x,y:g});const y=this.controlMap.get(h);if(y){this.isCtrl()&&(y.mode="no-mirror"),e?(y.x1=x,y.y1=g):i&&(y.x2=x,y.y2=g);const{x:t,y:n,x1:s,y1:h,x2:a,y2:l,mode:p}=y;if(void 0!==s&&void 0!==h&&void 0!==a&&void 0!==l&&"mirror-angle-length"!==p){if("mirror-angle"!==p){const o=120*f(u(t,n,s,h)+u(t,n,a,l),40,800)/150;if(v(t,n,s,h,a,l)<=o){let o,i,r,d;this.controlAbsorbStatus.isMirrorAngle=!0,e?(o=a,i=l,r=s,d=h):(o=s,i=h,r=a,d=l);const{x:c,y:x}=this.getMirrorPoint(o,i,t,n,r,d);e?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===p){const o=u(t,n,s,h),i=u(t,n,a,l),r=this.pointRadius/2;Math.abs(o-i)<=r?(e?(y.x1=t-(a-t),y.y1=n-(l-n)):(y.x2=t-(s-t),y.y2=n-(h-n)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)e?(y.x2-=d,y.y2-=c):i&&(y.x1-=d,y.y1-=c);else if("mirror-angle"===y.mode){let t=y.x1,o=y.y1,n=y.x2,s=y.y2;i&&(t=y.x2,o=y.y2,n=y.x1,s=y.y1);const{x:r,y:h}=y,{x:a,y:l}=this.getMirrorPoint(t,o,r,h,n,s);e?(y.x2=a,y.y2=l):(y.x1=a,y.y1=l)}let m,b,P=[];if(e?(m=y.x1,b=y.y1):(m=y.x2,b=y.y2),this.controlAbsorbStatus.isMirrorAngle){const e=new o({path:`M ${t} ${n} L ${m} ${b}`,stroke:"red",strokeWidth:1,editable:!1});P.push(e)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const t=new r({x:m,y:b,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});t.fill="red",P.push(t)}this.controlAbsorbBox.set({children:[...P]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,o,i,n,s){const r=u(n,s,o,i),h=t-o,a=e-i,l=Math.sqrt(h*h+a*a);return{x:o-h/l*r,y:i-a/l*r}}handleControlDragEnd(t){const{innerId:e}=t.target,o=this.controlMap.get(e);if(o){const{x1:t,y1:e,x2:i,y2:n}=o;void 0!==t&&void 0!==e&&void 0!==i&&void 0!==n&&(this.controlAbsorbStatus.isMirrorAngle&&(o.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===o.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(o.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,o=this.pointIdxMap.get(e);this.selectControlLine=void 0,this.handleSelectPoint(o.index),this.updateControlLines()}handlePointDrag(t){const{moveX:e,moveY:o}=t,{innerId:i}=t.target;let{x:n,y:s}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,o),a=n+r,l=s+h;t.target.set({x:a,y:l});const d=this.pointIdxMap.get(i);if(d){const{index:t}=d,e=this.points[t];e.x=a,e.y=l,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return void(this.selectPointIdx=void 0);this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-s)*i)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*n)})),e}))}outerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=p(e[t]/i+s))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=p(e[t]/n+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:o}=this.transform;let{a:i,b:n,c:s,d:r,scaleX:h,scaleY:a}=o;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*i+(e/=a)*n,y:t*s+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=y(this.outerTransformPoints(this.points)),this.editor.emitEvent(new I(I.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:y(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let t,e,o;const i=new Map(this.pointIdxMap),n=this.selectPointIdx,s=this.points.map(((s,h)=>{const{x:a=0,y:l=0,type:d}=s;if("end"===d)return null;const c=new r(Object.assign({x:a,y:l,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:n===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:e,rightIdx:h};return t||(t=x),i.set(c.innerId,x),o&&(o.rightIdx=h),o=x,e=h,c})).filter(Boolean);t.leftIdx=e,o.rightIdx=t.index,this.pointIdxMap=i,this.pointsBox.set({children:s})}drawStroke(){const t=new o({stroke:"#2193FF",strokeWidth:1,path:y(this.points),editable:!1});this.strokeBox.set({children:[t]})}createControl(t){if(!t)return[];const{x:e,y:i,x1:n,x2:s,y1:h,y2:a}=t;let l,d;void 0!==n&&void 0!==h&&(l=new r(Object.assign(Object.assign({x:n,y:h},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,t)),void 0!==s&&void 0!==a&&(d=new r(Object.assign(Object.assign({x:s,y:a},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,t));let c,x="";return l&&d?x=`M ${n} ${h} L ${e} ${i} L ${s} ${a}`:l?x=`M ${n} ${h} L ${e} ${i}`:d&&(x=`M ${e} ${i} L ${s} ${a}`),x&&(c=new o({path:x,stroke:"#2193FF",strokeWidth:1,editable:!1})),[c,l,d].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],o=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const i=this.createControl(e),n=this.createControl(t),s=this.createControl(o);this.controlsBox.set({children:[...i,...n,...s]})}};T=function(t,e,o,i){var n,s=arguments.length,r=s<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,o):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,o,i);else for(var h=t.length-1;h>=0;h--)(n=t[h])&&(r=(s<3?n(r):s>3?n(e,o,r):n(e,o))||r);return s>3&&r&&Object.defineProperty(e,o,r),r}([h()],T),o.setEditInner("SVGPathEditor");export{I as PathEditorEvent,L as deletePathSegment,M as getClosestSegmentLocation,A as splitPathSegment};
|
|
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].x1=a.x1,e[0].y1=a.y1)),e}(this.editTarget.getPath())),this.points=this.calculateMirrorMode(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const e=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new x(x.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:e}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,e)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:e}=this.editTarget,{scaleX:i,scaleY:o}=e;this.calculatePointStyle(e),!this.transform||i===this.transform.worldTransform.scaleX&&o===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},e),boxBounds:Object.assign({},t)}}onUnload(){this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}handleControlLineEnter(e){e.target.state="hover",this.hoverControlLine=e.target;const{isStraight:i,start:o,end:s}=e.target.data;if(i){const i=(o.x+s.x)/2,n=(o.y+s.y)/2,r=new t.Ellipse(Object.assign({x:i,y:n,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius,offsetY:-this.pointRadius,cursor:"pointer",data:{isAnchor:!0,index:e.target.data.index}},this.pointStyle));this.controlLineBox.add(r),e.target.data.anchorPoint=r}}handleControlLineLeave(t){this.hoverControlLine&&(this.hoverControlLine.state=void 0),t.target.data.anchorPoint?this.controlLineBox.remove(t.target.data.anchorPoint):t.target.data.isAnchor&&this.controlLineBox.remove(t.target)}updateControlLines(){const e=[];for(let i=0;i<this.points.length;i++){const o=this.points[i],s=0===i?this.points[this.points.length-1]:this.points[i-1],{x:n,y:r,x1:h,y1:a}=o,{x:d,y:l,x2:c,y2:x}=s;let p="";p=void 0!==h&&void 0!==a&&void 0!==c&&void 0!==x?`M ${d} ${l} C ${c} ${x} ${h} ${a} ${n} ${r}`:void 0!==c&&void 0!==x?`M ${d} ${l} Q ${c} ${x} ${n} ${r}`:void 0!==h&&void 0!==a?`M ${d} ${l} Q ${h} ${a} ${n} ${r}`:`M ${d} ${l} L ${n} ${r}`;const y=new t.Path({path:p,stroke:"transparent",strokeWidth:2,states:{hover:{stroke:"red"}},editable:!1});p.indexOf("L")>-1&&(y.data.isStraight=!0,y.data.start={x:d,y:l},y.data.end={x:n,y:r},y.data.index=i),e.push(y)}this.controlLineBox.set({children:e})}addPointAfter(t,e,i){const o={x:e,y:i};this.points.splice(t,0,o),t<this.selectPointIdx&&this.selectPointIdx++,this.handleSelectPoint(),this.selectPointIdx=t,this.reDraw()}calculateMirrorMode(t){return t.map((t=>{const{x:e,y:i,x1:o,y1:s,x2:n,y2:r}=t;if(void 0!==o&&void 0!==s&&null!=n&&void 0!==r){let h="no-mirror";if(0===l(e,i,o,s,n,r)){h="mirror-angle";a(d(e,i,o,s),1)===a(d(e,i,n,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:i}=t;if(e===i){const t=c(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=c(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,i=this.pointIdxMap.get(e),o=this.points[i.index];if(void 0!==o.x1&&void 0!==o.y1||void 0!==o.x2&&void 0!==o.y2)o.x1=void 0,o.x2=void 0,o.y1=void 0,o.y2=void 0;else{const[t,e,s,n]=this.getDefaultControls(this.points[i.leftIdx],this.points[i.index],this.points[i.rightIdx]);o.x1=t,o.y1=e,o.x2=s,o.y2=n,o.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,i){const o=.6*d(i.x,i.y,t.x,t.y),s=i.x-t.x,n=i.y-t.y,r=Math.sqrt(s*s+n*n),h=s/r,a=n/r,l=o/2,c={x:e.x+l*h,y:e.y+l*a},x={x:e.x-l*h,y:e.y-l*a};return[x.x,x.y,c.x,c.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(e){const{isLeft:i,isRight:o}=e.target.data;if("Ellipse"!==e.target.tag)return;const{moveX:s,moveY:n}=e,{innerId:r,x:h,y:a}=e.target,{x:x,y:p}=this.getTransformMovePosition(s,n),y=h+x,g=a+p;e.target.set({x:y,y:g});const u=this.controlMap.get(r);if(u){this.isCtrl()&&(u.mode="no-mirror"),i?(u.x1=y,u.y1=g):o&&(u.x2=y,u.y2=g);const{x:e,y:s,x1:n,y1:r,x2:h,y2:a,mode:v}=u;if(void 0!==n&&void 0!==r&&void 0!==h&&void 0!==a&&"mirror-angle-length"!==v){if("mirror-angle"!==v){const t=120*c(d(e,s,n,r)+d(e,s,h,a),40,800)/150;if(l(e,s,n,r,h,a)<=t){let t,o,d,l;this.controlAbsorbStatus.isMirrorAngle=!0,i?(t=h,o=a,d=n,l=r):(t=n,o=r,d=h,l=a);const{x:c,y:x}=this.getMirrorPoint(t,o,e,s,d,l);i?(u.x1=c,u.y1=x):(u.x2=c,u.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===v){const t=d(e,s,n,r),o=d(e,s,h,a),l=this.pointRadius/2;Math.abs(t-o)<=l?(i?(u.x1=e-(h-e),u.y1=s-(a-s)):(u.x2=e-(n-e),u.y2=s-(r-s)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===u.mode)i?(u.x2-=x,u.y2-=p):o&&(u.x1-=x,u.y1-=p);else if("mirror-angle"===u.mode){let t=u.x1,e=u.y1,s=u.x2,n=u.y2;o&&(t=u.x2,e=u.y2,s=u.x1,n=u.y1);const{x:r,y:h}=u,{x:a,y:d}=this.getMirrorPoint(t,e,r,h,s,n);i?(u.x2=a,u.y2=d):(u.x1=a,u.y1=d)}let f,b,P=[];if(i?(f=u.x1,b=u.y1):(f=u.x2,b=u.y2),this.controlAbsorbStatus.isMirrorAngle){const i=new t.Path({path:`M ${e} ${s} L ${f} ${b}`,stroke:"red",strokeWidth:1,editable:!1});P.push(i)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const e=new t.Ellipse({x:f,y:b,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});e.fill="red",P.push(e)}this.controlAbsorbBox.set({children:[...P]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,i,o,s,n){const r=d(s,n,i,o),h=t-i,a=e-o,l=Math.sqrt(h*h+a*a);return{x:i-h/l*r,y:o-a/l*r}}handleControlDragEnd(t){const{innerId:e}=t.target,i=this.controlMap.get(e);if(i){const{x1:t,y1:e,x2:o,y2:s}=i;void 0!==t&&void 0!==e&&void 0!==o&&void 0!==s&&(this.controlAbsorbStatus.isMirrorAngle&&(i.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===i.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(i.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,i=this.pointIdxMap.get(e);this.handleSelectPoint(i.index)}handlePointDrag(t){const{moveX:e,moveY:i}=t,{innerId:o}=t.target;let{x:s,y:n}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,i),a=s+r,d=n+h;t.target.set({x:a,y:d});const l=this.pointIdxMap.get(o);if(l){const{index:t}=l,e=this.points[t];e.x=a,e.y=d,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return;this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,i){e=e||this.editTarget.worldTransform,i=i||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=e;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=i;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-n)*o)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*s)})),e}))}outerTransformPoints(t,e,i){e=e||this.editTarget.worldTransform,i=i||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=e;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=i;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/o+n))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/s+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:i}=this.transform;let{a:o,b:s,c:n,d:r,scaleX:h,scaleY:a}=i;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*o+(e/=a)*s,y:t*n+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=h(this.outerTransformPoints(this.points)),this.editor.emitEvent(new x(x.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:h(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let e,i,o;const s=new Map(this.pointIdxMap),n=this.selectPointIdx,r=this.points.map(((r,h)=>{const{x:a=0,y:d=0,type:l}=r;if("end"===l)return null;const c=new t.Ellipse(Object.assign({x:a,y:d,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:n===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:i,rightIdx:h};return e||(e=x),s.set(c.innerId,x),o&&(o.rightIdx=h),o=x,i=h,c})).filter(Boolean);e.leftIdx=i,o.rightIdx=e.index,this.pointIdxMap=s,this.pointsBox.set({children:r})}drawStroke(){const e=new t.Path({stroke:"#2193FF",strokeWidth:1,path:h(this.points),editable:!1});this.strokeBox.set({children:[e]})}createControl(e){if(!e)return[];const{x:i,y:o,x1:s,x2:n,y1:r,y2:h}=e;let a,d;void 0!==s&&void 0!==r&&(a=new t.Ellipse(Object.assign(Object.assign({x:s,y:r},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(a.innerId,e)),void 0!==n&&void 0!==h&&(d=new t.Ellipse(Object.assign(Object.assign({x:n,y:h},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,e));let l,c="";return a&&d?c=`M ${s} ${r} L ${i} ${o} L ${n} ${h}`:a?c=`M ${s} ${r} L ${i} ${o}`:d&&(c=`M ${i} ${o} L ${n} ${h}`),c&&(l=new t.Path({path:c,stroke:"#2193FF",strokeWidth:1,editable:!1})),[l,a,d].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],i=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const o=this.createControl(e),s=this.createControl(t),n=this.createControl(i);this.controlsBox.set({children:[...o,...s,...n]})}};p=function(t,e,i,o){var s,n=arguments.length,r=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,o);else for(var h=t.length-1;h>=0;h--)(s=t[h])&&(r=(n<3?s(r):n>3?s(e,i,r):s(e,i))||r);return n>3&&r&&Object.defineProperty(e,i,r),r}([e.registerInnerEditor()],p),t.Path.setEditInner("SVGPathEditor"),exports.PathEditorEvent=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,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.MOVE,this.handleControlLineMove.bind(this)),this.controlLineBox.on_(t.PointerEvent.TAP,this.handleControlLineTap.bind(this))]}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,46].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&(t.preventDefault(),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=[];let h=-1;for(let a=0;a<t.length;a++){const l={};if(t[a]===o)l.x=t[++a],l.y=t[++a],l.type="start",l.move=!0,h=e.length;else if(t[a]===i)l.x=t[++a],l.y=t[++a];else if(t[a]===n)e[e.length-1].x2=t[++a],e[e.length-1].y2=t[++a],l.x1=t[++a],l.y1=t[++a],l.x=t[++a],l.y=t[++a];else if(t[a]===s)e[e.length-1].x2=t[++a],e[e.length-1].y2=t[++a],l.x=t[++a],l.y=t[++a];else if(t[a]===r){const t=e[h],o=e[e.length-1];if(!t||!o)continue;e.length-1>h&&t.x===o.x&&t.y===o.y&&(e.pop(),void 0!==o.x1&&void 0!==o.y1&&(t.x1=o.x1,t.y1=o.y1)),e[e.length-1].closed=!0;continue}e.push(l)}for(let t=e.length-1;t>0;){let o=t;for(;o>0&&!e[o].move;)o--;const i=e[o],n=e[t];t>o&&!n.closed&&i.x===n.x&&i.y===n.y&&(e.splice(t,1),e[t-1].closed=!0,void 0!==n.x1&&void 0!==n.y1&&(i.x1=n.x1,i.y1=n.y1)),t=o-1}return e}(this.editTarget.getPath())),this.points=this.calculateMirrorMode(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const e=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:e}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,e)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:e}=this.editTarget,{scaleX:o,scaleY:i}=e;this.calculatePointStyle(e),!this.transform||o===this.transform.worldTransform.scaleX&&i===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},e),boxBounds:Object.assign({},t)}}onUnload(){this.clearHoverAnchor(),this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){if(this.selectControlLine)return this.points=m(this.points,this.selectControlLine),this.selectControlLine=void 0,void this.reDraw();void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}isSameSegment(t,e){return!(!t||!e||t.fromIndex!==e.fromIndex||t.toIndex!==e.toIndex||t.insertIndex!==e.insertIndex||!!t.closing!=!!e.closing)}clearRemoveAnchorTimer(){this.removeAnchorTimer&&clearTimeout(this.removeAnchorTimer),this.removeAnchorTimer=void 0}clearHoverAnchor(){var t;this.clearRemoveAnchorTimer(),(null===(t=this.hoverAnchorPoint)||void 0===t?void 0:t.parent)&&this.controlLineBox.remove(this.hoverAnchorPoint),this.hoverAnchorPoint=void 0}scheduleHoverAnchorRemoval(){this.clearRemoveAnchorTimer();const t=this.hoverAnchorPoint;this.removeAnchorTimer=setTimeout((()=>{this.hoverAnchorPoint===t&&this.clearHoverAnchor()}),80)}updateHoverAnchor(e,o){const i=e.data.segment,n=o&&this.isSameSegment(i,this.selectControlLine)?u(this.points,i,o):{point:y(this.points,i,.5),t:.5},s=1.6*this.pointRadius;this.hoverAnchorPoint||(this.hoverAnchorPoint=new t.Ellipse(Object.assign(Object.assign({},this.pointStyle),{width:s,height:s,offsetX:-s/2,offsetY:-s/2,fill:"white",stroke:"#2193FF",strokeWidth:2,hitRadius:4,cursor:"copy",data:{isAnchor:!0}})),this.controlLineBox.add(this.hoverAnchorPoint)),this.hoverAnchorPoint.set({x:n.point.x,y:n.point.y}),this.hoverAnchorPoint.data.segment=i,this.hoverAnchorPoint.data.t=n.t}handleControlLineEnter(t){if(this.clearRemoveAnchorTimer(),t.target.data.isAnchor)return;if(!t.target.data.isControlLine)return;const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":"hover",this.hoverControlLine=t.target,this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineLeave(t){if(t.target.data.isControlLine){const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":void 0,this.hoverControlLine===t.target&&(this.hoverControlLine=void 0)}this.scheduleHoverAnchorRemoval()}handleControlLineMove(t){t.target.data.isControlLine&&this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineTap(t){if(t.target.data.isAnchor){const e=t.target.data.segment;return this.points=f(this.points,e,t.target.data.t),this.selectControlLine=void 0,this.selectPointIdx=e.insertIndex,this.clearHoverAnchor(),void this.reDraw()}t.target.data.isControlLine&&(this.handleSelectPoint(),this.selectControlLine=t.target.data.segment,this.updateControl(),this.updateControlLines())}updateControlLines(){this.clearHoverAnchor();const e=[],o=(o,i,n,s=!1)=>{const r=this.points[i],h=this.points[o],{x:a,y:l,x1:d,y1:c}=r,{x:x,y:g,x2:p,y2:v}=h,y={fromIndex:o,toIndex:i,insertIndex:n,closing:s};let u="";u=void 0!==d&&void 0!==c&&void 0!==p&&void 0!==v?`M ${x} ${g} C ${p} ${v} ${d} ${c} ${a} ${l}`:void 0!==p&&void 0!==v?`M ${x} ${g} Q ${p} ${v} ${a} ${l}`:void 0!==d&&void 0!==c?`M ${x} ${g} Q ${d} ${c} ${a} ${l}`:`M ${x} ${g} L ${a} ${l}`;const f=new t.Path({path:u,stroke:"transparent",strokeWidth:1.5,hitRadius:7,hitStroke:"all",cursor:"pointer",states:{hover:{stroke:"#2193FF",strokeWidth:2},selected:{stroke:"#EF4444",strokeWidth:2}},state:this.isSameSegment(y,this.selectControlLine)?"selected":void 0,editable:!1});f.data.isControlLine=!0,f.data.segment=y,e.push(f)};let i=0;for(let t=0;t<this.points.length;t++){const e=this.points[t];0===t||e.move||"start"===e.type?i=t:o(t-1,t,t),e.closed&&o(t,i,t+1,!0)}this.controlLineBox.set({children:e})}calculateMirrorMode(t){return t.map((t=>{const{x:e,y:o,x1:i,y1:n,x2:s,y2:r}=t;if(void 0!==i&&void 0!==n&&null!=s&&void 0!==r){let h="no-mirror";if(0===d(e,o,i,n,s,r)){h="mirror-angle";a(l(e,o,i,n),1)===a(l(e,o,s,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:o}=t;if(e===o){const t=c(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=c(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,o=this.pointIdxMap.get(e),i=this.points[o.index];if(void 0!==i.x1&&void 0!==i.y1||void 0!==i.x2&&void 0!==i.y2)i.x1=void 0,i.x2=void 0,i.y1=void 0,i.y2=void 0;else{const[t,e,n,s]=this.getDefaultControls(this.points[o.leftIdx],this.points[o.index],this.points[o.rightIdx]);i.x1=t,i.y1=e,i.x2=n,i.y2=s,i.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,o){const i=.6*l(o.x,o.y,t.x,t.y),n=o.x-t.x,s=o.y-t.y,r=Math.sqrt(n*n+s*s),h=n/r,a=s/r,d=i/2,c={x:e.x+d*h,y:e.y+d*a},x={x:e.x-d*h,y:e.y-d*a};return[x.x,x.y,c.x,c.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(e){const{isLeft:o,isRight:i}=e.target.data;if("Ellipse"!==e.target.tag)return;const{moveX:n,moveY:s}=e,{innerId:r,x:h,y:a}=e.target,{x:x,y:g}=this.getTransformMovePosition(n,s),p=h+x,v=a+g;e.target.set({x:p,y:v});const y=this.controlMap.get(r);if(y){this.isCtrl()&&(y.mode="no-mirror"),o?(y.x1=p,y.y1=v):i&&(y.x2=p,y.y2=v);const{x:e,y:n,x1:s,y1:r,x2:h,y2:a,mode:u}=y;if(void 0!==s&&void 0!==r&&void 0!==h&&void 0!==a&&"mirror-angle-length"!==u){if("mirror-angle"!==u){const t=120*c(l(e,n,s,r)+l(e,n,h,a),40,800)/150;if(d(e,n,s,r,h,a)<=t){let t,i,l,d;this.controlAbsorbStatus.isMirrorAngle=!0,o?(t=h,i=a,l=s,d=r):(t=s,i=r,l=h,d=a);const{x:c,y:x}=this.getMirrorPoint(t,i,e,n,l,d);o?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===u){const t=l(e,n,s,r),i=l(e,n,h,a),d=this.pointRadius/2;Math.abs(t-i)<=d?(o?(y.x1=e-(h-e),y.y1=n-(a-n)):(y.x2=e-(s-e),y.y2=n-(r-n)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)o?(y.x2-=x,y.y2-=g):i&&(y.x1-=x,y.y1-=g);else if("mirror-angle"===y.mode){let t=y.x1,e=y.y1,n=y.x2,s=y.y2;i&&(t=y.x2,e=y.y2,n=y.x1,s=y.y1);const{x:r,y:h}=y,{x:a,y:l}=this.getMirrorPoint(t,e,r,h,n,s);o?(y.x2=a,y.y2=l):(y.x1=a,y.y1=l)}let f,m,b=[];if(o?(f=y.x1,m=y.y1):(f=y.x2,m=y.y2),this.controlAbsorbStatus.isMirrorAngle){const o=new t.Path({path:`M ${e} ${n} L ${f} ${m}`,stroke:"red",strokeWidth:1,editable:!1});b.push(o)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const e=new t.Ellipse({x:f,y:m,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});e.fill="red",b.push(e)}this.controlAbsorbBox.set({children:[...b]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,o,i,n,s){const r=l(n,s,o,i),h=t-o,a=e-i,d=Math.sqrt(h*h+a*a);return{x:o-h/d*r,y:i-a/d*r}}handleControlDragEnd(t){const{innerId:e}=t.target,o=this.controlMap.get(e);if(o){const{x1:t,y1:e,x2:i,y2:n}=o;void 0!==t&&void 0!==e&&void 0!==i&&void 0!==n&&(this.controlAbsorbStatus.isMirrorAngle&&(o.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===o.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(o.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,o=this.pointIdxMap.get(e);this.selectControlLine=void 0,this.handleSelectPoint(o.index),this.updateControlLines()}handlePointDrag(t){const{moveX:e,moveY:o}=t,{innerId:i}=t.target;let{x:n,y:s}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,o),a=n+r,l=s+h;t.target.set({x:a,y:l});const d=this.pointIdxMap.get(i);if(d){const{index:t}=d,e=this.points[t];e.x=a,e.y=l,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return void(this.selectPointIdx=void 0);this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-s)*i)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*n)})),e}))}outerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/i+s))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/n+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:o}=this.transform;let{a:i,b:n,c:s,d:r,scaleX:h,scaleY:a}=o;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*i+(e/=a)*n,y:t*s+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=h(this.outerTransformPoints(this.points)),this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:h(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let e,o,i;const n=new Map(this.pointIdxMap),s=this.selectPointIdx,r=this.points.map(((r,h)=>{const{x:a=0,y:l=0,type:d}=r;if("end"===d)return null;const c=new t.Ellipse(Object.assign({x:a,y:l,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:s===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:o,rightIdx:h};return e||(e=x),n.set(c.innerId,x),i&&(i.rightIdx=h),i=x,o=h,c})).filter(Boolean);e.leftIdx=o,i.rightIdx=e.index,this.pointIdxMap=n,this.pointsBox.set({children:r})}drawStroke(){const e=new t.Path({stroke:"#2193FF",strokeWidth:1,path:h(this.points),editable:!1});this.strokeBox.set({children:[e]})}createControl(e){if(!e)return[];const{x:o,y:i,x1:n,x2:s,y1:r,y2:h}=e;let a,l;void 0!==n&&void 0!==r&&(a=new t.Ellipse(Object.assign(Object.assign({x:n,y:r},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(a.innerId,e)),void 0!==s&&void 0!==h&&(l=new t.Ellipse(Object.assign(Object.assign({x:s,y:h},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,e));let d,c="";return a&&l?c=`M ${n} ${r} L ${o} ${i} L ${s} ${h}`:a?c=`M ${n} ${r} L ${o} ${i}`:l&&(c=`M ${o} ${i} L ${s} ${h}`),c&&(d=new t.Path({path:c,stroke:"#2193FF",strokeWidth:1,editable:!1})),[d,a,l].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],o=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const i=this.createControl(e),n=this.createControl(t),s=this.createControl(o);this.controlsBox.set({children:[...i,...n,...s]})}};P=function(t,e,o,i){var n,s=arguments.length,r=s<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,o):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,o,i);else for(var h=t.length-1;h>=0;h--)(n=t[h])&&(r=(s<3?n(r):s>3?n(e,o,r):n(e,o))||r);return s>3&&r&&Object.defineProperty(e,o,r),r}([e.registerInnerEditor()],P),t.Path.setEditInner("SVGPathEditor"),exports.PathEditorEvent=b,exports.deletePathSegment=m,exports.getClosestSegmentLocation=u,exports.splitPathSegment=f;
|
|
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 };
|