340 lines
11 KiB
JavaScript
340 lines
11 KiB
JavaScript
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})`);
|
||
}
|
||
} |