feat(network): add SVG export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ChevronLeft, Save, Download, FileJson, Image, FileText, Undo2, Redo2, MousePointer2, Hand } from 'lucide-react'
|
import { ChevronLeft, Save, Download, FileJson, FileCode, Image, FileText, Undo2, Redo2, MousePointer2, Hand } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export type InteractionMode = 'select' | 'pan'
|
export type InteractionMode = 'select' | 'pan'
|
||||||
@@ -15,6 +15,7 @@ interface DiagramHeaderProps {
|
|||||||
onNameChange: (name: string) => void
|
onNameChange: (name: string) => void
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
onExportPng: () => void
|
onExportPng: () => void
|
||||||
|
onExportSvg: () => void
|
||||||
onExportPdf: () => void
|
onExportPdf: () => void
|
||||||
onExportJson: () => void
|
onExportJson: () => void
|
||||||
onUndo: () => void
|
onUndo: () => void
|
||||||
@@ -35,6 +36,7 @@ export function DiagramHeader({
|
|||||||
onNameChange,
|
onNameChange,
|
||||||
onSave,
|
onSave,
|
||||||
onExportPng,
|
onExportPng,
|
||||||
|
onExportSvg,
|
||||||
onExportPdf,
|
onExportPdf,
|
||||||
onExportJson,
|
onExportJson,
|
||||||
onUndo,
|
onUndo,
|
||||||
@@ -211,6 +213,12 @@ export function DiagramHeader({
|
|||||||
>
|
>
|
||||||
<Image size={12} /> Export PNG
|
<Image size={12} /> Export PNG
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { onExportSvg(); setShowExportMenu(false) }}
|
||||||
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-primary hover:bg-elevated"
|
||||||
|
>
|
||||||
|
<FileCode size={12} /> Export SVG
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onExportPdf(); setShowExportMenu(false) }}
|
onClick={() => { onExportPdf(); setShowExportMenu(false) }}
|
||||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-primary hover:bg-elevated"
|
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs text-primary hover:bg-elevated"
|
||||||
|
|||||||
@@ -643,6 +643,42 @@ function DiagramEditorInner() {
|
|||||||
}
|
}
|
||||||
}, [nodes, name])
|
}, [nodes, name])
|
||||||
|
|
||||||
|
const handleExportSvg = useCallback(async () => {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
toast.warning('Add some devices to the diagram before exporting')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { toSvg } = await import('html-to-image')
|
||||||
|
const IMAGE_WIDTH = 1920
|
||||||
|
const IMAGE_HEIGHT = 1080
|
||||||
|
const bounds = getNodesBounds(nodes)
|
||||||
|
const viewport = getViewportForBounds(bounds, IMAGE_WIDTH, IMAGE_HEIGHT, 0.5, 2, 0.15)
|
||||||
|
const flowEl = document.querySelector('.react-flow__viewport') as HTMLElement | null
|
||||||
|
if (!flowEl) {
|
||||||
|
toast.error('Could not find canvas to export')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dataUrl = await toSvg(flowEl, {
|
||||||
|
backgroundColor: '#16181f',
|
||||||
|
width: IMAGE_WIDTH,
|
||||||
|
height: IMAGE_HEIGHT,
|
||||||
|
style: {
|
||||||
|
width: `${IMAGE_WIDTH}px`,
|
||||||
|
height: `${IMAGE_HEIGHT}px`,
|
||||||
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '') || 'diagram'}.svg`
|
||||||
|
a.href = dataUrl
|
||||||
|
a.click()
|
||||||
|
} catch {
|
||||||
|
toast.error('SVG export failed')
|
||||||
|
}
|
||||||
|
}, [nodes, name])
|
||||||
|
|
||||||
const handleExportPdf = useCallback(() => {
|
const handleExportPdf = useCallback(() => {
|
||||||
window.print()
|
window.print()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -683,6 +719,7 @@ function DiagramEditorInner() {
|
|||||||
onNameChange={(n: string) => { setName(n); setIsDirty(true) }}
|
onNameChange={(n: string) => { setName(n); setIsDirty(true) }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onExportPng={handleExportPng}
|
onExportPng={handleExportPng}
|
||||||
|
onExportSvg={handleExportSvg}
|
||||||
onExportPdf={handleExportPdf}
|
onExportPdf={handleExportPdf}
|
||||||
onExportJson={handleExportJson}
|
onExportJson={handleExportJson}
|
||||||
onUndo={undo}
|
onUndo={undo}
|
||||||
|
|||||||
Reference in New Issue
Block a user