Merged from jingwen_main for image icon revision
This commit is contained in:
+31
-14
@@ -42,11 +42,14 @@
|
||||
name: 'Anchor',
|
||||
elementType: 'anchor',
|
||||
ports: {
|
||||
a1: { x: 0, y: -PORT_NODE_SIZE / 2, a: 180, width: 0.5 },
|
||||
b1: { x: 0, y: -PORT_NODE_SIZE / 2, a: 0, width: 0.5 }
|
||||
a1: { x: 0, y: 0, a: 180, width: 0.5 },
|
||||
b1: { x: 0, y: 0, a: 0, width: 0.5 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Defines local primitive components that do not require PDK lookup.
|
||||
const BASIC_COMPONENTS = {
|
||||
waveguide: {
|
||||
@@ -804,16 +807,13 @@
|
||||
}
|
||||
};
|
||||
}
|
||||
if (portNumber > 1) {
|
||||
const entries = [];
|
||||
Array.from({ length: portNumber }, (_, index) => {
|
||||
const y = elementPortOffset(index, portNumber, pitch);
|
||||
entries.push([`a${index + 1}`, { x: 0, y, a: 180, width }]);
|
||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
||||
});
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(element.ports));
|
||||
const entries = [];
|
||||
Array.from({ length: portNumber }, (_, index) => {
|
||||
const y = elementPortOffset(index, portNumber, pitch);
|
||||
entries.push([`a${index + 1}`, { x: 0, y, a: 180, width }]);
|
||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
||||
});
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
// Generate port metadata for built-in primitive components.
|
||||
@@ -984,6 +984,20 @@ ${pinLines}`;
|
||||
return `elements:\n${lines.join('\n')}`;
|
||||
};
|
||||
|
||||
const finiteNumberOrNull = (value) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
};
|
||||
|
||||
const getRouteEndpointWidth = (node, handleId) => {
|
||||
if (!node || !node.data) return null;
|
||||
const dataWidth = finiteNumberOrNull(node.data.width);
|
||||
if (dataWidth !== null) return dataWidth;
|
||||
const ports = node.data.ports || {};
|
||||
const portWidth = ports[handleId] ? finiteNumberOrNull(ports[handleId].width) : null;
|
||||
return portWidth;
|
||||
};
|
||||
|
||||
// Serialize canvas links into routed bundle YAML including route settings and bend points.
|
||||
const buildBundlesYaml = (page, manifest) => {
|
||||
const { nodes = [], edges = [] } = page || {};
|
||||
@@ -1004,6 +1018,9 @@ ${pinLines}`;
|
||||
? getElementPinName(targetNode, edge.targetHandle)
|
||||
: edge.targetHandle || 'unknown';
|
||||
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
||||
const routeWidth = getRouteEndpointWidth(sourceNode, edge.sourceHandle)
|
||||
?? getRouteEndpointWidth(targetNode, edge.targetHandle)
|
||||
?? route.width;
|
||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
||||
const pointsYaml = points.length > 0
|
||||
@@ -1014,7 +1031,7 @@ ${pinLines}`;
|
||||
return ` - id: ${toYamlScalar(edge.id)}
|
||||
xsection: ${route.xsection}
|
||||
family: ${route.family}
|
||||
width: ${Number(route.width)}
|
||||
width: ${Number(routeWidth)}
|
||||
radius: ${Number(route.radius)}
|
||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||
}
|
||||
@@ -1022,7 +1039,7 @@ ${pinLines}`;
|
||||
to: ${targetName}:${toPort}
|
||||
xsection: ${route.xsection}
|
||||
family: ${route.family}
|
||||
width: ${Number(route.width)}
|
||||
width: ${Number(routeWidth)}
|
||||
radius: ${Number(route.radius)}
|
||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||
});
|
||||
|
||||
+116
-83
@@ -1622,7 +1622,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 +1671,7 @@ Organization : OptiHK Limited
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'fill',
|
||||
objectFit: imgObjectFit || 'fill',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
onError={(e) => {
|
||||
@@ -1764,42 +1764,46 @@ Organization : OptiHK Limited
|
||||
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)',
|
||||
transform: componentVisualTransform,
|
||||
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
} : {}),
|
||||
...(isAnchorElement ? {
|
||||
width: PORT_NODE_SIZE,
|
||||
minHeight: PORT_NODE_SIZE,
|
||||
padding: 0,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
{isAnchorElement ? (
|
||||
<span style={{ fontSize: 8, fontWeight: 800, color: selected ? 'var(--accent)' : 'var(--text-main)' }}>A</span>
|
||||
) : (
|
||||
<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} />
|
||||
transform: componentVisualTransform,
|
||||
transformOrigin: 'center center',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
} : {}),
|
||||
...(isAnchorElement ? {
|
||||
width: PORT_NODE_SIZE,
|
||||
minHeight: PORT_NODE_SIZE,
|
||||
padding: 0,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
{isAnchorElement ? (
|
||||
<span style={{ fontSize: 8, fontWeight: 800, color: selected ? 'var(--accent)' : 'var(--text-main)' }}>A</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', minHeight: '100%' }}>
|
||||
{!data.hideIcon && 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)' }} />}
|
||||
</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)' }} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
position: 'absolute',
|
||||
top: 0, left: 0,
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
pointerEvents: 'none'
|
||||
@@ -1985,29 +1989,22 @@ Organization : OptiHK Limited
|
||||
const name = String(portName || '');
|
||||
return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right';
|
||||
};
|
||||
const anchorHandleVisualStyle = (portHandle, zIndex) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
...baseHandleStyle,
|
||||
zIndex,
|
||||
left: localLeft,
|
||||
top: localTop,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
};
|
||||
const anchorHandleVisualStyle = (portHandle, zIndex) => ({
|
||||
...baseHandleStyle,
|
||||
zIndex,
|
||||
left: portHandle.style?.left,
|
||||
top: portHandle.style?.top || '50%',
|
||||
right: portHandle.style?.right || 'auto',
|
||||
bottom: portHandle.style?.bottom || 'auto',
|
||||
transform: portHandle.style?.transform || 'translate(-50%, -50%)'
|
||||
});
|
||||
const pinLabelStyle = (portHandle) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
left: localLeft,
|
||||
top: localTop,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
left: portHandle.style?.left,
|
||||
top: portHandle.style?.top || '50%',
|
||||
right: portHandle.style?.right || 'auto',
|
||||
bottom: portHandle.style?.bottom || 'auto',
|
||||
transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)'
|
||||
};
|
||||
};
|
||||
@@ -3359,6 +3356,7 @@ Organization : OptiHK Limited
|
||||
const forge = isForgeComponent(componentName);
|
||||
onUpdateNode(selectedNode.id, {
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
componentName,
|
||||
label: componentName,
|
||||
ports: forge ? {} : undefined,
|
||||
@@ -3760,8 +3758,6 @@ Organization : OptiHK Limited
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const canvasViewportRef = useRef(null);
|
||||
const buildLayoutRequestRef = useRef(0);
|
||||
const buildLayoutBusyRef = useRef(false);
|
||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||
|
||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||
@@ -3807,6 +3803,14 @@ Organization : OptiHK Limited
|
||||
}
|
||||
: null
|
||||
), [mouseCanvasPoint, canvasOrigin]);
|
||||
const handleCanvasViewportMoveEnd = useCallback((event, viewport) => {
|
||||
if (!activePageId || !viewport) return;
|
||||
setPages(prev => prev.map(page => (
|
||||
page.id === activePageId
|
||||
? { ...page, viewport: { x: viewport.x, y: viewport.y, zoom: viewport.zoom } }
|
||||
: page
|
||||
)));
|
||||
}, [activePageId]);
|
||||
// Normalizes free-route control points and removes adjacent duplicates before storage.
|
||||
const compactRoutePoints = useCallback((points) => {
|
||||
return (points || [])
|
||||
@@ -5058,6 +5062,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;
|
||||
@@ -5084,12 +5117,18 @@ Organization : OptiHK Limited
|
||||
|
||||
useEffect(() => {
|
||||
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
||||
reactFlowInstance.fitBounds(
|
||||
{ x: 0, y: 0, width: activeCanvasSize.width, height: activeCanvasSize.height },
|
||||
{ padding: 0.12, duration: 0 }
|
||||
);
|
||||
if (activePage.viewport) {
|
||||
window.requestAnimationFrame(() => {
|
||||
reactFlowInstance.setViewport(activePage.viewport, { duration: 0 });
|
||||
});
|
||||
} else {
|
||||
reactFlowInstance.fitBounds(
|
||||
{ x: 0, y: 0, width: activeCanvasSize.width, height: activeCanvasSize.height },
|
||||
{ padding: 0.12, duration: 0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [activePage?.id, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]);
|
||||
}, [activePage?.id, activePage?.viewport, activeCanvasSize.width, activeCanvasSize.height, reactFlowInstance]);
|
||||
|
||||
useEffect(() => {
|
||||
setRulerStartPoint(null);
|
||||
@@ -5176,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 () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pages, loadComponentMetadata]);
|
||||
}, [pages, loadComponentMetadata, reactFlowInstance]);
|
||||
|
||||
const openTabs = useMemo(() => pages.filter(page => !page.isClosed), [pages]);
|
||||
|
||||
@@ -6210,64 +6257,49 @@ ${bundlesBlock}`;
|
||||
// Save the active page, generate layout preview assets, and show the preview tab.
|
||||
const handleBuildLayout = useCallback(async () => {
|
||||
if (!activePage) return;
|
||||
if (buildLayoutBusyRef.current) return;
|
||||
if (buildLayoutBusy) return;
|
||||
if (!validateRouteCrossings(activePage)) return;
|
||||
const buildPage = activePage;
|
||||
const buildRequestId = buildLayoutRequestRef.current + 1;
|
||||
buildLayoutRequestRef.current = buildRequestId;
|
||||
buildLayoutBusyRef.current = true;
|
||||
setBuildLayoutBusy(true);
|
||||
startBuildProgress('Building layout');
|
||||
const yamlContent = buildYamlForPage(buildPage);
|
||||
const layoutBounds = calculateLayoutBounds(buildPage);
|
||||
const yamlContent = buildYamlForPage(activePage);
|
||||
|
||||
// send to backend
|
||||
try {
|
||||
const response = await fetch('/api/save-layout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
project: currentProjectName,
|
||||
cell: buildPage.name,
|
||||
cell: activePage.name,
|
||||
content: yamlContent,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
const errData = await response.json();
|
||||
addLog(errData.error || 'Save failed, unknown error');
|
||||
stopBuildProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Successfully saved: ' + result.path);
|
||||
if (result.preview_error) {
|
||||
addLog('Preview skipped: ' + result.preview_error);
|
||||
}
|
||||
if (result.svg_ready && result.svg_url) {
|
||||
if (result.svg_url) {
|
||||
completeBuildProgress('Layout ready');
|
||||
openLayoutPreview(buildPage.name, result.svg_url, layoutBounds);
|
||||
openLayoutPreview(activePage.name, result.svg_url, calculateLayoutBounds(activePage));
|
||||
} else {
|
||||
if (result.preview_status === 'generated') {
|
||||
addLog('Layout SVG was not marked ready by the backend.');
|
||||
}
|
||||
completeBuildProgress('Layout saved');
|
||||
}
|
||||
} catch (err) {
|
||||
if (buildRequestId !== buildLayoutRequestRef.current) return;
|
||||
addLog('Save error: ' + err.message);
|
||||
stopBuildProgress();
|
||||
} finally {
|
||||
if (buildRequestId === buildLayoutRequestRef.current) {
|
||||
buildLayoutBusyRef.current = false;
|
||||
setBuildLayoutBusy(false);
|
||||
}
|
||||
setBuildLayoutBusy(false);
|
||||
}
|
||||
}, [activePage, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
||||
}, [activePage, buildLayoutBusy, buildYamlForPage, currentProjectName, addLog, openLayoutPreview, validateRouteCrossings, startBuildProgress, completeBuildProgress, stopBuildProgress]);
|
||||
|
||||
// Save YAML for every editable project/composite page without opening previews.
|
||||
const handleSaveProjectLayouts = useCallback(async () => {
|
||||
@@ -6551,6 +6583,7 @@ ${bundlesBlock}`;
|
||||
minZoom={0.02}
|
||||
maxZoom={4}
|
||||
defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}
|
||||
onMoveEnd={handleCanvasViewportMoveEnd}
|
||||
panOnDrag={false}
|
||||
selectionOnDrag={true}
|
||||
selectionMode={FULL_SELECTION_MODE}
|
||||
|
||||
Reference in New Issue
Block a user