From 14c2b9b08c8e482f98df482bb0ee05a3c2a02407 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Mon, 8 Jun 2026 00:05:48 +0200 Subject: [PATCH] feat: add screenshot functionality to export canvas layout as a PNG image --- src/App.jsx | 69 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/Tile.jsx | 7 +++--- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c9743ed..1f0c71f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,8 @@ import { Stage, Layer, Group } from 'react-konva'; import { RoomPolygon } from './RoomPolygon'; import { Tile } from './Tile'; import { computeRoomVertices } from './utils/geometry'; -import { Settings2, Plus, Trash2, Download, Upload } from 'lucide-react'; +import { Settings2, Plus, Trash2, Download, Upload, Camera } from 'lucide-react'; + // Fixed real-world side lengths in meters (+ 180° CW: A←C, B←D, C←A, D←B) const SCALE = 70; // 70 pixels per meter @@ -16,7 +17,7 @@ function App() { const SIDE_C = isReducedSize ? 4.50 : 10.40; const SIDE_D = 4.93; - const [alphaDeg, setAlphaDeg] = useState(90); + const alphaDeg = 90; const [tiles, setTiles] = useState([]); const [selectedId, setSelectedId] = useState(null); const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); @@ -28,6 +29,63 @@ function App() { const count240x120 = tiles.filter(t => t.width === 2.4 && t.height === 1.2).length; const countLights = tiles.filter(t => t.type === 'light').length; const containerRef = useRef(null); + const stageRef = useRef(null); + + const downloadScreenshot = () => { + if (stageRef.current) { + const currentSelectedId = selectedId; + setSelectedId(null); + + // Force a slight delay to allow React to render without selection border + setTimeout(() => { + const stage = stageRef.current; + const stageCanvas = stage.toCanvas({ pixelRatio: 2 }); + + const exportCanvas = document.createElement('canvas'); + exportCanvas.width = stageCanvas.width; + exportCanvas.height = stageCanvas.height; + const ctx = exportCanvas.getContext('2d'); + + // Background color matching the dark theme + ctx.fillStyle = '#0f172a'; + ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); + + // Draw the matching grid dots + const dotSpacing = 24 * 2; + const dotRadius = 1 * 2; + ctx.fillStyle = 'rgba(51, 65, 85, 0.3)'; + + const stageX = stage.x() * 2; + const stageY = stage.y() * 2; + const startX = stageX % dotSpacing; + const startY = stageY % dotSpacing; + + for (let x = startX - dotSpacing; x < exportCanvas.width + dotSpacing; x += dotSpacing) { + for (let y = startY - dotSpacing; y < exportCanvas.height + dotSpacing; y += dotSpacing) { + ctx.beginPath(); + ctx.arc(x, y, dotRadius, 0, Math.PI * 2); + ctx.fill(); + } + } + + // Draw the stage on top + ctx.drawImage(stageCanvas, 0, 0); + + // Create download link + const dataUrl = exportCanvas.toDataURL('image/png'); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataUrl); + downloadAnchorNode.setAttribute("download", "room-layout.png"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + + // Restore selection + setSelectedId(currentSelectedId); + }, 50); + } + }; + // Responsive canvas size useEffect(() => { @@ -77,8 +135,6 @@ function App() { }; const addTile = (width, height, label) => { - const isSquare = width === height; - // Assign colors based on dimensions let color = "rgba(16, 185, 129, 0.6)"; // Default Green let stroke = "#10b981"; @@ -264,6 +320,10 @@ function App() { Exportieren... + +