Routing nchor added
This commit is contained in:
@@ -10,7 +10,7 @@ assert(
|
||||
'canvas.html should use buildInstancesYaml for layout instance export'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('buildCanvasPortsYaml(activePage.nodes)'),
|
||||
canvasHtml.includes('buildCanvasPortsYaml(page.nodes)'),
|
||||
'canvas.html should export ports from active canvas port nodes'
|
||||
);
|
||||
assert(
|
||||
@@ -22,16 +22,20 @@ assert(
|
||||
'project layout export should not filter out regular PDK instances'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Elements: {'),
|
||||
'library tree should add an Elements folder'
|
||||
canvasHtml.includes('Cells: cellEntries') &&
|
||||
canvasHtml.includes('Basic: basicEntries') &&
|
||||
canvasHtml.includes('PDK: library || {}'),
|
||||
'library tree should expose top-level Cells, Basic, and PDK groups'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes("__name__: 'Port'") && canvasHtml.includes("__name__: 'Anchor'"),
|
||||
'Elements folder should expose Port and Anchor as separate virtual components'
|
||||
'Basic folder should expose Port and Anchor as separate virtual components'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('const isElementComponentGrid = isComponentGrid && entries.every(([, childData]) => childData.__element__ === true);'),
|
||||
'Elements folder should bypass category-card grouping and render separate virtual component leaves'
|
||||
canvasHtml.includes('isDirectLeafGrid') &&
|
||||
canvasHtml.includes('childData.__cell__ === true || childData.__element__ === true || childData.__basic__ === true') &&
|
||||
canvasHtml.includes('<div className="category-grid">'),
|
||||
'Cells and Basic folders should bypass category grouping and render direct draggable leaves in a 2D grid'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('element-card-icon port-icon') && canvasHtml.includes('element-card-icon anchor-icon'),
|
||||
@@ -46,7 +50,7 @@ assert(
|
||||
'virtual elements should not show PDK or generate_with_forge component selection'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('buildElementsYaml(activePage.nodes)'),
|
||||
canvasHtml.includes('buildElementsYaml(page.nodes)'),
|
||||
'canvas layout export should include an elements section'
|
||||
);
|
||||
assert(
|
||||
|
||||
@@ -21,9 +21,169 @@ assert.strictEqual(handles.find(handle => handle.name === 'a1').style.top, '15%'
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'a2').style.top, '85%');
|
||||
assert.strictEqual(handles.find(handle => handle.name === 'ep2b').style.left, '50%');
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
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 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(
|
||||
{
|
||||
left: helpers.buildElementPorts('anchor').left.a,
|
||||
right: helpers.buildElementPorts('anchor').right.a,
|
||||
},
|
||||
{ left: 180, right: 0 },
|
||||
'Anchor objects should default to 180 degree left port and 0 degree right port'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
{
|
||||
left: helpers.buildElementPorts('anchor').left,
|
||||
right: helpers.buildElementPorts('anchor').right,
|
||||
},
|
||||
{
|
||||
left: { x: 0, y: -15, a: 180, width: 0.5 },
|
||||
right: { x: 30, y: -15, a: 0, width: 0.5 }
|
||||
},
|
||||
'Anchor ports should sit on the left and right edges of a port-sized circle'
|
||||
);
|
||||
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, 4],
|
||||
'basic waveguide symbol should use a narrow default height'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
helpers.getBasicComponentMetadata('90 bend', { radius: 15 }).box_size,
|
||||
[15, 15],
|
||||
'90 bend symbol should be square with side length equal to radius'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
helpers.getBasicComponentMetadata('180 bend', { radius: 15 }).box_size,
|
||||
[15, 30],
|
||||
'180 bend symbol should be one radius wide and two radii tall'
|
||||
);
|
||||
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',
|
||||
@@ -31,14 +191,32 @@ const yaml = helpers.buildInstanceYaml({
|
||||
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: [
|
||||
{
|
||||
@@ -74,7 +252,7 @@ assert(projectInstancesYaml.includes('component: canvas_1'));
|
||||
const pagePortsYaml = helpers.buildPortsYaml({ x: 50, y: 150, a: 90 });
|
||||
assert(pagePortsYaml.includes('- name: port'));
|
||||
assert(pagePortsYaml.includes('x: 50.0'));
|
||||
assert(pagePortsYaml.includes('y: 150.0'));
|
||||
assert(pagePortsYaml.includes('y: -150.0'));
|
||||
assert(pagePortsYaml.includes('angle: 90.0'));
|
||||
|
||||
const componentPorts = helpers.buildPageComponentPorts({ x: 12, y: -6, a: 180 });
|
||||
@@ -128,12 +306,14 @@ const canvasPortsYaml = helpers.buildCanvasPortsYaml(elementNodes);
|
||||
assert(canvasPortsYaml.includes('name: in0'));
|
||||
assert(canvasPortsYaml.includes('description: "input port"'));
|
||||
assert(canvasPortsYaml.includes('width: 0.7'));
|
||||
assert(canvasPortsYaml.includes('y: -20.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'));
|
||||
|
||||
const instancesWithoutElements = helpers.buildInstancesYaml({
|
||||
nodes: elementNodes,
|
||||
@@ -142,6 +322,7 @@ const instancesWithoutElements = helpers.buildInstancesYaml({
|
||||
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, { x: 10, y: 20, a: 180, width: 0.7 });
|
||||
@@ -193,13 +374,80 @@ const routeYaml = helpers.buildBundlesYaml({
|
||||
target: 'b',
|
||||
sourceHandle: 'out',
|
||||
targetHandle: 'in',
|
||||
data: { route: { xsection: 'metal_1', family: 'electrical', width: 5, radius: 20, routing_type: 'standard_bend' } }
|
||||
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 edgeA = {
|
||||
id: 'edge-a-b',
|
||||
@@ -227,5 +475,16 @@ const crossingNodes = {
|
||||
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' } }
|
||||
};
|
||||
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.findSameFamilyRouteCrossing(edgeB, [edgeA], crossingNodes).conflictEdge.id, 'edge-a-b');
|
||||
assert.strictEqual(helpers.findSameFamilyRouteCrossing(edgeC, [edgeA], crossingNodes), null);
|
||||
|
||||
@@ -30,6 +30,12 @@ assert(
|
||||
serverPy.includes('create_routed_layout_svg'),
|
||||
'save-layout route should use routed preview generation when links exist'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('cell_routes_path') &&
|
||||
serverPy.includes('write_route_points_sidecar') &&
|
||||
serverPy.includes('.routes.yml'),
|
||||
'save-layout should write a sidecar route-points file for manually drawn link guidance'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('svg_url'),
|
||||
'save-layout response should include an svg_url for the new layout tab'
|
||||
@@ -78,3 +84,20 @@ assert(
|
||||
gdsBuilderPy.includes('_cells_have_links') && gdsBuilderPy.includes('Routed Build GDS requires mxpic_router'),
|
||||
'Build GDS should not silently fall back to unrouted gdstk when links are present'
|
||||
);
|
||||
|
||||
assert(
|
||||
serverPy.includes('def scoped_pdk_root_for_project') &&
|
||||
serverPy.includes('read_project_meta(project_name).get("technology")') &&
|
||||
serverPy.includes('os.path.join(base_root, foundry, technology)'),
|
||||
'backend should resolve a project-scoped PDK root from selected foundry/technology'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('request.args.get(\'project\')') &&
|
||||
serverPy.includes('scoped_pdk_root_for_project(project)'),
|
||||
'library/component APIs should accept ?project= and search inside the selected technology folder'
|
||||
);
|
||||
assert(
|
||||
serverPy.includes('__path__') &&
|
||||
serverPy.includes('os.path.relpath(root, path_root)'),
|
||||
'library tree leaves should preserve component paths relative to the role PDK root'
|
||||
);
|
||||
|
||||
@@ -4,11 +4,16 @@ const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
|
||||
const canvasHelpers = fs.readFileSync(path.join(root, 'frontend', 'canvas-helpers.js'), 'utf8');
|
||||
|
||||
assert(
|
||||
canvasHtml.includes('Build GDS'),
|
||||
'Project Tree header should include a Build GDS button'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Save YAML for all canvases') && canvasHtml.includes('handleSaveProjectLayouts'),
|
||||
'Project Tree should include a save button that writes YAML for all canvases'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('/api/build-gds'),
|
||||
'Build GDS button should call the backend build-gds API'
|
||||
@@ -70,6 +75,280 @@ assert(
|
||||
'route editor should offer standard_bend as a routing type'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('findSameFamilyRouteCrossing'),
|
||||
'canvas should validate same-family route crossings'
|
||||
canvasHtml.includes('findSameTypeRouteCrossing'),
|
||||
'canvas should validate same-xsection route crossings'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('link-mode-tabs') &&
|
||||
canvasHtml.includes('link-mode-summary') &&
|
||||
canvasHtml.includes('link-mode-menu') &&
|
||||
canvasHtml.includes('currentLinkXsection') &&
|
||||
canvasHtml.includes('setCurrentLinkXsection') &&
|
||||
canvasHtml.includes("['strip', 'rib_low', 'metal_1', 'metal_2']"),
|
||||
'canvas should expose a collapsed route-type selector for new links'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('handleBasicConnection') &&
|
||||
canvasHtml.includes('onConnect={handleBasicConnection}') &&
|
||||
canvasHtml.includes('nodesConnectable={true}') &&
|
||||
canvasHtml.includes('connectionMode="loose"') &&
|
||||
canvasHtml.includes('data: { route }') &&
|
||||
canvasHtml.includes('addEdge(candidate, p.edges)'),
|
||||
'canvas should use React Flow native pin-to-pin connections for new links'
|
||||
);
|
||||
assert(
|
||||
!canvasHtml.includes('linkDraft') &&
|
||||
!canvasHtml.includes('routingMode') &&
|
||||
!canvasHtml.includes('toggleRoutingMode') &&
|
||||
!canvasHtml.includes('handleLinkCanvasMouseDown') &&
|
||||
!canvasHtml.includes('handleLinkCanvasMouseMove') &&
|
||||
!canvasHtml.includes('finalizeLinkDraft') &&
|
||||
!canvasHtml.includes('__link_draft_edge__') &&
|
||||
!canvasHtml.includes('Routing mode: click anywhere to start a point route.'),
|
||||
'current interactive point-link drawing mode should be removed from the canvas'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('<Handle') &&
|
||||
canvasHtml.includes('type="source"') &&
|
||||
canvasHtml.includes('type="target"') &&
|
||||
!canvasHtml.includes('nodesConnectable={false}'),
|
||||
'component and port handles should remain connectable for basic pin linking'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes("selectable: true") &&
|
||||
canvasHtml.includes("type: 'parallelRoute'") &&
|
||||
canvasHtml.includes('hasRoutePoints') &&
|
||||
canvasHtml.includes("hasRoutePoints ? 'parallelRoute' : view.type") &&
|
||||
canvasHtml.includes('data-route-edge-id') &&
|
||||
canvasHtml.includes('handleRouteEdgeMouseDown') &&
|
||||
canvasHtml.includes('selectEdgeById') &&
|
||||
canvasHtml.includes('vectorEffect="non-scaling-stroke"') &&
|
||||
canvasHtml.includes('onEdgeMouseDown={handleReactFlowEdgeMouseDown}'),
|
||||
'saved manual routes should remain selectable through a zoom-independent custom SVG hit path and React Flow edge mouse handling'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('panOnDrag={false}'),
|
||||
'left mouse drag should not pan the canvas'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('selectionOnDrag={true}') &&
|
||||
canvasHtml.includes('selectionMode={FULL_SELECTION_MODE}') &&
|
||||
canvasHtml.includes('SelectionMode'),
|
||||
'left mouse drag should draw a full-coverage selection area instead of panning'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('const path = points.length >= 2') &&
|
||||
canvasHtml.includes('points.slice(1).map(point => `L ${point.x},${point.y}`)') &&
|
||||
canvasHtml.includes('position: { x: first.x - 6, y: first.y - 6 }') &&
|
||||
canvasHtml.includes('position: { x: last.x - 6, y: last.y - 6 }'),
|
||||
'free routes should render all saved point-to-point line segments and keep hidden endpoints aligned'
|
||||
);
|
||||
assert(
|
||||
!canvasHtml.includes('buildOrthogonalPoints') &&
|
||||
!canvasHtml.includes('buildManhattanRoutePoints') &&
|
||||
!canvasHtml.includes('findNearestPort') &&
|
||||
!canvasHtml.includes('linkPreviewSnapPort'),
|
||||
'custom link drawing geometry helpers should be deleted from the current canvas code'
|
||||
);
|
||||
const routePointNodeBlock = canvasHtml.slice(
|
||||
canvasHtml.indexOf('const RulerPointNode'),
|
||||
canvasHtml.indexOf('const RulerMeasurementNode')
|
||||
);
|
||||
assert(
|
||||
routePointNodeBlock.includes('<Handle') &&
|
||||
routePointNodeBlock.includes('type="source"') &&
|
||||
routePointNodeBlock.includes('type="target"') &&
|
||||
routePointNodeBlock.includes('id="route"'),
|
||||
'route point nodes should expose hidden source and target handles so route edges can attach and render'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('build-progress') && canvasHtml.includes('buildProgress'),
|
||||
'Build Layout and Build GDS should show progress while running'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('port-name-label'),
|
||||
'component nodes should render visible port name labels'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('multiSelectionKeyCode="Shift"'),
|
||||
'ReactFlow should allow shift multi-select for links'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes("deleteKeyCode={['Backspace', 'Delete']}"),
|
||||
'ReactFlow should allow selected links to be deleted with the Delete key'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('selectedEdges'),
|
||||
'canvas should track multiple selected links for batch route editing'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('__mixed__'),
|
||||
'multi-link route editor should show mixed values as --'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Flip X') && canvasHtml.includes('Mirror Y'),
|
||||
'component inspector should expose flip/flop controls'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('onNodeMouseDown') && canvasHtml.includes('spaceRotateNodeIdRef'),
|
||||
'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'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('normalizeAngle,') && canvasHtml.includes('normalizeAngle(Number(node.data?.rotation || 0) + 90)'),
|
||||
'Space rotation should import normalizeAngle before using it'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('component-floating-label') && canvasHtml.includes('component-visual-body'),
|
||||
'component labels should float outside the rotated body'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('--floating-label-bg') && canvasHtml.includes('--port-label-bg') && canvasHtml.includes('--mini-button-bg'),
|
||||
'theme variables should keep labels, port chips, and header buttons readable in light and dark modes'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('className="site-nav-actions"') &&
|
||||
canvasHtml.includes('className="canvas-toolbar"') &&
|
||||
canvasHtml.includes('grid-snap-label') &&
|
||||
canvasHtml.includes('body.light-mode .canvas-toolbar'),
|
||||
'dashboard/logout should move to a site-level top-right action group and the canvas toolbar should keep readable grid text in light mode'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('body.light-mode .component-floating-label') &&
|
||||
canvasHtml.includes('body.light-mode .port-name-label') &&
|
||||
canvasHtml.includes('body.light-mode .mini-btn'),
|
||||
'light mode should override dark translucent label and button surfaces'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Canvas Size') && canvasHtml.includes('canvasSize') && canvasHtml.includes('DEFAULT_CANVAS_SIZE'),
|
||||
'project tree should expose a canvas size control with a 5000 um default'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('CanvasBoundaryNode') && canvasHtml.includes('canvas-boundary-node'),
|
||||
'canvas should render a bold boundary rectangle in flow coordinates'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('renderNodes') && canvasHtml.includes('nodeExtent={canvasNodeExtent}'),
|
||||
'ReactFlow should render the boundary node and constrain draggable nodes to the canvas extent'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('minZoom={0.02}') && canvasHtml.includes('defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}'),
|
||||
'large 5000 um canvases should zoom out far enough to fit on one screen'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('onWheel={handleWheel}') &&
|
||||
canvasHtml.includes('calculateLayoutBounds(activePage)') &&
|
||||
canvasHtml.includes('layoutBounds') &&
|
||||
canvasHtml.includes('stageWidth') &&
|
||||
canvasHtml.includes('stageHeight'),
|
||||
'layout preview should mouse-wheel zoom and size 100% from calculated box_size layout bounds'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('reactFlowInstance.fitBounds') &&
|
||||
canvasHtml.includes('width: activeCanvasSize.width') &&
|
||||
canvasHtml.includes('height: activeCanvasSize.height'),
|
||||
'switching canvases should fit the full canvas boundary instead of resetting to 100% zoom'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('boxSize') && canvasHtml.includes('normalizeBoxSize'),
|
||||
'component metadata box_size should drive rendered component box dimensions'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('chooseCategoryComponent') && canvasHtml.includes('[id, data.ports, data.componentName, data.boxSize]'),
|
||||
'category drops and metadata refreshes should remeasure components with YAML box sizes'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Background color="#334155" gap={10} size={1}'),
|
||||
'default grid spacing should be 10 um'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Ruler') &&
|
||||
canvasHtml.includes('rulerMode') &&
|
||||
canvasHtml.includes('onPaneClick={handleRulerPaneClick}') &&
|
||||
canvasHtml.includes('onNodeClick={handleRulerPaneClick}') &&
|
||||
canvasHtml.includes('onPaneMouseMove={handleRulerMouseMove}'),
|
||||
'canvas should expose a ruler mode controlled from the top toolbar, allow measuring on component bodies, and preview to the mouse'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('createRulerMeasurement') &&
|
||||
canvasHtml.includes('rulerPointNode') &&
|
||||
canvasHtml.includes('rulerMeasurementNode') &&
|
||||
canvasHtml.includes('rulerPreviewPoint') &&
|
||||
canvasHtml.includes('strokeDasharray: rulerPreviewMeasurement ? undefined') &&
|
||||
canvasHtml.includes('renderEdges'),
|
||||
'ruler mode should render temporary point nodes, a live solid preview edge, and a measurement label'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Box Size') &&
|
||||
canvasHtml.includes('selectedNodeBoxSize') &&
|
||||
canvasHtml.includes('box-size-readout'),
|
||||
'component inspector should show the selected component YAML box size'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('coordinate-grid'),
|
||||
'selected component coordinates should be displayed horizontally in the right panel'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('selectedPositionNodes.length > 1') &&
|
||||
canvasHtml.includes('const delta = val - Number') &&
|
||||
canvasHtml.includes('selectedNodes={selectedNodes}'),
|
||||
'multi-selected components should move together when editing X or Y in the inspector'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),
|
||||
'port information should append optional human-readable port descriptions'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('BASIC_COMPONENTS') &&
|
||||
canvasHelpers.includes('BASIC_COMPONENTS') &&
|
||||
canvasHelpers.includes('waveguide') &&
|
||||
canvasHelpers.includes('90 bend') &&
|
||||
canvasHelpers.includes('180 bend') &&
|
||||
canvasHelpers.includes('circle') &&
|
||||
canvasHelpers.includes('taper'),
|
||||
'component library should expose basic Nazca primitives'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('Cells: cellEntries') &&
|
||||
canvasHtml.includes('Basic: basicEntries') &&
|
||||
canvasHtml.includes('PDK: library || {}'),
|
||||
'component library should keep top-level Cells, Basic, and PDK folders'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('isDirectLeafGrid ? (') &&
|
||||
canvasHtml.includes('<div className="category-grid">') &&
|
||||
canvasHtml.includes("isUserCell ? 'compact-tree-card' : ''"),
|
||||
'Basic, Port, and Anchor entries should render as consistent 2D cards instead of compact list rows'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('ParallelRouteEdge') &&
|
||||
canvasHtml.includes('parallelOffset') &&
|
||||
canvasHtml.includes("type: 'parallelRoute'") &&
|
||||
canvasHtml.includes('edgeTypes={edgeTypes}'),
|
||||
'overlapped links should render with separated parallel edge paths'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('data: { route, points: routePoints }') &&
|
||||
canvasHtml.includes('normalizeRoutePoints(link.points'),
|
||||
'manual link route points should be stored on edges and restored from saved YAML'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('createComponentSymbolMetrics') &&
|
||||
!canvasHtml.includes('Math.min(128') &&
|
||||
!canvasHtml.includes('Math.min(64'),
|
||||
'component icon/symbol dimensions should scale with component box size without old hard caps'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('/api/library?project=') &&
|
||||
canvasHtml.includes('/api/component/${encodeURIComponent(componentName)}?project=') &&
|
||||
canvasHtml.includes('/api/component/${encodeURIComponent(compName)}?project='),
|
||||
'canvas should pass project context when loading library and component metadata'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('/api/component/${encodeURIComponent(componentData.name)}/image?project=') &&
|
||||
canvasHtml.includes('return obj.__path__.split'),
|
||||
'component images and saved component paths should use technology-scoped library metadata'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
|
||||
|
||||
assert(
|
||||
canvasHtml.includes('loadedProjectPage'),
|
||||
'project loading should reuse the saved project canvas instead of creating a duplicate empty project tab'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('nonProjectPages'),
|
||||
'project composite map should exclude the saved project page itself'
|
||||
);
|
||||
assert(
|
||||
canvasHtml.includes('layoutToCanvasY'),
|
||||
'loading saved layout YAML should convert GDS/layout Y coordinates back to canvas coordinates'
|
||||
);
|
||||
Reference in New Issue
Block a user