/** * Task G2 – LayoutBar: Layout-Verwaltung + Viewport-Minimap-Widget. * * - Listet Layouts des aktiven Dokuments (documentService-Fallback) * - Klick auf Eintrag = zoom-to gespeichertem Viewport (Bridge) * - „+“ speichert aktuellen Viewport als neues Layout * - „🗑“ löscht Layout; je Eintrag Minimap (Modellbereich + Viewport-Rechteck) */ import React, { useCallback, useEffect, useState } from 'react'; import type { LayoutDefinition } from '../types/cad.types'; import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService'; import { insertTitleBlockForLayout } from '../services/titleBlockService'; import { printLayout } from '../services/printService'; import { useT } from '../i18n/strings'; export interface LayoutView { center: { x: number; y: number }; scale: number; } export interface LayoutBridge { zoomTo(center: { x: number; y: number }, scale: number): void; getView(): LayoutView; } interface LayoutBarProps { /** Aktives Dokument (Fallback: documentService, Task G2 UI-Unabhängigkeit). */ doc?: { getAllLayouts(): LayoutDefinition[]; addLayout(l: LayoutDefinition): void; deleteLayout(id: string): void; getAllElements?(): Array<{ x: number; y: number; width: number; height: number }>; onChanged?(cb: () => void): () => void; } | null; /** Canvas-Bridge (Fallback: window.__v2LayoutBridge aus CanvasArea). */ bridge?: LayoutBridge | null; } interface Rect { x: number; y: number; w: number; h: number } /** * Minimap-Geometrie (Task G2, rein & getestet): * Modell-BBox füllt das Widget (Stretch mit 6px Padding); ohne Elemente * Volles Widget. Viewport-Rechteck = nominale Ansicht 800×600/scale Welt. */ export function computeMinimapRects( elements: Array<{ x: number; y: number; width: number; height: number }>, view: LayoutView, widgetW: number, widgetH: number, ): { model: Rect; view: Rect } { if (!Array.isArray(elements) || elements.length === 0) { return { model: { x: 0, y: 0, w: widgetW, h: widgetH }, view: { x: 0, y: 0, w: widgetW, h: widgetH } }; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const el of elements) { minX = Math.min(minX, el.x - el.width / 2); minY = Math.min(minY, el.y - el.height / 2); maxX = Math.max(maxX, el.x + el.width / 2); maxY = Math.max(maxY, el.y + el.height / 2); } if (!isFinite(minX) || maxX - minX <= 0 || maxY - minY <= 0) { return { model: { x: 0, y: 0, w: widgetW, h: widgetH }, view: { x: 0, y: 0, w: widgetW, h: widgetH } }; } const PAD = 6; const model: Rect = { x: PAD, y: PAD, w: widgetW - 2 * PAD, h: widgetH - 2 * PAD }; const wx = (w: number) => model.x + ((w - minX) / (maxX - minX)) * model.w; const wy = (w: number) => model.y + ((w - minY) / (maxY - minY)) * model.h; const viewW = 800 / view.scale; const viewH = 600 / view.scale; const viewRect: Rect = { x: wx(view.center.x - viewW / 2), y: wy(view.center.y - viewH / 2), w: (viewW / (maxX - minX)) * model.w, h: (viewH / (maxY - minY)) * model.h, }; return { model, view: viewRect }; } const WIDGET_W = 100; const WIDGET_H = 80; const LayoutBar: React.FC = ({ doc: docProp, bridge: bridgeProp }) => { const t = useT(); const [doc, setDoc] = useState(docProp ?? getActiveDocument()); const [layouts, setLayouts] = useState([]); const bridge: LayoutBridge | null = bridgeProp ?? ((typeof window !== 'undefined' ? (window as unknown as { __v2LayoutBridge?: LayoutBridge }).__v2LayoutBridge : undefined) ?? null); const reload = useCallback(() => { setLayouts(doc ? doc.getAllLayouts() : []); }, [doc]); useEffect(() => { reload(); // Live-Updates (auch Remote via CRDT), falls das Doc es anbietet const off = doc?.onChanged?.(reload); return () => { if (off) off(); }; }, [reload]); // Fallback-Verdrahtung: aktives Dokument aus dem documentService useEffect(() => { if (docProp) return; setDoc(getActiveDocument()); const off = onActiveDocumentChanged(() => setDoc(getActiveDocument())); return off; }, [docProp]); const handleAdd = () => { if (!bridge || !doc) return; const view = bridge.getView(); doc.addLayout({ id: `layout-${Date.now()}`, name: `Layout ${layouts.length + 1}`, viewport: { center: { ...view.center }, scale: view.scale }, }); reload(); }; const handleDelete = (id: string) => { doc?.deleteLayout(id); reload(); }; const handlePrint = () => { void printLayout(); }; const handleZoomTo = (l: LayoutDefinition) => { bridge?.zoomTo(l.viewport.center, l.viewport.scale); }; const elements = doc?.getAllElements?.() ?? []; return (
{t('layouts.title')}
{layouts.length === 0 ? (
{t('layouts.empty')}
) : (
{layouts.map((l) => { const rects = computeMinimapRects(elements, l.viewport, WIDGET_W, WIDGET_H); return (
); })}
)}
); }; export default LayoutBar;