Routing problem for multi-pin port and anchors are debugged
This commit is contained in:
+177
-39
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user