From 9c035e2be2e317a62326ac0870a26f1d4977462b Mon Sep 17 00:00:00 2001 From: findthelorax Date: Mon, 20 Apr 2026 23:23:50 -0400 Subject: [PATCH] adding in path geometry for the add point + --- .../edges/__tests__/waypointUtils.test.ts | 15 ++++++- .../src/components/canvas/edges/index.tsx | 8 ++-- .../components/canvas/edges/waypointUtils.ts | 44 +++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/canvas/edges/__tests__/waypointUtils.test.ts b/frontend/src/components/canvas/edges/__tests__/waypointUtils.test.ts index a9dadf5..ba9c253 100644 --- a/frontend/src/components/canvas/edges/__tests__/waypointUtils.test.ts +++ b/frontend/src/components/canvas/edges/__tests__/waypointUtils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { buildWaypointPath, distToSegment, findInsertIndex, getWaypointLabelPosition, snap45, snap45both } from '../waypointUtils' +import { buildWaypointPath, distToSegment, findInsertIndex, getAddWaypointHandlePosition, getWaypointLabelPosition, snap45, snap45both } from '../waypointUtils' describe('buildWaypointPath — bezier (default)', () => { it('builds a catmull-rom curve with no waypoints (start = end clamp)', () => { @@ -192,3 +192,16 @@ describe('getWaypointLabelPosition', () => { expect(point).toEqual({ x: 10, y: 20 }) }) }) + +describe('getAddWaypointHandlePosition', () => { + it('places bezier add handle on the rendered curved segment', () => { + const point = getAddWaypointHandlePosition(0, 0, [{ x: 50, y: 100 }], 100, 0, 0, 'bezier') + expect(point.x).toBeCloseTo(21.875, 3) + expect(point.y).toBeCloseTo(56.25, 3) + }) + + it('keeps smooth add handle at straight segment midpoint', () => { + const point = getAddWaypointHandlePosition(0, 0, [{ x: 50, y: 0 }, { x: 50, y: 100 }], 100, 100, 1, 'smooth') + expect(point).toEqual({ x: 50, y: 50 }) + }) +}) diff --git a/frontend/src/components/canvas/edges/index.tsx b/frontend/src/components/canvas/edges/index.tsx index 9ed771f..3a6c24d 100644 --- a/frontend/src/components/canvas/edges/index.tsx +++ b/frontend/src/components/canvas/edges/index.tsx @@ -13,7 +13,7 @@ import type { EdgeData, EdgeType, Waypoint } from '@/types' import { useThemeStore } from '@/stores/themeStore' import { useCanvasStore } from '@/stores/canvasStore' import { THEMES } from '@/utils/themes' -import { buildWaypointPath, getWaypointLabelPosition, snap45, snap45both } from './waypointUtils' +import { buildWaypointPath, getAddWaypointHandlePosition, getWaypointLabelPosition, snap45, snap45both } from './waypointUtils' const VLAN_COLORS = ['#00d4ff', '#a855f7', '#39d353', '#ff6e00', '#e3b341', '#f85149'] @@ -161,9 +161,9 @@ function segmentMidpoints( const isSmooth = pathStyle === 'smooth' return pts.slice(0, -1).map((a, i) => { - const b = pts[i + 1] - let mx = (a.x + b.x) / 2 - const my = (a.y + b.y) / 2 + const base = getAddWaypointHandlePosition(sourceX, sourceY, waypoints, targetX, targetY, i, pathStyle) + let mx = base.x + const my = base.y // For smooth style with no existing waypoints, bias the single + handle onto // the source handle axis so clicking it creates a perpendicular exit. diff --git a/frontend/src/components/canvas/edges/waypointUtils.ts b/frontend/src/components/canvas/edges/waypointUtils.ts index fa78c77..5b2107f 100644 --- a/frontend/src/components/canvas/edges/waypointUtils.ts +++ b/frontend/src/components/canvas/edges/waypointUtils.ts @@ -238,6 +238,50 @@ export function getWaypointLabelPosition( return lastSegment.pointAt(1) } +function getBezierSegmentPoint( + pts: Waypoint[], + insertIndex: number, + t: number, +): Waypoint { + const i = Math.max(0, Math.min(insertIndex, pts.length - 2)) + const p0 = pts[Math.max(i - 1, 0)] + const p1 = pts[i] + const p2 = pts[i + 1] + const p3 = pts[Math.min(i + 2, pts.length - 1)] + const cp1 = { + x: p1.x + (p2.x - p0.x) / 6, + y: p1.y + (p2.y - p0.y) / 6, + } + const cp2 = { + x: p2.x - (p3.x - p1.x) / 6, + y: p2.y - (p3.y - p1.y) / 6, + } + + return interpolateCubic(p1, cp1, cp2, p2, t) +} + +export function getAddWaypointHandlePosition( + sourceX: number, sourceY: number, + waypoints: Waypoint[], + targetX: number, targetY: number, + insertIndex: number, + pathStyle: string = 'bezier', +): Waypoint { + const pts = [{ x: sourceX, y: sourceY }, ...waypoints, { x: targetX, y: targetY }] + + if (pts.length < 2) return { x: sourceX, y: sourceY } + + if (pathStyle !== 'smooth') { + return getBezierSegmentPoint(pts, insertIndex, 0.5) + } + + const i = Math.max(0, Math.min(insertIndex, pts.length - 2)) + return { + x: (pts[i].x + pts[i + 1].x) / 2, + y: (pts[i].y + pts[i + 1].y) / 2, + } +} + // ── 45° snapping ────────────────────────────────────────────────────────────── /**