完成缩放和平移

This commit is contained in:
锦恢 2024-12-24 21:40:27 +08:00
parent 2d27298b29
commit 45619460cf
18 changed files with 328 additions and 81 deletions

View File

@ -125,4 +125,4 @@ a {
.el-select__wrapper.is-disabled {
opacity: 0.6;
box-shadow: unset !important;
}
}

View File

@ -1,14 +1,26 @@
<template>
<div>
<div id="netlist"></div>
<div id="netlist" :ref="el => netlist = el">
<svg></svg>
</div>
</div>
</template>
<script setup>
import { defineComponent } from 'vue';
import { registerWheelEvent } from '@/hook/wheel-event';
import { defineComponent, onMounted, ref, computed } from 'vue';
defineComponent({ name: 'netlist-render' });
const netlist = ref(null);
onMounted(async () => {
const netlistElement = netlist.value;
const wheelHandler = registerWheelEvent(netlistElement);
netlistElement.addEventListener('wheel', event => {
wheelHandler(event);
});
});
</script>
@ -20,5 +32,6 @@ defineComponent({ name: 'netlist-render' });
display: flex;
justify-content: center;
align-items: center;
transition: var(--animation-5s);
}
</style>

View File

@ -40,6 +40,20 @@
</el-dialog>
</div>
<br>
<div class="setting-option">
<span class="option-title">
{{ t('render-animation') }}
</span>
<el-switch
v-model="globalSetting.renderAnimation"
size="large"
active-text="ON"
inactive-text="OFF"
/>
</div>
</div>
</el-scrollbar>
</div>

View File

