fix: replace synthetic canvas preview with real drawing data

- Load drawings + elements via API
- Render actual SVG elements (rect, circle, line, text, polygon, arc)
- Auto-scale viewBox to element bounding box
- Show "Leere Zeichnung" placeholder when no elements
This commit is contained in:
Leopoldadmin
2026-07-04 18:26:14 +02:00
parent 3af9640ace
commit 6c04ca6793
+85 -30
View File
@@ -10,8 +10,10 @@ import {
getProjects, deleteProject, updateProject,
getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder,
moveProjectToFolder, moveProjectFolder, getProjectShares, getUsers, createProjectShare,
getDrawings, getElements,
type Project, type ProjectFolder, type ProjectShare, type SimpleUser,
} from '../services/api';
import type { CADElement } from '../types/cad.types';
const API_BASE = import.meta.env.VITE_API_BASE || '';
const UNFILED_ID = '__unfiled__';
@@ -20,43 +22,96 @@ interface DashboardProps {
onOpenProject: (projectId: string) => void;
}
/** Render a static SVG preview of a project's first few elements */
function CanvasPreview({ project }: { project: Project }) {
// Static preview: use project description or name hash to generate a deterministic mini-canvas
const seed = project.id.charCodeAt(0) + project.id.charCodeAt(project.id.length - 1);
const shapes = useMemo(() => {
const arr: React.ReactNode[] = [];
const colors = ['#2563eb', '#8b5cf6', '#10b981', '#f59e0b'];
for (let i = 0; i < 4; i++) {
const x = 10 + ((seed + i * 23) % 60);
const y = 5 + ((seed + i * 17) % 40);
const w = 8 + ((seed + i * 13) % 20);
const h = 8 + ((seed + i * 7) % 15);
const color = colors[i % colors.length];
const shapeType = (seed + i) % 3;
if (shapeType === 0) {
arr.push(
<rect key={i} x={x} y={y} width={w} height={h} fill="none" stroke={color} strokeWidth={1} rx={1} />
);
} else if (shapeType === 1) {
arr.push(
<circle key={i} cx={x + w / 2} cy={y + h / 2} r={Math.min(w, h) / 2} fill="none" stroke={color} strokeWidth={1} />
);
} else {
arr.push(
<line key={i} x1={x} y1={y} x2={x + w} y2={y + h} stroke={color} strokeWidth={1} />
);
/** Render a real SVG preview of a project's first drawing elements */
function CanvasPreview({ project, token }: { project: Project; token: string }) {
const [elements, setElements] = useState<CADElement[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function loadPreview() {
try {
const drawings = await getDrawings(token, project.id);
if (drawings.length === 0) { if (!cancelled) setElements([]); return; }
const els = await getElements(token, drawings[0].id);
if (!cancelled) setElements(els);
} catch {
if (!cancelled) setElements([]);
} finally {
if (!cancelled) setLoading(false);
}
}
return arr;
}, [seed]);
loadPreview();
return () => { cancelled = true; };
}, [token, project.id]);
// Compute bounding box of all elements for viewBox scaling
const bbox = useMemo(() => {
if (elements.length === 0) return null;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const el of elements) {
minX = Math.min(minX, el.x);
minY = Math.min(minY, el.y);
maxX = Math.max(maxX, el.x + el.width);
maxY = Math.max(maxY, el.y + el.height);
}
return { minX, minY, w: Math.max(maxX - minX, 10), h: Math.max(maxY - minY, 10) };
}, [elements]);
const svgElements = useMemo(() => {
if (!bbox || elements.length === 0) return [];
return elements.slice(0, 50).map((el) => {
const stroke = el.properties.stroke || '#60a5fa';
const fill = el.properties.fill || 'none';
const sw = Math.max((el.properties.strokeWidth || 1), 0.5);
const x = el.x - bbox.minX;
const y = el.y - bbox.minY;
const w = el.width;
const h = el.height;
switch (el.type) {
case 'rect':
case 'table':
case 'stage':
return <rect key={el.id} x={x} y={y} width={w} height={h} fill={fill} stroke={stroke} strokeWidth={sw} />;
case 'circle':
return <circle key={el.id} cx={x + w/2} cy={y + h/2} r={Math.min(w, h)/2} fill={fill} stroke={stroke} strokeWidth={sw} />;
case 'line':
case 'leader':
return <line key={el.id} x1={x} y1={y} x2={x + w} y2={y + h} stroke={stroke} strokeWidth={sw} />;
case 'text':
return <text key={el.id} x={x} y={y + Math.min(h, 10)} fontSize={Math.min(el.properties.fontSize || 8, 10)} fill={stroke}>{el.properties.text || ''}</text>;
case 'polygon':
case 'polyline':
if (el.properties.points && el.properties.points.length > 1) {
const pts = el.properties.points.map(p => `${p.x - bbox.minX},${p.y - bbox.minY}`).join(' ');
return <polygon key={el.id} points={pts} fill={fill} stroke={stroke} strokeWidth={sw} />;
}
return null;
case 'chair':
return <circle key={el.id} cx={x + w/2} cy={y + h/2} r={Math.min(w, h)/2} fill={fill} stroke={stroke} strokeWidth={sw} />;
case 'arc':
return <path key={el.id} d={`M ${x} ${y+h/2} A ${w/2} ${h/2} 0 0 1 ${x+w} ${y+h/2}`} fill="none" stroke={stroke} strokeWidth={sw} />;
default:
return <rect key={el.id} x={x} y={y} width={w} height={h} fill="none" stroke={stroke} strokeWidth={sw} strokeDasharray="2,2" />;
}
});
}, [elements, bbox]);
return (
<div className="dashboard-card-preview" title="Vorschau der Zeichenfläche">
{loading ? (
<div className="dashboard-preview-loading">...</div>
) : elements.length === 0 || !bbox ? (
<svg viewBox="0 0 100 60" preserveAspectRatio="xMidYMid meet" className="dashboard-preview-svg">
<rect x="0" y="0" width="100" height="60" fill="var(--color-canvas-bg-2, #1e293b)" />
{shapes}
<text x="50" y="35" textAnchor="middle" fontSize="6" fill="#64748b">Leere Zeichnung</text>
</svg>
) : (
<svg viewBox={`${bbox.minX - 5} ${bbox.minY - 5} ${bbox.w + 10} ${bbox.h + 10}`} preserveAspectRatio="xMidYMid meet" className="dashboard-preview-svg">
<rect x={bbox.minX - 5} y={bbox.minY - 5} width={bbox.w + 10} height={bbox.h + 10} fill="var(--color-canvas-bg-2, #1e293b)" />
{svgElements}
</svg>
)}
</div>
);
}
@@ -624,7 +679,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
onDragStart={(e) => handleProjectDragStart(e, project.id)}
onDragEnd={() => { setDraggedProjectId(null); setDragOverFolderId(null); }}
>
<CanvasPreview project={project} />
<CanvasPreview project={project} token={token!} />
<h3 className="dashboard-card-title">{project.name}</h3>
{project.description && <p className="dashboard-card-desc">{project.description}</p>}
<div className="dashboard-project-meta">