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

@@ -110,74 +110,97 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
</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); ?>">
<div class="shape-editor">
<div class="shape-editor" id="device-type-shape-editor">
<div class="shape-toolbox">
<h4>Werkzeuge</h4>
<p class="hint">Element in die Zeichenfläche ziehen, dann per Klick auswählen und bearbeiten.</p>
<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-canvas">
<svg id="shape-canvas" viewBox="0 0 400 200" role="img" aria-label="Gerätezeichnung">
<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-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 class="shape-control-grid">
<label>
x
<input type="number" id="shape-x" value="20" step="1">
</label>
<label>
y
<input type="number" id="shape-y" value="20" step="1">
</label>
<label>
Breite
<input type="number" id="shape-width" value="120" step="1">
</label>
<label>
Höhe
<input type="number" id="shape-height" value="60" step="1">
</label>
<label>
Radius
<input type="number" id="shape-radius" value="30" step="1">
</label>
<label>
Text
<input type="text" id="shape-text" value="Label">
</label>
<label>
Füllung
<input type="color" id="shape-fill" value="#cccccc">
</label>
<label>
Strich
<input type="color" id="shape-stroke" value="#333333">
</label>
<label>
Strichbreite
<input type="number" id="shape-stroke-width" value="1" step="0.5">
</label>
</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>
<button type="button" class="button button-primary" id="shape-add">Form hinzufügen</button>
<p class="hint">Shapes werden als JSON gespeichert und können jederzeit angepasst werden.</p>
<div class="shape-overlay-form" id="shape-overlay-form" hidden>
<div class="shape-control-grid">
<label>
Typ
<input type="text" id="shape-param-type" readonly>
</label>
<label>
X
<input type="number" id="shape-param-x" step="1">
</label>
<label>
Y
<input type="number" id="shape-param-y" step="1">
</label>
<label data-field="width">
Breite
<input type="number" id="shape-param-width" step="1">
</label>
<label data-field="height">
Höhe
<input type="number" id="shape-param-height" step="1">
</label>
<label data-field="radius">
Radius
<input type="number" id="shape-param-radius" step="1">
</label>
<label data-field="font_size">
Font-Size
<input type="number" id="shape-param-font-size" step="1">
</label>
<label data-field="text">
Text
<input type="text" id="shape-param-text">
</label>
<label>
Füllung
<input type="color" id="shape-param-fill">
</label>
<label>
Strich
<input type="color" id="shape-param-stroke">
</label>
<label>
Strichbreite
<input type="number" id="shape-param-stroke-width" step="0.5" min="0">
</label>
</div>
<div class="shape-port-settings">
<label class="inline-checkbox">
<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 class="shape-overlay-actions">
<button type="button" class="button button-danger" id="shape-delete">Objekt entfernen</button>
</div>
</div>
</div>
</div>
<div class="shape-list">
<h4>Shapes</h4>
<ul id="shape-list"></ul>
</div>
</fieldset>
<!-- =========================
@@ -323,86 +346,132 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
}
.shape-editor {
display: flex;
gap: 18px;
flex-wrap: wrap;
margin-top: 20px;
display: grid;
grid-template-columns: 180px 1fr 320px;
gap: 16px;
margin-top: 16px;
}
.shape-editor-canvas {
flex: 1 1 320px;
min-width: 280px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
padding: 10px;
padding: 12px;
}
.shape-editor-canvas svg {
width: 100%;
height: 200px;
min-height: 320px;
display: block;
font-family: inherit;
cursor: crosshair;
}
.shape-editor-controls {
flex: 1 1 220px;
min-width: 220px;
.shape-toolbox,
.shape-overlay {
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;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
grid-template-columns: repeat(2, minmax(120px, 1fr));
gap: 8px;
margin: 12px 0;
}
.shape-control-grid label {
font-size: 0.8rem;
.shape-overlay-form label {
font-size: 0.82rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.shape-control-grid input,
.shape-control-grid select {
.shape-overlay-form input[type="number"],
.shape-overlay-form input[type="text"],
.shape-overlay-form input[type="color"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
.shape-list {
margin-top: 12px;
.shape-port-settings {
margin-top: 10px;
}
.shape-list ul {
list-style: none;
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;
.inline-checkbox {
flex-direction: row !important;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid #eee;
font-size: 0.85rem;
gap: 6px !important;
}
.shape-list li:last-child {
border-bottom: none;
.shape-overlay-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.shape-list button {
padding: 4px 8px;
font-size: 0.75rem;
.shape-overlay-empty {
margin: 6px 0 0;
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 {
@@ -454,172 +523,4 @@ function confirmDelete(id) {
}
</script>
<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>
<script src="/assets/js/device-type-shape-editor.js" defer></script>