1. Anchor routing added with mutiport

This commit is contained in:
2026-05-30 12:04:02 +08:00
parent 2d9b2b0983
commit 5a3a80700f
23 changed files with 1226 additions and 234 deletions
+109 -12
View File
@@ -10,6 +10,8 @@
const DEFAULT_COMPONENT_BOX_SIZE = { width: 132, height: 82 };
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
const PORT_NODE_SIZE = 30;
const ANCHOR_NODE_WIDTH = 8;
const DEFAULT_ELEMENT_PITCH = 10;
const ELEMENT_COMPONENTS = {
Port: {
name: 'Port',
@@ -22,8 +24,8 @@
name: 'Anchor',
elementType: 'anchor',
ports: {
left: { x: 0, y: -PORT_NODE_SIZE / 2, a: 180, width: 0.5 },
right: { x: PORT_NODE_SIZE, y: -PORT_NODE_SIZE / 2, a: 0, width: 0.5 }
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 }
}
}
};
@@ -514,19 +516,64 @@
const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode');
const isElementNode = (node) => node && node.data && (node.data.elementType === 'port' || node.data.elementType === 'anchor');
const normalizePortNumber = (value) => {
const number = Math.floor(Number(value));
return Number.isFinite(number) ? Math.max(1, number) : 1;
};
const normalizePitch = (value) => {
const number = Number(value);
return Number.isFinite(number) ? Math.max(0, number) : DEFAULT_ELEMENT_PITCH;
};
const elementPortOffset = (index, count, pitch) => ((count - 1) / 2 - index) * pitch;
const buildElementBoxSize = (data) => {
const portNumber = normalizePortNumber(data && data.portNumber);
const pitch = normalizePitch(data && data.pitch);
const handleClearance = Math.max(pitch, 14);
return {
width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : PORT_NODE_SIZE,
height: Math.max(PORT_NODE_SIZE, PORT_NODE_SIZE + Math.max(0, portNumber - 1) * handleClearance)
};
};
const buildElementPorts = (elementType, data) => {
const element = ELEMENT_COMPONENTS[elementType === 'anchor' ? 'Anchor' : 'Port'];
if (!element) return {};
const portNumber = normalizePortNumber(data && data.portNumber);
const pitch = normalizePitch(data && data.pitch);
const width = Number((data && data.width) || 0.5);
if (element.elementType === 'port') {
if (portNumber > 1) {
return Object.fromEntries(Array.from({ length: portNumber }, (_, index) => [
`port_${index + 1}`,
{
x: 0,
y: elementPortOffset(index, portNumber, pitch),
a: Number((data && (data.angle ?? data.a)) ?? 0),
width
}
]));
}
return {
port: {
x: 0,
y: 0,
a: Number((data && (data.angle ?? data.a)) ?? 0),
width: Number((data && data.width) || 0.5)
width
}
};
}
if (portNumber > 1) {
const entries = [];
Array.from({ length: portNumber }, (_, index) => {
const y = -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 }]);
});
return Object.fromEntries(entries);
}
return JSON.parse(JSON.stringify(element.ports));
};
@@ -598,12 +645,24 @@
if (portNodes.length > 0) {
return portNodes.reduce((ports, node) => {
const data = node.data || {};
ports[getNodePortName(node)] = {
x: Number((node.position && node.position.x) || 0),
y: Number((node.position && node.position.y) || 0),
a: Number(data.angle ?? data.a ?? 0),
width: Number(data.width || 0.5)
};
const baseName = getNodePortName(node);
const elementPorts = buildElementPorts('port', data);
const entries = Object.entries(elementPorts);
entries.forEach(([portName, portInfo]) => {
const exportName = entries.length === 1
? baseName
: `${baseName}_${portName.replace(/^port_/, '')}`;
const point = getNodePortCanvasPoint(node, portName) || {
x: Number((node.position && node.position.x) || 0),
y: Number((node.position && node.position.y) || 0)
};
ports[exportName] = {
x: Number(point.x || 0),
y: Number(point.y || 0),
a: Number(portInfo.a ?? data.angle ?? data.a ?? 0),
width: Number(portInfo.width || data.width || 0.5)
};
});
return ports;
}, {});
}
@@ -645,11 +704,15 @@
const data = node.data || {};
const name = data.componentDisplayName || data.portName || node.id;
const angle = data.elementType === 'port' ? data.angle : data.rotation;
const portNumber = normalizePortNumber(data.portNumber);
const pitch = normalizePitch(data.pitch);
return ` ${name}:
type: ${data.elementType}
x: ${Number((node.position && node.position.x) || 0).toFixed(1)}
y: ${canvasToLayoutY((node.position && node.position.y) || 0).toFixed(1)}
angle: ${Number(angle || 0).toFixed(1)}
port_number: ${portNumber}
pitch: ${Number(pitch)}
layer: ${data.layer || 'WG_CORE'}
width: ${Number(data.width || 0.5)}
description: ${toYamlScalar(data.description || '')}`;
@@ -718,7 +781,28 @@ ${linksYaml}`;
const x = Number((node.position && node.position.x) || 0);
const y = Number((node.position && node.position.y) || 0);
if (node.type === 'portNode' || (node.data && node.data.elementType === 'port')) {
return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
const ports = buildElementPorts('port', node.data);
const portInfo = ports && portName ? ports[portName] : ports.port;
if (!portInfo) return { x: roundMeasureValue(x), y: roundMeasureValue(y) };
const transformedInfo = transformPortInfo(portInfo, { rotation: 0 });
return {
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
};
}
if (node.type === 'anchorNode' || (node.data && node.data.elementType === 'anchor')) {
const ports = buildElementPorts('anchor', node.data);
const portInfo = ports && portName ? ports[portName] : null;
if (!portInfo) return null;
const transformedInfo = transformPortInfo(portInfo, {
rotation: (node.data && node.data.rotation) || 0,
flip: Boolean(node.data && node.data.flip),
flop: Boolean(node.data && node.data.flop)
});
return {
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
};
}
const ports = node.data && node.data.ports;
const portInfo = ports && portName ? ports[portName] : null;
@@ -758,7 +842,9 @@ ${linksYaml}`;
});
const handle = handles.find(item => item.name === handleId);
if (handle) {
const componentSize = normalizeBoxSize({ box_size: node.data && node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
const componentSize = node.data && node.data.elementType
? buildElementBoxSize(node.data)
: normalizeBoxSize({ box_size: node.data && node.data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
let x = Number((node.position && node.position.x) || 0);
let y = Number((node.position && node.position.y) || 0);
if (handle.position === 'left') {
@@ -825,14 +911,23 @@ ${linksYaml}`;
return o1 !== o2 && o3 !== o4;
};
const routeTypeKey = (route) => {
const xsection = String((route && route.xsection) || '').trim().toLowerCase();
if (xsection === 'metal1') return 'metal_1';
if (xsection === 'metal2') return 'metal_2';
if (xsection === 'rib') return 'rib_low';
return xsection;
};
const findSameTypeRouteCrossing = (candidateEdge, existingEdges, nodeMap, manifest) => {
const candidateRoute = createRouteSettings(manifest, candidateEdge.data && candidateEdge.data.route);
const candidateType = routeTypeKey(candidateRoute);
const candidatePoints = getEdgeRoutePoints(candidateEdge, nodeMap);
for (const edge of existingEdges || []) {
if (!edge || edge.id === candidateEdge.id) continue;
if (edge.source === candidateEdge.source || edge.source === candidateEdge.target || edge.target === candidateEdge.source || edge.target === candidateEdge.target) continue;
const route = createRouteSettings(manifest, edge.data && edge.data.route);
if (route.xsection !== candidateRoute.xsection) continue;
if (routeTypeKey(route) !== candidateType) continue;
const points = getEdgeRoutePoints(edge, nodeMap);
if (routeSegmentsIntersect(candidatePoints, points)) {
return { conflictEdge: edge, xsection: route.xsection };
@@ -849,6 +944,7 @@ ${linksYaml}`;
DEFAULT_COMPONENT_BOX_SIZE,
DEFAULT_CANVAS_SIZE,
PORT_NODE_SIZE,
DEFAULT_ELEMENT_PITCH,
ELEMENT_COMPONENTS,
BASIC_COMPONENTS,
DEFAULT_FORGE_ARGUMENTS,
@@ -878,6 +974,7 @@ ${linksYaml}`;
getNodePortCanvasPoint,
buildPortHandles,
buildElementPorts,
buildElementBoxSize,
buildBasicComponentPorts,
getBasicComponentMetadata,
buildInstanceYaml,
+303 -38
View File
@@ -1433,6 +1433,7 @@
DEFAULT_COMPONENT_BOX_SIZE,
DEFAULT_CANVAS_SIZE,
PORT_NODE_SIZE,
DEFAULT_ELEMENT_PITCH,
ELEMENT_COMPONENTS,
BASIC_COMPONENTS,
createForgeArguments,
@@ -1446,6 +1447,7 @@
calculateLayoutBounds,
buildPortHandles,
buildElementPorts,
buildElementBoxSize,
getBasicComponentMetadata,
buildInstancesYaml,
buildPageComponentPorts,
@@ -1703,10 +1705,29 @@
const PortNode = ({ id, data, selected }) => {
const angle = data.angle ?? 0;
const handleId = data.portName || data.componentDisplayName || 'port';
const ports = buildElementPorts('port', data);
const elementSize = buildElementBoxSize(data);
const localHandlePorts = Object.fromEntries(
Object.entries(ports).map(([name, info]) => [name, { ...info, a: 0 }])
);
const portHandles = useMemo(
() => buildPortHandles(localHandlePorts, { rotation: 0 }),
[localHandlePorts]
);
const handlePositionMap = {
left: Position.Left,
right: Position.Right,
top: Position.Top,
bottom: Position.Bottom
};
const baseHandleStyle = {
background: 'var(--accent)',
width: 8,
height: 8
};
return (
<div style={{
width: PORT_NODE_SIZE, height: PORT_NODE_SIZE, borderRadius: '50%',
width: elementSize.width, height: elementSize.height, borderRadius: 999,
background: selected ? 'var(--accent)' : 'var(--bg-card)',
border: selected ? '2px solid white' : '2px solid var(--accent)',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
@@ -1716,18 +1737,32 @@
transform: `rotate(${angle}deg)`,
}}>
<span>P</span>
<Handle type="source" position={Position.Right} id={handleId} style={{ background: 'var(--accent)', width: 8, height: 8 }} />
<Handle type="target" position={Position.Right} id={handleId} style={{ background: 'var(--accent)', width: 8, height: 8 }} />
{portHandles.map((portHandle) => (
<React.Fragment key={portHandle.name}>
<Handle type="source" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
<Handle type="target" position={handlePositionMap[portHandle.position]} id={portHandle.name} style={{ ...baseHandleStyle, ...portHandle.style }} />
</React.Fragment>
))}
</div>
);
};
const AnchorNode = memo(({ id, data, selected }) => {
const updateNodeInternals = useUpdateNodeInternals();
const ports = data.ports || buildElementPorts('anchor');
const anchorRotation = data.rotation || 0;
const anchorVisualRotation = -Number(anchorRotation || 0);
const ports = buildElementPorts('anchor', data);
const elementSize = buildElementBoxSize(data);
const localAnchorHandlePorts = Object.fromEntries(
Object.entries(ports).map(([name, info]) => [name, { ...info, a: name.startsWith('a') || name.startsWith('left') ? 180 : 0 }])
);
const portHandles = useMemo(
() => buildPortHandles(ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
[ports, data.rotation, data.flip, data.flop]
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
[localAnchorHandlePorts, data.flip, data.flop]
);
const anchorDirectionHandles = useMemo(
() => new Map(buildPortHandles(localAnchorHandlePorts, { rotation: Number(anchorRotation || 0), flip: Boolean(data.flip), flop: Boolean(data.flop) }).map(handle => [handle.name, handle.position])),
[localAnchorHandlePorts, anchorRotation, data.flip, data.flop]
);
const handlePositionMap = {
left: Position.Left,
@@ -1742,6 +1777,32 @@
border: '1px solid var(--bg-main)',
borderRadius: '50%'
};
const anchorPortVisualSide = (portName) => {
const name = String(portName || '');
return name.startsWith('a') || name.startsWith('left') ? 'left' : 'right';
};
const anchorPortVisualTop = (portName) => {
const match = String(portName || '').match(/(\d+)$/);
const index = match ? Math.max(1, Number(match[1])) : 1;
const portCount = Math.max(1, Math.floor(Number(data.portNumber || 1)));
if (portCount <= 1) return elementSize.height / 2;
const travel = Math.max(0, elementSize.height - baseHandleStyle.height);
return baseHandleStyle.height / 2 + ((index - 1) / (portCount - 1)) * travel;
};
const anchorHandleVisualStyle = (portHandle, zIndex) => {
const visualSide = anchorPortVisualSide(portHandle.name);
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
const localTop = anchorPortVisualTop(portHandle.name);
return {
...baseHandleStyle,
zIndex,
left: localLeft,
top: localTop,
right: 'auto',
bottom: 'auto',
transform: 'translate(-50%, -50%)'
};
};
useEffect(() => {
updateNodeInternals(id);
@@ -1750,9 +1811,9 @@
return (
<div style={{
position: 'relative',
width: PORT_NODE_SIZE,
height: PORT_NODE_SIZE,
borderRadius: '50%',
width: elementSize.width,
height: elementSize.height,
borderRadius: 999,
background: selected ? 'var(--accent)' : 'var(--bg-card)',
border: selected ? '2px solid white' : '2px solid var(--accent)',
display: 'flex',
@@ -1762,23 +1823,24 @@
fontSize: 10,
fontWeight: 800,
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
transform: `rotate(${anchorVisualRotation}deg)`,
}}>
<span>A</span>
{portHandles.map((portHandle) => (
<React.Fragment key={portHandle.name}>
<Handle
type="source"
position={handlePositionMap[portHandle.position]}
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
id={portHandle.name}
title={portHandle.name}
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10 }}
style={anchorHandleVisualStyle(portHandle, 10)}
/>
<Handle
type="target"
position={handlePositionMap[portHandle.position]}
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
id={portHandle.name}
title={portHandle.name}
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5 }}
style={anchorHandleVisualStyle(portHandle, 5)}
/>
</React.Fragment>
))}
@@ -1814,9 +1876,25 @@
</div>
));
const routeDirectionVector = (direction) => {
if (direction === 'left') return { x: -1, y: 0 };
if (direction === 'right') return { x: 1, y: 0 };
if (direction === 'top') return { x: 0, y: -1 };
if (direction === 'bottom') return { x: 0, y: 1 };
return null;
};
const directionToReactFlowPosition = (direction) => {
if (direction === 'left') return Position.Left;
if (direction === 'right') return Position.Right;
if (direction === 'top') return Position.Top;
if (direction === 'bottom') return Position.Bottom;
return undefined;
};
const ParallelRouteEdge = memo(({ id, sourceX, sourceY, targetX, targetY, markerEnd, style, selected, data }) => {
const offset = Number(data?.parallelOffset || 0);
let rawPoints = Array.isArray(data?.points) && data.points.length >= 2
const hasExplicitPoints = Array.isArray(data?.points) && data.points.length >= 2;
let rawPoints = hasExplicitPoints
? data.points.map(point => ({ x: Number(point.x), y: Number(point.y) })).filter(point => Number.isFinite(point.x) && Number.isFinite(point.y))
: [{ x: sourceX, y: sourceY }, { x: targetX, y: targetY }];
if (!data?.freeRoute && rawPoints.length >= 2) {
@@ -1828,6 +1906,20 @@
{ x: Number(targetPoint.x), y: Number(targetPoint.y) }
];
}
const sourceVector = routeDirectionVector(data?.sourceDirection);
const targetVector = routeDirectionVector(data?.targetDirection);
if (!hasExplicitPoints && (sourceVector || targetVector) && rawPoints.length >= 2) {
const stubLength = Math.min(48, Math.max(18, Math.hypot(targetX - sourceX, targetY - sourceY) / 4));
const directedPoints = [rawPoints[0]];
if (sourceVector) {
directedPoints.push({ x: rawPoints[0].x + sourceVector.x * stubLength, y: rawPoints[0].y + sourceVector.y * stubLength });
}
if (targetVector) {
directedPoints.push({ x: rawPoints[1].x + targetVector.x * stubLength, y: rawPoints[1].y + targetVector.y * stubLength });
}
directedPoints.push(rawPoints[1]);
rawPoints = directedPoints;
}
const firstPoint = rawPoints[0] || { x: sourceX, y: sourceY };
const lastPoint = rawPoints[rawPoints.length - 1] || { x: targetX, y: targetY };
const dx = lastPoint.x - firstPoint.x;
@@ -2543,7 +2635,7 @@
if (selectedNode) {
setLocalX(selectedNode.position.x.toFixed(3));
setLocalY(selectedNode.position.y.toFixed(3));
const rot = selectedNode.id === 'page-port'
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
? (selectedNode.data?.angle ?? 0)
: (selectedNode.data?.rotation ?? 0);
setLocalRotation(rot.toFixed(3));
@@ -2722,12 +2814,19 @@
const updatePortField = (key, value, type = 'text') => {
if (!selectedNode) return;
const nextValue = type === 'number' ? Number(value || 0) : value;
let nextValue = type === 'number' ? Number(value || 0) : value;
if (key === 'portNumber') nextValue = Math.max(1, Math.floor(nextValue || 1));
if (key === 'pitch') nextValue = Math.max(0, Number(nextValue || 0));
const dataUpdate = { [key]: nextValue };
if (key === 'portName') {
dataUpdate.componentDisplayName = value || selectedNode.data?.componentDisplayName;
dataUpdate.label = value || selectedNode.data?.label;
}
if (key === 'portNumber' || key === 'pitch' || key === 'width') {
const nextData = { ...selectedNode.data, ...dataUpdate };
dataUpdate.ports = buildElementPorts(selectedNode.data?.elementType === 'anchor' ? 'anchor' : 'port', nextData);
dataUpdate.boxSize = buildElementBoxSize(nextData);
}
onUpdateNode(selectedNode.id, { data: dataUpdate });
};
@@ -2819,7 +2918,7 @@
updateRotation(selectedNode.id, val, selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port');
setLocalRotation(val.toFixed(3));
} else if (selectedNode) {
const rot = selectedNode.id === 'page-port'
const rot = selectedNode.id === 'page-port' || selectedNode.type === 'portNode' || selectedNode.data?.elementType === 'port'
? (selectedNode.data?.angle ?? 0)
: (selectedNode.data?.rotation ?? 0);
setLocalRotation(rot.toFixed(3));
@@ -2893,6 +2992,24 @@
value={selectedNode.data?.width ?? 0.5}
onChange={(event) => updatePortField('width', event.target.value, 'number')}
/>
<br /><br />
<label>Port Number</label>
<input
type="number"
min="1"
step="1"
value={selectedNode.data?.portNumber ?? 1}
onChange={(event) => updatePortField('portNumber', event.target.value, 'number')}
/>
<br /><br />
<label>Pitch</label>
<input
type="number"
min="0"
step="1"
value={selectedNode.data?.pitch ?? DEFAULT_ELEMENT_PITCH}
onChange={(event) => updatePortField('pitch', event.target.value, 'number')}
/>
</div>
</div>
)}
@@ -2914,6 +3031,24 @@
value={selectedNode.data?.description || ''}
onChange={(event) => onUpdateNode(selectedNode.id, { data: { description: event.target.value } })}
/>
<br /><br />
<label>Port Number</label>
<input
type="number"
min="1"
step="1"
value={selectedNode.data?.portNumber ?? 1}
onChange={(event) => updatePortField('portNumber', event.target.value, 'number')}
/>
<br /><br />
<label>Pitch</label>
<input
type="number"
min="0"
step="1"
value={selectedNode.data?.pitch ?? DEFAULT_ELEMENT_PITCH}
onChange={(event) => updatePortField('pitch', event.target.value, 'number')}
/>
</div>
</div>
)}
@@ -3473,8 +3608,18 @@
style: { width: activeCanvasSize.width, height: activeCanvasSize.height, zIndex: -1, pointerEvents: 'none' }
}, ...currentNodes, ...freeRouteEndpointNodes, ...rulerNodes];
}, [activePage, currentNodes, activeCanvasSize, freeRouteEndpointNodes, rulerNodes]);
const getAnchorHandleRouteDirection = useCallback((node, handleId) => {
if (!node || !handleId || !(node.type === 'anchorNode' || node.data?.elementType === 'anchor')) return null;
const handles = buildPortHandles(buildElementPorts('anchor', node.data), {
rotation: Number(node.data?.rotation || 0),
flip: Boolean(node.data?.flip),
flop: Boolean(node.data?.flop)
});
return handles.find(handle => handle.name === handleId)?.position || null;
}, []);
const renderEdges = useMemo(() => {
const groups = new Map();
const nodeMap = Object.fromEntries(currentNodes.map(node => [node.id, node]));
currentEdges.forEach(edge => {
const sourceEndpoint = `${edge.source}:${edge.sourceHandle || ''}`;
const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`;
@@ -3487,11 +3632,22 @@
const targetEndpoint = `${edge.target}:${edge.targetHandle || ''}`;
const key = [sourceEndpoint, targetEndpoint].sort().join('<>');
const group = groups.get(key) || [];
if (group.length <= 1 && !(edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2)) return edge;
const sourceDirection = getAnchorHandleRouteDirection(nodeMap[edge.source], edge.sourceHandle);
const targetDirection = getAnchorHandleRouteDirection(nodeMap[edge.target], edge.targetHandle);
const usesAnchorDirection = Boolean(sourceDirection || targetDirection);
const hasRoutePoints = edge.data && Array.isArray(edge.data.points) && edge.data.points.length >= 2;
const directionalEdge = usesAnchorDirection
? {
...edge,
sourcePosition: directionToReactFlowPosition(sourceDirection),
targetPosition: directionToReactFlowPosition(targetDirection)
}
: edge;
if (group.length <= 1 && !hasRoutePoints) return directionalEdge;
const index = group.indexOf(edge.id);
const offset = (index - (group.length - 1) / 2) * 18;
return {
...edge,
...directionalEdge,
type: 'parallelRoute',
data: {
...(edge.data || {}),
@@ -3500,7 +3656,7 @@
};
});
return [...separatedEdges, ...rulerEdges];
}, [currentEdges, rulerEdges]);
}, [currentEdges, currentNodes, getAnchorHandleRouteDirection, rulerEdges]);
const [projectCompositeMap, setProjectCompositeMap] = useState({});
const [standaloneComposites, setStandaloneComposites] = useState([]);
@@ -3748,7 +3904,15 @@
return {
...p,
nodes: p.nodes.map(node => {
if (node.id !== nodeId || node.type !== 'rotatableNode' || node.data?.elementType) return node;
if (node.id !== nodeId) return node;
if (node.type === 'portNode' || node.data?.elementType === 'port') {
return { ...node, data: { ...node.data, angle: normalizeAngle(Number(node.data?.angle || 0) + 90) } };
}
if (node.type === 'anchorNode' || node.data?.elementType === 'anchor') {
const rotation = normalizeAngle(Number(node.data?.rotation || 0) + 90);
return { ...node, data: { ...node.data, rotation } };
}
if (node.type !== 'rotatableNode') return node;
const rotation = normalizeAngle(Number(node.data?.rotation || 0) + 90);
return { ...node, data: { ...node.data, rotation } };
})
@@ -3759,12 +3923,14 @@
const getSpaceRotationTarget = useCallback(() => {
if (spaceRotateNodeIdRef.current) return spaceRotateNodeIdRef.current;
const selectedSpaceNode = selectedNode;
if (!selectedSpaceNode || selectedSpaceNode.type !== 'rotatableNode' || selectedSpaceNode.data?.elementType) return null;
if (!selectedSpaceNode) return null;
if (selectedSpaceNode.type !== 'rotatableNode' && selectedSpaceNode.type !== 'portNode' && selectedSpaceNode.type !== 'anchorNode') return null;
return selectedSpaceNode.id;
}, [selectedNode]);
const onNodeMouseDown = useCallback((event, node) => {
if (event.button !== 0 || node.type !== 'rotatableNode' || node.data?.elementType) return;
if (event.button !== 0) return;
if (node.type !== 'rotatableNode' && node.type !== 'portNode' && node.type !== 'anchorNode') return;
spaceRotateNodeIdRef.current = node.id;
}, []);
@@ -4061,6 +4227,89 @@
return names;
}, []);
const getAvailableComponentsForLoadedComponent = useCallback((componentName) => {
if (!library || !componentName || isForgeComponent(componentName) || isBasicComponent(componentName)) return undefined;
const componentEntries = collectComponentNames(library);
const matchedComponent = componentEntries.find(component => component.name === componentName);
if (!matchedComponent) return undefined;
const sameCategoryComponents = componentEntries
.filter(component => component.category === matchedComponent.category)
.map(component => component.name)
.filter(Boolean);
return Array.from(new Set([FORGE_COMPONENT_LABEL, ...sameCategoryComponents, componentName]));
}, [library, collectComponentNames]);
const buildElementNodesFromYaml = useCallback((doc, usesGdsYUp, nodeNameMap = {}) => {
const nodes = [];
Object.entries(doc.elements || {}).forEach(([elementName, element]) => {
if (!element || typeof element !== 'object') return;
const elementType = element.type === 'anchor' ? 'anchor' : (element.type === 'port' ? 'port' : '');
if (!elementType) return;
if (elementType === 'port' && elementName === 'port' && Array.isArray(doc.ports) && doc.ports.length > 0) {
return;
}
const portNumberValue = Math.floor(Number(element.port_number ?? element.portNumber ?? 1));
const portNumber = Number.isFinite(portNumberValue) ? Math.max(1, portNumberValue) : 1;
const pitchValue = Number(element.pitch ?? DEFAULT_ELEMENT_PITCH);
const pitch = Number.isFinite(pitchValue) ? Math.max(0, pitchValue) : DEFAULT_ELEMENT_PITCH;
const widthValue = Number(element.width ?? 0.5);
const width = Number.isFinite(widthValue) ? widthValue : 0.5;
const xValue = Number(element.x || 0);
const yValue = Number(element.y || 0);
const x = Number.isFinite(xValue) ? xValue : 0;
const y = Number.isFinite(yValue) ? yValue : 0;
const baseData = {
label: elementName,
componentDisplayName: elementName,
elementType,
width,
portNumber,
pitch,
layer: element.layer || 'WG_CORE',
description: element.description || '',
boxSize: buildElementBoxSize({ elementType, portNumber, pitch })
};
const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
nodeNameMap[elementName] = nodeId;
if (elementType === 'port') {
const angle = Number(element.angle ?? element.a ?? 0);
nodes.push({
id: nodeId,
type: 'portNode',
position: {
x,
y: usesGdsYUp ? layoutToCanvasY(y) : y,
},
data: {
...baseData,
portName: elementName,
angle: Number.isFinite(angle) ? angle : 0,
ports: buildElementPorts('port', { angle: Number.isFinite(angle) ? angle : 0, width, portNumber, pitch })
},
});
return;
}
const rotation = Number(element.angle ?? element.rotation ?? 0);
nodes.push({
id: nodeId,
type: 'anchorNode',
position: {
x,
y: usesGdsYUp ? layoutToCanvasY(y) : y,
},
data: {
...baseData,
componentName: 'Anchor',
category: null,
rotation: Number.isFinite(rotation) ? rotation : 0,
hideIcon: true,
ports: buildElementPorts('anchor', { portNumber, pitch, width })
},
});
});
return nodes;
}, []);
useEffect(() => {
const input = document.getElementById('open-yaml-input');
if (!input) return;
@@ -4072,8 +4321,8 @@
const text = await file.text();
const doc = jsyaml.load(text);
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
if (!doc.instances) {
alert('no instances found');
if (!doc.instances && !doc.elements) {
alert('no instances or elements found');
return;
}
@@ -4081,14 +4330,18 @@
const newEdges = [];
const nodeNameMap = {};
const isProject = doc.type === 'project';
if (!isProject) {
nodeNameMap.port = 'page-port';
}
for (const [instName, inst] of Object.entries(doc.instances)) {
for (const [instName, inst] of Object.entries(doc.instances || {})) {
const compPath = inst.component || '';
const compName = compPath.split('/').pop();
const instIsForge = isForgeComponent(compPath) || isForgeComponent(compName);
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
let category = '';
if (!isProject && displayCompName && library && !instIsForge) {
@@ -4124,7 +4377,7 @@
flop: toBooleanFlag(inst.flop),
componentDisplayName: instName,
type: isProject ? 'composite' : undefined,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : undefined,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
@@ -4132,6 +4385,7 @@
},
});
}
newNodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
if (!isProject) {
const links = doc.bundles?.output_bus?.links;
@@ -4198,7 +4452,7 @@
if (isProject) {
setProjectCompositeMap(prev => ({
...prev,
[newPageName]: [...(prev[newPageName] || []), ...Object.keys(doc.instances)]
[newPageName]: [...(prev[newPageName] || []), ...Object.keys(doc.instances || {})]
}));
} else {
setStandaloneComposites(prev => {
@@ -4208,7 +4462,7 @@
if (library) {
const compTree = {};
for (const inst of Object.values(doc.instances)) {
for (const inst of Object.values(doc.instances || {})) {
const compPath = inst.component || '';
const compName = compPath.split('/').pop();
if (isForgeComponent(compPath) || isForgeComponent(compName)) continue;
@@ -4240,7 +4494,7 @@
input.addEventListener('change', handleFile);
return () => input.removeEventListener('change', handleFile);
}, [library, technologyManifest, makeFreeRouteEdge]);
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
useEffect(() => {
setProjectCompositeMap(prev => {
@@ -4307,6 +4561,7 @@
}
];
const edges = [];
nodeNameMap.port = 'page-port';
Object.entries(doc.instances || {}).forEach(([instName, inst]) => {
const compPath = inst.component || '';
@@ -4315,6 +4570,7 @@
const instIsBasic = isBasicComponent(compPath) || isBasicComponent(compName);
const displayCompName = instIsForge ? FORGE_COMPONENT_LABEL : (instIsBasic ? compPath : compName);
const basicMetadata = instIsBasic ? getBasicComponentMetadata(displayCompName, inst.settings) : null;
const loadedAvailableComponents = getAvailableComponentsForLoadedComponent(displayCompName);
const nodeId = `node-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
nodeNameMap[instName] = nodeId;
nodes.push({
@@ -4332,7 +4588,7 @@
flip: toBooleanFlag(inst.flip ?? inst.mirror),
flop: toBooleanFlag(inst.flop),
componentDisplayName: instName,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : undefined,
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
@@ -4340,6 +4596,7 @@
},
});
});
nodes.push(...buildElementNodesFromYaml(doc, usesGdsYUp, nodeNameMap));
const links = doc.bundles?.output_bus?.links;
if (links) {
@@ -4422,7 +4679,7 @@
};
loadProject();
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge]);
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
useEffect(() => {
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
@@ -4900,7 +5157,9 @@
const position = clampPositionToCanvas(
reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }),
activePage?.canvasSize || activeCanvasSize,
parsedData.type === 'element' ? { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE } : DEFAULT_COMPONENT_BOX_SIZE
parsedData.type === 'element'
? buildElementBoxSize({ elementType: parsedData.elementType === 'anchor' ? 'anchor' : 'port', portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH })
: DEFAULT_COMPONENT_BOX_SIZE
);
if (parsedData.type === 'basic') {
const componentName = parsedData.componentName || parsedData.name;
@@ -4946,14 +5205,18 @@
elementType: 'port',
angle: 0,
width: 0.5,
portNumber: 1,
pitch: DEFAULT_ELEMENT_PITCH,
layer: 'WG_CORE',
description: ''
description: '',
boxSize: buildElementBoxSize({ elementType: 'port', portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH }),
ports: buildElementPorts('port', { angle: 0, width: 0.5, portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH })
},
}
: {
id: Date.now().toString(),
type: 'anchorNode',
position: clampPositionToCanvas(position, activePage?.canvasSize || activeCanvasSize, { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE }),
position: clampPositionToCanvas(position, activePage?.canvasSize || activeCanvasSize, buildElementBoxSize({ elementType: 'anchor', portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH })),
data: {
label: elementName,
componentName: 'Anchor',
@@ -4962,11 +5225,13 @@
category: null,
rotation: 0,
width: 0.5,
portNumber: 1,
pitch: DEFAULT_ELEMENT_PITCH,
layer: 'WG_CORE',
description: '',
hideIcon: true,
boxSize: { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE },
ports: buildElementPorts('anchor')
boxSize: buildElementBoxSize({ elementType: 'anchor', portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH }),
ports: buildElementPorts('anchor', { portNumber: 1, pitch: DEFAULT_ELEMENT_PITCH, width: 0.5 })
},
};
setPages(prev => prev.map(p => {