完成 d3 数据项目 + 渲染的架构调整

This commit is contained in:
锦恢 2024-12-25 15:10:09 +08:00
parent 2a1dc56148
commit 425c513635
7 changed files with 302 additions and 151 deletions

View File

@ -34,4 +34,13 @@ onMounted(async () => {
align-items: center;
transition: var(--animation-5s);
}
.grab {
cursor: grab;
}
.grabbing {
cursor: grabbing;
}
</style>

View File

@ -5,11 +5,30 @@ export class CellRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} cells
*/
constructor(selection, cells) {
this.parentSelection = selection;
this.data = cells;
/**
* @type {BasicD3DataItem[]}
*/
this.data = [];
}
/**
* @description elknode 关于 器件 的数据添加为 d3 数据项目
* @param {ElkNode} node
* @param {HTMLElement} element
*/
addAsD3DataItem(node, element) {
this.data.push({
element,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
fill: 'var(--main-dark-color)'
});
}
render() {
@ -32,12 +51,23 @@ export class CellRender {
cellSelections = cellSelections
.transition()
.duration(1000)
.attr('stroke-opacity', 1);
.attr('stroke-opacity', 1)
.attr('class', 'grab')
.on('end', function (data) {
console.log('enter end');
const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data);
});
} else {
cellSelections = cellSelections
.attr('class', 'grab')
.each(function (data) {
console.log('enter end'); // 在这里执行你需要的逻辑
const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data);
});
}
cellSelections.on('end', function (data) {
const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data);
});
this.selections = cellSelections;
return cellSelections;
@ -54,10 +84,22 @@ export class CellRender {
export function registerDragEvent(selection, data) {
// 创建拖拽行为
const drag = d3.drag();
drag.on("drag", event => dragged(event, selection, data));
drag.on('start', event => dragStart(event, selection, data));
drag.on('drag', event => dragged(event, selection, data));
drag.on('end', event => dragEnd(event, selection, data))
selection.call(drag);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragStart(event, selection, data) {
selection.attr('class', 'grabbing');
}
/**
*
@ -67,5 +109,17 @@ export function registerDragEvent(selection, data) {
function dragged(event, selection, data) {
data.x = event.x;
data.y = event.y;
selection.attr('x', event.x).attr('y', event.y);
selection
.attr('x', event.x)
.attr('y', event.y);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragEnd(event, selection, data) {
selection.attr('class', 'grab');
}

View File

@ -3,13 +3,32 @@ import { globalSetting } from '../global';
export class ConnectionRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} connections
* @param {d3.Selection} selection
*/
constructor(selection, connections) {
constructor(selection) {
this.parentSelection = selection;
this.data = connections;
/**
* @type {BasicD3DataItem[]}
*/
this.data = [];
}
/**
* @description elknode 关于 module connection 的数据添加为 d3 数据项目
* @param {ElkPort} cellPort 连接点对象
* @param {ElkNode} node 当前的实体port/例化模块/器件
*/
addAsD3DataItem(cellPort, node) {
this.data.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
});
}
render() {
@ -32,39 +51,9 @@ export class ConnectionRender {
connectionSelections
.attr('fill', d => d.fill)
.attr('r', d => d.r)
.on('end', function (data) {
const connectionSelection = d3.select(this);
registerDragEvent(connectionSelection, data);
});
.attr('r', d => d.r);
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

@ -4,11 +4,11 @@ import ELK from 'elkjs';
import { Module } from './layout';
import { globalLookup, globalSetting } from '../global';
import { registerCellDragEvent } from './drag';
import { PortRender } from './port';
import { InstantiationRender } from './instantiation';
import { CellRender } from './cell';
import { ConnectionRender } from './connection';
import { WireRender } from './wire';
export class NetlistRender {
/**
@ -129,112 +129,77 @@ export class NetlistRender {
.attr('width', this.renderWidth)
.attr('height', this.renderHeight);
await this.renderLine(svg, computedLayout, ratio);
await this.renderEntity(svg, computedLayout, ratio);
// 将分组作为一个后续操作的 parent selection
const g = svg.append('g');
await this.renderLine(g, computedLayout, ratio);
await this.renderEntity(g, computedLayout, ratio);
// svg 挂载为全局注册的 selection
this.selection = svg;
// 注册平移和缩放
// this.registerRenderTransform(g);
return svg;
}
/**
* @description 绘制实体
* @param {d3.Selection} svg
* @param {d3.Selection} parentSelection
* @param {ElkNode} computedLayout
* @param {number} ratio
*/
async renderEntity(svg, computedLayout, ratio) {
async renderEntity(parentSelection, computedLayout, ratio) {
// node 可能是如下的几类
// - module 的 port
// - 器件(基础器件 & 例化模块)
// - 器件的 port
// 生成用于绘制的 d3 数据结构
// 默认需要渲染成矩形的(缺失样式的器件、例化模块等等)
const ports = [];
const instantiations = [];
const cells = [];
const connections = [];
const skinManager = globalLookup.skinManager;
// TODO: 把下面的逻辑融合进入 render 中
// 创建各个主要实体的 render
this.cellRender = new CellRender(parentSelection);
this.portRender = new PortRender(parentSelection);
this.instantiationRender = new InstantiationRender(parentSelection);
this.connectionRender = new ConnectionRender(parentSelection);
for (const node of computedLayout.children) {
const skin = skinManager.querySkin(node.renderName);
if (skin) {
// 具有 skin 的器件
cells.push({
element: skin.meta.svgDoc.documentElement,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
fill: 'var(--main-dark-color)',
});
this.cellRender.addAsD3DataItem(node, skin.meta.svgDoc.documentElement);
} else {
if (node.renderType === 'port') {
ports.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
});
this.portRender.addAsD3DataItem(node);
} 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
});
this.instantiationRender.addAsD3DataItem(node);
}
}
// 如果存在 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
});
this.connectionRender.addAsD3DataItem(cellPort, node);
}
}
this.portRender = new PortRender(svg, ports);
this.portRender.render();
// this.instantiationRender = new InstantiationRender(svg, instantiations);
// TODO: 实现它
// this.instantiationRender.render();
this.cellRender = new CellRender(svg, cells);
this.cellRender.render();
this.connectionRender = new ConnectionRender(svg, connections);
this.connectionRender.render();
this.connectionRender.render();
}
/**
* @description 绘制连线
* @param {d3.Selection} svg
* @param {d3.Selection} parentSelection
* @param {ElkNode} computedLayout
* @param {number} ratio
*/
async renderLine(svg, computedLayout, ratio) {
const lines = [];
async renderLine(parentSelection, computedLayout, ratio) {
this.wireRender = new WireRender(parentSelection);
for (const edge of computedLayout.edges) {
for (const section of edge.sections || []) {
const points = [];
@ -243,47 +208,60 @@ export class NetlistRender {
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)'
});
}
this.wireRender.addAsD3DataItems(points);
}
}
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);
}
this.wireRender.render();
}
lineSelection.attr('stroke-width', data => data.strokeWidth);
/**
* @description 注册平移和缩放
* @param {d3.Selection} parentSelection
*/
registerRenderTransform(parentSelection) {
// 创建缩放行为
const zoom = d3.zoom()
// 设置缩放范围(最小缩放比例 0.5,最大缩放比例 5
.scaleExtent([0.5, 5])
.on("zoom", zoomed);
// 将缩放行为应用到 SVG
this.selection.call(zoom);
// 缩放事件处理函数
function zoomed(event) {
const { transform, sourceEvent } = event;
if (sourceEvent && sourceEvent.type === "wheel") {
if (globalSetting.renderAnimation) {
parentSelection
.transition()
.duration(350)
.attr('transform', transform);
} else {
parentSelection.attr('transform', transform);
}
} else if (sourceEvent && sourceEvent.type === "mousemove") {
if (globalSetting.renderAnimation) {
parentSelection.attr('transform', transform);
} else {
parentSelection.attr('transform', transform);
}
}
}
}
/**
* @description globalLookup 中更新 svg 的方位
*/
updateLocationFromGlobal() {
const svg = globalLookup.netlistRender.selection;
if (!svg) {
return;
}
// const svg = globalLookup.netlistRender.selection;
// if (!svg) {
// return;
// }
svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`);
// svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`);
}
/**

View File

@ -7,5 +7,27 @@ export class InstantiationRender {
*/
constructor(selection) {
this.selection = selection;
/**
* @type {BasicD3DataItem[]}
*/
this.data = []
}
/**
* @description elknode 关于 例化模块 的数据添加为 d3 数据项目
* @param {ElkNode} node
*/
addAsD3DataItem(node) {
this.data.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
});
}
}

