2 Commits

Author SHA1 Message Date
xsxx03-art 2846899097 rotation bug fixed 2026-06-08 18:54:42 +08:00
xsxx03-art 2ddd30e7bb update canvas.html 2026-06-05 20:32:16 +08:00
2 changed files with 94 additions and 25 deletions
+17
View File
@@ -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,
+77 -25
View File
@@ -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,9 +1722,51 @@ 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(
() => {
const result = {};
const ports = Object.entries(data.ports || {}).filter(([name]) => name !== 'a0' && name !== 'b0');
if (ports.length === 0) return result;
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
ports.forEach(([, info]) => {
const x = Number(info.x || 0);
const y = Number(info.y || 0);
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
});
ports.forEach(([name, info]) => {
let x = Number(info.x || 0);
let y = Number(info.y || 0);
let a = Number(info.a || 0);
if (data.flip) {
y = minY + maxY - y;
a = -a;
}
if (data.flop) {
x = minX + maxX - x;
a = normalizeAngle(180 - a);
}
result[name] = { ...info, x, y, a: normalizeAngle(a) };
});
return result;
},
[data.ports, data.flip, data.flop]
);
const portHandles = useMemo(
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }),
() => buildPortHandles(flippedPorts, { rotation: 0, boxSize: componentSize }),
[data.ports, data.rotation, data.flip, data.flop, componentSize]
);
const portDirectionMap = useMemo(
@@ -1731,20 +1776,22 @@ Organization : OptiHK Limited
const isAnchorElement = data.elementType === 'anchor';
const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName);
const visualSize = isAnchorElement ? { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE } : componentSize;
const componentVisualTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
const componentVisualTransform = `rotate(${data.rotation || 0}deg)`;
const componentBodyTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
const iconSize = createComponentSymbolMetrics(componentSize);
const portLabelStyle = (portHandle) => {
const base = { ...portHandle.style };
const unrotate = `rotate(${-(data.rotation || 0)}deg)`;
if (portHandle.position === 'left') {
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: `${unrotate} translateY(-50%)`, textAlign: 'right' };
}
if (portHandle.position === 'right') {
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: 'translateY(-50%)', textAlign: 'left' };
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: `${unrotate} translateY(-50%)`, textAlign: 'left' };
}
if (portHandle.position === 'top') {
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: `${unrotate} translateX(-50%)`, textAlign: 'center' };
}
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: 'translateX(-50%)', textAlign: 'center' };
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: `${unrotate} translateX(-50%)`, textAlign: 'center' };
};
return (
@@ -1768,7 +1815,7 @@ Organization : OptiHK Limited
...(visualSize.height < 50 && !isAnchorElement ? { padding: '2px 4px' } : {}),
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
transform: componentVisualTransform,
transform: componentBodyTransform,
transformOrigin: 'center center',
...(isBasicCompactComponent ? {
padding: 0,
@@ -1806,35 +1853,38 @@ Organization : OptiHK Limited
top: 0, left: 0,
width: componentSize.width,
height: visualSize.height,
transform: componentVisualTransform,
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 (
<React.Fragment key={portHandle.name}>
<Handle
type="source"
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
position={handlePositionMap[effectiveDir]}
id={portHandle.name}
title={portHandle.name}
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
/>
<Handle
type="target"
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
position={handlePositionMap[effectiveDir]}
id={portHandle.name}
title={portHandle.name}
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
/>
</React.Fragment>
))}
</div>
{portHandles.map((portHandle) => (
<React.Fragment key={`label-${portHandle.name}`}>
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
);
})}
{portHandles.map((portHandle) => (
<span key={`label-${portHandle.name}`} className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
{portHandle.name}
</span>
</React.Fragment>
))}
))}
</div>
</div>
);
}, (prevProps, nextProps) => {
@@ -3974,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
@@ -3998,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([]);
@@ -5985,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,
@@ -5994,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;
@@ -6010,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) => {