2024-12-24 21:40:27 +08:00

340 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, Module>}
*/
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<ElkNode>}
*/
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})`);
}
}