import * as d3 from 'd3'; import ELK from 'elkjs'; import { Module } from './layout'; import { globalLookup, globalSetting } from '../global'; import { registerCellDragEvent } from './drag'; export class NetlistRender { /** * * @param {YosysRawNet} rawNet * @param {number} renderHeight * @param {number} renderWidth */ constructor() { /** * @type {ElkGraph} */ this.elkGraph = { id: 'root', layoutOptions: { 'elk.progressBar': true, 'layered.nodePlacement.strategy': 'SIDE_BASED' }, children: [], edges: [] }; /** * @type {Map} */ this.nameToModule = new Map(); } /** * @description 加载 yosys 格式的 json * @param {YosysRawNet} rawNet */ load(rawNet) { /** * @type {YosysRawNet} */ this.rawNet = rawNet; // 转换为 elkjs 格式的 graph for (const [moduleName, rawModule] of Object.entries(rawNet.modules)) { const top = parseInt(rawModule.attributes.top); // 一开始只渲染 top 模块 if (top) { const module = new Module(moduleName, rawModule); const portNodes = module.makeNetsElkNodes(); const cellNodes = module.makeCellsElkNodes(); const [constantNodes, connectionEdges] = module.makeConnectionElkNodes(); this.elkGraph.children.push(...portNodes); this.elkGraph.children.push(...cellNodes); this.elkGraph.children.push(...constantNodes); this.elkGraph.edges.push(...connectionEdges); this.nameToModule.set(moduleName, module); } } } /** * @description 根据信息创建布局对象 * @returns {Promise} */ async createLayout() { const elk = new ELK(); const graph = this.elkGraph; const layoutGraph = await elk.layout(graph, { layoutOptions: { 'org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers': 35, 'org.eclipse.elk.spacing.nodeNode': 35, 'org.eclipse.elk.layered.layering.strategy': 'LONGEST_PATH' } }); return layoutGraph; } /** * * @param {ElkNode} computedLayout * @param {string} container 类似于 "#chart" * @returns {d3.Selection} */ async render(computedLayout, container) { const virtualHeight = computedLayout.height; const virtualWidth = computedLayout.width; // 根据 height 进行放缩(可以通过设置进行调整) const ratio = this.renderHeight / virtualHeight; // 遍历计算布局进行创建 const svg = d3.select(container) .selectAll('svg') .attr('width', virtualWidth) .attr('height', virtualHeight); await this.renderLine(svg, computedLayout, ratio); await this.renderEntity(svg, computedLayout, ratio); this.selection = svg; return svg; } /** * @description 绘制实体 * @param {d3.Selection} svg * @param {ElkNode} computedLayout * @param {number} ratio */ async renderEntity(svg, computedLayout, ratio) { // node 可能是如下的几类 // - module 的 port // - 器件(基础器件 & 例化模块) // - 器件的 port // 生成用于绘制的 d3 数据结构 // 默认需要渲染成矩形的(缺失样式的器件、例化模块等等) const squares = []; const connections = []; const svgElements = []; const skinManager = globalLookup.skinManager; for (const node of computedLayout.children) { const skin = skinManager.querySkin(node.renderName); if (skin) { // 具有 skin 的器件 svgElements.push({ element: skin.meta.svgDoc.documentElement, x: node.x, y: node.y, width: node.width, height: node.height, fill: 'var(--main-dark-color)', }); } else { // 没有 skin 的器件 squares.push({ x: node.x, y: node.y, width: node.width, height: node.height, fill: 'var(--main-dark-color)', text: node.renderName, rx: 3, ry: 3 }); } // 如果存在 port,绘制 port for (const cellPort of node.ports || []) { connections.push({ x: cellPort.x + node.x, y: cellPort.y + node.y + 0.5, // 0.5 是为了线宽 width: cellPort.width, height: cellPort.height, fill: 'var(--main-color)', text: '', r: 3.5 }); } } if (globalSetting.renderAnimation) { svg.selectAll('rect') .data(squares) .enter() .append('rect') .attr('x', data => data.x) .attr('y', data => data.y) .attr('width', data => data.width) .attr('height', data => data.height) .attr('fill', d => d.fill) .transition() .duration(1000) .attr('stroke', 'var(--main-color)') .attr('stroke-width', 2) .attr('rx', d => d.rx) .attr('ry', d => d.ry); const renderSvg = svg.selectAll('g') .data(svgElements) .enter() .append(data => { const element = data.element; element.setAttribute('x', data.x); element.setAttribute('y', data.y); element.setAttribute('stroke-opacity', 0); return element; }) .transition() .duration(1000) .attr('stroke-opacity', 1); registerCellDragEvent(renderSvg); svg.selectAll('circle') .data(connections) .enter() .append('circle') .attr('cx', data => data.x) .attr('cy', data => data.y) .attr('width', data => data.width) .attr('height', data => data.height) .transition() .duration(1000) .attr('fill', d => d.fill) .attr('r', d => d.r); svg.selectAll('text') .data(squares) .enter() .append('text') .attr('x', data => data.x + data.width / 2) // 文本的 x 坐标(居中) .attr('y', data => data.y + data.height / 2) // 文本的 y 坐标(居中) .attr('dominant-baseline', 'middle') // 文本垂直居中 .attr('text-anchor', 'middle') // 文本水平居中 .attr('fill', 'white') // 文本颜色 .attr('font-size', '0') .transition() .duration(1000) .attr('font-size', '12px') .text(data => data.text); // 设置文本内容 } else { svg.selectAll('rect') .data(squares) .enter() .append('rect') .attr('x', data => data.x) .attr('y', data => data.y) .attr('width', data => data.width) .attr('height', data => data.height) .attr('fill', d => d.fill) .attr('stroke', 'var(--main-color)') .attr('stroke-width', 2) .attr('rx', d => d.rx) .attr('ry', d => d.ry); svg.selectAll('g') .data(svgElements) .enter() .append(data => { const element = data.element; element.setAttribute('x', data.x); element.setAttribute('y', data.y); return element; }); svg.selectAll('circle') .data(connections) .enter() .append('circle') .attr('cx', data => data.x) .attr('cy', data => data.y) .attr('width', data => data.width) .attr('height', data => data.height) .attr('fill', d => d.fill) .attr('r', d => d.r); svg.selectAll('text') .data(squares) .enter() .append('text') .attr('x', data => data.x + data.width / 2) // 文本的 x 坐标(居中) .attr('y', data => data.y + data.height / 2) // 文本的 y 坐标(居中) .attr('dominant-baseline', 'middle') // 文本垂直居中 .attr('text-anchor', 'middle') // 文本水平居中 .attr('fill', 'white') // 文本颜色 .attr('font-size', '12px') .text(data => data.text); // 设置文本内容 } } /** * @description 绘制连线 * @param {d3.Selection} svg * @param {ElkNode} computedLayout * @param {number} ratio */ async renderLine(svg, computedLayout, ratio) { const lines = []; for (const edge of computedLayout.edges) { for (const section of edge.sections || []) { const points = []; points.push(section.startPoint); for (const point of section.bendPoints || []) { points.push(point); } points.push(section.endPoint); for (let i = 0; i < points.length - 1; ++ i) { lines.push({ x1: points[i].x, y1: points[i].y, x2: points[i + 1].x, y2: points[i + 1].y, strokeWidth: 2, color: 'var(--foreground)' }); } } } let lineSelection = svg.selectAll('line') .data(lines) .enter() .append('line') .attr('x1', data => data.x1) .attr('y1', data => data.y1) .attr('x2', data => data.x2) .attr('y2', data => data.y2) .attr('stroke', data => data.color); if (globalSetting.renderAnimation) { lineSelection = lineSelection.transition().duration(1000); } lineSelection.attr('stroke-width', data => data.strokeWidth); } /** * @description 从 globalLookup 中更新 svg 的方位 */ updateLocationFromGlobal() { const svg = globalLookup.netlistRender.selection; if (!svg) { return; } svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`); } }