);
};
@@ -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 (
-
-
A
- {portHandles.map((portHandle) => (
-
-
-
-
- ))}
+
+
+ {anchorDisplayName}
+ Anchor
+
+
+ {portHandles.map((portHandle) => (
+
+
+
+
+ {pinLabelFromPortName(portHandle.name)}
+
+
+ ))}
+
);
});
@@ -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}`;
))}
)}
+ {activePage && activePage.type !== 'layoutPreview' && (
+
+ X {displayMousePoint ? displayMousePoint.x.toFixed(3) : '--'} um
+ Y {displayMousePoint ? displayMousePoint.y.toFixed(3) : '--'} um
+ O {canvasOrigin.x.toFixed(3)}, {canvasOrigin.y.toFixed(3)}
+
+ )}
+
+ {originPickMode && mouseScreenPoint && activePage?.type !== 'layoutPreview' && (
+
+ )}
+
{activePage && activePage.type !== 'layoutPreview' && (