|
|
|
@ -1,17 +1,26 @@
|
|
|
|
|
import {
|
|
|
|
|
CSSProperties,
|
|
|
|
|
ReactElement,
|
|
|
|
|
RefObject,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
|
|
|
|
import { angle, middlePos, Pos, relativeTo } from "./Pos"
|
|
|
|
|
import {
|
|
|
|
|
add,
|
|
|
|
|
angle,
|
|
|
|
|
middlePos,
|
|
|
|
|
Pos,
|
|
|
|
|
posWithinBase,
|
|
|
|
|
ratioWithinBase,
|
|
|
|
|
} from "./Pos"
|
|
|
|
|
|
|
|
|
|
import "../../style/bendable_arrows.css"
|
|
|
|
|
import Draggable from "react-draggable"
|
|
|
|
|
|
|
|
|
|
export interface BendableArrowProps {
|
|
|
|
|
area: RefObject<HTMLElement>
|
|
|
|
|
startPos: Pos
|
|
|
|
|
segments: Segment[]
|
|
|
|
|
onSegmentsChanges: (edges: Segment[]) => void
|
|
|
|
@ -55,6 +64,7 @@ function Triangle({ fill }: { fill: string }) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BendableArrow({
|
|
|
|
|
area,
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
@ -66,15 +76,10 @@ export default function BendableArrow({
|
|
|
|
|
}: BendableArrowProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
|
|
|
|
|
|
|
|
const pathRef = useRef<SVGPathElement>(null)
|
|
|
|
|
|
|
|
|
|
const styleWidth = style?.width ?? ArrowStyleDefaults.width
|
|
|
|
|
|
|
|
|
|
const [controlPointsDots, setControlPointsDots] = useState<ReactElement[]>(
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setInternalSegments(segments)
|
|
|
|
|
}, [segments])
|
|
|
|
@ -86,49 +91,38 @@ export default function BendableArrow({
|
|
|
|
|
const headRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const tailRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const basePos =
|
|
|
|
|
containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
setControlPointsDots(computeControlPoints(basePos))
|
|
|
|
|
|
|
|
|
|
const selectionHandler = (e: MouseEvent) => {
|
|
|
|
|
if (!(e.target instanceof Node)) return
|
|
|
|
|
|
|
|
|
|
const isSelected = containerRef.current!.contains(e.target)
|
|
|
|
|
setIsSelected(isSelected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mousedown", selectionHandler)
|
|
|
|
|
return () => document.removeEventListener("mousedown", selectionHandler)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
function computeControlPoints(basePos: Pos) {
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
function computeControlPoints(parentBase: DOMRect) {
|
|
|
|
|
return segments.map(({ next, controlPoint }, i) => {
|
|
|
|
|
const prev = i == 0 ? startPos : segments[i - 1].next
|
|
|
|
|
|
|
|
|
|
const prevRelative = posWithinBase(prev, parentBase)
|
|
|
|
|
const nextRelative = posWithinBase(next, parentBase)
|
|
|
|
|
|
|
|
|
|
const cpPos =
|
|
|
|
|
controlPoint ||
|
|
|
|
|
ratioWithinBase(
|
|
|
|
|
add(
|
|
|
|
|
{
|
|
|
|
|
x: prevRelative.x / 2 + nextRelative.x / 2,
|
|
|
|
|
y: prevRelative.y / 2 + nextRelative.y / 2,
|
|
|
|
|
},
|
|
|
|
|
parentBase,
|
|
|
|
|
),
|
|
|
|
|
parentBase,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ControlPoint
|
|
|
|
|
key={i}
|
|
|
|
|
pos={cpPos}
|
|
|
|
|
basePos={basePos}
|
|
|
|
|
posRatio={cpPos}
|
|
|
|
|
parentBase={parentBase}
|
|
|
|
|
onPosValidated={(controlPoint) => {
|
|
|
|
|
const segment = internalSegments[i]
|
|
|
|
|
const segments = internalSegments.toSpliced(i, 1, {
|
|
|
|
|
const segment = segments[i]
|
|
|
|
|
const newSegments = segments.toSpliced(i, 1, {
|
|
|
|
|
...segment,
|
|
|
|
|
controlPoint,
|
|
|
|
|
})
|
|
|
|
|
onSegmentsChanges(segments)
|
|
|
|
|
onSegmentsChanges(newSegments)
|
|
|
|
|
}}
|
|
|
|
|
onMoves={(controlPoint) => {
|
|
|
|
|
setInternalSegments((is) => {
|
|
|
|
@ -144,6 +138,8 @@ export default function BendableArrow({
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const update = useCallback(() => {
|
|
|
|
|
const parentBase = area.current!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
// only one segment is supported for now, which is the first.
|
|
|
|
|
// any other segments will be ignored
|
|
|
|
|
const segment = internalSegments[0] ?? null
|
|
|
|
@ -152,14 +148,11 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
const endPos = segment.next
|
|
|
|
|
|
|
|
|
|
const basePos =
|
|
|
|
|
containerRef.current!.parentElement!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
const startRelative = relativeTo(startPos, basePos)
|
|
|
|
|
const endRelative = relativeTo(endPos!, basePos)
|
|
|
|
|
const startRelative = posWithinBase(startPos, parentBase)
|
|
|
|
|
const endRelative = posWithinBase(endPos, parentBase)
|
|
|
|
|
|
|
|
|
|
const controlPoint = segment.controlPoint
|
|
|
|
|
? relativeTo(segment.controlPoint, basePos)
|
|
|
|
|
? posWithinBase(segment.controlPoint, parentBase)
|
|
|
|
|
: {
|
|
|
|
|
x: startRelative.x / 2 + endRelative.x / 2,
|
|
|
|
|
y: startRelative.y / 2 + endRelative.y / 2,
|
|
|
|
@ -205,10 +198,13 @@ export default function BendableArrow({
|
|
|
|
|
const segmentsRelatives = internalSegments.map(
|
|
|
|
|
({ next, controlPoint }) => {
|
|
|
|
|
return {
|
|
|
|
|
next: relativeTo(next, basePos),
|
|
|
|
|
next: posWithinBase(next, parentBase),
|
|
|
|
|
cp: controlPoint
|
|
|
|
|
? relativeTo(controlPoint, basePos)
|
|
|
|
|
: undefined,
|
|
|
|
|
? posWithinBase(controlPoint, parentBase)
|
|
|
|
|
: {
|
|
|
|
|
x: startRelative.x / 2 + endRelative.x / 2,
|
|
|
|
|
y: startRelative.y / 2 + endRelative.y / 2,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
@ -219,11 +215,7 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
if (idx == internalSegments.length - 1) {
|
|
|
|
|
//if it is the last element
|
|
|
|
|
next = constraintInCircle(next, controlPoint, endRadius!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cp == undefined) {
|
|
|
|
|
return `L${next.x - left} ${next.y - top}`
|
|
|
|
|
next = constraintInCircle(next, cp, endRadius!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
|
|
|
|
@ -235,12 +227,27 @@ export default function BendableArrow({
|
|
|
|
|
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
|
|
|
|
|
pathRef.current!.setAttribute("d", d)
|
|
|
|
|
Object.assign(svgRef.current!.style, svgStyle)
|
|
|
|
|
|
|
|
|
|
setControlPointsDots(computeControlPoints(basePos))
|
|
|
|
|
}, [startPos, internalSegments])
|
|
|
|
|
|
|
|
|
|
useEffect(update, [update])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const selectionHandler = (e: MouseEvent) => {
|
|
|
|
|
if (!(e.target instanceof Node)) return
|
|
|
|
|
|
|
|
|
|
const isSelected = containerRef.current!.contains(e.target)
|
|
|
|
|
setIsSelected(isSelected)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mousedown", selectionHandler)
|
|
|
|
|
window.addEventListener("resize", update)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("mousedown", selectionHandler)
|
|
|
|
|
window.removeEventListener("resize", update)
|
|
|
|
|
}
|
|
|
|
|
}, [update, containerRef])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
@ -275,37 +282,41 @@ export default function BendableArrow({
|
|
|
|
|
{style?.tail?.call(style) ?? <Triangle fill={"blue"} />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isSelected && controlPointsDots}
|
|
|
|
|
{isSelected &&
|
|
|
|
|
computeControlPoints(area.current!.getBoundingClientRect())}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ControlPointProps {
|
|
|
|
|
pos: Pos
|
|
|
|
|
basePos: Pos
|
|
|
|
|
posRatio: Pos
|
|
|
|
|
parentBase: DOMRect
|
|
|
|
|
onMoves: (currentPos: Pos) => void
|
|
|
|
|
onPosValidated: (newPos: Pos | undefined) => void
|
|
|
|
|
radius?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ControlPoint({
|
|
|
|
|
pos,
|
|
|
|
|
posRatio,
|
|
|
|
|
parentBase,
|
|
|
|
|
onMoves,
|
|
|
|
|
onPosValidated,
|
|
|
|
|
radius = 7,
|
|
|
|
|
}: ControlPointProps) {
|
|
|
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
const pos = posWithinBase(posRatio, parentBase)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Draggable
|
|
|
|
|
nodeRef={ref}
|
|
|
|
|
onStop={() => {
|
|
|
|
|
const pointPos = middlePos(ref.current!.getBoundingClientRect())
|
|
|
|
|
onPosValidated(pointPos)
|
|
|
|
|
onPosValidated(ratioWithinBase(pointPos, parentBase))
|
|
|
|
|
}}
|
|
|
|
|
onDrag={() => {
|
|
|
|
|
const pointPos = middlePos(ref.current!.getBoundingClientRect())
|
|
|
|
|
onMoves(pointPos)
|
|
|
|
|
onMoves(ratioWithinBase(pointPos, parentBase))
|
|
|
|
|
}}
|
|
|
|
|
position={{ x: pos.x - radius, y: pos.y - radius }}>
|
|
|
|
|
<div
|
|
|
|
|