完成 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; align-items: center;
transition: var(--animation-5s); transition: var(--animation-5s);
} }
.grab {
cursor: grab;
}
.grabbing {
cursor: grabbing;
}
</style> </style>

View File

@ -5,11 +5,30 @@ export class CellRender {
/** /**
* *
* @param {d3.Selection} selection * @param {d3.Selection} selection
* @param {BasicD3DataItem} cells
*/ */
constructor(selection, cells) { constructor(selection, cells) {
this.parentSelection = selection; 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() { render() {
@ -32,12 +51,23 @@ export class CellRender {
cellSelections = cellSelections cellSelections = cellSelections
.transition() .transition()
.duration(1000) .duration(1000)
.attr('stroke-opacity', 1); .attr('stroke-opacity', 1)
} .attr('class', 'grab')
cellSelections.on('end', function (data) { .on('end', function (data) {
console.log('enter end');
const cellSelection = d3.select(this); const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data); registerDragEvent(cellSelection, data);
}); });
} else {
cellSelections = cellSelections
.attr('class', 'grab')
.each(function (data) {
console.log('enter end'); // 在这里执行你需要的逻辑
const cellSelection = d3.select(this);
registerDragEvent(cellSelection, data);
});
}
this.selections = cellSelections; this.selections = cellSelections;
return cellSelections; return cellSelections;
@ -54,10 +84,22 @@ export class CellRender {
export function registerDragEvent(selection, data) { export function registerDragEvent(selection, data) {
// 创建拖拽行为 // 创建拖拽行为
const drag = d3.drag(); 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); 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) { function dragged(event, selection, data) {
data.x = event.x; data.x = event.x;
data.y = event.y; 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 { export class ConnectionRender {
/** /**
*
* @param {d3.Selection} selection * @param {d3.Selection} selection
* @param {BasicD3DataItem} connections
*/ */
constructor(selection, connections) { constructor(selection) {
this.parentSelection = 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() { render() {
@ -32,39 +51,9 @@ export class ConnectionRender {
connectionSelections connectionSelections
.attr('fill', d => d.fill) .attr('fill', d => d.fill)
.attr('r', d => d.r) .attr('r', d => d.r);
.on('end', function (data) {
const connectionSelection = d3.select(this);
registerDragEvent(connectionSelection, data);
});
this.selections = connectionSelections; this.selections = connectionSelections;
return 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 { Module } from './layout';
import { globalLookup, globalSetting } from '../global'; import { globalLookup, globalSetting } from '../global';
import { registerCellDragEvent } from './drag';
import { PortRender } from './port'; import { PortRender } from './port';
import { InstantiationRender } from './instantiation'; import { InstantiationRender } from './instantiation';
import { CellRender } from './cell'; import { CellRender } from './cell';
import { ConnectionRender } from './connection'; import { ConnectionRender } from './connection';
import { WireRender } from './wire';
export class NetlistRender { export class NetlistRender {
/** /**
@ -129,112 +129,77 @@ export class NetlistRender {
.attr('width', this.renderWidth) .attr('width', this.renderWidth)
.attr('height', this.renderHeight); .attr('height', this.renderHeight);
await this.renderLine(svg, computedLayout, ratio); // 将分组作为一个后续操作的 parent selection
await this.renderEntity(svg, computedLayout, ratio); const g = svg.append('g');
await this.renderLine(g, computedLayout, ratio);
await this.renderEntity(g, computedLayout, ratio);
// svg 挂载为全局注册的 selection
this.selection = svg; this.selection = svg;
// 注册平移和缩放
// this.registerRenderTransform(g);
return svg; return svg;
} }
/** /**
* @description 绘制实体 * @description 绘制实体
* @param {d3.Selection} svg * @param {d3.Selection} parentSelection
* @param {ElkNode} computedLayout * @param {ElkNode} computedLayout
* @param {number} ratio * @param {number} ratio
*/ */
async renderEntity(svg, computedLayout, ratio) { async renderEntity(parentSelection, computedLayout, ratio) {
// node 可能是如下的几类 // node 可能是如下的几类
// - module 的 port // - module 的 port
// - 器件(基础器件 & 例化模块) // - 器件(基础器件 & 例化模块)
// - 器件的 port // - 器件的 port
// 生成用于绘制的 d3 数据结构
// 默认需要渲染成矩形的(缺失样式的器件、例化模块等等)
const ports = [];
const instantiations = [];
const cells = [];
const connections = [];
const skinManager = globalLookup.skinManager; 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) { for (const node of computedLayout.children) {
const skin = skinManager.querySkin(node.renderName); const skin = skinManager.querySkin(node.renderName);
if (skin) { if (skin) {
// 具有 skin 的器件 // 具有 skin 的器件
cells.push({ this.cellRender.addAsD3DataItem(node, skin.meta.svgDoc.documentElement);
element: skin.meta.svgDoc.documentElement,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
fill: 'var(--main-dark-color)',
});
} else { } else {
if (node.renderType === 'port') { if (node.renderType === 'port') {
ports.push({ this.portRender.addAsD3DataItem(node);
x: node.x,
y: node.y,
width: node.width,
height: node.height,
fill: 'var(--main-dark-color)',
text: node.renderName,
rx: 3,
ry: 3
});
} else { } else {
// 没有 skin 的器件或者端口 // 没有 skin 的器件或者端口
instantiations.push({ this.instantiationRender.addAsD3DataItem(node);
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
for (const cellPort of node.ports || []) { for (const cellPort of node.ports || []) {
connections.push({ this.connectionRender.addAsD3DataItem(cellPort, node);
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.portRender = new PortRender(svg, ports);
this.portRender.render(); this.portRender.render();
// TODO: 实现它
// this.instantiationRender = new InstantiationRender(svg, instantiations);
// this.instantiationRender.render(); // this.instantiationRender.render();
this.cellRender = new CellRender(svg, cells);
this.cellRender.render(); this.cellRender.render();
this.connectionRender = new ConnectionRender(svg, connections);
this.connectionRender.render(); this.connectionRender.render();
} }
/** /**
* @description 绘制连线 * @description 绘制连线
* @param {d3.Selection} svg * @param {d3.Selection} parentSelection
* @param {ElkNode} computedLayout * @param {ElkNode} computedLayout
* @param {number} ratio * @param {number} ratio
*/ */
async renderLine(svg, computedLayout, ratio) { async renderLine(parentSelection, computedLayout, ratio) {
const lines = [];
this.wireRender = new WireRender(parentSelection);
for (const edge of computedLayout.edges) { for (const edge of computedLayout.edges) {
for (const section of edge.sections || []) { for (const section of edge.sections || []) {
const points = []; const points = [];
@ -243,47 +208,60 @@ export class NetlistRender {
points.push(point); points.push(point);
} }
points.push(section.endPoint); points.push(section.endPoint);
this.wireRender.addAsD3DataItems(points);
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') this.wireRender.render();
.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);
/**
* @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) { if (globalSetting.renderAnimation) {
lineSelection = lineSelection.transition().duration(1000); 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);
}
}
} }
lineSelection.attr('stroke-width', data => data.strokeWidth);
} }
/** /**
* @description globalLookup 中更新 svg 的方位 * @description globalLookup 中更新 svg 的方位
*/ */
updateLocationFromGlobal() { updateLocationFromGlobal() {
const svg = globalLookup.netlistRender.selection; // const svg = globalLookup.netlistRender.selection;
if (!svg) { // if (!svg) {
return; // 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) { constructor(selection) {
this.selection = 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 {d3.Selection} selection
* @param {BasicD3DataItem} ports
*/ */
constructor(selection, ports) { constructor(selection) {
this.parentSelection = 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() { render() {
@ -26,6 +45,7 @@ export class PortRender {
.attr('height', data => data.height) .attr('height', data => data.height)
.attr('fill', d => d.fill); .attr('fill', d => d.fill);
portSelections.append("text") portSelections.append("text")
.attr("x", d => d.width / 2) // 文本的 x 坐标(居中) .attr("x", d => d.width / 2) // 文本的 x 坐标(居中)
.attr("y", d => d.height / 2) // 文本的 y 坐标(居中) .attr("y", d => d.height / 2) // 文本的 y 坐标(居中)
@ -38,16 +58,28 @@ export class PortRender {
portSelections = portSelections portSelections = portSelections
.transition() .transition()
.duration(1000) .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 portSelections
.attr('stroke', 'var(--main-color)') .attr('stroke', 'var(--main-color)')
.attr('stroke-width', 2) .attr('stroke-width', 2)
.attr('rx', d => d.rx) .attr('rx', d => d.rx)
.attr('ry', d => d.ry) .attr('ry', d => d.ry)
.on('end', function (data) { .attr('class', 'grab')
.each(function (data) {
const portSelection = d3.select(this); const portSelection = d3.select(this);
registerDragEvent(portSelection, data); registerDragEvent(portSelection, data);
}); });
}
this.selections = portSelections; this.selections = portSelections;
return portSelections; return portSelections;
@ -64,10 +96,22 @@ export class PortRender {
export function registerDragEvent(selection, data) { export function registerDragEvent(selection, data) {
// 创建拖拽行为 // 创建拖拽行为
const drag = d3.drag(); const drag = d3.drag();
drag.on("start", event => dragStart(event, selection, data));
drag.on("drag", event => dragged(event, selection, data)); drag.on("drag", event => dragged(event, selection, data));
drag.on("end", event => dragEnd(event, selection, data));
selection.call(drag); selection.call(drag);
} }
/**
*
* @param {d3.D3DragEvent} event
* @param {d3.Selection} selection
*/
function dragStart(event, selection, data) {
selection.attr('class', 'grabbing');
}
/** /**
* *
@ -79,3 +123,12 @@ function dragged(event, selection, data) {
data.y = event.y; 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

@ -1,4 +1,5 @@
import * as d3 from 'd3'; import * as d3 from 'd3';
import { globalSetting } from '../global';
export class WireRender { export class WireRender {
/** /**
@ -7,5 +8,50 @@ export class WireRender {
*/ */
constructor(selection) { constructor(selection) {
this.selection = 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 就不注册拖拽事件了
} }
} }