1. Anchor routing added with mutiport

This commit is contained in:
2026-05-30 12:04:02 +08:00
parent 2d9b2b0983
commit 5a3a80700f
23 changed files with 1226 additions and 234 deletions
+64 -10
View File
@@ -142,22 +142,22 @@ assert.strictEqual(
);
assert.deepStrictEqual(
{
left: helpers.buildElementPorts('anchor').left.a,
right: helpers.buildElementPorts('anchor').right.a,
a1: helpers.buildElementPorts('anchor').a1.a,
b1: helpers.buildElementPorts('anchor').b1.a,
},
{ left: 180, right: 0 },
'Anchor objects should default to 180 degree left port and 0 degree right port'
{ a1: 180, b1: 0 },
'Anchor objects should default to a1 for the left port and b1 for the right port'
);
assert.deepStrictEqual(
{
left: helpers.buildElementPorts('anchor').left,
right: helpers.buildElementPorts('anchor').right,
a1: helpers.buildElementPorts('anchor').a1,
b1: helpers.buildElementPorts('anchor').b1,
},
{
left: { x: 0, y: -15, a: 180, width: 0.5 },
right: { x: 30, y: -15, a: 0, width: 0.5 }
a1: { x: 0, y: -15, a: 180, width: 0.5 },
b1: { x: 0, y: -15, a: 0, width: 0.5 }
},
'Anchor ports should sit on the left and right edges of a port-sized circle'
'Anchor a/b port pairs should share coordinates and keep opposite directions'
);
assert.deepStrictEqual(
helpers.buildBasicComponentPorts('waveguide', { length: 120, width: 0.6 }).b1,
@@ -300,7 +300,46 @@ const elementNodes = [
assert.deepStrictEqual(helpers.buildElementPorts('port', { angle: 90, width: 0.8 }), {
port: { x: 0, y: 0, a: 90, width: 0.8 }
});
assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('anchor')), ['left', 'right']);
assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('anchor')), ['a1', 'b1']);
assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('port', { portNumber: 3, pitch: 10 })), ['port_1', 'port_2', 'port_3']);
assert.deepStrictEqual(helpers.buildElementPorts('port', { portNumber: 3, pitch: 10 }).port_1, { x: 0, y: 10, a: 0, width: 0.5 });
assert.deepStrictEqual(helpers.buildElementPorts('port', { portNumber: 3 }).port_1, { x: 0, y: 10, a: 0, width: 0.5 });
assert.deepStrictEqual(Object.keys(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 })), ['a1', 'b1', 'a2', 'b2']);
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).b2, { x: 0, y: -21, a: 0, width: 0.5 });
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).a2, { x: 0, y: -21, a: 180, width: 0.5 });
assert.deepStrictEqual(
helpers.getNodePortCanvasPoint({
id: 'anchor-rotated',
type: 'anchorNode',
position: { x: 100, y: 200 },
data: {
elementType: 'anchor',
rotation: 90,
portNumber: 1,
pitch: 10,
ports: helpers.buildElementPorts('anchor', { portNumber: 1, pitch: 10 })
}
}, 'a1'),
{ x: 115, y: 200 },
'Anchor port endpoint coordinates should rotate with the anchor body'
);
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 1 }), { width: 30, height: 30 });
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 1 }), { width: 8, height: 30 });
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 4, pitch: 10 }), { width: 8, height: 72 });
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 4, pitch: 10 }), { width: 30, height: 72 });
assert.deepStrictEqual(
helpers.buildPageComponentPorts(null, [{
id: 'port-array',
type: 'portNode',
position: { x: 100, y: 200 },
data: { componentDisplayName: 'array', elementType: 'port', portNumber: 3, pitch: 10, width: 0.6 }
}]),
{
array_1: { x: 100, y: 190, a: 0, width: 0.6 },
array_2: { x: 100, y: 200, a: 0, width: 0.6 },
array_3: { x: 100, y: 210, a: 0, width: 0.6 }
}
);
const canvasPortsYaml = helpers.buildCanvasPortsYaml(elementNodes);
assert(canvasPortsYaml.includes('name: in0'));
@@ -314,6 +353,8 @@ assert(elementsYaml.includes('type: port'));
assert(elementsYaml.includes('anchor_1:'));
assert(elementsYaml.includes('type: anchor'));
assert(elementsYaml.includes('y: -20.0'));
assert(elementsYaml.includes('port_number: 1'));
assert(elementsYaml.includes('pitch: 10'));
const instancesWithoutElements = helpers.buildInstancesYaml({
nodes: elementNodes,
@@ -484,7 +525,20 @@ const edgeD = {
target: 'f',
data: { route: { xsection: 'rib_low', family: 'optical' } }
};
const edgeE = {
id: 'edge-metal-alias',
source: 'e',
target: 'f',
data: { route: { xsection: 'metal1', family: 'electrical' } }
};
const edgeF = {
id: 'edge-metal-underscore',
source: 'a',
target: 'b',
data: { route: { xsection: 'metal_1', family: 'electrical' } }
};
assert.strictEqual(helpers.findSameTypeRouteCrossing(edgeB, [edgeA], crossingNodes).conflictEdge.id, 'edge-a-b');
assert.strictEqual(helpers.findSameTypeRouteCrossing(edgeC, [edgeA], crossingNodes), null);
assert.strictEqual(helpers.findSameTypeRouteCrossing(edgeD, [edgeA], crossingNodes), null);
assert.strictEqual(helpers.findSameTypeRouteCrossing(edgeE, [edgeF], crossingNodes).conflictEdge.id, 'edge-metal-underscore');
assert.strictEqual(helpers.findSameFamilyRouteCrossing(edgeB, [edgeA], crossingNodes).conflictEdge.id, 'edge-a-b');
+18
View File
@@ -85,6 +85,24 @@ assert(
'Build GDS should not silently fall back to unrouted gdstk when links are present'
);
const routerDir = path.resolve(root, '..', 'mxpic_router', 'mxpic_router');
if (fs.existsSync(routerDir)) {
const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8');
const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8');
assert(
routerLoaderPy.includes('port_number: int = 1') &&
routerLoaderPy.includes('pitch: float = 10.0') &&
routerLoaderPy.includes('port_number=_int(element.get("port_number"'),
'mxpic_router loader should parse multi-port anchor metadata from exported elements'
);
assert(
routerBuilderPy.includes('for index in range(port_number):') &&
routerBuilderPy.includes('a{index + 1}') &&
routerBuilderPy.includes('b{index + 1}'),
'mxpic_router builder should register aN/bN pins for multi-port anchors'
);
}
assert(
serverPy.includes('def scoped_pdk_root_for_project') &&
serverPy.includes('read_project_meta(project_name).get("technology")') &&
+45 -2
View File
@@ -196,8 +196,51 @@ assert(
'holding a component and pressing Space should rotate it by 90 degrees'
);
assert(
canvasHtml.includes('getSpaceRotationTarget') && canvasHtml.includes('selectedSpaceNode'),
'Space rotation should also use the currently selected component when no mouse-hold target is active'
canvasHtml.includes('getSpaceRotationTarget') &&
canvasHtml.includes('selectedSpaceNode') &&
canvasHtml.includes('node.type !== \'rotatableNode\' && node.type !== \'portNode\' && node.type !== \'anchorNode\'') &&
canvasHtml.includes('node.type === \'portNode\' || node.data?.elementType === \'port\'') &&
canvasHtml.includes('angle: normalizeAngle(Number(node.data?.angle || 0) + 90)'),
'Space rotation should also rotate selected Port and Anchor elements'
);
assert(
canvasHtml.includes('const anchorRotation = data.rotation || 0') &&
canvasHtml.includes('const anchorVisualRotation = -Number(anchorRotation || 0)') &&
canvasHtml.includes('transform: `rotate(${anchorVisualRotation}deg)`') &&
canvasHtml.includes('buildPortHandles(localAnchorHandlePorts, { rotation: 0') &&
canvasHtml.includes('anchorDirectionHandles') &&
canvasHtml.includes('rotation: Number(anchorRotation || 0)') &&
canvasHtml.includes('anchorHandleVisualStyle(portHandle') &&
canvasHtml.includes('anchorPortVisualSide') &&
canvasHtml.includes('portHandle.name') &&
canvasHtml.includes('visualSide === \'left\' ? 0 : elementSize.width') &&
canvasHtml.includes('anchorPortVisualTop') &&
canvasHtml.includes('(index - 1) / (portCount - 1)') &&
canvasHtml.includes('elementSize.height - baseHandleStyle.height') &&
canvasHtml.includes('localLeft') &&
canvasHtml.includes('localTop') &&
canvasHtml.includes('handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]') &&
canvasHtml.includes('getAnchorHandleRouteDirection') &&
canvasHtml.includes('rotation: Number(node.data?.rotation || 0)') &&
canvasHtml.includes('directionToReactFlowPosition') &&
canvasHtml.includes('sourcePosition: directionToReactFlowPosition(sourceDirection)') &&
canvasHtml.includes('targetPosition: directionToReactFlowPosition(targetDirection)') &&
!canvasHtml.includes('type: \'parallelRoute\',\n data: {\n ...(edge.data || {}),\n parallelOffset: offset,\n sourceDirection,\n targetDirection') &&
!canvasHtml.includes('rotatedAnchorHandlePositions'),
'Anchor port circles should split into side columns and spread across the full anchor body while built-in rectangular links use rotated directions'
);
assert(
canvasHtml.includes('Port Number') &&
canvasHtml.includes('Pitch') &&
canvasHtml.includes('portNumber') &&
canvasHtml.includes('pitch') &&
canvasHtml.includes('DEFAULT_ELEMENT_PITCH') &&
canvasHtml.includes('buildElementBoxSize') &&
canvasHtml.includes('height: elementSize.height') &&
canvasHtml.includes('elementType: \'anchor\'') &&
canvasHtml.includes('pitch: DEFAULT_ELEMENT_PITCH') &&
canvasHtml.includes('ports: buildElementPorts'),
'Port and Anchor inspectors should expose port number and pitch, default to 10 um pitch, and grow in height'
);
assert(
canvasHtml.includes('const componentIndexesByPrefixRef = useRef({});') &&
+24
View File
@@ -17,3 +17,27 @@ assert(
canvasHtml.includes('layoutToCanvasY'),
'loading saved layout YAML should convert GDS/layout Y coordinates back to canvas coordinates'
);
assert(
canvasHtml.includes('buildElementNodesFromYaml'),
'project loading should rebuild saved anchor/port element nodes from YAML elements'
);
assert(
canvasHtml.includes('Object.entries(doc.elements || {})'),
'project loading should read doc.elements, not only doc.instances'
);
assert(
canvasHtml.includes('nodeNameMap[elementName] = nodeId'),
'loaded element names should be registered so saved links can reconnect to anchors and ports'
);
assert(
canvasHtml.includes('getAvailableComponentsForLoadedComponent'),
'project loading should reconstruct PDK component selection options for saved instances'
);
assert(
canvasHtml.includes('availableComponents: instIsForge ? [FORGE_COMPONENT_LABEL] : loadedAvailableComponents'),
'loaded PDK instances should keep availableComponents so the right panel can show the PDK selector'
);
assert(
canvasHtml.includes('Array.from(new Set([FORGE_COMPONENT_LABEL, ...sameCategoryComponents'),
'loaded PDK selector choices should include forge and same-category library components'
);