diff --git a/frontend/canvas-helpers.js b/frontend/canvas-helpers.js
index 93fa7fd..d06f606 100644
--- a/frontend/canvas-helpers.js
+++ b/frontend/canvas-helpers.js
@@ -1282,6 +1282,22 @@ bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
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;
@@ -1324,6 +1340,7 @@ bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
createComponentSymbolMetrics,
transformPortInfo,
getNodePortCanvasPoint,
+ getRotatableNodeHandleDirection,
buildPortHandles,
buildElementPorts,
buildElementPinEntries,
diff --git a/frontend/canvas.html b/frontend/canvas.html
index 2e83cf9..67fdcae 100644
--- a/frontend/canvas.html
+++ b/frontend/canvas.html
@@ -1566,6 +1566,7 @@ Organization : OptiHK Limited
calculateLayoutBounds,
calculateCompositeBoxSize,
buildPortHandles,
+ getRotatableNodeHandleDirection,
buildElementPorts,
getElementPinName,
buildElementBoxSize,
@@ -1710,8 +1711,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]);
@@ -1731,6 +1734,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(
() => {
@@ -1856,24 +1869,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}
@@ -4127,8 +4144,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
@@ -4151,7 +4170,7 @@ Organization : OptiHK Limited
};
});
return [...separatedEdges, ...rulerEdges];
- }, [currentEdges, currentNodes, getAnchorHandleRouteDirection, rulerEdges]);
+ }, [currentEdges, currentNodes, getAnchorHandleRouteDirection, getRotatableNodeHandleDirection, rulerEdges]);
const [projectCompositeMap, setProjectCompositeMap] = useState({});
const [standaloneComposites, setStandaloneComposites] = useState([]);
@@ -6130,6 +6149,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,
@@ -6139,9 +6159,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;
@@ -6155,7 +6174,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) => {