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