From 425c513635c9f129c15aa2c5c0785be8d804e6e9 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Wed, 25 Dec 2024 15:10:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20d3=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=20+=20=E6=B8=B2=E6=9F=93=E7=9A=84=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/render/index.vue | 9 ++ src/hook/render/cell.js | 72 ++++++++++++-- src/hook/render/connection.js | 61 +++++------- src/hook/render/index.js | 166 ++++++++++++++----------------- src/hook/render/instantiation.js | 22 ++++ src/hook/render/port.js | 77 +++++++++++--- src/hook/render/wire.js | 46 +++++++++ 7 files changed, 302 insertions(+), 151 deletions(-) diff --git a/src/components/render/index.vue b/src/components/render/index.vue index 449b502..7f5d512 100644 --- a/src/components/render/index.vue +++ b/src/components/render/index.vue @@ -34,4 +34,13 @@ onMounted(async () => { align-items: center; transition: var(--animation-5s); } + +.grab { + cursor: grab; +} + +.grabbing { + cursor: grabbing; +} + \ No newline at end of file diff --git a/src/hook/render/cell.js b/src/hook/render/cell.js index 892e4fc..6017bb3 100644 --- a/src/hook/render/cell.js +++ b/src/hook/render/cell.js @@ -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'); } \ No newline at end of file diff --git a/src/hook/render/connection.js b/src/hook/render/connection.js index e8f8549..9d5e061 100644 --- a/src/hook/render/connection.js +++ b/src/hook/render/connection.js @@ -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); -} \ No newline at end of file diff --git a/src/hook/render/index.js b/src/hook/render/index.js index fe92e2e..fbaca85 100644 --- a/src/hook/render/index.js +++ b/src/hook/render/index.js @@ -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})`); } /** diff --git a/src/hook/render/instantiation.js b/src/hook/render/instantiation.js index cfcdcdd..6acebed 100644 --- a/src/hook/render/instantiation.js +++ b/src/hook/render/instantiation.js @@ -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 + }); } } \ No newline at end of file diff --git a/src/hook/render/port.js b/src/hook/render/port.js index 4fc46a8..1629146 100644 --- a/src/hook/render/port.js +++ b/src/hook/render/port.js @@ -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'); } \ No newline at end of file diff --git a/src/hook/render/wire.js b/src/hook/render/wire.js index 1335d0f..fe125e7 100644 --- a/src/hook/render/wire.js +++ b/src/hook/render/wire.js @@ -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 就不注册拖拽事件了 } } \ No newline at end of file