835 lines
30 KiB
JavaScript
835 lines
30 KiB
JavaScript
/*
|
|
* Description: Static and helper regression tests for MXPIC EDA frontend/backend integration contracts.
|
|
* Inside functions: N/A - assertion-based test/module script.
|
|
* Developer : Qin Yue @ 2026
|
|
* Organization : OptiHK Limited
|
|
*/
|
|
const assert = require('assert');
|
|
const helpers = require('../frontend/canvas-helpers.js');
|
|
|
|
const handles = helpers.buildPortHandles({
|
|
a0: { x: 0, y: 0, a: 180 },
|
|
b0: { x: 0, y: 0, a: 0 },
|
|
a1: { x: -91.7, y: 4.475, a: 180 },
|
|
a2: { x: -91.7, y: -4.475, a: 180 },
|
|
b1: { x: 91.7, y: 4.475, a: 0 },
|
|
b2: { x: 91.7, y: -4.475, a: 0 },
|
|
ep2a: { x: -37.8, y: -20, a: 270 },
|
|
ep2b: { x: -37.8, y: 20, a: 90 },
|
|
});
|
|
|
|
assert.deepStrictEqual(handles.map(handle => handle.name), ['a1', 'a2', 'b1', 'b2', 'ep2b', 'ep2a']);
|
|
assert.deepStrictEqual(handles.filter(handle => handle.position === 'left').map(handle => handle.name), ['a1', 'a2']);
|
|
assert.deepStrictEqual(handles.filter(handle => handle.position === 'right').map(handle => handle.name), ['b1', 'b2']);
|
|
assert.deepStrictEqual(handles.find(handle => handle.name === 'ep2b').position, 'top');
|
|
assert.deepStrictEqual(handles.find(handle => handle.name === 'ep2a').position, 'bottom');
|
|
assert.strictEqual(handles.find(handle => handle.name === 'a1').style.top, '15%');
|
|
assert.strictEqual(handles.find(handle => handle.name === 'a1').style.left, 0);
|
|
assert.strictEqual(handles.find(handle => handle.name === 'a2').style.top, '85%');
|
|
assert.strictEqual(handles.find(handle => handle.name === 'b1').style.left, '100%');
|
|
assert.strictEqual(handles.find(handle => handle.name === 'ep2b').style.left, '50%');
|
|
assert.strictEqual(handles.find(handle => handle.name === 'ep2b').style.top, 0);
|
|
assert.strictEqual(handles.find(handle => handle.name === 'ep2a').style.top, '100%');
|
|
|
|
const uniformLeftHandles = helpers.buildPortHandles({
|
|
p_top: { x: -10, y: 300, a: 180 },
|
|
p_mid: { x: -10, y: 20, a: 180 },
|
|
p_bottom: { x: -10, y: -5, a: 180 },
|
|
});
|
|
assert.deepStrictEqual(
|
|
uniformLeftHandles.map(handle => handle.style.top),
|
|
['15%', '50%', '85%'],
|
|
'ports on the same side should be uniformly spaced after sorting'
|
|
);
|
|
const exactCenteredHandles = helpers.buildPortHandles({
|
|
a1: { x: -91.7, y: 4.475, a: 180 },
|
|
a2: { x: -91.7, y: -4.475, a: 180 },
|
|
b1: { x: 91.7, y: 4.475, a: 0 },
|
|
b2: { x: 91.7, y: -4.475, a: 0 },
|
|
}, { boxSize: { width: 183.4, height: 29.65 } });
|
|
assert.strictEqual(
|
|
exactCenteredHandles.find(handle => handle.name === 'a1').style.top,
|
|
'34.907%',
|
|
'box_size-aware handles should use exact centered PDK y coordinates instead of uniform spacing'
|
|
);
|
|
assert.strictEqual(exactCenteredHandles.find(handle => handle.name === 'a2').style.top, '65.093%');
|
|
assert.strictEqual(exactCenteredHandles.find(handle => handle.name === 'b1').style.left, '100%');
|
|
const exactPositiveHandles = helpers.buildPortHandles({
|
|
a1: { x: 0, y: 10, a: 180 },
|
|
b1: { x: 150.4, y: 10, a: 0 },
|
|
a2: { x: 0, y: -10, a: 180 },
|
|
b2: { x: 150.4, y: -10, a: 0 },
|
|
}, { boxSize: { width: 150.4, height: 40 } });
|
|
assert.strictEqual(exactPositiveHandles.find(handle => handle.name === 'a1').style.top, '25%');
|
|
assert.strictEqual(exactPositiveHandles.find(handle => handle.name === 'a2').style.top, '75%');
|
|
const exactPortObjectHandles = helpers.buildPortHandles({
|
|
port_1: { x: 0, y: 10, a: 0 },
|
|
port_2: { x: 0, y: 0, a: 0 },
|
|
port_3: { x: 0, y: -10, a: 0 },
|
|
}, { boxSize: { width: 47, height: 58 } });
|
|
assert.deepStrictEqual(
|
|
exactPortObjectHandles.map(handle => handle.style.top),
|
|
['32.759%', '50%', '67.241%'],
|
|
'standalone Port handles should use their actual y offsets inside the Port body'
|
|
);
|
|
const exactAnchorHandles = helpers.buildPortHandles(
|
|
helpers.buildElementPorts('anchor', { portNumber: 3, pitch: 10 }),
|
|
{ boxSize: { width: 16, height: 58 } }
|
|
);
|
|
assert.deepStrictEqual(
|
|
exactAnchorHandles.filter(handle => handle.name.startsWith('a')).map(handle => handle.style.top),
|
|
['32.759%', '50%', '67.241%'],
|
|
'standalone Anchor handles should be centered around the Anchor body'
|
|
);
|
|
|
|
assert.deepStrictEqual(
|
|
helpers.normalizeBoxSize({ box_size: [946, 75] }),
|
|
{ width: 946, height: 75 },
|
|
'component box size should load from YAML box_size arrays'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.normalizeBoxSize({ box_size: ['946.0', '75.0'] }),
|
|
{ width: 946, height: 75 },
|
|
'component box size should accept numeric strings from YAML/JSON metadata'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.normalizeBoxSize({ box_sz: { width: 1200, height: 85 } }),
|
|
{ width: 1200, height: 85 },
|
|
'component box size should also accept box_sz objects'
|
|
);
|
|
assert.strictEqual(
|
|
helpers.PORT_NODE_SIZE,
|
|
30,
|
|
'Port and Anchor virtual elements should use the same 30 um canvas footprint'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.calculateLayoutBounds({
|
|
nodes: [{
|
|
position: { x: 100, y: 200 },
|
|
data: { componentName: 'rotated_component', boxSize: { width: 50, height: 20 }, rotation: 90 }
|
|
}]
|
|
}),
|
|
{
|
|
minX: 80,
|
|
minY: 200,
|
|
maxX: 100,
|
|
maxY: 250,
|
|
width: 20,
|
|
height: 50,
|
|
bottomLeft: { x: 80, y: 200 },
|
|
topRight: { x: 100, y: 250 }
|
|
},
|
|
'layout preview bounds should use component box_size and rotation to find device corners'
|
|
);
|
|
assert.strictEqual(
|
|
helpers.chooseCategoryComponent('generate with mxpic_forge', [
|
|
'generate with mxpic_forge',
|
|
'EC_SiN400_1310_0p5dB_L935_A0_QY_202604'
|
|
], 'edge_couplers'),
|
|
'EC_SiN400_1310_0p5dB_L935_A0_QY_202604',
|
|
'dropping an EC category should prefer the real PDK component so its YAML box_size is loaded'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.clampPositionToCanvas({ x: 4990, y: 5010 }, { width: 5000, height: 5000 }, { width: 946, height: 75 }),
|
|
{ x: 4054, y: 4925 },
|
|
'component drag position should keep the full component box inside the canvas boundary'
|
|
);
|
|
const rulerMeasurement = helpers.createRulerMeasurement({ x: 10, y: 20 }, { x: 40, y: 60 });
|
|
assert.deepStrictEqual(
|
|
rulerMeasurement,
|
|
{
|
|
start: { x: 10, y: 20 },
|
|
end: { x: 40, y: 60 },
|
|
dx: 30,
|
|
dy: 40,
|
|
distance: 50,
|
|
midpoint: { x: 25, y: 40 },
|
|
label: '50.000 um dx 30.000 dy 40.000'
|
|
},
|
|
'ruler measurement should calculate point-to-point distance in canvas um coordinates'
|
|
);
|
|
assert.strictEqual(
|
|
helpers.createRulerMeasurement({ x: 1 }, null),
|
|
null,
|
|
'ruler measurement should wait until both points are available'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.createComponentSymbolMetrics({ width: 946, height: 75 }),
|
|
{ width: 898.7, height: 51 },
|
|
'large edge-coupler symbols should scale close to the YAML box width instead of being capped near 300 um'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.createComponentSymbolMetrics({ width: 132, height: 82 }),
|
|
{ width: 118.8, height: 55.76 },
|
|
'default symbols should still scale proportionally inside normal component boxes'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.calculateCompositeBoxSize({
|
|
nodes: [
|
|
{
|
|
type: 'portNode',
|
|
position: { x: 10, y: 5 },
|
|
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10, width: 0.5 }
|
|
},
|
|
{
|
|
type: 'rotatableNode',
|
|
position: { x: 50, y: 20 },
|
|
data: { componentName: 'MMI_1', boxSize: { width: 80, height: 30 } }
|
|
},
|
|
{
|
|
type: 'rotatableNode',
|
|
position: { x: 160, y: 70 },
|
|
data: { componentName: 'MMI_2', boxSize: { width: 20, height: 10 } }
|
|
}
|
|
]
|
|
}),
|
|
{ width: 170, height: 75 },
|
|
'composite canvas symbols should use the bounds of exported ports and internal instance boxes'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.calculateCompositeBoxSize({
|
|
nodes: [{
|
|
type: 'portNode',
|
|
position: { x: 10, y: 5 },
|
|
data: { componentDisplayName: 'input', elementType: 'port', portNumber: 1, pitch: 10 }
|
|
}]
|
|
}),
|
|
helpers.DEFAULT_COMPONENT_BOX_SIZE,
|
|
'single-port empty canvases should keep the default component footprint'
|
|
);
|
|
|
|
const rotatedHandles = helpers.buildPortHandles({
|
|
left_port: { x: -50, y: 0, a: 180 },
|
|
top_port: { x: 0, y: 20, a: 90 },
|
|
}, { rotation: 180 });
|
|
assert.strictEqual(
|
|
rotatedHandles.find(handle => handle.name === 'left_port').position,
|
|
'right',
|
|
'rotating a component should rotate the React Flow handle side'
|
|
);
|
|
assert.strictEqual(
|
|
rotatedHandles.find(handle => handle.name === 'top_port').position,
|
|
'bottom',
|
|
'rotating a component should rotate vertical port handle sides'
|
|
);
|
|
const quarterTurnHandles = helpers.buildPortHandles({
|
|
out: { x: 50, y: 0, a: 0 },
|
|
up: { x: 0, y: 20, a: 90 },
|
|
}, { rotation: 90 });
|
|
assert.strictEqual(
|
|
quarterTurnHandles.find(handle => handle.name === 'out').position,
|
|
'bottom',
|
|
'90 degree canvas rotation should move a right-facing port direction to the bottom side'
|
|
);
|
|
assert.strictEqual(
|
|
quarterTurnHandles.find(handle => handle.name === 'up').position,
|
|
'right',
|
|
'90 degree canvas rotation should move a top-facing port direction to the right side'
|
|
);
|
|
const negativeQuarterTurnHandles = helpers.buildPortHandles({
|
|
out: { x: 50, y: 0, a: 0 },
|
|
down: { x: 0, y: -20, a: -90 },
|
|
}, { rotation: -90 });
|
|
assert.strictEqual(
|
|
negativeQuarterTurnHandles.find(handle => handle.name === 'out').position,
|
|
'top',
|
|
'-90 degree canvas rotation should move a right-facing port direction to the top side'
|
|
);
|
|
assert.strictEqual(
|
|
negativeQuarterTurnHandles.find(handle => handle.name === 'down').position,
|
|
'right',
|
|
'-90 degree canvas rotation should move a bottom-facing port direction to the right side'
|
|
);
|
|
|
|
const args = helpers.createForgeArguments();
|
|
assert(Object.keys(args).length >= 10);
|
|
assert.strictEqual(helpers.isForgeComponent('generate with mxpic_forge'), true);
|
|
assert.strictEqual(helpers.isBasicComponent('waveguide'), true);
|
|
assert.strictEqual(helpers.isBasicComponent('circle'), true);
|
|
assert.strictEqual(helpers.isBasicComponent('cricle'), true);
|
|
assert.strictEqual(
|
|
helpers.buildElementPorts('port').port.a,
|
|
0,
|
|
'Port objects should default to 0 degree angle'
|
|
);
|
|
assert.deepStrictEqual(
|
|
{
|
|
a1: helpers.buildElementPorts('anchor').a1.a,
|
|
b1: helpers.buildElementPorts('anchor').b1.a,
|
|
},
|
|
{ a1: 180, b1: 0 },
|
|
'Anchor objects should default to a1 for the left port and b1 for the right port'
|
|
);
|
|
assert.deepStrictEqual(
|
|
{
|
|
a1: helpers.buildElementPorts('anchor').a1,
|
|
b1: helpers.buildElementPorts('anchor').b1,
|
|
},
|
|
{
|
|
a1: { x: 0, y: -15, a: 180, width: 0.5 },
|
|
b1: { x: 0, y: -15, a: 0, width: 0.5 }
|
|
},
|
|
'Anchor a/b port pairs should share coordinates and keep opposite directions'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.buildBasicComponentPorts('waveguide', { length: 120, width: 0.6 }).b1,
|
|
{ x: 120, y: 0, a: 0, width: 0.6, xsection: 'strip', description: 'Optical power output' },
|
|
'basic waveguide ports should be generated from editable settings'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('waveguide', { length: 120, width: 0.5 }).box_size,
|
|
[120, 20],
|
|
'basic waveguide symbol should use a height that is two port-circle diameters'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('90 bend', { radius: 15 }).box_size,
|
|
[25, 25],
|
|
'90 bend symbol should not shrink below the radius-25 canvas size'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('90 bend', { radius: 30 }).box_size,
|
|
[30, 30],
|
|
'90 bend symbol should still scale above radius 25'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.buildBasicComponentPorts('90 bend', { radius: 15, width: 0.6 }),
|
|
{
|
|
a1: { x: 0, y: 12.5, a: 180, width: 0.6, xsection: 'strip', description: 'Optical power input' },
|
|
b1: { x: 12.5, y: 0, a: 90, width: 0.6, xsection: 'strip', description: 'Optical power output' }
|
|
},
|
|
'90 bend ports should sit at the middle of the left and top sides of the square'
|
|
);
|
|
const ninetyBendHandles = helpers.buildPortHandles(helpers.buildBasicComponentPorts('90 bend', { radius: 15 }));
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').position, 'left');
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').style.top, '50%');
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'a1').style.left, 0);
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').position, 'top');
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.left, '50%');
|
|
assert.strictEqual(ninetyBendHandles.find(handle => handle.name === 'b1').style.top, 0);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('180 bend', { radius: 15 }).box_size,
|
|
[25, 50],
|
|
'180 bend symbol should not shrink below the radius-25 canvas size'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('180 bend', { radius: 30 }).box_size,
|
|
[30, 60],
|
|
'180 bend symbol should still scale above radius 25'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('taper', { length: 80, width1: 0.4, width2: 1.2 }).box_size,
|
|
[80, 20],
|
|
'basic taper symbol should use a height that is two port-circle diameters'
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.getBasicComponentMetadata('taper', { length: 80, width1: 0.4, width2: 1.2 }).ports.a1.description,
|
|
'Optical power input',
|
|
'basic component metadata should include human-readable port descriptions'
|
|
);
|
|
|
|
const yaml = helpers.buildInstanceYaml({
|
|
instanceName: 'component_1',
|
|
componentName: 'generate with mxpic_forge',
|
|
componentPath: 'ignored/path',
|
|
position: { x: 12.34, y: -5 },
|
|
rotation: 90,
|
|
flip: true,
|
|
flop: true,
|
|
forgeArguments: { function_name: 'mmi1x2', length: 25.5, include_heater: true }
|
|
});
|
|
|
|
assert(yaml.includes('component: generate_with_forge'));
|
|
assert(yaml.includes('flip: 1'));
|
|
assert(yaml.includes('flop: 1'));
|
|
assert(yaml.includes('function_name: "mmi1x2"'));
|
|
assert(yaml.includes('length: 25.5'));
|
|
assert(yaml.includes('include_heater: true'));
|
|
|
|
const basicYaml = helpers.buildInstanceYaml({
|
|
instanceName: 'wg_1',
|
|
componentName: 'waveguide',
|
|
componentPath: 'ignored',
|
|
position: { x: 0, y: 0 },
|
|
rotation: 0,
|
|
flip: false,
|
|
flop: false,
|
|
basicArguments: { length: 88, width: 0.7, xsection: 'strip' }
|
|
});
|
|
assert(basicYaml.includes('component: waveguide'));
|
|
assert(basicYaml.includes('length: 88'));
|
|
assert(basicYaml.includes('width: 0.7'));
|
|
|
|
const projectInstancesYaml = helpers.buildInstancesYaml({
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
type: 'rotatableNode',
|
|
position: { x: 10, y: 20 },
|
|
data: {
|
|
componentDisplayName: 'component_1',
|
|
componentName: 'PDK_A',
|
|
rotation: 0
|
|
}
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
type: 'rotatableNode',
|
|
position: { x: 30, y: 40 },
|
|
data: {
|
|
componentDisplayName: 'cell_1',
|
|
componentName: 'canvas_1',
|
|
type: 'composite',
|
|
rotation: 90
|
|
}
|
|
}
|
|
],
|
|
resolveComponentPath: name => name === 'PDK_A' ? 'foundry/path/PDK_A' : name
|
|
});
|
|
|
|
assert(projectInstancesYaml.includes('component_1:'));
|
|
assert(projectInstancesYaml.includes('component: foundry/path/PDK_A'));
|
|
assert(projectInstancesYaml.includes('cell_1:'));
|
|
assert(projectInstancesYaml.includes('component: canvas_1'));
|
|
|
|
const pagePortsYaml = helpers.buildPortsYaml({ x: 50, y: 150, a: 90 });
|
|
assert(pagePortsYaml.includes('pins:'));
|
|
assert(!pagePortsYaml.includes('ports:'));
|
|
assert(pagePortsYaml.includes('- name: port_io1'));
|
|
assert(pagePortsYaml.includes('pin: io1'));
|
|
assert(pagePortsYaml.includes('x: 50.0'));
|
|
assert(pagePortsYaml.includes('y: -150.0'));
|
|
assert(pagePortsYaml.includes('angle: -90.0'));
|
|
|
|
const componentPorts = helpers.buildPageComponentPorts({ x: 12, y: -6, a: 180 });
|
|
assert.deepStrictEqual(componentPorts, {
|
|
port_io1: { element: 'port', pin: 'io1', x: 12, y: -6, a: 0, width: 0.5 }
|
|
});
|
|
|
|
const elementNodes = [
|
|
{
|
|
id: 'port-1',
|
|
type: 'portNode',
|
|
position: { x: 10, y: 20 },
|
|
data: {
|
|
componentDisplayName: 'in0',
|
|
elementType: 'port',
|
|
angle: 180,
|
|
width: 0.7,
|
|
layer: 'WG_CORE',
|
|
description: 'input port'
|
|
}
|
|
},
|
|
{
|
|
id: 'anchor-1',
|
|
type: 'rotatableNode',
|
|
position: { x: 30, y: 40 },
|
|
data: {
|
|
componentDisplayName: 'anchor_1',
|
|
componentName: 'Anchor',
|
|
elementType: 'anchor',
|
|
rotation: 0
|
|
}
|
|
},
|
|
{
|
|
id: 'mmi-1',
|
|
type: 'rotatableNode',
|
|
position: { x: 50, y: 60 },
|
|
data: {
|
|
componentDisplayName: 'component_1',
|
|
componentName: 'MMI',
|
|
rotation: 0
|
|
}
|
|
}
|
|
];
|
|
|
|
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')), ['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 }).a1, { x: 0, y: 6, a: 180, width: 0.5 });
|
|
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).b2, { x: 0, y: -6, a: 0, width: 0.5 });
|
|
assert.deepStrictEqual(helpers.buildElementPorts('anchor', { portNumber: 2, pitch: 12 }).a2, { x: 0, y: -6, 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.getNodePortCanvasPoint({
|
|
id: 'port-rotated',
|
|
type: 'portNode',
|
|
position: { x: 100, y: 200 },
|
|
data: {
|
|
elementType: 'port',
|
|
angle: 180,
|
|
portNumber: 3,
|
|
pitch: 10,
|
|
ports: helpers.buildElementPorts('port', { angle: 180, portNumber: 3, pitch: 10 })
|
|
}
|
|
}, 'port_1'),
|
|
{ x: 100, y: 210 },
|
|
'Port pin endpoint coordinates should rotate with the Port body'
|
|
);
|
|
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 1 }), { width: 47, height: 30 });
|
|
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 1 }), { width: 16, height: 30 });
|
|
assert.deepStrictEqual(helpers.buildElementBoxSize({ elementType: 'anchor', portNumber: 4, pitch: 10 }), { width: 16, height: 72 });
|
|
assert.deepStrictEqual(helpers.buildElementBoxSize({ portNumber: 4, pitch: 10 }), { width: 47, height: 72 });
|
|
assert.deepStrictEqual(
|
|
helpers.buildElementBoxSize({ elementType: 'port', portName: 'input_port' }),
|
|
{ width: 82, height: 30 },
|
|
'Port object width should grow to fit the displayed port instance name'
|
|
);
|
|
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_io1: { element: 'array', pin: 'io1', x: 100, y: 190, a: 180, width: 0.6 },
|
|
array_io2: { element: 'array', pin: 'io2', x: 100, y: 200, a: 180, width: 0.6 },
|
|
array_io3: { element: 'array', pin: 'io3', x: 100, y: 210, a: 180, width: 0.6 }
|
|
}
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.buildPageComponentPorts(null, [{
|
|
id: 'port-array-rotated-90',
|
|
type: 'portNode',
|
|
position: { x: 100, y: 200 },
|
|
data: { componentDisplayName: 'array', elementType: 'port', angle: 90, portNumber: 2, pitch: 10, width: 0.6 }
|
|
}]),
|
|
{
|
|
array_io1: { element: 'array', pin: 'io1', x: 95, y: 200, a: -90, width: 0.6 },
|
|
array_io2: { element: 'array', pin: 'io2', x: 105, y: 200, a: -90, width: 0.6 }
|
|
}
|
|
);
|
|
assert.deepStrictEqual(
|
|
helpers.buildPageComponentPorts(null, [{
|
|
id: 'port-array-rotated',
|
|
type: 'portNode',
|
|
position: { x: 100, y: 200 },
|
|
data: { componentDisplayName: 'array', elementType: 'port', angle: 180, portNumber: 3, pitch: 10, width: 0.6 }
|
|
}]),
|
|
{
|
|
array_io1: { element: 'array', pin: 'io1', x: 100, y: 210, a: 0, width: 0.6 },
|
|
array_io2: { element: 'array', pin: 'io2', x: 100, y: 200, a: 0, width: 0.6 },
|
|
array_io3: { element: 'array', pin: 'io3', x: 100, y: 190, a: 0, width: 0.6 }
|
|
},
|
|
'Rotated Port object pins should export rotated coordinates along with their rotated angle'
|
|
);
|
|
|
|
const canvasPortsYaml = helpers.buildCanvasPortsYaml(elementNodes);
|
|
assert(canvasPortsYaml.includes('pins:'));
|
|
assert(!canvasPortsYaml.includes('ports:'));
|
|
assert(canvasPortsYaml.includes('name: in0_io1'));
|
|
assert(canvasPortsYaml.includes('element: in0'));
|
|
assert(canvasPortsYaml.includes('pin: io1'));
|
|
assert(canvasPortsYaml.includes('description: "input port"'));
|
|
assert(canvasPortsYaml.includes('width: 0.7'));
|
|
assert(canvasPortsYaml.includes('y: -20.0'));
|
|
assert(canvasPortsYaml.includes('angle: 0.0'));
|
|
|
|
const elementsYaml = helpers.buildElementsYaml(elementNodes);
|
|
assert(elementsYaml.includes('in0:'));
|
|
assert(elementsYaml.includes('type: port'));
|
|
assert(elementsYaml.includes('anchor_1:'));
|
|
assert(elementsYaml.includes('type: anchor'));
|
|
assert(elementsYaml.includes('y: -20.0'));
|
|
assert(elementsYaml.includes('pin_number: 1'));
|
|
assert(elementsYaml.includes('name: in0_io1'));
|
|
assert(elementsYaml.includes('role: io1'));
|
|
assert(elementsYaml.includes('name: anchor_1_a1'));
|
|
assert(elementsYaml.includes('role: a1'));
|
|
assert(elementsYaml.includes('pitch: 10'));
|
|
|
|
const instancesWithoutElements = helpers.buildInstancesYaml({
|
|
nodes: elementNodes,
|
|
resolveComponentPath: name => name
|
|
});
|
|
assert(!instancesWithoutElements.includes('anchor_1:'));
|
|
assert(!instancesWithoutElements.includes('in0:'));
|
|
assert(instancesWithoutElements.includes('component_1:'));
|
|
assert(instancesWithoutElements.includes('y: -60.0'));
|
|
|
|
const multiPortComponentPorts = helpers.buildPageComponentPorts(null, elementNodes);
|
|
assert.deepStrictEqual(multiPortComponentPorts.in0_io1, { element: 'in0', pin: 'io1', x: 10, y: 20, a: 0, width: 0.7 });
|
|
|
|
const technologyManifest = {
|
|
defaults: { xsection: 'strip', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
|
xsections: {
|
|
strip: { family: 'optical', default_width: 0.45 },
|
|
rib_low: { family: 'optical', default_width: 0.5 },
|
|
metal_1: { family: 'electrical', default_width: 5 },
|
|
metal_2: { family: 'electrical', default_width: 6 }
|
|
},
|
|
routing_types: ['euler_bend', 'standard_bend']
|
|
};
|
|
|
|
const routeDefaults = helpers.createRouteSettings(technologyManifest);
|
|
assert.deepStrictEqual(routeDefaults, {
|
|
xsection: 'strip',
|
|
family: 'optical',
|
|
width: 0.45,
|
|
radius: 10,
|
|
routing_type: 'euler_bend',
|
|
bundle_group: '',
|
|
widthEdited: false
|
|
});
|
|
|
|
const groupedRouteDefaults = helpers.createRouteSettings(technologyManifest, { bundle_group: 'group_A' });
|
|
assert.strictEqual(groupedRouteDefaults.bundle_group, 'group_A');
|
|
|
|
const metalRoute = helpers.updateRouteXsection(routeDefaults, 'metal_1', technologyManifest);
|
|
assert.strictEqual(metalRoute.family, 'electrical');
|
|
assert.strictEqual(metalRoute.width, 5);
|
|
|
|
const manuallyEditedWidth = helpers.updateRouteField(routeDefaults, 'width', 0.62, technologyManifest);
|
|
const changedXsection = helpers.updateRouteXsection(manuallyEditedWidth, 'rib_low', technologyManifest);
|
|
assert.strictEqual(changedXsection.width, 0.62);
|
|
assert.strictEqual(changedXsection.family, 'optical');
|
|
|
|
const styledStrip = helpers.routeStyleForSettings({ xsection: 'strip', family: 'optical' }, false);
|
|
const styledMetal = helpers.routeStyleForSettings({ xsection: 'metal_1', family: 'electrical' }, true);
|
|
assert.notStrictEqual(styledStrip.style.stroke, styledMetal.style.stroke);
|
|
assert(styledMetal.style.strokeDasharray, 'electrical routes should use a visibly different line treatment');
|
|
assert(styledMetal.style.strokeWidth > styledStrip.style.strokeWidth);
|
|
|
|
const routeYaml = helpers.buildBundlesYaml({
|
|
nodes: [
|
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } }
|
|
],
|
|
edges: [{
|
|
id: 'edge-a-b',
|
|
source: 'a',
|
|
target: 'b',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: { xsection: 'metal_1', family: 'electrical', width: 5, radius: 20, routing_type: 'standard_bend' },
|
|
points: [{ x: 0, y: 0 }, { x: 40, y: 20 }]
|
|
}
|
|
}]
|
|
}, technologyManifest);
|
|
assert(routeYaml.includes('xsection: metal_1'));
|
|
assert(routeYaml.includes('family: electrical'));
|
|
assert(routeYaml.includes('radius: 20'));
|
|
assert(routeYaml.includes('routing_type: standard_bend'));
|
|
assert(routeYaml.includes('points:'));
|
|
assert(routeYaml.includes('x: 40.0'));
|
|
assert(routeYaml.includes('y: -20.0'));
|
|
|
|
const anchoredRouteYaml = helpers.buildBundlesYaml({
|
|
nodes: [
|
|
{
|
|
id: 'src-node',
|
|
type: 'rotatableNode',
|
|
position: { x: 10, y: 20 },
|
|
data: {
|
|
componentDisplayName: 'src_inst',
|
|
boxSize: [100, 40],
|
|
ports: { out: { x: -10, y: 0, a: 180 } }
|
|
}
|
|
},
|
|
{
|
|
id: 'dst-node',
|
|
type: 'rotatableNode',
|
|
position: { x: 120, y: 20 },
|
|
data: {
|
|
componentDisplayName: 'dst_inst',
|
|
boxSize: [100, 40],
|
|
ports: { in: { x: 10, y: 0, a: 0 } }
|
|
}
|
|
}
|
|
],
|
|
edges: [{
|
|
id: 'edge-src-dst',
|
|
source: 'src-node',
|
|
target: 'dst-node',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: { xsection: 'strip', family: 'optical', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
|
points: [{ x: 0, y: 0 }, { x: 80, y: 0 }, { x: 80, y: 60 }]
|
|
}
|
|
}]
|
|
}, technologyManifest);
|
|
assert(anchoredRouteYaml.includes('from: src_inst:out'));
|
|
assert(anchoredRouteYaml.includes('to: dst_inst:in'));
|
|
assert(anchoredRouteYaml.includes('x: 0.0'));
|
|
assert(anchoredRouteYaml.includes('y: -20.0'));
|
|
assert(anchoredRouteYaml.includes('x: 130.0'));
|
|
|
|
const freeRouteYaml = helpers.buildBundlesYaml({
|
|
nodes: [],
|
|
edges: [{
|
|
id: 'route-free-1',
|
|
source: '__free_route_route-free-1_start__',
|
|
target: '__free_route_route-free-1_end__',
|
|
data: {
|
|
freeRoute: true,
|
|
route: { xsection: 'strip', family: 'optical', width: 0.45, radius: 10, routing_type: 'euler_bend' },
|
|
points: [{ x: 10, y: 20 }, { x: 80, y: 20 }, { x: 80, y: 120 }]
|
|
}
|
|
}]
|
|
}, technologyManifest);
|
|
assert(freeRouteYaml.includes('id: "route-free-1"'));
|
|
assert(!freeRouteYaml.includes('from:'));
|
|
assert(!freeRouteYaml.includes('to:'));
|
|
assert(freeRouteYaml.includes('points:'));
|
|
assert(freeRouteYaml.includes('x: 80.0'));
|
|
assert(freeRouteYaml.includes('y: -120.0'));
|
|
|
|
const groupedBundlesYaml = helpers.buildBundlesYaml({
|
|
nodes: [
|
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } },
|
|
{ id: 'c', data: { componentDisplayName: 'inst_c' } },
|
|
{ id: 'd', data: { componentDisplayName: 'inst_d' } }
|
|
],
|
|
edges: [
|
|
{
|
|
id: 'edge-group-a',
|
|
source: 'a',
|
|
target: 'b',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: {
|
|
xsection: 'strip',
|
|
family: 'optical',
|
|
width: 0.45,
|
|
radius: 10,
|
|
routing_type: 'euler_bend',
|
|
bundle_group: 'optical_bus'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
id: 'edge-group-b',
|
|
source: 'c',
|
|
target: 'd',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: {
|
|
xsection: 'metal_1',
|
|
family: 'electrical',
|
|
width: 5,
|
|
radius: 20,
|
|
routing_type: 'standard_bend',
|
|
bundle_group: 'electrical_bus'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}, technologyManifest);
|
|
assert(groupedBundlesYaml.includes(' optical_bus:\n xsection: strip\n family: optical\n routing_type: euler_bend\n links:'));
|
|
assert(groupedBundlesYaml.includes(' electrical_bus:\n xsection: metal_1\n family: electrical\n routing_type: standard_bend\n links:'));
|
|
assert(groupedBundlesYaml.includes('from: inst_a:out'));
|
|
assert(groupedBundlesYaml.includes('from: inst_c:out'));
|
|
assert(!groupedBundlesYaml.includes('bundle_group:'), 'bundle_group should choose the YAML key, not be written inside links');
|
|
|
|
const splitFreeWireBundlesYaml = helpers.buildBundlesYaml({
|
|
nodes: [
|
|
{ id: 'a', data: { componentDisplayName: 'inst_a' } },
|
|
{ id: 'b', data: { componentDisplayName: 'inst_b' } },
|
|
{ id: 'c', data: { componentDisplayName: 'inst_c' } },
|
|
{ id: 'd', data: { componentDisplayName: 'inst_d' } }
|
|
],
|
|
edges: [
|
|
{
|
|
id: 'edge-free-strip',
|
|
source: 'a',
|
|
target: 'b',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: { xsection: 'strip', family: 'optical', width: 0.45, radius: 10, routing_type: 'euler_bend' }
|
|
}
|
|
},
|
|
{
|
|
id: 'edge-free-metal',
|
|
source: 'c',
|
|
target: 'd',
|
|
sourceHandle: 'out',
|
|
targetHandle: 'in',
|
|
data: {
|
|
route: { xsection: 'metal_1', family: 'electrical', width: 5, radius: 20, routing_type: 'standard_bend' }
|
|
}
|
|
}
|
|
]
|
|
}, technologyManifest);
|
|
assert(splitFreeWireBundlesYaml.includes(' free_wires:\n xsection: strip\n family: optical\n routing_type: euler_bend\n links:'));
|
|
assert(splitFreeWireBundlesYaml.includes(' free_wires_metal_1:\n xsection: metal_1\n family: electrical\n routing_type: standard_bend\n links:'));
|
|
assert(!splitFreeWireBundlesYaml.includes('bundle_group:'), 'free-wire bundle names should not be duplicated into link metadata');
|
|
|
|
const edgeA = {
|
|
id: 'edge-a-b',
|
|
source: 'a',
|
|
target: 'b',
|
|
data: { route: { family: 'optical' } }
|
|
};
|
|
const edgeB = {
|
|
id: 'edge-c-d',
|
|
source: 'c',
|
|
target: 'd',
|
|
data: { route: { family: 'optical' } }
|
|
};
|
|
const edgeC = {
|
|
id: 'edge-e-f',
|
|
source: 'e',
|
|
target: 'f',
|
|
data: { route: { family: 'electrical' } }
|
|
};
|
|
const crossingNodes = {
|
|
a: { position: { x: 0, y: 0 } },
|
|
b: { position: { x: 100, y: 100 } },
|
|
c: { position: { x: 0, y: 100 } },
|
|
d: { position: { x: 100, y: 0 } },
|
|
e: { position: { x: 0, y: 100 } },
|
|
f: { position: { x: 100, y: 0 } }
|
|
};
|
|
edgeA.data.route.xsection = 'strip';
|
|
edgeB.data.route.xsection = 'strip';
|
|
edgeC.data.route.xsection = 'metal_1';
|
|
const edgeD = {
|
|
id: 'edge-g-h',
|
|
source: 'e',
|
|
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');
|