Bundle group added to .yml generation and canvas
This commit is contained in:
+85
-37
@@ -22,6 +22,7 @@
|
|||||||
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
|
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
|
||||||
// Base visual diameter and hit area used for port and anchor handles.
|
// Base visual diameter and hit area used for port and anchor handles.
|
||||||
const PORT_NODE_SIZE = 30;
|
const PORT_NODE_SIZE = 30;
|
||||||
|
const FREE_WIRES_BUNDLE_GROUP = 'free_wires';
|
||||||
const PORT_LABEL_MIN_CHARS = 5;
|
const PORT_LABEL_MIN_CHARS = 5;
|
||||||
const PORT_LABEL_CHAR_WIDTH = 7;
|
const PORT_LABEL_CHAR_WIDTH = 7;
|
||||||
const PORT_LABEL_HORIZONTAL_PADDING = 12;
|
const PORT_LABEL_HORIZONTAL_PADDING = 12;
|
||||||
@@ -137,6 +138,26 @@
|
|||||||
return (technology.xsections && technology.xsections[xsection]) || technology.xsections.strip || {};
|
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.
|
// Normalize route settings so every edge has xsection, family, width, radius, and bend type.
|
||||||
const createRouteSettings = (manifest, overrides) => {
|
const createRouteSettings = (manifest, overrides) => {
|
||||||
const technology = getTechnologyManifest(manifest);
|
const technology = getTechnologyManifest(manifest);
|
||||||
@@ -150,6 +171,7 @@
|
|||||||
width: Number((overrides && overrides.width) ?? xsectionInfo.default_width ?? defaults.width ?? 0.45),
|
width: Number((overrides && overrides.width) ?? xsectionInfo.default_width ?? defaults.width ?? 0.45),
|
||||||
radius: Number((overrides && overrides.radius) ?? xsectionInfo.default_radius ?? defaults.radius ?? 10),
|
radius: Number((overrides && overrides.radius) ?? xsectionInfo.default_radius ?? defaults.radius ?? 10),
|
||||||
routing_type: (overrides && overrides.routing_type) || defaults.routing_type || 'euler_bend',
|
routing_type: (overrides && overrides.routing_type) || defaults.routing_type || 'euler_bend',
|
||||||
|
bundle_group: (overrides && (overrides.bundle_group ?? overrides.bundleGroup)) || '',
|
||||||
widthEdited: Boolean(overrides && overrides.widthEdited)
|
widthEdited: Boolean(overrides && overrides.widthEdited)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -809,7 +831,8 @@
|
|||||||
}
|
}
|
||||||
const entries = [];
|
const entries = [];
|
||||||
Array.from({ length: portNumber }, (_, index) => {
|
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([`a${index + 1}`, { x: 0, y, a: 180, width }]);
|
||||||
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
entries.push([`b${index + 1}`, { x: 0, y, a: 0, width }]);
|
||||||
});
|
});
|
||||||
@@ -1004,54 +1027,76 @@ ${pinLines}`;
|
|||||||
const nodeMap = {};
|
const nodeMap = {};
|
||||||
nodes.forEach(n => { nodeMap[n.id] = n; });
|
nodes.forEach(n => { nodeMap[n.id] = n; });
|
||||||
|
|
||||||
let linksYaml = '';
|
const groups = new Map();
|
||||||
if (edges.length > 0) {
|
let primaryFreeWireXsection = '';
|
||||||
const linkLines = edges.map(edge => {
|
const freeWireGroupForRoute = (route) => {
|
||||||
const sourceNode = nodeMap[edge.source];
|
const xsectionName = normalizeBundleGroupName(route.xsection, 'strip');
|
||||||
const targetNode = nodeMap[edge.target];
|
if (!primaryFreeWireXsection) {
|
||||||
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
primaryFreeWireXsection = xsectionName;
|
||||||
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
return FREE_WIRES_BUNDLE_GROUP;
|
||||||
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
|
}
|
||||||
? getElementPinName(sourceNode, edge.sourceHandle)
|
return xsectionName === primaryFreeWireXsection
|
||||||
: edge.sourceHandle || 'unknown';
|
? FREE_WIRES_BUNDLE_GROUP
|
||||||
const toPort = targetNode && targetNode.data && targetNode.data.elementType
|
: `${FREE_WIRES_BUNDLE_GROUP}_${xsectionName}`;
|
||||||
? getElementPinName(targetNode, edge.targetHandle)
|
};
|
||||||
: edge.targetHandle || 'unknown';
|
|
||||||
const route = createRouteSettings(manifest, edge.data && edge.data.route);
|
edges.forEach(edge => {
|
||||||
const routeWidth = getRouteEndpointWidth(sourceNode, edge.sourceHandle)
|
const sourceNode = nodeMap[edge.source];
|
||||||
?? getRouteEndpointWidth(targetNode, edge.targetHandle)
|
const targetNode = nodeMap[edge.target];
|
||||||
?? route.width;
|
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
|
||||||
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
|
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
|
||||||
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
|
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
|
||||||
const pointsYaml = points.length > 0
|
? getElementPinName(sourceNode, edge.sourceHandle)
|
||||||
? `\n points:\n${points.map(point => ` - x: ${Number(point.x || 0).toFixed(1)}\n y: ${canvasToLayoutY(point.y).toFixed(1)}`).join('\n')}`
|
: edge.sourceHandle || 'unknown';
|
||||||
: '';
|
const toPort = targetNode && targetNode.data && targetNode.data.elementType
|
||||||
const isFreeRoute = Boolean(edge.data && edge.data.freeRoute) || (!sourceNode && !targetNode && points.length >= 2);
|
? getElementPinName(targetNode, edge.targetHandle)
|
||||||
if (isFreeRoute) {
|
: edge.targetHandle || 'unknown';
|
||||||
return ` - id: ${toYamlScalar(edge.id)}
|
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}
|
xsection: ${route.xsection}
|
||||||
family: ${route.family}
|
family: ${route.family}
|
||||||
width: ${Number(routeWidth)}
|
width: ${Number(routeWidth)}
|
||||||
radius: ${Number(route.radius)}
|
radius: ${Number(route.radius)}
|
||||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
routing_type: ${route.routing_type}${pointsYaml}`
|
||||||
}
|
: ` - from: ${sourceName}:${fromPort}
|
||||||
return ` - from: ${sourceName}:${fromPort}
|
|
||||||
to: ${targetName}:${toPort}
|
to: ${targetName}:${toPort}
|
||||||
xsection: ${route.xsection}
|
xsection: ${route.xsection}
|
||||||
family: ${route.family}
|
family: ${route.family}
|
||||||
width: ${Number(routeWidth)}
|
width: ${Number(routeWidth)}
|
||||||
radius: ${Number(route.radius)}
|
radius: ${Number(route.radius)}
|
||||||
routing_type: ${route.routing_type}${pointsYaml}`;
|
routing_type: ${route.routing_type}${pointsYaml}`;
|
||||||
});
|
const routeGroupName = normalizeBundleGroupName(route.bundle_group, '');
|
||||||
linksYaml = linkLines.join('\n');
|
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)
|
return `# 3. Bundles (Grouped links for multi-bus/parallel routing)
|
||||||
bundles:
|
bundles:${groupsYaml ? `\n${groupsYaml}` : ' {}'}`;
|
||||||
output_bus:
|
|
||||||
routing_type: euler_bend
|
|
||||||
links:
|
|
||||||
${linksYaml}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the center point of a node when a more precise port point is unavailable.
|
// Return the center point of a node when a more precise port point is unavailable.
|
||||||
@@ -1252,6 +1297,7 @@ ${linksYaml}`;
|
|||||||
BASIC_COMPONENTS,
|
BASIC_COMPONENTS,
|
||||||
DEFAULT_FORGE_ARGUMENTS,
|
DEFAULT_FORGE_ARGUMENTS,
|
||||||
FALLBACK_TECHNOLOGY_MANIFEST,
|
FALLBACK_TECHNOLOGY_MANIFEST,
|
||||||
|
FREE_WIRES_BUNDLE_GROUP,
|
||||||
canvasToLayoutY,
|
canvasToLayoutY,
|
||||||
layoutToCanvasY,
|
layoutToCanvasY,
|
||||||
createForgeArguments,
|
createForgeArguments,
|
||||||
@@ -1259,6 +1305,8 @@ ${linksYaml}`;
|
|||||||
updateRouteField,
|
updateRouteField,
|
||||||
updateRouteXsection,
|
updateRouteXsection,
|
||||||
routeStyleForSettings,
|
routeStyleForSettings,
|
||||||
|
normalizeBundleGroupName,
|
||||||
|
freeWireBundleGroupName,
|
||||||
findSameTypeRouteCrossing,
|
findSameTypeRouteCrossing,
|
||||||
findSameFamilyRouteCrossing,
|
findSameFamilyRouteCrossing,
|
||||||
isForgeComponent,
|
isForgeComponent,
|
||||||
|
|||||||
+183
-70
@@ -1580,6 +1580,9 @@ Organization : OptiHK Limited
|
|||||||
updateRouteField,
|
updateRouteField,
|
||||||
updateRouteXsection,
|
updateRouteXsection,
|
||||||
routeStyleForSettings,
|
routeStyleForSettings,
|
||||||
|
FREE_WIRES_BUNDLE_GROUP,
|
||||||
|
normalizeBundleGroupName,
|
||||||
|
freeWireBundleGroupName,
|
||||||
findSameTypeRouteCrossing,
|
findSameTypeRouteCrossing,
|
||||||
createRulerMeasurement,
|
createRulerMeasurement,
|
||||||
createComponentSymbolMetrics,
|
createComponentSymbolMetrics,
|
||||||
@@ -1589,6 +1592,15 @@ Organization : OptiHK Limited
|
|||||||
|
|
||||||
const FULL_SELECTION_MODE = SelectionMode && SelectionMode.Full ? SelectionMode.Full : 'full';
|
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 = {};
|
const iconPromiseCache = {};
|
||||||
// Loads and caches category icons so repeated library renders do not refetch the same image.
|
// 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.
|
// 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 [componentData, setComponentData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [enlarged, setEnlarged] = useState(null);
|
const [enlarged, setEnlarged] = useState(null);
|
||||||
const [editingComponentName, setEditingComponentName] = useState(false);
|
const [editingComponentName, setEditingComponentName] = useState(false);
|
||||||
const [tempComponentName, setTempComponentName] = useState('');
|
const [tempComponentName, setTempComponentName] = useState('');
|
||||||
|
const [newBundleGroupName, setNewBundleGroupName] = useState('');
|
||||||
const [localX, setLocalX] = useState('');
|
const [localX, setLocalX] = useState('');
|
||||||
const [localY, setLocalY] = useState('');
|
const [localY, setLocalY] = useState('');
|
||||||
const [localRotation, setLocalRotation] = useState('');
|
const [localRotation, setLocalRotation] = useState('');
|
||||||
@@ -3046,9 +3059,51 @@ Organization : OptiHK Limited
|
|||||||
family: mixedValue('family'),
|
family: mixedValue('family'),
|
||||||
width: mixedValue('width'),
|
width: mixedValue('width'),
|
||||||
radius: mixedValue('radius'),
|
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 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 (
|
return (
|
||||||
<aside style={{
|
<aside style={{
|
||||||
width: width, background: 'var(--bg-card)', borderLeft: '1px solid var(--border)',
|
width: width, background: 'var(--bg-card)', borderLeft: '1px solid var(--border)',
|
||||||
@@ -3064,7 +3119,14 @@ Organization : OptiHK Limited
|
|||||||
<label>XSection</label>
|
<label>XSection</label>
|
||||||
<select
|
<select
|
||||||
value={route.xsection}
|
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>}
|
{route.xsection === '__mixed__' && <option value="__mixed__" disabled>--</option>}
|
||||||
{xsections.map(xsection => (
|
{xsections.map(xsection => (
|
||||||
@@ -3072,6 +3134,44 @@ Organization : OptiHK Limited
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<br /><br />
|
<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>
|
<label>Width</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -3803,6 +3903,20 @@ Organization : OptiHK Limited
|
|||||||
const selectedEdge = selectedEdges[0] || null;
|
const selectedEdge = selectedEdges[0] || null;
|
||||||
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
|
const selectedNodes = useMemo(() => currentNodes.filter(n => n.selected), [currentNodes]);
|
||||||
const selectedNode = selectedNodes[0] || null;
|
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 linkXsectionChoices = useMemo(() => {
|
||||||
const manifestSections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
|
const manifestSections = Object.keys((technologyManifest || FALLBACK_TECHNOLOGY_MANIFEST).xsections || {});
|
||||||
const preferred = ['strip', 'rib_low', 'metal_1', 'metal_2'];
|
const preferred = ['strip', 'rib_low', 'metal_1', 'metal_2'];
|
||||||
@@ -3813,7 +3927,13 @@ Organization : OptiHK Limited
|
|||||||
return ordered.length ? ordered : preferred;
|
return ordered.length ? ordered : preferred;
|
||||||
}, [technologyManifest]);
|
}, [technologyManifest]);
|
||||||
const currentLinkRoute = useMemo(
|
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]
|
[technologyManifest, currentLinkXsection]
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -4805,40 +4925,36 @@ Organization : OptiHK Limited
|
|||||||
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
||||||
|
|
||||||
if (!isProject) {
|
if (!isProject) {
|
||||||
const links = doc.bundles?.output_bus?.links;
|
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
||||||
if (links) {
|
const route = createRouteSettings(technologyManifest, { ...bundle, ...link, bundle_group: bundleName });
|
||||||
const linkArray = Array.isArray(links) ? links : [links];
|
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
|
||||||
linkArray.forEach(link => {
|
if (link.from && link.to) {
|
||||||
const route = createRouteSettings(technologyManifest, link);
|
const [fromInst, fromPort] = link.from.split(':');
|
||||||
const routePoints = normalizeRoutePoints(link.points, doc.coordinate_system === 'gds_y_up');
|
const [toInst, toPort] = link.to.split(':');
|
||||||
if (link.from && link.to) {
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const [fromInst, fromPort] = link.from.split(':');
|
const targetId = nodeNameMap[toInst];
|
||||||
const [toInst, toPort] = link.to.split(':');
|
if (sourceId && targetId) {
|
||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceNode = newNodes.find(node => node.id === sourceId);
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetNode = newNodes.find(node => node.id === targetId);
|
||||||
if (sourceId && targetId) {
|
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||||
const sourceNode = newNodes.find(node => node.id === sourceId);
|
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||||
const targetNode = newNodes.find(node => node.id === targetId);
|
const view = routeStyleForSettings(route, false);
|
||||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
newEdges.push({
|
||||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||||
const view = routeStyleForSettings(route, false);
|
source: sourceId,
|
||||||
newEdges.push({
|
target: targetId,
|
||||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
sourceHandle,
|
||||||
source: sourceId,
|
targetHandle,
|
||||||
target: targetId,
|
type: view.type,
|
||||||
sourceHandle,
|
style: view.style,
|
||||||
targetHandle,
|
data: { route, points: routePoints },
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
} 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);
|
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));
|
nodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
|
||||||
|
|
||||||
const links = doc.bundles?.output_bus?.links;
|
forEachBundleLink(doc, (bundleName, bundle, link) => {
|
||||||
if (links) {
|
const route = createRouteSettings(manifest, { ...bundle, ...link, bundle_group: bundleName });
|
||||||
const linkArray = Array.isArray(links) ? links : [links];
|
const routePoints = normalizeRoutePoints(link.points, usesGdsYUp);
|
||||||
linkArray.forEach(link => {
|
if (link.from && link.to) {
|
||||||
const route = createRouteSettings(manifest, link);
|
const [fromInst, fromPort] = link.from.split(':');
|
||||||
const routePoints = normalizeRoutePoints(link.points, usesGdsYUp);
|
const [toInst, toPort] = link.to.split(':');
|
||||||
if (link.from && link.to) {
|
const sourceId = nodeNameMap[fromInst];
|
||||||
const [fromInst, fromPort] = link.from.split(':');
|
const targetId = nodeNameMap[toInst];
|
||||||
const [toInst, toPort] = link.to.split(':');
|
if (!sourceId || !targetId) return;
|
||||||
const sourceId = nodeNameMap[fromInst];
|
const sourceNode = nodes.find(node => node.id === sourceId);
|
||||||
const targetId = nodeNameMap[toInst];
|
const targetNode = nodes.find(node => node.id === targetId);
|
||||||
if (!sourceId || !targetId) return;
|
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||||
const sourceNode = nodes.find(node => node.id === sourceId);
|
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||||
const targetNode = nodes.find(node => node.id === targetId);
|
const view = routeStyleForSettings(route, false);
|
||||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
edges.push({
|
||||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||||
const view = routeStyleForSettings(route, false);
|
source: sourceId,
|
||||||
edges.push({
|
target: targetId,
|
||||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
sourceHandle,
|
||||||
source: sourceId,
|
targetHandle,
|
||||||
target: targetId,
|
type: view.type,
|
||||||
sourceHandle,
|
style: view.style,
|
||||||
targetHandle,
|
data: { route, points: routePoints },
|
||||||
type: view.type,
|
});
|
||||||
style: view.style,
|
} else if (routePoints.length >= 2) {
|
||||||
data: { route, points: routePoints },
|
const edgeId = link.id || `route-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||||
});
|
edges.push(makeFreeRouteEdge(edgeId, routePoints, route));
|
||||||
} 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 {
|
return {
|
||||||
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
id: `cell-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||||
@@ -6641,6 +6753,7 @@ ${bundlesBlock}`;
|
|||||||
selectedNodes={selectedNodes}
|
selectedNodes={selectedNodes}
|
||||||
selectedEdge={selectedEdge}
|
selectedEdge={selectedEdge}
|
||||||
selectedEdges={selectedEdges}
|
selectedEdges={selectedEdges}
|
||||||
|
bundleGroupOptions={bundleGroupOptions}
|
||||||
technologyManifest={technologyManifest}
|
technologyManifest={technologyManifest}
|
||||||
projectName={currentProjectName}
|
projectName={currentProjectName}
|
||||||
compositeNames={compositePageNames}
|
compositeNames={compositePageNames}
|
||||||
|
|||||||
@@ -63,3 +63,20 @@ assert(
|
|||||||
!canvasHtml.includes("activePage.nodes.filter(n => n.selected && n.id !== 'page-port')"),
|
!canvasHtml.includes("activePage.nodes.filter(n => n.selected && n.id !== 'page-port')"),
|
||||||
'copy/delete should not exclude port nodes'
|
'copy/delete should not exclude port nodes'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('Bundle Group') &&
|
||||||
|
canvasHtml.includes('bundleGroupOptions') &&
|
||||||
|
canvasHtml.includes('compatibleBundleGroupOptions'),
|
||||||
|
'route editor should expose a Bundle Group dropdown filtered by compatible xsection'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('newBundleGroupName') &&
|
||||||
|
canvasHtml.includes('normalizeBundleGroupName') &&
|
||||||
|
canvasHtml.includes('onAddBundleGroup'),
|
||||||
|
'route editor should provide an add flow that sanitizes new bundle group names'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('routeStyleForSettings({ xsection: option.xsection') ||
|
||||||
|
canvasHtml.includes('routeStyleForSettings({ xsection: group.xsection'),
|
||||||
|
'bundle group dropdown options should use route xsection colors'
|
||||||
|
);
|
||||||
|
|||||||
@@ -588,9 +588,13 @@ assert.deepStrictEqual(routeDefaults, {
|
|||||||
width: 0.45,
|
width: 0.45,
|
||||||
radius: 10,
|
radius: 10,
|
||||||
routing_type: 'euler_bend',
|
routing_type: 'euler_bend',
|
||||||
|
bundle_group: '',
|
||||||
widthEdited: false
|
widthEdited: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupedRouteDefaults = helpers.createRouteSettings(technologyManifest, { bundle_group: 'group_A' });
|
||||||
|
assert.strictEqual(groupedRouteDefaults.bundle_group, 'group_A');
|
||||||
|
|
||||||
const metalRoute = helpers.updateRouteXsection(routeDefaults, 'metal_1', technologyManifest);
|
const metalRoute = helpers.updateRouteXsection(routeDefaults, 'metal_1', technologyManifest);
|
||||||
assert.strictEqual(metalRoute.family, 'electrical');
|
assert.strictEqual(metalRoute.family, 'electrical');
|
||||||
assert.strictEqual(metalRoute.width, 5);
|
assert.strictEqual(metalRoute.width, 5);
|
||||||
@@ -692,6 +696,90 @@ assert(freeRouteYaml.includes('points:'));
|
|||||||
assert(freeRouteYaml.includes('x: 80.0'));
|
assert(freeRouteYaml.includes('x: 80.0'));
|
||||||
assert(freeRouteYaml.includes('y: -120.0'));
|
assert(freeRouteYaml.includes('y: -120.0'));
|
||||||
|
|
||||||
|
const groupedBundlesYaml = helpers.buildBundlesYaml({
|
||||||
|
nodes: [
|
||||||
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
||||||
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } },
|
||||||
|
{ id: 'c', data: { componentDisplayName: 'inst_c' } },
|
||||||
|
{ id: 'd', data: { componentDisplayName: 'inst_d' } }
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'edge-group-a',
|
||||||
|
source: 'a',
|
||||||
|
target: 'b',
|
||||||
|
sourceHandle: 'out',
|
||||||
|
targetHandle: 'in',
|
||||||
|
data: {
|
||||||
|
route: {
|
||||||
|
xsection: 'strip',
|
||||||
|
family: 'optical',
|
||||||
|
width: 0.45,
|
||||||
|
radius: 10,
|
||||||
|
routing_type: 'euler_bend',
|
||||||
|
bundle_group: 'optical_bus'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edge-group-b',
|
||||||
|
source: 'c',
|
||||||
|
target: 'd',
|
||||||
|
sourceHandle: 'out',
|
||||||
|
targetHandle: 'in',
|
||||||
|
data: {
|
||||||
|
route: {
|
||||||
|
xsection: 'metal_1',
|
||||||
|
family: 'electrical',
|
||||||
|
width: 5,
|
||||||
|
radius: 20,
|
||||||
|
routing_type: 'standard_bend',
|
||||||
|
bundle_group: 'electrical_bus'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, technologyManifest);
|
||||||
|
assert(groupedBundlesYaml.includes(' optical_bus:\n xsection: strip\n family: optical\n routing_type: euler_bend\n links:'));
|
||||||
|
assert(groupedBundlesYaml.includes(' electrical_bus:\n xsection: metal_1\n family: electrical\n routing_type: standard_bend\n links:'));
|
||||||
|
assert(groupedBundlesYaml.includes('from: inst_a:out'));
|
||||||
|
assert(groupedBundlesYaml.includes('from: inst_c:out'));
|
||||||
|
assert(!groupedBundlesYaml.includes('bundle_group:'), 'bundle_group should choose the YAML key, not be written inside links');
|
||||||
|
|
||||||
|
const splitFreeWireBundlesYaml = helpers.buildBundlesYaml({
|
||||||
|
nodes: [
|
||||||
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
||||||
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } },
|
||||||
|
{ id: 'c', data: { componentDisplayName: 'inst_c' } },
|
||||||
|
{ id: 'd', data: { componentDisplayName: 'inst_d' } }
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'edge-free-strip',
|
||||||
|
source: 'a',
|
||||||
|
target: 'b',
|
||||||
|
sourceHandle: 'out',
|
||||||
|
targetHandle: 'in',
|
||||||
|
data: {
|
||||||
|
route: { xsection: 'strip', family: 'optical', width: 0.45, radius: 10, routing_type: 'euler_bend' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edge-free-metal',
|
||||||
|
source: 'c',
|
||||||
|
target: 'd',
|
||||||
|
sourceHandle: 'out',
|
||||||
|
targetHandle: 'in',
|
||||||
|
data: {
|
||||||
|
route: { xsection: 'metal_1', family: 'electrical', width: 5, radius: 20, routing_type: 'standard_bend' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, technologyManifest);
|
||||||
|
assert(splitFreeWireBundlesYaml.includes(' free_wires:\n xsection: strip\n family: optical\n routing_type: euler_bend\n links:'));
|
||||||
|
assert(splitFreeWireBundlesYaml.includes(' free_wires_metal_1:\n xsection: metal_1\n family: electrical\n routing_type: standard_bend\n links:'));
|
||||||
|
assert(!splitFreeWireBundlesYaml.includes('bundle_group:'), 'free-wire bundle names should not be duplicated into link metadata');
|
||||||
|
|
||||||
const edgeA = {
|
const edgeA = {
|
||||||
id: 'edge-a-b',
|
id: 'edge-a-b',
|
||||||
source: 'a',
|
source: 'a',
|
||||||
|
|||||||
@@ -47,3 +47,15 @@ assert(
|
|||||||
canvasHtml.includes('Array.from(new Set([FORGE_COMPONENT_LABEL, ...sameCategoryComponents'),
|
canvasHtml.includes('Array.from(new Set([FORGE_COMPONENT_LABEL, ...sameCategoryComponents'),
|
||||||
'loaded PDK selector choices should include forge and same-category library components'
|
'loaded PDK selector choices should include forge and same-category library components'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('Object.entries(doc.bundles || {})'),
|
||||||
|
'project and YAML loading should iterate all saved bundle groups, not only output_bus'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!canvasHtml.includes('doc.bundles?.output_bus?.links'),
|
||||||
|
'project and YAML loading should not hardcode bundles.output_bus.links'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
canvasHtml.includes('bundle_group: bundleName'),
|
||||||
|
'loaded route metadata should remember the YAML bundle key as route.bundle_group'
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user