620 lines
23 KiB
JavaScript
620 lines
23 KiB
JavaScript
/**
|
||
* 将各个节点转换为 elknode 的数据结构
|
||
*/
|
||
|
||
import { globalLookup } from "../global";
|
||
import { pinkLog } from "../utils";
|
||
import { Cell, dotConnect, ModuleView } from "./yosys";
|
||
|
||
// 线段的宽度
|
||
export const LINE_WIDTH = 2;
|
||
// 分组最小 ID
|
||
export const MIN_PARTITION_INDEX = 1;
|
||
// 分组最大 ID
|
||
export const MAX_PARTITION_INDEX = 10000000;
|
||
// 每一个例化最多能占据的列数
|
||
export const MAX_INSTANCE_COL_NUM = 1000;
|
||
// 每一列最多的同类器件数量
|
||
export const MAX_SAME_TYPE_INSTANCE_NUM = 5;
|
||
// 其他常量
|
||
export const LAYOUT_CONSTANT = {
|
||
// port
|
||
PORT_WIDTH: 50,
|
||
PORT_HEIGHT: 25,
|
||
|
||
// 例化模块(这只是最小,height 会根据端口数量进行调整)
|
||
INSTANTIATION_WIDTH: 80,
|
||
INSTANTIATION_HEIGHT: 80,
|
||
|
||
// port 顶部的空余,一般用来放置标签的
|
||
PORT_TOP_MARGIN: 30,
|
||
// port 内部的 padding,用于绘制
|
||
PORT_INNER_PADDING: 20,
|
||
|
||
// 例化模块上部分的空缺区域
|
||
INSTANCE_TITLE_HEIGHT: 10,
|
||
INSTANCE_TOP_PADDING: 20,
|
||
INSTANCE_LEFT_MARGIN: 10,
|
||
INSTANCE_RIGHT_MARGIN: 10,
|
||
|
||
// 常数
|
||
CONSTANT_WIDTH: 50,
|
||
CONSTANT_HEIGHT: 30,
|
||
|
||
// 器件的端口
|
||
CELL_PORT_HEIGHT: 1,
|
||
CELL_PORT_WIDTH: 1,
|
||
CELL_LEFT_MARGIN: 10,
|
||
CELL_RIGHT_MARGIN: 10
|
||
};
|
||
|
||
export const ELK_DIRECTION = {
|
||
LEFT: 'WEST',
|
||
RIGHT: 'EAST',
|
||
TOP: 'NORTH',
|
||
BOTTOM: 'SOUTH'
|
||
}
|
||
|
||
|
||
export function makeEdgeId(fromId, toId) {
|
||
return fromId + '-' + toId;
|
||
}
|
||
|
||
export class Module {
|
||
/**
|
||
* @param {string} name
|
||
* @param {YosysNetModule} module
|
||
*/
|
||
constructor(name, module) {
|
||
this.module = module;
|
||
this.name = name;
|
||
|
||
/**
|
||
* @description
|
||
* @type {ModuleView}
|
||
*/
|
||
this.view = new ModuleView(name, module);
|
||
|
||
this.commonPartitionIndex = 10;
|
||
this.partitionIndex = 11;
|
||
|
||
/**
|
||
* @description 统计一个例化模块 name 出现的次数,用来分配布局分组索引
|
||
* @type {Map<string, number>}
|
||
*/
|
||
this.instanceNameCounter = new Map();
|
||
}
|
||
|
||
/**
|
||
* @description 从 ports 中创建所有变量相关的节点
|
||
* @returns {ElkNode[]}
|
||
*/
|
||
makeNetsElkNodes() {
|
||
const nodes = [];
|
||
|
||
// 绘制 ports
|
||
for (const name of this.view.nameToPort.keys()) {
|
||
const port = this.view.nameToPort.get(name);
|
||
|
||
// 根据长度估算 name 的渲染长度
|
||
const estimateLength = Math.max(12 * 0.6 * name.length + 10, LAYOUT_CONSTANT.PORT_WIDTH);
|
||
|
||
if (port.direction === 'input') {
|
||
// 为 port 设置连接点
|
||
const portConnection = {
|
||
id: dotConnect(port.id, '0'),
|
||
renderType: 'portConnection',
|
||
source: 'port',
|
||
// 对于 input 的 port 而言,它的端口只有一个且为输出
|
||
direction: 'output',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: estimateLength,
|
||
y: LAYOUT_CONSTANT.PORT_HEIGHT / 2
|
||
};
|
||
|
||
const node = {
|
||
id: port.id,
|
||
name: port.name,
|
||
renderName: name,
|
||
renderType: 'port',
|
||
source: 'port',
|
||
// 对于 input 的 port 而言,它的端口只有一个且为输入
|
||
direction: 'input',
|
||
type: port.direction,
|
||
width: estimateLength,
|
||
height: LAYOUT_CONSTANT.PORT_HEIGHT,
|
||
ports: [portConnection],
|
||
layoutOptions: {
|
||
'partitioning.partition': MIN_PARTITION_INDEX,
|
||
'org.eclipse.elk.portConstraints': 'FIXED_POS'
|
||
}
|
||
};
|
||
|
||
nodes.push(node);
|
||
} else {
|
||
// 为 port 设置连接点
|
||
const portConnection = {
|
||
id: dotConnect(port.id, '0'),
|
||
renderType: 'portConnection',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: 0,
|
||
y: LAYOUT_CONSTANT.PORT_HEIGHT / 2
|
||
};
|
||
|
||
const node = {
|
||
id: port.id,
|
||
name: port.name,
|
||
renderName: name,
|
||
renderType: 'port',
|
||
type: port.direction,
|
||
width: estimateLength,
|
||
height: LAYOUT_CONSTANT.PORT_HEIGHT,
|
||
ports: [portConnection],
|
||
layoutOptions: {
|
||
'partitioning.partition': MAX_PARTITION_INDEX,
|
||
'org.eclipse.elk.portConstraints': 'FIXED_POS'
|
||
}
|
||
};
|
||
|
||
nodes.push(node);
|
||
}
|
||
|
||
}
|
||
|
||
// 非 port 的其他内部变量的节点不需要绘制,因为它们总能被表示为中间变量
|
||
// 它们的操作可以表示为一系列器件的组合或者直连
|
||
|
||
return nodes;
|
||
}
|
||
|
||
/**
|
||
* @description 从 cells(器件) 中创建节点
|
||
* @returns {[ElkNode[], ElkEdge[]]}
|
||
*/
|
||
makeCellsElkNodes() {
|
||
const nodes = [];
|
||
const edges = [];
|
||
|
||
const skinManager = globalLookup.skinManager;
|
||
for (const name of this.view.nameToCell.keys()) {
|
||
const cell = this.view.nameToCell.get(name);
|
||
const skin = skinManager.querySkin(cell.type);
|
||
|
||
if (skin) {
|
||
const node = this.makeCellNode(cell, skin);
|
||
nodes.push(node);
|
||
} else {
|
||
const node = this.makeInstanceNode(cell);
|
||
nodes.push(node);
|
||
}
|
||
}
|
||
|
||
return nodes;
|
||
}
|
||
|
||
/**
|
||
* @description 从 cells.connections 中创建边
|
||
* @returns {[ElkNode[], ElkEdge[]]}
|
||
*/
|
||
makeConnectionElkNodes() {
|
||
const nodes = [];
|
||
|
||
// 完成去重
|
||
const id2EdgeCount = new Map();
|
||
const edges = [];
|
||
|
||
const tree = this.view;
|
||
|
||
for (const cellName of tree.nameToCell.keys()) {
|
||
const cell = tree.nameToCell.get(cellName);
|
||
|
||
for (const connectionName of cell.nameToConnection.keys()) {
|
||
const connection = cell.nameToConnection.get(connectionName);
|
||
// 遍历器件端口的每一个连接点
|
||
// 比如对于端口 input [31:0] data ,它的32个位不一定是完全导向一个变量(虽然我们愿意认为,大部分情况下是这样)
|
||
// 大部分情况下,👇的 wireIds.length 都是 1
|
||
for (const wire of connection.wires) {
|
||
const wireId = wire.wireId;
|
||
const id = wire.id;
|
||
if (typeof wireId === 'string') {
|
||
// 常数
|
||
// 如果是常数,需要先创建代表常数的节点,常数一定是器件的输入,而非输出
|
||
|
||
// 估算常量的宽度
|
||
let renderString = parseInt(wireId, 2);
|
||
if (isNaN(renderString)) {
|
||
if (wireId.toString().startsWith('x')) {
|
||
renderString = `${wireId.toString().length}{1'bx}`;
|
||
} else {
|
||
renderString = wireId.toString();
|
||
}
|
||
}
|
||
|
||
const constantWidth = Math.max(12 * 0.6 * renderString.toString().length + 10, LAYOUT_CONSTANT.CONSTANT_WIDTH);
|
||
console.log(constantWidth);
|
||
|
||
const constantConnection = {
|
||
id: dotConnect(id, '0'),
|
||
renderType: 'portConnection',
|
||
source: 'port',
|
||
// 对于 constant 的 port 而言,它的端口只有一个且为输出
|
||
direction: 'output',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: constantWidth,
|
||
y: LAYOUT_CONSTANT.CONSTANT_HEIGHT / 2
|
||
};
|
||
|
||
const node = {
|
||
id,
|
||
name: wireId,
|
||
renderType: 'constant',
|
||
width: constantWidth,
|
||
height: LAYOUT_CONSTANT.CONSTANT_HEIGHT,
|
||
ports: [constantConnection],
|
||
layoutOptions: {
|
||
'partitioning.partition': this.commonPartitionIndex,
|
||
'org.eclipse.elk.portConstraints': 'FIXED_POS'
|
||
}
|
||
};
|
||
|
||
nodes.push(node);
|
||
|
||
// 创建常数到器件的连线
|
||
const edge = {
|
||
id: makeEdgeId(cell.id, id),
|
||
source: id,
|
||
sourcePort: constantConnection.id,
|
||
target: cell.id,
|
||
targetPort: connection.id
|
||
};
|
||
|
||
edges.push(edge);
|
||
} else {
|
||
// 如果不是一个常数连接,那么存在两种可能
|
||
// 1. 当前的器件的这个端口和某一个 port 连接
|
||
// 2. 当前的器件的这个端口和另一个器件的一个端口连接
|
||
if (tree.wireIdToPort.has(wireId)) {
|
||
|
||
// 当前的器件的这个端口和某一个 port 连接
|
||
const port = tree.wireIdToPort.get(wireId);
|
||
|
||
// 器件的端口的方向有比 port 更高的优先级。
|
||
// 器件当前的口为 input,那么所连接的 port 就必须是 input,即便这个 port 是 output
|
||
|
||
if (connection.direction === 'input') {
|
||
const edgeId = makeEdgeId(port.id, connection.id);
|
||
if (!id2EdgeCount.has(edgeId)) {
|
||
id2EdgeCount.set(edgeId, 0);
|
||
|
||
const edge = {
|
||
// id 遵循 sourcePort-targetPort
|
||
id: edgeId,
|
||
source: port.id,
|
||
sourcePort: dotConnect(port.id, '0'),
|
||
target: cell.id,
|
||
targetPort: connection.id
|
||
};
|
||
edges.push(edge);
|
||
}
|
||
const counter = id2EdgeCount.get(edgeId);
|
||
id2EdgeCount.set(edgeId, counter + 1);
|
||
|
||
} else {
|
||
const edgeId = makeEdgeId(connection.id, port.id);
|
||
if (!id2EdgeCount.has(edgeId)) {
|
||
id2EdgeCount.set(edgeId, 0);
|
||
const edge = {
|
||
id: edgeId,
|
||
source: cell.id,
|
||
sourcePort: connection.id,
|
||
target: port.id,
|
||
targetPort: dotConnect(port.id, '0')
|
||
};
|
||
edges.push(edge);
|
||
}
|
||
|
||
const counter = id2EdgeCount.get(edgeId);
|
||
id2EdgeCount.set(edgeId, counter + 1);
|
||
|
||
}
|
||
} else if (tree.wireIdToConnection.has(wireId)) {
|
||
|
||
// 当前的器件的这个端口和另一个器件的一个端口连接
|
||
const conn = tree.wireIdToConnection.get(wireId);
|
||
|
||
// 防止查询到自己
|
||
if (conn.id === connection.id && conn.cell.id === cell.id) {
|
||
continue;
|
||
}
|
||
|
||
if (conn.direction === 'input') {
|
||
const edgeId = makeEdgeId(conn.id, connection.id);
|
||
if (!id2EdgeCount.has(edgeId)) {
|
||
id2EdgeCount.set(edgeId, 0);
|
||
const edge = {
|
||
// id 遵循 sourcePort-targetPort
|
||
id: edgeId,
|
||
source: cell.id,
|
||
sourcePort: connection.id,
|
||
target: conn.cell.id,
|
||
targetPort: conn.id
|
||
};
|
||
edges.push(edge);
|
||
}
|
||
|
||
const counter = id2EdgeCount.get(edgeId);
|
||
id2EdgeCount.set(edgeId, counter + 1);
|
||
|
||
} else {
|
||
const edgeId = makeEdgeId(connection.id, conn.id);
|
||
if (!id2EdgeCount.has(edgeId)) {
|
||
id2EdgeCount.set(edgeId, 0);
|
||
const edge = {
|
||
id: edgeId,
|
||
source: conn.cell.id,
|
||
sourcePort: conn.id,
|
||
target: cell.id,
|
||
targetPort: connection.id
|
||
};
|
||
edges.push(edge);
|
||
}
|
||
|
||
const counter = id2EdgeCount.get(edgeId);
|
||
id2EdgeCount.set(edgeId, counter + 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
pinkLog('#edge: ' + edges.length);
|
||
this.id2EdgeCount = id2EdgeCount;
|
||
|
||
return [nodes, edges];
|
||
}
|
||
|
||
/**
|
||
* @description 制作器件对应的 node
|
||
* @param {Cell} cell
|
||
* @param {import("../skin").SkinResource} skin
|
||
*/
|
||
makeCellNode(cell, skin) {
|
||
// 内置器件
|
||
// 创建器件节点的 port, port 和 connection 一一对应
|
||
const meta = skin.meta;
|
||
|
||
const height = meta.height;
|
||
const width = meta.width + LAYOUT_CONSTANT.CELL_LEFT_MARGIN + LAYOUT_CONSTANT.CELL_RIGHT_MARGIN;
|
||
|
||
const ports = [];
|
||
|
||
// 统计分配到左右两侧的 port
|
||
const leftSideConnections = [];
|
||
const rightSideConnections = [];
|
||
const topSideConnections = [];
|
||
|
||
for (const connection of cell.nameToConnection.values()) {
|
||
const yOffset = meta.getPortYOffset(connection.name);
|
||
if (yOffset === 0) {
|
||
topSideConnections.push(connection);
|
||
} else if (connection.direction === 'input') {
|
||
leftSideConnections.push(connection);
|
||
} else {
|
||
rightSideConnections.push(connection);
|
||
}
|
||
}
|
||
|
||
// 计算上侧的
|
||
for (let i = 0; i < topSideConnections.length; ++ i) {
|
||
const connection = topSideConnections[i];
|
||
const xOffset = meta.getPortXOffset(connection.name);
|
||
|
||
console.log(xOffset);
|
||
|
||
ports.push({
|
||
id: connection.id,
|
||
renderName: connection.name,
|
||
renderType: 'cellPort',
|
||
direction: 'input',
|
||
source: 'cell',
|
||
isVertical: true,
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: xOffset,
|
||
y: 0
|
||
});
|
||
}
|
||
|
||
// 计算左侧的
|
||
for (let i = 0; i < leftSideConnections.length; ++ i) {
|
||
const connection = leftSideConnections[i];
|
||
const yOffset = meta.getPortYOffset(connection.name);
|
||
|
||
ports.push({
|
||
id: connection.id,
|
||
renderName: connection.name,
|
||
renderType: 'cellPort',
|
||
direction: 'input',
|
||
source: 'cell',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: 0,
|
||
y: yOffset
|
||
});
|
||
}
|
||
|
||
// 计算右侧的
|
||
for (let i = 0; i < rightSideConnections.length; ++ i) {
|
||
const connection = rightSideConnections[i];
|
||
const yOffset = meta.getPortYOffset(connection.name);
|
||
|
||
ports.push({
|
||
id: connection.id,
|
||
renderName: connection.name,
|
||
renderType: 'cellPort',
|
||
direction: 'output',
|
||
source: 'cell',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: width,
|
||
y: yOffset
|
||
});
|
||
}
|
||
|
||
const node = {
|
||
id: cell.id,
|
||
name: cell.name,
|
||
renderName: cell.type,
|
||
renderType: 'cell',
|
||
width,
|
||
height,
|
||
ports,
|
||
layoutOptions: {
|
||
'org.eclipse.elk.portConstraints': 'FIXED_POS',
|
||
'partitioning.partition': this.commonPartitionIndex
|
||
}
|
||
};
|
||
|
||
return node;
|
||
}
|
||
|
||
/**
|
||
* @description 制作例化模块对应的 node
|
||
* @param {Cell} cell
|
||
*/
|
||
makeInstanceNode(cell) {
|
||
// 统计分配到左右两侧的 port
|
||
const leftSideConnections = [];
|
||
const rightSideConnections = [];
|
||
|
||
for (const conn of cell.nameToConnection.values()) {
|
||
if (conn.direction === 'input') {
|
||
leftSideConnections.push(conn);
|
||
} else {
|
||
rightSideConnections.push(conn);
|
||
}
|
||
}
|
||
|
||
// 根据一侧的 port 数量来决定 height
|
||
const maxPortNum = Math.max(leftSideConnections.length, rightSideConnections.length);
|
||
const instanceHeight = Math.max(
|
||
// 这是例化模块高度的最小值
|
||
LAYOUT_CONSTANT.INSTANTIATION_HEIGHT,
|
||
// 每一个 connection 占据的空间为:【上方空余区域(用来防止文本的)】 + 【本身的高度】
|
||
(LAYOUT_CONSTANT.PORT_TOP_MARGIN + LAYOUT_CONSTANT.CELL_PORT_HEIGHT) * (maxPortNum + 1)
|
||
);
|
||
// 宽度等于预设宽度 + 两倍的 inner padding + left margin + right margin
|
||
const instanceWidth = LAYOUT_CONSTANT.INSTANTIATION_WIDTH +
|
||
2 * LAYOUT_CONSTANT.PORT_INNER_PADDING +
|
||
LAYOUT_CONSTANT.INSTANCE_LEFT_MARGIN +
|
||
LAYOUT_CONSTANT.INSTANCE_RIGHT_MARGIN;
|
||
|
||
// 创建器件节点的 port, port 和 connection 一一对应
|
||
const ports = [];
|
||
let inputCount = 0;
|
||
let outputCount = 0;
|
||
for (const connectionName of cell.nameToConnection.keys()) {
|
||
const connection = cell.nameToConnection.get(connectionName);
|
||
const portSide = connection.direction === 'input' ? ELK_DIRECTION.LEFT: ELK_DIRECTION.RIGHT;
|
||
if (connection.direction === 'input') {
|
||
inputCount ++;
|
||
const offsetY = inputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN +
|
||
(inputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT +
|
||
LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING;
|
||
|
||
ports.push({
|
||
id: connection.id,
|
||
renderName: connectionName,
|
||
renderType: 'cellPort',
|
||
direction: 'input',
|
||
source: 'instance',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: 0,
|
||
y: offsetY
|
||
});
|
||
} else {
|
||
outputCount ++;
|
||
const offsetY = outputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN +
|
||
(outputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT +
|
||
LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING;
|
||
|
||
ports.push({
|
||
id: connection.id,
|
||
renderName: connectionName,
|
||
renderType: 'cellPort',
|
||
direction: 'output',
|
||
source: 'instance',
|
||
width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
|
||
height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
|
||
x: instanceWidth,
|
||
y: offsetY
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
const node = {
|
||
id: cell.id,
|
||
name: cell.name,
|
||
parentName: this.name,
|
||
renderName: cell.type,
|
||
renderType: 'instance',
|
||
width: instanceWidth,
|
||
height: instanceHeight + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING,
|
||
ports,
|
||
layoutOptions: {
|
||
// 强制固定 port 的方向
|
||
'org.eclipse.elk.portConstraints': 'FIXED_POS',
|
||
// TODO: 同名例化模块对齐
|
||
'partitioning.partition': this.commonPartitionIndex
|
||
}
|
||
};
|
||
|
||
return node;
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {import('../jsdoc').ElkPort} port
|
||
* @returns {ConnectionMarginParameter}
|
||
*/
|
||
export function getMarginParamter(port) {
|
||
if (!port) {
|
||
// 如果是例化模块,那么当前的 port 就是空
|
||
// 因为例化模块内部的变量一定和外部直连,所以例化模块内部是没有 port 的
|
||
return {
|
||
leftMargin: 0,
|
||
rightMargin: 0
|
||
}
|
||
}
|
||
switch (port.source) {
|
||
case 'instance':
|
||
return {
|
||
leftMargin: LAYOUT_CONSTANT.INSTANCE_LEFT_MARGIN,
|
||
rightMargin: LAYOUT_CONSTANT.INSTANCE_RIGHT_MARGIN
|
||
}
|
||
case 'cell':
|
||
return {
|
||
leftMargin: LAYOUT_CONSTANT.CELL_LEFT_MARGIN,
|
||
rightMargin: LAYOUT_CONSTANT.CELL_RIGHT_MARGIN
|
||
}
|
||
default:
|
||
return {
|
||
leftMargin: 0,
|
||
rightMargin: 0
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @typedef ConnectionMarginParameter
|
||
* @property {number} leftMargin
|
||
* @property {number} rightMargin
|
||
*/ |