From 9886b8c881545cebb3c3c77923a37efd263586ea Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Sun, 29 Dec 2024 01:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20portnames=20=E7=9A=84?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hook/render/index.js | 20 +++++- src/hook/render/instantiation.js | 120 +++++++++++++++++++++++++------ src/hook/render/layout.js | 120 ++++++++++++++++++++++++------- 3 files changed, 211 insertions(+), 49 deletions(-) diff --git a/src/hook/render/index.js b/src/hook/render/index.js index fae771a..d836de2 100644 --- a/src/hook/render/index.js +++ b/src/hook/render/index.js @@ -28,7 +28,7 @@ export class NetlistRender { children: [], edges: [], layoutOptions: { - // org.eclipse. 可以去除 + // ... 'org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers': 35, // node 之间的最小间距 'elk.spacing.nodeNode': 35, @@ -103,6 +103,8 @@ export class NetlistRender { // 构造模块树 for (const [moduleName, rawModule] of Object.entries(rawNet.modules)) { const module = new Module(moduleName, rawModule); + this.nameToModule.set(moduleName, module); + if (moduleName === this.topModuleName) { // 构造符合 elk 格式的节点数据 topModule = module; @@ -115,8 +117,6 @@ export class NetlistRender { this.elkGraph.children.push(...cellNodes); this.elkGraph.children.push(...constantNodes); this.elkGraph.edges.push(...connectionEdges); - - this.nameToModule.set(moduleName, module); } } @@ -453,4 +453,18 @@ export class NetlistRender { this.resizeMonitor.observe(element); } + /** + * @description 展开例化模块 + */ + expandInstance() { + + } + + + /** + * @description 关闭例化模块 + */ + closeInstance() { + + } } \ No newline at end of file diff --git a/src/hook/render/instantiation.js b/src/hook/render/instantiation.js index ad068bc..4c09371 100644 --- a/src/hook/render/instantiation.js +++ b/src/hook/render/instantiation.js @@ -1,6 +1,7 @@ import * as d3 from 'd3'; import { NetlistRender } from '.'; import { globalSetting } from '../global'; +import { LAYOUT_CONSTANT } from './layout'; export class InstantiationRender { /** @@ -28,9 +29,60 @@ export class InstantiationRender { * @description 将 elknode 关于 例化模块 的数据添加为 d3 数据项目 * @param {ElkNode} node */ - addAsD3DataItem(node) { + addAsD3DataItem(node) { + const nodeModule = this.rootRender.nameToModule.get(node.renderName); + const view = nodeModule.view; + + const portnames = []; + let inputCount = 0; + let outputCount = 0; + const instanceWidth = LAYOUT_CONSTANT.INSTANTIATION_WIDTH + 2 * LAYOUT_CONSTANT.PORT_INNER_PADDING; + + // 统计出两侧 port 字符串最长的 + let inputPortMaxLength = 0; + let outputPortMaxLength = 0; + + for (const portName of view.nameToPort.keys()) { + const port = view.nameToPort.get(portName); + if (port.direction === 'input') { + inputPortMaxLength = Math.max(portName.length, inputPortMaxLength); + } else { + outputPortMaxLength = Math.max(portName.length, outputPortMaxLength); + } + } + + for (const portName of view.nameToPort.keys()) { + const port = view.nameToPort.get(portName); + if (port.direction === 'input') { + inputCount ++; + const offsetY = inputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN + + (inputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT + + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT; + + portnames.push({ + name: portName, + x: 5, + y: offsetY, + align: 'left' + }); + } else { + outputCount ++; + const offsetY = outputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN + + (outputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT + + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT; + + portnames.push({ + name: portName, + x: instanceWidth - 5, + y: offsetY, + align: 'end' + }); + } + } + this.data.push({ id: node.id, + type: node.renderName, // 例化模块的模块名字 x: node.x, y: node.y, name: node.name, @@ -38,8 +90,10 @@ export class InstantiationRender { height: node.height, fill: 'var(--main-dark-color)', text: node.renderName, + portnames, rx: 3, - ry: 3 + ry: 3, + expandText: '📌' }); } @@ -57,36 +111,62 @@ export class InstantiationRender { .attr("transform", d => `translate(${d.x}, ${d.y})`); let instances = instantiationSelections.append('rect') + .attr('y', LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT) .attr('width', data => data.width) - .attr('height', data => data.height) + .attr('height', data => data.height - LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT) .attr('fill', d => d.fill); - let texts = instantiationSelections.append('text') - .attr('x', data => data.width / 2) // 文本的 x 坐标(居中) - .attr('y', data => data.height / 2) // 文本的 y 坐标(居中) - .attr('dominant-baseline', 'middle') // 文本垂直居中 - .attr('text-anchor', 'middle') // 文本水平居中 - .attr('fill', 'var(--foreground)') // 文本颜色 - .attr('font-size', '0') - .transition() - .duration(1000) - .attr('font-size', '15px') - .attr('class', 'port-caption') - .text(data => data.name); // 设置文本内容 - + let texts = instantiationSelections.append('text') + .attr('x', 0) + .attr('y', LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT - 8) + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'left') + .attr('fill', 'var(--main-dark-color)') + .attr('font-size', '0') + .attr('font-size', '12px') + .attr('class', 'port-caption') + .text(data => data.name + ' ' + data.expandText) + .each(function(data) { + const text = d3.select(this); + const bbox = text.node().getBBox(); + instantiationSelections.insert('rect', 'text') + .attr('x', -1) + .attr('y', LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT - bbox.height - 5) + .attr('width', bbox.width + 10) + .attr('height', bbox.height + 5) + .attr('fill', 'var(--main-color)'); + }) + .on('click', function(event) { + + }); + + + // 获取 port 相关的信息 + instantiationSelections.each(function(node) { + d3.select(this) // 选择当前节点 + .selectAll('.port-name') // 选择所有端口名称元素 + .data(node.portnames) // 绑定端口名称数据 + .enter() + .append('text') // 创建文本元素 + .attr('class', 'port-name') + .attr('x', d => d.x) // 使用端口的相对 x 坐标 + .attr('y', d => d.y) // 使用端口的相对 y 坐标 + .attr('dominant-baseline', 'middle') // 文本垂直居中 + .attr('text-anchor', d => d.align) + .attr('fill', 'var(--foreground)') // 文本颜色 + .attr('font-size', '10px') // 设置字体大小 + .text(d => d.name); // 设置文本内容 + }); + if (globalSetting.renderAnimation) { instances.transition() .duration(1000) .attr('stroke', 'var(--main-color)') .attr('stroke-width', 2) - .attr('rx', d => d.rx) - .attr('ry', d => d.ry); } else { instances.attr('stroke', 'var(--main-color)') .attr('stroke-width', 2) - .attr('rx', d => d.rx) - .attr('ry', d => d.ry); } instantiationSelections diff --git a/src/hook/render/layout.js b/src/hook/render/layout.js index d613128..09db0f1 100644 --- a/src/hook/render/layout.js +++ b/src/hook/render/layout.js @@ -5,8 +5,17 @@ import { globalLookup } from "../global"; 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, @@ -18,6 +27,11 @@ export const LAYOUT_CONSTANT = { // port 顶部的空余,一般用来放置标签的 PORT_TOP_MARGIN: 30, + // port 内部的 padding,用于绘制 + PORT_INNER_PADDING: 20, + + // 例化模块上部分的空缺区域 + INSTANCE_TITLE_HEIGHT: 10, // 常数 CONSTANT_WIDTH: 50, @@ -54,6 +68,15 @@ export class Module { * @type {ModuleView} */ this.view = new ModuleView(name, module); + + this.commonPartitionIndex = 10; + this.partitionIndex = 11; + + /** + * @description 统计一个例化模块 name 出现的次数,用来分配布局分组索引 + * @type {Map} + */ + this.instanceNameCounter = new Map(); } /** @@ -66,15 +89,13 @@ export class Module { // 绘制 ports for (const name of this.view.nameToPort.keys()) { const port = this.view.nameToPort.get(name); - - if (port.direction === 'input') { // 为 port 设置连接点 const portConnection = { id: dotConnect(port.id, '0'), renderType: 'portConnection', - width: 1, - height: 1, + width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, + height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: LAYOUT_CONSTANT.PORT_WIDTH, y: LAYOUT_CONSTANT.PORT_HEIGHT / 2 }; @@ -89,7 +110,7 @@ export class Module { height: LAYOUT_CONSTANT.PORT_HEIGHT, ports: [portConnection], layoutOptions: { - 'partitioning.partition': 1, + 'partitioning.partition': MIN_PARTITION_INDEX, 'org.eclipse.elk.portConstraints': 'FIXED_POS' } }; @@ -100,8 +121,8 @@ export class Module { const portConnection = { id: dotConnect(port.id, '0'), renderType: 'portConnection', - width: 1, - height: 1, + width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, + height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: 0, y: LAYOUT_CONSTANT.PORT_HEIGHT / 2 }; @@ -116,7 +137,7 @@ export class Module { height: LAYOUT_CONSTANT.PORT_HEIGHT, ports: [portConnection], layoutOptions: { - 'partitioning.partition': 999, + 'partitioning.partition': MAX_PARTITION_INDEX, 'org.eclipse.elk.portConstraints': 'FIXED_POS' } }; @@ -211,28 +232,74 @@ export class Module { ports, layoutOptions: { 'org.eclipse.elk.portConstraints': 'FIXED_POS', - 'partitioning.partition': 10 + 'partitioning.partition': this.commonPartitionIndex } }; nodes.push(node); } else { // 例化模块 + + // 统计分配到左右两侧的 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) + ); + // 宽度等于预设宽度+两倍的padding + const instanceWidth = LAYOUT_CONSTANT.INSTANTIATION_WIDTH + 2 * LAYOUT_CONSTANT.PORT_INNER_PADDING; // 创建器件节点的 port, port 和 connection 一一对应 - const ports = []; + 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; - ports.push({ - id: connection.id, - renderName: connectionName, - renderType: 'cellPort', - width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, - height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, - properties: { - 'port.side': portSide - } - }) + 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; + + ports.push({ + id: connection.id, + renderName: connectionName, + renderType: 'cellPort', + 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; + + ports.push({ + id: connection.id, + renderName: connectionName, + renderType: 'cellPort', + width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, + height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, + x: instanceWidth, + y: offsetY + }); + } } const node = { @@ -240,13 +307,14 @@ export class Module { name: cell.name, renderName: cell.type, renderType: 'cell', - width: LAYOUT_CONSTANT.INSTANTIATION_WIDTH, - height: LAYOUT_CONSTANT.INSTANTIATION_HEIGHT, + width: instanceWidth, + height: instanceHeight + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT, ports, layoutOptions: { // 强制固定 port 的方向 - 'org.eclipse.elk.portConstraints': 'FIXED_SIDE', - 'partitioning.partition': 10 + 'org.eclipse.elk.portConstraints': 'FIXED_POS', + // TODO: 同名例化模块对齐 + 'partitioning.partition': this.commonPartitionIndex } }; @@ -288,7 +356,7 @@ export class Module { height: LAYOUT_CONSTANT.CONSTANT_HEIGHT, layoutOptions: { 'elk.port.side': 'WEST', - 'partitioning.partition': 10 + 'partitioning.partition': this.commonPartitionIndex } };