feat: add interactive edge waypoints with smooth path editing
- Drag waypoints to reshape edges; double-click a waypoint to remove it - + handles at segment midpoints to insert new waypoints - Bezier style: catmull-rom smooth curves through waypoints - Smooth style: rounded-corner polyline with soft 45° snap (snaps within 15px) - First + handle biased to source axis for perpendicular node exit - snap45both: ray-intersection solver ensures both adjacent segments snap to 45° simultaneously - Clear path button in EdgeModal when waypoints exist - Waypoints serialised/deserialised with canvas state
This commit is contained in:
@@ -236,6 +236,36 @@ describe('serializeEdge', () => {
|
||||
expect(result.custom_color).toBeNull()
|
||||
expect(result.path_style).toBeNull()
|
||||
})
|
||||
|
||||
it('serializes waypoints when present', () => {
|
||||
const edge = makeRfEdge({ data: { type: 'ethernet', waypoints: [{ x: 10, y: 20 }, { x: 30, y: 40 }] } })
|
||||
const result = serializeEdge(edge)
|
||||
expect(result.waypoints).toEqual([{ x: 10, y: 20 }, { x: 30, y: 40 }])
|
||||
})
|
||||
|
||||
it('serializes waypoints as null when empty array', () => {
|
||||
const edge = makeRfEdge({ data: { type: 'ethernet', waypoints: [] } })
|
||||
const result = serializeEdge(edge)
|
||||
expect(result.waypoints).toBeNull()
|
||||
})
|
||||
|
||||
it('serializes waypoints as null when absent', () => {
|
||||
const result = serializeEdge(makeRfEdge())
|
||||
expect(result.waypoints).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deserializeApiEdge — waypoints', () => {
|
||||
it('restores waypoints from API edge', () => {
|
||||
const edge = makeApiEdge({ waypoints: [{ x: 5, y: 15 }, { x: 25, y: 35 }] })
|
||||
const result = deserializeApiEdge(edge)
|
||||
expect((result.data as { waypoints: unknown }).waypoints).toEqual([{ x: 5, y: 15 }, { x: 25, y: 35 }])
|
||||
})
|
||||
|
||||
it('has no waypoints when API edge has none', () => {
|
||||
const result = deserializeApiEdge(makeApiEdge())
|
||||
expect((result.data as { waypoints?: unknown }).waypoints).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ── deserializeApiNode — regular nodes ───────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
import type { NodeData, EdgeData } from '@/types'
|
||||
import type { NodeData, EdgeData, Waypoint } from '@/types'
|
||||
import { normalizeHandle } from '@/utils/handleUtils'
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
@@ -46,6 +46,7 @@ export interface ApiEdge {
|
||||
animated?: boolean | 'snake' | 'flow' | 'none'
|
||||
source_handle?: string | null
|
||||
target_handle?: string | null
|
||||
waypoints?: Waypoint[] | null
|
||||
}
|
||||
|
||||
// ── Serialization (RF node → API save payload) ───────────────────────────────
|
||||
@@ -121,6 +122,7 @@ export function serializeEdge(e: Edge<EdgeData>): Record<string, unknown> {
|
||||
animated: e.data?.animated ?? false,
|
||||
source_handle: normalizeHandle(e.sourceHandle),
|
||||
target_handle: normalizeHandle(e.targetHandle),
|
||||
waypoints: e.data?.waypoints?.length ? e.data.waypoints : null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user