/* eslint-disable */ import * as d3 from 'd3'; import { globalSetting } from '../global'; import { NetlistRender } from '.'; import { getMarginParamter, LAYOUT_CONSTANT, LINE_WIDTH } from './layout'; import { svgResource } from '../skin/draw'; import { PulseLine } from '../skin/plusation'; export class WireRender { /** * * @param {d3.Selection} selection * @param {NetlistRender} rootRender */ constructor(selection, rootRender) { this.selection = selection; this.rootRender = rootRender; /** * @type {BasicD3DataItem[]} */ this.data = []; /** * @description id 到管理数据项的映射 * @type {Map} */ this.id2manager = rootRender.id2manager; this.idCounter = 0; this.arrowHeight = 12; this.arrowWidth = 12; } /** * @description 将 elknode 关于 wire 的数据添加为 d3 数据项目 * @param {import('../jsdoc').ElkPoint[]} points 长度至少为 2 的数组,代表经历过的点集 * @param {import('../jsdoc').ElkEdge} edge * @param {Map} id2port * @param {number} width */ addAsD3DataItems(points, edge, id2port, width) { const linePaths = []; const beginPoint = points.at(0); const endPoint = points.at(-1); const lastTwoPoint = points.at(-2); const sourcePort = id2port.get(edge.sourcePort); const targetPort = id2port.get(edge.targetPort); const sourceMargin = getMarginParamter(sourcePort); const targetMargin = getMarginParamter(targetPort); beginPoint.x -= sourceMargin.rightMargin; const direction = judgeDirection(endPoint, lastTwoPoint); // 特殊情况: targetPort 为 undefined if (targetPort === undefined) { const port = id2port.get(edge.orginalTarget); // TODO: 检查正确性 endPoint.x -= LAYOUT_CONSTANT.INSTANCE_RIGHT_MARGIN; } else { // 判断当前的方向 if (direction === 'up' || direction === 'down') { lastTwoPoint.x += targetMargin.leftMargin + 1; endPoint.x += targetMargin.leftMargin + 1; } else { endPoint.x += targetMargin.leftMargin; } } for (let i = 0; i < points.length; ++ i) { // 根据点的信息创建 path const command = i === 0 ? 'M': 'L'; const x = points[i].x; const y = points[i].y; linePaths.push(`${command}${x},${y}`); } const lineSvg = linePaths.join(' '); const arrowLocation = getArrowLocation(endPoint, direction, this.arrowWidth, this.arrowHeight); this.data.push({ id: this.idCounter, svg: lineSvg, endPoint: points.at(-1), width: width, arrow: { icon: direction + '-arrow', x: arrowLocation.x, y: arrowLocation.y, width: this.arrowWidth, height: this.arrowHeight }, active: false }); this.idCounter ++; } render() { const data = this.data; const id2manager = this.id2manager; const _this = this; const arrowHeight = 12; const arrowWidth = 12; let lineSelections = this.selection.selectAll('path.lines') .data(data) .enter() .append('path') .attr('d', d => d.svg) .attr('class', 'connection-line') .attr("fill", "none") .attr('stroke', 'var(--wire-color)') this.lineSelections = lineSelections; const arrows = new Map(); let arrowSelections = this.selection.selectAll('svg.line-arrow') .data(data) .enter() .append(data => { const arrowInfo = data.arrow; // 获取箭头 svg const element = svgResource.get(arrowInfo.icon); element.setAttribute('x', arrowInfo.x); element.setAttribute('y', arrowInfo.y); element.setAttribute('width', arrowInfo.width); element.setAttribute('height', arrowInfo.height); element.setAttribute('opacity', 'var(--line-arrow-opacity)'); return element; }) .each(function(data) { const selection = d3.select(this); arrows.set(data.id, selection); }); arrowSelections.raise(); const id2animation = new Map(); const id2PluseLine = new Map(); lineSelections.on('click', function(_, data) { data.active = !data.active; if (data.active) { const pathSelection = d3.select(this); const pulse = new PulseLine(); id2PluseLine.set(data.id, pulse); // 开启脉冲动画 pulse.loadToSelection(_this.selection, data); pulse.startAnimation(); } else { if (id2PluseLine.has(data.id)) { const pulse = id2PluseLine.get(data.id); pulse.destory(); id2PluseLine.delete(data.id); // 关闭高亮 const selection = d3.select(this); selection.attr('stroke', 'var(--wire-color)'); const arrowSelection = arrows.get(data.id); arrowSelection.selectAll('path').attr('fill', 'var(--wire-color)'); } } }); lineSelections.on('mouseenter', function(_, data) { const selection = d3.select(this); selection.raise(); selection.attr('stroke', 'var(--wire-active-color)'); const arrowSelection = arrows.get(data.id); arrowSelection.raise(); arrowSelection.selectAll('path').attr('fill', 'var(--wire-active-color)'); }); lineSelections.on('mouseleave', function(_, data) { if (!id2PluseLine.has(data.id)) { const selection = d3.select(this); selection.attr('stroke', 'var(--wire-color)'); const arrowSelection = arrows.get(data.id); arrowSelection.selectAll('path').attr('fill', 'var(--wire-color)'); } }); if (globalSetting.renderAnimation) { lineSelections = lineSelections .transition() .duration(1000); } lineSelections .attr('stroke-width', d => { const incrementWidth = globalSetting.boldMultiWidthWire ? 1 : 0; return d.width > 1 ? LINE_WIDTH + incrementWidth: LINE_WIDTH; }) .each(function (data) { const selection = d3.select(this); // const manager = _this.createDataManager(selection, data); }); } /** * * @param {d3.Selection} selection * @param {BasicD3DataItem} data * @returns {BasicD3ManagmentItem} */ createDataManager(selection, data) { const id2manager = this.id2manager; // wire 不需要拖拽上下文 const managerItem = { data, selection, type: 'wire' }; if (!id2manager.has(data.id)) { id2manager.set(data.id, []); } id2manager.get(data.id).push(managerItem); return managerItem; } } /** * @description 计算贝塞尔插值,输入 0 - 1 的值 (delta),输出 [oldVal, newVal] 中间的插值 * @param {*} delta * @param {*} oldVal * @param {*} newVal * @returns */ function cubicBezierAnimation(delta, oldVal, newVal) { delta = 3 * (1 - delta) * (1 - delta) * delta + 3 * (1 - delta) * delta * delta + delta * delta * delta; return (1 - delta) * oldVal + delta * newVal; } /** * * @param {import('../jsdoc').ElkPoint} lastPoint * @param {import('../jsdoc').ElkPoint} lastTowPoint * @returns {'left' | 'right' | 'up' | 'down'} */ function judgeDirection(lastPoint, lastTowPoint) { if (lastPoint.x !== lastTowPoint.x) { return lastPoint.x > lastTowPoint.x ? 'right': 'left'; } else { return lastPoint.y > lastTowPoint.y ? 'down': 'up'; } } /** * * @param {import('../jsdoc').ElkPoint} lastPoint * @param {'left' | 'right' | 'up' | 'down'} direction */ function getArrowLocation(lastPoint, direction, arrowWidth, arrowHeight) { switch (direction) { case 'left': return { x: lastPoint.x + LAYOUT_CONSTANT.CELL_PORT_WIDTH - 2.5, y: lastPoint.y - arrowHeight / 2 }; case 'right': return { x: lastPoint.x - LAYOUT_CONSTANT.CELL_PORT_WIDTH - arrowWidth + 2.5, y: lastPoint.y - arrowHeight / 2 }; case 'up': return { x: lastPoint.x + LAYOUT_CONSTANT.CELL_PORT_WIDTH - arrowWidth / 2 - 1, y: lastPoint.y - arrowHeight / 2 }; case 'down': return { x: lastPoint.x + LAYOUT_CONSTANT.CELL_PORT_WIDTH - arrowWidth / 2 - 1, y: lastPoint.y - arrowHeight / 2 - 5 }; default: break; } }