Routing problem for multi-pin port and anchors are debugged
This commit is contained in:
+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