|
|
|
@ -4,6 +4,7 @@ import {
|
|
|
|
|
RefObject,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useLayoutEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from "react"
|
|
|
|
@ -18,6 +19,8 @@ import {
|
|
|
|
|
Pos,
|
|
|
|
|
posWithinBase,
|
|
|
|
|
ratioWithinBase,
|
|
|
|
|
relativeTo,
|
|
|
|
|
size,
|
|
|
|
|
} from "./Pos"
|
|
|
|
|
|
|
|
|
|
import "../../style/bendable_arrows.css"
|
|
|
|
@ -29,6 +32,7 @@ export interface BendableArrowProps {
|
|
|
|
|
segments: Segment[]
|
|
|
|
|
onSegmentsChanges: (edges: Segment[]) => void
|
|
|
|
|
forceStraight: boolean
|
|
|
|
|
wavy: boolean
|
|
|
|
|
|
|
|
|
|
startRadius?: number
|
|
|
|
|
endRadius?: number
|
|
|
|
@ -45,8 +49,8 @@ export interface ArrowStyle {
|
|
|
|
|
tail?: () => ReactElement
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ArrowStyleDefaults = {
|
|
|
|
|
width: 4,
|
|
|
|
|
const ArrowStyleDefaults: ArrowStyle = {
|
|
|
|
|
width: 3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Segment {
|
|
|
|
@ -54,22 +58,51 @@ export interface Segment {
|
|
|
|
|
controlPoint?: Pos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
|
|
|
|
|
const theta = angle(pos, from)
|
|
|
|
|
/**
|
|
|
|
|
* Given a circle shaped by a central position, and a radius, return
|
|
|
|
|
* a position that is constrained on its perimeter, pointing to the direction
|
|
|
|
|
* between the circle's center and the reference position.
|
|
|
|
|
* @param center circle's center.
|
|
|
|
|
* @param reference a reference point used to create the angle where the returned position
|
|
|
|
|
* will point to on the circle's perimeter
|
|
|
|
|
* @param radius circle's radius.
|
|
|
|
|
*/
|
|
|
|
|
function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos {
|
|
|
|
|
const theta = angle(center, reference)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
x: pos.x - Math.sin(theta) * radius,
|
|
|
|
|
y: pos.y - Math.cos(theta) * radius,
|
|
|
|
|
x: center.x - Math.sin(theta) * radius,
|
|
|
|
|
y: center.y - Math.cos(theta) * radius,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An arrow that follows a bézier curve built from given segments that can be edited, added or removed by the user
|
|
|
|
|
* The arrow only works with relative positions within a given area.
|
|
|
|
|
* All position handled by the arrow must be positions where x and y are a percentage within the area's surface
|
|
|
|
|
* (0.5, 0.5) is a position at the middle of the area
|
|
|
|
|
* (1, 0.75) means that the position is at 100percent to the right of given area, and 75 percent to the bottom
|
|
|
|
|
* @param area
|
|
|
|
|
* @param startPos
|
|
|
|
|
* @param segments
|
|
|
|
|
* @param onSegmentsChanges
|
|
|
|
|
* @param wavy
|
|
|
|
|
* @param forceStraight
|
|
|
|
|
* @param style
|
|
|
|
|
* @param startRadius
|
|
|
|
|
* @param endRadius
|
|
|
|
|
* @param onDeleteRequested
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
|
|
|
|
export default function BendableArrow({
|
|
|
|
|
area,
|
|
|
|
|
startPos,
|
|
|
|
|
|
|
|
|
|
segments,
|
|
|
|
|
onSegmentsChanges,
|
|
|
|
|
|
|
|
|
|
forceStraight,
|
|
|
|
|
wavy,
|
|
|
|
|
|
|
|
|
|
style,
|
|
|
|
|
startRadius = 0,
|
|
|
|
@ -82,18 +115,46 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
const styleWidth = style?.width ?? ArrowStyleDefaults.width
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setInternalSegments(segments)
|
|
|
|
|
}, [segments])
|
|
|
|
|
|
|
|
|
|
const [internalSegments, setInternalSegments] = useState(segments)
|
|
|
|
|
// Cache the segments so that when the user is changing the segments (it moves an ArrowPoint),
|
|
|
|
|
// it does not unwind to this arrow's component parent until validated.
|
|
|
|
|
// The changes are validated (meaning that onSegmentsChanges is called) when the
|
|
|
|
|
// user releases an ArrowPoint.
|
|
|
|
|
const [internalSegments, setInternalSegments] = useState<FullSegment[]>(
|
|
|
|
|
() => computeInternalSegments(segments),
|
|
|
|
|
)
|
|
|
|
|
// If the (original) segments changes, overwrite the current ones.
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
setInternalSegments(computeInternalSegments(segments))
|
|
|
|
|
}, [startPos, segments])
|
|
|
|
|
|
|
|
|
|
const [isSelected, setIsSelected] = useState(false)
|
|
|
|
|
|
|
|
|
|
const headRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const tailRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
function computeControlPoints(parentBase: DOMRect) {
|
|
|
|
|
function computeInternalSegments(segments: Segment[]): FullSegment[] {
|
|
|
|
|
return segments.map((segment, idx) => {
|
|
|
|
|
if (idx == 0) {
|
|
|
|
|
return {
|
|
|
|
|
start: startPos,
|
|
|
|
|
controlPoint: segment.controlPoint,
|
|
|
|
|
end: segment.next,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const start = segments[idx - 1].next
|
|
|
|
|
return {
|
|
|
|
|
start,
|
|
|
|
|
controlPoint: segment.controlPoint,
|
|
|
|
|
end: segment.next,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Computes and return the segments edition points
|
|
|
|
|
* @param parentBase
|
|
|
|
|
*/
|
|
|
|
|
function computePoints(parentBase: DOMRect) {
|
|
|
|
|
return segments.flatMap(({ next, controlPoint }, i) => {
|
|
|
|
|
const prev = i == 0 ? startPos : segments[i - 1].next
|
|
|
|
|
|
|
|
|
@ -137,6 +198,7 @@ export default function BendableArrow({
|
|
|
|
|
//next pos point (only if this is not the last segment)
|
|
|
|
|
i != segments.length - 1 && (
|
|
|
|
|
<ArrowPoint
|
|
|
|
|
key={i + "-2"}
|
|
|
|
|
className={"arrow-point-next"}
|
|
|
|
|
posRatio={next}
|
|
|
|
|
parentBase={parentBase}
|
|
|
|
@ -153,12 +215,20 @@ export default function BendableArrow({
|
|
|
|
|
segments.toSpliced(Math.max(i - 1, 0), 1),
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onMoves={(next) => {
|
|
|
|
|
onMoves={(end) => {
|
|
|
|
|
setInternalSegments((is) => {
|
|
|
|
|
return is.toSpliced(i, 1, {
|
|
|
|
|
return is.toSpliced(
|
|
|
|
|
i,
|
|
|
|
|
2,
|
|
|
|
|
{
|
|
|
|
|
...is[i],
|
|
|
|
|
next,
|
|
|
|
|
})
|
|
|
|
|
end,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
...is[i + 1],
|
|
|
|
|
start: end,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
@ -167,33 +237,30 @@ export default function BendableArrow({
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates the states based on given parameters, which causes the arrow to re-render.
|
|
|
|
|
*/
|
|
|
|
|
const update = useCallback(() => {
|
|
|
|
|
const parentBase = area.current!.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
const firstSegment = internalSegments[0] ?? null
|
|
|
|
|
if (firstSegment == null)
|
|
|
|
|
throw new Error("segments might not be empty.")
|
|
|
|
|
const segment = internalSegments[0] ?? null
|
|
|
|
|
if (segment == 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 endRelative = posWithinBase(lastSegment.end, parentBase)
|
|
|
|
|
|
|
|
|
|
const startNext =
|
|
|
|
|
firstSegment.controlPoint && !forceStraight
|
|
|
|
|
? posWithinBase(firstSegment.controlPoint, parentBase)
|
|
|
|
|
: posWithinBase(firstSegment.next, parentBase)
|
|
|
|
|
segment.controlPoint && !forceStraight
|
|
|
|
|
? posWithinBase(segment.controlPoint, parentBase)
|
|
|
|
|
: posWithinBase(segment.end, parentBase)
|
|
|
|
|
|
|
|
|
|
const endPrevious = forceStraight
|
|
|
|
|
? startRelative
|
|
|
|
|
: lastSegment.controlPoint
|
|
|
|
|
? posWithinBase(lastSegment.controlPoint, parentBase)
|
|
|
|
|
: internalSegments[internalSegments.length - 2]
|
|
|
|
|
? posWithinBase(
|
|
|
|
|
internalSegments[internalSegments.length - 2].next,
|
|
|
|
|
parentBase,
|
|
|
|
|
)
|
|
|
|
|
: startRelative
|
|
|
|
|
: posWithinBase(lastSegment.start, parentBase)
|
|
|
|
|
|
|
|
|
|
const tailPos = constraintInCircle(
|
|
|
|
|
startRelative,
|
|
|
|
@ -229,52 +296,95 @@ export default function BendableArrow({
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const segmentsRelatives = (
|
|
|
|
|
forceStraight ? internalSegments.slice(-1) : internalSegments
|
|
|
|
|
).map(({ next, controlPoint }, idx) => {
|
|
|
|
|
const nextPos = posWithinBase(next, parentBase)
|
|
|
|
|
return {
|
|
|
|
|
next: nextPos,
|
|
|
|
|
cp:
|
|
|
|
|
forceStraight
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
start: startPos,
|
|
|
|
|
controlPoint: undefined,
|
|
|
|
|
end: lastSegment.end,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
: internalSegments
|
|
|
|
|
).map(({ start, controlPoint, end }, idx) => {
|
|
|
|
|
const svgPosRelativeToBase = { x: left, y: top }
|
|
|
|
|
|
|
|
|
|
const nextRelative = relativeTo(
|
|
|
|
|
posWithinBase(end, parentBase),
|
|
|
|
|
svgPosRelativeToBase,
|
|
|
|
|
)
|
|
|
|
|
const startRelative = relativeTo(
|
|
|
|
|
posWithinBase(start, parentBase),
|
|
|
|
|
svgPosRelativeToBase,
|
|
|
|
|
)
|
|
|
|
|
const controlPointRelative =
|
|
|
|
|
controlPoint && !forceStraight
|
|
|
|
|
? posWithinBase(controlPoint, parentBase)
|
|
|
|
|
: between(
|
|
|
|
|
idx == 0
|
|
|
|
|
? startRelative
|
|
|
|
|
: posWithinBase(
|
|
|
|
|
internalSegments[idx - 1].next,
|
|
|
|
|
parentBase,
|
|
|
|
|
),
|
|
|
|
|
nextPos,
|
|
|
|
|
),
|
|
|
|
|
? relativeTo(
|
|
|
|
|
posWithinBase(controlPoint, parentBase),
|
|
|
|
|
svgPosRelativeToBase,
|
|
|
|
|
)
|
|
|
|
|
: between(startRelative, nextRelative)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
start: startRelative,
|
|
|
|
|
end: nextRelative,
|
|
|
|
|
cp: controlPointRelative,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const computedSegments = segmentsRelatives
|
|
|
|
|
.map(({ next: n, cp }, idx) => {
|
|
|
|
|
let next = n
|
|
|
|
|
|
|
|
|
|
if (idx == internalSegments.length - 1) {
|
|
|
|
|
.map(({ start, cp, end: e }, idx) => {
|
|
|
|
|
let end = e
|
|
|
|
|
if (idx == segmentsRelatives.length - 1) {
|
|
|
|
|
//if it is the last element
|
|
|
|
|
next = constraintInCircle(next, cp, endRadius!)
|
|
|
|
|
end = constraintInCircle(end, cp, endRadius!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousSegment =
|
|
|
|
|
idx != 0 ? segmentsRelatives[idx - 1] : undefined
|
|
|
|
|
|
|
|
|
|
const previousSegmentCpAndCurrentPosVector = minus(
|
|
|
|
|
start,
|
|
|
|
|
previousSegment?.cp ?? between(start, end),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const smoothCp = previousSegment
|
|
|
|
|
? add(start, previousSegmentCpAndCurrentPosVector)
|
|
|
|
|
: cp
|
|
|
|
|
|
|
|
|
|
if (wavy) {
|
|
|
|
|
return wavyBezier(start, smoothCp, cp, end, 10, 10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (forceStraight) {
|
|
|
|
|
return `L${end.x} ${end.y}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
|
|
|
|
|
cp.y - top
|
|
|
|
|
}, ${next.x - left} ${next.y - top}`
|
|
|
|
|
return `C${smoothCp.x} ${smoothCp.y}, ${cp.x} ${cp.y}, ${end.x} ${end.y}`
|
|
|
|
|
})
|
|
|
|
|
.join(" ")
|
|
|
|
|
|
|
|
|
|
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
|
|
|
|
|
pathRef.current!.setAttribute("d", d)
|
|
|
|
|
Object.assign(svgRef.current!.style, svgStyle)
|
|
|
|
|
}, [startPos, internalSegments, forceStraight])
|
|
|
|
|
}, [
|
|
|
|
|
startPos,
|
|
|
|
|
internalSegments,
|
|
|
|
|
forceStraight,
|
|
|
|
|
startRadius,
|
|
|
|
|
endRadius,
|
|
|
|
|
style,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Will update the arrow when the props change
|
|
|
|
|
useEffect(update, [update])
|
|
|
|
|
|
|
|
|
|
// Adds a selection handler
|
|
|
|
|
// Also force an update when the window is resized
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const selectionHandler = (e: MouseEvent) => {
|
|
|
|
|
if (!(e.target instanceof Node)) return
|
|
|
|
|
|
|
|
|
|
// The arrow is selected if the mouse clicks on an element that belongs to the current arrow
|
|
|
|
|
const isSelected = containerRef.current!.contains(e.target)
|
|
|
|
|
setIsSelected(isSelected)
|
|
|
|
|
}
|
|
|
|
@ -288,6 +398,7 @@ export default function BendableArrow({
|
|
|
|
|
}
|
|
|
|
|
}, [update, containerRef])
|
|
|
|
|
|
|
|
|
|
// Inserts a segment where the mouse double clicks on the arrow
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (forceStraight) return
|
|
|
|
|
|
|
|
|
@ -304,16 +415,30 @@ export default function BendableArrow({
|
|
|
|
|
let segmentInsertionIsOnRightOfCP = false
|
|
|
|
|
for (let i = 0; i < segments.length; i++) {
|
|
|
|
|
const segment = segments[i]
|
|
|
|
|
const beforeSegment = i != 0 ? segments[i - 1] : undefined
|
|
|
|
|
const beforeSegmentPos = i > 1 ? segments[i - 2].next : startPos
|
|
|
|
|
|
|
|
|
|
let currentPos = i == 0 ? startPos : segments[i - 1].next
|
|
|
|
|
let nextPos = segment.next
|
|
|
|
|
let controlPointPos = segment.controlPoint
|
|
|
|
|
const currentPos = beforeSegment ? beforeSegment.next : startPos
|
|
|
|
|
const nextPos = segment.next
|
|
|
|
|
const segmentCp = segment.controlPoint
|
|
|
|
|
? segment.controlPoint
|
|
|
|
|
: between(currentPos, nextPos)
|
|
|
|
|
|
|
|
|
|
const smoothCp = beforeSegment
|
|
|
|
|
? add(
|
|
|
|
|
currentPos,
|
|
|
|
|
minus(
|
|
|
|
|
currentPos,
|
|
|
|
|
beforeSegment.controlPoint ??
|
|
|
|
|
between(beforeSegmentPos, currentPos),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: segmentCp
|
|
|
|
|
|
|
|
|
|
const result = searchOnSegment(
|
|
|
|
|
currentPos,
|
|
|
|
|
controlPointPos,
|
|
|
|
|
smoothCp,
|
|
|
|
|
segmentCp,
|
|
|
|
|
nextPos,
|
|
|
|
|
clickPosBaseRatio,
|
|
|
|
|
0.05,
|
|
|
|
@ -373,7 +498,9 @@ export default function BendableArrow({
|
|
|
|
|
ref={pathRef}
|
|
|
|
|
stroke={"#000"}
|
|
|
|
|
strokeWidth={styleWidth}
|
|
|
|
|
strokeDasharray={style?.dashArray}
|
|
|
|
|
strokeDasharray={
|
|
|
|
|
style?.dashArray ?? ArrowStyleDefaults.dashArray
|
|
|
|
|
}
|
|
|
|
|
fill="none"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onKeyUp={(e) => {
|
|
|
|
@ -399,7 +526,7 @@ export default function BendableArrow({
|
|
|
|
|
|
|
|
|
|
{!forceStraight &&
|
|
|
|
|
isSelected &&
|
|
|
|
|
computeControlPoints(area.current!.getBoundingClientRect())}
|
|
|
|
|
computePoints(area.current!.getBoundingClientRect())}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
@ -420,32 +547,154 @@ enum PointSegmentSearchResult {
|
|
|
|
|
NOT_FOUND,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FullSegment {
|
|
|
|
|
start: Pos
|
|
|
|
|
controlPoint: Pos | undefined
|
|
|
|
|
end: Pos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* returns a path delimiter that follows a given cubic béziers curve, but with additional waves on it, of the given
|
|
|
|
|
* density and amplitude.
|
|
|
|
|
* @param start
|
|
|
|
|
* @param cp1
|
|
|
|
|
* @param cp2
|
|
|
|
|
* @param end
|
|
|
|
|
* @param wavesPer100px
|
|
|
|
|
* @param amplitude
|
|
|
|
|
*/
|
|
|
|
|
function wavyBezier(
|
|
|
|
|
start: Pos,
|
|
|
|
|
cp1: Pos,
|
|
|
|
|
cp2: Pos,
|
|
|
|
|
end: Pos,
|
|
|
|
|
wavesPer100px: number,
|
|
|
|
|
amplitude: number,
|
|
|
|
|
): string {
|
|
|
|
|
function getVerticalAmplification(t: number): Pos {
|
|
|
|
|
const velocity = cubicBeziersDerivative(start, cp1, cp2, end, t)
|
|
|
|
|
const velocityLength = size(velocity)
|
|
|
|
|
//rotate the velocity by 90 deg
|
|
|
|
|
const projection = { x: velocity.y, y: -velocity.x }
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
x: (projection.x / velocityLength) * amplitude,
|
|
|
|
|
y: (projection.y / velocityLength) * amplitude,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result: string = ""
|
|
|
|
|
|
|
|
|
|
const dist = distance(start, cp1) + distance(cp1, cp2) + distance(cp2, end)
|
|
|
|
|
|
|
|
|
|
// we need two phases in order to complete a wave
|
|
|
|
|
const waveLength = (dist / 100) * wavesPer100px * 2
|
|
|
|
|
const step = 1 / waveLength
|
|
|
|
|
|
|
|
|
|
// 0 : middle to up
|
|
|
|
|
// 1 : up to middle
|
|
|
|
|
// 2 : middle to down
|
|
|
|
|
// 3 : down to middle
|
|
|
|
|
let phase = 0
|
|
|
|
|
|
|
|
|
|
for (let t = step; t <= 1; ) {
|
|
|
|
|
const pos = cubicBeziers(start, cp1, cp2, end, t)
|
|
|
|
|
const amplification = getVerticalAmplification(t)
|
|
|
|
|
|
|
|
|
|
let nextPos
|
|
|
|
|
if (phase == 1 || phase == 3) {
|
|
|
|
|
nextPos = pos
|
|
|
|
|
} else {
|
|
|
|
|
if (phase == 0) {
|
|
|
|
|
nextPos = add(pos, amplification)
|
|
|
|
|
} else {
|
|
|
|
|
nextPos = minus(pos, amplification)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const controlPointBase: Pos = cubicBeziers(
|
|
|
|
|
start,
|
|
|
|
|
cp1,
|
|
|
|
|
cp2,
|
|
|
|
|
end,
|
|
|
|
|
t - step / 2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const controlPoint: Pos =
|
|
|
|
|
phase == 0 || phase == 1
|
|
|
|
|
? add(controlPointBase, amplification)
|
|
|
|
|
: minus(controlPointBase, amplification)
|
|
|
|
|
|
|
|
|
|
result += `Q${controlPoint.x} ${controlPoint.y} ${nextPos.x} ${nextPos.y}`
|
|
|
|
|
phase = (phase + 1) % 4
|
|
|
|
|
t += step
|
|
|
|
|
if (t < 1 && t > 1 - step) t = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cubicBeziersDerivative(
|
|
|
|
|
start: Pos,
|
|
|
|
|
cp1: Pos,
|
|
|
|
|
cp2: Pos,
|
|
|
|
|
end: Pos,
|
|
|
|
|
t: number,
|
|
|
|
|
): Pos {
|
|
|
|
|
return add(
|
|
|
|
|
add(
|
|
|
|
|
mul(minus(cp1, start), 3 * (1 - t) ** 2),
|
|
|
|
|
mul(minus(cp2, cp1), 6 * (1 - t) * t),
|
|
|
|
|
),
|
|
|
|
|
mul(minus(end, cp2), 3 * t ** 2),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cubicBeziers(
|
|
|
|
|
start: Pos,
|
|
|
|
|
cp1: Pos,
|
|
|
|
|
cp2: Pos,
|
|
|
|
|
end: Pos,
|
|
|
|
|
t: number,
|
|
|
|
|
): Pos {
|
|
|
|
|
return add(
|
|
|
|
|
add(
|
|
|
|
|
add(mul(start, (1 - t) ** 3), mul(cp1, 3 * t * (1 - t) ** 2)),
|
|
|
|
|
mul(cp2, 3 * t ** 2 * (1 - t)),
|
|
|
|
|
),
|
|
|
|
|
mul(end, t ** 3),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a quadratic bézier curve (start position, end position and a middle control point position)
|
|
|
|
|
* search if the given `point` lies on the curve, within a minimum acceptance distance.
|
|
|
|
|
* @param start
|
|
|
|
|
* @param cp1
|
|
|
|
|
* @param cp2
|
|
|
|
|
* @param end
|
|
|
|
|
* @param point
|
|
|
|
|
* @param minDistance
|
|
|
|
|
*/
|
|
|
|
|
function searchOnSegment(
|
|
|
|
|
startPos: Pos,
|
|
|
|
|
controlPoint: Pos,
|
|
|
|
|
endPos: Pos,
|
|
|
|
|
start: Pos,
|
|
|
|
|
cp1: Pos,
|
|
|
|
|
cp2: Pos,
|
|
|
|
|
end: 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)
|
|
|
|
|
const dist = distance(start, cp1) + distance(cp1, cp2) + distance(cp2, end)
|
|
|
|
|
const step = 1 / (dist / minDistance)
|
|
|
|
|
|
|
|
|
|
function getDistanceAt(t: number): number {
|
|
|
|
|
// apply the bezier function
|
|
|
|
|
const pos = add(
|
|
|
|
|
add(controlPoint, mul(p0MinusP1, (1 - t) ** 2)),
|
|
|
|
|
mul(p2MinusP1, t ** 2),
|
|
|
|
|
)
|
|
|
|
|
return distance(pos, point)
|
|
|
|
|
return distance(cubicBeziers(start, cp1, cp2, end, t), point)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let t = 0; t < 1; t += step) {
|
|
|
|
|
if (getDistanceAt(t) <= minDistance)
|
|
|
|
|
const distance = getDistanceAt(t)
|
|
|
|
|
|
|
|
|
|
if (distance <= minDistance)
|
|
|
|
|
return t >= 0.5
|
|
|
|
|
? PointSegmentSearchResult.RIGHT_TO_CONTROL_POINT
|
|
|
|
|
: PointSegmentSearchResult.LEFT_TO_CONTROL_POINT
|
|
|
|
@ -454,14 +703,17 @@ function searchOnSegment(
|
|
|
|
|
return PointSegmentSearchResult.NOT_FOUND
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let t = 0
|
|
|
|
|
let slice = 0.5
|
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
|
|
|
t += slice
|
|
|
|
|
slice /= 2
|
|
|
|
|
// console.log(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An arrow point, that can be moved.
|
|
|
|
|
* @param className
|
|
|
|
|
* @param posRatio
|
|
|
|
|
* @param parentBase
|
|
|
|
|
* @param onMoves
|
|
|
|
|
* @param onPosValidated
|
|
|
|
|
* @param onRemove
|
|
|
|
|
* @param radius
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
|
|
|
|
function ArrowPoint({
|
|
|
|
|
className,
|
|
|
|
|
posRatio,
|
|
|
|
|