1. Anchor routing added with mutiport
This commit is contained in:
+109
-12
@@ -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
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user