完成渲染的架构

This commit is contained in:
锦恢 2024-12-24 23:29:09 +08:00
parent 45619460cf
commit 2a1dc56148
11 changed files with 365 additions and 137 deletions

1
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-drag": "^3.0.0",
"element-plus": "^2.9.1", "element-plus": "^2.9.1",
"elkjs": "^0.9.3", "elkjs": "^0.9.3",
"fflate": "^0.8.2", "fflate": "^0.8.2",

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"d3": "^7.9.0", "d3": "^7.9.0",
"d3-drag": "^3.0.0",
"element-plus": "^2.9.1", "element-plus": "^2.9.1",
"elkjs": "^0.9.3", "elkjs": "^0.9.3",
"fflate": "^0.8.2", "fflate": "^0.8.2",

View File

@ -207,3 +207,16 @@
* @property {'NORTH' | 'SOUTH' | 'EAST' | 'WEST'} [elk.port.side] - 端口的位置可选 "NORTH", "SOUTH", "EAST", "WEST" * @property {'NORTH' | 'SOUTH' | 'EAST' | 'WEST'} [elk.port.side] - 端口的位置可选 "NORTH", "SOUTH", "EAST", "WEST"
*/ */
/**
* @typedef BasicD3DataItem
* @property {number} x
* @property {number} y
* @property {number} width
* @property {number} height
* @property {string} [fill]
* @property {string} [text]
* @property {number} [rx]
* @property {number} [ry]
*/

71
src/hook/render/cell.js Normal file
View File