View File

@ -6,11 +6,30 @@ export class PortRender {
/**
*
* @param {d3.Selection} selection
* @param {BasicD3DataItem} ports
*/
constructor(selection, ports) {
constructor(selection) {
this.parentSelection = selection;
this.data = ports;
/**
* @type {BasicD3DataItem[]}
*/
this.data = [];
}
/**
* @description elknode 关于 port 的数据添加为 d3 数据项目
* @param {ElkNode} node
*/
addAsD3DataItem(node) {
this.data.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
});
}
render() {
@ -26,6 +45,7 @@ export class PortRender {
.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 坐标(居中)
@ -38,16 +58,28 @@ export class PortRender {
portSelections = portSelections
.transition()
.duration(1000)
.attr('stroke', 'var(--main-color)')
.attr('stroke-width', 2)
.attr('rx', d => d.rx)
.attr('ry', d => d.ry)
.attr('class', 'grab')
.on('end', function (data) {
const portSelection = d3.select(this);
registerDragEvent(portSelection, data);
});
} else {
portSelections
.attr('stroke', 'var(--main-color)')
.attr('stroke-width', 2)
.attr('rx', d => d.rx)
.attr('ry', d => d.ry)
.attr('class', 'grab')
.each(function (data) {
const portSelection = d3.select(this);
registerDragEvent(portSelection, data);
});
}
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;
@ -64,10 +96,22 @@ export class PortRender {
export function registerDragEvent(selection, data) {
// 创建拖拽行为
const drag = d3.drag();
drag.on("start", event => dragStart(event, selection, data));
drag.on("drag", event => dragged(event, selection, data));
drag.on("end", event => dragEnd(event, selection, data));
selection.call(drag);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragStart(event, selection, data) {
selection.attr('class', 'grabbing');
}
/**
*
@ -78,4 +122,13 @@ function dragged(event, selection, data) {
data.x = event.x;
data.y = event.y;
selection.attr('x', event.x).attr('y', event.y);
}
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragEnd(event, selection, data) {
selection.attr('class', 'grab');
}

View File

@ -1,4 +1,5 @@
import * as d3 from 'd3';
import { globalSetting } from '../global';
export class WireRender {
/**
@ -7,5 +8,50 @@ export class WireRender {
*/
constructor(selection) {
this.selection = selection;
/**
* @type {BasicD3DataItem[]}
*/
this.data = [];
}
/**
* @description elknode 关于 wire 的数据添加为 d3 数据项目
* @param {ElkPoint[]} points
*/
addAsD3DataItems(points) {
for (let i = 0; i < points.length - 1; ++ i) {
this.data.push({
x1: points[i].x,
y1: points[i].y,
x2: points[i + 1].x,
y2: points[i + 1].y,
strokeWidth: 2,
color: 'var(--foreground)'
});
}
}
render() {
const data = this.data;
let lineSelections = this.selection.selectAll('line')
.data(data)
.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) {
lineSelections = lineSelections
.transition()
.duration(1000);
}
lineSelections.attr('stroke-width', data => data.strokeWidth);
// line 就不注册拖拽事件了
}
}