Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2846899097 | |||
| 2ddd30e7bb | |||
| 866bc1de18 | |||
| 23d631c4f0 |
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
|
||||
+150
-64
@@ -1566,6 +1566,7 @@ Organization : OptiHK Limited
|
||||
calculateLayoutBounds,
|
||||
calculateCompositeBoxSize,
|
||||
buildPortHandles,
|
||||
getRotatableNodeHandleDirection,
|
||||
buildElementPorts,
|
||||
getElementPinName,
|
||||
buildElementBoxSize,
|
||||
@@ -1622,7 +1623,7 @@ Organization : OptiHK Limited
|
||||
|
||||
|
||||
// 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(() => {
|
||||
if (!category) return undefined;
|
||||
const cache = fetchIcon(category);
|
||||
@@ -1671,7 +1672,7 @@ Organization : OptiHK Limited
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'fill',
|
||||
objectFit: imgObjectFit || 'fill',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
onError={(e) => {
|
||||
@@ -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: 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,21 +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) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
|
||||
const unrotate = `rotate(${-(data.rotation || 0)}deg)`;
|
||||
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') {
|
||||
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') {
|
||||
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 (
|
||||
@@ -1759,19 +1805,18 @@ Organization : OptiHK Limited
|
||||
<span title={data.componentName}>{data.componentName}</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: componentSize.width,
|
||||
transform: componentVisualTransform,
|
||||
transformOrigin: 'center center',
|
||||
}}>
|
||||
<div
|
||||
className="component-visual-body"
|
||||
style={{
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
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)',
|
||||
<div
|
||||
className="component-visual-body"
|
||||
style={{
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
minHeight: visualSize.height,
|
||||
overflow: 'hidden',
|
||||
...(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: componentBodyTransform,
|
||||
transformOrigin: 'center center',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
@@ -1794,8 +1839,8 @@ Organization : OptiHK Limited
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', minHeight: '100%' }}>
|
||||
{!data.hideIcon && data.category && (
|
||||
<div style={{ width: iconSize.width, height: iconSize.height }}>
|
||||
<IconImg category={data.category} />
|
||||
<div style={{ maxWidth: iconSize.width, maxHeight: iconSize.height, width: '100%', aspectRatio: `${iconSize.width}/${iconSize.height}`, overflow: 'hidden' }}>
|
||||
<IconImg category={data.category} objectFit="contain" />
|
||||
</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)' }} />}
|
||||
@@ -1803,39 +1848,41 @@ Organization : OptiHK Limited
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
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]}
|
||||
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}>
|
||||
{portHandle.name}
|
||||
</span>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0,
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
transform: componentVisualTransform,
|
||||
transformOrigin: 'center center',
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{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[effectiveDir]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[effectiveDir]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
|
||||
/>
|
||||
</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>
|
||||
@@ -3977,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
|
||||
@@ -4001,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([]);
|
||||
@@ -5065,6 +5114,35 @@ Organization : OptiHK Limited
|
||||
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 nonProjectPages = cellPages.filter(page => page !== loadedProjectPage);
|
||||
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 () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pages, loadComponentMetadata]);
|
||||
}, [pages, loadComponentMetadata, reactFlowInstance]);
|
||||
|
||||
const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]);
|
||||
|
||||
@@ -5951,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,
|
||||
@@ -5960,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;
|
||||
@@ -5976,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) => {
|
||||
|
||||
Reference in New Issue
Block a user