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,
|
||||
|
||||
+448
-136
@@ -662,6 +662,18 @@ Organization : OptiHK Limited
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.origin-select-btn {
|
||||
border-color: rgba(45, 212, 191, 0.55);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.origin-select-btn.active {
|
||||
background: rgba(45, 212, 191, 0.16);
|
||||
border-color: var(--accent-green);
|
||||
color: var(--text-main);
|
||||
box-shadow: 0 0 0 1px rgba(45, 212, 191, 0.2), 0 10px 20px rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
|
||||
body.light-mode .mini-btn {
|
||||
background: var(--mini-button-bg);
|
||||
border-color: rgba(30, 48, 69, 0.18);
|
||||
@@ -697,7 +709,10 @@ Organization : OptiHK Limited
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
max-width: calc(100% - 30px);
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
@@ -723,6 +738,71 @@ Organization : OptiHK Limited
|
||||
color: #102033;
|
||||
}
|
||||
|
||||
.coordinate-readout {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 270px;
|
||||
justify-content: center;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: rgba(13, 22, 38, 0.9);
|
||||
color: var(--text-main);
|
||||
box-shadow: 0 14px 28px var(--shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
font: 600 0.62rem/1 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.coordinate-readout span {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
body.light-mode .coordinate-readout {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-color: rgba(30, 48, 69, 0.16);
|
||||
box-shadow: 0 14px 28px rgba(18, 32, 51, 0.12);
|
||||
}
|
||||
|
||||
.origin-crosshair {
|
||||
position: absolute;
|
||||
z-index: 18;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.origin-crosshair::before,
|
||||
.origin-crosshair::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 10px rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
|
||||
.origin-crosshair::before {
|
||||
width: 22px;
|
||||
height: 1px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.origin-crosshair::after {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.build-layout-btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
@@ -1195,6 +1275,34 @@ Organization : OptiHK Limited
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.port-pin-label {
|
||||
position: absolute;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
font-family: 'IBM Plex Mono', Consolas, Monaco, monospace;
|
||||
font-size: 0.34rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--port-label-text);
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.port-pin-label span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.anchor-node-shell {
|
||||
position: relative;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
}
|
||||
|
||||
.anchor-visual-body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.build-progress {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -1459,11 +1567,12 @@ Organization : OptiHK Limited
|
||||
calculateCompositeBoxSize,
|
||||
buildPortHandles,
|
||||
buildElementPorts,
|
||||
getElementPinName,
|
||||
buildElementBoxSize,
|
||||
getBasicComponentMetadata,
|
||||
buildInstancesYaml,
|
||||
buildPageComponentPorts,
|
||||
buildCanvasPortsYaml,
|
||||
buildCanvasPinsYaml,
|
||||
buildElementsYaml,
|
||||
buildBundlesYaml: buildRouteBundlesYaml,
|
||||
normalizeAngle,
|
||||
@@ -1599,9 +1708,9 @@ Organization : OptiHK Limited
|
||||
}, [id, data.ports, data.componentName, data.boxSize]);
|
||||
|
||||
const baseHandleStyle = {
|
||||
width: 8, height: 8,
|
||||
width: 6, height: 6,
|
||||
background: 'var(--bg-main)',
|
||||
border: '2px solid var(--accent)',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: '50%',
|
||||
};
|
||||
const handlePositionMap = {
|
||||
@@ -1610,27 +1719,32 @@ Organization : OptiHK Limited
|
||||
top: Position.Top,
|
||||
bottom: Position.Bottom
|
||||
};
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[data.ports, data.rotation, data.flip, data.flop]
|
||||
);
|
||||
const componentSize = normalizeBoxSize({ box_size: data.boxSize }, DEFAULT_COMPONENT_BOX_SIZE);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize }),
|
||||
[data.ports, data.rotation, data.flip, data.flop, componentSize]
|
||||
);
|
||||
const portDirectionMap = useMemo(
|
||||
() => new Map(portHandles.map(handle => [handle.name, handle.position])),
|
||||
[portHandles]
|
||||
);
|
||||
const isAnchorElement = data.elementType === 'anchor';
|
||||
const isBasicCompactComponent = isBasicComponent(data.componentName) && ['waveguide', 'taper', '90 bend'].includes(data.componentName);
|
||||
const visualSize = isAnchorElement ? { width: PORT_NODE_SIZE, height: PORT_NODE_SIZE } : componentSize;
|
||||
const componentVisualTransform = `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`;
|
||||
const iconSize = createComponentSymbolMetrics(componentSize);
|
||||
const portLabelStyle = (portHandle) => {
|
||||
const base = { ...portHandle.style };
|
||||
if (portHandle.position === 'left') {
|
||||
return { ...base, right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
|
||||
return { ...base, left: 'auto', right: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'right' };
|
||||
}
|
||||
if (portHandle.position === 'right') {
|
||||
return { ...base, left: 'calc(100% + 8px)', transform: 'translateY(-50%)', textAlign: 'left' };
|
||||
return { ...base, left: 'calc(100% + 8px)', right: 'auto', transform: 'translateY(-50%)', textAlign: 'left' };
|
||||
}
|
||||
if (portHandle.position === 'top') {
|
||||
return { ...base, bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'auto', bottom: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
}
|
||||
return { ...base, top: 'calc(100% + 8px)', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
return { ...base, top: 'calc(100% + 8px)', bottom: 'auto', transform: 'translateX(-50%)', textAlign: 'center' };
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1651,7 +1765,7 @@ Organization : OptiHK Limited
|
||||
height: visualSize.height,
|
||||
minHeight: visualSize.height,
|
||||
border: selected ? '2px solid var(--accent)' : '1px solid var(--border)',
|
||||
transform: `rotate(${data.rotation || 0}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`,
|
||||
transform: componentVisualTransform,
|
||||
boxShadow: selected ? '0 0 15px rgba(56, 189, 248, 0.2)' : '0 4px 6px rgba(0,0,0,0.3)',
|
||||
...(isBasicCompactComponent ? {
|
||||
padding: 0,
|
||||
@@ -1684,22 +1798,34 @@ Organization : OptiHK Limited
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
width: componentSize.width,
|
||||
height: visualSize.height,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10, pointerEvents: 'all' }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5, pointerEvents: 'all' }}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 10 }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style, zIndex: 5 }}
|
||||
/>
|
||||
<React.Fragment key={`label-${portHandle.name}`}>
|
||||
<span className="port-name-label" style={portLabelStyle(portHandle)} title={portHandle.name}>
|
||||
{portHandle.name}
|
||||
</span>
|
||||
@@ -1727,16 +1853,34 @@ Organization : OptiHK Limited
|
||||
|
||||
|
||||
// Renders standalone exported port elements with repeated port handles.
|
||||
const pinLabelFromPortName = (portName) => {
|
||||
const name = String(portName || '');
|
||||
const portMatch = name.match(/^port_(\d+)$/);
|
||||
if (portMatch) return portMatch[1];
|
||||
if (name === 'port') return '1';
|
||||
return name;
|
||||
};
|
||||
|
||||
const PortNode = ({ id, data, selected }) => {
|
||||
const angle = data.angle ?? 0;
|
||||
const canvasAngle = -Number(angle || 0);
|
||||
const portDisplayName = data.portName || data.componentDisplayName || data.label || '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 localPortHandles = useMemo(
|
||||
() => buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize }),
|
||||
[localHandlePorts, elementSize]
|
||||
);
|
||||
const portHandles = useMemo(
|
||||
() => buildPortHandles(localHandlePorts, { rotation: 0 }),
|
||||
[localHandlePorts]
|
||||
() => buildPortHandles(localHandlePorts, { rotation: canvasAngle }),
|
||||
[localHandlePorts, canvasAngle]
|
||||
);
|
||||
const portDirectionMap = useMemo(
|
||||
() => new Map(portHandles.map(handle => [handle.name, handle.position])),
|
||||
[portHandles]
|
||||
);
|
||||
const handlePositionMap = {
|
||||
left: Position.Left,
|
||||
@@ -1746,27 +1890,61 @@ Organization : OptiHK Limited
|
||||
};
|
||||
const baseHandleStyle = {
|
||||
background: 'var(--accent)',
|
||||
width: 6,
|
||||
height: 6
|
||||
width: 5,
|
||||
height: 5
|
||||
};
|
||||
const pinLabelStyle = (portHandle) => {
|
||||
const base = {
|
||||
left: portHandle.style?.left,
|
||||
right: portHandle.style?.right,
|
||||
top: portHandle.style?.top,
|
||||
bottom: portHandle.style?.bottom
|
||||
};
|
||||
if (portHandle.position === 'left') return { ...base, transform: 'translate(calc(-100% - 5px), -50%)' };
|
||||
if (portHandle.position === 'right') return { ...base, transform: 'translate(5px, -50%)' };
|
||||
if (portHandle.position === 'top') return { ...base, transform: 'translate(-50%, calc(-100% - 5px))' };
|
||||
return { ...base, transform: 'translate(-50%, 5px)' };
|
||||
};
|
||||
const pinLabelTextStyle = {
|
||||
transform: `rotate(${-canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`
|
||||
};
|
||||
return (
|
||||
<div style={{
|
||||
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',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 8, fontWeight: 'bold',
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
}}>
|
||||
<span>P</span>
|
||||
{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 style={{ width: elementSize.width, height: elementSize.height, position: 'relative' }}>
|
||||
<div className="component-floating-label" title={portDisplayName}>
|
||||
<strong>{portDisplayName}</strong>
|
||||
<span>Port</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: elementSize.width, height: elementSize.height, borderRadius: 7,
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 8, fontWeight: 'bold',
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`,
|
||||
}}>
|
||||
{localPortHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style }}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
style={{ ...baseHandleStyle, ...portHandle.style }}
|
||||
/>
|
||||
<div className="port-pin-label" style={pinLabelStyle(portHandle)}>
|
||||
<span style={pinLabelTextStyle}>{pinLabelFromPortName(portHandle.name)}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1776,17 +1954,18 @@ Organization : OptiHK Limited
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const anchorRotation = data.rotation || 0;
|
||||
const anchorVisualRotation = -Number(anchorRotation || 0);
|
||||
const anchorDisplayName = data.componentDisplayName || data.label || 'anchor';
|
||||
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(localAnchorHandlePorts, { rotation: 0, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[localAnchorHandlePorts, data.flip, data.flop]
|
||||
() => buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize, flip: Boolean(data.flip), flop: Boolean(data.flop) }),
|
||||
[localAnchorHandlePorts, elementSize, 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])),
|
||||
() => 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 = {
|
||||
@@ -1796,8 +1975,8 @@ Organization : OptiHK Limited
|
||||
bottom: Position.Bottom
|
||||
};
|
||||
const baseHandleStyle = {
|
||||
width: 6,
|
||||
height: 6,
|
||||
width: 5,
|
||||
height: 5,
|
||||
background: 'var(--accent)',
|
||||
border: '1px solid var(--bg-main)',
|
||||
borderRadius: '50%'
|
||||
@@ -1806,18 +1985,10 @@ Organization : OptiHK Limited
|
||||
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);
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
...baseHandleStyle,
|
||||
zIndex,
|
||||
@@ -1828,47 +1999,66 @@ Organization : OptiHK Limited
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
};
|
||||
const pinLabelStyle = (portHandle) => {
|
||||
const visualSide = anchorPortVisualSide(portHandle.name);
|
||||
const localLeft = visualSide === 'left' ? 0 : elementSize.width;
|
||||
const localTop = portHandle.style?.top || '50%';
|
||||
return {
|
||||
left: localLeft,
|
||||
top: localTop,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
transform: visualSide === 'left' ? 'translate(calc(-100% - 5px), -50%)' : 'translate(5px, -50%)'
|
||||
};
|
||||
};
|
||||
const pinLabelTextStyle = {
|
||||
transform: `rotate(${Number(anchorRotation || 0)}deg)`
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, data.ports, data.rotation, data.flip, data.flop, updateNodeInternals]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
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',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
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[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 10)}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 5)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div className="anchor-node-shell" style={{ width: elementSize.width, height: elementSize.height }}>
|
||||
<div className="component-floating-label" title={anchorDisplayName}>
|
||||
<strong>{anchorDisplayName}</strong>
|
||||
<span>Anchor</span>
|
||||
</div>
|
||||
<div className="anchor-visual-body" style={{
|
||||
width: elementSize.width,
|
||||
height: elementSize.height,
|
||||
borderRadius: 999,
|
||||
background: selected ? 'var(--accent)' : 'var(--bg-card)',
|
||||
border: selected ? '2px solid white' : '2px solid var(--accent)',
|
||||
color: selected ? 'white' : 'var(--accent)',
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
boxShadow: selected ? '0 0 10px rgba(56,189,248,0.4)' : 'none',
|
||||
transform: `rotate(${anchorVisualRotation}deg)`,
|
||||
}}>
|
||||
{portHandles.map((portHandle) => (
|
||||
<React.Fragment key={portHandle.name}>
|
||||
<Handle
|
||||
type="source"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 10)}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]}
|
||||
id={portHandle.name}
|
||||
title={portHandle.name}
|
||||
style={anchorHandleVisualStyle(portHandle, 5)}
|
||||
/>
|
||||
<div className="port-pin-label" style={pinLabelStyle(portHandle)}>
|
||||
<span style={pinLabelTextStyle}>{pinLabelFromPortName(portHandle.name)}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -2919,7 +3109,7 @@ Organization : OptiHK Limited
|
||||
onUpdateNode(selectedNode.id, {
|
||||
data: {
|
||||
basicArguments: nextArguments,
|
||||
ports: metadata?.ports || {},
|
||||
ports: metadata?.pins || metadata?.ports || {},
|
||||
boxSize: metadata ? normalizeBoxSize(metadata) : selectedNode.data?.boxSize
|
||||
}
|
||||
});
|
||||
@@ -3558,6 +3748,10 @@ Organization : OptiHK Limited
|
||||
const [rulerStartPoint, setRulerStartPoint] = useState(null);
|
||||
const [rulerEndPoint, setRulerEndPoint] = useState(null);
|
||||
const [rulerPreviewPoint, setRulerPreviewPoint] = useState(null);
|
||||
const [mouseCanvasPoint, setMouseCanvasPoint] = useState(null);
|
||||
const [mouseScreenPoint, setMouseScreenPoint] = useState(null);
|
||||
const [canvasOrigin, setCanvasOrigin] = useState({ x: 0, y: 0 });
|
||||
const [originPickMode, setOriginPickMode] = useState(false);
|
||||
const [projectTechnology, setProjectTechnology] = useState('');
|
||||
const [technologyManifest, setTechnologyManifest] = useState(FALLBACK_TECHNOLOGY_MANIFEST);
|
||||
const [currentLinkXsection, setCurrentLinkXsection] = useState('strip');
|
||||
@@ -3565,6 +3759,7 @@ Organization : OptiHK Limited
|
||||
const [clipboard, setClipboard] = useState({ nodes: [] });
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const canvasViewportRef = useRef(null);
|
||||
const edgeTypes = useMemo(() => ({ parallelRoute: ParallelRouteEdge }), []);
|
||||
|
||||
const activePage = useMemo(() => pages.find(p => p.id === activePageId) || null, [pages, activePageId]);
|
||||
@@ -3602,6 +3797,14 @@ Organization : OptiHK Limited
|
||||
[rulerStartPoint, rulerActiveEndPoint]
|
||||
);
|
||||
const rulerPreviewMeasurement = !rulerEndPoint && rulerPreviewPoint ? rulerMeasurement : null;
|
||||
const displayMousePoint = useMemo(() => (
|
||||
mouseCanvasPoint
|
||||
? {
|
||||
x: Number((mouseCanvasPoint.x - canvasOrigin.x).toFixed(3)),
|
||||
y: Number((mouseCanvasPoint.y - canvasOrigin.y).toFixed(3))
|
||||
}
|
||||
: null
|
||||
), [mouseCanvasPoint, canvasOrigin]);
|
||||
// Normalizes free-route control points and removes adjacent duplicates before storage.
|
||||
const compactRoutePoints = useCallback((points) => {
|
||||
return (points || [])
|
||||
@@ -3743,7 +3946,7 @@ Organization : OptiHK Limited
|
||||
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),
|
||||
rotation: -Number(node.data?.rotation || 0),
|
||||
flip: Boolean(node.data?.flip),
|
||||
flop: Boolean(node.data?.flop)
|
||||
});
|
||||
@@ -4414,10 +4617,7 @@ Organization : OptiHK Limited
|
||||
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 portNumberValue = Math.floor(Number(element.pin_number ?? element.pinNumber ?? 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;
|
||||
@@ -4436,6 +4636,7 @@ Organization : OptiHK Limited
|
||||
pitch,
|
||||
layer: element.layer || 'WG_CORE',
|
||||
description: element.description || '',
|
||||
pinNames: Object.fromEntries((element.pins || []).map(pin => [pin.role, pin.name]).filter(([role, name]) => role && name)),
|
||||
boxSize: buildElementBoxSize({ elementType, portNumber, pitch })
|
||||
};
|
||||
const nodeId = `element-${elementName}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
@@ -4479,6 +4680,14 @@ Organization : OptiHK Limited
|
||||
return nodes;
|
||||
}, []);
|
||||
|
||||
const resolveLoadedPinHandle = useCallback((node, pinName) => {
|
||||
if (!node || !node.data?.elementType) return pinName;
|
||||
const elementType = node.data.elementType === 'anchor' ? 'anchor' : 'port';
|
||||
const ports = buildElementPorts(elementType, node.data);
|
||||
const matched = Object.keys(ports || {}).find(portName => getElementPinName(node, portName) === pinName);
|
||||
return matched || pinName;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const input = document.getElementById('open-yaml-input');
|
||||
if (!input) return;
|
||||
@@ -4547,7 +4756,7 @@ Organization : OptiHK Limited
|
||||
componentDisplayName: instName,
|
||||
type: isProject ? 'composite' : undefined,
|
||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
||||
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||
@@ -4569,13 +4778,17 @@ Organization : OptiHK Limited
|
||||
const sourceId = nodeNameMap[fromInst];
|
||||
const targetId = nodeNameMap[toInst];
|
||||
if (sourceId && targetId) {
|
||||
const sourceNode = newNodes.find(node => node.id === sourceId);
|
||||
const targetNode = newNodes.find(node => node.id === targetId);
|
||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||
const view = routeStyleForSettings(route, false);
|
||||
newEdges.push({
|
||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
sourceHandle: fromPort,
|
||||
targetHandle: toPort,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type: view.type,
|
||||
style: view.style,
|
||||
data: { route, points: routePoints },
|
||||
@@ -4591,8 +4804,8 @@ Organization : OptiHK Limited
|
||||
|
||||
const newPageId = Date.now().toString() + Math.random().toString(36).substr(2, 5);
|
||||
const newPageName = file.name.replace(/\.(yaml|yml)$/i, '');
|
||||
const importedPort = Array.isArray(doc.ports) && doc.ports[0]
|
||||
? { x: Number(doc.ports[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.ports[0].y) : Number(doc.ports[0].y || 0), a: Number(doc.ports[0].angle ?? doc.ports[0].a ?? 0), width: Number(doc.ports[0].width || 0.5) }
|
||||
const importedPin = Array.isArray(doc.pins) && doc.pins[0]
|
||||
? { x: Number(doc.pins[0].x || 0), y: usesGdsYUp ? layoutToCanvasY(doc.pins[0].y) : Number(doc.pins[0].y || 0), a: Number(doc.pins[0].angle ?? doc.pins[0].a ?? 0), width: Number(doc.pins[0].width || 0.5) }
|
||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||
const newPage = {
|
||||
id: newPageId,
|
||||
@@ -4603,8 +4816,8 @@ Organization : OptiHK Limited
|
||||
{
|
||||
id: 'page-port',
|
||||
type: 'portNode',
|
||||
position: { x: importedPort.x, y: importedPort.y },
|
||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPort.a, width: importedPort.width || 0.5, layer: 'WG_CORE', description: '' },
|
||||
position: { x: importedPin.x, y: importedPin.y },
|
||||
data: { label: 'port', componentDisplayName: 'port', portName: 'port', elementType: 'port', angle: importedPin.a, width: importedPin.width || 0.5, layer: 'WG_CORE', description: '' },
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
deletable: false,
|
||||
@@ -4612,7 +4825,7 @@ Organization : OptiHK Limited
|
||||
...newNodes,
|
||||
],
|
||||
edges: newEdges,
|
||||
port: importedPort,
|
||||
port: importedPin,
|
||||
};
|
||||
|
||||
setPages(prev => [...prev, newPage]);
|
||||
@@ -4663,7 +4876,7 @@ Organization : OptiHK Limited
|
||||
|
||||
input.addEventListener('change', handleFile);
|
||||
return () => input.removeEventListener('change', handleFile);
|
||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
||||
}, [library, technologyManifest, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectCompositeMap(prev => {
|
||||
@@ -4713,9 +4926,9 @@ Organization : OptiHK Limited
|
||||
const pageFromYaml = (cellName, content, manifest, knownCompositeNames = new Set()) => {
|
||||
const doc = jsyaml.load(content) || {};
|
||||
const usesGdsYUp = doc.coordinate_system === 'gds_y_up';
|
||||
const firstPort = Array.isArray(doc.ports) ? doc.ports[0] : null;
|
||||
const pagePort = firstPort
|
||||
? { x: Number(firstPort.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPort.y) : Number(firstPort.y || 0), a: Number(firstPort.angle ?? firstPort.a ?? 0), width: Number(firstPort.width || 0.5) }
|
||||
const firstPin = Array.isArray(doc.pins) ? doc.pins[0] : null;
|
||||
const pagePort = firstPin
|
||||
? { x: Number(firstPin.x || 0), y: usesGdsYUp ? layoutToCanvasY(firstPin.y) : Number(firstPin.y || 0), a: Number(firstPin.angle ?? firstPin.a ?? 0), width: Number(firstPin.width || 0.5) }
|
||||
: { x: 50, y: 150, a: 0, width: 0.5 };
|
||||
const nodeNameMap = {};
|
||||
const nodes = [
|
||||
@@ -4760,7 +4973,7 @@ Organization : OptiHK Limited
|
||||
componentDisplayName: instName,
|
||||
type: instIsComposite ? 'composite' : undefined,
|
||||
availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents,
|
||||
ports: instIsBasic ? (basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
ports: instIsBasic ? (basicMetadata?.pins || basicMetadata?.ports || {}) : (instIsForge ? {} : undefined),
|
||||
boxSize: instIsBasic && basicMetadata ? normalizeBoxSize(basicMetadata) : undefined,
|
||||
forgeArguments: instIsForge ? createForgeArguments(inst.settings) : undefined,
|
||||
basicArguments: instIsBasic ? createBasicSettings(displayCompName, inst.settings) : undefined,
|
||||
@@ -4781,13 +4994,17 @@ Organization : OptiHK Limited
|
||||
const sourceId = nodeNameMap[fromInst];
|
||||
const targetId = nodeNameMap[toInst];
|
||||
if (!sourceId || !targetId) return;
|
||||
const sourceNode = nodes.find(node => node.id === sourceId);
|
||||
const targetNode = nodes.find(node => node.id === targetId);
|
||||
const sourceHandle = resolveLoadedPinHandle(sourceNode, fromPort);
|
||||
const targetHandle = resolveLoadedPinHandle(targetNode, toPort);
|
||||
const view = routeStyleForSettings(route, false);
|
||||
edges.push({
|
||||
id: `edge-${sourceId}-${fromPort}-${targetId}-${toPort}`,
|
||||
id: `edge-${sourceId}-${sourceHandle}-${targetId}-${targetHandle}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
sourceHandle: fromPort,
|
||||
targetHandle: toPort,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type: view.type,
|
||||
style: view.style,
|
||||
data: { route, points: routePoints },
|
||||
@@ -4861,7 +5078,7 @@ Organization : OptiHK Limited
|
||||
};
|
||||
|
||||
loadProject();
|
||||
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent]);
|
||||
}, [library, currentProjectName, loadTechnologyManifest, toBooleanFlag, makeFreeRouteEdge, buildElementNodesFromYaml, getAvailableComponentsForLoadedComponent, resolveLoadedPinHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activePage && activePage.type !== 'layoutPreview' && reactFlowInstance) {
|
||||
@@ -4949,7 +5166,7 @@ Organization : OptiHK Limited
|
||||
position: clampPositionToCanvas(node.position, page.canvasSize || DEFAULT_CANVAS_SIZE, boxSize),
|
||||
data: {
|
||||
...node.data,
|
||||
ports: metadata.ports || {},
|
||||
ports: metadata.pins || metadata.ports || {},
|
||||
boxSize,
|
||||
foundry: metadata.foundry || '',
|
||||
process: metadata.process || ''
|
||||
@@ -5273,7 +5490,7 @@ Organization : OptiHK Limited
|
||||
type: 'composite',
|
||||
category: null,
|
||||
rotation: 0,
|
||||
ports: parsedData.ports || {},
|
||||
ports: parsedData.pins || parsedData.ports || {},
|
||||
boxSize: compositeBoxSize
|
||||
}
|
||||
};
|
||||
@@ -5324,7 +5541,7 @@ Organization : OptiHK Limited
|
||||
type: 'composite',
|
||||
category: null,
|
||||
rotation: 0,
|
||||
ports: parsedData.ports || {},
|
||||
ports: parsedData.pins || parsedData.ports || {},
|
||||
boxSize: compositeBoxSize
|
||||
}
|
||||
};
|
||||
@@ -5377,7 +5594,7 @@ Organization : OptiHK Limited
|
||||
libraryCategory: 'basic',
|
||||
category: 'basic',
|
||||
rotation: 0,
|
||||
ports: metadata?.ports || {},
|
||||
ports: metadata?.pins || metadata?.ports || {},
|
||||
boxSize: metadata ? normalizeBoxSize(metadata) : DEFAULT_COMPONENT_BOX_SIZE,
|
||||
basicArguments
|
||||
},
|
||||
@@ -5585,13 +5802,16 @@ Organization : OptiHK Limited
|
||||
setRulerStartPoint(null);
|
||||
setRulerEndPoint(null);
|
||||
setRulerPreviewPoint(null);
|
||||
} else {
|
||||
setOriginPickMode(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Convert a pane click or pointer event into canvas ruler coordinates.
|
||||
const eventToRulerPoint = useCallback((event) => {
|
||||
// Convert a pane click or pointer event into canvas coordinates.
|
||||
const eventToCanvasPoint = useCallback((event) => {
|
||||
if (!reactFlowInstance || !event) return null;
|
||||
const rawPoint = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
return {
|
||||
x: Number(Math.min(activeCanvasSize.width, Math.max(0, rawPoint.x)).toFixed(3)),
|
||||
@@ -5599,12 +5819,41 @@ Organization : OptiHK Limited
|
||||
};
|
||||
}, [reactFlowInstance, activeCanvasSize.width, activeCanvasSize.height]);
|
||||
|
||||
const updateMouseCanvasPoint = useCallback((event) => {
|
||||
if (!activePage || activePage.type === 'layoutPreview') return null;
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (!nextPoint) return null;
|
||||
setMouseCanvasPoint(nextPoint);
|
||||
const rect = canvasViewportRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
setMouseScreenPoint({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
});
|
||||
}
|
||||
return nextPoint;
|
||||
}, [activePage, eventToCanvasPoint]);
|
||||
|
||||
const toggleOriginPickMode = useCallback(() => {
|
||||
setOriginPickMode(prev => {
|
||||
const next = !prev;
|
||||
if (next) {
|
||||
setRulerMode(false);
|
||||
setRulerStartPoint(null);
|
||||
setRulerEndPoint(null);
|
||||
setRulerPreviewPoint(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Set ruler start/end points from canvas clicks.
|
||||
const handleRulerPaneClick = useCallback((event) => {
|
||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const nextPoint = eventToRulerPoint(event);
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (!nextPoint) return;
|
||||
if (!rulerStartPoint || rulerEndPoint) {
|
||||
setRulerStartPoint(nextPoint);
|
||||
setRulerEndPoint(null);
|
||||
@@ -5618,14 +5867,47 @@ Organization : OptiHK Limited
|
||||
if (measurement) {
|
||||
addLog(`Ruler distance: ${measurement.label}`);
|
||||
}
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint, addLog]);
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint, addLog]);
|
||||
|
||||
// Update the live ruler preview point while measuring.
|
||||
const handleRulerMouseMove = useCallback((event) => {
|
||||
if (!rulerMode || !reactFlowInstance || !activePage || activePage.type === 'layoutPreview') return;
|
||||
if (!rulerStartPoint || rulerEndPoint) return;
|
||||
setRulerPreviewPoint(eventToRulerPoint(event));
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToRulerPoint]);
|
||||
const nextPoint = eventToCanvasPoint(event);
|
||||
if (nextPoint) setRulerPreviewPoint(nextPoint);
|
||||
}, [rulerMode, reactFlowInstance, activePage, rulerStartPoint, rulerEndPoint, eventToCanvasPoint]);
|
||||
|
||||
const chooseCanvasOriginFromEvent = useCallback((event) => {
|
||||
if (!originPickMode || !activePage || activePage.type === 'layoutPreview') return false;
|
||||
const nextPoint = updateMouseCanvasPoint(event) || eventToCanvasPoint(event);
|
||||
if (!nextPoint) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setCanvasOrigin(nextPoint);
|
||||
setOriginPickMode(false);
|
||||
addLog(`Canvas origin: (${nextPoint.x.toFixed(3)}, ${nextPoint.y.toFixed(3)}) um`);
|
||||
return true;
|
||||
}, [originPickMode, activePage, updateMouseCanvasPoint, eventToCanvasPoint, addLog]);
|
||||
|
||||
const handleCanvasMouseMove = useCallback((event) => {
|
||||
updateMouseCanvasPoint(event);
|
||||
handleRulerMouseMove(event);
|
||||
}, [updateMouseCanvasPoint, handleRulerMouseMove]);
|
||||
|
||||
const handleCanvasPaneClick = useCallback((event) => {
|
||||
if (chooseCanvasOriginFromEvent(event)) return;
|
||||
handleRulerPaneClick(event);
|
||||
}, [chooseCanvasOriginFromEvent, handleRulerPaneClick]);
|
||||
|
||||
const handleCanvasNodeClick = useCallback((event) => {
|
||||
if (chooseCanvasOriginFromEvent(event)) return;
|
||||
handleRulerPaneClick(event);
|
||||
}, [chooseCanvasOriginFromEvent, handleRulerPaneClick]);
|
||||
|
||||
const handleCanvasMouseLeave = useCallback(() => {
|
||||
setMouseCanvasPoint(null);
|
||||
setMouseScreenPoint(null);
|
||||
}, []);
|
||||
|
||||
// Select a route edge by id with optional additive selection.
|
||||
const selectEdgeById = useCallback((edgeId, additive = false) => {
|
||||
@@ -5873,7 +6155,7 @@ type: ${page.type === 'project' ? 'project' : 'composite'}
|
||||
version: "1.0.0"
|
||||
|
||||
# 1. External Ports (How this cell connects to the outside world)
|
||||
${buildCanvasPortsYaml(page.nodes)}
|
||||
${buildCanvasPinsYaml(page.nodes)}
|
||||
|
||||
# 2. Instances (The sub-components dropped onto this canvas)
|
||||
instances:`;
|
||||
@@ -6098,8 +6380,11 @@ ${bundlesBlock}`;
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
ref={canvasViewportRef}
|
||||
style={{ flex: 1, position: 'relative' }}
|
||||
onMouseDownCapture={handleCanvasMouseDown}
|
||||
onMouseMoveCapture={handleCanvasMouseMove}
|
||||
onMouseLeave={handleCanvasMouseLeave}
|
||||
>
|
||||
<div className="canvas-toolbar">
|
||||
<span className="grid-snap-label">Snap to Grid</span>
|
||||
@@ -6154,6 +6439,14 @@ ${bundlesBlock}`;
|
||||
<button className="mini-btn" onClick={toggleRulerMode} aria-pressed={rulerMode ? 'true' : 'false'}>
|
||||
{rulerMode ? 'Ruler On' : 'Ruler'}
|
||||
</button>
|
||||
<button
|
||||
className={`mini-btn origin-select-btn ${originPickMode ? 'active' : ''}`}
|
||||
onClick={toggleOriginPickMode}
|
||||
aria-pressed={originPickMode ? 'true' : 'false'}
|
||||
title="Select canvas origin"
|
||||
>
|
||||
{originPickMode ? 'Picking Origin' : 'Origin Select'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{buildProgress.active && (
|
||||
@@ -6176,6 +6469,25 @@ ${bundlesBlock}`;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activePage && activePage.type !== 'layoutPreview' && (
|
||||
<div
|
||||
className="coordinate-readout"
|
||||
style={{ bottom: rulerMode ? 58 : 18 }}
|
||||
title={`Origin (${canvasOrigin.x.toFixed(3)}, ${canvasOrigin.y.toFixed(3)}) um`}
|
||||
>
|
||||
X {displayMousePoint ? displayMousePoint.x.toFixed(3) : '--'} um
|
||||
Y {displayMousePoint ? displayMousePoint.y.toFixed(3) : '--'} um
|
||||
<span>O {canvasOrigin.x.toFixed(3)}, {canvasOrigin.y.toFixed(3)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{originPickMode && mouseScreenPoint && activePage?.type !== 'layoutPreview' && (
|
||||
<div
|
||||
className="origin-crosshair"
|
||||
style={{ left: mouseScreenPoint.x, top: mouseScreenPoint.y }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activePage && activePage.type !== 'layoutPreview' && (
|
||||
<button
|
||||
onClick={handleBuildLayout}
|
||||
@@ -6196,12 +6508,12 @@ ${bundlesBlock}`;
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={handleBasicConnection}
|
||||
onPaneClick={handleRulerPaneClick}
|
||||
onPaneMouseMove={handleRulerMouseMove}
|
||||
onPaneClick={handleCanvasPaneClick}
|
||||
onPaneMouseMove={handleCanvasMouseMove}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onNodeClick={handleRulerPaneClick}
|
||||
onNodeMouseMove={handleRulerMouseMove}
|
||||
onNodeClick={handleCanvasNodeClick}
|
||||
onNodeMouseMove={handleCanvasMouseMove}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeMouseDown={onNodeMouseDown}
|
||||
onEdgeMouseDown={handleReactFlowEdgeMouseDown}
|
||||
|
||||
Reference in New Issue
Block a user