diff --git a/frontend/canvas-helpers.js b/frontend/canvas-helpers.js index 16cdde1..ee14d3e 100644 --- a/frontend/canvas-helpers.js +++ b/frontend/canvas-helpers.js @@ -1237,6 +1237,22 @@ ${linksYaml}`; return null; }; + const getRotatableNodeHandleDirection = (node, handleId) => { + if (!node || !handleId) return null; + if (node.type !== 'rotatableNode' && !(!node.data?.elementType && node.data?.componentName)) return null; + const ports = node.data && node.data.ports; + if (!ports || !ports[handleId]) return null; + const boxSize = normalizeBoxSize({ box_size: node.data && node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE); + const handles = buildPortHandles(ports, { + rotation: Number((node.data && node.data.rotation) || 0), + flip: Boolean(node.data && node.data.flip), + flop: Boolean(node.data && node.data.flop), + boxSize + }); + const found = handles.find(handle => handle.name === handleId); + return found ? found.position : null; + }; + // Backward-compatible alias for same-type route crossing validation. const findSameFamilyRouteCrossing = findSameTypeRouteCrossing; @@ -1276,6 +1292,7 @@ ${linksYaml}`; createComponentSymbolMetrics, transformPortInfo, getNodePortCanvasPoint, + getRotatableNodeHandleDirection, buildPortHandles, buildElementPorts, buildElementPinEntries, diff --git a/frontend/canvas.html b/frontend/canvas.html index 9135751..3e48201 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -1566,6 +1566,7 @@ Organization : OptiHK Limited calculateLayoutBounds, calculateCompositeBoxSize, buildPortHandles, + getRotatableNodeHandleDirection, buildElementPorts, getElementPinName, buildElementBoxSize, @@ -1698,8 +1699,10 @@ Organization : OptiHK Limited useEffect(() => { const transformKey = `${data.rotation || 0}:${data.flip ? 1 : 0}:${data.flop ? 1 : 0}`; if (prevTransformRef.current !== transformKey) { - updateNodeInternalsRef.current(id); prevTransformRef.current = transformKey; + requestAnimationFrame(() => { + updateNodeInternalsRef.current(id); + }); } }, [data.rotation, data.flip, data.flop, id]); @@ -1719,6 +1722,16 @@ Organization : OptiHK Limited top: Position.Top, bottom: Position.Bottom }; + const rotateHandleDirection = (dir, rot) => { + const norm = ((rot % 360) + 360) % 360; + const map = { + 0: { right: 'right', left: 'left', top: 'top', bottom: 'bottom' }, + 90: { right: 'bottom', left: 'top', top: 'left', bottom: 'right' }, + 180: { right: 'left', left: 'right', top: 'bottom', bottom: 'top' }, + 270: { right: 'top', left: 'bottom', top: 'right', bottom: 'left' } + }; + return (map[norm] || map[0])[dir] || dir; + }; const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE); const flippedPorts = useMemo( () => { @@ -1844,24 +1857,28 @@ Organization : OptiHK Limited transformOrigin: 'center center', pointerEvents: 'none' }}> - {portHandles.map((portHandle) => ( + {portHandles.map((portHandle) => { + const originalDir = portDirectionMap.get(portHandle.name) || portHandle.position; + const effectiveDir = rotateHandleDirection(originalDir, data.rotation || 0); + return ( - ))} + ); + })} {portHandles.map((portHandle) => ( {portHandle.name} @@ -4007,8 +4024,10 @@ Organization : OptiHK Limited const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`; const key = [sourceEndpoint, targetEndpoint].sort().join('<>'); const group = groups.get(key) || []; - const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle); - const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle); + const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle) + || getRotatableNodeHandleDirection(nodeMap[edge.source], edge.sourceHandle); + const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle) + || getRotatableNodeHandleDirection(nodeMap[edge.target], edge.targetHandle); const usesAnchorDirection = Boolean(sourceDirection || targetDirection); const hasRoutePoints = edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2; const directionalEdge = usesAnchorDirection @@ -4031,7 +4050,7 @@ Organization : OptiHK Limited }; }); return [...separatedEdges, ...rulerEdges]; - }, [currentEdges, currentNodes, getAnchorHandleRouteDirection, rulerEdges]); + }, [currentEdges, currentNodes, getAnchorHandleRouteDirection, getRotatableNodeHandleDirection, rulerEdges]); const [projectCompositeMap, setProjectCompositeMap] = useState({}); const [standaloneComposites, setStandaloneComposites] = useState([]); @@ -6018,6 +6037,7 @@ Organization : OptiHK Limited const route = currentLinkRoute; const view = routeStyleForSettings(route, false); const edgeId = `edge-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}-${Date.now()}`; + const nodeMap = Object.fromEntries(activePage.nodes.map(node => [node.id, node])); const candidate = { id: edgeId, source: connection.source, @@ -6027,9 +6047,8 @@ Organization : OptiHK Limited type: view.type, selectable: true, style: view.style, - data: { route } + data: { route }, }; - const nodeMap = Object.fromEntries(activePage.nodes.map(node => [node.id, node])); const conflict = findSameTypeRouteCrossing(candidate, activePage.edges, nodeMap, technologyManifest); if (conflict) { const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source; @@ -6043,7 +6062,7 @@ Organization : OptiHK Limited : p ))); addLog(`Connected ${connection.sourceHandle} to ${connection.targetHandle}.`); - }, [activePageId, activePage, rulerMode, currentLinkRoute, technologyManifest, addLog]); + }, [activePageId, activePage, rulerMode, currentLinkRoute, technologyManifest, addLog, getAnchorHandleRouteDirection]); // Select custom route edges from their SVG hit target. const handleRouteEdgeMouseDown = useCallback((event) => {