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,