// ============================================ // 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(); updateLegendPreview(); } 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.
'; updateLegendPreview(); return; } container.innerHTML = legendItems.map((item, index) => `
${item.svg}
`).join(''); updateLegendPreview(); } function updateLegendItem(index, field, value) { if (legendItems[index]) { legendItems[index][field] = value; saveLegendToStorage(); updateLegendPreview(); } } 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 VORSCHAU ========== function generateLegendSVG() { if (legendItems.length === 0) return null; const itemHeight = 50; const width = 320; const height = legendItems.length * itemHeight + 60; let svg = ` Legende `; legendItems.forEach((item, index) => { const y = 60 + index * itemHeight; svg += ` ${item.svg.replace(/]*>/, '').replace('', '')} ${escapeHtml(item.name)} ${item.description ? `${escapeHtml(item.description)}` : ''} `; }); svg += ''; return svg; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function updateLegendPreview() { const previewBox = document.getElementById('legendPreviewBox'); if (!previewBox) return; if (legendItems.length === 0) { previewBox.innerHTML = '
Keine Eintraege vorhanden
'; return; } const svg = generateLegendSVG(); if (svg) { previewBox.innerHTML = svg; } } async function copyLegendAsImage() { if (legendItems.length === 0) { showNotification('Legende ist leer', 'error'); return; } const svg = generateLegendSVG(); if (!svg) return; try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const itemHeight = 50; const width = 320; const height = legendItems.length * itemHeight + 60; canvas.width = width * 2; canvas.height = height * 2; ctx.scale(2, 2); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); const img = new Image(); const svgBlob = new Blob([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, width, height); URL.revokeObjectURL(url); canvas.toBlob(async (blob) => { try { await navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]); showNotification('Legende in Zwischenablage kopiert!'); } catch (err) { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'legende.png'; link.click(); showNotification('Legende als PNG heruntergeladen'); } }, 'image/png'); } catch (err) { console.error('Fehler beim Kopieren:', err); showNotification('Fehler beim Kopieren', 'error'); } } // ========== 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('', '')} ${escapeHtml(item.name)} ${item.description ? `${escapeHtml(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 UI ========== function toggleTextGenerator() { var generator = document.querySelector('.text-generator'); generator.classList.toggle('collapsed'); } // Custom Symbols aus localStorage var customSymbols = []; // ========== 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; }; }