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
+448 -136
View File
@@ -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}