WIP svg editor

This commit is contained in:
Troy Grunt
2026-02-11 22:09:47 +01:00
parent 88ff04aa01
commit c31e1a308d
2 changed files with 660 additions and 268 deletions

View File

@@ -0,0 +1,491 @@
(() => {
const SVG_NS = 'http://www.w3.org/2000/svg';
function initEditor() {
const editor = document.getElementById('device-type-shape-editor');
const svg = document.getElementById('shape-canvas');
const hiddenInput = document.getElementById('shape-definition');
if (!editor || !svg || !hiddenInput) {
return;
}
const overlay = {
empty: document.getElementById('shape-overlay-empty'),
form: document.getElementById('shape-overlay-form'),
type: document.getElementById('shape-param-type'),
x: document.getElementById('shape-param-x'),
y: document.getElementById('shape-param-y'),
width: document.getElementById('shape-param-width'),
height: document.getElementById('shape-param-height'),
radius: document.getElementById('shape-param-radius'),
fontSize: document.getElementById('shape-param-font-size'),
text: document.getElementById('shape-param-text'),
fill: document.getElementById('shape-param-fill'),
stroke: document.getElementById('shape-param-stroke'),
strokeWidth: document.getElementById('shape-param-stroke-width'),
isPort: document.getElementById('shape-param-is-port'),
portName: document.getElementById('shape-param-port-name'),
portNameLabel: document.getElementById('shape-port-name-label'),
deleteButton: document.getElementById('shape-delete')
};
const fieldVisibility = {
width: editor.querySelector('[data-field="width"]'),
height: editor.querySelector('[data-field="height"]'),
radius: editor.querySelector('[data-field="radius"]'),
text: editor.querySelector('[data-field="text"]'),
font_size: editor.querySelector('[data-field="font_size"]')
};
let draggedTemplate = null;
let dragState = {
active: false,
shapeId: null,
offsetX: 0,
offsetY: 0
};
let selectedShapeId = null;
let shapes = normalizeShapeList(readJson(hiddenInput.value));
bindToolbarDragEvents(editor);
bindCanvasDropEvents(svg);
bindCanvasPointerEvents(svg);
bindOverlayEvents(overlay);
render();
function bindToolbarDragEvents(root) {
const tools = root.querySelectorAll('.shape-tool[data-shape-template]');
tools.forEach((tool) => {
tool.addEventListener('dragstart', (event) => {
draggedTemplate = String(tool.dataset.shapeTemplate || '').trim();
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('text/plain', draggedTemplate);
}
});
tool.addEventListener('dragend', () => {
draggedTemplate = null;
});
});
}
function bindCanvasDropEvents(canvas) {
const canvasWrap = canvas.closest('.shape-editor-canvas');
if (!canvasWrap) {
return;
}
canvasWrap.addEventListener('dragover', (event) => {
if (!draggedTemplate) {
return;
}
event.preventDefault();
canvasWrap.classList.add('drag-over');
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
});
canvasWrap.addEventListener('dragleave', () => {
canvasWrap.classList.remove('drag-over');
});
canvasWrap.addEventListener('drop', (event) => {
if (!draggedTemplate) {
return;
}
event.preventDefault();
canvasWrap.classList.remove('drag-over');
const point = toSvgPoint(event, canvas);
const shape = createDefaultShape(draggedTemplate, point.x, point.y);
shapes.push(shape);
selectedShapeId = shape.id;
persist();
render();
});
}
function bindCanvasPointerEvents(canvas) {
canvas.addEventListener('pointerdown', (event) => {
const target = event.target;
if (!(target instanceof SVGElement)) {
return;
}
const shapeElement = target.closest('[data-shape-id]');
if (!shapeElement) {
selectedShapeId = null;
renderOverlay();
return;
}
const shapeId = shapeElement.getAttribute('data-shape-id');
const shape = findShape(shapeId);
if (!shape) {
return;
}
selectedShapeId = shape.id;
renderOverlay();
const point = toSvgPoint(event, canvas);
const anchor = getShapeAnchor(shape);
dragState = {
active: true,
shapeId: shape.id,
offsetX: anchor.x - point.x,
offsetY: anchor.y - point.y
};
shapeElement.setPointerCapture(event.pointerId);
});
canvas.addEventListener('pointermove', (event) => {
if (!dragState.active || !dragState.shapeId) {
return;
}
const shape = findShape(dragState.shapeId);
if (!shape) {
return;
}
const point = toSvgPoint(event, canvas);
const x = Math.round(point.x + dragState.offsetX);
const y = Math.round(point.y + dragState.offsetY);
setShapeAnchor(shape, x, y);
persist();
render();
});
canvas.addEventListener('pointerup', () => {
dragState.active = false;
dragState.shapeId = null;
});
canvas.addEventListener('pointercancel', () => {
dragState.active = false;
dragState.shapeId = null;
});
}
function bindOverlayEvents(o) {
const inputs = [
o.x, o.y, o.width, o.height, o.radius, o.fontSize, o.text, o.fill, o.stroke, o.strokeWidth, o.isPort, o.portName
];
inputs.forEach((input) => {
if (!input) {
return;
}
const eventName = input.type === 'checkbox' ? 'change' : 'input';
input.addEventListener(eventName, applyOverlayToSelectedShape);
});
o.deleteButton.addEventListener('click', () => {
if (!selectedShapeId) {
return;
}
shapes = shapes.filter((shape) => shape.id !== selectedShapeId);
selectedShapeId = null;
persist();
render();
});
}
function applyOverlayToSelectedShape() {
const shape = findShape(selectedShapeId);
if (!shape) {
return;
}
shape.x = toNumberOrDefault(overlay.x.value, shape.x);
shape.y = toNumberOrDefault(overlay.y.value, shape.y);
shape.width = toNumberOrDefault(overlay.width.value, shape.width);
shape.height = toNumberOrDefault(overlay.height.value, shape.height);
shape.radius = toNumberOrDefault(overlay.radius.value, shape.radius);
shape.fontSize = toNumberOrDefault(overlay.fontSize.value, shape.fontSize);
shape.text = String(overlay.text.value || shape.text || 'Text');
shape.fill = normalizeColor(overlay.fill.value, shape.fill);
shape.stroke = normalizeColor(overlay.stroke.value, shape.stroke);
shape.strokeWidth = toNumberOrDefault(overlay.strokeWidth.value, shape.strokeWidth);
shape.isPort = overlay.isPort.checked;
if (shape.isPort) {
shape.portName = String(overlay.portName.value || '').trim();
} else {
shape.portName = '';
}
persist();
render();
}
function render() {
renderCanvas();
renderOverlay();
}
function renderCanvas() {
svg.querySelectorAll('.shape-object').forEach((el) => el.remove());
shapes.forEach((shape) => {
const element = createSvgShapeElement(shape);
if (!element) {
return;
}
element.classList.add('shape-object');
if (shape.id === selectedShapeId) {
element.classList.add('is-selected');
}
if (shape.isPort) {
element.classList.add('is-port');
}
element.setAttribute('data-shape-id', shape.id);
svg.appendChild(element);
});
}
function renderOverlay() {
const selected = findShape(selectedShapeId);
const hasSelection = !!selected;
overlay.empty.hidden = hasSelection;
overlay.form.hidden = !hasSelection;
if (!selected) {
return;
}
overlay.type.value = selected.type;
overlay.x.value = selected.x;
overlay.y.value = selected.y;
overlay.width.value = selected.width;
overlay.height.value = selected.height;
overlay.radius.value = selected.radius;
overlay.fontSize.value = selected.fontSize;
overlay.text.value = selected.text || '';
overlay.fill.value = normalizeColor(selected.fill, '#cccccc');
overlay.stroke.value = normalizeColor(selected.stroke, '#333333');
overlay.strokeWidth.value = selected.strokeWidth;
overlay.isPort.checked = !!selected.isPort;
overlay.portName.value = selected.portName || '';
const isRect = selected.type === 'rect';
const isCircle = selected.type === 'circle';
const isText = selected.type === 'text';
setFieldVisible(fieldVisibility.width, isRect);
setFieldVisible(fieldVisibility.height, isRect);
setFieldVisible(fieldVisibility.radius, isCircle);
setFieldVisible(fieldVisibility.text, isText);
setFieldVisible(fieldVisibility.font_size, isText);
overlay.portNameLabel.hidden = !selected.isPort;
}
function persist() {
hiddenInput.value = JSON.stringify(shapes);
}
function findShape(id) {
if (!id) {
return null;
}
return shapes.find((shape) => shape.id === id) || null;
}
}
function createSvgShapeElement(shape) {
const fill = normalizeColor(shape.fill, '#cccccc');
const stroke = normalizeColor(shape.stroke, '#333333');
const strokeWidth = shape.strokeWidth > 0 ? shape.strokeWidth : 1;
if (shape.type === 'rect') {
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('x', String(shape.x));
rect.setAttribute('y', String(shape.y));
rect.setAttribute('width', String(shape.width));
rect.setAttribute('height', String(shape.height));
rect.setAttribute('fill', fill);
rect.setAttribute('stroke', stroke);
rect.setAttribute('stroke-width', String(strokeWidth));
return rect;
}
if (shape.type === 'circle') {
const circle = document.createElementNS(SVG_NS, 'circle');
circle.setAttribute('cx', String(shape.x));
circle.setAttribute('cy', String(shape.y));
circle.setAttribute('r', String(shape.radius));
circle.setAttribute('fill', fill);
circle.setAttribute('stroke', stroke);
circle.setAttribute('stroke-width', String(strokeWidth));
return circle;
}
if (shape.type === 'text') {
const text = document.createElementNS(SVG_NS, 'text');
text.setAttribute('x', String(shape.x));
text.setAttribute('y', String(shape.y));
text.setAttribute('fill', fill);
text.setAttribute('font-size', String(shape.fontSize));
text.setAttribute('text-anchor', 'start');
text.setAttribute('dominant-baseline', 'hanging');
text.textContent = shape.text || 'Text';
return text;
}
return null;
}
function normalizeShapeList(value) {
if (!Array.isArray(value)) {
return [];
}
return value.map((shape, index) => {
const normalizedType = ['rect', 'circle', 'text'].includes(shape.type) ? shape.type : 'rect';
const legacyRadius = toNumberOrDefault(shape.r, 26);
return {
id: String(shape.id || `shape_${Date.now()}_${index}`),
type: normalizedType,
x: toNumberOrDefault(shape.x, 20),
y: toNumberOrDefault(shape.y, 20),
width: toNumberOrDefault(shape.width, 120),
height: toNumberOrDefault(shape.height, 60),
radius: toNumberOrDefault(shape.radius, legacyRadius),
text: String(shape.text || 'Text'),
fontSize: toNumberOrDefault(shape.fontSize, 16),
fill: normalizeColor(shape.fill, '#cccccc'),
stroke: normalizeColor(shape.stroke, '#333333'),
strokeWidth: toNumberOrDefault(shape.strokeWidth, 1),
isPort: !!shape.isPort,
portName: String(shape.portName || '')
};
});
}
function createDefaultShape(type, x, y) {
const uid = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
if (type === 'circle') {
return {
id: uid,
type: 'circle',
x: Math.round(x),
y: Math.round(y),
width: 0,
height: 0,
radius: 26,
text: '',
fontSize: 16,
fill: '#cfe6ff',
stroke: '#1f5f99',
strokeWidth: 1,
isPort: false,
portName: ''
};
}
if (type === 'text') {
return {
id: uid,
type: 'text',
x: Math.round(x),
y: Math.round(y),
width: 0,
height: 0,
radius: 0,
text: 'Text',
fontSize: 16,
fill: '#2a2a2a',
stroke: '#2a2a2a',
strokeWidth: 0,
isPort: false,
portName: ''
};
}
return {
id: uid,
type: 'rect',
x: Math.round(x),
y: Math.round(y),
width: 120,
height: 60,
radius: 0,
text: '',
fontSize: 16,
fill: '#d9e8b3',
stroke: '#4d5f27',
strokeWidth: 1,
isPort: false,
portName: ''
};
}
function getShapeAnchor(shape) {
if (shape.type === 'circle') {
return { x: shape.x, y: shape.y };
}
return { x: shape.x, y: shape.y };
}
function setShapeAnchor(shape, x, y) {
shape.x = x;
shape.y = y;
}
function setFieldVisible(field, visible) {
if (!field) {
return;
}
field.hidden = !visible;
}
function toSvgPoint(event, svg) {
const pt = svg.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
const ctm = svg.getScreenCTM();
if (!ctm) {
return { x: 0, y: 0 };
}
const p = pt.matrixTransform(ctm.inverse());
return { x: p.x, y: p.y };
}
function toNumberOrDefault(value, fallback) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
function readJson(raw) {
if (!raw || !String(raw).trim()) {
return [];
}
try {
return JSON.parse(raw);
} catch (error) {
return [];
}
}
function normalizeColor(value, fallback) {
const v = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
return v;
}
return fallback;
}
document.addEventListener('DOMContentLoaded', initEditor);
})();

