Skip to content

Commit 20a3ebe

Browse files
author
JackWang032
committed
feat: support export parse tree image and optimize graph styles
1 parent f08a88b commit 20a3ebe

File tree

3 files changed

+96
-21
lines changed

3 files changed

+96
-21
lines changed

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@types/dagre": "^0.7.52",
1616
"@xyflow/react": "^12.4.2",
1717
"dagre": "^0.8.5",
18+
"html-to-image": "^1.11.13",
1819
"monaco-editor": "0.31.0",
1920
"react": "^18.2.0",
2021
"react-dom": "^18.2.0",

website/pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/src/extensions/workbench/treeVisualizerPanel.tsx

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import {
1010
Background,
1111
BackgroundVariant,
1212
Controls,
13-
ReactFlowProvider
13+
ReactFlowProvider,
14+
useReactFlow,
15+
getNodesBounds,
16+
getViewportForBounds,
17+
ControlButton
1418
} from '@xyflow/react';
1519
import dagre from 'dagre';
20+
import { toPng } from 'html-to-image';
1621
import { SerializedTreeNode } from 'monaco-sql-languages/esm/languageService';
1722

1823
import '@xyflow/react/dist/style.css';
@@ -45,12 +50,12 @@ interface NodeStyleProps {
4550
const calculateTextWidth = (text: string): number => {
4651
const canvas = document.createElement('canvas');
4752
const context = canvas.getContext('2d');
48-
if (!context) return 120;
53+
if (!context) return 80;
4954

50-
context.font = '13px Monaco, monospace';
55+
context.font = '14px Monaco, monospace';
5156
const metrics = context.measureText(text);
5257
// 添加内边距和一些缓冲空间
53-
return Math.max(120, Math.ceil(metrics.width + 40));
58+
return Math.max(80, Math.ceil(metrics.width + 20));
5459
};
5560

5661
// 自定义节点样式
@@ -64,31 +69,25 @@ const getNodeStyle = ({
6469
const width = calculateTextWidth(label);
6570

6671
return {
67-
padding: '8px 12px',
68-
border: '2px solid #4a90e2',
69-
borderRadius: '6px',
72+
padding: '8px 0px',
73+
borderRadius: '8px',
7074
backgroundColor:
7175
displayType === NodeDisplayType.TerminalNode
72-
? 'rgb(136 205 255)'
76+
? 'rgba(22, 163, 74, 0.8)'
7377
: displayType === NodeDisplayType.ErrorNode
74-
? 'rgb(255 205 210)'
75-
: '#fff',
76-
color: '#2c3e50',
77-
fontSize: '13px',
78+
? 'rgba(220, 38, 38, 0.8)'
79+
: 'rgba(27, 126, 191, 0.8)',
80+
color: '#ffffff',
81+
fontSize: '12px',
7882
width: width,
7983
textAlign: 'center' as const,
8084
transition: 'all 0.2s ease',
81-
opacity: hasSelection && !isSelected && !isChild ? 0.3 : 1,
82-
boxShadow: isSelected
83-
? '0 0 10px #4a90e2'
84-
: isChild
85-
? '0 0 6px #4a90e2'
86-
: '0 2px 6px rgba(0,0,0,0.1)'
85+
opacity: hasSelection && !isSelected && !isChild ? 0.3 : 1
8786
};
8887
};
8988

9089
const edgeStyle = {
91-
stroke: '#4a90e2',
90+
stroke: 'rgb(14, 99, 156)',
9291
strokeWidth: 2
9392
};
9493

@@ -142,6 +141,70 @@ const getLayoutedElements = <T extends Record<string, any>>(
142141
return { nodes: layoutedNodes, edges };
143142
};
144143

144+
const downloadImage = (dataUrl: string) => {
145+
const a = document.createElement('a');
146+
a.setAttribute('download', `parse-tree-${new Date().toLocaleString()}.png`);
147+
a.setAttribute('href', dataUrl);
148+
a.click();
149+
};
150+
151+
const DownloadButton = () => {
152+
const { getNodes } = useReactFlow();
153+
154+
const onClick = () => {
155+
const nodes = getNodes();
156+
if (nodes.length === 0) return;
157+
158+
const nodesBounds = getNodesBounds(nodes);
159+
160+
const padding = 30;
161+
const imageWidth = nodesBounds.width + padding;
162+
const imageHeight = nodesBounds.height + padding;
163+
164+
// 计算用于导出的视口变换
165+
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.1, 2, 0.1);
166+
167+
const viewportElement = document.querySelector('.react-flow__viewport') as HTMLElement;
168+
if (!viewportElement) return;
169+
170+
toPng(viewportElement, {
171+
backgroundColor: '#ffffff',
172+
width: imageWidth,
173+
height: imageHeight,
174+
style: {
175+
width: `${imageWidth}px`,
176+
height: `${imageHeight}px`,
177+
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
178+
}
179+
})
180+
.then((dataUrl: string) => {
181+
downloadImage(dataUrl);
182+
})
183+
.catch((error: Error) => {
184+
console.error('Download image failed:', error);
185+
});
186+
};
187+
188+
return (
189+
<ControlButton onClick={onClick} title="Download Image">
190+
<svg
191+
width="16"
192+
height="16"
193+
viewBox="0 0 24 24"
194+
fill="none"
195+
stroke="currentColor"
196+
strokeWidth="2"
197+
strokeLinecap="round"
198+
strokeLinejoin="round"
199+
>
200+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
201+
<polyline points="7,10 12,15 17,10" />
202+
<line x1="12" y1="15" x2="12" y2="3" />
203+
</svg>
204+
</ControlButton>
205+
);
206+
};
207+
145208
const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
146209
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([]);
147210
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
@@ -280,6 +343,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
280343
edges={edges}
281344
onNodesChange={onNodesChange}
282345
onEdgesChange={onEdgesChange}
346+
colorMode="dark"
283347
fitView
284348
fitViewOptions={{ padding: 0.2 }}
285349
minZoom={0.1}
@@ -308,7 +372,7 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
308372
gap={12}
309373
size={1}
310374
color="#91919a"
311-
style={{ opacity: 0.6 }}
375+
style={{ opacity: 0.7 }}
312376
/>
313377
<Controls
314378
showInteractive={false}
@@ -317,7 +381,9 @@ const TreeVisualizerContent = ({ parseTree }: TreeVisualizerPanelProps) => {
317381
flexDirection: 'column',
318382
gap: '8px'
319383
}}
320-
/>
384+
>
385+
<DownloadButton />
386+
</Controls>
321387
</ReactFlow>
322388
</div>
323389
);

0 commit comments

Comments
 (0)