// ============================================ // ANWENDUNGSLOGIK // Gutachter Symbolbibliothek v2.0 // ============================================ // ========== GLOBALE VARIABLEN ========== let currentFilter = 'all'; let currentSearch = ''; let selectedSymbols = new Set(); let legendItems = []; // ========== INITIALISIERUNG ========== document.addEventListener('DOMContentLoaded', function() { renderSymbols(); setupEventListeners(); loadLegendFromStorage(); }); // ========== EVENT LISTENERS ========== function setupEventListeners() { // Suche document.getElementById('searchInput').addEventListener('input', function(e) { currentSearch = e.target.value.toLowerCase(); renderSymbols(); }); // Filter Pills document.querySelectorAll('.filter-pill').forEach(pill => { pill.addEventListener('click', function() { document.querySelectorAll('.filter-pill').forEach(p => p.classList.remove('active')); this.classList.add('active'); currentFilter = this.dataset.filter; renderSymbols(); }); }); // Modal schließen document.getElementById('legendModal').addEventListener('click', function(e) { if (e.target === this) { closeLegendModal(); } }); // Escape-Taste zum Schließen document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeLegendModal(); } }); } // ========== SYMBOLE RENDERN ========== function renderSymbols() { const container = document.getElementById('symbolGrid'); container.innerHTML = ''; let hasResults = false; Object.keys(SYMBOLS).forEach(categoryKey => { const category = SYMBOLS[categoryKey]; // Filter nach Kategorie if (currentFilter !== 'all' && currentFilter !== categoryKey) { return; } const filteredItems = category.items.filter(item => { if (!currentSearch) return true; return item.name.toLowerCase().includes(currentSearch) || item.tags.some(tag => tag.toLowerCase().includes(currentSearch)); }); if (filteredItems.length === 0) return; hasResults = true; // Kategorie-Header const categoryHeader = document.createElement('div'); categoryHeader.className = 'category-header'; categoryHeader.innerHTML = `${category.icon} ${category.name}${filteredItems.length} Symbole`; container.appendChild(categoryHeader); // Symbol-Grid für diese Kategorie const categoryGrid = document.createElement('div'); categoryGrid.className = 'category-grid'; filteredItems.forEach(item => { const card = createSymbolCard(item, categoryKey); categoryGrid.appendChild(card); }); container.appendChild(categoryGrid); }); if (!hasResults) { container.innerHTML = '
Keine Symbole gefunden. Versuchen Sie einen anderen Suchbegriff.
'; } } // ========== SYMBOL-KARTE ERSTELLEN ========== function createSymbolCard(item, categoryKey) { const card = document.createElement('div'); card.className = 'symbol-card'; // Spezielle Klasse für Vermessungssymbole if (categoryKey.startsWith('vermessung_')) { card.classList.add('vermessung'); } const isSelected = selectedSymbols.has(item.id); if (isSelected) { card.classList.add('selected'); } card.innerHTML = `
${item.svg}
${item.name}
`; return card; } // ========== SYMBOL FINDEN ========== function findSymbol(id) { for (const categoryKey of Object.keys(SYMBOLS)) { const item = SYMBOLS[categoryKey].items.find(i => i.id === id); if (item) return item; } return null; } // ========== BILD KOPIEREN (transparent) ========== async function copyAsImage(id) { const item = findSymbol(id); if (!item) return; try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const size = 256; canvas.width = size; canvas.height = size; // Transparenter Hintergrund (kein fillRect) // ctx.fillStyle = 'white'; // Entfernt für Transparenz // ctx.fillRect(0, 0, size, size); // Entfernt für Transparenz const img = new Image(); const svgBlob = new Blob([item.svg], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(svgBlob); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = url; }); ctx.drawImage(img, 0, 0, size, size); URL.revokeObjectURL(url); canvas.toBlob(async (blob) => { try { await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]); showNotification('Bild in Zwischenablage kopiert!'); } catch (err) { // Fallback: Download const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = item.filename.replace('.svg', '.png'); link.click(); showNotification('PNG heruntergeladen (Kopieren nicht unterstützt)'); } }, 'image/png'); } catch (err) { console.error('Fehler beim Bild-Export:', err); showNotification('Fehler beim Kopieren', 'error'); } } // ========== SVG DOWNLOAD ========== function downloadSVG(id) { const item = findSymbol(id); if (!item) return; const blob = new Blob([item.svg], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = item.filename; link.click(); URL.revokeObjectURL(url); showNotification('SVG heruntergeladen!'); } // ========== DXF EXPORT (AutoCAD R12 kompatibel) ========== function svgToDxf(svgString, scaleFactor = 1) { const parser = new DOMParser(); const svg = parser.parseFromString(svgString, 'image/svg+xml').documentElement; const viewBox = svg.getAttribute('viewBox')?.split(' ').map(Number) || [0, 0, 64, 64]; const height = viewBox[3]; let entities = ''; function flipY(y) { return (height - y) * scaleFactor; } function scaleX(x) { return x * scaleFactor; } function processElement(el) { const tag = el.tagName?.toLowerCase(); if (!tag) return; switch(tag) { case 'line': const x1 = parseFloat(el.getAttribute('x1') || 0); const y1 = parseFloat(el.getAttribute('y1') || 0); const x2 = parseFloat(el.getAttribute('x2') || 0); const y2 = parseFloat(el.getAttribute('y2') || 0); entities += createDxfLine(scaleX(x1), flipY(y1), scaleX(x2), flipY(y2)); break; case 'rect': const rx = parseFloat(el.getAttribute('x') || 0); const ry = parseFloat(el.getAttribute('y') || 0); const rw = parseFloat(el.getAttribute('width') || 0); const rh = parseFloat(el.getAttribute('height') || 0); // Rechteck als 4 Linien entities += createDxfLine(scaleX(rx), flipY(ry), scaleX(rx + rw), flipY(ry)); entities += createDxfLine(scaleX(rx + rw), flipY(ry), scaleX(rx + rw), flipY(ry + rh)); entities += createDxfLine(scaleX(rx + rw), flipY(ry + rh), scaleX(rx), flipY(ry + rh)); entities += createDxfLine(scaleX(rx), flipY(ry + rh), scaleX(rx), flipY(ry)); break; case 'circle': const cx = parseFloat(el.getAttribute('cx') || 0); const cy = parseFloat(el.getAttribute('cy') || 0); const r = parseFloat(el.getAttribute('r') || 0); entities += createDxfCircle(scaleX(cx), flipY(cy), r * scaleFactor); break; case 'ellipse': const ecx = parseFloat(el.getAttribute('cx') || 0); const ecy = parseFloat(el.getAttribute('cy') || 0); const erx = parseFloat(el.getAttribute('rx') || 0); const ery = parseFloat(el.getAttribute('ry') || 0); // Ellipse als Kreis approximieren (Durchschnitt) entities += createDxfCircle(scaleX(ecx), flipY(ecy), ((erx + ery) / 2) * scaleFactor); break; case 'polygon': case 'polyline': const points = el.getAttribute('points'); if (points) { const pts = points.trim().split(/[\s,]+/).map(Number); for (let i = 0; i < pts.length - 2; i += 2) { entities += createDxfLine( scaleX(pts[i]), flipY(pts[i+1]), scaleX(pts[i+2]), flipY(pts[i+3]) ); } // Polygon schließen if (tag === 'polygon' && pts.length >= 4) { entities += createDxfLine( scaleX(pts[pts.length-2]), flipY(pts[pts.length-1]), scaleX(pts[0]), flipY(pts[1]) ); } } break; case 'path': const d = el.getAttribute('d'); if (d) { const pathEntities = parseSvgPath(d, scaleX, flipY); entities += pathEntities; } break; case 'text': const tx = parseFloat(el.getAttribute('x') || 0); const ty = parseFloat(el.getAttribute('y') || 0); const textContent = el.textContent || ''; const fontSize = parseFloat(el.getAttribute('font-size') || 10); entities += createDxfText(scaleX(tx), flipY(ty), textContent, fontSize * scaleFactor * 0.7); break; case 'g': case 'svg': Array.from(el.children).forEach(child => processElement(child)); break; } } processElement(svg); // DXF mit AutoCAD R12 Format (AC1009) - CRLF Zeilenenden const dxf = [ '0', 'SECTION', '2', 'HEADER', '9', '$ACADVER', '1', 'AC1009', '9', '$INSBASE', '10', '0.0', '20', '0.0', '30', '0.0', '9', '$EXTMIN', '10', '0.0', '20', '0.0', '30', '0.0', '9', '$EXTMAX', '10', String(height * scaleFactor), '20', String(height * scaleFactor), '30', '0.0', '0', 'ENDSEC', '0', 'SECTION', '2', 'TABLES', '0', 'TABLE', '2', 'LAYER', '70', '1', '0', 'LAYER', '2', '0', '70', '0', '62', '7', '6', 'CONTINUOUS', '0', 'ENDTAB', '0', 'ENDSEC', '0', 'SECTION', '2', 'ENTITIES', entities, '0', 'ENDSEC', '0', 'EOF' ].join('\r\n'); return dxf; } function createDxfLine(x1, y1, x2, y2) { return [ '0', 'LINE', '8', '0', '10', x1.toFixed(4), '20', y1.toFixed(4), '30', '0.0', '11', x2.toFixed(4), '21', y2.toFixed(4), '31', '0.0', '' ].join('\r\n'); } function createDxfCircle(cx, cy, r) { return [ '0', 'CIRCLE', '8', '0', '10', cx.toFixed(4), '20', cy.toFixed(4), '30', '0.0', '40', r.toFixed(4), '' ].join('\r\n'); } function createDxfText(x, y, text, height) { return [ '0', 'TEXT', '8', '0', '10', x.toFixed(4), '20', y.toFixed(4), '30', '0.0', '40', height.toFixed(4), '1', text, '' ].join('\r\n'); } function downloadDXF(id) { const item = findSymbol(id); if (!item) return; const dxf = svgToDxf(item.dxfSvg || item.svg, 1); const blob = new Blob([dxf], { type: 'application/dxf' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = item.filename.replace('.svg', '.dxf'); link.click(); URL.revokeObjectURL(url); showNotification('DXF heruntergeladen!'); } // ========== LEGENDE FUNKTIONEN ========== function toggleLegendSelection(id) { if (selectedSymbols.has(id)) { selectedSymbols.delete(id); } else { selectedSymbols.add(id); // Zur Legende hinzufügen const item = findSymbol(id); if (item && !legendItems.find(l => l.id === id)) { legendItems.push({ id: item.id, name: item.name, svg: item.svg, description: '' }); } } renderSymbols(); updateLegendCount(); saveLegendToStorage(); } function updateLegendCount() { const countEl = document.getElementById('legendCount'); if (countEl) { countEl.textContent = legendItems.length; } } function openLegendModal() { const modal = document.getElementById('legendModal'); modal.classList.add('active'); renderLegendEditor(); } function closeLegendModal() { const modal = document.getElementById('legendModal'); modal.classList.remove('active'); } function renderLegendEditor() { const container = document.getElementById('legendItems'); if (legendItems.length === 0) { container.innerHTML = '
Keine Symbole in der Legende. Klicken Sie auf 📑 bei einem Symbol, um es hinzuzufügen.
'; return; } container.innerHTML = legendItems.map((item, index) => `
${item.svg}
`).join(''); } function updateLegendItem(index, field, value) { if (legendItems[index]) { legendItems[index][field] = value; saveLegendToStorage(); } } function moveLegendItem(index, direction) { const newIndex = index + direction; if (newIndex >= 0 && newIndex < legendItems.length) { const temp = legendItems[index]; legendItems[index] = legendItems[newIndex]; legendItems[newIndex] = temp; renderLegendEditor(); saveLegendToStorage(); } } function removeLegendItem(index) { const item = legendItems[index]; if (item) { selectedSymbols.delete(item.id); } legendItems.splice(index, 1); renderLegendEditor(); renderSymbols(); updateLegendCount(); saveLegendToStorage(); } function clearLegend() { if (confirm('Möchten Sie die Legende wirklich leeren?')) { legendItems = []; selectedSymbols.clear(); renderLegendEditor(); renderSymbols(); updateLegendCount(); saveLegendToStorage(); } } // ========== LEGENDE SPEICHERN/LADEN ========== function saveLegendToStorage() { try { localStorage.setItem('gutachter_legende', JSON.stringify(legendItems)); localStorage.setItem('gutachter_selected', JSON.stringify([...selectedSymbols])); } catch (e) { console.warn('LocalStorage nicht verfügbar'); } } function loadLegendFromStorage() { try { const saved = localStorage.getItem('gutachter_legende'); const savedSelected = localStorage.getItem('gutachter_selected'); if (saved) { legendItems = JSON.parse(saved); } if (savedSelected) { selectedSymbols = new Set(JSON.parse(savedSelected)); } updateLegendCount(); } catch (e) { console.warn('Fehler beim Laden der Legende'); } } // ========== LEGENDE EXPORTIEREN ========== function exportLegendSVG() { if (legendItems.length === 0) { showNotification('Legende ist leer', 'error'); return; } const itemHeight = 50; const width = 400; const height = legendItems.length * itemHeight + 60; let svg = ` Legende `; legendItems.forEach((item, index) => { const y = 60 + index * itemHeight; svg += ` ${item.svg.replace(/]*>/, '').replace('', '')} ${item.name} ${item.description ? `${item.description}` : ''} `; }); svg += ''; const blob = new Blob([svg], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'legende.svg'; link.click(); URL.revokeObjectURL(url); showNotification('Legende als SVG exportiert!'); } function exportLegendPNG() { if (legendItems.length === 0) { showNotification('Legende ist leer', 'error'); return; } const itemHeight = 50; const width = 400; const height = legendItems.length * itemHeight + 60; const canvas = document.createElement('canvas'); canvas.width = width * 2; canvas.height = height * 2; const ctx = canvas.getContext('2d'); ctx.scale(2, 2); // Hintergrund // ctx.fillStyle = 'white'; // Entfernt für Transparenz ctx.fillRect(0, 0, width, height); // Titel ctx.fillStyle = '#000'; ctx.font = 'bold 18px Arial'; ctx.fillText('Legende', 20, 30); // Linie ctx.strokeStyle = '#ccc'; ctx.beginPath(); ctx.moveTo(20, 40); ctx.lineTo(width - 20, 40); ctx.stroke(); // Symbole laden und zeichnen let loadedCount = 0; legendItems.forEach((item, index) => { const y = 60 + index * itemHeight; const img = new Image(); const svgBlob = new Blob([item.svg], { type: 'image/svg+xml' }); img.src = URL.createObjectURL(svgBlob); img.onload = () => { ctx.drawImage(img, 20, y - 5, 32, 32); ctx.fillStyle = '#000'; ctx.font = 'bold 14px Arial'; ctx.fillText(item.name, 60, y + 15); if (item.description) { ctx.fillStyle = '#666'; ctx.font = '11px Arial'; ctx.fillText(item.description, 60, y + 30); } loadedCount++; if (loadedCount === legendItems.length) { canvas.toBlob(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'legende.png'; link.click(); URL.revokeObjectURL(url); showNotification('Legende als PNG exportiert!'); }, 'image/png'); } }; }); } // ========== ZIP EXPORT ========== async function downloadAllAsZip() { showNotification('ZIP wird erstellt...', 'info'); // Simple ZIP ohne externe Bibliothek const files = []; Object.keys(SYMBOLS).forEach(categoryKey => { const category = SYMBOLS[categoryKey]; category.items.forEach(item => { files.push({ name: `${categoryKey}/${item.filename}`, content: item.svg }); files.push({ name: `${categoryKey}/${item.filename.replace('.svg', '.dxf')}`, content: svgToDxf(item.svg, 1) }); }); }); // Da wir keine ZIP-Bibliothek haben, erstellen wir einen Download-Dialog const info = `Symbolbibliothek enthält ${files.length / 2} Symbole in ${Object.keys(SYMBOLS).length} Kategorien.\n\n` + `Für den ZIP-Download empfehlen wir, die Symbole einzeln herunterzuladen oder ` + `das Projekt lokal mit einer ZIP-Bibliothek zu erweitern.`; alert(info); showNotification('ZIP-Export: Siehe Hinweis', 'info'); } // ========== BENACHRICHTIGUNGEN ========== function showNotification(message, type = 'success') { // Bestehende Notification entfernen const existing = document.querySelector('.notification'); if (existing) existing.remove(); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('show'); }, 10); setTimeout(() => { notification.classList.remove('show'); setTimeout(() => notification.remove(), 300); }, 2500); } // ========== SVG PATH PARSER FÜR DXF ========== function parseSvgPath(d, scaleX, flipY) { let entities = ""; let currentX = 0, currentY = 0; let startX = 0, startY = 0; // Tokenize path data const commands = d.match(/[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*/g) || []; commands.forEach(cmd => { const type = cmd[0]; const args = cmd.slice(1).trim().split(/[\s,]+/).filter(s => s).map(Number); switch(type) { case "M": // Absolute moveto currentX = args[0]; currentY = args[1]; startX = currentX; startY = currentY; // Weitere Punkt-Paare sind implizite LineTo for(let i = 2; i < args.length; i += 2) { entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(args[i]), flipY(args[i+1])); currentX = args[i]; currentY = args[i+1]; } break; case "m": // Relative moveto currentX += args[0]; currentY += args[1]; startX = currentX; startY = currentY; for(let i = 2; i < args.length; i += 2) { const nx = currentX + args[i], ny = currentY + args[i+1]; entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(nx), flipY(ny)); currentX = nx; currentY = ny; } break; case "L": // Absolute lineto for(let i = 0; i < args.length; i += 2) { entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(args[i]), flipY(args[i+1])); currentX = args[i]; currentY = args[i+1]; } break; case "l": // Relative lineto for(let i = 0; i < args.length; i += 2) { const nx = currentX + args[i], ny = currentY + args[i+1]; entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(nx), flipY(ny)); currentX = nx; currentY = ny; } break; case "H": // Absolute horizontal for(let i = 0; i < args.length; i++) { entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(args[i]), flipY(currentY)); currentX = args[i]; } break; case "h": // Relative horizontal for(let i = 0; i < args.length; i++) { const nx = currentX + args[i]; entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(nx), flipY(currentY)); currentX = nx; } break; case "V": // Absolute vertical for(let i = 0; i < args.length; i++) { entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(currentX), flipY(args[i])); currentY = args[i]; } break; case "v": // Relative vertical for(let i = 0; i < args.length; i++) { const ny = currentY + args[i]; entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(currentX), flipY(ny)); currentY = ny; } break; case "Q": // Quadratic Bezier (approximiert als Linie zum Endpunkt) case "q": for(let i = 0; i < args.length; i += 4) { let ex, ey; if(type === "Q") { ex = args[i+2]; ey = args[i+3]; } else { ex = currentX + args[i+2]; ey = currentY + args[i+3]; } // Quadratische Bezier als Polyline approximieren const cx = type === "Q" ? args[i] : currentX + args[i]; const cy = type === "Q" ? args[i+1] : currentY + args[i+1]; for(let t = 0; t <= 1; t += 0.25) { const t2 = Math.min(t + 0.25, 1); const x1 = (1-t)*(1-t)*currentX + 2*(1-t)*t*cx + t*t*ex; const y1 = (1-t)*(1-t)*currentY + 2*(1-t)*t*cy + t*t*ey; const x2 = (1-t2)*(1-t2)*currentX + 2*(1-t2)*t2*cx + t2*t2*ex; const y2 = (1-t2)*(1-t2)*currentY + 2*(1-t2)*t2*cy + t2*t2*ey; entities += createDxfLine(scaleX(x1), flipY(y1), scaleX(x2), flipY(y2)); } currentX = ex; currentY = ey; } break; case "C": // Cubic Bezier case "c": for(let i = 0; i < args.length; i += 6) { let c1x, c1y, c2x, c2y, ex, ey; if(type === "C") { c1x = args[i]; c1y = args[i+1]; c2x = args[i+2]; c2y = args[i+3]; ex = args[i+4]; ey = args[i+5]; } else { c1x = currentX + args[i]; c1y = currentY + args[i+1]; c2x = currentX + args[i+2]; c2y = currentY + args[i+3]; ex = currentX + args[i+4]; ey = currentY + args[i+5]; } // Kubische Bezier als Polyline approximieren for(let t = 0; t <= 1; t += 0.2) { const t2 = Math.min(t + 0.2, 1); const x1 = Math.pow(1-t,3)*currentX + 3*Math.pow(1-t,2)*t*c1x + 3*(1-t)*t*t*c2x + t*t*t*ex; const y1 = Math.pow(1-t,3)*currentY + 3*Math.pow(1-t,2)*t*c1y + 3*(1-t)*t*t*c2y + t*t*t*ey; const x2 = Math.pow(1-t2,3)*currentX + 3*Math.pow(1-t2,2)*t2*c1x + 3*(1-t2)*t2*t2*c2x + t2*t2*t2*ex; const y2 = Math.pow(1-t2,3)*currentY + 3*Math.pow(1-t2,2)*t2*c1y + 3*(1-t2)*t2*t2*c2y + t2*t2*t2*ey; entities += createDxfLine(scaleX(x1), flipY(y1), scaleX(x2), flipY(y2)); } currentX = ex; currentY = ey; } break; case "Z": case "z": // Close path if(currentX !== startX || currentY !== startY) { entities += createDxfLine(scaleX(currentX), flipY(currentY), scaleX(startX), flipY(startY)); } currentX = startX; currentY = startY; break; } }); return entities; } // ========== TEXT-GENERATOR v4 ========== // Features: Farben, Skalierung, abknickbare Pfeile (korrigiert), Pfeilspitzengroesse, PNG/JPG, Speichern var textGenState = { text: '', fontSize: 16, textColor: '#000000', frameColor: '#000000', shape: 'none', frameScale: 100, framePadding: 10, lineStyle: 'solid', lineWeight: 2, arrow: 'none', arrowLength: 40, arrowAngle: 0, arrowBend: 50, arrowSize: 10 }; // Custom Symbols aus localStorage var customSymbols = []; function initTextGenerator() { // Load custom symbols loadCustomSymbols(); var textInput = document.getElementById('customText'); var fontSizeInput = document.getElementById('fontSize'); var fontSizeValue = document.getElementById('fontSizeValue'); var textColorInput = document.getElementById('textColor'); var textColorValue = document.getElementById('textColorValue'); var frameColorInput = document.getElementById('frameColor'); var frameColorValue = document.getElementById('frameColorValue'); var frameScaleInput = document.getElementById('frameScale'); var frameScaleValue = document.getElementById('frameScaleValue'); var framePaddingInput = document.getElementById('framePadding'); var framePaddingValue = document.getElementById('framePaddingValue'); var arrowLengthInput = document.getElementById('arrowLength'); var arrowLengthValue = document.getElementById('arrowLengthValue'); var arrowAngleInput = document.getElementById('arrowAngle'); var arrowAngleValue = document.getElementById('arrowAngleValue'); var arrowBendInput = document.getElementById('arrowBend'); var arrowBendValue = document.getElementById('arrowBendValue'); var arrowSizeInput = document.getElementById('arrowSize'); var arrowSizeValue = document.getElementById('arrowSizeValue'); // Text input if (textInput) { textInput.addEventListener('input', function(e) { textGenState.text = e.target.value; updateTextPreview(); }); } // Font size if (fontSizeInput) { fontSizeInput.addEventListener('input', function(e) { textGenState.fontSize = parseInt(e.target.value); fontSizeValue.textContent = e.target.value + 'px'; updateTextPreview(); }); } // Text color if (textColorInput) { textColorInput.addEventListener('input', function(e) { textGenState.textColor = e.target.value; textColorValue.textContent = e.target.value; updateTextPreview(); }); } // Frame color if (frameColorInput) { frameColorInput.addEventListener('input', function(e) { textGenState.frameColor = e.target.value; frameColorValue.textContent = e.target.value; updateTextPreview(); }); } // Frame scale if (frameScaleInput) { frameScaleInput.addEventListener('input', function(e) { textGenState.frameScale = parseInt(e.target.value); frameScaleValue.textContent = e.target.value + '%'; updateTextPreview(); }); } // Frame padding if (framePaddingInput) { framePaddingInput.addEventListener('input', function(e) { textGenState.framePadding = parseInt(e.target.value); framePaddingValue.textContent = e.target.value + 'px'; updateTextPreview(); }); } // Arrow length if (arrowLengthInput) { arrowLengthInput.addEventListener('input', function(e) { textGenState.arrowLength = parseInt(e.target.value); arrowLengthValue.textContent = e.target.value + 'px'; updateTextPreview(); }); } // Arrow angle if (arrowAngleInput) { arrowAngleInput.addEventListener('input', function(e) { textGenState.arrowAngle = parseInt(e.target.value); arrowAngleValue.textContent = e.target.value + '\u00B0'; updateTextPreview(); }); } // Arrow bend position if (arrowBendInput) { arrowBendInput.addEventListener('input', function(e) { textGenState.arrowBend = parseInt(e.target.value); arrowBendValue.textContent = e.target.value + '%'; updateTextPreview(); }); } // Arrow size if (arrowSizeInput) { arrowSizeInput.addEventListener('input', function(e) { textGenState.arrowSize = parseInt(e.target.value); arrowSizeValue.textContent = e.target.value + 'px'; updateTextPreview(); }); } // Shape buttons document.querySelectorAll('.shape-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.shape-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); textGenState.shape = btn.dataset.shape; updateFrameScaleVisibility(); updateTextPreview(); }); }); // Line style buttons document.querySelectorAll('.line-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.line-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); textGenState.lineStyle = btn.dataset.style; updateTextPreview(); }); }); // Line weight buttons document.querySelectorAll('.weight-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.weight-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); textGenState.lineWeight = parseInt(btn.dataset.weight); updateTextPreview(); }); }); // Arrow buttons document.querySelectorAll('.arrow-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.arrow-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); textGenState.arrow = btn.dataset.arrow; updateArrowDetailsVisibility(); updateTextPreview(); }); }); updateFrameScaleVisibility(); updateArrowDetailsVisibility(); updateTextPreview(); } function updateFrameScaleVisibility() { var row = document.getElementById('frameScaleRow'); if (row) { row.style.display = textGenState.shape === 'none' ? 'none' : 'flex'; } } function updateArrowDetailsVisibility() { var row = document.getElementById('arrowDetailsRow'); if (row) { row.style.display = textGenState.arrow === 'none' ? 'none' : 'flex'; } } function toggleTextGenerator() { var generator = document.querySelector('.text-generator'); generator.classList.toggle('collapsed'); } function getStrokeDasharray(style) { switch(style) { case 'dashed': return '8,4'; case 'dotted': return '2,3'; default: return 'none'; } } function generateTextSVG() { var text = textGenState.text || 'Text'; var fontSize = textGenState.fontSize; var textColor = textGenState.textColor; var frameColor = textGenState.frameColor; var shape = textGenState.shape; var frameScale = textGenState.frameScale / 100; var padding = textGenState.framePadding; var lineStyle = textGenState.lineStyle; var lineWeight = textGenState.lineWeight; var arrow = textGenState.arrow; var arrowLength = textGenState.arrowLength; var arrowAngle = textGenState.arrowAngle; var arrowBend = textGenState.arrowBend / 100; var arrowSize = textGenState.arrowSize; // Zeilen aufteilen var lines = text.split('\n'); if (lines.length === 0) lines = ['Text']; // Berechne die laengste Zeile var maxLineLength = 0; for (var i = 0; i < lines.length; i++) { if (lines[i].length > maxLineLength) { maxLineLength = lines[i].length; } } if (maxLineLength === 0) maxLineLength = 4; // Dimensionen berechnen var charWidth = fontSize * 0.6; var lineHeight = fontSize * 1.3; var textWidth = maxLineLength * charWidth; var textHeight = lines.length * lineHeight; // Basis-SVG-Groesse (Text + Padding) var baseWidth = textWidth + (padding * 2); var baseHeight = textHeight + (padding * 2); // Rahmen mit Skalierung var frameWidth = baseWidth * frameScale; var frameHeight = baseHeight * frameScale; // Mindestgroesse if (frameWidth < 40) frameWidth = 40; if (frameHeight < 30) frameHeight = 30; // Pfeil-Raum berechnen (inkl. Pfeilspitze) var arrowSpace = (arrow !== 'none') ? arrowLength + arrowSize + 5 : 0; // SVG-Groesse mit Pfeil-Raum var svgWidth = frameWidth; var svgHeight = frameHeight; var offsetX = 0; var offsetY = 0; if (arrow === 'left') { svgWidth += arrowSpace; offsetX = arrowSpace; } else if (arrow === 'right') { svgWidth += arrowSpace; } else if (arrow === 'top') { svgHeight += arrowSpace; offsetY = arrowSpace; } else if (arrow === 'bottom') { svgHeight += arrowSpace; } var cx = offsetX + frameWidth / 2; var cy = offsetY + frameHeight / 2; var dashArray = getStrokeDasharray(lineStyle); var dashAttr = dashArray !== 'none' ? ' stroke-dasharray="' + dashArray + '"' : ''; var shapeSvg = ''; var shapeX = offsetX + lineWeight / 2; var shapeY = offsetY + lineWeight / 2; var shapeW = frameWidth - lineWeight; var shapeH = frameHeight - lineWeight; switch(shape) { case 'rect': shapeSvg = ''; break; case 'square': var squareSize = Math.max(shapeW, shapeH); var sqX = cx - squareSize / 2; var sqY = cy - squareSize / 2; shapeSvg = ''; break; case 'circle': var radius = Math.max(shapeW, shapeH) / 2; shapeSvg = ''; break; case 'oval': var rx = shapeW / 2; var ry = shapeH / 2; shapeSvg = ''; break; case 'diamond': var halfW = shapeW / 2; var halfH = shapeH / 2; shapeSvg = ''; break; } // Pfeil generieren var arrowSvg = ''; if (arrow !== 'none') { arrowSvg = generateArrowPath(arrow, cx, cy, frameWidth, frameHeight, offsetX, offsetY, arrowLength, arrowAngle, arrowBend, frameColor, lineWeight, dashAttr, arrowSize); } // Text-Elemente erzeugen var textSvg = ''; var totalTextHeight = lines.length * lineHeight; var startY = cy - (totalTextHeight / 2) + (fontSize * 0.8); for (var j = 0; j < lines.length; j++) { var lineY = startY + (j * lineHeight); var lineText = lines[j] || ' '; lineText = lineText.replace(/&/g, '&').replace(//g, '>'); textSvg += '' + lineText + ''; } textSvg += ''; return '' + shapeSvg + arrowSvg + textSvg + ''; } function generateArrowPath(direction, cx, cy, frameWidth, frameHeight, offsetX, offsetY, length, angle, bendPos, color, weight, dashAttr, arrowSize) { var startX, startY, midX, midY, endX, endY; var angleRad = angle * Math.PI / 180; // Startpunkt am Rahmen switch(direction) { case 'top': startX = cx; startY = offsetY; // Erster Teil geht gerade nach oben bis zum Knickpunkt midX = startX; midY = startY - length * bendPos; // Zweiter Teil knickt ab endX = midX + Math.sin(angleRad) * (length * (1 - bendPos)); endY = midY - Math.cos(angleRad) * (length * (1 - bendPos)); break; case 'bottom': startX = cx; startY = offsetY + frameHeight; midX = startX; midY = startY + length * bendPos; endX = midX + Math.sin(angleRad) * (length * (1 - bendPos)); endY = midY + Math.cos(angleRad) * (length * (1 - bendPos)); break; case 'left': startX = offsetX; startY = cy; midX = startX - length * bendPos; midY = startY; endX = midX - Math.cos(angleRad) * (length * (1 - bendPos)); endY = midY + Math.sin(angleRad) * (length * (1 - bendPos)); break; case 'right': startX = offsetX + frameWidth; startY = cy; midX = startX + length * bendPos; midY = startY; endX = midX + Math.cos(angleRad) * (length * (1 - bendPos)); endY = midY + Math.sin(angleRad) * (length * (1 - bendPos)); break; } // Pfad erstellen var pathD; if (angle !== 0 && bendPos > 0 && bendPos < 1) { // Mit Knick pathD = 'M ' + startX + ' ' + startY + ' L ' + midX + ' ' + midY + ' L ' + endX + ' ' + endY; } else { // Ohne Knick - gerade Linie pathD = 'M ' + startX + ' ' + startY + ' L ' + endX + ' ' + endY; } // Pfeilspitze: Richtung basiert auf dem LETZTEN Segment var lastSegmentStartX, lastSegmentStartY; if (angle !== 0 && bendPos > 0 && bendPos < 1) { // Letztes Segment ist von mid zu end lastSegmentStartX = midX; lastSegmentStartY = midY; } else { // Letztes Segment ist von start zu end lastSegmentStartX = startX; lastSegmentStartY = startY; } // Winkel des letzten Segments berechnen var arrowHeadAngle = Math.atan2(endY - lastSegmentStartY, endX - lastSegmentStartX); // Pfeilspitze Punkte var ah1x = endX - arrowSize * Math.cos(arrowHeadAngle - Math.PI / 6); var ah1y = endY - arrowSize * Math.sin(arrowHeadAngle - Math.PI / 6); var ah2x = endX - arrowSize * Math.cos(arrowHeadAngle + Math.PI / 6); var ah2y = endY - arrowSize * Math.sin(arrowHeadAngle + Math.PI / 6); var arrowHead = ''; return '' + arrowHead; } function updateTextPreview() { var preview = document.getElementById('textPreview'); if (preview) { preview.innerHTML = generateTextSVG(); } } // Hilfsfunktion: SVG zu Canvas rendern async function svgToCanvas(svg, scale) { var parser = new DOMParser(); var svgDoc = parser.parseFromString(svg, 'image/svg+xml'); var svgEl = svgDoc.documentElement; var svgWidth = parseFloat(svgEl.getAttribute('width')) || 100; var svgHeight = parseFloat(svgEl.getAttribute('height')) || 100; if (!scale) { scale = Math.max(200 / Math.min(svgWidth, svgHeight), 1); scale = Math.min(scale, 512 / Math.max(svgWidth, svgHeight)); } var canvasWidth = Math.round(svgWidth * scale); var canvasHeight = Math.round(svgHeight * scale); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = canvasWidth; canvas.height = canvasHeight; var img = new Image(); var svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); var url = URL.createObjectURL(svgBlob); await new Promise(function(resolve, reject) { img.onload = resolve; img.onerror = reject; img.src = url; }); ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); URL.revokeObjectURL(url); return canvas; } async function copyTextAsImage() { var svg = generateTextSVG(); try { var canvas = await svgToCanvas(svg); canvas.toBlob(async function(blob) { try { await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]); showNotification('In Zwischenablage kopiert!'); } catch (err) { var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'text-symbol.png'; link.click(); showNotification('PNG heruntergeladen'); } }, 'image/png'); } catch (err) { console.error('Fehler:', err); showNotification('Fehler beim Kopieren', 'error'); } } function downloadTextSVG() { var svg = generateTextSVG(); var blob = new Blob([svg], { type: 'image/svg+xml' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; var filename = textGenState.text ? textGenState.text.replace(/\n/g, '_').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 20) : 'symbol'; link.download = 'text-' + filename + '.svg'; link.click(); URL.revokeObjectURL(url); showNotification('SVG heruntergeladen!'); } async function downloadTextPNG() { var svg = generateTextSVG(); try { var canvas = await svgToCanvas(svg, 2); // 2x Skalierung fuer bessere Qualitaet canvas.toBlob(function(blob) { var link = document.createElement('a'); link.href = URL.createObjectURL(blob); var filename = textGenState.text ? textGenState.text.replace(/\n/g, '_').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 20) : 'symbol'; link.download = 'text-' + filename + '.png'; link.click(); URL.revokeObjectURL(link.href); showNotification('PNG heruntergeladen!'); }, 'image/png'); } catch (err) { console.error('Fehler:', err); showNotification('Fehler beim PNG-Export', 'error'); } } async function downloadTextJPG() { var svg = generateTextSVG(); try { var canvas = await svgToCanvas(svg, 2); var ctx = canvas.getContext('2d'); // Weisser Hintergrund fuer JPG var tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; var tempCtx = tempCanvas.getContext('2d'); tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); tempCtx.drawImage(canvas, 0, 0); tempCanvas.toBlob(function(blob) { var link = document.createElement('a'); link.href = URL.createObjectURL(blob); var filename = textGenState.text ? textGenState.text.replace(/\n/g, '_').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 20) : 'symbol'; link.download = 'text-' + filename + '.jpg'; link.click(); URL.revokeObjectURL(link.href); showNotification('JPG heruntergeladen!'); }, 'image/jpeg', 0.95); } catch (err) { console.error('Fehler:', err); showNotification('Fehler beim JPG-Export', 'error'); } } function downloadTextDXF() { var svg = generateTextSVG(); var dxf = svgToDxf(svg, 1); var blob = new Blob([dxf], { type: 'application/dxf' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; var filename = textGenState.text ? textGenState.text.replace(/\n/g, '_').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 20) : 'symbol'; link.download = 'text-' + filename + '.dxf'; link.click(); URL.revokeObjectURL(url); showNotification('DXF heruntergeladen!'); } function addTextToLegend() { var svg = generateTextSVG(); var text = textGenState.text || 'Text'; var displayName = text.replace(/\n/g, ' ').substring(0, 30); legendItems.push({ id: 'custom_text_' + Date.now(), name: displayName, svg: svg, description: '' }); updateLegendCount(); saveLegendToStorage(); showNotification('Zur Legende hinzugefuegt!'); } // ========== CUSTOM SYMBOLS STORAGE ========== function loadCustomSymbols() { try { var stored = localStorage.getItem('customSymbols'); if (stored) { customSymbols = JSON.parse(stored); } } catch (e) { console.error('Fehler beim Laden der eigenen Symbole:', e); customSymbols = []; } } function saveCustomSymbols() { try { localStorage.setItem('customSymbols', JSON.stringify(customSymbols)); } catch (e) { console.error('Fehler beim Speichern der eigenen Symbole:', e); } } function openSaveModal() { var modal = document.getElementById('saveModal'); var nameInput = document.getElementById('symbolName'); var descInput = document.getElementById('symbolDescription'); var suggestedName = textGenState.text ? textGenState.text.replace(/\n/g, ' ').substring(0, 30) : ''; nameInput.value = suggestedName; descInput.value = ''; modal.style.display = 'flex'; nameInput.focus(); } function closeSaveModal() { var modal = document.getElementById('saveModal'); modal.style.display = 'none'; } function saveCustomSymbol() { var nameInput = document.getElementById('symbolName'); var descInput = document.getElementById('symbolDescription'); var name = nameInput.value.trim(); if (!name) { showNotification('Bitte einen Namen eingeben!', 'error'); return; } var svg = generateTextSVG(); var newSymbol = { id: 'custom_' + Date.now(), name: name, description: descInput.value.trim(), svg: svg, category: 'custom', tags: ['eigene', 'custom', name.toLowerCase()], createdAt: new Date().toISOString(), state: JSON.parse(JSON.stringify(textGenState)) }; customSymbols.push(newSymbol); saveCustomSymbols(); closeSaveModal(); showNotification('Symbol "' + name + '" gespeichert!'); var activeFilter = document.querySelector('.filter-pill.active'); if (activeFilter && activeFilter.dataset.filter === 'custom') { renderSymbolGrid(); } } function deleteCustomSymbol(id) { if (!confirm('Symbol wirklich loeschen?')) return; customSymbols = customSymbols.filter(function(s) { return s.id !== id; }); saveCustomSymbols(); renderSymbolGrid(); showNotification('Symbol geloescht'); } var originalRenderSymbolGrid = typeof renderSymbolGrid === 'function' ? renderSymbolGrid : null; function renderSymbolGridWithCustom() { var activeFilter = document.querySelector('.filter-pill.active'); var filter = activeFilter ? activeFilter.dataset.filter : 'all'; if (filter === 'custom') { renderCustomSymbolsOnly(); } else if (originalRenderSymbolGrid) { originalRenderSymbolGrid(); if (filter === 'all') { appendCustomSymbols(); } } } function renderCustomSymbolsOnly() { var grid = document.getElementById('symbolGrid'); if (!grid) return; grid.innerHTML = ''; if (customSymbols.length === 0) { grid.innerHTML = '
Noch keine eigenen Symbole gespeichert.
Erstelle ein Text-Symbol und klicke auf "Speichern".
'; return; } customSymbols.forEach(function(symbol) { var card = createCustomSymbolCard(symbol); grid.appendChild(card); }); } function appendCustomSymbols() { var grid = document.getElementById('symbolGrid'); if (!grid || customSymbols.length === 0) return; customSymbols.forEach(function(symbol) { var card = createCustomSymbolCard(symbol); grid.appendChild(card); }); } function createCustomSymbolCard(symbol) { var card = document.createElement('div'); card.className = 'symbol-card'; card.innerHTML = '' + '
' + symbol.svg + '
' + '
' + escapeHtml(symbol.name) + '
' + '
' + '' + '' + '' + '' + '
'; return card; } function escapeHtml(text) { var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getSymbolById(id, isCustom) { if (isCustom) { return customSymbols.find(function(s) { return s.id === id; }); } return allSymbols.find(function(s) { return s.id === id; }); } // ========== IMPRESSUM ========== function openImpressum() { var modal = document.getElementById('impressumModal'); if (modal) modal.style.display = 'flex'; } function closeImpressum() { var modal = document.getElementById('impressumModal'); if (modal) modal.style.display = 'none'; } // Init beim Laden if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTextGenerator); } else { initTextGenerator(); } // ========== PNG/JPG DOWNLOAD FUER ALLE SYMBOLE ========== async function downloadSymbolPNG(id) { var symbol = findSymbol(id); if (!symbol) return; var svg = symbol.svg; try { var canvas = await svgToCanvas(svg, 2); canvas.toBlob(function(blob) { var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = symbol.id + '.png'; link.click(); URL.revokeObjectURL(link.href); showNotification('PNG heruntergeladen!'); }, 'image/png'); } catch (err) { console.error('Fehler:', err); showNotification('Fehler beim PNG-Export', 'error'); } } async function downloadSymbolJPG(id) { var symbol = findSymbol(id); if (!symbol) return; var svg = symbol.svg; try { var canvas = await svgToCanvas(svg, 2); // Weisser Hintergrund fuer JPG var tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; var tempCtx = tempCanvas.getContext('2d'); tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); tempCtx.drawImage(canvas, 0, 0); tempCanvas.toBlob(function(blob) { var link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = symbol.id + '.jpg'; link.click(); URL.revokeObjectURL(link.href); showNotification('JPG heruntergeladen!'); }, 'image/jpeg', 0.95); } catch (err) { console.error('Fehler:', err); showNotification('Fehler beim JPG-Export', 'error'); } } // SVG zu Canvas Hilfsfunktion (falls nicht vorhanden) if (typeof svgToCanvas !== 'function') { window.svgToCanvas = async function(svg, scale) { var parser = new DOMParser(); var svgDoc = parser.parseFromString(svg, 'image/svg+xml'); var svgEl = svgDoc.documentElement; var svgWidth = parseFloat(svgEl.getAttribute('width')) || parseFloat(svgEl.getAttribute('viewBox').split(' ')[2]) || 64; var svgHeight = parseFloat(svgEl.getAttribute('height')) || parseFloat(svgEl.getAttribute('viewBox').split(' ')[3]) || 64; if (!scale) scale = 2; var canvasWidth = Math.round(svgWidth * scale); var canvasHeight = Math.round(svgHeight * scale); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = canvasWidth; canvas.height = canvasHeight; var img = new Image(); var svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); var url = URL.createObjectURL(svgBlob); await new Promise(function(resolve, reject) { img.onload = resolve; img.onerror = reject; img.src = url; }); ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); URL.revokeObjectURL(url); return canvas; }; }