import React, { useState, useEffect, useCallback, useRef } from 'react'; import type { CADLayer, CADProperties, CADElement } from '../../types/cad.types'; import './PropertiesPanel.css'; // ─── Types (local-only, not duplicated from cad.types.ts) ───────────────────── interface YjsDocumentLike { layers: { forEach: (cb: (layer: CADLayer, id: string) => void) => void; observe: (cb: () => void) => void; unobserve: (cb: () => void) => void; }; elements: { toArray: () => CADElement[]; observe: (cb: () => void) => void; unobserve: (cb: () => void) => void; }; updateElement: (id: string, updates: Partial) => void; getElementById: (id: string) => CADElement | undefined; } interface FabricObjectLike { id?: string; type?: string; left?: number; top?: number; width?: number; height?: number; angle?: number; fill?: string | null; stroke?: string | null; strokeWidth?: number; strokeDashArray?: number[]; set: (props: Record) => void; setCoords: () => void; } interface FabricCanvasLike { getActiveObject: () => FabricObjectLike | null; on: (event: string, handler: (...args: unknown[]) => void) => void; off: (event: string, handler: (...args: unknown[]) => void) => void; renderAll: () => void; fire: (event: string, payload?: unknown) => void; } interface CanvasRefLike { getCanvas?: () => FabricCanvasLike; getSelectedElements?: () => FabricObjectLike[]; } interface PropertiesPanelProps { canvasRef: React.RefObject; yjsDocRef: React.RefObject; } // ─── Helpers ───────────────────────────────────────────────────────────────── const LINE_TYPE_DASH_MAP: Record = { solid: null, dashed: [8, 4], dotted: [2, 3], }; function dashArrayToLineType(dashArray?: number[] | null): 'solid' | 'dashed' | 'dotted' { if (!dashArray || dashArray.length === 0) return 'solid'; if (dashArray[0] <= 2) return 'dotted'; return 'dashed'; } function clampNumber(value: string, min: number, max: number): number { const n = parseFloat(value); if (Number.isNaN(n)) return min; return Math.min(Math.max(n, min), max); } // ─── Component ─────────────────────────────────────────────────────────────── const PropertiesPanel: React.FC = ({ canvasRef, yjsDocRef }) => { const [selectedElement, setSelectedElement] = useState(null); const [layers, setLayers] = useState([]); const [fabricObject, setFabricObject] = useState(null); const fabricCanvasRef = useRef(null); // Debounce timer for Yjs updates (300ms) — no new dependency needed const yjsDebounceRef = useRef | null>(null); // Clear debounce timer on unmount useEffect(() => { return () => { if (yjsDebounceRef.current) { clearTimeout(yjsDebounceRef.current); } }; }, []); // ── Sync layers from YjsDocument ────────────────────────────────────────── useEffect(() => { const yjsDoc = yjsDocRef?.current; if (!yjsDoc) return; const syncLayers = () => { const arr: CADLayer[] = []; yjsDoc.layers.forEach((layer, id) => { arr.push({ ...layer, id }); }); arr.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); setLayers(arr); }; syncLayers(); yjsDoc.layers.observe(syncLayers); return () => yjsDoc.layers.unobserve(syncLayers); }, [yjsDocRef]); // ── Sync elements from YjsDocument (for external updates) ───────────────── useEffect(() => { const yjsDoc = yjsDocRef?.current; if (!yjsDoc) return; const syncElements = () => { if (selectedElement) { const updated = yjsDoc.getElementById(selectedElement.id); if (updated) { setSelectedElement(updated); } } }; yjsDoc.elements.observe(syncElements); return () => yjsDoc.elements.unobserve(syncElements); }, [yjsDocRef, selectedElement]); // ── Wire fabric selection events ────────────────────────────────────────── useEffect(() => { const ref = canvasRef?.current; if (!ref || !ref.getCanvas) return; const fabricCanvas = ref.getCanvas(); if (!fabricCanvas) return; fabricCanvasRef.current = fabricCanvas; const handleSelection = () => { const active = fabricCanvas.getActiveObject(); if (!active) { setSelectedElement(null); setFabricObject(null); return; } setFabricObject(active); // Try to match with YjsDocument element by id const yjsDoc = yjsDocRef?.current; const elementId = (active as FabricObjectLike).id; if (elementId && yjsDoc) { const el = yjsDoc.getElementById(elementId); if (el) { setSelectedElement(el); return; } } // Fallback: build a transient element from fabric properties setSelectedElement({ id: (active as FabricObjectLike).id ?? 'transient', type: (active as FabricObjectLike).type ?? 'unknown', layerId: 'default', x: active.left ?? 0, y: active.top ?? 0, width: active.width ?? 0, height: active.height ?? 0, properties: { fill: (active.fill as string) ?? 'transparent', stroke: active.stroke ?? '#000000', strokeWidth: active.strokeWidth ?? 1, rotation: active.angle ?? 0, lineType: dashArrayToLineType(active.strokeDashArray), }, }); }; const handleCleared = () => { setSelectedElement(null); setFabricObject(null); }; fabricCanvas.on('selection:created', handleSelection); fabricCanvas.on('selection:updated', handleSelection); fabricCanvas.on('selection:cleared', handleCleared); return () => { fabricCanvas.off('selection:created', handleSelection); fabricCanvas.off('selection:updated', handleSelection); fabricCanvas.off('selection:cleared', handleCleared); }; }, [canvasRef, yjsDocRef]); // ── Update handlers (Panel → Canvas + YjsDocument) ────────────────────────── const updateFabricAndYjs = useCallback( (updates: Partial & { properties?: Partial }) => { if (!selectedElement) return; // Debounce Yjs write (300ms) — only write after user pauses typing if (yjsDebounceRef.current) { clearTimeout(yjsDebounceRef.current); } const elId = selectedElement.id; yjsDebounceRef.current = setTimeout(() => { const yjsDoc = yjsDocRef?.current; if (yjsDoc) { yjsDoc.updateElement(elId, updates); } yjsDebounceRef.current = null; }, 300); // Update fabric object directly (immediate, no debounce) const fabricObj = fabricObject; const fabricCanvas = fabricCanvasRef.current; if (fabricObj && fabricCanvas) { if (updates.x !== undefined) fabricObj.set({ left: updates.x }); if (updates.y !== undefined) fabricObj.set({ top: updates.y }); if (updates.width !== undefined) fabricObj.set({ width: updates.width }); if (updates.height !== undefined) fabricObj.set({ height: updates.height }); if (updates.properties?.rotation !== undefined) fabricObj.set({ angle: updates.properties.rotation }); if (updates.properties?.fill !== undefined) fabricObj.set({ fill: updates.properties.fill }); if (updates.properties?.stroke !== undefined) fabricObj.set({ stroke: updates.properties.stroke }); if (updates.properties?.strokeWidth !== undefined) fabricObj.set({ strokeWidth: updates.properties.strokeWidth }); if (updates.properties?.lineType !== undefined) { const dash = LINE_TYPE_DASH_MAP[updates.properties.lineType]; fabricObj.set({ strokeDashArray: dash }); } fabricObj.setCoords(); fabricCanvas.renderAll(); } // Update local state (immediate, no debounce) setSelectedElement((prev) => { if (!prev) return prev; return { ...prev, ...updates, properties: { ...prev.properties, ...updates.properties }, }; }); }, [selectedElement, fabricObject, yjsDocRef] ); // ── Field change handlers ────────────────────────────────────────────────── const handleNumberChange = (field: 'x' | 'y' | 'width' | 'height', value: string) => { const n = clampNumber(value, -100000, 100000); updateFabricAndYjs({ [field]: n }); }; const handleRotationChange = (value: string) => { const n = clampNumber(value, -360, 360); updateFabricAndYjs({ properties: { rotation: n } }); }; const handleFillChange = (value: string) => { updateFabricAndYjs({ properties: { fill: value } }); }; const handleStrokeChange = (value: string) => { updateFabricAndYjs({ properties: { stroke: value } }); }; const handleStrokeWidthChange = (value: string) => { const n = clampNumber(value, 0, 100); updateFabricAndYjs({ properties: { strokeWidth: n } }); }; const handleLineTypeChange = (value: 'solid' | 'dashed' | 'dotted') => { updateFabricAndYjs({ properties: { lineType: value } }); }; const handleLayerChange = (value: string) => { updateFabricAndYjs({ layerId: value }); }; // ── Render ────────────────────────────────────────────────────────────────── if (!selectedElement) { return (

Kein Element ausgewählt. Wähle ein Element auf dem Canvas, um seine Eigenschaften zu bearbeiten.

); } const props = selectedElement.properties; const fillValue = props.fill ?? 'transparent'; const strokeValue = props.stroke ?? '#000000'; const strokeWidthValue = props.strokeWidth ?? 1; const rotationValue = props.rotation ?? 0; const lineTypeValue = props.lineType ?? 'solid'; return (
{/* Element info header */}
{selectedElement.type} ID: {selectedElement.id.substring(0, 12)}
{/* Position section */}
Position
{/* Size section */}
Größe
{/* Rotation section */}
Rotation
{/* Appearance section */}
Erscheinungsbild
{/* Layer assignment section */}
Layer
); }; export default PropertiesPanel;