feat(schematic): Wire-Dragging, Farbpropagierung, Busbar-Typen
- Wire-Segment-Dragging: Shift+Klick/Mittlere Maustaste zum Verschieben - Horizontale Segmente nur vertikal, vertikale nur horizontal - Grid-Snapping, Live-Vorschau, Start/End-Segmente fixiert - PWA: Automatische Farbpropagierung bei Einspeisung (L1/L2/L3/N/PE) - N-Phase als Input hinzugefuegt - propagateInputColor() aktualisiert Abgaenge - Busbar-Typen aus Datenbank statt hardcodiert - ChangeLog aktualisiert Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
95e1860940
commit
848232c5a6
4 changed files with 410 additions and 4 deletions
24
ChangeLog.md
24
ChangeLog.md
|
|
@ -14,6 +14,30 @@
|
|||
- Leitungen "verschwinden" hinter Bloecken und kommen auf der anderen Seite wieder raus
|
||||
- Professionelleres Erscheinungsbild wie in echten Schaltplan-Editoren
|
||||
|
||||
- **Wire-Segment-Dragging**: Leitungen koennen verschoben werden ohne Verbindungen zu verlieren
|
||||
- Shift+Klick oder Mittlere Maustaste auf Leitungssegment zum Ziehen
|
||||
- Horizontale Segmente nur vertikal verschiebbar, vertikale nur horizontal
|
||||
- Start- und End-Segmente (an Terminals) bleiben fix
|
||||
- Automatisches Grid-Snapping (25px)
|
||||
- Live-Vorschau waehrend dem Ziehen
|
||||
- Neue Funktionen: `parsePathToPoints()`, `pointsToPath()`, `findClickedSegment()`
|
||||
- `startWireDrag()`, `handleWireDragMove()`, `finishWireDrag()`, `cancelWireDrag()`
|
||||
|
||||
- **Busbar-Typen aus Datenbank**: Phasenschienen-Typen dynamisch aus DB laden
|
||||
- Edit-Dialog nutzt jetzt `busbarTypes` Array statt hardcodierter Optionen
|
||||
- `fk_busbar_type` wird beim Update korrekt gespeichert
|
||||
- Admin-Seite fuer Busbar-Typen mit phases_config JSON-Feld
|
||||
|
||||
- **PWA: Farbpropagierung bei Einspeisung**: Automatische Farbuebernahme
|
||||
- Bei Auswahl einer Input-Phase (L1/L2/L3/N/PE) wird Farbe automatisch gesetzt
|
||||
- Funktion `propagateInputColor()` aktualisiert Farben auf Abgaengen
|
||||
- Phase-Matching: L1 matched L1, LN, L1N; N matched N; etc.
|
||||
- Funktioniert online und offline (mit Queue)
|
||||
|
||||
- **PWA: N-Phase als Einspeisung**: Neutralleiter jetzt als Input-Phase waehlbar
|
||||
- INPUT_PHASES erweitert um 'N': ['L1', 'L2', 'L3', 'N', 'PE']
|
||||
- 3P und 3P+N entfernt (nur Einzel-Phasen)
|
||||
|
||||
### Verbesserungen
|
||||
|
||||
- **Zeichenmodus-Verhalten**: Konsistentes Verhalten im manuellen Zeichenmodus
|
||||
|
|
|
|||
|
|
@ -5222,6 +5222,14 @@
|
|||
wireExtendExistingPoints: [], // Existing path points of connection being extended
|
||||
WIRE_GRID_SIZE: 25, // Snap grid size in pixels (larger = fewer points)
|
||||
|
||||
// Wire segment dragging
|
||||
wireDragMode: false, // Currently dragging a wire segment
|
||||
wireDragConnId: null, // Connection ID being dragged
|
||||
wireDragSegmentIndex: null, // Index of segment being dragged (0-based)
|
||||
wireDragIsHorizontal: null, // true = horizontal segment (drag Y), false = vertical (drag X)
|
||||
wireDragStartPos: null, // Starting mouse position
|
||||
wireDragOriginalPoints: [], // Original path points before drag
|
||||
|
||||
// Display settings (persisted in localStorage)
|
||||
displaySettings: {
|
||||
phaseColors: true, // Color wires by phase (L1=brown, L2=black, L3=gray, N=blue, PE=green)
|
||||
|
|
@ -8031,6 +8039,44 @@
|
|||
this.addEventListener('mouseleave', function() {
|
||||
$visiblePath.attr('stroke-width', wireWidth);
|
||||
});
|
||||
|
||||
// Wire segment dragging - mousedown to start drag
|
||||
this.addEventListener('mousedown', function(e) {
|
||||
// Only with middle mouse button or Shift+left click
|
||||
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Don't start drag if in wire draw mode
|
||||
if (self.wireDrawMode) return;
|
||||
|
||||
var $svg = $(e.target).closest('svg');
|
||||
var svgRect = $svg[0].getBoundingClientRect();
|
||||
var clickX = e.clientX - svgRect.left;
|
||||
var clickY = e.clientY - svgRect.top;
|
||||
|
||||
if (self.startWireDrag(connId, clickX, clickY)) {
|
||||
// Successfully started drag - bind move/up handlers
|
||||
var moveHandler = function(moveE) {
|
||||
var mx = moveE.clientX - svgRect.left;
|
||||
var my = moveE.clientY - svgRect.top;
|
||||
self.handleWireDragMove(mx, my);
|
||||
};
|
||||
|
||||
var upHandler = function(upE) {
|
||||
var ux = upE.clientX - svgRect.left;
|
||||
var uy = upE.clientY - svgRect.top;
|
||||
self.finishWireDrag(ux, uy);
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
document.removeEventListener('mouseup', upHandler);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
document.addEventListener('mouseup', upHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
|
|
@ -8193,6 +8239,37 @@
|
|||
this.addEventListener('mouseleave', function() {
|
||||
$visiblePath.attr('stroke-width', wireWidth);
|
||||
});
|
||||
|
||||
// Wire segment dragging
|
||||
this.addEventListener('mousedown', function(e) {
|
||||
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (self.wireDrawMode) return;
|
||||
|
||||
var $svg = $(e.target).closest('svg');
|
||||
var svgRect = $svg[0].getBoundingClientRect();
|
||||
var clickX = e.clientX - svgRect.left;
|
||||
var clickY = e.clientY - svgRect.top;
|
||||
|
||||
if (self.startWireDrag(connId, clickX, clickY)) {
|
||||
var moveHandler = function(moveE) {
|
||||
var mx = moveE.clientX - svgRect.left;
|
||||
var my = moveE.clientY - svgRect.top;
|
||||
self.handleWireDragMove(mx, my);
|
||||
};
|
||||
var upHandler = function(upE) {
|
||||
var ux = upE.clientX - svgRect.left;
|
||||
var uy = upE.clientY - svgRect.top;
|
||||
self.finishWireDrag(ux, uy);
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
document.removeEventListener('mouseup', upHandler);
|
||||
};
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
document.addEventListener('mouseup', upHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.style.cursor = 'pointer';
|
||||
});
|
||||
},
|
||||
|
|
@ -10802,6 +10879,226 @@
|
|||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Wire Segment Dragging
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Parse SVG path data to array of points
|
||||
*/
|
||||
parsePathToPoints: function(pathData) {
|
||||
if (!pathData) return [];
|
||||
var points = [];
|
||||
var regex = /([ML])\s*([\d.]+)\s+([\d.]+)/g;
|
||||
var match;
|
||||
while ((match = regex.exec(pathData)) !== null) {
|
||||
points.push({
|
||||
x: parseFloat(match[2]),
|
||||
y: parseFloat(match[3])
|
||||
});
|
||||
}
|
||||
return points;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert points array back to SVG path string
|
||||
*/
|
||||
pointsToPath: function(points) {
|
||||
if (!points || points.length === 0) return '';
|
||||
var path = '';
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
path += (i === 0 ? 'M ' : 'L ') + Math.round(points[i].x) + ' ' + Math.round(points[i].y) + ' ';
|
||||
}
|
||||
return path.trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* Find which segment of a path was clicked
|
||||
* Returns: { index: segmentIndex, isHorizontal: bool } or null
|
||||
*/
|
||||
findClickedSegment: function(points, clickX, clickY, threshold) {
|
||||
threshold = threshold || 15;
|
||||
|
||||
for (var i = 0; i < points.length - 1; i++) {
|
||||
var p1 = points[i];
|
||||
var p2 = points[i + 1];
|
||||
|
||||
// Check if click is near this segment
|
||||
var isHorizontal = Math.abs(p1.y - p2.y) < 2; // Horizontal if Y values are same
|
||||
var isVertical = Math.abs(p1.x - p2.x) < 2; // Vertical if X values are same
|
||||
|
||||
if (isHorizontal) {
|
||||
// Horizontal segment - check if click is within X range and near Y
|
||||
var minX = Math.min(p1.x, p2.x);
|
||||
var maxX = Math.max(p1.x, p2.x);
|
||||
if (clickX >= minX - threshold && clickX <= maxX + threshold &&
|
||||
Math.abs(clickY - p1.y) <= threshold) {
|
||||
return { index: i, isHorizontal: true };
|
||||
}
|
||||
} else if (isVertical) {
|
||||
// Vertical segment - check if click is within Y range and near X
|
||||
var minY = Math.min(p1.y, p2.y);
|
||||
var maxY = Math.max(p1.y, p2.y);
|
||||
if (clickY >= minY - threshold && clickY <= maxY + threshold &&
|
||||
Math.abs(clickX - p1.x) <= threshold) {
|
||||
return { index: i, isHorizontal: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start dragging a wire segment
|
||||
*/
|
||||
startWireDrag: function(connId, clickX, clickY) {
|
||||
var conn = this.connections.find(function(c) { return c.id == connId; });
|
||||
if (!conn || !conn.path_data) return false;
|
||||
|
||||
var points = this.parsePathToPoints(conn.path_data);
|
||||
if (points.length < 3) return false; // Need at least 3 points to have a draggable segment
|
||||
|
||||
var segment = this.findClickedSegment(points, clickX, clickY);
|
||||
if (!segment) return false;
|
||||
|
||||
// Don't allow dragging the first or last segment (connected to terminals)
|
||||
if (segment.index === 0 || segment.index === points.length - 2) {
|
||||
this.showMessage('Start- und End-Segmente können nicht verschoben werden', 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.wireDragMode = true;
|
||||
this.wireDragConnId = connId;
|
||||
this.wireDragSegmentIndex = segment.index;
|
||||
this.wireDragIsHorizontal = segment.isHorizontal;
|
||||
this.wireDragStartPos = { x: clickX, y: clickY };
|
||||
this.wireDragOriginalPoints = JSON.parse(JSON.stringify(points));
|
||||
|
||||
// Visual feedback
|
||||
$(this.svgElement).css('cursor', segment.isHorizontal ? 'ns-resize' : 'ew-resize');
|
||||
this.log('Wire drag started - segment:', segment.index, 'horizontal:', segment.isHorizontal);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle mouse move during wire drag
|
||||
*/
|
||||
handleWireDragMove: function(mouseX, mouseY) {
|
||||
if (!this.wireDragMode) return;
|
||||
|
||||
var points = JSON.parse(JSON.stringify(this.wireDragOriginalPoints));
|
||||
var segIdx = this.wireDragSegmentIndex;
|
||||
|
||||
if (this.wireDragIsHorizontal) {
|
||||
// Horizontal segment - move Y (vertical drag)
|
||||
var deltaY = mouseY - this.wireDragStartPos.y;
|
||||
var newY = points[segIdx].y + deltaY;
|
||||
|
||||
// Snap to grid
|
||||
newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE;
|
||||
|
||||
// Update both points of this segment
|
||||
points[segIdx].y = newY;
|
||||
points[segIdx + 1].y = newY;
|
||||
} else {
|
||||
// Vertical segment - move X (horizontal drag)
|
||||
var deltaX = mouseX - this.wireDragStartPos.x;
|
||||
var newX = points[segIdx].x + deltaX;
|
||||
|
||||
// Snap to grid
|
||||
newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE;
|
||||
|
||||
// Update both points of this segment
|
||||
points[segIdx].x = newX;
|
||||
points[segIdx + 1].x = newX;
|
||||
}
|
||||
|
||||
// Update the visual path in SVG
|
||||
var newPath = this.pointsToPath(points);
|
||||
var $conn = $(this.svgElement).find('.schematic-connection[data-connection-id="' + this.wireDragConnId + '"]');
|
||||
var $hitarea = $(this.svgElement).find('.schematic-connection-hitarea[data-connection-id="' + this.wireDragConnId + '"]');
|
||||
var $shadow = $conn.closest('.schematic-connection-group').find('.schematic-connection-shadow');
|
||||
|
||||
$conn.attr('d', newPath);
|
||||
$hitarea.attr('d', newPath);
|
||||
$shadow.attr('d', newPath);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finish wire drag and save
|
||||
*/
|
||||
finishWireDrag: function(mouseX, mouseY) {
|
||||
if (!this.wireDragMode) return;
|
||||
|
||||
var points = JSON.parse(JSON.stringify(this.wireDragOriginalPoints));
|
||||
var segIdx = this.wireDragSegmentIndex;
|
||||
|
||||
if (this.wireDragIsHorizontal) {
|
||||
var deltaY = mouseY - this.wireDragStartPos.y;
|
||||
var newY = points[segIdx].y + deltaY;
|
||||
newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE;
|
||||
points[segIdx].y = newY;
|
||||
points[segIdx + 1].y = newY;
|
||||
} else {
|
||||
var deltaX = mouseX - this.wireDragStartPos.x;
|
||||
var newX = points[segIdx].x + deltaX;
|
||||
newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE;
|
||||
points[segIdx].x = newX;
|
||||
points[segIdx + 1].x = newX;
|
||||
}
|
||||
|
||||
// Clean up collinear points
|
||||
points = this.cleanupPathPoints(points);
|
||||
|
||||
var newPath = this.pointsToPath(points);
|
||||
var self = this;
|
||||
|
||||
// Save to server
|
||||
$.ajax({
|
||||
url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'update',
|
||||
connection_id: this.wireDragConnId,
|
||||
path_data: newPath,
|
||||
token: $('input[name="token"]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Update local data
|
||||
var conn = self.connections.find(function(c) { return c.id == self.wireDragConnId; });
|
||||
if (conn) {
|
||||
conn.path_data = newPath;
|
||||
}
|
||||
self.showMessage('Leitung verschoben', 'success');
|
||||
} else {
|
||||
self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error');
|
||||
self.renderConnections(); // Revert visual
|
||||
}
|
||||
self.cancelWireDrag();
|
||||
},
|
||||
error: function() {
|
||||
self.showMessage('Netzwerkfehler', 'error');
|
||||
self.renderConnections(); // Revert visual
|
||||
self.cancelWireDrag();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel wire drag
|
||||
*/
|
||||
cancelWireDrag: function() {
|
||||
this.wireDragMode = false;
|
||||
this.wireDragConnId = null;
|
||||
this.wireDragSegmentIndex = null;
|
||||
this.wireDragIsHorizontal = null;
|
||||
this.wireDragStartPos = null;
|
||||
this.wireDragOriginalPoints = [];
|
||||
$(this.svgElement).css('cursor', '');
|
||||
},
|
||||
|
||||
// Update an existing connection with extended path
|
||||
updateExtendedConnection: function(connId, newTargetEqId, newTargetTermId, newPathData) {
|
||||
var self = this;
|
||||
|
|
|
|||
89
js/pwa.js
89
js/pwa.js
|
|
@ -226,6 +226,17 @@
|
|||
// Medium-Type Change -> Spezifikationen laden
|
||||
$('#conn-medium-type').on('change', handleMediumTypeChange);
|
||||
|
||||
// Connection-Type Change -> Auto-Farbe bei Input-Phasen
|
||||
$('#conn-type').on('change', function() {
|
||||
if (App.connectionDirection === 'input') {
|
||||
const phase = $(this).val();
|
||||
if (phase) {
|
||||
const color = getPhaseColor(phase);
|
||||
$('#conn-color').val(color);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bestätigungsdialog
|
||||
$('#btn-confirm-ok').on('click', function() {
|
||||
closeModal('confirm');
|
||||
|
|
@ -2790,7 +2801,13 @@
|
|||
const response = await apiCall('ajax/pwa_api.php', data);
|
||||
if (response.success) {
|
||||
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
|
||||
updateLocal(list.find(c => c.id == App.editConnectionId));
|
||||
const conn = list.find(c => c.id == App.editConnectionId);
|
||||
updateLocal(conn);
|
||||
// Farbpropagierung bei Input
|
||||
if (App.connectionDirection === 'input' && connectionType) {
|
||||
const eqId = conn ? conn.fk_target : null;
|
||||
await propagateInputColor(eqId, connectionType, color);
|
||||
}
|
||||
renderEditor();
|
||||
showToast('Verbindung aktualisiert', 'success');
|
||||
} else {
|
||||
|
|
@ -2803,7 +2820,13 @@
|
|||
} else {
|
||||
queueOfflineAction(data);
|
||||
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
|
||||
updateLocal(list.find(c => c.id == App.editConnectionId));
|
||||
const conn = list.find(c => c.id == App.editConnectionId);
|
||||
updateLocal(conn);
|
||||
// Farbpropagierung bei Input (offline)
|
||||
if (App.connectionDirection === 'input' && connectionType) {
|
||||
const eqId = conn ? conn.fk_target : null;
|
||||
propagateInputColor(eqId, connectionType, color);
|
||||
}
|
||||
renderEditor();
|
||||
showToast('Wird synchronisiert...', 'warning');
|
||||
}
|
||||
|
|
@ -2846,6 +2869,10 @@
|
|||
if (App.connectionDirection === 'input') {
|
||||
newConn.fk_target = App.connectionEquipmentId;
|
||||
App.inputs.push(newConn);
|
||||
// Farbpropagierung bei neuem Input
|
||||
if (connectionType) {
|
||||
await propagateInputColor(App.connectionEquipmentId, connectionType, color);
|
||||
}
|
||||
} else {
|
||||
newConn.fk_source = App.connectionEquipmentId;
|
||||
App.outputs.push(newConn);
|
||||
|
|
@ -2865,6 +2892,10 @@
|
|||
if (App.connectionDirection === 'input') {
|
||||
newConn.fk_target = App.connectionEquipmentId;
|
||||
App.inputs.push(newConn);
|
||||
// Farbpropagierung bei neuem Input (offline)
|
||||
if (connectionType) {
|
||||
propagateInputColor(App.connectionEquipmentId, connectionType, color);
|
||||
}
|
||||
} else {
|
||||
newConn.fk_source = App.connectionEquipmentId;
|
||||
App.outputs.push(newConn);
|
||||
|
|
@ -2877,6 +2908,60 @@
|
|||
App.editConnectionId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbpropagierung: Wenn Input-Phase gesetzt, Farbe auf Outputs übertragen
|
||||
* @param {number} equipmentId - Equipment-ID
|
||||
* @param {string} phase - Phase (L1, L2, L3, N, PE)
|
||||
* @param {string} color - Farbe der Einspeisung
|
||||
*/
|
||||
async function propagateInputColor(equipmentId, phase, color) {
|
||||
if (!equipmentId || !phase || !color) return;
|
||||
|
||||
// Finde alle Outputs dieses Equipment
|
||||
const outputs = App.outputs.filter(o => o.fk_source == equipmentId);
|
||||
if (outputs.length === 0) return;
|
||||
|
||||
// Update Outputs mit passender Phase
|
||||
for (const output of outputs) {
|
||||
// Phase-Matching: L1 -> L1, LN, L1N; L2 -> L2; etc.
|
||||
const outputType = output.connection_type || '';
|
||||
let matches = false;
|
||||
|
||||
if (phase === 'L1' && (outputType.includes('L1') || outputType === 'LN')) matches = true;
|
||||
else if (phase === 'L2' && outputType.includes('L2')) matches = true;
|
||||
else if (phase === 'L3' && outputType.includes('L3')) matches = true;
|
||||
else if (phase === 'N' && outputType.includes('N')) matches = true;
|
||||
else if (phase === 'PE' && outputType.includes('PE')) matches = true;
|
||||
|
||||
if (matches && output.color !== color) {
|
||||
output.color = color;
|
||||
|
||||
// Backend aktualisieren
|
||||
if (App.isOnline) {
|
||||
try {
|
||||
await apiCall('ajax/pwa_api.php', {
|
||||
action: 'update_connection',
|
||||
connection_id: output.id,
|
||||
color: color
|
||||
});
|
||||
} catch (err) {
|
||||
queueOfflineAction({
|
||||
action: 'update_connection',
|
||||
connection_id: output.id,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
} else {
|
||||
queueOfflineAction({
|
||||
action: 'update_connection',
|
||||
connection_id: output.id,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection löschen (mit Bestätigung)
|
||||
*/
|
||||
|
|
|
|||
4
sw.js
4
sw.js
|
|
@ -3,8 +3,8 @@
|
|||
* Offline-First für Schaltschrank-Dokumentation
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v11.6';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v11.6';
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v11.7';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v11.7';
|
||||
|
||||
// Statische Assets die immer gecached werden (ohne Query-String)
|
||||
const STATIC_ASSETS = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue