Files
SPA-landing/symbols/js/app.js
architeur c0ae55a597 Refactor symbols app: cleanup and fix issues
- Removed duplicate files (index2/3/4.html, symbols.js duplicates)
- Kept index4.html as the main index.html (modular version)
- Removed old text-generator.js (replaced by modular version)
- Fixed ID mismatch in ui-bindings.js to match HTML
- Added square and circle shape support in svg-generator.js
- Added legend preview with copy functionality
- Removed 580 lines of obsolete text-generator v4 code from app.js
- Added addTextToLegend and addStandaloneArrowToLegend to export.js

Still TODO: Split large files to comply with 300 line limit
- app.js: 1219 lines
- styles.css: 1319 lines
- symbols.js: 870 lines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 21:09:39 +01:00

1220 lines
42 KiB
JavaScript

// ============================================
// 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 = `<span>${category.icon} ${category.name}</span><span class="category-count">${filteredItems.length} Symbole</span>`;
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 = '<div class="no-results">Keine Symbole gefunden. Versuchen Sie einen anderen Suchbegriff.</div>';
}
}
// ========== 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 = `
<div class="symbol-preview">${item.svg}</div>
<div class="symbol-name">${item.name}</div>
<div class="symbol-actions">
<button class="btn-action btn-copy" onclick="copyAsImage('${item.id}')" title="Als Bild kopieren (transparent)">
📋 Kopieren
</button>
<button class="btn-action btn-svg" onclick="downloadSVG('${item.id}')" title="SVG herunterladen">
⬇️ SVG
</button>
<button class="btn-action btn-png" onclick="downloadSymbolPNG('${item.id}')" title="PNG herunterladen">PNG</button>
<button class="btn-action btn-jpg" onclick="downloadSymbolJPG('${item.id}')" title="JPG herunterladen">JPG</button>
<button class="btn-action btn-dxf" onclick="downloadDXF('${item.id}')" title="DXF herunterladen">
📐 DXF
</button>
<button class="btn-action btn-legend ${isSelected ? 'active' : ''}" onclick="toggleLegendSelection('${item.id}')" title="Zur Legende hinzufügen">
📑
</button>
</div>
`;
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 = '<div class="legend-empty">Keine Symbole in der Legende. Klicken Sie auf 📑 bei einem Symbol, um es hinzuzufügen.</div>';
updateLegendPreview();
return;
}
container.innerHTML = legendItems.map((item, index) => `
<div class="legend-item" data-index="${index}">
<div class="legend-item-preview">${item.svg}</div>
<div class="legend-item-content">
<input type="text" class="legend-item-name" value="${item.name}"
oninput="updateLegendItem(${index}, 'name', this.value)" placeholder="Name">
<input type="text" class="legend-item-desc" value="${item.description || ''}"
oninput="updateLegendItem(${index}, 'description', this.value)" placeholder="Beschreibung (optional)">
</div>
<div class="legend-item-actions">
<button type="button" onclick="moveLegendItem(${index}, -1)" title="Nach oben" ${index === 0 ? 'disabled' : ''}>▲</button>
<button type="button" onclick="moveLegendItem(${index}, 1)" title="Nach unten" ${index === legendItems.length - 1 ? 'disabled' : ''}>▼</button>
<button type="button" onclick="removeLegendItem(${index})" title="Entfernen" class="btn-remove">✕</button>
</div>
</div>
`).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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<rect width="${width}" height="${height}" fill="white"/>
<text x="20" y="30" font-family="Arial" font-size="18" font-weight="bold">Legende</text>
<line x1="20" y1="40" x2="${width - 20}" y2="40" stroke="#ccc" stroke-width="1"/>`;
legendItems.forEach((item, index) => {
const y = 60 + index * itemHeight;
svg += `<g transform="translate(20, ${y})">
<g transform="scale(0.5)">${item.svg.replace(/<svg[^>]*>/, '').replace('</svg>', '')}</g>
<text x="50" y="20" font-family="Arial" font-size="14" font-weight="bold">${escapeHtml(item.name)}</text>
${item.description ? `<text x="50" y="35" font-family="Arial" font-size="11" fill="#666">${escapeHtml(item.description)}</text>` : ''}
</g>`;
});
svg += '</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 = '<div class="legend-preview-empty">Keine Eintraege vorhanden</div>';
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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<rect width="${width}" height="${height}" fill="white"/>
<text x="20" y="30" font-family="Arial" font-size="18" font-weight="bold">Legende</text>
<line x1="20" y1="40" x2="${width - 20}" y2="40" stroke="#ccc" stroke-width="1"/>`;
legendItems.forEach((item, index) => {
const y = 60 + index * itemHeight;
svg += `<g transform="translate(20, ${y})">
<g transform="scale(0.5)">${item.svg.replace(/<svg[^>]*>/, '').replace('</svg>', '')}</g>
<text x="50" y="20" font-family="Arial" font-size="14" font-weight="bold">${escapeHtml(item.name)}</text>
${item.description ? `<text x="50" y="35" font-family="Arial" font-size="11" fill="#666">${escapeHtml(item.description)}</text>` : ''}
</g>`;
});
svg += '</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 = '<div class="no-results">Noch keine eigenen Symbole gespeichert.<br>Erstelle ein Text-Symbol und klicke auf "Speichern".</div>';
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 =
'<button class="btn-delete" onclick="event.stopPropagation(); deleteCustomSymbol(\'' + symbol.id + '\')" title="Loeschen">X</button>' +
'<div class="symbol-preview">' + symbol.svg + '</div>' +
'<div class="symbol-name">' + escapeHtml(symbol.name) + '</div>' +
'<div class="symbol-actions">' +
'<button class="btn-action btn-copy" onclick="copySymbolAsImage(\'' + symbol.id + '\', true)" title="Kopieren">Kopieren</button>' +
'<button class="btn-action btn-svg" onclick="downloadSymbolSVG(\'' + symbol.id + '\', true)" title="SVG">SVG</button>' +
'<button class="btn-action btn-dxf" onclick="downloadSymbolDXF(\'' + symbol.id + '\', true)" title="DXF">DXF</button>' +
'<button class="btn-action btn-legend" onclick="addSymbolToLegend(\'' + symbol.id + '\', true)" title="Legende">+</button>' +
'</div>';
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;
};
}