Bundle group added to .yml generation and canvas

This commit is contained in:
2026-06-08 16:34:39 +08:00
parent 75dd78aa33
commit 7953c8b624
5 changed files with 385 additions and 107 deletions
+85 -37
View File
@@ -22,6 +22,7 @@
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
// Base visual diameter and hit area used for port and anchor handles.
const PORT_NODE_SIZE = 30;
const FREE_WIRES_BUNDLE_GROUP = 'free_wires';
const PORT_LABEL_MIN_CHARS = 5;
const PORT_LABEL_CHAR_WIDTH = 7;
const PORT_LABEL_HORIZONTAL_PADDING = 12;
@@ -137,6 +138,26 @@
return (technology.xsections && technology.xsections[xsection]) || technology.xsections.strip || {};
};
const cleanBundleGroupName = (value) => String(value ?? '')
.trim()
.replace(/\s+/g, '_')
.replace(/[^A-Za-z0-9_.-]/g, '_')
.replace(/_+/g, '_')
.replace(/^[._-]+|[._-]+$/g, '');
const normalizeBundleGroupName = (value, fallback = FREE_WIRES_BUNDLE_GROUP) => {
const cleaned = cleanBundleGroupName(value);
if (cleaned) return cleaned;
const fallbackText = fallback === null || fallback === undefined ? '' : String(fallback);
return cleanBundleGroupName(fallbackText) || (fallbackText === '' ? '' : FREE_WIRES_BUNDLE_GROUP);
};
const freeWireBundleGroupName = (xsection, defaultXsection) => {
const defaultName = normalizeBundleGroupName(defaultXsection || FALLBACK_TECHNOLOGY_MANIFEST.defaults.xsection || 'strip', 'strip');
const currentName = normalizeBundleGroupName(xsection || defaultXsection || defaultName, defaultName);
return currentName === defaultName ? FREE_WIRES_BUNDLE_GROUP : `${FREE_WIRES_BUNDLE_GROUP}_${currentName}`;
};
// Normalize route settings so every edge has xsection, family, width, radius, and bend type.
const createRouteSettings = (manifest, overrides) => {
const technology = getTechnologyManifest(manifest);
@@ -150,6 +171,7 @@
width: Number((overrides && overrides.width) ?? xsectionInfo.default_width ?? defaults.width ?? 0.45),
radius: Number((overrides && overrides.radius) ?? xsectionInfo.default_radius ?? defaults.radius ?? 10),
routing_type: (overrides && overrides.routing_type) || defaults.routing_type || 'euler_bend',
bundle_group: (overrides && (overrides.bundle_group ?? overrides.bundleGroup)) || '',
widthEdited: Boolean(overrides && overrides.widthEdited)
};
};
@@ -809,7 +831,8 @@
}
const entries = [];
Array.from({ length: portNumber }, (_, index) => {
const y = elementPortOffset(index, portNumber, pitch);
const defaultSingleAnchor = portNumber === 1;
const y = defaultSingleAnchor ? -PORT_NODE_SIZE / 2 : 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 }]);
});
@@ -1004,54 +1027,76 @@ ${pinLines}`;
const nodeMap = {};
nodes.forEach(n => { nodeMap[n.id] = n; });
let linksYaml = '';
if (edges.length > 0) {
const linkLines = edges.map(edge => {
const sourceNode = nodeMap[edge.source];
const targetNode = nodeMap[edge.target];
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
? getElementPinName(sourceNode, edge.sourceHandle)
: edge.sourceHandle || 'unknown';
const toPort = targetNode && targetNode.data && targetNode.data.elementType
? 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
? `\n points:\n${points.map(point => ` - x: ${Number(point.x || 0).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
: '';
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
if (isFreeRoute) {
return ` - id: ${toYamlScalar(edge.id)}
const groups = new Map();
let primaryFreeWireXsection = '';
const freeWireGroupForRoute = (route) => {
const xsectionName = normalizeBundleGroupName(route.xsection, 'strip');
if (!primaryFreeWireXsection) {
primaryFreeWireXsection = xsectionName;
return FREE_WIRES_BUNDLE_GROUP;
}
return xsectionName === primaryFreeWireXsection
? FREE_WIRES_BUNDLE_GROUP
: `${FREE_WIRES_BUNDLE_GROUP}_${xsectionName}`;
};
edges.forEach(edge => {
const sourceNode = nodeMap[edge.source];
const targetNode = nodeMap[edge.target];
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
? getElementPinName(sourceNode, edge.sourceHandle)
: edge.sourceHandle || 'unknown';
const toPort = targetNode && targetNode.data && targetNode.data.elementType
? 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
? `\n points:\n${points.map(point => ` - x: ${Number(point.x || 0).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
: '';
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
const linkYaml = isFreeRoute
? ` - id: ${toYamlScalar(edge.id)}
xsection: ${route.xsection}
family: ${route.family}
width: ${Number(routeWidth)}
radius: ${Number(route.radius)}
routing_type: ${route.routing_type}${pointsYaml}`;
}
return ` - from: ${sourceName}:${fromPort}
routing_type: ${route.routing_type}${pointsYaml}`
: ` - from: ${sourceName}:${fromPort}
to: ${targetName}:${toPort}
xsection: ${route.xsection}
family: ${route.family}
width: ${Number(routeWidth)}
radius: ${Number(route.radius)}
routing_type: ${route.routing_type}${pointsYaml}`;
});
linksYaml = linkLines.join('\n');
}
const routeGroupName = normalizeBundleGroupName(route.bundle_group, '');
const groupName = routeGroupName || freeWireGroupForRoute(route);
if (!groups.has(groupName)) {
groups.set(groupName, {
xsection: route.xsection,
family: route.family,
routing_type: route.routing_type,
links: []
});
}
groups.get(groupName).links.push(linkYaml);
});
const groupsYaml = Array.from(groups.entries()).map(([groupName, group]) => ` ${groupName}:
xsection: ${group.xsection}
family: ${group.family}
routing_type: ${group.routing_type}
links:
${group.links.join('\n')}`).join('\n');
return `# 3. Bundles (Grouped links for multi-bus/parallel routing)
bundles:
output_bus:
routing_type: euler_bend
links:
${linksYaml}`;
bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
};
// Return the center point of a node when a more precise port point is unavailable.
@@ -1252,6 +1297,7 @@ ${linksYaml}`;
BASIC_COMPONENTS,
DEFAULT_FORGE_ARGUMENTS,
FALLBACK_TECHNOLOGY_MANIFEST,
FREE_WIRES_BUNDLE_GROUP,
canvasToLayoutY,
layoutToCanvasY,
createForgeArguments,
@@ -1259,6 +1305,8 @@ ${linksYaml}`;
updateRouteField,
updateRouteXsection,
routeStyleForSettings,
normalizeBundleGroupName,
freeWireBundleGroupName,
findSameTypeRouteCrossing,
findSameFamilyRouteCrossing,
isForgeComponent,
+183 -70
View File
@@ -1580,6 +1580,9 @@ Organization : OptiHK Limited
updateRouteField,
updateRouteXsection,
routeStyleForSettings,
FREE_WIRES_BUNDLE_GROUP,
normalizeBundleGroupName,
freeWireBundleGroupName,
findSameTypeRouteCrossing,
createRulerMeasurement,
createComponentSymbolMetrics,
@@ -1589,6 +1592,15 @@ Organization : OptiHK Limited
const FULL_SELECTION_MODE = SelectionMode && SelectionMode.Full ? SelectionMode.Full : 'full';
const forEachBundleLink = (doc, callback) => {
Object.entries(doc.bundles || {}).forEach(([bundleName, bundleData]) => {
const bundle = bundleData && typeof bundleData === 'object' ? bundleData : {};
const links = bundle.links;
if (!links) return;
const linkArray = Array.isArray(links) ? links : [links];
linkArray.forEach(link => callback(bundleName, bundle, link || {}));
});
};
const iconPromiseCache = {};
// Loads and caches category icons so repeated library renders do not refetch the same image.
@@ -2841,12 +2853,13 @@ Organization : OptiHK Limited
};
// Renders editable properties for selected nodes, ports, anchors, and routes.
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const RightPanel = ({ selectedNode, selectedNodes = [], selectedEdge, selectedEdges = [], bundleGroupOptions = [], technologyManifest, projectName, compositeNames = [], width, onRenameComponent, onUpdateNode, onUpdateEdgeRoute }) => {
const [componentData, setComponentData] = useState(null);
const [loading, setLoading] = useState(false);
const [enlarged, setEnlarged] = useState(null);
const [editingComponentName, setEditingComponentName] = useState(false);
const [tempComponentName, setTempComponentName] = useState('');
const [newBundleGroupName, setNewBundleGroupName] = useState('');
const [localX, setLocalX] = useState('');
const [localY, setLocalY] = useState('');
const [localRotation, setLocalRotation] = useState('');
@@ -3046,9 +3059,51 @@ Organization : OptiHK Limited
family: mixedValue('family'),
width: mixedValue('width'),
radius: mixedValue('radius'),
routing_type: mixedValue('routing_type')
routing_type: mixedValue('routing_type'),
bundle_group: mixedValue('bundle_group')
};
const routingTypes = (technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).routing_types || ['euler_bend', 'standard_bend'];
const routeManifestDefaults = (technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).defaults || {};
const selectedRouteXsection = route.xsection === '__mixed__' ? firstRoute.xsection : route.xsection;
const selectedRouteFamily = route.family === '__mixed__' ? firstRoute.family : route.family;
const compatibleFreeWireGroup = (xsection) => {
const freeWireForXsection = bundleGroupOptions.find(option => (
option.name === FREE_WIRES_BUNDLE_GROUP && option.xsection === xsection
));
return freeWireForXsection
? FREE_WIRES_BUNDLE_GROUP
: freeWireBundleGroupName(xsection, routeManifestDefaults.xsection || 'strip');
};
const freeWireOptionName = compatibleFreeWireGroup(selectedRouteXsection);
const compatibleBundleGroupOptions = bundleGroupOptions
.filter(option => option.xsection === selectedRouteXsection)
.map(option => ({ ...option, name: normalizeBundleGroupName(option.name, freeWireOptionName) }));
if (!compatibleBundleGroupOptions.some(option => option.name === freeWireOptionName)) {
compatibleBundleGroupOptions.unshift({
name: freeWireOptionName,
xsection: selectedRouteXsection,
family: selectedRouteFamily
});
}
const selectedBundleGroupName = route.bundle_group === '__mixed__'
? '__mixed__'
: normalizeBundleGroupName(route.bundle_group, freeWireOptionName);
const selectedBundleGroupOption = compatibleBundleGroupOptions.find(option => option.name === selectedBundleGroupName) || compatibleBundleGroupOptions[0];
const bundleGroupOptionColor = (option) => routeStyleForSettings({ xsection: option.xsection, family: option.family }, false).style.stroke;
const selectedBundleGroupColor = selectedBundleGroupOption ? bundleGroupOptionColor(selectedBundleGroupOption) : routeStyleForSettings(route, false).style.stroke;
const onAddBundleGroup = () => {
if (route.xsection === '__mixed__') return;
const sanitizedName = normalizeBundleGroupName(newBundleGroupName, '');
if (!sanitizedName) return;
const collidesWithOtherXsection = bundleGroupOptions.some(option => (
option.name === sanitizedName && option.xsection !== selectedRouteXsection
));
const finalName = collidesWithOtherXsection
? `${sanitizedName}_${normalizeBundleGroupName(selectedRouteXsection, 'route')}`
: sanitizedName;
onUpdateEdgeRoute(selectedEdgeIds, currentRoute => updateRouteField(currentRoute, 'bundle_group', finalName, technologyManifest));
setNewBundleGroupName('');
};
return (
<aside style={{
width: width, background: 'var(--bg-card)', borderLeft: '1px solid var(--border)',
@@ -3064,7 +3119,14 @@ Organization : OptiHK Limited
<label>XSection</label>
<select
value={route.xsection}
onChange={(event) => onUpdateEdgeRoute(selectedEdgeIds, currentRoute => updateRouteXsection(currentRoute, event.target.value, technologyManifest))}
onChange={(event) => {
const nextXsection = event.target.value;
const nextBundleGroup = compatibleFreeWireGroup(nextXsection);
onUpdateEdgeRoute(selectedEdgeIds, currentRoute => ({
...updateRouteXsection(currentRoute, nextXsection, technologyManifest),
bundle_group: nextBundleGroup
}));
}}
>
{route.xsection === '__mixed__' && <option value="__mixed__" disabled>--</option>}
{xsections.map(xsection => (
@@ -3072,6 +3134,44 @@ Organization : OptiHK Limited
))}
</select>
<br /><br />
<label>Bundle Group</label>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 92px) auto', gap: 6, alignItems: 'center' }}>
<select
value={selectedBundleGroupName}
style={{ color: selectedBundleGroupColor }}
onChange={(event) => onUpdateEdgeRoute(selectedEdgeIds, currentRoute => updateRouteField(
currentRoute,
'bundle_group',
normalizeBundleGroupName(event.target.value, freeWireOptionName),
technologyManifest
))}
>
{route.bundle_group === '__mixed__' && <option value="__mixed__" disabled>--</option>}
{compatibleBundleGroupOptions.map(option => (
<option
key={`${option.name}-${option.xsection}`}
value={option.name}
style={{ color: bundleGroupOptionColor(option) }}
>
{option.name}
</option>
))}
</select>
<input
type="text"
value={newBundleGroupName}
placeholder="group_A"
onChange={(event) => setNewBundleGroupName(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') onAddBundleGroup();
}}
disabled={route.xsection === '__mixed__'}
/>
<button type="button" className="mini-btn" onClick={onAddBundleGroup} disabled={route.xsection === '__mixed__'}>
Add
</button>
</div>
<br /><br />
<label>Width</label>
<input
type="text"
@@ -3803,6 +3903,20 @@ Organization : OptiHK Limited
const selectedEdge = selectedEdges[0] || null;
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
const selectedNode = selectedNodes[0] || null;
const bundleGroupOptions = useMemo(() => {
const groups = new Map();
currentEdges.forEach(edge => {
const route = createRouteSettings(technologyManifest, edge.data?.route);
const name = normalizeBundleGroupName(route.bundle_group, '');
if (!name || groups.has(name)) return;
groups.set(name, {
name,
xsection: route.xsection,
family: route.family
});
});
return Array.from(groups.values());
}, [currentEdges, technologyManifest]);
const linkXsectionChoices = useMemo(() => {
const manifestSections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
const preferred = ['strip', 'rib_low', 'metal_1', 'metal_2'];
@@ -3813,7 +3927,13 @@ Organization : OptiHK Limited
return ordered.length ? ordered : preferred;
}, [technologyManifest]);
const currentLinkRoute = useMemo(
() => createRouteSettings(technologyManifest, { xsection: currentLinkXsection }),
() => {
const manifestDefaults = (technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).defaults || {};
return createRouteSettings(technologyManifest, {
xsection: currentLinkXsection,
bundle_group: freeWireBundleGroupName(currentLinkXsection, manifestDefaults.xsection || 'strip')
});
},
[technologyManifest, currentLinkXsection]
);
useEffect(() => {
@@ -4805,40 +4925,36 @@ Organization : OptiHK Limited
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
if (!isProject) {
const links = doc.bundles?.output_bus?.links;
if (links) {
const linkArray = Array.isArray(links) ? links : [links];
linkArray.forEach(link => {
const route = createRouteSettings(technologyManifest, link);
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
if (link.from && link.to) {
const [fromInst, fromPort] = link.from.split(':');
const [toInst, toPort] = link.to.split(':');
const sourceId = nodeNameMap[fromInst];
const targetId = nodeNameMap[toInst];
if (sourceId && targetId) {
const sourceNode = newNodes.find(node => node.id === sourceId);
const targetNode = newNodes.find(node => node.id === targetId);
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
const view = routeStyleForSettings(route, false);
newEdges.push({
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
source: sourceId,
target: targetId,
sourceHandle,
targetHandle,
type: view.type,
style: view.style,
data: { route, points: routePoints },
});
}
} else if (routePoints.length >= 2) {
const edgeId = link.id || `route-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
newEdges.push(makeFreeRouteEdge(edgeId, routePoints, route));
forEachBundleLink(doc, (bundleName, bundle, link) => {
const route = createRouteSettings(technologyManifest, { ...bundle, ...link, bundle_group: bundleName });
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
if (link.from && link.to) {
const [fromInst, fromPort] = link.from.split(':');
const [toInst, toPort] = link.to.split(':');
const sourceId = nodeNameMap[fromInst];
const targetId = nodeNameMap[toInst];
if (sourceId && targetId) {
const sourceNode = newNodes.find(node => node.id === sourceId);
const targetNode = newNodes.find(node => node.id === targetId);
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
const view = routeStyleForSettings(route, false);
newEdges.push({
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
source: sourceId,
target: targetId,
sourceHandle,
targetHandle,
type: view.type,
style: view.style,
data: { route, points: routePoints },
});
}
});
}
} else if (routePoints.length >= 2) {
const edgeId = link.id || `route-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
newEdges.push(makeFreeRouteEdge(edgeId, routePoints, route));
}
});
}
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
@@ -5021,39 +5137,35 @@ Organization : OptiHK Limited
});
nodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
const links = doc.bundles?.output_bus?.links;
if (links) {
const linkArray = Array.isArray(links) ? links : [links];
linkArray.forEach(link => {
const route = createRouteSettings(manifest, link);
const routePoints = normalizeRoutePoints(link.points, usesGdsYUp);
if (link.from && link.to) {
const [fromInst, fromPort] = link.from.split(':');
const [toInst, toPort] = link.to.split(':');
const sourceId = nodeNameMap[fromInst];
const targetId = nodeNameMap[toInst];
if (!sourceId || !targetId) return;
const sourceNode = nodes.find(node => node.id === sourceId);
const targetNode = nodes.find(node => node.id === targetId);
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
const view = routeStyleForSettings(route, false);
edges.push({
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
source: sourceId,
target: targetId,
sourceHandle,
targetHandle,
type: view.type,
style: view.style,
data: { route, points: routePoints },
});
} else if (routePoints.length >= 2) {
const edgeId = link.id || `route-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
edges.push(makeFreeRouteEdge(edgeId, routePoints, route));
}
});
}
forEachBundleLink(doc, (bundleName, bundle, link) => {
const route = createRouteSettings(manifest, { ...bundle, ...link, bundle_group: bundleName });
const routePoints = normalizeRoutePoints(link.points, usesGdsYUp);
if (link.from && link.to) {
const [fromInst, fromPort] = link.from.split(':');
const [toInst, toPort] = link.to.split(':');
const sourceId = nodeNameMap[fromInst];
const targetId = nodeNameMap[toInst];
if (!sourceId || !targetId) return;
const sourceNode = nodes.find(node => node.id === sourceId);
const targetNode = nodes.find(node => node.id === targetId);
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
const view = routeStyleForSettings(route, false);
edges.push({
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
source: sourceId,
target: targetId,
sourceHandle,
targetHandle,
type: view.type,
style: view.style,
data: { route, points: routePoints },
});
} else if (routePoints.length >= 2) {
const edgeId = link.id || `route-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
edges.push(makeFreeRouteEdge(edgeId, routePoints, route));
}
});
return {
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
@@ -6641,6 +6753,7 @@ ${bundlesBlock}`;
selectedNodes={selectedNodes}
selectedEdge={selectedEdge}
selectedEdges={selectedEdges}
bundleGroupOptions={bundleGroupOptions}
technologyManifest={technologyManifest}
projectName={currentProjectName}
compositeNames={compositePageNames}