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.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-1&&(y.data.isStraight=!0,y.data.start={x:d,y:l},y.data.end={x:n,y:r},y.data.index=i),t.push(y)}this.controlLineBox.set({children:t})}addPointAfter(t,i,e){const o={x:i,y:e};this.points.splice(t,0,o),t{const{x:i,y:e,x1:o,y1:s,x2:n,y2:r}=t;if(void 0!==o&&void 0!==s&&null!=n&&void 0!==r){let h="no-mirror";if(0===f(i,e,o,s,n,r)){h="mirror-angle";g(u(i,e,o,s),1)===g(u(i,e,n,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:i,scaleY:e}=t;if(i===e){const t=v(6*i,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=v(1.4*i,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:i}=t.target,e=this.pointIdxMap.get(i),o=this.points[e.index];if(void 0!==o.x1&&void 0!==o.y1||void 0!==o.x2&&void 0!==o.y2)o.x1=void 0,o.x2=void 0,o.y1=void 0,o.y2=void 0;else{const[t,i,s,n]=this.getDefaultControls(this.points[e.leftIdx],this.points[e.index],this.points[e.rightIdx]);o.x1=t,o.y1=i,o.x2=s,o.y2=n,o.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,i,e){const o=.6*u(e.x,e.y,t.x,t.y),s=e.x-t.x,n=e.y-t.y,r=Math.sqrt(s*s+n*n),h=s/r,a=n/r,d=o/2,l={x:i.x+d*h,y:i.y+d*a},c={x:i.x-d*h,y:i.y-d*a};return[c.x,c.y,l.x,l.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(t){const{isLeft:i,isRight:o}=t.target.data;if("Ellipse"!==t.target.tag)return;const{moveX:s,moveY:n}=t,{innerId:h,x:a,y:d}=t.target,{x:l,y:c}=this.getTransformMovePosition(s,n),x=a+l,p=d+c;t.target.set({x:x,y:p});const y=this.controlMap.get(h);if(y){this.isCtrl()&&(y.mode="no-mirror"),i?(y.x1=x,y.y1=p):o&&(y.x2=x,y.y2=p);const{x:t,y:s,x1:n,y1:h,x2:a,y2:d,mode:g}=y;if(void 0!==n&&void 0!==h&&void 0!==a&&void 0!==d&&"mirror-angle-length"!==g){if("mirror-angle"!==g){const e=120*v(u(t,s,n,h)+u(t,s,a,d),40,800)/150;if(f(t,s,n,h,a,d)<=e){let e,o,r,l;this.controlAbsorbStatus.isMirrorAngle=!0,i?(e=a,o=d,r=n,l=h):(e=n,o=h,r=a,l=d);const{x:c,y:x}=this.getMirrorPoint(e,o,t,s,r,l);i?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===g){const e=u(t,s,n,h),o=u(t,s,a,d),r=this.pointRadius/2;Math.abs(e-o)<=r?(i?(y.x1=t-(a-t),y.y1=s-(d-s)):(y.x2=t-(n-t),y.y2=s-(h-s)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)i?(y.x2-=l,y.y2-=c):o&&(y.x1-=l,y.y1-=c);else if("mirror-angle"===y.mode){let t=y.x1,e=y.y1,s=y.x2,n=y.y2;o&&(t=y.x2,e=y.y2,s=y.x1,n=y.y1);const{x:r,y:h}=y,{x:a,y:d}=this.getMirrorPoint(t,e,r,h,s,n);i?(y.x2=a,y.y2=d):(y.x1=a,y.y1=d)}let b,m,w=[];if(i?(b=y.x1,m=y.y1):(b=y.x2,m=y.y2),this.controlAbsorbStatus.isMirrorAngle){const i=new e({path:`M ${t} ${s} L ${b} ${m}`,stroke:"red",strokeWidth:1,editable:!1});w.push(i)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const t=new r({x:b,y:m,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});t.fill="red",w.push(t)}this.controlAbsorbBox.set({children:[...w]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,i,e,o,s,n){const r=u(s,n,e,o),h=t-e,a=i-o,d=Math.sqrt(h*h+a*a);return{x:e-h/d*r,y:o-a/d*r}}handleControlDragEnd(t){const{innerId:i}=t.target,e=this.controlMap.get(i);if(e){const{x1:t,y1:i,x2:o,y2:s}=e;void 0!==t&&void 0!==i&&void 0!==o&&void 0!==s&&(this.controlAbsorbStatus.isMirrorAngle&&(e.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===e.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(e.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:i}=t.target,e=this.pointIdxMap.get(i);this.handleSelectPoint(e.index)}handlePointDrag(t){const{moveX:i,moveY:e}=t,{innerId:o}=t.target;let{x:s,y:n}=t.target;const{x:r,y:h}=this.getTransformMovePosition(i,e),a=s+r,d=n+h;t.target.set({x:a,y:d});const l=this.pointIdxMap.get(o);if(l){const{index:t}=l,i=this.points[t];i.x=a,i.y=d,void 0!==i.x1&&(i.x1+=r),void 0!==i.y1&&(i.y1+=h),void 0!==i.x2&&(i.x2+=r),void 0!==i.y2&&(i.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const i=this.pointsBox.children[this.selectPointIdx];if(i&&(i.state="unSelect"),void 0===t)return;this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,i,e){i=i||this.editTarget.worldTransform,e=e||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=i;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=e;return t.map((t=>{const i=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==i[t]&&(i[t]=(i[t]-n)*o)})),["y","y1","y2"].forEach((t=>{void 0!==i[t]&&(i[t]=(i[t]-r)*s)})),i}))}outerTransformPoints(t,i,e){i=i||this.editTarget.worldTransform,e=e||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=i;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=e;return t.map((t=>{const i=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==i[t]&&(i[t]=g(i[t]/o+n))})),["y","y1","y2"].forEach((t=>{void 0!==i[t]&&(i[t]=g(i[t]/s+r))})),i}))}getTransformMovePosition(t,i){const{worldTransform:e}=this.transform;let{a:o,b:s,c:n,d:r,scaleX:h,scaleY:a}=e;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*o+(i/=a)*s,y:t*n+i*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const i=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=y(this.outerTransformPoints(this.points)),this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:i})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:y(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let t,i,e;const o=new Map(this.pointIdxMap),s=this.selectPointIdx,n=this.points.map(((n,h)=>{const{x:a=0,y:d=0,type:l}=n;if("end"===l)return null;const c=new r(Object.assign({x:a,y:d,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:s===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:i,rightIdx:h};return t||(t=x),o.set(c.innerId,x),e&&(e.rightIdx=h),e=x,i=h,c})).filter(Boolean);t.leftIdx=i,e.rightIdx=t.index,this.pointIdxMap=o,this.pointsBox.set({children:n})}drawStroke(){const t=new e({stroke:"#2193FF",strokeWidth:1,path:y(this.points),editable:!1});this.strokeBox.set({children:[t]})}createControl(t){if(!t)return[];const{x:i,y:o,x1:s,x2:n,y1:h,y2:a}=t;let d,l;void 0!==s&&void 0!==h&&(d=new r(Object.assign(Object.assign({x:s,y:h},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,t)),void 0!==n&&void 0!==a&&(l=new r(Object.assign(Object.assign({x:n,y:a},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,t));let c,x="";return d&&l?x=`M ${s} ${h} L ${i} ${o} L ${n} ${a}`:d?x=`M ${s} ${h} L ${i} ${o}`:l&&(x=`M ${i} ${o} L ${n} ${a}`),x&&(c=new e({path:x,stroke:"#2193FF",strokeWidth:1,editable:!1})),[c,d,l].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],i=this.points[this.selectPointIdx-1],e=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const o=this.createControl(i),s=this.createControl(t),n=this.createControl(e);this.controlsBox.set({children:[...o,...s,...n]})}};m=function(t,i,e,o){var s,n=arguments.length,r=n<3?i:null===o?o=Object.getOwnPropertyDescriptor(i,e):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,i,e,o);else for(var h=t.length-1;h>=0;h--)(s=t[h])&&(r=(n<3?s(r):n>3?s(i,e,r):s(i,e))||r);return n>3&&r&&Object.defineProperty(i,e,r),r}([h()],m),e.setEditInner("SVGPathEditor");export{b as PathEditorEvent}; +import{PathCommandMap as t,Event as e,Path as o,Box as i,DragEvent as n,PointerEvent as s,Ellipse as r}from"@leafer-ui/core";import{registerInnerEditor as h,InnerEditor as a}from"@leafer-in/editor";import"@leafer-in/state";"function"==typeof SuppressedError&&SuppressedError;const{M:l,L:d,C:c,Q:x,Z:g}=t;function y(t){const e=[];if(!t.length)return e;const o=(t,o)=>{const{x:i,y:n,x1:s,y1:r}=o;void 0===t.x2&&void 0===t.y2&&void 0===s&&void 0===r?e.push(d,i,n):void 0!==t.x2&&void 0!==t.y2&&void 0!==s&&void 0!==r?e.push(c,t.x2,t.y2,s,r,i,n):void 0!==t.x2&&void 0!==t.y2?e.push(x,t.x2,t.y2,i,n):void 0!==s&&void 0!==r&&e.push(x,s,r,i,n)};let i=t[0];for(let n=0;nvoid 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);sObject.assign({},t))),n=Math.max(.001,Math.min(.999,o)),{from:s,to:r,fromCoordinate:h,toCoordinate:a,outgoing:l,incoming:d}=w(i,e);let c;if(l&&d){const t=b(h,l,n),e=b(l,d,n),o=b(d,a,n),i=b(t,e,n),x=b(e,o,n),g=b(i,x,n);s.x2=t.x,s.y2=t.y,r.x1=o.x,r.y1=o.y,c=Object.assign(Object.assign({},g),{x1:i.x,y1:i.y,x2:x.x,y2:x.y,mode:"no-mirror"})}else if(l||d){const t=l||d,e=b(h,t,n),o=b(t,a,n),i=b(e,o,n);s.x2=void 0,s.y2=void 0,r.x1=void 0,r.y1=void 0,c=Object.assign(Object.assign({},i),{x1:e.x,y1:e.y,x2:o.x,y2:o.y,mode:"no-mirror"})}else c=C(i,e,n);return e.closing&&(s.closed=!1,c.closed=!0),i.splice(e.insertIndex,0,c),i}function L(t,e){const o=t.map((t=>Object.assign({},t))),i=o[e.fromIndex],n=o[e.toIndex];return i.x2=void 0,i.y2=void 0,n.x1=void 0,n.y1=void 0,e.closing?i.closed=!1:(n.move=!0,n.type="start"),o}class I extends e{constructor(t,e){super(t),e&&Object.assign(this,e)}}I.CHANGE="pathEditor.change";let T=class extends a{get tag(){return"SVGPathEditor"}constructor(t){super(t),this.strokeBox=new i,this.pointsBox=new i,this.controlsBox=new i,this.controlAbsorbBox=new i,this.controlLineBox=new i,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new i,this.controlsBox=new i,this.strokeBox=new i,this.controlAbsorbBox=new i,this.controlLineBox=new i,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(n.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(s.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(s.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(n.DRAG,this.handleControlDrag.bind(this)),this.controlsBox.on_(n.END,this.handleControlDragEnd.bind(this)),this.controlsBox.on_(s.DOWN,this.handleControlDown.bind(this)),this.controlsBox.on_(s.UP,this.handleControlUp.bind(this)),this.editor.app.on_(s.DOUBLE_TAP,(t=>{t.target!==this.editTarget&&this.editor.closeInnerEditor()})),this.controlLineBox.on_(s.ENTER,this.handleControlLineEnter.bind(this)),this.controlLineBox.on_(s.LEAVE,this.handleControlLineLeave.bind(this)),this.controlLineBox.on_(s.MOVE,this.handleControlLineMove.bind(this)),this.controlLineBox.on_(s.TAP,this.handleControlLineTap.bind(this))]}isCtrl(){return 1===this.downKey.length&&this.downKey.some((t=>[17,91].includes(t)))}isDel(){return 1===this.downKey.length&&this.downKey.some((t=>[8,46].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&(t.preventDefault(),this.handleDeletePoint())};document.addEventListener("keydown",t);const e=t=>{this.downKey=this.downKey.filter((e=>t.keyCode!==e))};document.addEventListener("keyup",e),this.keyEvents.push({type:"keydown",event:t}),this.keyEvents.push({type:"keyup",event:e})}removeKeyEventListener(){this.keyEvents.forEach((({type:t,event:e})=>{document.removeEventListener(t,e)})),this.keyEvents=[]}onLoad(){var t;this.calculatePointStyle(this.editTarget.worldTransform),this.points=this.innerTransformPoints(function(t){const e=[];let o=-1;for(let i=0;io&&t.x===i.x&&t.y===i.y&&(e.pop(),void 0!==i.x1&&void 0!==i.y1&&(t.x1=i.x1,t.y1=i.y1)),e[e.length-1].closed=!0;continue}e.push(n)}for(let t=e.length-1;t>0;){let o=t;for(;o>0&&!e[o].move;)o--;const i=e[o],n=e[t];t>o&&!n.closed&&i.x===n.x&&i.y===n.y&&(e.splice(t,1),e[t-1].closed=!0,void 0!==n.x1&&void 0!==n.y1&&(i.x1=n.x1,i.y1=n.y1)),t=o-1}return e}(this.editTarget.getPath())),this.points=this.calculateMirrorMode(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const e=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new I(I.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:e}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,e)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:e}=this.editTarget,{scaleX:o,scaleY:i}=e;this.calculatePointStyle(e),!this.transform||o===this.transform.worldTransform.scaleX&&i===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},e),boxBounds:Object.assign({},t)}}onUnload(){this.clearHoverAnchor(),this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){if(this.selectControlLine)return this.points=L(this.points,this.selectControlLine),this.selectControlLine=void 0,void this.reDraw();void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}isSameSegment(t,e){return!(!t||!e||t.fromIndex!==e.fromIndex||t.toIndex!==e.toIndex||t.insertIndex!==e.insertIndex||!!t.closing!=!!e.closing)}clearRemoveAnchorTimer(){this.removeAnchorTimer&&clearTimeout(this.removeAnchorTimer),this.removeAnchorTimer=void 0}clearHoverAnchor(){var t;this.clearRemoveAnchorTimer(),(null===(t=this.hoverAnchorPoint)||void 0===t?void 0:t.parent)&&this.controlLineBox.remove(this.hoverAnchorPoint),this.hoverAnchorPoint=void 0}scheduleHoverAnchorRemoval(){this.clearRemoveAnchorTimer();const t=this.hoverAnchorPoint;this.removeAnchorTimer=setTimeout((()=>{this.hoverAnchorPoint===t&&this.clearHoverAnchor()}),80)}updateHoverAnchor(t,e){const o=t.data.segment,i=e&&this.isSameSegment(o,this.selectControlLine)?M(this.points,o,e):{point:C(this.points,o,.5),t:.5},n=1.6*this.pointRadius;this.hoverAnchorPoint||(this.hoverAnchorPoint=new r(Object.assign(Object.assign({},this.pointStyle),{width:n,height:n,offsetX:-n/2,offsetY:-n/2,fill:"white",stroke:"#2193FF",strokeWidth:2,hitRadius:4,cursor:"copy",data:{isAnchor:!0}})),this.controlLineBox.add(this.hoverAnchorPoint)),this.hoverAnchorPoint.set({x:i.point.x,y:i.point.y}),this.hoverAnchorPoint.data.segment=o,this.hoverAnchorPoint.data.t=i.t}handleControlLineEnter(t){if(this.clearRemoveAnchorTimer(),t.target.data.isAnchor)return;if(!t.target.data.isControlLine)return;const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":"hover",this.hoverControlLine=t.target,this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineLeave(t){if(t.target.data.isControlLine){const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":void 0,this.hoverControlLine===t.target&&(this.hoverControlLine=void 0)}this.scheduleHoverAnchorRemoval()}handleControlLineMove(t){t.target.data.isControlLine&&this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineTap(t){if(t.target.data.isAnchor){const e=t.target.data.segment;return this.points=A(this.points,e,t.target.data.t),this.selectControlLine=void 0,this.selectPointIdx=e.insertIndex,this.clearHoverAnchor(),void this.reDraw()}t.target.data.isControlLine&&(this.handleSelectPoint(),this.selectControlLine=t.target.data.segment,this.updateControl(),this.updateControlLines())}updateControlLines(){this.clearHoverAnchor();const t=[],e=(e,i,n,s=!1)=>{const r=this.points[i],h=this.points[e],{x:a,y:l,x1:d,y1:c}=r,{x:x,y:g,x2:y,y2:p}=h,u={fromIndex:e,toIndex:i,insertIndex:n,closing:s};let v="";v=void 0!==d&&void 0!==c&&void 0!==y&&void 0!==p?`M ${x} ${g} C ${y} ${p} ${d} ${c} ${a} ${l}`:void 0!==y&&void 0!==p?`M ${x} ${g} Q ${y} ${p} ${a} ${l}`:void 0!==d&&void 0!==c?`M ${x} ${g} Q ${d} ${c} ${a} ${l}`:`M ${x} ${g} L ${a} ${l}`;const f=new o({path:v,stroke:"transparent",strokeWidth:1.5,hitRadius:7,hitStroke:"all",cursor:"pointer",states:{hover:{stroke:"#2193FF",strokeWidth:2},selected:{stroke:"#EF4444",strokeWidth:2}},state:this.isSameSegment(u,this.selectControlLine)?"selected":void 0,editable:!1});f.data.isControlLine=!0,f.data.segment=u,t.push(f)};let i=0;for(let t=0;t{const{x:e,y:o,x1:i,y1:n,x2:s,y2:r}=t;if(void 0!==i&&void 0!==n&&null!=s&&void 0!==r){let h="no-mirror";if(0===v(e,o,i,n,s,r)){h="mirror-angle";p(u(e,o,i,n),1)===p(u(e,o,s,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:o}=t;if(e===o){const t=f(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=f(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,o=this.pointIdxMap.get(e),i=this.points[o.index];if(void 0!==i.x1&&void 0!==i.y1||void 0!==i.x2&&void 0!==i.y2)i.x1=void 0,i.x2=void 0,i.y1=void 0,i.y2=void 0;else{const[t,e,n,s]=this.getDefaultControls(this.points[o.leftIdx],this.points[o.index],this.points[o.rightIdx]);i.x1=t,i.y1=e,i.x2=n,i.y2=s,i.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,o){const i=.6*u(o.x,o.y,t.x,t.y),n=o.x-t.x,s=o.y-t.y,r=Math.sqrt(n*n+s*s),h=n/r,a=s/r,l=i/2,d={x:e.x+l*h,y:e.y+l*a},c={x:e.x-l*h,y:e.y-l*a};return[c.x,c.y,d.x,d.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(t){const{isLeft:e,isRight:i}=t.target.data;if("Ellipse"!==t.target.tag)return;const{moveX:n,moveY:s}=t,{innerId:h,x:a,y:l}=t.target,{x:d,y:c}=this.getTransformMovePosition(n,s),x=a+d,g=l+c;t.target.set({x:x,y:g});const y=this.controlMap.get(h);if(y){this.isCtrl()&&(y.mode="no-mirror"),e?(y.x1=x,y.y1=g):i&&(y.x2=x,y.y2=g);const{x:t,y:n,x1:s,y1:h,x2:a,y2:l,mode:p}=y;if(void 0!==s&&void 0!==h&&void 0!==a&&void 0!==l&&"mirror-angle-length"!==p){if("mirror-angle"!==p){const o=120*f(u(t,n,s,h)+u(t,n,a,l),40,800)/150;if(v(t,n,s,h,a,l)<=o){let o,i,r,d;this.controlAbsorbStatus.isMirrorAngle=!0,e?(o=a,i=l,r=s,d=h):(o=s,i=h,r=a,d=l);const{x:c,y:x}=this.getMirrorPoint(o,i,t,n,r,d);e?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===p){const o=u(t,n,s,h),i=u(t,n,a,l),r=this.pointRadius/2;Math.abs(o-i)<=r?(e?(y.x1=t-(a-t),y.y1=n-(l-n)):(y.x2=t-(s-t),y.y2=n-(h-n)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)e?(y.x2-=d,y.y2-=c):i&&(y.x1-=d,y.y1-=c);else if("mirror-angle"===y.mode){let t=y.x1,o=y.y1,n=y.x2,s=y.y2;i&&(t=y.x2,o=y.y2,n=y.x1,s=y.y1);const{x:r,y:h}=y,{x:a,y:l}=this.getMirrorPoint(t,o,r,h,n,s);e?(y.x2=a,y.y2=l):(y.x1=a,y.y1=l)}let m,b,P=[];if(e?(m=y.x1,b=y.y1):(m=y.x2,b=y.y2),this.controlAbsorbStatus.isMirrorAngle){const e=new o({path:`M ${t} ${n} L ${m} ${b}`,stroke:"red",strokeWidth:1,editable:!1});P.push(e)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const t=new r({x:m,y:b,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});t.fill="red",P.push(t)}this.controlAbsorbBox.set({children:[...P]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,o,i,n,s){const r=u(n,s,o,i),h=t-o,a=e-i,l=Math.sqrt(h*h+a*a);return{x:o-h/l*r,y:i-a/l*r}}handleControlDragEnd(t){const{innerId:e}=t.target,o=this.controlMap.get(e);if(o){const{x1:t,y1:e,x2:i,y2:n}=o;void 0!==t&&void 0!==e&&void 0!==i&&void 0!==n&&(this.controlAbsorbStatus.isMirrorAngle&&(o.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===o.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(o.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,o=this.pointIdxMap.get(e);this.selectControlLine=void 0,this.handleSelectPoint(o.index),this.updateControlLines()}handlePointDrag(t){const{moveX:e,moveY:o}=t,{innerId:i}=t.target;let{x:n,y:s}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,o),a=n+r,l=s+h;t.target.set({x:a,y:l});const d=this.pointIdxMap.get(i);if(d){const{index:t}=d,e=this.points[t];e.x=a,e.y=l,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return void(this.selectPointIdx=void 0);this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-s)*i)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*n)})),e}))}outerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=p(e[t]/i+s))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=p(e[t]/n+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:o}=this.transform;let{a:i,b:n,c:s,d:r,scaleX:h,scaleY:a}=o;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*i+(e/=a)*n,y:t*s+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=y(this.outerTransformPoints(this.points)),this.editor.emitEvent(new I(I.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:y(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let t,e,o;const i=new Map(this.pointIdxMap),n=this.selectPointIdx,s=this.points.map(((s,h)=>{const{x:a=0,y:l=0,type:d}=s;if("end"===d)return null;const c=new r(Object.assign({x:a,y:l,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:n===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:e,rightIdx:h};return t||(t=x),i.set(c.innerId,x),o&&(o.rightIdx=h),o=x,e=h,c})).filter(Boolean);t.leftIdx=e,o.rightIdx=t.index,this.pointIdxMap=i,this.pointsBox.set({children:s})}drawStroke(){const t=new o({stroke:"#2193FF",strokeWidth:1,path:y(this.points),editable:!1});this.strokeBox.set({children:[t]})}createControl(t){if(!t)return[];const{x:e,y:i,x1:n,x2:s,y1:h,y2:a}=t;let l,d;void 0!==n&&void 0!==h&&(l=new r(Object.assign(Object.assign({x:n,y:h},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,t)),void 0!==s&&void 0!==a&&(d=new r(Object.assign(Object.assign({x:s,y:a},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,t));let c,x="";return l&&d?x=`M ${n} ${h} L ${e} ${i} L ${s} ${a}`:l?x=`M ${n} ${h} L ${e} ${i}`:d&&(x=`M ${e} ${i} L ${s} ${a}`),x&&(c=new o({path:x,stroke:"#2193FF",strokeWidth:1,editable:!1})),[c,l,d].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],o=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const i=this.createControl(e),n=this.createControl(t),s=this.createControl(o);this.controlsBox.set({children:[...i,...n,...s]})}};T=function(t,e,o,i){var n,s=arguments.length,r=s<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,o):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,o,i);else for(var h=t.length-1;h>=0;h--)(n=t[h])&&(r=(s<3?n(r):s>3?n(e,o,r):n(e,o))||r);return s>3&&r&&Object.defineProperty(e,o,r),r}([h()],T),o.setEditInner("SVGPathEditor");export{I as PathEditorEvent,L as deletePathSegment,M as getClosestSegmentLocation,A as splitPathSegment}; diff --git a/dist/index.min.cjs b/dist/index.min.cjs index c29e650918870792070a761d7b42eab336118d51..ded0aa5eefb57249516e2af3ec3c22e04e01e0f0 100644 --- a/dist/index.min.cjs +++ b/dist/index.min.cjs @@ -1 +1 @@ -"use strict";var t=require("@leafer-ui/core"),e=require("@leafer-in/editor");require("@leafer-in/state"),"function"==typeof SuppressedError&&SuppressedError;const{M:i,L:o,C:s,Q:n,Z:r}=t.PathCommandMap;function h(t){const e=[];e.push(i,t[0].x,t[0].y);for(let i=0;i{t.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-1&&(y.data.isStraight=!0,y.data.start={x:d,y:l},y.data.end={x:n,y:r},y.data.index=i),e.push(y)}this.controlLineBox.set({children:e})}addPointAfter(t,e,i){const o={x:e,y:i};this.points.splice(t,0,o),t{const{x:e,y:i,x1:o,y1:s,x2:n,y2:r}=t;if(void 0!==o&&void 0!==s&&null!=n&&void 0!==r){let h="no-mirror";if(0===l(e,i,o,s,n,r)){h="mirror-angle";a(d(e,i,o,s),1)===a(d(e,i,n,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:i}=t;if(e===i){const t=c(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=c(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,i=this.pointIdxMap.get(e),o=this.points[i.index];if(void 0!==o.x1&&void 0!==o.y1||void 0!==o.x2&&void 0!==o.y2)o.x1=void 0,o.x2=void 0,o.y1=void 0,o.y2=void 0;else{const[t,e,s,n]=this.getDefaultControls(this.points[i.leftIdx],this.points[i.index],this.points[i.rightIdx]);o.x1=t,o.y1=e,o.x2=s,o.y2=n,o.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,i){const o=.6*d(i.x,i.y,t.x,t.y),s=i.x-t.x,n=i.y-t.y,r=Math.sqrt(s*s+n*n),h=s/r,a=n/r,l=o/2,c={x:e.x+l*h,y:e.y+l*a},x={x:e.x-l*h,y:e.y-l*a};return[x.x,x.y,c.x,c.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(e){const{isLeft:i,isRight:o}=e.target.data;if("Ellipse"!==e.target.tag)return;const{moveX:s,moveY:n}=e,{innerId:r,x:h,y:a}=e.target,{x:x,y:p}=this.getTransformMovePosition(s,n),y=h+x,g=a+p;e.target.set({x:y,y:g});const u=this.controlMap.get(r);if(u){this.isCtrl()&&(u.mode="no-mirror"),i?(u.x1=y,u.y1=g):o&&(u.x2=y,u.y2=g);const{x:e,y:s,x1:n,y1:r,x2:h,y2:a,mode:v}=u;if(void 0!==n&&void 0!==r&&void 0!==h&&void 0!==a&&"mirror-angle-length"!==v){if("mirror-angle"!==v){const t=120*c(d(e,s,n,r)+d(e,s,h,a),40,800)/150;if(l(e,s,n,r,h,a)<=t){let t,o,d,l;this.controlAbsorbStatus.isMirrorAngle=!0,i?(t=h,o=a,d=n,l=r):(t=n,o=r,d=h,l=a);const{x:c,y:x}=this.getMirrorPoint(t,o,e,s,d,l);i?(u.x1=c,u.y1=x):(u.x2=c,u.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===v){const t=d(e,s,n,r),o=d(e,s,h,a),l=this.pointRadius/2;Math.abs(t-o)<=l?(i?(u.x1=e-(h-e),u.y1=s-(a-s)):(u.x2=e-(n-e),u.y2=s-(r-s)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===u.mode)i?(u.x2-=x,u.y2-=p):o&&(u.x1-=x,u.y1-=p);else if("mirror-angle"===u.mode){let t=u.x1,e=u.y1,s=u.x2,n=u.y2;o&&(t=u.x2,e=u.y2,s=u.x1,n=u.y1);const{x:r,y:h}=u,{x:a,y:d}=this.getMirrorPoint(t,e,r,h,s,n);i?(u.x2=a,u.y2=d):(u.x1=a,u.y1=d)}let f,b,P=[];if(i?(f=u.x1,b=u.y1):(f=u.x2,b=u.y2),this.controlAbsorbStatus.isMirrorAngle){const i=new t.Path({path:`M ${e} ${s} L ${f} ${b}`,stroke:"red",strokeWidth:1,editable:!1});P.push(i)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const e=new t.Ellipse({x:f,y:b,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});e.fill="red",P.push(e)}this.controlAbsorbBox.set({children:[...P]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,i,o,s,n){const r=d(s,n,i,o),h=t-i,a=e-o,l=Math.sqrt(h*h+a*a);return{x:i-h/l*r,y:o-a/l*r}}handleControlDragEnd(t){const{innerId:e}=t.target,i=this.controlMap.get(e);if(i){const{x1:t,y1:e,x2:o,y2:s}=i;void 0!==t&&void 0!==e&&void 0!==o&&void 0!==s&&(this.controlAbsorbStatus.isMirrorAngle&&(i.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===i.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(i.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,i=this.pointIdxMap.get(e);this.handleSelectPoint(i.index)}handlePointDrag(t){const{moveX:e,moveY:i}=t,{innerId:o}=t.target;let{x:s,y:n}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,i),a=s+r,d=n+h;t.target.set({x:a,y:d});const l=this.pointIdxMap.get(o);if(l){const{index:t}=l,e=this.points[t];e.x=a,e.y=d,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return;this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,i){e=e||this.editTarget.worldTransform,i=i||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=e;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=i;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-n)*o)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*s)})),e}))}outerTransformPoints(t,e,i){e=e||this.editTarget.worldTransform,i=i||this.editTarget.boxBounds;let{scaleX:o,scaleY:s}=e;o=Math.abs(o),s=Math.abs(s);const{x:n,y:r}=i;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/o+n))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/s+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:i}=this.transform;let{a:o,b:s,c:n,d:r,scaleX:h,scaleY:a}=i;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*o+(e/=a)*s,y:t*n+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=h(this.outerTransformPoints(this.points)),this.editor.emitEvent(new x(x.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:h(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let e,i,o;const s=new Map(this.pointIdxMap),n=this.selectPointIdx,r=this.points.map(((r,h)=>{const{x:a=0,y:d=0,type:l}=r;if("end"===l)return null;const c=new t.Ellipse(Object.assign({x:a,y:d,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:n===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:i,rightIdx:h};return e||(e=x),s.set(c.innerId,x),o&&(o.rightIdx=h),o=x,i=h,c})).filter(Boolean);e.leftIdx=i,o.rightIdx=e.index,this.pointIdxMap=s,this.pointsBox.set({children:r})}drawStroke(){const e=new t.Path({stroke:"#2193FF",strokeWidth:1,path:h(this.points),editable:!1});this.strokeBox.set({children:[e]})}createControl(e){if(!e)return[];const{x:i,y:o,x1:s,x2:n,y1:r,y2:h}=e;let a,d;void 0!==s&&void 0!==r&&(a=new t.Ellipse(Object.assign(Object.assign({x:s,y:r},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(a.innerId,e)),void 0!==n&&void 0!==h&&(d=new t.Ellipse(Object.assign(Object.assign({x:n,y:h},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(d.innerId,e));let l,c="";return a&&d?c=`M ${s} ${r} L ${i} ${o} L ${n} ${h}`:a?c=`M ${s} ${r} L ${i} ${o}`:d&&(c=`M ${i} ${o} L ${n} ${h}`),c&&(l=new t.Path({path:c,stroke:"#2193FF",strokeWidth:1,editable:!1})),[l,a,d].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],i=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const o=this.createControl(e),s=this.createControl(t),n=this.createControl(i);this.controlsBox.set({children:[...o,...s,...n]})}};p=function(t,e,i,o){var s,n=arguments.length,r=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,o);else for(var h=t.length-1;h>=0;h--)(s=t[h])&&(r=(n<3?s(r):n>3?s(e,i,r):s(e,i))||r);return n>3&&r&&Object.defineProperty(e,i,r),r}([e.registerInnerEditor()],p),t.Path.setEditInner("SVGPathEditor"),exports.PathEditorEvent=x; +"use strict";var t=require("@leafer-ui/core"),e=require("@leafer-in/editor");require("@leafer-in/state"),"function"==typeof SuppressedError&&SuppressedError;const{M:o,L:i,C:n,Q:s,Z:r}=t.PathCommandMap;function h(t){const e=[];if(!t.length)return e;const h=(t,o)=>{const{x:r,y:h,x1:a,y1:l}=o;void 0===t.x2&&void 0===t.y2&&void 0===a&&void 0===l?e.push(i,r,h):void 0!==t.x2&&void 0!==t.y2&&void 0!==a&&void 0!==l?e.push(n,t.x2,t.y2,a,l,r,h):void 0!==t.x2&&void 0!==t.y2?e.push(s,t.x2,t.y2,r,h):void 0!==a&&void 0!==l&&e.push(s,a,l,r,h)};let a=t[0];for(let i=0;ivoid 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);sObject.assign({},t))),n=Math.max(.001,Math.min(.999,o)),{from:s,to:r,fromCoordinate:h,toCoordinate:a,outgoing:l,incoming:d}=v(i,e);let c;if(l&&d){const t=g(h,l,n),e=g(l,d,n),o=g(d,a,n),i=g(t,e,n),x=g(e,o,n),p=g(i,x,n);s.x2=t.x,s.y2=t.y,r.x1=o.x,r.y1=o.y,c=Object.assign(Object.assign({},p),{x1:i.x,y1:i.y,x2:x.x,y2:x.y,mode:"no-mirror"})}else if(l||d){const t=l||d,e=g(h,t,n),o=g(t,a,n),i=g(e,o,n);s.x2=void 0,s.y2=void 0,r.x1=void 0,r.y1=void 0,c=Object.assign(Object.assign({},i),{x1:e.x,y1:e.y,x2:o.x,y2:o.y,mode:"no-mirror"})}else c=y(i,e,n);return e.closing&&(s.closed=!1,c.closed=!0),i.splice(e.insertIndex,0,c),i}function m(t,e){const o=t.map((t=>Object.assign({},t))),i=o[e.fromIndex],n=o[e.toIndex];return i.x2=void 0,i.y2=void 0,n.x1=void 0,n.y1=void 0,e.closing?i.closed=!1:(n.move=!0,n.type="start"),o}class b extends t.Event{constructor(t,e){super(t),e&&Object.assign(this,e)}}b.CHANGE="pathEditor.change";let P=class extends e.InnerEditor{get tag(){return"SVGPathEditor"}constructor(e){super(e),this.strokeBox=new t.Box,this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.points=[],this.pointIdxMap=new Map,this.controlMap=new Map,this.controlAbsorbStatus={},this.keyEvents=[],this.downKey=[],this.selectPointStyle={stroke:"white",fill:"#2193FF"},this.unSelectPointStyle={stroke:"#2193FF",fill:"white"},this.pointRadius=6,this.pointStyle=Object.assign({width:2*this.pointRadius,height:2*this.pointRadius,strokeWidth:2},this.unSelectPointStyle),this.pointsBox=new t.Box,this.controlsBox=new t.Box,this.strokeBox=new t.Box,this.controlAbsorbBox=new t.Box,this.controlLineBox=new t.Box,this.view.addMany(this.strokeBox,this.controlLineBox,this.controlsBox,this.pointsBox,this.controlAbsorbBox),this.eventIds=[this.pointsBox.on_(t.DragEvent.DRAG,this.handlePointDrag.bind(this)),this.pointsBox.on_(t.PointerEvent.DOWN,this.handlePointDown.bind(this)),this.pointsBox.on_(t.PointerEvent.TAP,this.handlePointTap.bind(this)),this.controlsBox.on_(t.DragEvent.DRAG,this.handleControlDrag.bind(this)),this.controlsBox.on_(t.DragEvent.END,this.handleControlDragEnd.bind(this)),this.controlsBox.on_(t.PointerEvent.DOWN,this.handleControlDown.bind(this)),this.controlsBox.on_(t.PointerEvent.UP,this.handleControlUp.bind(this)),this.editor.app.on_(t.PointerEvent.DOUBLE_TAP,(t=>{t.target!==this.editTarget&&this.editor.closeInnerEditor()})),this.controlLineBox.on_(t.PointerEvent.ENTER,this.handleControlLineEnter.bind(this)),this.controlLineBox.on_(t.PointerEvent.LEAVE,this.handleControlLineLeave.bind(this)),this.controlLineBox.on_(t.PointerEvent.MOVE,this.handleControlLineMove.bind(this)),this.controlLineBox.on_(t.PointerEvent.TAP,this.handleControlLineTap.bind(this))]}isCtrl(){return 1===this.downKey.length&&this.downKey.some((t=>[17,91].includes(t)))}isDel(){return 1===this.downKey.length&&this.downKey.some((t=>[8,46].includes(t)))}addKeyEventListener(){const t=t=>{this.downKey.push(t.keyCode),this.isDel()&&(t.preventDefault(),this.handleDeletePoint())};document.addEventListener("keydown",t);const e=t=>{this.downKey=this.downKey.filter((e=>t.keyCode!==e))};document.addEventListener("keyup",e),this.keyEvents.push({type:"keydown",event:t}),this.keyEvents.push({type:"keyup",event:e})}removeKeyEventListener(){this.keyEvents.forEach((({type:t,event:e})=>{document.removeEventListener(t,e)})),this.keyEvents=[]}onLoad(){var t;this.calculatePointStyle(this.editTarget.worldTransform),this.points=this.innerTransformPoints(function(t){const e=[];let h=-1;for(let a=0;ah&&t.x===o.x&&t.y===o.y&&(e.pop(),void 0!==o.x1&&void 0!==o.y1&&(t.x1=o.x1,t.y1=o.y1)),e[e.length-1].closed=!0;continue}e.push(l)}for(let t=e.length-1;t>0;){let o=t;for(;o>0&&!e[o].move;)o--;const i=e[o],n=e[t];t>o&&!n.closed&&i.x===n.x&&i.y===n.y&&(e.splice(t,1),e[t-1].closed=!0,void 0!==n.x1&&void 0!==n.y1&&(i.x1=n.x1,i.y1=n.y1)),t=o-1}return e}(this.editTarget.getPath())),this.points=this.calculateMirrorMode(this.points),this.editTargetDuplicate=this.editTarget.clone(),null===(t=this.editTarget.parent)||void 0===t||t.add(this.editTargetDuplicate),this.drawPoints(),this.drawInnerPath();const e=this.editTarget.clone();this.editTarget.visible=!1,this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!1,this.editBox.add(this.view),this.addKeyEventListener()}reDraw(){if(!this.transform)return;const{worldTransform:t,boxBounds:e}=this.transform;this.points=this.innerTransformPoints(this.outerTransformPoints(this.points,t,e)),this.drawPoints(),this.drawInnerPath(),this.updateControl()}onUpdate(){const{boxBounds:t,worldTransform:e}=this.editTarget,{scaleX:o,scaleY:i}=e;this.calculatePointStyle(e),!this.transform||o===this.transform.worldTransform.scaleX&&i===this.transform.worldTransform.scaleY||this.reDraw(),this.transform={worldTransform:Object.assign({},e),boxBounds:Object.assign({},t)}}onUnload(){this.clearHoverAnchor(),this.closeInnerEditor(),this.removeKeyEventListener(),this.editBox.remove(this.view)}handleDeletePoint(){if(this.selectControlLine)return this.points=m(this.points,this.selectControlLine),this.selectControlLine=void 0,void this.reDraw();void 0!==this.selectPointIdx&&(this.points.splice(this.selectPointIdx,1),this.selectPointIdx=void 0,this.reDraw())}isSameSegment(t,e){return!(!t||!e||t.fromIndex!==e.fromIndex||t.toIndex!==e.toIndex||t.insertIndex!==e.insertIndex||!!t.closing!=!!e.closing)}clearRemoveAnchorTimer(){this.removeAnchorTimer&&clearTimeout(this.removeAnchorTimer),this.removeAnchorTimer=void 0}clearHoverAnchor(){var t;this.clearRemoveAnchorTimer(),(null===(t=this.hoverAnchorPoint)||void 0===t?void 0:t.parent)&&this.controlLineBox.remove(this.hoverAnchorPoint),this.hoverAnchorPoint=void 0}scheduleHoverAnchorRemoval(){this.clearRemoveAnchorTimer();const t=this.hoverAnchorPoint;this.removeAnchorTimer=setTimeout((()=>{this.hoverAnchorPoint===t&&this.clearHoverAnchor()}),80)}updateHoverAnchor(e,o){const i=e.data.segment,n=o&&this.isSameSegment(i,this.selectControlLine)?u(this.points,i,o):{point:y(this.points,i,.5),t:.5},s=1.6*this.pointRadius;this.hoverAnchorPoint||(this.hoverAnchorPoint=new t.Ellipse(Object.assign(Object.assign({},this.pointStyle),{width:s,height:s,offsetX:-s/2,offsetY:-s/2,fill:"white",stroke:"#2193FF",strokeWidth:2,hitRadius:4,cursor:"copy",data:{isAnchor:!0}})),this.controlLineBox.add(this.hoverAnchorPoint)),this.hoverAnchorPoint.set({x:n.point.x,y:n.point.y}),this.hoverAnchorPoint.data.segment=i,this.hoverAnchorPoint.data.t=n.t}handleControlLineEnter(t){if(this.clearRemoveAnchorTimer(),t.target.data.isAnchor)return;if(!t.target.data.isControlLine)return;const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":"hover",this.hoverControlLine=t.target,this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineLeave(t){if(t.target.data.isControlLine){const e=t.target.data.segment;t.target.state=this.isSameSegment(e,this.selectControlLine)?"selected":void 0,this.hoverControlLine===t.target&&(this.hoverControlLine=void 0)}this.scheduleHoverAnchorRemoval()}handleControlLineMove(t){t.target.data.isControlLine&&this.updateHoverAnchor(t.target,t.getInnerPoint(this.controlLineBox))}handleControlLineTap(t){if(t.target.data.isAnchor){const e=t.target.data.segment;return this.points=f(this.points,e,t.target.data.t),this.selectControlLine=void 0,this.selectPointIdx=e.insertIndex,this.clearHoverAnchor(),void this.reDraw()}t.target.data.isControlLine&&(this.handleSelectPoint(),this.selectControlLine=t.target.data.segment,this.updateControl(),this.updateControlLines())}updateControlLines(){this.clearHoverAnchor();const e=[],o=(o,i,n,s=!1)=>{const r=this.points[i],h=this.points[o],{x:a,y:l,x1:d,y1:c}=r,{x:x,y:g,x2:p,y2:v}=h,y={fromIndex:o,toIndex:i,insertIndex:n,closing:s};let u="";u=void 0!==d&&void 0!==c&&void 0!==p&&void 0!==v?`M ${x} ${g} C ${p} ${v} ${d} ${c} ${a} ${l}`:void 0!==p&&void 0!==v?`M ${x} ${g} Q ${p} ${v} ${a} ${l}`:void 0!==d&&void 0!==c?`M ${x} ${g} Q ${d} ${c} ${a} ${l}`:`M ${x} ${g} L ${a} ${l}`;const f=new t.Path({path:u,stroke:"transparent",strokeWidth:1.5,hitRadius:7,hitStroke:"all",cursor:"pointer",states:{hover:{stroke:"#2193FF",strokeWidth:2},selected:{stroke:"#EF4444",strokeWidth:2}},state:this.isSameSegment(y,this.selectControlLine)?"selected":void 0,editable:!1});f.data.isControlLine=!0,f.data.segment=y,e.push(f)};let i=0;for(let t=0;t{const{x:e,y:o,x1:i,y1:n,x2:s,y2:r}=t;if(void 0!==i&&void 0!==n&&null!=s&&void 0!==r){let h="no-mirror";if(0===d(e,o,i,n,s,r)){h="mirror-angle";a(l(e,o,i,n),1)===a(l(e,o,s,r),1)&&(h="mirror-angle-length")}return Object.assign(Object.assign({},t),{mode:h})}return t}))}calculatePointStyle(t){const{scaleX:e,scaleY:o}=t;if(e===o){const t=c(6*e,5,7);this.pointRadius=t,this.pointStyle.width=2*t,this.pointStyle.height=2*t,this.pointStyle.strokeWidth=c(1.4*e,1,2)}}handlePointTap(t){if(this.isCtrl()){const{innerId:e}=t.target,o=this.pointIdxMap.get(e),i=this.points[o.index];if(void 0!==i.x1&&void 0!==i.y1||void 0!==i.x2&&void 0!==i.y2)i.x1=void 0,i.x2=void 0,i.y1=void 0,i.y2=void 0;else{const[t,e,n,s]=this.getDefaultControls(this.points[o.leftIdx],this.points[o.index],this.points[o.rightIdx]);i.x1=t,i.y1=e,i.x2=n,i.y2=s,i.mode="mirror-angle-length"}this.drawInnerPath()}this.updateControl(),this.updateControlLines()}getDefaultControls(t,e,o){const i=.6*l(o.x,o.y,t.x,t.y),n=o.x-t.x,s=o.y-t.y,r=Math.sqrt(n*n+s*s),h=n/r,a=s/r,d=i/2,c={x:e.x+d*h,y:e.y+d*a},x={x:e.x-d*h,y:e.y-d*a};return[x.x,x.y,c.x,c.y]}handleControlDown(t){this.selectControlPoint=t.target}handleControlUp(){this.selectControlPoint=null}handleControlDrag(e){const{isLeft:o,isRight:i}=e.target.data;if("Ellipse"!==e.target.tag)return;const{moveX:n,moveY:s}=e,{innerId:r,x:h,y:a}=e.target,{x:x,y:g}=this.getTransformMovePosition(n,s),p=h+x,v=a+g;e.target.set({x:p,y:v});const y=this.controlMap.get(r);if(y){this.isCtrl()&&(y.mode="no-mirror"),o?(y.x1=p,y.y1=v):i&&(y.x2=p,y.y2=v);const{x:e,y:n,x1:s,y1:r,x2:h,y2:a,mode:u}=y;if(void 0!==s&&void 0!==r&&void 0!==h&&void 0!==a&&"mirror-angle-length"!==u){if("mirror-angle"!==u){const t=120*c(l(e,n,s,r)+l(e,n,h,a),40,800)/150;if(d(e,n,s,r,h,a)<=t){let t,i,l,d;this.controlAbsorbStatus.isMirrorAngle=!0,o?(t=h,i=a,l=s,d=r):(t=s,i=r,l=h,d=a);const{x:c,y:x}=this.getMirrorPoint(t,i,e,n,l,d);o?(y.x1=c,y.y1=x):(y.x2=c,y.y2=x)}else this.controlAbsorbStatus.isMirrorAngle=!1}if(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===u){const t=l(e,n,s,r),i=l(e,n,h,a),d=this.pointRadius/2;Math.abs(t-i)<=d?(o?(y.x1=e-(h-e),y.y1=n-(a-n)):(y.x2=e-(s-e),y.y2=n-(r-n)),this.controlAbsorbStatus.isMirrorAngleLength=!0):this.controlAbsorbStatus.isMirrorAngleLength=!1}else this.controlAbsorbStatus.isMirrorAngleLength&&(this.controlAbsorbStatus.isMirrorAngleLength=!1)}if("mirror-angle-length"===y.mode)o?(y.x2-=x,y.y2-=g):i&&(y.x1-=x,y.y1-=g);else if("mirror-angle"===y.mode){let t=y.x1,e=y.y1,n=y.x2,s=y.y2;i&&(t=y.x2,e=y.y2,n=y.x1,s=y.y1);const{x:r,y:h}=y,{x:a,y:l}=this.getMirrorPoint(t,e,r,h,n,s);o?(y.x2=a,y.y2=l):(y.x1=a,y.y1=l)}let f,m,b=[];if(o?(f=y.x1,m=y.y1):(f=y.x2,m=y.y2),this.controlAbsorbStatus.isMirrorAngle){const o=new t.Path({path:`M ${e} ${n} L ${f} ${m}`,stroke:"red",strokeWidth:1,editable:!1});b.push(o)}if(this.controlAbsorbStatus.isMirrorAngleLength){this.selectControlPoint.fill="red";const e=new t.Ellipse({x:f,y:m,width:this.pointRadius,height:this.pointRadius,offsetX:-this.pointRadius/2,offsetY:-this.pointRadius/2,fill:"red"});e.fill="red",b.push(e)}this.controlAbsorbBox.set({children:[...b]}),this.updateControl(),this.updateControlLines(),this.drawInnerPath()}}getMirrorPoint(t,e,o,i,n,s){const r=l(n,s,o,i),h=t-o,a=e-i,d=Math.sqrt(h*h+a*a);return{x:o-h/d*r,y:i-a/d*r}}handleControlDragEnd(t){const{innerId:e}=t.target,o=this.controlMap.get(e);if(o){const{x1:t,y1:e,x2:i,y2:n}=o;void 0!==t&&void 0!==e&&void 0!==i&&void 0!==n&&(this.controlAbsorbStatus.isMirrorAngle&&(o.mode="mirror-angle"),(this.controlAbsorbStatus.isMirrorAngle||"mirror-angle"===o.mode)&&this.controlAbsorbStatus.isMirrorAngleLength&&(o.mode="mirror-angle-length"))}this.controlAbsorbStatus={},this.controlAbsorbBox.set({children:[]})}handlePointDown(t){const{innerId:e}=t.target,o=this.pointIdxMap.get(e);this.selectControlLine=void 0,this.handleSelectPoint(o.index),this.updateControlLines()}handlePointDrag(t){const{moveX:e,moveY:o}=t,{innerId:i}=t.target;let{x:n,y:s}=t.target;const{x:r,y:h}=this.getTransformMovePosition(e,o),a=n+r,l=s+h;t.target.set({x:a,y:l});const d=this.pointIdxMap.get(i);if(d){const{index:t}=d,e=this.points[t];e.x=a,e.y=l,void 0!==e.x1&&(e.x1+=r),void 0!==e.y1&&(e.y1+=h),void 0!==e.x2&&(e.x2+=r),void 0!==e.y2&&(e.y2+=h),this.updateControl(),this.drawInnerPath(),this.updateControlLines()}}handleSelectPoint(t){const e=this.pointsBox.children[this.selectPointIdx];if(e&&(e.state="unSelect"),void 0===t)return void(this.selectPointIdx=void 0);this.selectPointIdx=t;this.pointsBox.children[t].state="select"}innerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-s)*i)})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=(e[t]-r)*n)})),e}))}outerTransformPoints(t,e,o){e=e||this.editTarget.worldTransform,o=o||this.editTarget.boxBounds;let{scaleX:i,scaleY:n}=e;i=Math.abs(i),n=Math.abs(n);const{x:s,y:r}=o;return t.map((t=>{const e=Object.assign({},t);return["x","x1","x2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/i+s))})),["y","y1","y2"].forEach((t=>{void 0!==e[t]&&(e[t]=a(e[t]/n+r))})),e}))}getTransformMovePosition(t,e){const{worldTransform:o}=this.transform;let{a:i,b:n,c:s,d:r,scaleX:h,scaleY:a}=o;return h=Math.abs(h),a=Math.abs(a),{x:(t/=h)*i+(e/=a)*n,y:t*s+e*r}}closeInnerEditor(){var t;null===(t=this.editTarget.parent)||void 0===t||t.remove(this.editTargetDuplicate);const e=this.editTarget.clone();this.editTarget.visible=!0,this.editTarget.path=h(this.outerTransformPoints(this.points)),this.editor.emitEvent(new b(b.CHANGE,{value:this.editTarget,oldValue:e})),this.editor.selector.targetStroker.visible=!0,this.editor.off_(this.eventIds),this.eventIds=[],this.points=[]}onDestroy(){this.pointsBox=null}drawInnerPath(){this.editTargetDuplicate.set({path:h(this.outerTransformPoints(this.points))}),this.drawStroke(),this.updateControlLines()}drawPoints(){let e,o,i;const n=new Map(this.pointIdxMap),s=this.selectPointIdx,r=this.points.map(((r,h)=>{const{x:a=0,y:l=0,type:d}=r;if("end"===d)return null;const c=new t.Ellipse(Object.assign({x:a,y:l,cursor:"move",offsetX:-this.pointRadius,offsetY:-this.pointRadius,states:{select:Object.assign({},this.selectPointStyle),unSelect:Object.assign({},this.unSelectPointStyle)},state:s===h?"select":"unSelect"},this.pointStyle)),x={index:h,leftIdx:o,rightIdx:h};return e||(e=x),n.set(c.innerId,x),i&&(i.rightIdx=h),i=x,o=h,c})).filter(Boolean);e.leftIdx=o,i.rightIdx=e.index,this.pointIdxMap=n,this.pointsBox.set({children:r})}drawStroke(){const e=new t.Path({stroke:"#2193FF",strokeWidth:1,path:h(this.points),editable:!1});this.strokeBox.set({children:[e]})}createControl(e){if(!e)return[];const{x:o,y:i,x1:n,x2:s,y1:r,y2:h}=e;let a,l;void 0!==n&&void 0!==r&&(a=new t.Ellipse(Object.assign(Object.assign({x:n,y:r},this.pointStyle),{cursor:"pointer",data:{isLeft:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(a.innerId,e)),void 0!==s&&void 0!==h&&(l=new t.Ellipse(Object.assign(Object.assign({x:s,y:h},this.pointStyle),{cursor:"pointer",data:{isRight:!0},offsetX:-this.pointRadius,offsetY:-this.pointRadius})),this.controlMap.set(l.innerId,e));let d,c="";return a&&l?c=`M ${n} ${r} L ${o} ${i} L ${s} ${h}`:a?c=`M ${n} ${r} L ${o} ${i}`:l&&(c=`M ${o} ${i} L ${s} ${h}`),c&&(d=new t.Path({path:c,stroke:"#2193FF",strokeWidth:1,editable:!1})),[d,a,l].filter(Boolean)}updateControl(){if(void 0===this.selectPointIdx)return void this.controlsBox.set({children:[]});const t=this.points[this.selectPointIdx],e=this.points[this.selectPointIdx-1],o=this.points[this.selectPointIdx+1];if(void 0===t)return;if(this.selectControlPoint){const t=this.controlMap.get(this.selectControlPoint.innerId);this.controlMap.clear(),this.controlMap.set(this.selectControlPoint.innerId,t)}else this.controlMap.clear();const i=this.createControl(e),n=this.createControl(t),s=this.createControl(o);this.controlsBox.set({children:[...i,...n,...s]})}};P=function(t,e,o,i){var n,s=arguments.length,r=s<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,o):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,o,i);else for(var h=t.length-1;h>=0;h--)(n=t[h])&&(r=(s<3?n(r):s>3?n(e,o,r):n(e,o))||r);return s>3&&r&&Object.defineProperty(e,o,r),r}([e.registerInnerEditor()],P),t.Path.setEditInner("SVGPathEditor"),exports.PathEditorEvent=b,exports.deletePathSegment=m,exports.getClosestSegmentLocation=u,exports.splitPathSegment=f; diff --git a/src/index.ts b/src/index.ts index 18bd15470c59fcf04cfc7d2c80e3029c25dc8b6e..b690a1e1b14b4378bffe4031c6f9f314d9ac40b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,9 @@ import './svg-path-editor'; export { PathEditorEvent } from './event'; +export { + deletePathSegment, + getClosestSegmentLocation, + splitPathSegment, +} from './segment'; +export type { IPathSegment } from './type'; diff --git a/src/segment.ts b/src/segment.ts new file mode 100644 index 0000000000000000000000000000000000000000..8830cebdedd6ed2dcff8c39e1d38db371ff846d7 --- /dev/null +++ b/src/segment.ts @@ -0,0 +1,186 @@ +import { IPathSegment, IPoint } from './type'; + +interface ICoordinate { + x: number; + y: number; +} + +export interface IClosestSegmentLocation { + point: ICoordinate; + t: number; +} + +const hasPoint = (x?: number, y?: number): x is number => + x !== undefined && y !== undefined; + +const lerp = (from: ICoordinate, to: ICoordinate, t: number) => ({ + x: from.x + (to.x - from.x) * t, + y: from.y + (to.y - from.y) * t, +}); + +const distanceSquared = (left: ICoordinate, right: ICoordinate) => + Math.pow(left.x - right.x, 2) + Math.pow(left.y - right.y, 2); + +function getSegmentPoints(points: IPoint[], segment: IPathSegment) { + const from = points[segment.fromIndex] as Required> & IPoint; + const to = points[segment.toIndex] as Required> & 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; + // 当前选中的 控制点 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; 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 };