完成 cell 的最小拓扑图的构造

This commit is contained in:
锦恢 2024-12-26 21:46:55 +08:00
parent b718780cf5
commit b8bbc0e290
20 changed files with 315 additions and 85 deletions

View File

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4440655 */ font-family: "iconfont"; /* Project id 4440655 */
src: url('iconfont.woff2?t=1734508613033') format('woff2'); src: url('iconfont.woff2?t=1735201425292') format('woff2');
} }
.iconfont { .iconfont {
@ -11,6 +11,18 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-single-click:before {
content: "\e664";
}
.icon-wheel-change:before {
content: "\e6ad";
}
.icon-double-click:before {
content: "\e6fc";
}
.icon-i18n:before { .icon-i18n:before {
content: "\e678"; content: "\e678";
} }

Binary file not shown.

View File

@ -1,7 +1,23 @@
<template> <template>
<div class="about-wrapper"> <div class="about-wrapper">
<div class="usermanual"> <div class="usermanual">
<h2>{{ t('usermanual') }}</h2>
<div class="usermanual-item">
<div><span class="iconfont icon-single-click"/> + <span class="iconfont icon-mouse"/></div>
<div>{{ t('usermanual.move-view') }}</div>
</div>
<div class="usermanual-item">
<div><span class="iconfont icon-wheel-change"/></div>
<div>{{ t('usermanual.scale-view') }}</div>
</div>
<div class="usermanual-item">
<div><span class="iconfont icon-ctrl"/> + <span class="iconfont icon-wheel-change"/></div>
<div>{{ t('usermanual.scale-view') }}+</div>
</div>
<div class="usermanual-item">
<div><span class="iconfont icon-double-click"/></div>
<div>{{ t('usermanual.scale-view') }}+</div>
</div>
</div> </div>
<br> <br>

View File

@ -176,7 +176,11 @@
* @typedef ElkEdge * @typedef ElkEdge
* @property {string} id 边的唯一标识符 * @property {string} id 边的唯一标识符
* @property {string[]} sources 边的源节点列表 * @property {string[]} sources 边的源节点列表
* @property {string} source 边的源节点
* @property {string} sourcePort 边的源节点的port
* @property {string[]} targets 边的目标节点列表 * @property {string[]} targets 边的目标节点列表
* @property {string} target 边的目标节点
* @property {string} targetPort 边的目标节点的port
* @property {string} [container] 所在容器的 id 可选由布局算法生成 * @property {string} [container] 所在容器的 id 可选由布局算法生成
* @property {ElkSection[]} [sections] 具体的连线规则可选由布局算法生成 * @property {ElkSection[]} [sections] 具体的连线规则可选由布局算法生成
* @property {ElkLayoutOptions} [layoutOptions] 边的布局选项可选 * @property {ElkLayoutOptions} [layoutOptions] 边的布局选项可选
@ -232,5 +236,20 @@
/** /**
* @typedef DragContext * @typedef DragContext
* @property {ElkGraph} elkGraph * @property {ElkGraph} elkGraph 用于更新的最小图
* @property {DragNeighbor[]} neighbors 当前 selection 代表的节点的邻居
*/
/**
* @typedef DragNeighbor
* @property {'port' | 'cell'} type
* @property {'source' | 'target'} location
* @property {ElkEdge} edge 构成这个邻居关系的 edge
* @property {d3.Selection} selection
* @property {DragConnection[]} connections 如果 type port, 则为空
*/
/**
* @typedef DragConnection
* @property {d3.Selection} selection
*/ */

View File

@ -1,6 +1,7 @@
import * as d3 from 'd3'; import * as d3 from 'd3';
import { globalSetting } from '../global'; import { globalSetting } from '../global';
import { NetlistRender } from '.'; import { NetlistRender } from '.';
import { LAYOUT_CONSTANT, makeEdgeId } from './layout';
export class CellRender { export class CellRender {
/** /**
@ -58,6 +59,11 @@ export class CellRender {
element.setAttribute('stroke-opacity', 0); element.setAttribute('stroke-opacity', 0);
} }
return element; return element;
})
.each(function (data) {
const cellSelection = d3.select(this);
const manager = _this.createDataManager(cellSelection, data);
registerDragEvent(manager, rootRender);
}); });
if (globalSetting.renderAnimation) { if (globalSetting.renderAnimation) {
@ -65,20 +71,10 @@ export class CellRender {
.transition() .transition()
.duration(1000) .duration(1000)
.attr('stroke-opacity', 1) .attr('stroke-opacity', 1)
.attr('class', 'grab') .attr('class', 'grab');
.on('end', function (data) {
const cellSelection = d3.select(this);
const manager = _this.createDataManager(cellSelection, data);
registerDragEvent(manager, rootRender);
});
} else { } else {
cellSelections = cellSelections cellSelections = cellSelections
.attr('class', 'grab') .attr('class', 'grab');
.each(function (data) {
const cellSelection = d3.select(this);
const manager = _this.createDataManager(cellSelection, data);
registerDragEvent(manager, rootRender);
});
} }
this.selections = cellSelections; this.selections = cellSelections;
@ -95,6 +91,7 @@ export class CellRender {
const id2manager = this.id2manager; const id2manager = this.id2manager;
// 创建拖拽上下文 // 创建拖拽上下文
const dragContext = { const dragContext = {
neighbors: [],
elkGraph: { elkGraph: {
// elk 是无状态的id 取什么名字都行 // elk 是无状态的id 取什么名字都行
id: 'root', id: 'root',
@ -114,7 +111,7 @@ export class CellRender {
id2manager.set(data.id, []); id2manager.set(data.id, []);
} }
id2manager.set(data.id, managerItem); id2manager.get(data.id).push(managerItem);
return managerItem; return managerItem;
} }
} }
@ -130,9 +127,9 @@ export function registerDragEvent(manager, rootRender) {
// 创建拖拽行为 // 创建拖拽行为
const drag = d3.drag(); const drag = d3.drag();
drag.on('start', async event => dragStart(event, manager, rootRender)); drag.on('start', async event => await dragStart(event, manager, rootRender));
drag.on('drag', async event => dragged(event, manager, rootRender)); drag.on('drag', async event => await dragged(event, manager, rootRender));
drag.on('end', async event => dragEnd(event, manager, rootRender)) drag.on('end', async event => await dragEnd(event, manager, rootRender))
manager.selection.call(drag); manager.selection.call(drag);
} }
@ -143,13 +140,105 @@ export function registerDragEvent(manager, rootRender) {
* @param {BasicD3ManagmentItem} manager * @param {BasicD3ManagmentItem} manager
* @param {NetlistRender} rootRender * @param {NetlistRender} rootRender
*/ */
function dragStart(event, manager, rootRender) { async function dragStart(event, manager, rootRender) {
const selection = manager.selection; const selection = manager.selection;
selection.attr('class', 'grabbing'); selection.attr('class', 'grabbing');
// 更新拓扑图中各个节点的节点坐标 // 更新当前的 elkGraph
const context = manager.dragContext;
const nodes = [];
const edges = [];
const layoutOptions = {
// node 固定不动
'org.eclipse.elk.stress.fixed': true,
// node 的 port 固定不动
'org.eclipse.elk.portConstraints': 'FIXED_POS'
};
// 添加自己的节点
const cell = manager.selection.datum();
// 获取 connection
const id2manager = rootRender.id2manager;
const cellNode = {
id: cell.id,
x: cell.x,
y: cell.y,
width: cell.width,
height: cell.height,
// 后续遍历时获取该 port
ports: [],
layoutOptions
};
// 添加邻居的节点和边
for (const neighbor of context.neighbors) {
const ndata = neighbor.selection.datum();
const node = {
id: ndata.id,
width: ndata.width,
height: ndata.height,
x: ndata.x,
y: ndata.y,
ports: [],
layoutOptions
};
// 加入 port如果有的话
for (const connection of neighbor.connections) {
const conndata = connection.selection.datum();
node.ports.push({
id: conndata.id,
width: conndata.width,
height: conndata.height,
x: conndata.x,
y: conndata.y
});
}
nodes.push(node);
edges.push({
id: neighbor.edge.id,
source: neighbor.edge.source,
sourcePort: neighbor.edge.sourcePort,
target: neighbor.edge.target,
targetPort: neighbor.edge.targetPort
});
if (neighbor.location === 'source') {
// 此时 cell 的 connection 就是 targetPort
const cellConnectionId = neighbor.edge.targetPort;
const connectionManager = id2manager.get(cellConnectionId)[0];
const conndata = connectionManager.data;
cellNode.ports.push({
id: conndata.id,
width: conndata.width,
height: conndata.height,
x: conndata.x,
y: conndata.y
});
} else {
// 此时 cell 的 connection 就是 sourcePort
const cellConnectionId = neighbor.edge.sourcePort;
const connectionManager = id2manager.get(cellConnectionId)[0];
const conndata = connectionManager.data;
cellNode.ports.push({
id: conndata.id,
width: conndata.width,
height: conndata.height,
x: conndata.x,
y: conndata.y
});
}
}
nodes.push(cellNode);
context.elkGraph.children = nodes;
context.elkGraph.edges = edges;
} }
/** /**
@ -158,7 +247,7 @@ function dragStart(event, manager, rootRender) {
* @param {BasicD3ManagmentItem} manager * @param {BasicD3ManagmentItem} manager
* @param {NetlistRender} rootRender * @param {NetlistRender} rootRender
*/ */
function dragged(event, manager, rootRender) { async function dragged(event, manager, rootRender) {
// 当拖动结束时D3 会根据绑定的数据data.x 和 data.y重新渲染元素导致元素回到初始位置。 // 当拖动结束时D3 会根据绑定的数据data.x 和 data.y重新渲染元素导致元素回到初始位置。
// 所以需要 手动更新 data.x 和 data.y // 所以需要 手动更新 data.x 和 data.y
const selection = manager.selection; const selection = manager.selection;
@ -173,7 +262,9 @@ function dragged(event, manager, rootRender) {
// 根据最小拓扑图,提取出关键点,重新计算布局 // 根据最小拓扑图,提取出关键点,重新计算布局
const context = manager.dragContext; const context = manager.dragContext;
console.log(context.elkGraph); const elkGraph = context.elkGraph;
const computedLayout = await rootRender.elk.layout(elkGraph);
console.log(computedLayout);
} }
/** /**
@ -182,7 +273,7 @@ function dragged(event, manager, rootRender) {
* @param {BasicD3ManagmentItem} manager * @param {BasicD3ManagmentItem} manager
* @param {NetlistRender} rootRender * @param {NetlistRender} rootRender
*/ */
function dragEnd(event, manager, rootRender) { async function dragEnd(event, manager, rootRender) {
const selection = manager.selection; const selection = manager.selection;
selection.attr('class', 'grab'); selection.attr('class', 'grab');
} }

View File

@ -92,7 +92,7 @@ export class ConnectionRender {
id2manager.set(data.id, []); id2manager.set(data.id, []);
} }
id2manager.set(data.id, managerItem); id2manager.get(data.id).push(managerItem);
return managerItem; return managerItem;
} }
} }

View File

@ -121,25 +121,6 @@ export class NetlistRender {
return layoutGraph; return layoutGraph;
} }
/**
* @description 窗体大小发生变化时
*/
registerResizeHandler(element) {
this.resizeMonitor = new ResizeObserver(entries => {
for (const entry of entries) {
const { height, width } = entry.contentRect;
this.renderHeight = height;
this.renderWidth = width;
if (this.selection) {
this.selection
.attr('width', this.renderWidth)
.attr('height', this.renderHeight)
}
}
});
this.resizeMonitor.observe(element);
}
/** /**
* *
* @param {ElkNode} computedLayout * @param {ElkNode} computedLayout
@ -167,9 +148,13 @@ export class NetlistRender {
// 将分组作为一个后续操作的 parent selection // 将分组作为一个后续操作的 parent selection
const g = svg.append('g'); const g = svg.append('g');
// debug
console.log(computedLayout); console.log(computedLayout);
// 生成连接
await this.renderLine(g, computedLayout, ratio); await this.renderLine(g, computedLayout, ratio);
// 生成实体
await this.renderEntity(g, computedLayout, ratio); await this.renderEntity(g, computedLayout, ratio);
// svg 挂载为全局注册的 selection // svg 挂载为全局注册的 selection
@ -181,6 +166,11 @@ export class NetlistRender {
// 根据最大最小尺寸微调全局方位 // 根据最大最小尺寸微调全局方位
this.adjustLocation(g); this.adjustLocation(g);
setTimeout(() => {
// 生成各个 manager 的拖拽上下文
this.createManagerDragContext(computedLayout);
}, 1000);
return svg; return svg;
} }
@ -340,22 +330,77 @@ export class NetlistRender {
this.zoom.transform, this.zoom.transform,
transform transform
); );
}
// if (globalSetting.renderAnimation) { /**
// parentSelection * @description 生成各个 manager 的拖拽上下文
// .transition() * @param {ElkNode} computedLayout
// .duration(1000) */
// .attr('transform', transform); createManagerDragContext(computedLayout) {
// } else { const id2manager = this.id2manager;
// parentSelection.attr('transform', transform);
// } /**
* @returns {DragNeighbor}
*/
function makeDragNeighbor(id, portId) {
if (id === portId) {
// id 和 portId 都代表 port
const manager = id2manager.get(id)[0];
return {
type: 'port',
selection: manager.selection,
connections: []
}
} else {
// id 代表 cell portId 代表 connection
const cellManager = id2manager.get(id)[0];
const connectionManager = id2manager.get(portId)[0];
return {
type: 'cell',
selection: cellManager.selection,
connections: [
{ selection: connectionManager.selection }
]
}
}
}
for (const edge of computedLayout.edges) {
// 遍历 edge根据两侧 sourcePort 和 targetPort 将对应的 manager 中的 dragContext 进行更新
const sourceNeighoor = makeDragNeighbor(edge.source, edge.sourcePort);
const targetNeighoor = makeDragNeighbor(edge.target, edge.targetPort);
sourceNeighoor.location = 'source';
targetNeighoor.location = 'target';
sourceNeighoor.edge = edge;
targetNeighoor.edge = edge;
const sourceManager = id2manager.get(edge.source)[0];
const targetManager = id2manager.get(edge.target)[0];
sourceManager.dragContext.neighbors.push(targetNeighoor);
targetManager.dragContext.neighbors.push(sourceNeighoor);
}
} }
/** /**
* @description 初次渲染完成后布局不一定正确需要进行调整output ports * @description 窗体大小发生变化时
*/ */
adjustLayoyt() { registerResizeHandler(element) {
this.resizeMonitor = new ResizeObserver(entries => {
for (const entry of entries) {
const { height, width } = entry.contentRect;
this.renderHeight = height;
this.renderWidth = width;
if (this.selection) {
this.selection
.attr('width', this.renderWidth)
.attr('height', this.renderHeight)
}
}
});
this.resizeMonitor.observe(element);
} }
} }

