|
|
|
@ -1,5 +1,12 @@
|
|
|
|
|
import {CSSProperties, ReactElement, useCallback, useEffect, useRef, useState,} from "react"
|
|
|
|
|
import {angle, middlePos, Pos, relativeTo} from "./Pos"
|
|
|
|
|
import {
|
|
|
|
|
CSSProperties,
|
|
|
|
|
ReactElement,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
|
|
|
|
import { angle, middlePos, Pos, relativeTo } from "./Pos"
|
|
|
|
|
|
|
|
|
|
import "../../style/bendable_arrows.css"
|
|
|
|
|
import Draggable from "react-draggable"
|
|
|
|
@ -39,27 +46,24 @@ function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function Triangle({fill}: {fill: string}) {
|
|
|
|
|
function Triangle({ fill }: { fill: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<svg viewBox={"0 0 50 50"} width={20} height={20}>
|
|
|
|
|
<polygon points={"50 0, 0 0, 25 40"} fill={fill}/>
|
|
|
|
|
<polygon points={"50 0, 0 0, 25 40"} fill={fill} />
|
|
|
|
|
</svg>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BendableArrow({
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
|
onSegmentsChanges,
|
|
|
|
|
|
|
|
|
|
style,
|
|
|
|
|
startRadius = 0,
|
|
|
|
|
endRadius = 0,
|
|
|
|
|
}: BendableArrowProps) {
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
|
onSegmentsChanges,
|
|
|
|
|
|
|
|
|
|
style,
|
|
|
|
|
startRadius = 0,
|
|
|
|
|
endRadius = 0,
|
|
|
|
|
}: BendableArrowProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
|
|
|
|
|
|
|
@ -73,47 +77,45 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setInternalSegments(segments)
|
|
|
|
|
}, [segments]);
|
|
|
|
|
}, [segments])
|
|
|
|
|
|
|
|
|
|
const [internalSegments, setInternalSegments] = useState(segments)
|
|
|
|
|
|
|
|
|
|
const [isSelected, setIsSelected] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const headRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const tailRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
const basePos = containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
const basePos =
|
|
|
|
|
containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
setControlPointsDots(computeControlPoints(basePos))
|
|
|
|
|
|
|
|
|
|
const selectionHandler = (e: MouseEvent) => {
|
|
|
|
|
if (!(e.target instanceof Node))
|
|
|
|
|
return
|
|
|
|
|
if (!(e.target instanceof Node)) return
|
|
|
|
|
|
|
|
|
|
setIsSelected(containerRef.current!.contains(e.target))
|
|
|
|
|
const isSelected = containerRef.current!.contains(e.target)
|
|
|
|
|
setIsSelected(isSelected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousedown', selectionHandler)
|
|
|
|
|
return () => document.removeEventListener('mousedown', selectionHandler)
|
|
|
|
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mousedown", selectionHandler)
|
|
|
|
|
return () => document.removeEventListener("mousedown", selectionHandler)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
function computeControlPoints(basePos: Pos) {
|
|
|
|
|
return internalSegments.map(({next, controlPoint}, i) => {
|
|
|
|
|
return internalSegments.map(({ next, controlPoint }, i) => {
|
|
|
|
|
const prev = i == 0 ? startPos : internalSegments[i - 1].next
|
|
|
|
|
|
|
|
|
|
const prevRelative = relativeTo(prev, basePos)
|
|
|
|
|
const nextRelative = relativeTo(next, basePos)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cpPos = controlPoint ? relativeTo(controlPoint, basePos) : {
|
|
|
|
|
x: prevRelative.x / 2 + nextRelative.x / 2,
|
|
|
|
|
y: prevRelative.y / 2 + nextRelative.y / 2,
|
|
|
|
|
}
|
|
|
|
|
const cpPos = controlPoint
|
|
|
|
|
? relativeTo(controlPoint, basePos)
|
|
|
|
|
: {
|
|
|
|
|
x: prevRelative.x / 2 + nextRelative.x / 2,
|
|
|
|
|
y: prevRelative.y / 2 + nextRelative.y / 2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ControlPoint
|
|
|
|
@ -122,12 +124,18 @@ export default function BendableArrow({
|
|
|
|
|
basePos={basePos}
|
|
|
|
|
onPosValidated={(controlPoint) => {
|
|
|
|
|
const segment = internalSegments[i]
|
|
|
|
|
const segments = internalSegments.toSpliced(i, 1, {...segment, controlPoint})
|
|
|
|
|
const segments = internalSegments.toSpliced(i, 1, {
|
|
|
|
|
...segment,
|
|
|
|
|
controlPoint,
|
|
|
|
|
})
|
|
|
|
|
onSegmentsChanges(segments)
|
|
|
|
|
}}
|
|
|
|
|
onMoves={(controlPoint) => {
|
|
|
|
|
setInternalSegments(is => {
|
|
|
|
|
return is.toSpliced(i, 1, {...is[i], controlPoint})
|
|
|
|
|
setInternalSegments((is) => {
|
|
|
|
|
return is.toSpliced(i, 1, {
|
|
|
|
|
...is[i],
|
|
|
|
|
controlPoint,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
@ -135,9 +143,7 @@ export default function BendableArrow({
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const update = useCallback(() => {
|
|
|
|
|
|
|
|
|
|
// only one segment is supported for now, which is the first.
|
|
|
|
|
// any other segments will be ignored
|
|
|
|
|
const segment = internalSegments[0] ?? null
|
|
|
|
@ -146,16 +152,18 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
const endPos = segment.next
|
|
|
|
|
|
|
|
|
|
const basePos = containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
const basePos =
|
|
|
|
|
containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
const startRelative = relativeTo(startPos, basePos)
|
|
|
|
|
const endRelative = relativeTo(endPos!, basePos)
|
|
|
|
|
|
|
|
|
|
const controlPoint = segment.controlPoint ? relativeTo(segment.controlPoint, basePos) : {
|
|
|
|
|
x: startRelative.x / 2 + endRelative.x / 2,
|
|
|
|
|
y: startRelative.y / 2 + endRelative.y / 2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const controlPoint = segment.controlPoint
|
|
|
|
|
? relativeTo(segment.controlPoint, basePos)
|
|
|
|
|
: {
|
|
|
|
|
x: startRelative.x / 2 + endRelative.x / 2,
|
|
|
|
|
y: startRelative.y / 2 + endRelative.y / 2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tailPos = constraintInCircle(
|
|
|
|
|
startRelative,
|
|
|
|
@ -171,53 +179,56 @@ export default function BendableArrow({
|
|
|
|
|
const left = Math.min(tailPos.x, headPos.x)
|
|
|
|
|
const top = Math.min(tailPos.y, headPos.y)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.assign(tailRef.current!.style, {
|
|
|
|
|
left: tailPos.x + "px",
|
|
|
|
|
top: tailPos.y + "px",
|
|
|
|
|
transformOrigin: "top center",
|
|
|
|
|
transform: `translateX(-50%) rotate(${-angle(tailPos, controlPoint) * (180 / Math.PI)}deg)`
|
|
|
|
|
transform: `translateX(-50%) rotate(${
|
|
|
|
|
-angle(tailPos, controlPoint) * (180 / Math.PI)
|
|
|
|
|
}deg)`,
|
|
|
|
|
} as CSSProperties)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.assign(headRef.current!.style, {
|
|
|
|
|
left: headPos.x + "px",
|
|
|
|
|
top: headPos.y + "px",
|
|
|
|
|
transformOrigin: "top center",
|
|
|
|
|
transform: `translateX(-50%) rotate(${-angle(headPos, controlPoint) * (180 / Math.PI)}deg)`
|
|
|
|
|
transform: `translateX(-50%) rotate(${
|
|
|
|
|
-angle(headPos, controlPoint) * (180 / Math.PI)
|
|
|
|
|
}deg)`,
|
|
|
|
|
} as CSSProperties)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const svgStyle: CSSProperties = {
|
|
|
|
|
left: left + "px",
|
|
|
|
|
top: top + "px",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segmentsRelatives = internalSegments.map(({next, controlPoint}) => {
|
|
|
|
|
return {
|
|
|
|
|
next: relativeTo(next, basePos),
|
|
|
|
|
cp: controlPoint ? relativeTo(controlPoint, basePos) : undefined
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const segmentsRelatives = internalSegments.map(
|
|
|
|
|
({ next, controlPoint }) => {
|
|
|
|
|
return {
|
|
|
|
|
next: relativeTo(next, basePos),
|
|
|
|
|
cp: controlPoint
|
|
|
|
|
? relativeTo(controlPoint, basePos)
|
|
|
|
|
: undefined,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const computedSegments = segmentsRelatives
|
|
|
|
|
.map(({next: n, cp}, idx) => {
|
|
|
|
|
.map(({ next: n, cp }, idx) => {
|
|
|
|
|
let next = n
|
|
|
|
|
|
|
|
|
|
if (idx == internalSegments.length - 1) {
|
|
|
|
|
//if it is the last element
|
|
|
|
|
next = constraintInCircle(
|
|
|
|
|
next,
|
|
|
|
|
controlPoint,
|
|
|
|
|
endRadius!,
|
|
|
|
|
)
|
|
|
|
|
next = constraintInCircle(next, controlPoint, endRadius!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cp == undefined) {
|
|
|
|
|
return `L${next.x - left} ${next.y - top}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${cp.y - top}, ${next.x - left} ${next.y - top}`
|
|
|
|
|
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
|
|
|
|
|
cp.y - top
|
|
|
|
|
}, ${next.x - left} ${next.y - top}`
|
|
|
|
|
})
|
|
|
|
|
.join(" ")
|
|
|
|
|
|
|
|
|
@ -225,16 +236,15 @@ export default function BendableArrow({
|
|
|
|
|
pathRef.current!.setAttribute("d", d)
|
|
|
|
|
Object.assign(svgRef.current!.style, svgStyle)
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
setControlPointsDots(computeControlPoints(basePos))
|
|
|
|
|
}
|
|
|
|
|
setControlPointsDots(computeControlPoints(basePos))
|
|
|
|
|
}, [startPos, internalSegments])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(update, [update])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} style={{position: "absolute", top: 0, left: 0}}>
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
style={{ position: "absolute", top: 0, left: 0 }}>
|
|
|
|
|
<svg
|
|
|
|
|
ref={svgRef}
|
|
|
|
|
style={{
|
|
|
|
@ -251,18 +261,18 @@ export default function BendableArrow({
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
|
|
<div className={"arrow-head"}
|
|
|
|
|
style={{position: "absolute", transformOrigin: "center"}}
|
|
|
|
|
ref={headRef}
|
|
|
|
|
>
|
|
|
|
|
{style?.head?.call(style) ?? <Triangle fill={"red"}/>}
|
|
|
|
|
<div
|
|
|
|
|
className={"arrow-head"}
|
|
|
|
|
style={{ position: "absolute", transformOrigin: "center" }}
|
|
|
|
|
ref={headRef}>
|
|
|
|
|
{style?.head?.call(style) ?? <Triangle fill={"red"} />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className={"arrow-tail"}
|
|
|
|
|
style={{position: "absolute", transformOrigin: "center"}}
|
|
|
|
|
ref={tailRef}
|
|
|
|
|
>
|
|
|
|
|
{style?.tail?.call(style) ?? <Triangle fill={"blue"}/>}
|
|
|
|
|
<div
|
|
|
|
|
className={"arrow-tail"}
|
|
|
|
|
style={{ position: "absolute", transformOrigin: "center" }}
|
|
|
|
|
ref={tailRef}>
|
|
|
|
|
{style?.tail?.call(style) ?? <Triangle fill={"blue"} />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isSelected && controlPointsDots}
|
|
|
|
@ -272,18 +282,18 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
interface ControlPointProps {
|
|
|
|
|
pos: Pos
|
|
|
|
|
basePos: Pos,
|
|
|
|
|
basePos: Pos
|
|
|
|
|
onMoves: (currentPos: Pos) => void
|
|
|
|
|
onPosValidated: (newPos: Pos) => void,
|
|
|
|
|
onPosValidated: (newPos: Pos | undefined) => void
|
|
|
|
|
radius?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ControlPoint({
|
|
|
|
|
pos,
|
|
|
|
|
onMoves,
|
|
|
|
|
onPosValidated,
|
|
|
|
|
radius = 7,
|
|
|
|
|
}: ControlPointProps) {
|
|
|
|
|
pos,
|
|
|
|
|
onMoves,
|
|
|
|
|
onPosValidated,
|
|
|
|
|
radius = 7,
|
|
|
|
|
}: ControlPointProps) {
|
|
|
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
@ -297,8 +307,7 @@ function ControlPoint({
|
|
|
|
|
const pointPos = middlePos(ref.current!.getBoundingClientRect())
|
|
|
|
|
onMoves(pointPos)
|
|
|
|
|
}}
|
|
|
|
|
position={{x: pos.x - radius, y: pos.y - radius}}
|
|
|
|
|
>
|
|
|
|
|
position={{ x: pos.x - radius, y: pos.y - radius }}>
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
className={"arrow-edge-control-point"}
|
|
|
|
@ -307,6 +316,12 @@ function ControlPoint({
|
|
|
|
|
width: radius * 2,
|
|
|
|
|
height: radius * 2,
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key == "Delete") {
|
|
|
|
|
onPosValidated(undefined)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
/>
|
|
|
|
|
</Draggable>
|
|
|
|
|
)
|
|
|
|
|