Documents
Home>Documents>Dev>Frontend

Canvas Editor: Edge Snapping and Bezier Path Rendering

6 min readJun 12, 2025Feb 22, 2026

The Challenge of Edge Connections

In an AI workflow editor, edges (connections between nodes) are a core interaction. To let users connect nodes intuitively, I implemented port snapping, Bezier path rendering, and type validation.

Bezier Path Calculation

flowchart LR
    A["출발점 (x1,y1)"] --> B["제어점1 (cx1,cy1)"]
    B --> C["제어점2 (cx2,cy2)"]
    C --> D["도착점 (x2,y2)"]
function calcBezierPath(
  source: Point, target: Point
): string {
  const dx = Math.abs(target.x - source.x);
  const controlOffset = Math.max(50, dx * 0.4);

  return `M ${source.x} ${source.y}
          C ${source.x + controlOffset} ${source.y},
            ${target.x - controlOffset} ${target.y},
            ${target.x} ${target.y}`;
}

Port Position Calculation and Zoom Level

Without accounting for the zoom level, port positions will be misaligned from their actual locations:

// 6/12 커밋: 줌 레벨을 고려한 포트 좌표 계산
function getPortWorldPosition(portElement: HTMLElement, zoom: number) {
  const rect = portElement.getBoundingClientRect();
  const canvasRect = canvasRef.current.getBoundingClientRect();

  return {
    x: (rect.left - canvasRect.left + rect.width / 2) / zoom,
    y: (rect.top - canvasRect.top + rect.height / 2) / zoom
  };
}

Snap Algorithm

flowchart TD
    A[마우스 이동] --> B[가까운 포트 탐색]
    B --> C{거리 < 임계값?}
    C -->|Yes| D{타입 호환?}
    C -->|No| E[일반 미리보기]
    D -->|호환| F[스냅 미리보기]
    D -->|비호환| G[스냅 해제 + 미리보기 초기화]
    F --> H[마우스 업 시 연결 확정]

Preventing Duplicate Edges

A bug that allowed duplicate edges on the same port pair was fixed on 6/13:

function canAddEdge(source: string, target: string): boolean {
  return !edges.some(
    e => e.source === source && e.target === target
  );
}

Edge Re-wiring Logic

Functionality to detach an existing edge and reconnect it to a different port (6/13):

function handlePortMouseDown(portId: string) {
  // 이미 연결된 엣지가 있으면 분리
  const existingEdge = edges.find(e => e.target === portId);
  if (existingEdge) {
    removeEdge(existingEdge.id);
    startEdgeFrom(existingEdge.source);
  } else {
    startEdgeFrom(portId);
  }
}

These features were implemented across a total of 12 commits (6/11–6/18).

Tags
CanvasBezierEdgeSnappingInteractionTypeScript