update canvas.html

This commit is contained in:
xsxx03-art
2026-06-04 19:32:28 +08:00
parent 23d631c4f0
commit 866bc1de18
+89 -55
View File
@@ -1622,7 +1622,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 +1671,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) => {
@@ -1721,7 +1721,7 @@ Organization : OptiHK Limited
}; };
const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE); const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const portHandles = useMemo( const portHandles = useMemo(
() => buildPortHandles(data.ports, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }), () => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), 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(
@@ -1735,17 +1735,16 @@ Organization : OptiHK Limited
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})`;
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: '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: '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: '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: 'translateX(-50%)', textAlign: 'center' };
}; };
return ( return (
@@ -1759,19 +1758,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: componentVisualTransform,
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 +1792,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,41 +1801,40 @@ 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' 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) => ( {portHandles.map((portHandle) => (
<React.Fragment key={`label-${portHandle.name}`}> <React.Fragment key={portHandle.name}>
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}> <Handle
{portHandle.name} type="source"
</span> 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> </React.Fragment>
))} ))}
</div> </div>
{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>
))}
</div> </div>
); );
}, (prevProps, nextProps) => { }, (prevProps, nextProps) => {
@@ -5065,6 +5062,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 +5215,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]);