View File

@@ -110,73 +110,96 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Gerätedesign (Rechtecke, Kreise, Text)</legend> <legend>Gerätedesign (SVG-Editor)</legend>
<input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>"> <input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>">
<div class="shape-editor"> <div class="shape-editor" id="device-type-shape-editor">
<div class="shape-editor-canvas"> <div class="shape-toolbox">
<svg id="shape-canvas" viewBox="0 0 400 200" role="img" aria-label="Gerätezeichnung"> <h4>Werkzeuge</h4>
<rect width="100%" height="100%" fill="#f8f8f8" stroke="#ddd" stroke-width="1"></rect> <p class="hint">Element in die Zeichenfläche ziehen, dann per Klick auswählen und bearbeiten.</p>
</svg> <div class="shape-tool-list">
<button type="button" class="shape-tool" draggable="true" data-shape-template="rect">Rechteck</button>
<button type="button" class="shape-tool" draggable="true" data-shape-template="circle">Kreis</button>
<button type="button" class="shape-tool" draggable="true" data-shape-template="text">Text</button>
</div> </div>
<div class="shape-editor-controls">
<div class="form-group">
<label for="shape-type">Form</label>
<select id="shape-type">
<option value="rect">Rechteck</option>
<option value="circle">Kreis</option>
<option value="text">Text</option>
</select>
</div> </div>
<div class="shape-editor-canvas">
<svg id="shape-canvas" viewBox="0 0 800 360" role="img" aria-label="Gerätezeichnung">
<rect width="100%" height="100%" fill="#f8f8f8" stroke="#ddd" stroke-width="1"></rect>
</svg>
<p class="hint">Bestehende SVG-Objekte sind anklickbar und per Drag-and-Drop verschiebbar.</p>
</div>
<div class="shape-overlay" id="shape-overlay">
<h4>Objekt-Parameter</h4>
<p class="shape-overlay-empty" id="shape-overlay-empty">Kein Objekt ausgewählt.</p>
<div class="shape-overlay-form" id="shape-overlay-form" hidden>
<div class="shape-control-grid"> <div class="shape-control-grid">
<label> <label>
x Typ
<input type="number" id="shape-x" value="20" step="1"> <input type="text" id="shape-param-type" readonly>
</label> </label>
<label> <label>
y X
<input type="number" id="shape-y" value="20" step="1"> <input type="number" id="shape-param-x" step="1">
</label> </label>
<label> <label>
Y
<input type="number" id="shape-param-y" step="1">
</label>
<label data-field="width">
Breite Breite
<input type="number" id="shape-width" value="120" step="1"> <input type="number" id="shape-param-width" step="1">
</label> </label>
<label> <label data-field="height">
Höhe Höhe
<input type="number" id="shape-height" value="60" step="1"> <input type="number" id="shape-param-height" step="1">
</label> </label>
<label> <label data-field="radius">
Radius Radius
<input type="number" id="shape-radius" value="30" step="1"> <input type="number" id="shape-param-radius" step="1">
</label> </label>
<label> <label data-field="font_size">
Font-Size
<input type="number" id="shape-param-font-size" step="1">
</label>
<label data-field="text">
Text Text
<input type="text" id="shape-text" value="Label"> <input type="text" id="shape-param-text">
</label> </label>
<label> <label>
Füllung Füllung
<input type="color" id="shape-fill" value="#cccccc"> <input type="color" id="shape-param-fill">
</label> </label>
<label> <label>
Strich Strich
<input type="color" id="shape-stroke" value="#333333"> <input type="color" id="shape-param-stroke">
</label> </label>
<label> <label>
Strichbreite Strichbreite
<input type="number" id="shape-stroke-width" value="1" step="0.5"> <input type="number" id="shape-param-stroke-width" step="0.5" min="0">
</label> </label>
</div> </div>
<button type="button" class="button button-primary" id="shape-add">Form hinzufügen</button> <div class="shape-port-settings">
<p class="hint">Shapes werden als JSON gespeichert und können jederzeit angepasst werden.</p> <label class="inline-checkbox">
</div> <input type="checkbox" id="shape-param-is-port">
Dieses Objekt ist ein Port
</label>
<label id="shape-port-name-label" hidden>
Port-Name
<input type="text" id="shape-param-port-name" placeholder="z.B. Gi1/0/1">
</label>
</div> </div>
<div class="shape-list"> <div class="shape-overlay-actions">
<h4>Shapes</h4> <button type="button" class="button button-danger" id="shape-delete">Objekt entfernen</button>
<ul id="shape-list"></ul> </div>
</div>
</div>
</div> </div>
</fieldset> </fieldset>
@@ -323,86 +346,132 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
} }
.shape-editor { .shape-editor {
display: flex; display: grid;
gap: 18px; grid-template-columns: 180px 1fr 320px;
flex-wrap: wrap; gap: 16px;
margin-top: 20px; margin-top: 16px;
} }
.shape-editor-canvas { .shape-editor-canvas {
flex: 1 1 320px;
min-width: 280px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 6px; border-radius: 6px;
background: white; background: white;
padding: 10px; padding: 12px;
} }
.shape-editor-canvas svg { .shape-editor-canvas svg {
width: 100%; width: 100%;
height: 200px; min-height: 320px;
display: block; display: block;
font-family: inherit; font-family: inherit;
cursor: crosshair;
} }
.shape-editor-controls { .shape-toolbox,
flex: 1 1 220px; .shape-overlay {
min-width: 220px; border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
background: #fff;
} }
.shape-control-grid { .shape-toolbox h4,
.shape-overlay h4 {
margin: 0 0 8px;
}
.shape-tool-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.shape-tool {
text-align: left;
border: 1px solid #bbb;
background: #f7f7f7;
color: #222;
padding: 10px;
border-radius: 4px;
cursor: grab;
}
.shape-tool:active {
cursor: grabbing;
}
.shape-editor-canvas.drag-over {
outline: 2px dashed #007bff;
outline-offset: 2px;
}
.shape-object {
cursor: move;
}
.shape-object.is-selected {
filter: drop-shadow(0 0 5px rgba(0, 123, 255, 0.7));
}
.shape-object.is-port {
stroke-dasharray: 4 2;
}
.shape-overlay-form .shape-control-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); grid-template-columns: repeat(2, minmax(120px, 1fr));
gap: 8px; gap: 8px;
margin: 12px 0;
} }
.shape-control-grid label { .shape-overlay-form label {
font-size: 0.8rem; font-size: 0.82rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.shape-control-grid input, .shape-overlay-form input[type="number"],
.shape-control-grid select { .shape-overlay-form input[type="text"],
.shape-overlay-form input[type="color"] {
width: 100%;
padding: 6px 8px; padding: 6px 8px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 4px;
font-family: inherit; font-family: inherit;
} }
.shape-list { .shape-port-settings {
margin-top: 12px; margin-top: 10px;
} }
.shape-list ul { .inline-checkbox {
list-style: none; flex-direction: row !important;
padding: 0;
margin: 6px 0 0;
border: 1px solid #eee;
border-radius: 4px;
background: #fafafa;
max-height: 130px;
overflow-y: auto;
}
.shape-list li {
display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 6px !important;
padding: 8px 12px;
border-bottom: 1px solid #eee;
font-size: 0.85rem;
} }
.shape-list li:last-child { .shape-overlay-actions {
border-bottom: none; margin-top: 12px;
display: flex;
justify-content: flex-end;
} }
.shape-list button { .shape-overlay-empty {
padding: 4px 8px; margin: 6px 0 0;
font-size: 0.75rem; color: #666;
font-size: 0.9rem;
}
.hint {
font-size: 0.8rem;
color: #666;
margin: 8px 0 0;
}
@media (max-width: 1100px) {
.shape-editor {
grid-template-columns: 1fr;
}
} }
.button { .button {
@@ -454,172 +523,4 @@ function confirmDelete(id) {
} }
</script> </script>
<script> <script src="/assets/js/device-type-shape-editor.js" defer></script>
document.addEventListener('DOMContentLoaded', () => {
const hiddenInput = document.getElementById('shape-definition');
const svgCanvas = document.getElementById('shape-canvas');
const shapeList = document.getElementById('shape-list');
const addShapeButton = document.getElementById('shape-add');
const typeSelect = document.getElementById('shape-type');
const xInput = document.getElementById('shape-x');
const yInput = document.getElementById('shape-y');
const widthInput = document.getElementById('shape-width');
const heightInput = document.getElementById('shape-height');
const radiusInput = document.getElementById('shape-radius');
const textInput = document.getElementById('shape-text');
const fillInput = document.getElementById('shape-fill');
const strokeInput = document.getElementById('shape-stroke');
const strokeWidthInput = document.getElementById('shape-stroke-width');
if (!hiddenInput || !svgCanvas || !shapeList) {
return;
}
let shapes = [];
function parseShapes() {
try {
const parsed = JSON.parse(hiddenInput.value || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function persistShapes() {
hiddenInput.value = JSON.stringify(shapes);
}
function clearGenerated() {
svgCanvas.querySelectorAll('.generated-shape').forEach(el => el.remove());
}
function createSvgElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
function renderCanvas() {
clearGenerated();
shapes.forEach((shape) => {
let el;
const fill = shape.fill || '#cccccc';
const stroke = shape.stroke || '#333333';
const strokeWidth = typeof shape.strokeWidth === 'number' ? shape.strokeWidth : 1;
if (shape.type === 'rect') {
el = createSvgElement('rect');
el.setAttribute('x', shape.x ?? 10);
el.setAttribute('y', shape.y ?? 10);
el.setAttribute('width', shape.width ?? 120);
el.setAttribute('height', shape.height ?? 60);
} else if (shape.type === 'circle') {
el = createSvgElement('circle');
el.setAttribute('cx', shape.x ?? 60);
el.setAttribute('cy', shape.y ?? 60);
el.setAttribute('r', shape.r ?? 30);
} else if (shape.type === 'text') {
el = createSvgElement('text');
el.setAttribute('x', shape.x ?? 30);
el.setAttribute('y', shape.y ?? 20);
el.setAttribute('fill', fill);
el.setAttribute('font-size', '18');
el.setAttribute('text-anchor', 'middle');
el.setAttribute('dominant-baseline', 'central');
el.textContent = shape.text || 'Text';
el.classList.add('generated-shape');
svgCanvas.appendChild(el);
return;
}
if (!el) {
return;
}
el.setAttribute('fill', fill);
el.setAttribute('stroke', stroke);
el.setAttribute('stroke-width', strokeWidth);
el.classList.add('generated-shape');
svgCanvas.appendChild(el);
});
}
function renderList() {
shapeList.innerHTML = '';
if (shapes.length === 0) {
const empty = document.createElement('li');
empty.innerHTML = '<em>Noch keine Formen definiert.</em>';
shapeList.appendChild(empty);
return;
}
shapes.forEach((shape, index) => {
const li = document.createElement('li');
const label = document.createElement('span');
const typeLabels = {
rect: 'Rechteck',
circle: 'Kreis',
text: 'Text'
};
const summary = `${typeLabels[shape.type] || shape.type} @ (${shape.x ?? 0}, ${shape.y ?? 0})`;
label.textContent = summary;
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.textContent = 'Entfernen';
removeButton.classList.add('button', 'button-small', 'button-danger');
removeButton.dataset.removeShape = index;
li.appendChild(label);
li.appendChild(removeButton);
shapeList.appendChild(li);
});
}
function getNumberValue(input, fallback) {
const value = parseFloat(input.value);
return Number.isFinite(value) ? value : fallback;
}
addShapeButton.addEventListener('click', () => {
const type = typeSelect.value;
const baseShape = {
type,
x: getNumberValue(xInput, 20),
y: getNumberValue(yInput, 20),
fill: fillInput.value || '#cccccc',
stroke: strokeInput.value || '#333333',
strokeWidth: getNumberValue(strokeWidthInput, 1)
};
if (type === 'rect') {
baseShape.width = getNumberValue(widthInput, 120);
baseShape.height = getNumberValue(heightInput, 60);
} else if (type === 'circle') {
baseShape.r = getNumberValue(radiusInput, 30);
} else if (type === 'text') {
baseShape.text = textInput.value || 'Text';
}
shapes.push(baseShape);
renderCanvas();
renderList();
persistShapes();
});
shapeList.addEventListener('click', (event) => {
if (!event.target.dataset.removeShape) {
return;
}
const index = Number(event.target.dataset.removeShape);
shapes.splice(index, 1);
renderCanvas();
renderList();
persistShapes();
});
shapes = parseShapes();
renderCanvas();
renderList();
persistShapes();
});
</script>