@ -0,0 +1,71 @@
import * as d3 from 'd3';
import { globalSetting } from '../global';
export class CellRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} cells
*/
constructor(selection, cells) {
this.parentSelection = selection;
this.data = cells;
}
render() {
const data = this.data;
let cellSelections = this.parentSelection.selectAll('g')
.data(data)
.enter()
.append(data => {
const element = data.element;
element.setAttribute('x', data.x);
element.setAttribute('y', data.y);
if (globalSetting.renderAnimation) {
element.setAttribute('stroke-opacity', 0);
}
return element;
});
if (globalSetting.renderAnimation) {
cellSelections = cellSelections
.transition()
.duration(1000)
.attr('stroke-opacity', 1);
}
cellSelections.on('end', function (data) {
const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data);
});
this.selections = cellSelections;
return cellSelections;
}
}
/**
* @description 注册关于 器件 的拖动事件
*
* 需要提取最小拓扑子图然后重新调整各个区域的尺寸
* @param {d3.Selection} selection
* @param {any} data
*/
export function registerDragEvent(selection, data) {
// 创建拖拽行为
const drag = d3.drag();
drag.on("drag", event => dragged(event, selection, data));
selection.call(drag);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragged(event, selection, data) {
data.x = event.x;
data.y = event.y;
selection.attr('x', event.x).attr('y', event.y);
}

View File

@ -0,0 +1,70 @@
import * as d3 from 'd3';
import { globalSetting } from '../global';
export class ConnectionRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} connections
*/
constructor(selection, connections) {
this.parentSelection = selection;
this.data = connections;
}
render() {
const data = this.data;
let connectionSelections = this.parentSelection.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', data => data.x)
.attr('cy', data => data.y)
.attr('width', data => data.width)
.attr('height', data => data.height)
if (globalSetting.renderAnimation) {
connectionSelections = connectionSelections
.transition()
.duration(1000);
}
connectionSelections
.attr('fill', d => d.fill)
.attr('r', d => d.r)
.on('end', function (data) {
const connectionSelection = d3.select(this);
registerDragEvent(connectionSelection, data);
});
this.selections = connectionSelections;
return connectionSelections;
}
}
/**
* @description 注册关于 器件 的拖动事件
*
* 需要提取最小拓扑子图然后重新调整各个区域的尺寸
* @param {d3.Selection} selection
* @param {any} data
*/
export function registerDragEvent(selection, data) {
// 创建拖拽行为
const drag = d3.drag();
drag.on("drag", event => dragged(event, selection, data));
selection.call(drag);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragged(event, selection, data) {
data.x = event.x;
data.y = event.y;
selection.attr('x', event.x).attr('y', event.y);
}

View File

@ -1,14 +0,0 @@
import * as d3 from 'd3';
import ELK from 'elkjs';
/**
* @description 注册关于 器件 的拖动事件
*
* 需要提取最小拓扑子图然后重新调整各个区域的尺寸
* @param {d3.Transition} renderSvg
*/
export function registerCellDragEvent(renderSvg) {
// renderSvg.call(d3.drag().on('drag', (event, g) => {
// console.log(event);
// }));
}

View File

@ -5,6 +5,10 @@ import ELK from 'elkjs';
import { Module } from './layout'; import { Module } from './layout';
import { globalLookup, globalSetting } from '../global'; import { globalLookup, globalSetting } from '../global';
import { registerCellDragEvent } from './drag'; import { registerCellDragEvent } from './drag';
import { PortRender } from './port';
import { InstantiationRender } from './instantiation';
import { CellRender } from './cell';
import { ConnectionRender } from './connection';
export class NetlistRender { export class NetlistRender {
/** /**
@ -82,6 +86,25 @@ export class NetlistRender {
return layoutGraph; return layoutGraph;
} }
/**
* @description 窗体大小发生变化时
*/
registerResizeHandler(element) {
this.resizeMonitor = new ResizeObserver(entries => {
for (const entry of entries) {
const { height, width } = entry.contentRect;
this.renderHeight = height;
this.renderWidth = width;
if (this.selection) {
this.selection
.attr('width', this.renderWidth)
.attr('height', this.renderHeight)
}
}
});
this.resizeMonitor.observe(element);
}
/** /**
* *
* @param {ElkNode} computedLayout * @param {ElkNode} computedLayout
@ -92,14 +115,19 @@ export class NetlistRender {
const virtualHeight = computedLayout.height; const virtualHeight = computedLayout.height;
const virtualWidth = computedLayout.width; const virtualWidth = computedLayout.width;
const element = document.querySelector(container);
this.renderHeight = element.clientHeight;
this.renderWidth = element.clientWidth;
this.registerResizeHandler(element);
// 根据 height 进行放缩(可以通过设置进行调整) // 根据 height 进行放缩(可以通过设置进行调整)
const ratio = this.renderHeight / virtualHeight; const ratio = this.renderHeight / virtualHeight;
// 遍历计算布局进行创建 // 遍历计算布局进行创建
const svg = d3.select(container) const svg = d3.select(container)
.selectAll('svg') .selectAll('svg')
.attr('width', virtualWidth) .attr('width', this.renderWidth)
.attr('height', virtualHeight); .attr('height', this.renderHeight);
await this.renderLine(svg, computedLayout, ratio); await this.renderLine(svg, computedLayout, ratio);
await this.renderEntity(svg, computedLayout, ratio); await this.renderEntity(svg, computedLayout, ratio);
@ -123,17 +151,21 @@ export class NetlistRender {
// 生成用于绘制的 d3 数据结构 // 生成用于绘制的 d3 数据结构
// 默认需要渲染成矩形的(缺失样式的器件、例化模块等等) // 默认需要渲染成矩形的(缺失样式的器件、例化模块等等)
const squares = []; const ports = [];
const instantiations = [];
const cells = [];
const connections = []; const connections = [];
const svgElements = [];
const skinManager = globalLookup.skinManager; const skinManager = globalLookup.skinManager;
// TODO: 把下面的逻辑融合进入 render 中
for (const node of computedLayout.children) { for (const node of computedLayout.children) {
const skin = skinManager.querySkin(node.renderName); const skin = skinManager.querySkin(node.renderName);
if (skin) { if (skin) {
// 具有 skin 的器件 // 具有 skin 的器件
svgElements.push({ cells.push({
element: skin.meta.svgDoc.documentElement, element: skin.meta.svgDoc.documentElement,
x: node.x, x: node.x,
y: node.y, y: node.y,
@ -142,17 +174,30 @@ export class NetlistRender {
fill: 'var(--main-dark-color)', fill: 'var(--main-dark-color)',
}); });
} else { } else {
// 没有 skin 的器件 if (node.renderType === 'port') {
squares.push({ ports.push({
x: node.x, x: node.x,
y: node.y, y: node.y,
width: node.width, width: node.width,
height: node.height, height: node.height,
fill: 'var(--main-dark-color)', fill: 'var(--main-dark-color)',
text: node.renderName, text: node.renderName,
rx: 3, rx: 3,
ry: 3 ry: 3
}); });
} else {
// 没有 skin 的器件或者端口
instantiations.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 // 如果存在 port绘制 port
@ -169,114 +214,17 @@ export class NetlistRender {
} }
} }
if (globalSetting.renderAnimation) { this.portRender = new PortRender(svg, ports);
svg.selectAll('rect') this.portRender.render();
.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') // this.instantiationRender = new InstantiationRender(svg, instantiations);
.data(svgElements) // this.instantiationRender.render();
.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); this.cellRender = new CellRender(svg, cells);
this.cellRender.render();
svg.selectAll('circle') this.connectionRender = new ConnectionRender(svg, connections);
.data(connections) this.connectionRender.render();
.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); // 设置文本内容
}
} }
/** /**
@ -337,4 +285,39 @@ export class NetlistRender {
svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`); svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`);
} }
/**
* @description 渲染 module 中的 port
*/
renderPorts() {
}
/**
* @description 渲染 module 中的例化模块
*/
renderInstantiations() {
}
/**
* @description 渲染 module 中的基础器件
*/
renderCells() {
}
/**
* @description 渲染每一个 例化模块/基础器件 的接入端口
*/
renderConnections() {
}
/**
* @description 渲染连线
*/
renderWires() {
}
} }

View File

@ -0,0 +1,11 @@
import * as d3 from 'd3';
export class InstantiationRender {
/**
*
* @param {d3.Selection} selection
*/
constructor(selection) {
this.selection = selection;
}
}

81
src/hook/render/port.js Normal file
View File

@ -0,0 +1,81 @@
import * as d3 from 'd3';
import ELK from 'elkjs';
import { globalLookup, globalSetting } from '../global';
export class PortRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} ports
*/
constructor(selection, ports) {
this.parentSelection = selection;
this.data = ports;
}
render() {
const data = this.data;
let portSelections = this.parentSelection.selectAll('rect')
.data(data)
.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);
portSelections.append("text")
.attr("x", d => d.width / 2) // 文本的 x 坐标(居中)
.attr("y", d => d.height / 2) // 文本的 y 坐标(居中)
.attr("text-anchor", "middle") // 文本水平居中
.attr("dominant-baseline", "middle") // 文本垂直居中
.text(d => d.text) // 设置文本内容
.attr("fill", "black"); // 文本颜色
if (globalSetting.renderAnimation) {
portSelections = portSelections
.transition()
.duration(1000)
}
portSelections
.attr('stroke', 'var(--main-color)')
.attr('stroke-width', 2)
.attr('rx', d => d.rx)
.attr('ry', d => d.ry)
.on('end', function (data) {
const portSelection = d3.select(this);
registerDragEvent(portSelection, data);
});
this.selections = portSelections;
return portSelections;
}
}
/**
* @description 注册关于 器件 的拖动事件
*
* 需要提取最小拓扑子图然后重新调整各个区域的尺寸
* @param {d3.Selection} selection
* @param {any} data
*/
export function registerDragEvent(selection, data) {
// 创建拖拽行为
const drag = d3.drag();
drag.on("drag", event => dragged(event, selection, data));
selection.call(drag);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragged(event, selection, data) {
data.x = event.x;
data.y = event.y;
selection.attr('x', event.x).attr('y', event.y);
}

11
src/hook/render/wire.js Normal file
View File

@ -0,0 +1,11 @@
import * as d3 from 'd3';
export class WireRender {
/**
*
* @param {d3.Selection} selection
*/
constructor(selection) {
this.selection = selection;
}
}