From e96a3bfb99eabbe1d7e13eca8899c00f4259fac7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 14:53:40 +0000 Subject: [PATCH] Fix port spacing and improve manual wire alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port dots now use exact fractional positions (rounded to nearest px) instead of snapping each port independently to the 8-px routing grid. On components with many ports (PDH: 14 left / 10 right), the old per-port STEP-snap produced alternating 32 / 40 px gaps; the new code gives uniform ~26–36 px spacing. Manual waypoint axis-lock now includes all port positions in the diagram (not just the current wire's own endpoints), so dragging a waypoint within ALIGN_TOL of any component port's x or y axis snaps onto it cleanly — no manual pixel hunting needed to get a straight horizontal or vertical run into a port. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01G5gLiKEHhUgk7ZZ2TPdRze --- src/components/canvas/WireEdge.jsx | 13 ++++++++++++- src/utils/geometry.js | 11 +++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/canvas/WireEdge.jsx b/src/components/canvas/WireEdge.jsx index 6399730..5fd7f45 100644 --- a/src/components/canvas/WireEdge.jsx +++ b/src/components/canvas/WireEdge.jsx @@ -8,6 +8,8 @@ import { useRouting } from './RoutingContext'; import { useDrcMarks } from './DrcContext'; import useDiagramStore from '../../store/diagramStore'; import { buildSvgPath, STEP } from '../../utils/routingUtils'; +import { portPosition } from '../../utils/geometry'; +import { getDefinition } from '../../data/componentLibrary'; const MIN_SEG = 26; // only show an insert dot on segments at least this long (px) const DRC_HALO = { error: '#f87171', warning: '#fbbf24', info: '#38bdf8' }; @@ -73,7 +75,16 @@ function WireEdge({ id, selected }) { ev.preventDefault(); dragRef.current = { idx, pts: base }; setDraft(base); - const others = base.filter((_, k) => k !== idx); + // Axis-lock candidates: this wire's other control points + every port in the + // diagram. Collected once at drag-start (getState, non-reactive) so the per- + // frame move handler can run without subscribing to the full node list. + const { nodes: allNodes, customDefinitions: allDefs } = useDiagramStore.getState(); + const allPortPts = allNodes.flatMap((n) => { + const d = getDefinition(n.data.definitionId, allDefs); + if (!d) return []; + return (n.data.ports ?? []).map((p) => portPosition(n, p, d)); + }); + const others = [...base.filter((_, k) => k !== idx), ...allPortPts]; const onMove = (e) => { // Work from the RAW cursor position so axis-locking onto a port/waypoint // wins over the grid: with a capture radius wider than the grid step, a diff --git a/src/utils/geometry.js b/src/utils/geometry.js index e987cc8..81fe244 100644 --- a/src/utils/geometry.js +++ b/src/utils/geometry.js @@ -1,6 +1,5 @@ // Shared geometry for port placement — used by ComponentNode (handle CSS %) // and routingUtils (absolute flow coordinates). Both MUST agree. -import { STEP } from './routingUtils'; export function portsOnSide(ports, side) { return ports.filter((p) => p.side === side).sort((a, b) => a.order - b.order); @@ -14,13 +13,13 @@ export function portFraction(ports, port) { return (idx + 1) / (n + 1); } -// Offset in px from the edge's start for a port, snapped to the routing lattice -// so wires leave the port aligned to the grid (no off-axis jog). `edgeLen` is -// the side length (height for left/right, width for top/bottom). +// Offset in px from the edge's start for a port. Uses the exact fractional +// position (rounded to nearest pixel) so ports are always evenly spaced +// regardless of the routing grid step. The routing engine handles sub-grid +// alignment via the exit stub, so no grid-snap is needed here. export function portOffsetPx(ports, port, edgeLen) { const raw = edgeLen * portFraction(ports, port); - const snapped = Math.round(raw / STEP) * STEP; - return Math.min(edgeLen - 1, Math.max(1, snapped)); + return Math.min(edgeLen - 1, Math.max(1, Math.round(raw))); } // Snapped fraction (0..1) used for the visual handle/label so the dot sits