4 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
xsxx03-art 866bc1de18 update canvas.html 2026-06-04 19:32:28 +08:00
xsxx03-art 23d631c4f0 __pycache__ 2026-06-04 17:22:30 +08:00
16 changed files with 167 additions and 64 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
View File
@@ -1237,6 +1237,22 @@ ${linksYaml}`;
return null; 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. // Backward-compatible alias for same-type route crossing validation.
const findSameFamilyRouteCrossing = findSameTypeRouteCrossing; const findSameFamilyRouteCrossing = findSameTypeRouteCrossing;
@@ -1276,6 +1292,7 @@ ${linksYaml}`;
createComponentSymbolMetrics, createComponentSymbolMetrics,
transformPortInfo, transformPortInfo,
getNodePortCanvasPoint, getNodePortCanvasPoint,
getRotatableNodeHandleDirection,
buildPortHandles, buildPortHandles,
buildElementPorts, buildElementPorts,
buildElementPinEntries, buildElementPinEntries,
+150 -64
View File
@@ -1566,6 +1566,7 @@ Organization : OptiHK Limited
calculateLayoutBounds, calculateLayoutBounds,
calculateCompositeBoxSize, calculateCompositeBoxSize,
buildPortHandles, buildPortHandles,
getRotatableNodeHandleDirection,
buildElementPorts, buildElementPorts,
getElementPinName, getElementPinName,
buildElementBoxSize, buildElementBoxSize,
@@ -1622,7 +1623,7 @@ Organization : OptiHK Limited
// Displays a category icon with cached loading and graceful failure behavior. // Displays a category icon with cached loading and graceful failure behavior.
const IconImg = memo(({ category, containerStyle }) => { const IconImg = memo(({ category, containerStyle, objectFit: imgObjectFit }) => {
const [src, setSrc] = useState(() => { const [src, setSrc] = useState(() => {
if (!category) return undefined; if (!category) return undefined;
const cache = fetchIcon(category); const cache = fetchIcon(category);
@@ -1671,7 +1672,7 @@ Organization : OptiHK Limited
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
objectFit: 'fill', objectFit: imgObjectFit || 'fill',
pointerEvents: 'none', pointerEvents: 'none',
}} }}
onError={(e) => { onError={(e) => {
@@ -1698,8 +1699,10 @@ Organization : OptiHK Limited
useEffect(() => { useEffect(() => {
const transformKey = `${data.rotation || 0}:${data.flip ? 1 : 0}:${data.flop ? 1 : 0}`; const transformKey = `${data.rotation || 0}:${data.flip ? 1 : 0}:${data.flop ? 1 : 0}`;
if (prevTransformRef.current !== transformKey) { if (prevTransformRef.current !== transformKey) {
updateNodeInternalsRef.current(id);
prevTransformRef.current = transformKey; prevTransformRef.current = transformKey;
requestAnimationFrame(() => {
updateNodeInternalsRef.current(id);
});
} }
}, [data.rotation, data.flip, data.flop, id]); }, [data.rotation, data.flip, data.flop, id]);
@@ -1719,9 +1722,51 @@ Organization : OptiHK Limited
top: Position.Top, top: Position.Top,
bottom: Position.Bottom 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 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( const portHandles = useMemo(
() => buildPortHandles(data.ports, { 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] [data.ports, data.rotation, data.flip, data.flop, componentSize]
); );
const portDirectionMap = useMemo( const portDirectionMap = useMemo(
@@ -1731,21 +1776,22 @@ Organization : OptiHK Limited
const isAnchorElement = data.elementType === 'anchor'; const isAnchorElement = data.elementType === 'anchor';
const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName); 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 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 iconSize = createComponentSymbolMetrics(componentSize);
const portLabelStyle = (portHandle) => { const portLabelStyle = (portHandle) => {
const base = { ...portHandle.style }; const base = { ...portHandle.style };
const unrotate = `rotate(${-(data.rotation || 0)}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`; const unrotate = `rotate(${-(data.rotation || 0)}deg)`;
if (portHandle.position === 'left') { if (portHandle.position === 'left') {
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: `translateY(-50%) ${unrotate}`, textAlign: 'right' }; return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: `${unrotate} translateY(-50%)`, textAlign: 'right' };
} }
if (portHandle.position === 'right') { if (portHandle.position === 'right') {
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: `translateY(-50%) ${unrotate}`, textAlign: 'left' }; return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: `${unrotate} translateY(-50%)`, textAlign: 'left' };
} }
if (portHandle.position === 'top') { if (portHandle.position === 'top') {
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: `translateX(-50%) ${unrotate}`, 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%) ${unrotate}`, textAlign: 'center' }; return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: `${unrotate} translateX(-50%)`, textAlign: 'center' };
}; };
return ( return (
@@ -1759,19 +1805,18 @@ Organization : OptiHK Limited
<span title={data.componentName}>{data.componentName}</span> <span title={data.componentName}>{data.componentName}</span>
)} )}
</div> </div>
<div style={{ <div
position: 'relative', className="component-visual-body"
width: componentSize.width, style={{
transform: componentVisualTransform, width: componentSize.width,
transformOrigin: 'center center', height: visualSize.height,
}}> minHeight: visualSize.height,
<div overflow: 'hidden',
className="component-visual-body" ...(visualSize.height < 50 && !isAnchorElement ? { padding: '2px 4px' } : {}),
style={{ border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
width: componentSize.width, boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
height: visualSize.height, transform: componentBodyTransform,
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)', transformOrigin: 'center center',
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
...(isBasicCompactComponent ? { ...(isBasicCompactComponent ? {
padding: 0, padding: 0,
display: 'flex', display: 'flex',
@@ -1794,8 +1839,8 @@ Organization : OptiHK Limited
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', minHeight: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', minHeight: '100%' }}>
{!data.hideIcon && data.category && ( {!data.hideIcon && data.category && (
<div style={{ width: iconSize.width, height: iconSize.height }}> <div style={{ maxWidth: iconSize.width, maxHeight: iconSize.height, width: '100%', aspectRatio: `${iconSize.width}/${iconSize.height}`, overflow: 'hidden' }}>
<IconImg category={data.category} /> <IconImg category={data.category} objectFit="contain" />
</div> </div>
)} )}
{!data.category && <div style={{ width: iconSize.width, height: iconSize.height, borderRadius: 4, border: '1px solid var(--border-strong)', background: 'rgba(148, 163, 184, 0.08)' }} />} {!data.category && <div style={{ width: iconSize.width, height: iconSize.height, borderRadius: 4, border: '1px solid var(--border-strong)', background: 'rgba(148, 163, 184, 0.08)' }} />}
@@ -1803,39 +1848,41 @@ Organization : OptiHK Limited
)} )}
</div> </div>
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: 0, left: 0, top: 0, left: 0,
width: '100%', width: componentSize.width,
height: '100%', height: visualSize.height,
pointerEvents: 'none' transform: componentVisualTransform,
}}> transformOrigin: 'center center',
{portHandles.map((portHandle) => ( pointerEvents: 'none'
<React.Fragment key={portHandle.name}> }}>
<Handle {portHandles.map((portHandle) => {
type="source" const originalDir = portDirectionMap.get(portHandle.name) || portHandle.position;
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]} const effectiveDir = rotateHandleDirection(originalDir, data.rotation || 0);
id={portHandle.name} return (
title={portHandle.name} <React.Fragment key={portHandle.name}>
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }} <Handle
/> type="source"
<Handle position={handlePositionMap[effectiveDir]}
type="target" id={portHandle.name}
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]} title={portHandle.name}
id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
title={portHandle.name} />
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }} <Handle
/> type="target"
</React.Fragment> position={handlePositionMap[effectiveDir]}
))} id={portHandle.name}
</div> title={portHandle.name}
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
{portHandles.map((portHandle) => ( />
<React.Fragment key={`label-${portHandle.name}`}>
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
{portHandle.name}
</span>
</React.Fragment> </React.Fragment>
);
})}
{portHandles.map((portHandle) => (
<span key={`label-${portHandle.name}`} className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
{portHandle.name}
</span>
))} ))}
</div> </div>
</div> </div>
@@ -3977,8 +4024,10 @@ Organization : OptiHK Limited
const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`; const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`;
const key = [sourceEndpoint, targetEndpoint].sort().join('<>'); const key = [sourceEndpoint, targetEndpoint].sort().join('<>');
const group = groups.get(key) || []; const group = groups.get(key) || [];
const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle); const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle)
const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle); || 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 usesAnchorDirection = Boolean(sourceDirection || targetDirection);
const hasRoutePoints = edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2; const hasRoutePoints = edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2;
const directionalEdge = usesAnchorDirection const directionalEdge = usesAnchorDirection
@@ -4001,7 +4050,7 @@ Organization : OptiHK Limited
}; };
}); });
return [...separatedEdges, ...rulerEdges]; return [...separatedEdges, ...rulerEdges];
}, [currentEdges, currentNodes, getAnchorHandleRouteDirection, rulerEdges]); }, [currentEdges, currentNodes, getAnchorHandleRouteDirection, getRotatableNodeHandleDirection, rulerEdges]);
const [projectCompositeMap, setProjectCompositeMap] = useState({}); const [projectCompositeMap, setProjectCompositeMap] = useState({});
const [standaloneComposites, setStandaloneComposites] = useState([]); const [standaloneComposites, setStandaloneComposites] = useState([]);
@@ -5065,6 +5114,35 @@ Organization : OptiHK Limited
return boxSize ? { ...node, data: { ...node.data, boxSize } } : node; return boxSize ? { ...node, data: { ...node.data, boxSize } } : node;
}) })
})); }));
// Pre-fetch PDK component metadata so nodes render with correct boxSize immediately.
const allNodes = cellPages.flatMap(page => page.nodes);
const pdkNames = [...new Set(allNodes
.filter(n => n.data?.componentName && !n.data?.elementType
&& !isForgeComponent(n.data.componentName)
&& !isBasicComponent(n.data.componentName))
.map(n => n.data.componentName))];
if (pdkNames.length > 0) {
const metaResults = await Promise.all(
pdkNames.map(name => loadComponentMetadata(name).catch(() => null))
);
const metaMap = new Map(
pdkNames.filter((_, i) => metaResults[i]).map((name, i) => [name, metaResults[i]])
);
for (const page of cellPages) {
page.nodes = page.nodes.map(node => {
const metadata = metaMap.get(node.data?.componentName);
if (!metadata) return node;
const sz = normalizeBoxSize(metadata);
return {
...node,
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, sz),
data: { ...node.data, boxSize: sz, ports: metadata.pins || metadata.ports || {}, foundry: metadata.foundry || '', process: metadata.process || '' }
};
});
}
}
const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName); const loadedProjectPage = cellPages.find(page => page.type === 'project' && page.name === currentProjectName);
const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage); const nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
const resolvedProjectPage = loadedProjectPage || projectPage; const resolvedProjectPage = loadedProjectPage || projectPage;
@@ -5189,12 +5267,20 @@ Organization : OptiHK Limited
}; };
}) })
}))); })));
// Force React Flow to re-measure nodes whose boxSize / ports have changed.
requestAnimationFrame(() => {
const updatedIds = results.filter(r => r.metadata).map(r => r.nodeId);
if (updatedIds.length > 0 && reactFlowInstance.updateNodeInternals) {
reactFlowInstance.updateNodeInternals(updatedIds);
}
});
}); });
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [pages, loadComponentMetadata]); }, [pages, loadComponentMetadata, reactFlowInstance]);
const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]); const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]);
@@ -5951,6 +6037,7 @@ Organization : OptiHK Limited
const route = currentLinkRoute; const route = currentLinkRoute;
const view = routeStyleForSettings(route, false); const view = routeStyleForSettings(route, false);
const edgeId = `edge-${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}-${Date.now()}`; 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 = { const candidate = {
id: edgeId, id: edgeId,
source: connection.source, source: connection.source,
@@ -5960,9 +6047,8 @@ Organization : OptiHK Limited
type: view.type, type: view.type,
selectable: true, selectable: true,
style: view.style, 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); const conflict = findSameTypeRouteCrossing(candidate, activePage.edges, nodeMap, technologyManifest);
if (conflict) { if (conflict) {
const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source; const source = nodeMap[conflict.conflictEdge.source]?.data?.componentDisplayName || conflict.conflictEdge.source;
@@ -5976,7 +6062,7 @@ Organization : OptiHK Limited
: p : p
))); )));
addLog(`Connected ${connection.sourceHandle} to ${connection.targetHandle}.`); 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. // Select custom route edges from their SVG hit target.
const handleRouteEdgeMouseDown = useCallback((event) => { const handleRouteEdgeMouseDown = useCallback((event) => {