@ -19,6 +19,7 @@ export function setDefaultCss() {
document.body.style.setProperty('--el-color-info-light-9', 'var(--vscode-focusBorder)');
document.body.style.setProperty('--el-color-info', 'var(--foreground)');
document.body.style.setProperty('--el-color-info-light-8', 'var(--vscode-focusBorder)');
// document.body.style.setProperty('--el-color-white', 'var(--background)');
// 设置全局宏

View File

@ -7,7 +7,8 @@ import { NetlistRender } from './render';
export const emitter = mitt();
export const globalSetting = reactive({
language: 'zh'
language: 'zh',
renderAnimation: true
});
export const globalLookup = {
@ -19,7 +20,22 @@ export const globalLookup = {
/**
* @type {NetlistRender}
*/
netlistRender: new NetlistRender()
netlistRender: new NetlistRender(),
/**
* @type {number}
*/
svgScale: 1,
/**
* @type {number}
*/
svgTranslateX: 0,
/**
* @type {number}
*/
svgTranslateY: 0
};
function loadSetting() {

14
src/hook/render/drag.js Normal file
View File

@ -0,0 +1,14 @@
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

@ -3,7 +3,8 @@ import ELK from 'elkjs';
import { Module } from './layout';
import { globalLookup } from '../global';
import { globalLookup, globalSetting } from '../global';
import { registerCellDragEvent } from './drag';
export class NetlistRender {
/**
@ -77,7 +78,6 @@ export class NetlistRender {
'org.eclipse.elk.layered.layering.strategy': 'LONGEST_PATH'
}
});
console.log(layoutGraph);
return layoutGraph;
}
@ -96,12 +96,15 @@ export class NetlistRender {
const ratio = this.renderHeight / virtualHeight;
// 遍历计算布局进行创建
const svg = d3.select(container).append('svg')
const svg = d3.select(container)
.selectAll('svg')
.attr('width', virtualWidth)
.attr('height', virtualHeight);
await this.renderEntity(svg, computedLayout, ratio);
await this.renderLine(svg, computedLayout, ratio);
await this.renderEntity(svg, computedLayout, ratio);
this.selection = svg;
return svg;
}
@ -121,12 +124,15 @@ export class NetlistRender {
// 生成用于绘制的 d3 数据结构
// 默认需要渲染成矩形的(缺失样式的器件、例化模块等等)
const squares = [];
const connections = [];
const svgElements = [];
const skinManager = globalLookup.skinManager;
for (const node of computedLayout.children) {
const skin = skinManager.querySkin(node.renderName);
if (skin) {
// 具有 skin 的器件
svgElements.push({
element: skin.meta.svgDoc.documentElement,
x: node.x,
@ -136,6 +142,7 @@ export class NetlistRender {
fill: 'var(--main-dark-color)',
});
} else {
// 没有 skin 的器件
squares.push({
x: node.x,
y: node.y,
@ -150,54 +157,126 @@ export class NetlistRender {
// 如果存在 port绘制 port
for (const cellPort of node.ports || []) {
squares.push({
connections.push({
x: cellPort.x + node.x,
y: cellPort.y + node.y,
y: cellPort.y + node.y + 0.5, // 0.5 是为了线宽
width: cellPort.width,
height: cellPort.height,
fill: 'var(--main-color)',
text: '',
rx: 0,
ry: 0
r: 3.5
});
}
}
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;
});
if (globalSetting.renderAnimation) {
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)
.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')
.data(svgElements)
.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);
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); // 设置文本内容
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)
.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); // 设置文本内容
}
}
/**
@ -230,7 +309,7 @@ export class NetlistRender {
}
}
svg.selectAll('line')
let lineSelection = svg.selectAll('line')
.data(lines)
.enter()
.append('line')
@ -238,7 +317,24 @@ export class NetlistRender {
.attr('y1', data => data.y1)
.attr('x2', data => data.x2)
.attr('y2', data => data.y2)
.attr('stroke-width', data => data.strokeWidth)
.attr('stroke', data => data.color);
if (globalSetting.renderAnimation) {
lineSelection = lineSelection.transition().duration(1000);
}
lineSelection.attr('stroke-width', data => data.strokeWidth);
}
/**
* @description globalLookup 中更新 svg 的方位
*/
updateLocationFromGlobal() {
const svg = globalLookup.netlistRender.selection;
if (!svg) {
return;
}
svg.attr('transform', `translate(${globalLookup.svgTranslateX}, ${globalLookup.svgTranslateY}) scale(${globalLookup.svgScale})`);
}
}

View File

@ -134,16 +134,6 @@ export class Module {
for (let i = 0; i < leftSideConnections.length; ++ i) {
const connection = leftSideConnections[i];
const yOffset = meta.getPortYOffset(connection.name) * SKIN_SCALE;
const targetY = yOffset;
// 默认采用 JUSTIFIED均分一侧的 height
// 我们绘制的 svg 的 port offset y 未必和 elk 分配得到的一样,因此需要进行修正
const layoutY = height / (leftSideConnections.length + 1) * (i + 1);
const extraOffset = LINE_WIDTH * leftSideConnections.length;
const anchorY = layoutY - targetY;
console.log(yOffset);
ports.push({
id: connection.id,
@ -161,17 +151,7 @@ export class Module {
// 计算右侧的
for (let i = 0; i < rightSideConnections.length; ++ i) {
const connection = rightSideConnections[i];
const yOffset = meta.getPortYOffset(connection.name) * SKIN_SCALE;
const targetY = yOffset;
// 默认采用 JUSTIFIED均分一侧的 height
// 我们绘制的 svg 的 port offset y 未必和 elk 分配得到的一样,因此需要进行修正
const layoutY = height / (rightSideConnections.length + 1) * (i + 1);
const extraOffset = LINE_WIDTH * rightSideConnections.length / 2;
const anchorY = targetY - layoutY + extraOffset;
console.log(yOffset);
const yOffset = meta.getPortYOffset(connection.name) * SKIN_SCALE;
ports.push({
id: connection.id,

104
src/hook/wheel-event.js Normal file
View File

@ -0,0 +1,104 @@
import { globalLookup } from "./global";
const MAX_SCALE = 10.0;
const MIN_SCALE = 0.0;
/**
*
* @param {HTMLElement} element
* @param {WheelEvent} event
*/
function windowsKeydown(element, event) {
const svg = globalLookup.netlistRender.selection;
if (!svg) {
return;
}
const { deltaX, deltaY } = event;
if (event.ctrlKey) {
if (deltaY < 0) {
// scale up
const nextScale = globalLookup.svgScale + 0.1;
if (nextScale <= MAX_SCALE) {
globalLookup.svgScale = nextScale;
globalLookup.netlistRender.updateLocationFromGlobal();
}
} else if (deltaY > 0) {
// scale down
const nextScale = globalLookup.svgScale - 0.1;
if (nextScale >= MIN_SCALE) {
globalLookup.svgScale = nextScale;
globalLookup.netlistRender.updateLocationFromGlobal();
}
}
event.preventDefault();
} else if (event.shiftKey) {
event.preventDefault();
} else if (deltaX !== 0 && deltaY === 0) {
if (deltaX > 0) {
// scroll left
const translateX = globalLookup.svgTranslateX - 50;
globalLookup.svgTranslateX = translateX;
globalLookup.netlistRender.updateLocationFromGlobal();
} else {
// scroll right
const translateX = globalLookup.svgTranslateX + 50;
globalLookup.svgTranslateX = translateX;
globalLookup.netlistRender.updateLocationFromGlobal();
}
event.preventDefault();
} else if (deltaX === 0 && deltaY !== 0) {
if (deltaY > 0) {
// scroll up
const translateY = globalLookup.svgTranslateY - 50;
globalLookup.svgTranslateY = translateY;
globalLookup.netlistRender.updateLocationFromGlobal();
} else {
// scroll down
const translateY = globalLookup.svgTranslateY + 50;
globalLookup.svgTranslateY = translateY;
globalLookup.netlistRender.updateLocationFromGlobal();
}
event.preventDefault();
}
}
/**
*
* @param {HTMLElement} element
* @param {WheelEvent} event
*/
function linuxKeydown(element, event) {
}
/**
*
* @param {HTMLElement} element
* @param {WheelEvent} event
*/
function macOsKeydown(element, event) {
}
/**
* @description 注册滚轮事件
* @param {HTMLElement} element
* @returns {(event: WheelEvent) => void}
*/
export function registerWheelEvent(element) {
const platform = navigator.platform;
if (platform.startsWith('Win')) {
return event => windowsKeydown(element, event);
} else if (platform.startsWith('Linux')) {
return event => linuxKeydown(element, event);
} else if (platform.startsWith('Mac')) {
return event => macOsKeydown(element, event);
} else {
throw Error('不支持的操作系统!');
}
}

View File

@ -8,5 +8,6 @@
"cancel": "إلغاء",
"tips": "نصائح",
"language-setting": "اللغة",
"setting.language.change-dialog": "لقد قمت بتغيير اللغة إلى {0} ، ونوصي بإعادة تشغيل Netlist Viewer"
"setting.language.change-dialog": "لقد قمت بتغيير اللغة إلى {0} ، ونوصي بإعادة تشغيل Netlist Viewer",
"render-animation": "تفعيل الرسوم المتحركة للعرض"
}

View File

@ -8,5 +8,6 @@
"cancel": "Abbrechen",
"tips": "Tipps",
"language-setting": "Sprache",
"setting.language.change-dialog": "Sie haben die Sprache auf {0} geändert. Wir empfehlen Ihnen, Netlist Viewer neu zu starten."
"setting.language.change-dialog": "Sie haben die Sprache auf {0} geändert. Wir empfehlen Ihnen, Netlist Viewer neu zu starten.",
"render-animation": "Rendering-Animation aktivieren"
}

View File

@ -3,10 +3,11 @@
"general-setting": "General",
"appearance-setting": "Appearance",
"current-version": "current version",
"copyright": "The copyright of this software belongs to <a href=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"https://github.com/Digital-EDA\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" target=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"_blank\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\">Digital-IDE</a> project team. Welcome to <a href=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"https://github.com/Digital-EDA/Digital-IDE\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\">Star</a>.",
"copyright": "The copyright of this software belongs to <a href=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"https://github.com/Digital-EDA\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" target=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"_blank\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\">Digital-IDE</a> project team. Welcome to <a href=\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"https://github.com/Digital-EDA/Digital-IDE\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\">Star</a>.",
"confirm": "confirm",
"cancel": "cancel",
"tips": "Tips",
"language-setting": "Language",
"setting.language.change-dialog": "You have changed the language to {0}, we recommend restarting Netlist Viewer."
"setting.language.change-dialog": "You have changed the language to {0}, we recommend restarting Netlist Viewer.",
"render-animation": "enable rendering animation"
}

View File

@ -8,5 +8,6 @@
"cancel": "Annuler",
"tips": "Conseils",
"language-setting": "Langue",
"setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Netlist Viewer."
"setting.language.change-dialog": "Vous avez changé la langue en {0}, nous vous recommandons de redémarrer Netlist Viewer.",
"render-animation": "Activer l'animation de rendu"
}

View File

@ -8,5 +8,6 @@
"cancel": "キャンセル",
"tips": "ヒント",
"language-setting": "言語",
"setting.language.change-dialog": "言語を {0} に変更しました。Netlist Viewer を再起動することをお勧めします。"
"setting.language.change-dialog": "言語を {0} に変更しました。Netlist Viewer を再起動することをお勧めします。",
"render-animation": "レンダリングアニメーションを有効にする"
}

View File

@ -8,5 +8,6 @@
"cancel": "취소",
"tips": "팁",
"language-setting": "언어",
"setting.language.change-dialog": "언어를 {0} 으로 변경했습니다. Netlist Viewer 를 다시 시작하는 것을 권장합니다."
"setting.language.change-dialog": "언어를 {0} 으로 변경했습니다. Netlist Viewer 를 다시 시작하는 것을 권장합니다.",
"render-animation": "렌더링 애니메이션 활성화"
}

View File

@ -8,5 +8,6 @@
"cancel": "Отменить",
"tips": "Советы",
"language-setting": "Язык",
"setting.language.change-dialog": "Вы изменили язык на {0}, мы рекомендуем перезапустить Netlist Viewer."
"setting.language.change-dialog": "Вы изменили язык на {0}, мы рекомендуем перезапустить Netlist Viewer.",
"render-animation": "Включить анимацию рендеринга"
}

View File

@ -8,5 +8,6 @@
"cancel": "取消",
"tips": "提示",
"language-setting": "语言",
"setting.language.change-dialog": "您已经更换语言为 {0} ,我们建议您重启 Netlist Viewer"
"setting.language.change-dialog": "您已经更换语言为 {0} ,我们建议您重启 Netlist Viewer",
"render-animation": "开启渲染动画"
}

View File

@ -8,5 +8,6 @@
"cancel": "取消",
"tips": "提示",
"language-setting": "語言",
"setting.language.change-dialog": "您已將語言更改為 {0} ,我們建議您重新啟動 Netlist Viewer。"
"setting.language.change-dialog": "您已將語言更改為 {0} ,我們建議您重新啟動 Netlist Viewer。",
"render-animation": "開啟渲染動畫"
}