Routing problem for multi-pin port and anchors are debugged

This commit is contained in:
2026-05-31 22:16:44 +08:00
parent 9b4e8da796
commit ce7f6e95c4
36 changed files with 2470 additions and 676 deletions
+177 -39
View File
@@ -22,8 +22,11 @@
const DEFAULT_CANVAS_SIZE = { width: 5000, height: 5000 };
// Base visual diameter and hit area used for port and anchor handles.
const PORT_NODE_SIZE = 30;
// Narrow anchor body width used in the canvas visual representation.
const ANCHOR_NODE_WIDTH = 8;
const PORT_LABEL_MIN_CHARS = 5;
const PORT_LABEL_CHAR_WIDTH = 7;
const PORT_LABEL_HORIZONTAL_PADDING = 12;
// Anchor body width used in the canvas visual representation.
const ANCHOR_NODE_WIDTH = 16;
// Default spacing between repeated anchor or port pins.
const DEFAULT_ELEMENT_PITCH = 10;
// Defines built-in port and anchor element metadata before per-node expansion.
@@ -477,7 +480,7 @@
const nextY = x * sin + y * cos;
x = nextX;
y = nextY;
angle += rotation;
angle -= rotation;
}
return {
@@ -489,16 +492,86 @@
};
// Create ordered React Flow handles for all ports on a single visual side.
const buildSideHandles = (ports, side) => {
const clampPercent = (value) => roundPercent(Math.min(100, Math.max(0, value)));
const createCoordinateMetrics = (ports, boxSize) => {
const width = positiveNumber(boxSize && boxSize.width);
const height = positiveNumber(boxSize && boxSize.height);
if (!width || !height) return null;
const values = { x: [], y: [] };
ports.forEach(port => {
const info = port && port.info;
const x = Number(info && info.x);
const y = Number(info && info.y);
if (Number.isFinite(x)) values.x.push(x);
if (Number.isFinite(y)) values.y.push(y);
});
return { width, height, values };
};
const coordinateAxisMode = (axisValues, size) => {
if (!axisValues || axisValues.length === 0 || !size) return 'fallback';
const min = Math.min(...axisValues);
const max = Math.max(...axisValues);
const epsilon = Math.max(0.001, size * 0.001);
if (Math.abs(max - min) <= epsilon) {
return Math.abs(max) <= epsilon ? 'centered' : 'fallback';
}
if (min < -epsilon && max <= size / 2 + epsilon && min >= -size / 2 - epsilon) {
return 'centered';
}
if (min >= -epsilon && max <= size + epsilon) {
return 'positive';
}
return 'fallback';
};
const coordinatePercent = (info, axis, metrics) => {
if (!metrics) return null;
const value = Number(info && info[axis]);
if (!Number.isFinite(value)) return null;
const size = axis === 'x' ? metrics.width : metrics.height;
const mode = coordinateAxisMode(metrics.values[axis], size);
if (mode === 'centered') {
return axis === 'x'
? clampPercent(50 + (value / size) * 100)
: clampPercent(50 - (value / size) * 100);
}
if (mode === 'positive') {
return axis === 'x'
? clampPercent((value / size) * 100)
: clampPercent(100 - (value / size) * 100);
}
return null;
};
const buildSideHandles = (ports, side, metrics) => {
const vertical = side === 'left' || side === 'right';
return ports.map((port, index) => {
const explicitPercent = Number(port.info && port.info.handlePercent);
const percent = Number.isFinite(explicitPercent) ? explicitPercent : fallbackPercent(index, ports.length);
const exactPercent = coordinatePercent(port.info, vertical ? 'y' : 'x', metrics);
const percent = Number.isFinite(explicitPercent)
? explicitPercent
: exactPercent !== null
? exactPercent
: fallbackPercent(index, ports.length);
const percentValue = `${percent}%`;
const style = vertical
? { top: percentValue, transform: side === 'left' ? 'translate(-50%, -50%)' : 'translate(50%, -50%)' }
: { left: percentValue, transform: side === 'top' ? 'translate(-50%, -50%)' : 'translate(-50%, 50%)' };
? {
left: side === 'left' ? 0 : '100%',
right: 'auto',
top: percentValue,
bottom: 'auto',
transform: 'translate(-50%, -50%)'
}
: {
left: percentValue,
right: 'auto',
top: side === 'top' ? 0 : '100%',
bottom: 'auto',
transform: 'translate(-50%, -50%)'
};
return {
name: port.name,
@@ -511,13 +584,18 @@
// Group transformed ports into canvas handles with side and position styling.
const buildPortHandles = (ports, transform) => {
const options = transform || {};
const grouped = { left: [], right: [], top: [], bottom: [] };
const allPorts = [];
Object.entries(ports || {}).forEach(([name, info]) => {
if (name === 'a0' || name === 'b0') return;
const transformedInfo = transformPortInfo(info, transform);
const transformedInfo = transformPortInfo(info, options);
const side = portSideFromAngle(transformedInfo.a);
grouped[side].push({ name, info: transformedInfo });
const port = { name, info: transformedInfo };
grouped[side].push(port);
allPorts.push(port);
});
const metrics = createCoordinateMetrics(allPorts, options.boxSize);
Object.values(grouped).forEach(sidePorts => {
sidePorts.sort((a, b) => {
@@ -529,10 +607,10 @@
});
return [
...buildSideHandles(grouped.left, 'left'),
...buildSideHandles(grouped.right, 'right'),
...buildSideHandles(grouped.top, 'top'),
...buildSideHandles(grouped.bottom, 'bottom')
...buildSideHandles(grouped.left, 'left', metrics),
...buildSideHandles(grouped.right, 'right', metrics),
...buildSideHandles(grouped.top, 'top', metrics),
...buildSideHandles(grouped.bottom, 'bottom', metrics)
];
};
@@ -615,6 +693,35 @@
return name || (node && node.id) || 'port';
};
const pinRoleFromElementPortName = (elementType, portName) => {
const name = String(portName || '');
if (elementType === 'anchor') {
const anchorMatch = name.match(/^([ab])(\d+)$/);
return anchorMatch ? `${anchorMatch[1]}${anchorMatch[2]}` : name;
}
const portMatch = name.match(/^port_(\d+)$/);
return portMatch ? `io${portMatch[1]}` : 'io1';
};
const defaultElementPinName = (elementName, role) => `${elementName}_${role}`;
const getElementPinName = (node, portName) => {
const data = (node && node.data) || {};
const elementType = data.elementType === 'anchor' ? 'anchor' : 'port';
const elementName = getNodePortName(node);
const role = pinRoleFromElementPortName(elementType, portName);
return (data.pinNames && data.pinNames[role]) || defaultElementPinName(elementName, role);
};
const buildElementPinEntries = (node) => {
const data = (node && node.data) || {};
const elementType = data.elementType === 'anchor' ? 'anchor' : 'port';
return Object.keys(buildElementPorts(elementType, data)).map(portName => {
const role = pinRoleFromElementPortName(elementType, portName);
return { role, name: getElementPinName(node, portName) };
});
};
// Detect standalone port nodes that become top-level layout ports.
const isPortElementNode = (node) => node && (node.data && node.data.elementType === 'port' || node.id === 'page-port' || node.type === 'portNode');
// Detect built-in port or anchor nodes for element YAML export.
@@ -658,8 +765,13 @@
const portNumber = normalizePortNumber(data && data.portNumber);
const pitch = normalizePitch(data && data.pitch);
const handleClearance = Math.max(pitch, 14);
const portDisplayName = String((data && (data.portName || data.componentDisplayName || data.label)) || 'port');
const portWidth = Math.max(
PORT_NODE_SIZE,
PORT_LABEL_HORIZONTAL_PADDING + Math.max(PORT_LABEL_MIN_CHARS, portDisplayName.length) * PORT_LABEL_CHAR_WIDTH
);
return {
width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : PORT_NODE_SIZE,
width: data && data.elementType === 'anchor' ? ANCHOR_NODE_WIDTH : portWidth,
height: Math.max(PORT_NODE_SIZE, PORT_NODE_SIZE + Math.max(0, portNumber - 1) * handleClearance)
};
};
@@ -695,7 +807,7 @@
if (portNumber > 1) {
const entries = [];
Array.from({ length: portNumber }, (_, index) => {
const y = -PORT_NODE_SIZE / 2 + elementPortOffset(index, portNumber, pitch);
const y = 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 }]);
});
@@ -771,40 +883,41 @@
};
};
// Flip an internal standalone Port angle into the outward-facing cell port
// angle used when this canvas is placed as a component elsewhere.
// Export standalone Port pins as the outward-facing pin angle.
const externalPortAngle = (angle) => normalizeAngle(Number(angle ?? 0) + 180);
// Convert standalone port nodes into page-level layout ports.
const buildPageComponentPorts = (port, nodes) => {
// Convert standalone port nodes into page-level layout pins.
const buildPageComponentPins = (port, nodes) => {
const portNodes = (nodes || []).filter(isPortElementNode);
if (portNodes.length > 0) {
return portNodes.reduce((ports, node) => {
return portNodes.reduce((pins, node) => {
const data = node.data || {};
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 exportName = getElementPinName(node, portName);
const point = getNodePortCanvasPoint(node, portName) || {
x: Number((node.position && node.position.x) || 0),
y: Number((node.position && node.position.y) || 0)
};
ports[exportName] = {
pins[exportName] = {
element: baseName,
pin: pinRoleFromElementPortName('port', portName),
x: Number(point.x || 0),
y: Number(point.y || 0),
a: externalPortAngle(portInfo.a ?? data.angle ?? data.a ?? 0),
width: Number(portInfo.width || data.width || 0.5)
};
});
return ports;
return pins;
}, {});
}
if (!port) return {};
return {
port: {
port_io1: {
element: 'port',
pin: 'io1',
x: Number(port.x || 0),
y: Number(port.y || 0),
a: externalPortAngle(port.a || 0),
@@ -813,27 +926,34 @@
};
};
// Serialize standalone canvas ports into a layout ports YAML section.
const buildCanvasPortsYaml = (nodes, fallbackPort) => {
const ports = buildPageComponentPorts(fallbackPort, nodes);
const entries = Object.entries(ports);
if (entries.length === 0) return 'ports: []';
// Backward-compatible helper name for callers that still use the old JS API.
const buildPageComponentPorts = buildPageComponentPins;
// Serialize standalone canvas pins into a layout pins YAML section.
const buildCanvasPinsYaml = (nodes, fallbackPort) => {
const pins = buildPageComponentPins(fallbackPort, nodes);
const entries = Object.entries(pins);
if (entries.length === 0) return 'pins: []';
const sourceNodes = new Map((nodes || []).filter(isPortElementNode).map(node => [getNodePortName(node), node]));
const lines = entries.map(([name, info]) => {
const data = (sourceNodes.get(name) && sourceNodes.get(name).data) || {};
const data = (sourceNodes.get(info.element) && sourceNodes.get(info.element).data) || {};
const description = data.description ? `\n description: ${toYamlScalar(data.description)}` : '';
return `- name: ${name}
${data.layer ? `layer: ${data.layer}` : 'layer: WG_CORE'}
element: ${info.element}
pin: ${info.pin}
x: ${Number(info.x || 0).toFixed(1)}
y: ${canvasToLayoutY(info.y).toFixed(1)}
angle: ${Number(info.a || 0).toFixed(1)}
width: ${Number(info.width || 0.5)}${description}`;
});
return `ports:\n${lines.join('\n')}`;
return `pins:\n${lines.join('\n')}`;
};
const buildCanvasPortsYaml = buildCanvasPinsYaml;
// Maintain legacy single-port YAML export behavior for older callers.
const buildPortsYaml = (port) => buildCanvasPortsYaml([], port);
const buildPortsYaml = (port) => buildCanvasPinsYaml([], port);
// Serialize built-in port and anchor nodes into layout element metadata.
const buildElementsYaml = (nodes) => {
@@ -845,16 +965,21 @@
const angle = data.elementType === 'port' ? data.angle : data.rotation;
const portNumber = normalizePortNumber(data.portNumber);
const pitch = normalizePitch(data.pitch);
const pinLines = buildElementPinEntries(node)
.map(pin => ` - name: ${pin.name}\n role: ${pin.role}`)
.join('\n');
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}
pin_number: ${portNumber}
pitch: ${Number(pitch)}
layer: ${data.layer || 'WG_CORE'}
width: ${Number(data.width || 0.5)}
description: ${toYamlScalar(data.description || '')}`;
description: ${toYamlScalar(data.description || '')}
pins:
${pinLines}`;
});
return `elements:\n${lines.join('\n')}`;
};
@@ -872,8 +997,12 @@
const targetNode = nodeMap[edge.target];
const sourceName = sourceNode ? (sourceNode.data.componentDisplayName || sourceNode.id) : edge.source;
const targetName = targetNode ? (targetNode.data.componentDisplayName || targetNode.id) : edge.target;
const fromPort = edge.sourceHandle || 'unknown';
const toPort = edge.targetHandle || 'unknown';
const fromPort = sourceNode && sourceNode.data && sourceNode.data.elementType
? getElementPinName(sourceNode, edge.sourceHandle)
: edge.sourceHandle || 'unknown';
const toPort = targetNode && targetNode.data && targetNode.data.elementType
? getElementPinName(targetNode, edge.targetHandle)
: edge.targetHandle || 'unknown';
const route = createRouteSettings(manifest, edge.data && edge.data.route);
const storedPoints = Array.isArray(edge.data && edge.data.points) ? edge.data.points : [];
const points = storedPoints.length >= 2 ? getEdgeRoutePoints(edge, nodeMap) : [];
@@ -926,7 +1055,12 @@ ${linksYaml}`;
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 });
const data = node.data || {};
const transformedInfo = transformPortInfo(portInfo, {
rotation: data.angle ?? data.a ?? data.rotation ?? 0,
flip: Boolean(data.flip),
flop: Boolean(data.flop)
});
return {
x: roundMeasureValue(x + Number(transformedInfo.x || 0)),
y: roundMeasureValue(y - Number(transformedInfo.y || 0))
@@ -1127,12 +1261,16 @@ ${linksYaml}`;
getNodePortCanvasPoint,
buildPortHandles,
buildElementPorts,
buildElementPinEntries,
getElementPinName,
buildElementBoxSize,
buildBasicComponentPorts,
getBasicComponentMetadata,
buildInstanceYaml,
buildInstancesYaml,
buildPageComponentPorts,
buildPageComponentPins,
buildCanvasPinsYaml,
buildCanvasPortsYaml,
buildBundlesYaml,
buildPortsYaml,