feat: drag relation tags along threads
This commit is contained in:
+59
-4
@@ -2,16 +2,71 @@ import type { CaseState, Evidence, TimelineRange, Viewport, WidgetRelation } fro
|
||||
|
||||
export interface BoardPoint { x: number; y: number }
|
||||
|
||||
export function threadCurve(from: BoardPoint, to: BoardPoint, tightness = 65) {
|
||||
function threadControls(from: BoardPoint, to: BoardPoint, tightness = 65) {
|
||||
const tautness = Math.max(0, Math.min(100, Number(tightness) || 0)) / 100
|
||||
const distance = Math.hypot(to.x - from.x, to.y - from.y)
|
||||
const sag = (1 - tautness) * Math.min(190, Math.max(45, distance * .28))
|
||||
const c1 = { x: from.x + (to.x - from.x) / 3, y: from.y + (to.y - from.y) / 3 + sag }
|
||||
const c2 = { x: from.x + (to.x - from.x) * 2 / 3, y: from.y + (to.y - from.y) * 2 / 3 + sag }
|
||||
const midpoint = {
|
||||
x: (from.x + 3 * c1.x + 3 * c2.x + to.x) / 8,
|
||||
y: (from.y + 3 * c1.y + 3 * c2.y + to.y) / 8,
|
||||
return { c1, c2 }
|
||||
}
|
||||
|
||||
function cubicPoint(from: BoardPoint, c1: BoardPoint, c2: BoardPoint, to: BoardPoint, t: number) {
|
||||
const inverse = 1 - t
|
||||
return {
|
||||
x: inverse ** 3 * from.x + 3 * inverse ** 2 * t * c1.x + 3 * inverse * t ** 2 * c2.x + t ** 3 * to.x,
|
||||
y: inverse ** 3 * from.y + 3 * inverse ** 2 * t * c1.y + 3 * inverse * t ** 2 * c2.y + t ** 3 * to.y,
|
||||
}
|
||||
}
|
||||
|
||||
function cubicTangent(from: BoardPoint, c1: BoardPoint, c2: BoardPoint, to: BoardPoint, t: number) {
|
||||
const inverse = 1 - t
|
||||
return {
|
||||
x: 3 * inverse ** 2 * (c1.x - from.x) + 6 * inverse * t * (c2.x - c1.x) + 3 * t ** 2 * (to.x - c2.x),
|
||||
y: 3 * inverse ** 2 * (c1.y - from.y) + 6 * inverse * t * (c2.y - c1.y) + 3 * t ** 2 * (to.y - c2.y),
|
||||
}
|
||||
}
|
||||
|
||||
export function threadTagLateralLimit(tightness = 65) {
|
||||
const normalized = Math.max(0, Math.min(100, Number(tightness) || 0))
|
||||
return Math.round(10 + (100 - normalized) * .6)
|
||||
}
|
||||
|
||||
export function threadTagPlacement(from: BoardPoint, to: BoardPoint, tightness = 65, positionPercent = 50, lateralOffset = 0) {
|
||||
const { c1, c2 } = threadControls(from, to, tightness)
|
||||
const position = Math.max(5, Math.min(95, Number(positionPercent) || 50))
|
||||
const t = position / 100
|
||||
const point = cubicPoint(from, c1, c2, to, t)
|
||||
const tangent = cubicTangent(from, c1, c2, to, t)
|
||||
const length = Math.hypot(tangent.x, tangent.y) || 1
|
||||
const normal = { x: -tangent.y / length, y: tangent.x / length }
|
||||
const maxLateralOffset = threadTagLateralLimit(tightness)
|
||||
const offset = Math.max(-maxLateralOffset, Math.min(maxLateralOffset, Number(lateralOffset) || 0))
|
||||
return { x: point.x + normal.x * offset, y: point.y + normal.y * offset, positionPercent: position, lateralOffset: offset, maxLateralOffset }
|
||||
}
|
||||
|
||||
export function projectThreadTag(from: BoardPoint, to: BoardPoint, tightness: number, pointer: BoardPoint) {
|
||||
const { c1, c2 } = threadControls(from, to, tightness)
|
||||
let bestT = .5
|
||||
let bestDistance = Number.POSITIVE_INFINITY
|
||||
for (let index = 5; index <= 95; index += 1) {
|
||||
const t = index / 100
|
||||
const point = cubicPoint(from, c1, c2, to, t)
|
||||
const distance = (pointer.x - point.x) ** 2 + (pointer.y - point.y) ** 2
|
||||
if (distance < bestDistance) { bestDistance = distance; bestT = t }
|
||||
}
|
||||
const point = cubicPoint(from, c1, c2, to, bestT)
|
||||
const tangent = cubicTangent(from, c1, c2, to, bestT)
|
||||
const length = Math.hypot(tangent.x, tangent.y) || 1
|
||||
const normal = { x: -tangent.y / length, y: tangent.x / length }
|
||||
const maxLateralOffset = threadTagLateralLimit(tightness)
|
||||
const lateralOffset = Math.max(-maxLateralOffset, Math.min(maxLateralOffset, (pointer.x - point.x) * normal.x + (pointer.y - point.y) * normal.y))
|
||||
return { positionPercent: Math.round(bestT * 100), lateralOffset: Math.round(lateralOffset), maxLateralOffset }
|
||||
}
|
||||
|
||||
export function threadCurve(from: BoardPoint, to: BoardPoint, tightness = 65) {
|
||||
const { c1, c2 } = threadControls(from, to, tightness)
|
||||
const midpoint = cubicPoint(from, c1, c2, to, .5)
|
||||
return { path: `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`, midpoint }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user