|
|
|
@ -1,4 +1,12 @@
|
|
|
|
|
import {CSSProperties, ReactElement, RefObject, useCallback, useEffect, useRef, useState,} from "react"
|
|
|
|
|
import {
|
|
|
|
|
CSSProperties,
|
|
|
|
|
ReactElement,
|
|
|
|
|
RefObject,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
|
|
|
|
import {
|
|
|
|
|
add,
|
|
|
|
|
angle,
|
|
|
|
@ -10,7 +18,6 @@ import {
|
|
|
|
|
Pos,
|
|
|
|
|
posWithinBase,
|
|
|
|
|
ratioWithinBase,
|
|
|
|
|
relativeTo,
|
|
|
|
|
} from "./Pos"
|
|
|
|
|
|
|
|
|
|
import "../../style/bendable_arrows.css"
|
|
|
|
@ -21,6 +28,7 @@ export interface BendableArrowProps {
|
|
|
|
|
startPos: Pos
|
|
|
|
|
segments: Segment[]
|
|
|
|
|
onSegmentsChanges: (edges: Segment[]) => void
|
|
|
|
|
forceStraight: boolean
|
|
|
|
|
|
|
|
|
|
startRadius?: number
|
|
|
|
|
endRadius?: number
|
|
|
|
@ -32,6 +40,7 @@ export interface BendableArrowProps {
|
|
|
|
|
|
|
|
|
|
export interface ArrowStyle {
|
|
|
|
|
width?: number
|
|
|
|
|
dashArray?: string
|
|
|
|
|
head?: () => ReactElement
|
|
|
|
|
tail?: () => ReactElement
|
|
|
|
|
}
|
|
|
|
@ -54,27 +63,19 @@ function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}/>
|
|
|
|
|
</svg>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BendableArrow({
|
|
|
|
|
area,
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
|
onSegmentsChanges,
|
|
|
|
|
|
|
|
|
|
style,
|
|
|
|
|
startRadius = 0,
|
|
|
|
|
endRadius = 0,
|
|
|
|
|
onDeleteRequested = () => {
|
|
|
|
|
},
|
|
|
|
|
}: BendableArrowProps) {
|
|
|
|
|
area,
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
|
onSegmentsChanges,
|
|
|
|
|
forceStraight,
|
|
|
|
|
|
|
|
|
|
style,
|
|
|
|
|
startRadius = 0,
|
|
|
|
|
endRadius = 0,
|
|
|
|
|
onDeleteRequested,
|
|
|
|
|
}: BendableArrowProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const svgRef = useRef<SVGSVGElement>(null)
|
|
|
|
|
const pathRef = useRef<SVGPathElement>(null)
|
|
|
|
@ -93,7 +94,7 @@ export default function BendableArrow({
|
|
|
|
|
const tailRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
function computeControlPoints(parentBase: DOMRect) {
|
|
|
|
|
return segments.flatMap(({next, controlPoint}, i) => {
|
|
|
|
|
return segments.flatMap(({ next, controlPoint }, i) => {
|
|
|
|
|
const prev = i == 0 ? startPos : segments[i - 1].next
|
|
|
|
|
|
|
|
|
|
const prevRelative = posWithinBase(prev, parentBase)
|
|
|
|
@ -134,34 +135,34 @@ export default function BendableArrow({
|
|
|
|
|
}}
|
|
|
|
|
/>,
|
|
|
|
|
//next pos point (only if this is not the last segment)
|
|
|
|
|
i != segments.length - 1 && <ArrowPoint
|
|
|
|
|
className={"arrow-point-next"}
|
|
|
|
|
posRatio={next}
|
|
|
|
|
parentBase={parentBase}
|
|
|
|
|
onPosValidated={next => {
|
|
|
|
|
const currentSegment = segments[i]
|
|
|
|
|
const newSegments = segments.toSpliced(i, 1, {
|
|
|
|
|
...currentSegment,
|
|
|
|
|
next,
|
|
|
|
|
})
|
|
|
|
|
onSegmentsChanges(newSegments)
|
|
|
|
|
}}
|
|
|
|
|
onRemove={() => {
|
|
|
|
|
onSegmentsChanges(segments.toSpliced(
|
|
|
|
|
Math.max(i - 1, 0),
|
|
|
|
|
1,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onMoves={next => {
|
|
|
|
|
setInternalSegments((is) => {
|
|
|
|
|
return is.toSpliced(i, 1, {
|
|
|
|
|
...is[i],
|
|
|
|
|
i != segments.length - 1 && (
|
|
|
|
|
<ArrowPoint
|
|
|
|
|
className={"arrow-point-next"}
|
|
|
|
|
posRatio={next}
|
|
|
|
|
parentBase={parentBase}
|
|
|
|
|
onPosValidated={(next) => {
|
|
|
|
|
const currentSegment = segments[i]
|
|
|
|
|
const newSegments = segments.toSpliced(i, 1, {
|
|
|
|
|
...currentSegment,
|
|
|
|
|
next,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
onSegmentsChanges(newSegments)
|
|
|
|
|
}}
|
|
|
|
|
onRemove={() => {
|
|
|
|
|
onSegmentsChanges(
|
|
|
|
|
segments.toSpliced(Math.max(i - 1, 0), 1),
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onMoves={(next) => {
|
|
|
|
|
setInternalSegments((is) => {
|
|
|
|
|
return is.toSpliced(i, 1, {
|
|
|
|
|
...is[i],
|
|
|
|
|
next,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
@ -170,21 +171,28 @@ export default function BendableArrow({
|
|
|
|
|
const parentBase = area.current!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
const firstSegment = internalSegments[0] ?? null
|
|
|
|
|
if (firstSegment == null) throw new Error("segments might not be empty.")
|
|
|
|
|
if (firstSegment == null)
|
|
|
|
|
throw new Error("segments might not be empty.")
|
|
|
|
|
|
|
|
|
|
const lastSegment = internalSegments[internalSegments.length - 1]
|
|
|
|
|
|
|
|
|
|
const startRelative = posWithinBase(startPos, parentBase)
|
|
|
|
|
const endRelative = posWithinBase(lastSegment.next, parentBase)
|
|
|
|
|
|
|
|
|
|
const startNext = firstSegment.controlPoint
|
|
|
|
|
? posWithinBase(firstSegment.controlPoint, parentBase)
|
|
|
|
|
: posWithinBase(firstSegment.next, parentBase)
|
|
|
|
|
|
|
|
|
|
const endPrevious = lastSegment.controlPoint
|
|
|
|
|
? posWithinBase(lastSegment.controlPoint, parentBase)
|
|
|
|
|
: internalSegments[internalSegments.length - 2]
|
|
|
|
|
? posWithinBase(internalSegments[internalSegments.length - 2].next, parentBase)
|
|
|
|
|
const startNext =
|
|
|
|
|
firstSegment.controlPoint && !forceStraight
|
|
|
|
|
? posWithinBase(firstSegment.controlPoint, parentBase)
|
|
|
|
|
: posWithinBase(firstSegment.next, parentBase)
|
|
|
|
|
|
|
|
|
|
const endPrevious = forceStraight
|
|
|
|
|
? startRelative
|
|
|
|
|
: lastSegment.controlPoint
|
|
|
|
|
? posWithinBase(lastSegment.controlPoint, parentBase)
|
|
|
|
|
: internalSegments[internalSegments.length - 2]
|
|
|
|
|
? posWithinBase(
|
|
|
|
|
internalSegments[internalSegments.length - 2].next,
|
|
|
|
|
parentBase,
|
|
|
|
|
)
|
|
|
|
|
: startRelative
|
|
|
|
|
|
|
|
|
|
const tailPos = constraintInCircle(
|
|
|
|
@ -192,11 +200,7 @@ export default function BendableArrow({
|
|
|
|
|
startNext,
|
|
|
|
|
startRadius!,
|
|
|
|
|
)
|
|
|
|
|
const headPos = constraintInCircle(
|
|
|
|
|
endRelative,
|
|
|
|
|
endPrevious,
|
|
|
|
|
endRadius!,
|
|
|
|
|
)
|
|
|
|
|
const headPos = constraintInCircle(endRelative, endPrevious, endRadius!)
|
|
|
|
|
|
|
|
|
|
const left = Math.min(tailPos.x, headPos.x)
|
|
|
|
|
const top = Math.min(tailPos.y, headPos.y)
|
|
|
|
@ -224,25 +228,29 @@ export default function BendableArrow({
|
|
|
|
|
top: top + "px",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segmentsRelatives = internalSegments.map(
|
|
|
|
|
({next, controlPoint}, idx) => {
|
|
|
|
|
const nextPos = posWithinBase(next, parentBase)
|
|
|
|
|
return {
|
|
|
|
|
next: nextPos,
|
|
|
|
|
cp: controlPoint
|
|
|
|
|
const segmentsRelatives = (
|
|
|
|
|
forceStraight ? internalSegments.slice(-1) : internalSegments
|
|
|
|
|
).map(({ next, controlPoint }, idx) => {
|
|
|
|
|
const nextPos = posWithinBase(next, parentBase)
|
|
|
|
|
return {
|
|
|
|
|
next: nextPos,
|
|
|
|
|
cp:
|
|
|
|
|
controlPoint && !forceStraight
|
|
|
|
|
? posWithinBase(controlPoint, parentBase)
|
|
|
|
|
: between(
|
|
|
|
|
idx == 0
|
|
|
|
|
? startRelative
|
|
|
|
|
: posWithinBase(internalSegments[idx - 1].next, parentBase),
|
|
|
|
|
nextPos
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
idx == 0
|
|
|
|
|
? startRelative
|
|
|
|
|
: posWithinBase(
|
|
|
|
|
internalSegments[idx - 1].next,
|
|
|
|
|
parentBase,
|
|
|
|
|
),
|
|
|
|
|
nextPos,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const computedSegments = segmentsRelatives
|
|
|
|
|
.map(({next: n, cp}, idx) => {
|
|
|
|
|
.map(({ next: n, cp }, idx) => {
|
|
|
|
|
let next = n
|
|
|
|
|
|
|
|
|
|
if (idx == internalSegments.length - 1) {
|
|
|
|
@ -259,7 +267,7 @@ 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)
|
|
|
|
|
}, [startPos, internalSegments])
|
|
|
|
|
}, [startPos, internalSegments, forceStraight])
|
|
|
|
|
|
|
|
|
|
useEffect(update, [update])
|
|
|
|
|
|
|
|
|
@ -281,12 +289,16 @@ export default function BendableArrow({
|
|
|
|
|
}, [update, containerRef])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (forceStraight) return
|
|
|
|
|
|
|
|
|
|
const addSegment = (e: MouseEvent) => {
|
|
|
|
|
const parentBase = area.current!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
const clickAbsolutePos: Pos = {x: e.x, y: e.y}
|
|
|
|
|
const clickPosBaseRatio = ratioWithinBase(clickAbsolutePos, parentBase)
|
|
|
|
|
const clickAbsolutePos: Pos = { x: e.x, y: e.y }
|
|
|
|
|
const clickPosBaseRatio = ratioWithinBase(
|
|
|
|
|
clickAbsolutePos,
|
|
|
|
|
parentBase,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let segmentInsertionIndex = -1
|
|
|
|
|
let segmentInsertionIsOnRightOfCP = false
|
|
|
|
@ -295,53 +307,60 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
let currentPos = i == 0 ? startPos : segments[i - 1].next
|
|
|
|
|
let nextPos = segment.next
|
|
|
|
|
let controlPointPos = segment.controlPoint ? segment.controlPoint : between(currentPos, nextPos)
|
|
|
|
|
|
|
|
|
|
const result = searchOnSegment(currentPos, controlPointPos, nextPos, clickPosBaseRatio, 0.05)
|
|
|
|
|
if (result == PointSegmentSearchResult.NOT_FOUND)
|
|
|
|
|
continue
|
|
|
|
|
let controlPointPos = segment.controlPoint
|
|
|
|
|
? segment.controlPoint
|
|
|
|
|
: between(currentPos, nextPos)
|
|
|
|
|
|
|
|
|
|
const result = searchOnSegment(
|
|
|
|
|
currentPos,
|
|
|
|
|
controlPointPos,
|
|
|
|
|
nextPos,
|
|
|
|
|
clickPosBaseRatio,
|
|
|
|
|
0.05,
|
|
|
|
|
)
|
|
|
|
|
if (result == PointSegmentSearchResult.NOT_FOUND) continue
|
|
|
|
|
|
|
|
|
|
segmentInsertionIndex = i
|
|
|
|
|
segmentInsertionIsOnRightOfCP = result == PointSegmentSearchResult.RIGHT_TO_CONTROL_POINT
|
|
|
|
|
segmentInsertionIsOnRightOfCP =
|
|
|
|
|
result == PointSegmentSearchResult.RIGHT_TO_CONTROL_POINT
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (segmentInsertionIndex == -1)
|
|
|
|
|
return
|
|
|
|
|
if (segmentInsertionIndex == -1) return
|
|
|
|
|
|
|
|
|
|
const splicedSegment: Segment = segments[segmentInsertionIndex]
|
|
|
|
|
|
|
|
|
|
let newSegments: Segment[]
|
|
|
|
|
if (segmentInsertionIsOnRightOfCP) {
|
|
|
|
|
newSegments = segments.toSpliced(
|
|
|
|
|
segmentInsertionIndex,
|
|
|
|
|
1,
|
|
|
|
|
{next: clickPosBaseRatio, controlPoint: splicedSegment.controlPoint},
|
|
|
|
|
{next: splicedSegment.next, controlPoint: undefined}
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
newSegments = segments.toSpliced(
|
|
|
|
|
onSegmentsChanges(
|
|
|
|
|
segments.toSpliced(
|
|
|
|
|
segmentInsertionIndex,
|
|
|
|
|
1,
|
|
|
|
|
{next: clickPosBaseRatio, controlPoint: undefined},
|
|
|
|
|
{next: splicedSegment.next, controlPoint: splicedSegment.controlPoint}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSegmentsChanges(newSegments)
|
|
|
|
|
{
|
|
|
|
|
next: clickPosBaseRatio,
|
|
|
|
|
controlPoint: segmentInsertionIsOnRightOfCP
|
|
|
|
|
? splicedSegment.controlPoint
|
|
|
|
|
: undefined,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
next: splicedSegment.next,
|
|
|
|
|
controlPoint: segmentInsertionIsOnRightOfCP
|
|
|
|
|
? undefined
|
|
|
|
|
: splicedSegment.controlPoint,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pathRef?.current?.addEventListener('dblclick', addSegment)
|
|
|
|
|
pathRef?.current?.addEventListener("dblclick", addSegment)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
pathRef?.current?.removeEventListener('dblclick', addSegment)
|
|
|
|
|
pathRef?.current?.removeEventListener("dblclick", addSegment)
|
|
|
|
|
}
|
|
|
|
|
}, [pathRef, segments]);
|
|
|
|
|
}, [pathRef, segments, onSegmentsChanges])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
style={{position: "absolute", top: 0, left: 0}}>
|
|
|
|
|
style={{ position: "absolute", top: 0, left: 0 }}>
|
|
|
|
|
<svg
|
|
|
|
|
ref={svgRef}
|
|
|
|
|
style={{
|
|
|
|
@ -354,29 +373,33 @@ export default function BendableArrow({
|
|
|
|
|
ref={pathRef}
|
|
|
|
|
stroke={"#000"}
|
|
|
|
|
strokeWidth={styleWidth}
|
|
|
|
|
strokeDasharray={style?.dashArray}
|
|
|
|
|
fill="none"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onKeyUp={(e) => {
|
|
|
|
|
if (e.key == "Delete") onDeleteRequested()
|
|
|
|
|
if (onDeleteRequested && e.key == "Delete")
|
|
|
|
|
onDeleteRequested()
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={"arrow-head"}
|
|
|
|
|
style={{position: "absolute", transformOrigin: "center"}}
|
|
|
|
|
style={{ position: "absolute", transformOrigin: "center" }}
|
|
|
|
|
ref={headRef}>
|
|
|
|
|
{style?.head?.call(style) ?? <Triangle fill={"red"}/>}
|
|
|
|
|
{style?.head?.call(style)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={"arrow-tail"}
|
|
|
|
|
style={{position: "absolute", transformOrigin: "center"}}
|
|
|
|
|
style={{ position: "absolute", transformOrigin: "center" }}
|
|
|
|
|
ref={tailRef}>
|
|
|
|
|
{style?.tail?.call(style) ?? <Triangle fill={"blue"}/>}
|
|
|
|
|
{style?.tail?.call(style)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isSelected && computeControlPoints(area.current!.getBoundingClientRect())}
|
|
|
|
|
{!forceStraight &&
|
|
|
|
|
isSelected &&
|
|
|
|
|
computeControlPoints(area.current!.getBoundingClientRect())}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
@ -394,13 +417,20 @@ interface ControlPointProps {
|
|
|
|
|
enum PointSegmentSearchResult {
|
|
|
|
|
LEFT_TO_CONTROL_POINT,
|
|
|
|
|
RIGHT_TO_CONTROL_POINT,
|
|
|
|
|
NOT_FOUND
|
|
|
|
|
NOT_FOUND,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function searchOnSegment(startPos: Pos, controlPoint: Pos, endPos: Pos, point: Pos, minDistance: number): PointSegmentSearchResult {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const step = 1 / ((distance(startPos, controlPoint) + distance(controlPoint, endPos)) / minDistance)
|
|
|
|
|
function searchOnSegment(
|
|
|
|
|
startPos: Pos,
|
|
|
|
|
controlPoint: Pos,
|
|
|
|
|
endPos: Pos,
|
|
|
|
|
point: Pos,
|
|
|
|
|
minDistance: number,
|
|
|
|
|
): PointSegmentSearchResult {
|
|
|
|
|
const step =
|
|
|
|
|
1 /
|
|
|
|
|
((distance(startPos, controlPoint) + distance(controlPoint, endPos)) /
|
|
|
|
|
minDistance)
|
|
|
|
|
|
|
|
|
|
const p0MinusP1 = minus(startPos, controlPoint)
|
|
|
|
|
const p2MinusP1 = minus(endPos, controlPoint)
|
|
|
|
@ -408,22 +438,12 @@ function searchOnSegment(startPos: Pos, controlPoint: Pos, endPos: Pos, point: P
|
|
|
|
|
function getDistanceAt(t: number): number {
|
|
|
|
|
// apply the bezier function
|
|
|
|
|
const pos = add(
|
|
|
|
|
add(
|
|
|
|
|
controlPoint,
|
|
|
|
|
mul(
|
|
|
|
|
p0MinusP1,
|
|
|
|
|
(1 - t) ** 2
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
mul(
|
|
|
|
|
p2MinusP1,
|
|
|
|
|
t ** 2
|
|
|
|
|
)
|
|
|
|
|
add(controlPoint, mul(p0MinusP1, (1 - t) ** 2)),
|
|
|
|
|
mul(p2MinusP1, t ** 2),
|
|
|
|
|
)
|
|
|
|
|
return distance(pos, point)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let t = 0; t < 1; t += step) {
|
|
|
|
|
if (getDistanceAt(t) <= minDistance)
|
|
|
|
|
return t >= 0.5
|
|
|
|
@ -439,18 +459,18 @@ let slice = 0.5
|
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
|
|
|
t += slice
|
|
|
|
|
slice /= 2
|
|
|
|
|
// console.log(t)
|
|
|
|
|
// console.log(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ArrowPoint({
|
|
|
|
|
className,
|
|
|
|
|
posRatio,
|
|
|
|
|
parentBase,
|
|
|
|
|
onMoves,
|
|
|
|
|
onPosValidated,
|
|
|
|
|
onRemove,
|
|
|
|
|
radius = 7,
|
|
|
|
|
}: ControlPointProps) {
|
|
|
|
|
className,
|
|
|
|
|
posRatio,
|
|
|
|
|
parentBase,
|
|
|
|
|
onMoves,
|
|
|
|
|
onPosValidated,
|
|
|
|
|
onRemove,
|
|
|
|
|
radius = 7,
|
|
|
|
|
}: ControlPointProps) {
|
|
|
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
const pos = posWithinBase(posRatio, parentBase)
|
|
|
|
@ -466,7 +486,7 @@ function ArrowPoint({
|
|
|
|
|
const pointPos = middlePos(ref.current!.getBoundingClientRect())
|
|
|
|
|
onMoves(ratioWithinBase(pointPos, parentBase))
|
|
|
|
|
}}
|
|
|
|
|
position={{x: pos.x - radius, y: pos.y - radius}}>
|
|
|
|
|
position={{ x: pos.x - radius, y: pos.y - radius }}>
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
className={`arrow-point ${className}`}
|
|
|
|
|