diff --git a/src/components/DimensionOverlay.tsx b/src/components/DimensionOverlay.tsx index b0ab82eb..5aab7100 100644 --- a/src/components/DimensionOverlay.tsx +++ b/src/components/DimensionOverlay.tsx @@ -22,6 +22,48 @@ interface Props { const SNAP_THRESHOLD_PX = 16 const SNAP_MARKER_SIZE = 5 +const SNAP_SEARCH_RADIUS_PX = 220 + +const isCursorNearBoundingBoxInPx = ( + point: { x: number; y: number } | null, + bounds: BoundingBox, + transform: Matrix, + radiusPx: number, +) => { + if (!point) return true + + const cursorInScreen = applyToPoint(transform, point) + const boundsTopLeftInScreen = applyToPoint(transform, { + x: bounds.minX, + y: bounds.minY, + }) + const boundsBottomRightInScreen = applyToPoint(transform, { + x: bounds.maxX, + y: bounds.maxY, + }) + + const minX = Math.min(boundsTopLeftInScreen.x, boundsBottomRightInScreen.x) + const maxX = Math.max(boundsTopLeftInScreen.x, boundsBottomRightInScreen.x) + const minY = Math.min(boundsTopLeftInScreen.y, boundsBottomRightInScreen.y) + const maxY = Math.max(boundsTopLeftInScreen.y, boundsBottomRightInScreen.y) + + const closestX = Math.max(minX, Math.min(cursorInScreen.x, maxX)) + const closestY = Math.max(minY, Math.min(cursorInScreen.y, maxY)) + + const distancePx = Math.hypot( + cursorInScreen.x - closestX, + cursorInScreen.y - closestY, + ) + + if (distancePx <= radiusPx) return true + + return ( + cursorInScreen.x >= minX - radiusPx && + cursorInScreen.x <= maxX + radiusPx && + cursorInScreen.y >= minY - radiusPx && + cursorInScreen.y <= maxY + radiusPx + ) +} const shouldExcludePrimitiveFromSnapping = (primitive: Primitive) => { if (primitive.pcb_drawing_type === "text") return true @@ -76,61 +118,64 @@ export const DimensionOverlay = ({ const [dStart, setDStart] = useState({ x: 0, y: 0 }) // End of dimension tool line in real-world coordinates (not screen) const [dEnd, setDEnd] = useState({ x: 0, y: 0 }) + const [cursorRwPoint, setCursorRwPoint] = useState<{ + x: number + y: number + } | null>(null) const mousePosRef = useRef({ x: 0, y: 0 }) const containerRef = useRef(null) const container = containerRef.current! const containerBounds = container?.getBoundingClientRect() - const elementBoundingBoxes = useMemo(() => { - const boundingBoxes = new Map() + const elementSnapData = useMemo(() => { + const data = new Map< + object, + { + bounds?: BoundingBox + points: { + anchor: NinePointAnchor | string + point: { x: number; y: number } + element: object + }[] + } + >() for (const primitive of primitives) { if (!primitive._element) continue if (shouldExcludePrimitiveFromSnapping(primitive)) continue - if (primitive.pcb_drawing_type === "pill") continue - if ( - primitive.pcb_drawing_type === "rect" && - primitive.ccw_rotation && - primitive.ccw_rotation !== 0 - ) - continue - const bbox = getPrimitiveBoundingBox(primitive) - if (!bbox) continue - - const existing = boundingBoxes.get(primitive._element as object) - boundingBoxes.set( - primitive._element as object, - mergeBoundingBoxes(existing ?? undefined, bbox), - ) - } - - return boundingBoxes - }, [primitives]) - - const primitiveSnappingPoints = useMemo(() => { - const snapPoints: { - anchor: NinePointAnchor | string - point: { x: number; y: number } - element: object - }[] = [] - for (const primitive of primitives) { - if (!primitive._element) continue - if (shouldExcludePrimitiveFromSnapping(primitive)) continue + const element = primitive._element as object + const existing = data.get(element) + const entry = existing ?? { bounds: undefined, points: [] } + + if (primitive.pcb_drawing_type !== "pill") { + if ( + !( + primitive.pcb_drawing_type === "rect" && + primitive.ccw_rotation && + primitive.ccw_rotation !== 0 + ) + ) { + const bbox = getPrimitiveBoundingBox(primitive) + if (bbox) { + entry.bounds = mergeBoundingBoxes(entry.bounds, bbox) + } + } + } const primitivePoints = getPrimitiveSnapPoints(primitive) - if (primitivePoints.length === 0) continue - for (const snap of primitivePoints) { - snapPoints.push({ + entry.points.push({ anchor: snap.anchor, point: snap.point, - element: primitive._element as object, + element, }) } + + data.set(element, entry) } - return snapPoints + return data }, [primitives]) const snappingPoints = useMemo(() => { @@ -140,8 +185,19 @@ export const DimensionOverlay = ({ element: object | null }[] = [] - elementBoundingBoxes.forEach((bounds, element) => { + elementSnapData.forEach((entry, element) => { + const bounds = entry.bounds if (!bounds) return + if ( + !isCursorNearBoundingBoxInPx( + cursorRwPoint, + bounds, + transform!, + SNAP_SEARCH_RADIUS_PX, + ) + ) { + return + } const centerX = (bounds.minX + bounds.maxX) / 2 const centerY = (bounds.minY + bounds.maxY) / 2 @@ -168,12 +224,9 @@ export const DimensionOverlay = ({ element, }) } + points.push(...entry.points) }) - for (const snap of primitiveSnappingPoints) { - points.push(snap) - } - points.push({ anchor: "origin", point: { x: 0, y: 0 }, @@ -181,7 +234,7 @@ export const DimensionOverlay = ({ }) return points - }, [elementBoundingBoxes, primitiveSnappingPoints]) + }, [cursorRwPoint, elementSnapData, transform]) const snappingPointsWithScreen = useMemo(() => { return snappingPoints.map((snap, index) => ({ @@ -340,6 +393,7 @@ export const DimensionOverlay = ({ const rwPoint = applyToPoint(inverse(transform!), { x, y }) mousePosRef.current.x = rwPoint.x mousePosRef.current.y = rwPoint.y + setCursorRwPoint({ x: rwPoint.x, y: rwPoint.y }) if (dimensionToolStretching) { const snap = findSnap(rwPoint)