View File

@ -5,7 +5,6 @@
import { globalLookup } from "../global"; import { globalLookup } from "../global";
import { Cell, ModuleTree } from "./yosys"; import { Cell, ModuleTree } from "./yosys";
export const SKIN_SCALE = 1;
export const LINE_WIDTH = 2; export const LINE_WIDTH = 2;
export const LAYOUT_CONSTANT = { export const LAYOUT_CONSTANT = {
@ -22,8 +21,8 @@ export const LAYOUT_CONSTANT = {
CONSTANT_HEIGHT: 50, CONSTANT_HEIGHT: 50,
// 器件的端口 // 器件的端口
CELL_PORT_HEIGHT: 5, CELL_PORT_HEIGHT: 1,
CELL_PORT_WIDTH: 5 CELL_PORT_WIDTH: 1
}; };
export const ELK_DIRECTION = { export const ELK_DIRECTION = {
@ -121,8 +120,8 @@ export class Module {
// 创建器件节点的 port, port 和 connection 一一对应 // 创建器件节点的 port, port 和 connection 一一对应
const meta = skin.meta; const meta = skin.meta;
const height = meta.height * SKIN_SCALE; const height = meta.height;
const width = meta.width * SKIN_SCALE; const width = meta.width;
const ports = []; const ports = [];
@ -141,14 +140,14 @@ export class Module {
// 计算左侧的 // 计算左侧的
for (let i = 0; i < leftSideConnections.length; ++ i) { for (let i = 0; i < leftSideConnections.length; ++ i) {
const connection = leftSideConnections[i]; const connection = leftSideConnections[i];
const yOffset = meta.getPortYOffset(connection.name) * SKIN_SCALE; const yOffset = meta.getPortYOffset(connection.name);
ports.push({ ports.push({
id: connection.id, id: connection.id,
renderName: connection.name, renderName: connection.name,
renderType: 'cellPort', renderType: 'cellPort',
width: 1, width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
height: 1, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
x: 0, x: 0,
y: yOffset y: yOffset
}); });
@ -159,14 +158,14 @@ export class Module {
// 计算右侧的 // 计算右侧的
for (let i = 0; i < rightSideConnections.length; ++ i) { for (let i = 0; i < rightSideConnections.length; ++ i) {
const connection = rightSideConnections[i]; const connection = rightSideConnections[i];
const yOffset = meta.getPortYOffset(connection.name) * SKIN_SCALE; const yOffset = meta.getPortYOffset(connection.name);
ports.push({ ports.push({
id: connection.id, id: connection.id,
renderName: connection.name, renderName: connection.name,
renderType: 'cellPort', renderType: 'cellPort',
width: 1, width: LAYOUT_CONSTANT.CELL_PORT_WIDTH,
height: 1, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT,
x: width, x: width,
y: yOffset y: yOffset
}); });
@ -267,8 +266,10 @@ export class Module {
// 创建常数到器件的连线 // 创建常数到器件的连线
const edge = { const edge = {
id: makeEdgeId(cell.id, id), id: makeEdgeId(cell.id, id),
sources: [connection.id], source: cell.id,
targets: [id] sourcePort: connection.id,
target: id,
targetPort: id
}; };
edges.push(edge); edges.push(edge);

View File

@ -111,6 +111,7 @@ export class PortRender {
const id2manager = this.id2manager; const id2manager = this.id2manager;
// 创建拖拽上下文 // 创建拖拽上下文
const dragContext = { const dragContext = {
neighbors: [],
elkGraph: { elkGraph: {
// elk 是无状态的id 取什么名字都行 // elk 是无状态的id 取什么名字都行
id: 'root', id: 'root',
@ -130,7 +131,7 @@ export class PortRender {
id2manager.set(data.id, []); id2manager.set(data.id, []);
} }
id2manager.set(data.id, managerItem); id2manager.get(data.id).push(managerItem);
return managerItem; return managerItem;
} }
} }

View File

@ -91,7 +91,7 @@ export class WireRender {
id2manager.set(data.id, []); id2manager.set(data.id, []);
} }
id2manager.set(data.id, managerItem); id2manager.get(data.id).push(managerItem);
return managerItem; return managerItem;
} }
} }

View File

@ -74,8 +74,8 @@ class SkinMeta {
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml'); const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');
const element = svgDoc.documentElement; const element = svgDoc.documentElement;
this.width = element.getAttribute('width'); this.width = parseFloat(element.getAttribute('width'));
this.height = element.getAttribute('height'); this.height = parseFloat(element.getAttribute('height'));
this.svgDoc = svgDoc; this.svgDoc = svgDoc;
/** /**

View File

@ -9,5 +9,10 @@
"tips": "نصائح", "tips": "نصائح",
"language-setting": "اللغة", "language-setting": "اللغة",
"setting.language.change-dialog": "لقد قمت بتغيير اللغة إلى {0} ، ونوصي بإعادة تشغيل Netlist Viewer", "setting.language.change-dialog": "لقد قمت بتغيير اللغة إلى {0} ، ونوصي بإعادة تشغيل Netlist Viewer",
"render-animation": "تفعيل الرسوم المتحركة للعرض" "render-animation": "تفعيل الرسوم المتحركة للعرض",
"usermanual": "دليل المستخدم",
"usermanual.click-move": "انقر + اسحب",
"usermanual.move-view": "عرض الجوال",
"usermanual.scale-view": "تكبير/تصغير العرض",
"usermanual.scale-view-more": "تكبير العرض (مقياس أكبر)"
} }

View File

@ -9,5 +9,10 @@
"tips": "Tipps", "tips": "Tipps",
"language-setting": "Sprache", "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" "render-animation": "Rendering-Animation aktivieren",
"usermanual": "Benutzerhandbuch",
"usermanual.click-move": "Klicken + Ziehen",
"usermanual.move-view": "Mobile Ansicht",
"usermanual.scale-view": "Ansicht zoomen",
"usermanual.scale-view-more": "Ansicht vergrößern (größerer Maßstab)"
} }

File diff suppressed because one or more lines are too long

View File

@ -9,5 +9,10 @@
"tips": "Conseils", "tips": "Conseils",
"language-setting": "Langue", "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" "render-animation": "Activer l'animation de rendu",
"usermanual": "Manuel utilisateur",
"usermanual.click-move": "Cliquer + Glisser",
"usermanual.move-view": "Vue mobile",
"usermanual.scale-view": "Zoom de la vue",
"usermanual.scale-view-more": "Zoom de la vue (échelle plus grande)"
} }

View File

@ -9,5 +9,10 @@
"tips": "ヒント", "tips": "ヒント",
"language-setting": "言語", "language-setting": "言語",
"setting.language.change-dialog": "言語を {0} に変更しました。Netlist Viewer を再起動することをお勧めします。", "setting.language.change-dialog": "言語を {0} に変更しました。Netlist Viewer を再起動することをお勧めします。",
"render-animation": "レンダリングアニメーションを有効にする" "render-animation": "レンダリングアニメーションを有効にする",
"usermanual": "使用説明",
"usermanual.click-move": "クリック + ドラッグ",
"usermanual.move-view": "モバイル表示",
"usermanual.scale-view": "ビューのズーム",
"usermanual.scale-view-more": "ビューのズーム(より大きなスケール)"
} }

View File

@ -9,5 +9,10 @@
"tips": "팁", "tips": "팁",
"language-setting": "언어", "language-setting": "언어",
"setting.language.change-dialog": "언어를 {0} 으로 변경했습니다. Netlist Viewer 를 다시 시작하는 것을 권장합니다.", "setting.language.change-dialog": "언어를 {0} 으로 변경했습니다. Netlist Viewer 를 다시 시작하는 것을 권장합니다.",
"render-animation": "렌더링 애니메이션 활성화" "render-animation": "렌더링 애니메이션 활성화",
"usermanual": "사용 설명서",
"usermanual.click-move": "클릭 + 드래그",
"usermanual.move-view": "모바일 보기",
"usermanual.scale-view": "보기 확대/축소",
"usermanual.scale-view-more": "보기 확대 (더 큰 스케일)"
} }

View File

@ -9,5 +9,10 @@
"tips": "Советы", "tips": "Советы",
"language-setting": "Язык", "language-setting": "Язык",
"setting.language.change-dialog": "Вы изменили язык на {0}, мы рекомендуем перезапустить Netlist Viewer.", "setting.language.change-dialog": "Вы изменили язык на {0}, мы рекомендуем перезапустить Netlist Viewer.",
"render-animation": "Включить анимацию рендеринга" "render-animation": "Включить анимацию рендеринга",
"usermanual": "Руководство пользователя",
"usermanual.click-move": "Клик + Перетаскивание",
"usermanual.move-view": "Мобильный вид",
"usermanual.scale-view": "Масштабирование вида",
"usermanual.scale-view-more": "Масштабирование вида (больший масштаб)"
} }

View File

@ -9,5 +9,10 @@
"tips": "提示", "tips": "提示",
"language-setting": "语言", "language-setting": "语言",
"setting.language.change-dialog": "您已经更换语言为 {0} ,我们建议您重启 Netlist Viewer", "setting.language.change-dialog": "您已经更换语言为 {0} ,我们建议您重启 Netlist Viewer",
"render-animation": "开启渲染动画" "render-animation": "开启渲染动画",
"usermanual": "使用说明",
"usermanual.click-move": "点击 + 拖动",
"usermanual.move-view": "移动视图",
"usermanual.scale-view": "视图缩放",
"usermanual.scale-view-more": "视图缩放(尺度更大)"
} }

View File

@ -9,5 +9,10 @@
"tips": "提示", "tips": "提示",
"language-setting": "語言", "language-setting": "語言",
"setting.language.change-dialog": "您已將語言更改為 {0} ,我們建議您重新啟動 Netlist Viewer。", "setting.language.change-dialog": "您已將語言更改為 {0} ,我們建議您重新啟動 Netlist Viewer。",
"render-animation": "開啟渲染動畫" "render-animation": "開啟渲染動畫",
"usermanual": "使用說明",
"usermanual.click-move": "點擊 + 拖動",
"usermanual.move-view": "移動視圖",
"usermanual.scale-view": "視圖縮放",
"usermanual.scale-view-more": "視圖縮放(尺度更大)"
} }