WIP: device type zeichner,
This commit is contained in:
@@ -22,7 +22,7 @@ if ($deviceTypeId > 0) {
|
||||
[$deviceTypeId]
|
||||
);
|
||||
|
||||
if ($deviceType) {
|
||||
if ($deviceType) {
|
||||
$ports = $sql->get(
|
||||
"SELECT * FROM device_type_ports WHERE device_type_id = ? ORDER BY name",
|
||||
"i",
|
||||
@@ -31,6 +31,11 @@ if ($deviceTypeId > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
$shapeDefinition = $deviceType['shape_definition'] ?? '[]';
|
||||
if (trim($shapeDefinition) === '') {
|
||||
$shapeDefinition = '[]';
|
||||
}
|
||||
|
||||
$isEdit = !empty($deviceType);
|
||||
$pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType['name']) : "Neuer Gerätetyp";
|
||||
|
||||
@@ -104,6 +109,77 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Gerätedesign (Rechtecke, Kreise, Text)</legend>
|
||||
|
||||
<input type="hidden" name="shape_definition" id="shape-definition" value="<?php echo htmlspecialchars($shapeDefinition); ?>">
|
||||
|
||||
<div class="shape-editor">
|
||||
<div class="shape-editor-canvas">
|
||||
<svg id="shape-canvas" viewBox="0 0 400 200" role="img" aria-label="Gerätezeichnung">
|
||||
<rect width="100%" height="100%" fill="#f8f8f8" stroke="#ddd" stroke-width="1"></rect>
|
||||
</svg>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="shape-list">
|
||||
<h4>Shapes</h4>
|
||||
<ul id="shape-list"></ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- =========================
|
||||
Port-Definitionen
|
||||
========================= -->
|
||||
@@ -246,6 +322,89 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.shape-editor {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.shape-editor-canvas {
|
||||
flex: 1 1 320px;
|
||||
min-width: 280px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.shape-editor-canvas svg {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.shape-editor-controls {
|
||||
flex: 1 1 220px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.shape-control-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.shape-control-grid label {
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.shape-control-grid input,
|
||||
.shape-control-grid select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.shape-list {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.shape-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.shape-list button {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px 15px;
|
||||
background: #007bff;
|
||||
@@ -294,3 +453,173 @@ 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>
|
||||
|
||||
@@ -22,6 +22,14 @@ $name = trim($_POST['name'] ?? '');
|
||||
$category = $_POST['category'] ?? 'other';
|
||||
$comment = trim($_POST['comment'] ?? '');
|
||||
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
|
||||
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
||||
$shapeDefinition = '[]';
|
||||
if ($rawShapes !== '') {
|
||||
$decoded = json_decode($rawShapes, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$shapeDefinition = json_encode($decoded, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Validierung
|
||||
@@ -88,31 +96,31 @@ if ($deviceTypeId > 0) {
|
||||
// UPDATE
|
||||
if ($imagePath) {
|
||||
$sql->set(
|
||||
"UPDATE device_types SET name = ?, category = ?, comment = ?, image_path = ?, image_type = ? WHERE id = ?",
|
||||
"sssisi",
|
||||
[$name, $category, $comment, $imagePath, $imageType, $deviceTypeId]
|
||||
"UPDATE device_types SET name = ?, category = ?, comment = ?, image_path = ?, image_type = ?, shape_definition = ? WHERE id = ?",
|
||||
"ssssssi",
|
||||
[$name, $category, $comment, $imagePath, $imageType, $shapeDefinition, $deviceTypeId]
|
||||
);
|
||||
} else {
|
||||
$sql->set(
|
||||
"UPDATE device_types SET name = ?, category = ?, comment = ? WHERE id = ?",
|
||||
"sssi",
|
||||
[$name, $category, $comment, $deviceTypeId]
|
||||
"UPDATE device_types SET name = ?, category = ?, comment = ?, shape_definition = ? WHERE id = ?",
|
||||
"ssssi",
|
||||
[$name, $category, $comment, $shapeDefinition, $deviceTypeId]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// INSERT
|
||||
if ($imagePath) {
|
||||
$deviceTypeId = $sql->set(
|
||||
"INSERT INTO device_types (name, category, comment, image_path, image_type) VALUES (?, ?, ?, ?, ?)",
|
||||
"sssss",
|
||||
[$name, $category, $comment, $imagePath, $imageType],
|
||||
"INSERT INTO device_types (name, category, comment, image_path, image_type, shape_definition) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
"ssssss",
|
||||
[$name, $category, $comment, $imagePath, $imageType, $shapeDefinition],
|
||||
true
|
||||
);
|
||||
} else {
|
||||
$deviceTypeId = $sql->set(
|
||||
"INSERT INTO device_types (name, category, comment, image_path, image_type) VALUES (?, ?, ?, NULL, ?)",
|
||||
"ssss",
|
||||
[$name, $category, $comment, 'bitmap'],
|
||||
"INSERT INTO device_types (name, category, comment, image_path, image_type, shape_definition) VALUES (?, ?, ?, NULL, ?, ?)",
|
||||
"sssss",
|
||||
[$name, $category, $comment, 'bitmap', $shapeDefinition],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user