feat: add screenshot functionality to export canvas layout as a PNG image
This commit is contained in:
69
src/App.jsx
69
src/App.jsx
@@ -3,7 +3,8 @@ import { Stage, Layer, Group } from 'react-konva';
|
|||||||
import { RoomPolygon } from './RoomPolygon';
|
import { RoomPolygon } from './RoomPolygon';
|
||||||
import { Tile } from './Tile';
|
import { Tile } from './Tile';
|
||||||
import { computeRoomVertices } from './utils/geometry';
|
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)
|
// Fixed real-world side lengths in meters (+ 180° CW: A←C, B←D, C←A, D←B)
|
||||||
const SCALE = 70; // 70 pixels per meter
|
const SCALE = 70; // 70 pixels per meter
|
||||||
@@ -16,7 +17,7 @@ function App() {
|
|||||||
const SIDE_C = isReducedSize ? 4.50 : 10.40;
|
const SIDE_C = isReducedSize ? 4.50 : 10.40;
|
||||||
const SIDE_D = 4.93;
|
const SIDE_D = 4.93;
|
||||||
|
|
||||||
const [alphaDeg, setAlphaDeg] = useState(90);
|
const alphaDeg = 90;
|
||||||
const [tiles, setTiles] = useState([]);
|
const [tiles, setTiles] = useState([]);
|
||||||
const [selectedId, setSelectedId] = useState(null);
|
const [selectedId, setSelectedId] = useState(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
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 count240x120 = tiles.filter(t => t.width === 2.4 && t.height === 1.2).length;
|
||||||
const countLights = tiles.filter(t => t.type === 'light').length;
|
const countLights = tiles.filter(t => t.type === 'light').length;
|
||||||
const containerRef = useRef(null);
|
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
|
// Responsive canvas size
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,8 +135,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addTile = (width, height, label) => {
|
const addTile = (width, height, label) => {
|
||||||
const isSquare = width === height;
|
|
||||||
|
|
||||||
// Assign colors based on dimensions
|
// Assign colors based on dimensions
|
||||||
let color = "rgba(16, 185, 129, 0.6)"; // Default Green
|
let color = "rgba(16, 185, 129, 0.6)"; // Default Green
|
||||||
let stroke = "#10b981";
|
let stroke = "#10b981";
|
||||||
@@ -264,6 +320,10 @@ function App() {
|
|||||||
<Download size={14} /> Exportieren...
|
<Download size={14} /> Exportieren...
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button onClick={downloadScreenshot} className="btn btn-primary" style={{ backgroundColor: '#4b5563', boxShadow: '0 4px 6px -1px rgba(75, 85, 99, 0.4)' }}>
|
||||||
|
<Camera size={14} /> Screenshot...
|
||||||
|
</button>
|
||||||
|
|
||||||
<label className="btn btn-primary" style={{ backgroundColor: '#4b5563', boxShadow: '0 4px 6px -1px rgba(75, 85, 99, 0.4)', cursor: 'pointer', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
<label className="btn btn-primary" style={{ backgroundColor: '#4b5563', boxShadow: '0 4px 6px -1px rgba(75, 85, 99, 0.4)', cursor: 'pointer', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<Upload size={14} /> Importieren...
|
<Upload size={14} /> Importieren...
|
||||||
<input type="file" accept=".json" onChange={importTiles} style={{ display: 'none' }} />
|
<input type="file" accept=".json" onChange={importTiles} style={{ display: 'none' }} />
|
||||||
@@ -282,6 +342,7 @@ function App() {
|
|||||||
{/* Main Canvas Area */}
|
{/* Main Canvas Area */}
|
||||||
<main className="canvas-container" ref={containerRef}>
|
<main className="canvas-container" ref={containerRef}>
|
||||||
<Stage
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
width={dimensions.width}
|
width={dimensions.width}
|
||||||
height={dimensions.height}
|
height={dimensions.height}
|
||||||
draggable
|
draggable
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
import { Rect, Group, Transformer } from 'react-konva';
|
import { Rect, Group, Transformer } from 'react-konva';
|
||||||
|
|
||||||
export function Tile({ id, initialX, initialY, scale, meterWidth = 1.6, meterHeight = 1.6, color = "rgba(16, 185, 129, 0.6)", stroke = "#10b981", isSelected, onSelect, type, rotation = 0, onChange }) {
|
export function Tile({ initialX, initialY, scale, meterWidth = 1.6, meterHeight = 1.6, color = "rgba(16, 185, 129, 0.6)", stroke = "#10b981", isSelected, onSelect, type, rotation = 0, onChange }) {
|
||||||
const shapeRef = useRef();
|
const shapeRef = useRef();
|
||||||
const trRef = useRef();
|
const trRef = useRef();
|
||||||
const pixelWidth = meterWidth * scale;
|
const pixelWidth = meterWidth * scale;
|
||||||
@@ -54,7 +55,7 @@ export function Tile({ id, initialX, initialY, scale, meterWidth = 1.6, meterHei
|
|||||||
y: e.target.y()
|
y: e.target.y()
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onTransformEnd={(e) => {
|
onTransformEnd={() => {
|
||||||
const node = shapeRef.current;
|
const node = shapeRef.current;
|
||||||
node.scaleX(1);
|
node.scaleX(1);
|
||||||
node.scaleY(1);
|
node.scaleY(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user