diff --git a/js/kundenkarte.js b/js/kundenkarte.js
old mode 100755
new mode 100644
index 2dc0493..dcc383c
--- a/js/kundenkarte.js
+++ b/js/kundenkarte.js
@@ -464,7 +464,7 @@
// Get tooltip data from data attribute (faster than AJAX)
var tooltipDataStr = $item.attr('data-tooltip');
if (!tooltipDataStr) {
- console.log('No tooltip data for anlage', anlageId);
+ // No tooltip data available - silently return
return;
}
@@ -770,7 +770,7 @@
var field = data.fields[key];
// Handle header fields as section titles (must span both grid columns)
if (field.type === 'header') {
- html += '' + this.escapeHtml(field.label) + '';
+ html += '' + this.escapeHtml(field.label) + '';
} else if (field.value) {
html += '' + this.escapeHtml(field.label) + ':';
html += '' + this.escapeHtml(field.value) + '';
@@ -3836,9 +3836,11 @@
'L3': '#808080', // Gray
'N': '#0066cc', // Blue
'PE': '#27ae60', // Green-Yellow
- 'L1N': '#3498db', // Combined
+ 'LN': '#8B4513', // Single phase output (brown - phase determined by busbar)
+ 'L1N': '#3498db', // L1 + N input (legacy/busbar)
'3P': '#2c3e50', // 3-phase
- '3P+N': '#34495e' // 3-phase + neutral
+ '3P+N': '#34495e', // 3-phase + neutral
+ 'DATA': '#9b59b6' // Data cable (purple)
},
// State
@@ -3850,11 +3852,19 @@
equipment: [],
dragState: null,
selectedConnection: null,
+ DEBUG: false, // Set to true to enable debug logging
+
+ // Conditional logging
+ log: function() {
+ if (this.DEBUG && console && console.log) {
+ console.log.apply(console, arguments);
+ }
+ },
init: function(anlageId) {
- console.log('ConnectionEditor.init called with anlageId:', anlageId);
+ this.log('ConnectionEditor.init called with anlageId:', anlageId);
if (!anlageId) {
- console.log('ConnectionEditor: No anlageId, aborting');
+ this.log('ConnectionEditor: No anlageId, aborting');
return;
}
this.currentAnlageId = anlageId;
@@ -3862,13 +3872,13 @@
// Restore expanded state from localStorage
var savedState = localStorage.getItem('kundenkarte_connection_editor_expanded');
this.isExpanded = savedState === 'true';
- console.log('ConnectionEditor: isExpanded =', this.isExpanded);
+ this.log('ConnectionEditor: isExpanded =', this.isExpanded);
this.bindEvents();
- console.log('ConnectionEditor: Events bound');
+ this.log('ConnectionEditor: Events bound');
this.renderAllEditors();
- console.log('ConnectionEditor: Editors rendered');
+ this.log('ConnectionEditor: Editors rendered');
},
bindEvents: function() {
@@ -5138,17 +5148,19 @@
// Constants (40% larger for better visibility)
TE_WIDTH: 56,
RAIL_HEIGHT: 14, // Hutschiene Höhe
- RAIL_SPACING: 308, // Abstand zwischen Hutschienen (Platz für Blöcke + Verbindungen)
+ RAIL_SPACING: 380, // Abstand zwischen Hutschienen (erhöht für Routing-Zone)
BLOCK_HEIGHT: 112,
TERMINAL_RADIUS: 8,
GRID_SIZE: 14,
MIN_TOP_MARGIN: 100, // Minimaler Platz oben für Panel (inkl. Hauptsammelschienen)
MAIN_BUSBAR_HEIGHT: 60, // Höhe des Hauptsammelschienen-Bereichs (L1,L2,L3,N,PE)
BLOCK_INNER_OFFSET: 80, // Zusätzlicher Offset für Blöcke innerhalb des Panels
- BOTTOM_MARGIN: 80, // Basis-Platz unten (wird dynamisch erweitert)
+ BOTTOM_MARGIN: 100, // Platz unten für Abgänge + Routing
PANEL_GAP: 56, // Abstand zwischen Panels
PANEL_PADDING: 28, // Innenabstand Panel
CONNECTION_ROUTE_SPACE: 17, // Platz pro Verbindungsroute
+ OUTPUT_ZONE_HEIGHT: 130, // Platz für Abgänge (lineLength max 120 + Label)
+ ROUTING_ZONE_HEIGHT: 60, // Dedizierte Zone für Leitungsführung zwischen Carriern
// Colors
COLORS: {
@@ -5170,7 +5182,10 @@
'L2': '#1a1a1a',
'L3': '#666666',
'N': '#0066cc',
- 'PE': '#27ae60'
+ 'PE': '#27ae60',
+ 'LN': '#8B4513', // Single phase output (brown)
+ '3P+N': '#34495e', // 3-phase + neutral
+ 'DATA': '#9b59b6' // Data cable (purple)
},
// State
@@ -5187,13 +5202,202 @@
svgElement: null,
scale: 1,
+ // Performance: Index Maps für O(1) Lookups statt O(n) find()
+ _equipmentById: {},
+ _carrierById: {},
+ _connectionById: {},
+ DEBUG: false, // Set to true to enable console.log
+
// Manual wire drawing mode
wireDrawMode: false,
wireDrawPoints: [], // Array of {x, y} points for current wire
wireDrawSourceEq: null, // Source equipment ID
wireDrawSourceTerm: null, // Source terminal ID
+ wireDrawFromJunction: false, // Drawing from junction on existing wire
+ wireDrawJunctionConn: null, // Connection ID we're branching from
+ wireDrawJunctionPoint: null, // {x, y} junction point on existing wire
+ wireExtendMode: false, // Extending an existing connection
+ wireExtendConnId: null, // Connection ID being extended
+ wireExtendExistingPoints: [], // Existing path points of connection being extended
WIRE_GRID_SIZE: 25, // Snap grid size in pixels (larger = fewer points)
+ // Display settings (persisted in localStorage)
+ displaySettings: {
+ phaseColors: true, // Color wires by phase (L1=brown, L2=black, L3=gray, N=blue, PE=green)
+ wireWidth: 'normal', // 'thin', 'normal', 'thick'
+ showJunctions: true, // Show dots at wire junctions
+ terminalStyle: 'circle' // 'circle', 'square', 'ring'
+ },
+
+ // Load display settings from localStorage
+ loadDisplaySettings: function() {
+ try {
+ var saved = localStorage.getItem('kundenkarte_schematic_settings');
+ if (saved) {
+ var parsed = JSON.parse(saved);
+ this.displaySettings = Object.assign({}, this.displaySettings, parsed);
+ }
+ } catch (e) {
+ console.warn('Could not load schematic settings:', e);
+ }
+ },
+
+ // Save display settings to localStorage
+ saveDisplaySettings: function() {
+ try {
+ localStorage.setItem('kundenkarte_schematic_settings', JSON.stringify(this.displaySettings));
+ } catch (e) {
+ console.warn('Could not save schematic settings:', e);
+ }
+ },
+
+ // Get wire width in pixels
+ getWireWidth: function() {
+ switch (this.displaySettings.wireWidth) {
+ case 'thin': return 2;
+ case 'thick': return 5;
+ default: return 3.5;
+ }
+ },
+
+ // Get wire color based on phase (if phaseColors enabled)
+ getWireColor: function(connectionType) {
+ if (!this.displaySettings.phaseColors) {
+ return '#f1c40f'; // Default yellow
+ }
+ // Extract phase from connection type
+ var phase = connectionType ? connectionType.toUpperCase() : '';
+ if (phase.indexOf('L1') !== -1) return '#8B4513'; // Brown
+ if (phase.indexOf('L2') !== -1) return '#1a1a1a'; // Black
+ if (phase.indexOf('L3') !== -1) return '#808080'; // Gray
+ if (phase.indexOf('PE') !== -1) return '#00aa00'; // Green
+ if (phase.indexOf('N') !== -1) return '#0066cc'; // Blue
+ if (phase === '3P' || phase === 'L1L2L3') return '#9b59b6'; // Purple for 3-phase
+ return '#f1c40f'; // Default yellow
+ },
+
+ // Get terminal shape SVG based on style setting
+ getTerminalShape: function(radius, fillColor) {
+ var style = this.displaySettings.terminalStyle || 'circle';
+ // Add invisible hit area (larger than visible terminal) for easier clicking
+ var hitAreaRadius = radius * 1.4; // 11px hit area - prevents overlap on dense terminals
+ var hitArea = '';
+
+ switch (style) {
+ case 'ring':
+ // Hollow circle
+ return hitArea + '';
+ case 'dot':
+ // Smaller solid dot
+ return hitArea + '';
+ case 'square':
+ // Square shape
+ var size = radius * 1.5;
+ return hitArea + '';
+ case 'circle':
+ default:
+ // Default filled circle
+ return hitArea + '';
+ }
+ },
+
+ // Show settings dialog
+ showSettingsDialog: function() {
+ var self = this;
+
+ // Remove existing dialog
+ $('.schematic-settings-dialog').remove();
+
+ var html = '
';
+
+ html += '
';
+ html += '
Anzeigeoptionen
';
+ html += '';
+ html += '
';
+
+ // Phase colors
+ html += '
';
+ html += '';
+ html += '
';
+
+ // Junction dots
+ html += '
';
+ html += '';
+ html += '
';
+
+ // Wire width
+ html += '
';
+ html += '';
+ html += '
';
+ ['thin', 'normal', 'thick'].forEach(function(w) {
+ var labels = { thin: 'Dünn', normal: 'Normal', thick: 'Dick' };
+ var checked = self.displaySettings.wireWidth === w ? 'checked' : '';
+ html += '';
+ });
+ html += '
';
+ html += '
';
+
+ // Terminal style
+ html += '
';
+ html += '';
+ html += '
';
+ ['circle', 'square', 'ring'].forEach(function(s) {
+ var labels = { circle: '● Kreis', square: '■ Quadrat', ring: '○ Ring' };
+ var checked = self.displaySettings.terminalStyle === s ? 'checked' : '';
+ html += '';
+ });
+ html += '
';
+ html += '
';
+
+ // Buttons
+ html += '
';
+ html += '';
+ html += '
';
+
+ html += '
';
+
+ // Overlay
+ html += '';
+
+ $('body').append(html);
+
+ // Event handlers
+ $('.settings-close, .schematic-settings-overlay').on('click', function() {
+ $('.schematic-settings-dialog, .schematic-settings-overlay').remove();
+ });
+
+ $('.settings-apply').on('click', function() {
+ // Collect settings
+ self.displaySettings.phaseColors = $('.setting-phase-colors').is(':checked');
+ self.displaySettings.showJunctions = $('.setting-junctions').is(':checked');
+ self.displaySettings.wireWidth = $('input[name="wire-width"]:checked').val() || 'normal';
+ self.displaySettings.terminalStyle = $('input[name="terminal-style"]:checked').val() || 'circle';
+
+ // Save and apply
+ self.saveDisplaySettings();
+ self.render();
+
+ // Close dialog
+ $('.schematic-settings-dialog, .schematic-settings-overlay').remove();
+ self.showMessage('Einstellungen gespeichert', 'success');
+ });
+ },
+
// Default terminal configs for common types
// Terminals are bidirectional - no strict input/output
DEFAULT_TERMINALS: {
@@ -5205,12 +5409,49 @@
'LS3P': { terminals: [{id: 't1', label: 'L1', pos: 'top'}, {id: 't2', label: 'L2', pos: 'top'}, {id: 't3', label: 'L3', pos: 'top'}, {id: 't4', label: 'L1', pos: 'bottom'}, {id: 't5', label: 'L2', pos: 'bottom'}, {id: 't6', label: 'L3', pos: 'bottom'}] }
},
+ // Performance: Build index maps for O(1) lookups
+ buildIndexes: function() {
+ var self = this;
+ this._equipmentById = {};
+ this._carrierById = {};
+ this._connectionById = {};
+
+ this.equipment.forEach(function(eq) {
+ self._equipmentById[eq.id] = eq;
+ });
+ this.carriers.forEach(function(c) {
+ self._carrierById[c.id] = c;
+ });
+ this.connections.forEach(function(conn) {
+ self._connectionById[conn.id] = conn;
+ });
+ },
+
+ // O(1) lookup helpers
+ getEquipmentById: function(id) {
+ return id ? this._equipmentById[id] : null;
+ },
+ getCarrierById: function(id) {
+ return id ? this._carrierById[id] : null;
+ },
+ getConnectionById: function(id) {
+ return id ? this._connectionById[id] : null;
+ },
+
+ // Conditional logging (only when DEBUG is true)
+ log: function() {
+ if (this.DEBUG && console && console.log) {
+ console.log.apply(console, arguments);
+ }
+ },
+
init: function(anlageId) {
if (!anlageId) {
console.error('SchematicEditor.init: No anlageId provided!');
return;
}
this.anlageId = anlageId;
+ this.loadDisplaySettings();
this.bindEvents();
this.loadData();
},
@@ -5222,9 +5463,11 @@
$(document).off('click.terminal').on('click.terminal', '.schematic-terminal', function(e) {
e.preventDefault();
e.stopPropagation();
+ var $clicked = $(this);
+ console.log('Terminal clicked:', $clicked.data('equipment-id'), $clicked.data('terminal-id'), 'element:', this);
// Only handle in manual wire draw mode
if (self.wireDrawMode) {
- self.handleTerminalClick($(this));
+ self.handleTerminalClick($clicked);
}
});
@@ -5308,6 +5551,12 @@
});
});
+ // Straighten connections (remove diagonals, orthogonal only)
+ $(document).off('click.straightenConns').on('click.straightenConns', '.schematic-straighten-connections', function(e) {
+ e.preventDefault();
+ self.straightenConnections();
+ });
+
// BOM (Bill of Materials) / Stückliste
$(document).off('click.bomGenerate').on('click.bomGenerate', '.schematic-bom-generate', function(e) {
e.preventDefault();
@@ -5349,6 +5598,12 @@
self.zoomToFit();
});
+ // Settings button
+ $(document).off('click.schematicSettings').on('click.schematicSettings', '.schematic-settings-btn', function(e) {
+ e.preventDefault();
+ self.showSettingsDialog();
+ });
+
// ======================================
// Keyboard Shortcuts
// ======================================
@@ -5645,7 +5900,7 @@
self.wireDrawPoints.push({x: snapped.x, y: snapped.y});
self.updateWirePreview();
- console.log('Wire point added:', snapped.x, snapped.y, 'Total points:', self.wireDrawPoints.length);
+ self.log('Wire point added:', snapped.x, snapped.y, 'Total points:', self.wireDrawPoints.length);
});
// SVG mousemove for wire preview - show cursor and preview line
@@ -5669,15 +5924,15 @@
$(document).off('contextmenu.wireDraw').on('contextmenu.wireDraw', '.schematic-editor-canvas svg', function(e) {
if (!self.wireDrawMode) return;
e.preventDefault();
- if (self.wireDrawSourceEq) {
- // Cancel entire drawing
+ if (self.wireDrawSourceEq || self.wireDrawFromJunction) {
+ // Cancel entire drawing (from terminal or junction)
self.cancelWireDrawing();
}
});
// Escape to cancel wire drawing
$(document).off('keydown.wireDraw').on('keydown.wireDraw', function(e) {
- if (e.key === 'Escape' && self.wireDrawMode && self.wireDrawSourceEq) {
+ if (e.key === 'Escape' && self.wireDrawMode && (self.wireDrawSourceEq || self.wireDrawFromJunction)) {
self.cancelWireDrawing();
}
});
@@ -6286,6 +6541,9 @@
var checkComplete = function() {
if (connectionsLoaded && bridgesLoaded) {
+ // Build index maps for O(1) lookups (performance optimization)
+ self.buildIndexes();
+
// Initialize canvas now that all data is loaded
if (!self.isInitialized) {
self.initCanvas();
@@ -6294,6 +6552,12 @@
}
// Reset loading flag
self.isLoading = false;
+
+ // Refresh wire grid if wire draw mode is active
+ // (equipment positions may have changed after render)
+ if (self.wireDrawMode) {
+ self.showWireGrid();
+ }
}
};
@@ -6346,18 +6610,12 @@
calculateLayout: function() {
var self = this;
- // Calculate dynamic TOP_MARGIN based on number of connections going up
- // Count connections that route above blocks (top-to-top connections)
- var topConnections = 0;
- this.connections.forEach(function(conn) {
- if (parseInt(conn.is_rail) !== 1 && conn.fk_source && conn.fk_target) {
- topConnections++;
- }
- });
- // Panel top margin (where the panel border starts)
+ // Use FIXED top margin to prevent layout shifts when connections change
+ // This ensures path_data coordinates remain valid after saving
+ // Allocate space for up to 10 routing lanes (enough for most schematics)
+ var maxRoutingLanes = 10;
this.panelTopMargin = this.MIN_TOP_MARGIN;
- // Block top margin = panel margin + inner offset + connection routing space
- this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + topConnections * this.CONNECTION_ROUTE_SPACE;
+ this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + maxRoutingLanes * this.CONNECTION_ROUTE_SPACE;
// Calculate canvas size based on panels
// Panels nebeneinander, Hutschienen pro Panel untereinander
@@ -6654,12 +6912,13 @@
var terminalHtml = '';
this.equipment.forEach(function(eq) {
- var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); });
+ // Use index lookup O(1) instead of find() O(n)
+ var carrier = self.getCarrierById(eq.carrier_id);
if (!carrier) {
return;
}
if (typeof carrier._x === 'undefined' || carrier._x === null) {
- console.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping');
+ self.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping');
return;
}
@@ -6830,7 +7089,7 @@
terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" ';
terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">';
- terminalHtml += '';
+ terminalHtml += self.getTerminalShape(self.TERMINAL_RADIUS, terminalColor);
// Label - position depends on whether there are stacked terminals
var labelText = self.escapeHtml(term.label);
@@ -6874,7 +7133,7 @@
terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" ';
terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">';
- terminalHtml += '';
+ terminalHtml += self.getTerminalShape(self.TERMINAL_RADIUS, terminalColor);
// Label - position depends on whether there are stacked terminals
var labelText = self.escapeHtml(term.label);
@@ -7010,13 +7269,11 @@
return;
}
- // Get carrier for this busbar
- var carrier = self.carriers.find(function(c) {
- return String(c.id) === String(conn.fk_carrier);
- });
+ // Get carrier for this busbar (O(1) lookup)
+ var carrier = self.getCarrierById(conn.fk_carrier);
if (!carrier || typeof carrier._x === 'undefined') {
- console.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position');
+ self.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position');
return;
}
@@ -7151,7 +7408,7 @@
html += '';
renderedCount++;
- console.log(' Busbar #' + conn.id + ': Rendered from TE ' + startTE + ' to ' + endTE + ' on carrier ' + carrier.id +
+ self.log(' Busbar #' + conn.id + ': Rendered from TE ' + startTE + ' to ' + endTE + ' on carrier ' + carrier.id +
', x=' + startX + ', y=' + busbarY + ', width=' + width + ', color=' + color);
});
@@ -7166,25 +7423,33 @@
var html = '';
var renderedCount = 0;
+ // Get display settings
+ var wireWidth = this.getWireWidth();
+ var shadowWidth = wireWidth + 4;
+ var hoverWidth = wireWidth + 1.5;
+
this.connections.forEach(function(conn, connIndex) {
// Check is_rail as integer (PHP may return string "1" or "0")
if (parseInt(conn.is_rail) === 1) {
return;
}
- var sourceEq = conn.fk_source ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_source); }) : null;
- var targetEq = conn.fk_target ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_target); }) : null;
+ // Use index lookup O(1) instead of find() O(n)
+ var sourceEq = self.getEquipmentById(conn.fk_source);
+ var targetEq = self.getEquipmentById(conn.fk_target);
- var color = conn.color || self.PHASE_COLORS[conn.connection_type] || self.COLORS.connection;
+ var color = conn.color || self.getWireColor(conn.connection_type);
// ========================================
// ABGANG (Output) - source exists, no target
// Direction depends on terminal: top terminal = line goes UP, bottom terminal = line goes DOWN
+ // BUT: If path_data exists, render as normal wire with that path (junction to another wire)
// ========================================
- if (sourceEq && !conn.fk_target) {
+ if (sourceEq && !conn.fk_target && !conn.path_data) {
var sourceTerminals = self.getTerminals(sourceEq);
var sourceTermId = conn.source_terminal_id || 't2';
var sourcePos = self.getTerminalPosition(sourceEq, sourceTermId, sourceTerminals);
+ var isBundled = conn.bundled_terminals === 'all';
if (!sourcePos) {
// Fallback: center bottom of equipment
@@ -7195,6 +7460,26 @@
};
}
+ // For bundled terminals: calculate span across all terminals with same position
+ var bundleWidth = 0;
+ var bundleCenterX = sourcePos.x;
+ if (isBundled && sourceTerminals && sourceTerminals.length > 1) {
+ // Find all terminals with same position (top/bottom)
+ var samePosTerm = sourceTerminals.filter(function(t) {
+ return t.pos === (sourcePos.isTop ? 'top' : 'bottom');
+ });
+ if (samePosTerm.length > 1) {
+ var positions = samePosTerm.map(function(t) {
+ var pos = self.getTerminalPosition(sourceEq, t.id, sourceTerminals);
+ return pos ? pos.x : bundleCenterX;
+ });
+ var minX = Math.min.apply(null, positions);
+ var maxX = Math.max.apply(null, positions);
+ bundleWidth = maxX - minX;
+ bundleCenterX = (minX + maxX) / 2;
+ }
+ }
+
// Calculate line length based on label text
var labelText = conn.output_label || '';
var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || '');
@@ -7206,26 +7491,39 @@
var startY = sourcePos.y;
var endY = goingUp ? (startY - lineLength) : (startY + lineLength);
- // Draw vertical line
- var path = 'M ' + sourcePos.x + ' ' + startY + ' L ' + sourcePos.x + ' ' + endY;
+ // Use bundle center for bundled connections
+ var lineX = isBundled && bundleWidth > 0 ? bundleCenterX : sourcePos.x;
- html += '';
+ // Draw vertical line
+ var path = 'M ' + lineX + ' ' + startY + ' L ' + lineX + ' ' + endY;
+
+ html += '';
+
+ // For bundled: draw horizontal bar connecting all terminals
+ if (isBundled && bundleWidth > 0) {
+ var barY = startY + (goingUp ? -5 : 5);
+ html += '';
+ }
// Invisible hit area for clicking
var hitY = goingUp ? endY : startY;
- html += '';
+ var hitWidth = isBundled && bundleWidth > 0 ? Math.max(40, bundleWidth + 20) : 40;
+ html += '';
// Connection line
html += '';
+ html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
// Arrow at end (pointing away from equipment)
+ var arrowSize = isBundled && bundleWidth > 0 ? 8 : 5;
if (goingUp) {
// Arrow pointing UP
- html += '';
+ html += '';
} else {
// Arrow pointing DOWN
- html += '';
+ html += '';
}
// Labels - vertical text on both sides
@@ -7233,9 +7531,9 @@
// Left side: Bezeichnung (output_label)
if (conn.output_label) {
- html += '';
+ html += 'transform="rotate(-90 ' + (lineX - 10) + ' ' + labelY + ')">';
html += self.escapeHtml(conn.output_label);
html += '';
}
@@ -7245,9 +7543,9 @@
if (conn.medium_type) cableInfo = conn.medium_type;
if (conn.medium_spec) cableInfo += ' ' + conn.medium_spec;
if (cableInfo) {
- html += '';
+ html += 'transform="rotate(-90 ' + (lineX + 10) + ' ' + labelY + ')">';
html += self.escapeHtml(cableInfo.trim());
html += '';
}
@@ -7255,7 +7553,7 @@
// Phase type at end of line
if (conn.connection_type) {
var phaseY = goingUp ? (endY - 10) : (endY + 14);
- html += '';
html += conn.connection_type;
html += '';
@@ -7267,7 +7565,67 @@
}
// ========================================
- // ANSCHLUSSPUNKT (Input) - no source, target exists
+ // JUNCTION CONNECTION - branch from existing wire
+ // No source equipment but has path_data (manually drawn from junction point)
+ // ========================================
+ if (!conn.fk_source && targetEq && conn.path_data) {
+ var targetTerminals = self.getTerminals(targetEq);
+ var targetTermId = conn.target_terminal_id || 't1';
+ var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals);
+ if (!targetPos) return;
+
+ // Use the manually drawn path
+ var path = conn.path_data;
+
+ // Junction connections use phase colors
+ var junctionColor = color;
+
+ // Extract first point from path for junction marker
+ var pathMatch = path.match(/M\s*([\d.-]+)\s*([\d.-]+)/);
+ var junctionX = pathMatch ? parseFloat(pathMatch[1]) : 0;
+ var junctionY = pathMatch ? parseFloat(pathMatch[2]) : 0;
+
+ html += '';
+
+ html += '';
+ html += '';
+
+ html += '';
+
+ // Junction marker (dot at start point)
+ if (self.displaySettings.showJunctions) {
+ html += '';
+ }
+
+ // Label if present
+ if (conn.output_label) {
+ var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120);
+ var labelHeight = 22;
+
+ // Find safe position that doesn't overlap equipment
+ var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight);
+ var labelX = labelPos ? labelPos.x : (junctionX + targetPos.x) / 2;
+ var labelY = labelPos ? labelPos.y : (junctionY + targetPos.y) / 2;
+
+ // Badge with solid background and border for visibility
+ // Text always white for readability (e.g. L2 black wire)
+ html += '';
+ html += '';
+ html += self.escapeHtml(conn.output_label);
+ html += '';
+ }
+
+ html += '';
+ renderedCount++;
+ return;
+ }
+
+ // ========================================
+ // ANSCHLUSSPUNKT (Input) - no source, target exists, NO path_data
// Draw a line coming FROM ABOVE into the terminal
// All Anschlusspunkte use a uniform color (light blue)
// ========================================
@@ -7295,7 +7653,7 @@
// Connection line
html += '';
+ html += 'fill="none" stroke="' + inputColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
// Circle at top (external source indicator)
html += '';
@@ -7324,6 +7682,53 @@
return;
}
+ // ========================================
+ // LEITUNG ZU LEITUNG - source exists, no target, but has path_data
+ // Wire ends at another wire (junction), rendered with stored path
+ // ========================================
+ if (sourceEq && !conn.fk_target && conn.path_data) {
+ // Use the manually drawn path
+ var path = conn.path_data;
+
+ // Extract last point for junction marker
+ var pathPoints = path.match(/[ML]\s*([\d.-]+)\s*([\d.-]+)/g);
+ var lastMatch = pathPoints && pathPoints.length > 0 ? pathPoints[pathPoints.length - 1].match(/([\d.-]+)\s*([\d.-]+)/) : null;
+ var junctionX = lastMatch ? parseFloat(lastMatch[1]) : 0;
+ var junctionY = lastMatch ? parseFloat(lastMatch[2]) : 0;
+
+ html += '';
+
+ html += '';
+ html += '';
+
+ html += '';
+
+ // Junction marker (dot at end point where it connects to other wire)
+ html += '';
+
+ // Label if present
+ if (conn.output_label) {
+ var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120);
+ var labelHeight = 22;
+ var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight);
+ var labelX = labelPos ? labelPos.x : junctionX;
+ var labelY = labelPos ? labelPos.y : junctionY - 20;
+
+ html += '';
+ html += '';
+ html += self.escapeHtml(conn.output_label);
+ html += '';
+ }
+
+ html += '';
+ renderedCount++;
+ return;
+ }
+
// ========================================
// NORMAL CONNECTION - both source and target exist
// ========================================
@@ -7351,30 +7756,52 @@
path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq);
}
- html += '';
+ html += '';
html += '';
html += '';
html += '';
+ html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
if (conn.output_label) {
- var labelX = (sourcePos.x + targetPos.x) / 2;
- var labelY = sourcePos.isTop ? sourcePos.y - 70 - routeOffset : sourcePos.y + 70 + routeOffset;
+ // Calculate label dimensions
+ var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120);
+ var labelHeight = 22;
- var labelWidth = Math.min(conn.output_label.length * 8 + 14, 112);
- html += '';
- html += '';
+ // Find safe label position that doesn't overlap equipment
+ var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight);
+ if (!labelPos) {
+ labelPos = self.getPathMidpoint(path);
+ }
+ var labelX = labelPos ? labelPos.x : (sourcePos.x + targetPos.x) / 2;
+ var labelY = labelPos ? labelPos.y : (sourcePos.y + targetPos.y) / 2;
+
+ // Badge with solid background and border for visibility
+ // Text always white for readability (e.g. L2 black wire)
+ html += '';
+ html += '';
html += self.escapeHtml(conn.output_label);
html += '';
}
if (conn.connection_type && !conn.output_label) {
- var typeX = (sourcePos.x + targetPos.x) / 2;
- var typeY = (sourcePos.y + targetPos.y) / 2;
- html += '';
+ // Also check for safe position for type labels
+ var typeWidth = conn.connection_type.length * 9 + 14;
+ var typeHeight = 18;
+ var typePos = self.findSafeLabelPosition(path, typeWidth, typeHeight);
+ var typeX = typePos ? typePos.x : (sourcePos.x + targetPos.x) / 2;
+ var typeY = typePos ? typePos.y : (sourcePos.y + targetPos.y) / 2;
+
+ // Badge background for type label too
+ // Text always white for readability
+ html += '';
+ html += '';
html += conn.connection_type;
html += '';
}
@@ -7397,21 +7824,78 @@
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
+
+ // If we're in wire draw mode with a source terminal selected,
+ // create a junction: connection ends at the click point on the wire
+ if (self.wireDrawMode && self.wireDrawSourceEq && connId) {
+ var conn = self.connections.find(function(c) { return c.id == connId; });
+ if (conn) {
+ // Get click position relative to SVG
+ var $svg = $(e.target).closest('svg');
+ var svgRect = $svg[0].getBoundingClientRect();
+ var rawClickX = e.clientX - svgRect.left;
+ var rawClickY = e.clientY - svgRect.top;
+
+ // Snap click point to the nearest point on the wire
+ var snappedPoint = self.snapToWirePath(conn.path_data, rawClickX, rawClickY);
+ var clickX = Math.round(snappedPoint.x);
+ var clickY = Math.round(snappedPoint.y);
+
+ var sourceEqId = self.wireDrawSourceEq;
+ var sourceTermId = self.wireDrawSourceTerm;
+
+ // Build path: source terminal → existing points → click point on wire
+ var pathPoints = self.wireDrawPoints.slice();
+
+ // Add click point with orthogonal correction
+ if (pathPoints.length > 0) {
+ var lastPt = pathPoints[pathPoints.length - 1];
+ if (lastPt.x !== clickX && lastPt.y !== clickY) {
+ // Insert intermediate point for 90° bend
+ pathPoints.push({x: lastPt.x, y: clickY});
+ }
+ }
+ pathPoints.push({x: clickX, y: clickY});
+
+ // Remove redundant points (points on same line as neighbors)
+ pathPoints = self.cleanupPathPoints(pathPoints);
+
+ // Build path string
+ var pathData = '';
+ for (var i = 0; i < pathPoints.length; i++) {
+ var pt = pathPoints[i];
+ pathData += (i === 0 ? 'M ' : 'L ') + pt.x + ' ' + pt.y + ' ';
+ }
+
+ // Store connection values to inherit from clicked wire
+ self._inheritFromConnection = conn;
+ self._pendingPathData = pathData.trim() || null;
+
+ // Clear source terminal but keep draw mode active
+ self.cleanupWireDrawState(true);
+
+ // Leitung endet an der angeklickten Leitung (kein Ziel-Terminal)
+ self.showConnectionLabelDialog(sourceEqId, sourceTermId, null, null);
+ return;
+ }
+ }
+
+ // Normal click: show popup
if (connId) {
self.showConnectionPopup(connId, e.clientX, e.clientY);
}
});
this.addEventListener('mouseenter', function() {
- $visiblePath.attr('stroke-width', '5');
+ $visiblePath.attr('stroke-width', hoverWidth);
});
this.addEventListener('mouseleave', function() {
- $visiblePath.attr('stroke-width', '3.5');
+ $visiblePath.attr('stroke-width', wireWidth);
});
this.style.cursor = 'pointer';
});
- // Abgang (Output) groups - click to edit
+ // Abgang (Output) groups - click to edit or junction
$layer.find('.schematic-output-group').each(function() {
var $group = $(this);
var connId = $group.data('connection-id');
@@ -7420,25 +7904,142 @@
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
+
+ // If in wire draw mode with source terminal, create junction at click point
+ if (self.wireDrawMode && self.wireDrawSourceEq && connId) {
+ var conn = self.connections.find(function(c) { return c.id == connId; });
+ if (conn) {
+ // Get click position relative to SVG
+ var $svg = $(e.target).closest('svg');
+ var svgRect = $svg[0].getBoundingClientRect();
+ var rawClickX = e.clientX - svgRect.left;
+ var rawClickY = e.clientY - svgRect.top;
+
+ // Abgänge are vertical lines - snap X to the line's X coordinate
+ // Get source equipment position for the output
+ var outputEq = self.equipment.find(function(eq) { return eq.id == conn.fk_source; });
+ var clickX, clickY;
+ if (outputEq && conn.source_terminal_id) {
+ var terminals = self.getTerminals(outputEq);
+ var termPos = self.getTerminalPosition(outputEq, conn.source_terminal_id, terminals);
+ if (termPos) {
+ // Snap X to terminal X (vertical line), keep Y from click
+ clickX = Math.round(termPos.x);
+ clickY = Math.round(rawClickY);
+ } else {
+ clickX = Math.round(rawClickX);
+ clickY = Math.round(rawClickY);
+ }
+ } else {
+ clickX = Math.round(rawClickX);
+ clickY = Math.round(rawClickY);
+ }
+
+ var sourceEqId = self.wireDrawSourceEq;
+ var sourceTermId = self.wireDrawSourceTerm;
+
+ // Build path ending at click point
+ var pathPoints = self.wireDrawPoints.slice();
+ if (pathPoints.length > 0) {
+ var lastPt = pathPoints[pathPoints.length - 1];
+ if (lastPt.x !== clickX && lastPt.y !== clickY) {
+ pathPoints.push({x: lastPt.x, y: clickY});
+ }
+ }
+ pathPoints.push({x: clickX, y: clickY});
+
+ // Remove redundant points
+ pathPoints = self.cleanupPathPoints(pathPoints);
+
+ var pathData = '';
+ for (var i = 0; i < pathPoints.length; i++) {
+ var pt = pathPoints[i];
+ pathData += (i === 0 ? 'M ' : 'L ') + pt.x + ' ' + pt.y + ' ';
+ }
+
+ self._inheritFromConnection = conn;
+ self._pendingPathData = pathData.trim() || null;
+ self.cleanupWireDrawState(true);
+ // Leitung endet an Abgang
+ self.showConnectionLabelDialog(sourceEqId, sourceTermId, null, null);
+ return;
+ }
+ }
+
if (connId) {
self.showConnectionPopup(connId, e.clientX, e.clientY);
}
});
this.addEventListener('mouseenter', function() {
- $visiblePath.attr('stroke-width', '5');
+ $visiblePath.attr('stroke-width', hoverWidth);
});
this.addEventListener('mouseleave', function() {
- $visiblePath.attr('stroke-width', '3');
+ $visiblePath.attr('stroke-width', wireWidth);
});
});
- // Anschlusspunkt (Input) groups - click to edit
+ // Anschlusspunkt (Input) groups - click to edit or junction
$layer.find('.schematic-input-group').each(function() {
var $group = $(this);
var connId = $group.data('connection-id');
var $visiblePath = $group.find('.schematic-connection');
+ this.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // If in wire draw mode with source terminal, connect to input's target
+ if (self.wireDrawMode && self.wireDrawSourceEq && connId) {
+ var conn = self.connections.find(function(c) { return c.id == connId; });
+ if (conn && conn.fk_target) {
+ var sourceEqId = self.wireDrawSourceEq;
+ var sourceTermId = self.wireDrawSourceTerm;
+ var targetEqId = conn.fk_target;
+ var targetTermId = conn.target_terminal_id || 'input';
+
+ // Build path with existing points
+ var pathPoints = self.wireDrawPoints.slice();
+ var targetEq = self.equipment.find(function(eq) { return eq.id == targetEqId; });
+ if (targetEq) {
+ var terminals = self.getTerminals(targetEq);
+ var targetPos = self.getTerminalPosition(targetEq, targetTermId, terminals);
+ if (targetPos) {
+ pathPoints.push({x: targetPos.x, y: targetPos.y});
+ }
+ }
+ var pathData = '';
+ for (var i = 0; i < pathPoints.length; i++) {
+ pathData += (i === 0 ? 'M' : 'L') + ' ' + pathPoints[i].x + ' ' + pathPoints[i].y + ' ';
+ }
+
+ self._inheritFromConnection = conn;
+ self._pendingPathData = pathData.trim() || null;
+ self.cleanupWireDrawState(true);
+ self.showConnectionLabelDialog(sourceEqId, sourceTermId, targetEqId, targetTermId);
+ return;
+ }
+ }
+
+ if (connId) {
+ self.showConnectionPopup(connId, e.clientX, e.clientY);
+ }
+ });
+
+ this.addEventListener('mouseenter', function() {
+ $visiblePath.attr('stroke-width', hoverWidth);
+ });
+ this.addEventListener('mouseleave', function() {
+ $visiblePath.attr('stroke-width', wireWidth);
+ });
+ });
+
+ // Junction (Branch) groups - click to edit
+ $layer.find('.schematic-junction-group').each(function() {
+ var $group = $(this);
+ var connId = $group.data('connection-id');
+ var $visiblePath = $group.find('.schematic-connection');
+
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
@@ -7448,11 +8049,12 @@
});
this.addEventListener('mouseenter', function() {
- $visiblePath.attr('stroke-width', '5');
+ $visiblePath.attr('stroke-width', hoverWidth);
});
this.addEventListener('mouseleave', function() {
- $visiblePath.attr('stroke-width', '3');
+ $visiblePath.attr('stroke-width', wireWidth);
});
+ this.style.cursor = 'pointer';
});
},
@@ -7589,7 +8191,7 @@
var $addBusbar = $('');
$controls.append($addBusbar);
} else {
- console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position');
+ self.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position');
}
});
@@ -8572,6 +9174,12 @@
return this.DEFAULT_TERMINALS['LS'].terminals;
},
+ // Get total terminal count for equipment (used for bundled terminals feature)
+ getTerminalCount: function(eq) {
+ var terminals = this.getTerminals(eq);
+ return terminals ? terminals.length : 2;
+ },
+
getTerminalPosition: function(eq, terminalId, terminals) {
// Find the terminal
var terminal = null;
@@ -8628,10 +9236,13 @@
var widthTE = parseFloat(eq.width_te) || 1;
// Terminal im festen TE-Raster platzieren
- // Jeder Terminal belegt 1 TE - Index bestimmt welches TE
- var teIndex = termIndex % widthTE;
+ // Use terminal.col if defined, otherwise fall back to index
+ var teIndex = terminal.col !== undefined ? terminal.col : (termIndex % widthTE);
var x = eq._x + (teIndex * this.TE_WIDTH) + (this.TE_WIDTH / 2);
- var y = isTop ? (eq._y - 5) : (eq._y + eq._height + 5);
+
+ // For stacked terminals, add row offset
+ var rowOffset = terminal.row !== undefined ? terminal.row * 14 : 0;
+ var y = isTop ? (eq._y - 5 - rowOffset) : (eq._y + eq._height + 5 + rowOffset);
return { x: x, y: y, isTop: isTop };
},
@@ -8657,6 +9268,270 @@
return this.createSimpleRoute(x1, y1, x2, y2, source.isTop, target.isTop, routeOffset);
},
+ // Remove redundant points that lie on the same line as their neighbors
+ cleanupPathPoints: function(points) {
+ if (points.length < 3) return points;
+
+ var cleaned = [points[0]];
+
+ for (var i = 1; i < points.length - 1; i++) {
+ var prev = cleaned[cleaned.length - 1];
+ var curr = points[i];
+ var next = points[i + 1];
+
+ // Check if curr is on the same line as prev and next
+ var sameHorizontal = (prev.y === curr.y && curr.y === next.y);
+ var sameVertical = (prev.x === curr.x && curr.x === next.x);
+
+ // Only keep point if it's a corner (direction change)
+ if (!sameHorizontal && !sameVertical) {
+ cleaned.push(curr);
+ }
+ }
+
+ // Always add the last point
+ cleaned.push(points[points.length - 1]);
+
+ return cleaned;
+ },
+
+ // Snap a point to the nearest point on a wire path
+ snapToWirePath: function(pathData, clickX, clickY) {
+ if (!pathData) return {x: clickX, y: clickY};
+
+ // Parse path to get points
+ var points = [];
+ var commands = pathData.match(/[MLHVZ][^MLHVZ]*/gi);
+ if (!commands) return {x: clickX, y: clickY};
+
+ var currentX = 0, currentY = 0;
+ commands.forEach(function(cmd) {
+ var type = cmd[0].toUpperCase();
+ var coords = cmd.slice(1).trim().split(/[\s,]+/).map(parseFloat);
+ if (type === 'M' || type === 'L') {
+ currentX = coords[0];
+ currentY = coords[1];
+ points.push({x: currentX, y: currentY});
+ } else if (type === 'H') {
+ currentX = coords[0];
+ points.push({x: currentX, y: currentY});
+ } else if (type === 'V') {
+ currentY = coords[0];
+ points.push({x: currentX, y: currentY});
+ }
+ });
+
+ if (points.length < 2) return {x: clickX, y: clickY};
+
+ // Find nearest point on any segment
+ var nearestPoint = {x: clickX, y: clickY};
+ var minDist = Infinity;
+
+ for (var i = 1; i < points.length; i++) {
+ var p1 = points[i - 1];
+ var p2 = points[i];
+
+ // Project click point onto line segment
+ var dx = p2.x - p1.x;
+ var dy = p2.y - p1.y;
+ var lenSq = dx * dx + dy * dy;
+
+ var t = 0;
+ if (lenSq > 0) {
+ t = ((clickX - p1.x) * dx + (clickY - p1.y) * dy) / lenSq;
+ t = Math.max(0, Math.min(1, t)); // Clamp to segment
+ }
+
+ var projX = p1.x + t * dx;
+ var projY = p1.y + t * dy;
+
+ var dist = Math.sqrt((clickX - projX) * (clickX - projX) + (clickY - projY) * (clickY - projY));
+
+ if (dist < minDist) {
+ minDist = dist;
+ nearestPoint = {x: projX, y: projY};
+ }
+ }
+
+ return nearestPoint;
+ },
+
+ // Get the midpoint of an SVG path for label positioning
+ getPathMidpoint: function(pathData) {
+ if (!pathData) return null;
+
+ // Parse path commands (M, L, H, V)
+ var points = [];
+ var currentX = 0, currentY = 0;
+
+ // Match all path commands
+ var commands = pathData.match(/[MLHVZ][^MLHVZ]*/gi);
+ if (!commands) return null;
+
+ commands.forEach(function(cmd) {
+ var type = cmd[0].toUpperCase();
+ var coords = cmd.slice(1).trim().split(/[\s,]+/).map(parseFloat);
+
+ if (type === 'M' || type === 'L') {
+ currentX = coords[0];
+ currentY = coords[1];
+ points.push({x: currentX, y: currentY});
+ } else if (type === 'H') {
+ currentX = coords[0];
+ points.push({x: currentX, y: currentY});
+ } else if (type === 'V') {
+ currentY = coords[0];
+ points.push({x: currentX, y: currentY});
+ }
+ });
+
+ if (points.length < 2) return null;
+
+ // Calculate total path length
+ var totalLength = 0;
+ var segments = [];
+ for (var i = 1; i < points.length; i++) {
+ var dx = points[i].x - points[i-1].x;
+ var dy = points[i].y - points[i-1].y;
+ var len = Math.sqrt(dx*dx + dy*dy);
+ segments.push({start: points[i-1], end: points[i], length: len});
+ totalLength += len;
+ }
+
+ // Find the point at half the total length
+ var halfLength = totalLength / 2;
+ var accumulated = 0;
+
+ for (var j = 0; j < segments.length; j++) {
+ var seg = segments[j];
+ if (accumulated + seg.length >= halfLength) {
+ // Midpoint is in this segment
+ var t = (halfLength - accumulated) / seg.length;
+ return {
+ x: seg.start.x + t * (seg.end.x - seg.start.x),
+ y: seg.start.y + t * (seg.end.y - seg.start.y)
+ };
+ }
+ accumulated += seg.length;
+ }
+
+ // Fallback: return geometric center
+ return {
+ x: (points[0].x + points[points.length-1].x) / 2,
+ y: (points[0].y + points[points.length-1].y) / 2
+ };
+ },
+
+ // Check if a label rectangle overlaps with any equipment block
+ labelOverlapsEquipment: function(labelX, labelY, labelWidth, labelHeight) {
+ var self = this;
+ var padding = 5; // Safety margin
+
+ // Label bounding box
+ var lx1 = labelX - labelWidth / 2 - padding;
+ var ly1 = labelY - labelHeight / 2 - padding;
+ var lx2 = labelX + labelWidth / 2 + padding;
+ var ly2 = labelY + labelHeight / 2 + padding;
+
+ for (var i = 0; i < this.equipment.length; i++) {
+ var eq = this.equipment[i];
+ if (typeof eq._x === 'undefined' || typeof eq._y === 'undefined') continue;
+
+ var eqWidth = (parseFloat(eq.width_te) || 1) * this.TE_WIDTH;
+ var eqHeight = eq._height || this.BLOCK_HEIGHT;
+
+ // Equipment bounding box
+ var ex1 = eq._x;
+ var ey1 = eq._y;
+ var ex2 = eq._x + eqWidth;
+ var ey2 = eq._y + eqHeight;
+
+ // Check for overlap
+ if (!(lx2 < ex1 || lx1 > ex2 || ly2 < ey1 || ly1 > ey2)) {
+ return eq; // Return the overlapping equipment
+ }
+ }
+ return null; // No overlap
+ },
+
+ // Find a safe label position along a path that doesn't overlap equipment
+ findSafeLabelPosition: function(pathData, labelWidth, labelHeight) {
+ var self = this;
+ if (!pathData) return null;
+
+ // Parse path to get all points
+ var points = [];
+ var commands = pathData.match(/[ML]\s*[\d.-]+\s*[\d.-]+/gi);
+ if (!commands) return null;
+
+ commands.forEach(function(cmd) {
+ var coords = cmd.match(/[\d.-]+/g);
+ if (coords && coords.length >= 2) {
+ points.push({ x: parseFloat(coords[0]), y: parseFloat(coords[1]) });
+ }
+ });
+
+ if (points.length < 2) return null;
+
+ // Try positions along the path (at 10%, 30%, 50%, 70%, 90%)
+ var percentages = [0.5, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9];
+
+ // Calculate total path length and segment lengths
+ var totalLength = 0;
+ var segments = [];
+ for (var i = 1; i < points.length; i++) {
+ var dx = points[i].x - points[i-1].x;
+ var dy = points[i].y - points[i-1].y;
+ var len = Math.sqrt(dx*dx + dy*dy);
+ segments.push({ start: points[i-1], end: points[i], length: len });
+ totalLength += len;
+ }
+
+ // Try each percentage
+ for (var p = 0; p < percentages.length; p++) {
+ var targetLength = totalLength * percentages[p];
+ var accumulated = 0;
+
+ for (var j = 0; j < segments.length; j++) {
+ var seg = segments[j];
+ if (accumulated + seg.length >= targetLength) {
+ var t = (targetLength - accumulated) / seg.length;
+ var testX = seg.start.x + t * (seg.end.x - seg.start.x);
+ var testY = seg.start.y + t * (seg.end.y - seg.start.y);
+
+ // Check if this position is safe
+ if (!this.labelOverlapsEquipment(testX, testY, labelWidth, labelHeight)) {
+ return { x: testX, y: testY };
+ }
+ break;
+ }
+ accumulated += seg.length;
+ }
+ }
+
+ // All positions overlap - try to find position outside equipment
+ // Return the midpoint but offset away from nearest equipment
+ var midPos = this.getPathMidpoint(pathData);
+ if (!midPos) return null;
+
+ var overlappingEq = this.labelOverlapsEquipment(midPos.x, midPos.y, labelWidth, labelHeight);
+ if (overlappingEq) {
+ // Move label away from the equipment
+ var eqCenterY = overlappingEq._y + (overlappingEq._height || this.BLOCK_HEIGHT) / 2;
+
+ // Move above or below the equipment
+ if (midPos.y < eqCenterY) {
+ // Label is above center, move it further up
+ midPos.y = overlappingEq._y - labelHeight / 2 - 10;
+ } else {
+ // Label is below center, move it further down
+ midPos.y = overlappingEq._y + (overlappingEq._height || this.BLOCK_HEIGHT) + labelHeight / 2 + 10;
+ }
+ }
+
+ return midPos;
+ },
+
// Pathfinding-based routing with obstacle avoidance and connection spreading
// Improved version using A* with better orthogonal routing
createPathfindingRoute: function(x1, y1, x2, y2, connIndex) {
@@ -8901,10 +9776,20 @@
handleTerminalClick: function($terminal) {
var eqId = $terminal.data('equipment-id');
var termId = $terminal.data('terminal-id');
+ this.log('handleTerminalClick called - eqId:', eqId, 'termId:', termId);
+ this.log('State: wireDrawMode:', this.wireDrawMode, 'wireExtendMode:', this.wireExtendMode, 'wireDrawSourceEq:', this.wireDrawSourceEq);
// Only works in manual wire draw mode
if (!this.wireDrawMode) return;
+ // In extend mode, ANY terminal click finishes the drawing
+ // (user wants to extend the wire to this terminal)
+ if (this.wireExtendMode) {
+ this.log('Extend mode: finishing wire to terminal', eqId, termId);
+ this.finishWireDrawing(eqId, termId);
+ return;
+ }
+
if (!this.wireDrawSourceEq) {
// First terminal - start drawing
this.wireDrawSourceEq = eqId;
@@ -8922,10 +9807,13 @@
}
$terminal.find('.schematic-terminal-circle').attr('stroke', '#ff0').attr('stroke-width', '3');
- this.showMessage('Rasterpunkte klicken, Rechtsklick = Abbruch, dann Ziel-Terminal klicken', 'info');
+ this.showMessage('Rasterpunkte klicken, Rechtsklick/ESC = Abbruch, gleiches Terminal = Abbruch', 'info');
this.showWireGrid();
- } else if (eqId !== this.wireDrawSourceEq) {
- // Second terminal - finish and save
+ } else if (eqId === this.wireDrawSourceEq && termId === this.wireDrawSourceTerm) {
+ // Same terminal clicked again - cancel drawing
+ this.cancelWireDrawing();
+ } else {
+ // Different terminal - finish and save
this.finishWireDrawing(eqId, termId);
}
},
@@ -8937,10 +9825,12 @@
if (this.wireDrawMode) {
$btn.addClass('active').css('background', '#27ae60');
- this.showMessage('Manueller Zeichenmodus: Klicken Sie auf ein START-Terminal (roter Kreis)', 'info');
+ this.showMessage('Manueller Zeichenmodus: Klicke Terminal oder Leitung für Startpunkt', 'info');
this.showWireGrid();
// Highlight all terminals to show they are clickable
- $(this.svgElement).find('.schematic-terminal-circle').css('cursor', 'crosshair');
+ $(this.svgElement).find('.schematic-terminal').css('cursor', 'crosshair');
+ // Also highlight connections as clickable for junction
+ $(this.svgElement).find('.schematic-connection-hitarea').css('cursor', 'crosshair');
} else {
$btn.removeClass('active').css('background', '');
this.cancelWireDrawing();
@@ -8949,10 +9839,329 @@
}
},
+ // Start drawing from a junction point on an existing connection
+ startJunctionDraw: function(connId, event) {
+ var self = this;
+ var conn = this.connections.find(function(c) { return String(c.id) === String(connId); });
+ if (!conn) return;
+
+ // Get click position in SVG coordinates
+ var svg = this.svgElement;
+ var pt = svg.createSVGPoint();
+ pt.x = event.clientX;
+ pt.y = event.clientY;
+ var svgPt = pt.matrixTransform(svg.getScreenCTM().inverse());
+
+ // Find nearest point on the connection path
+ var junctionPoint = this.findNearestPointOnConnection(conn, svgPt.x, svgPt.y);
+ if (!junctionPoint) {
+ this.showMessage('Konnte Abzweigpunkt nicht berechnen', 'error');
+ return;
+ }
+
+ // Store junction info
+ this.wireDrawFromJunction = true;
+ this.wireDrawJunctionConn = connId;
+ this.wireDrawJunctionPoint = junctionPoint;
+ this.wireDrawPoints = [{x: junctionPoint.x, y: junctionPoint.y}];
+
+ // Visual feedback - draw a marker at junction point
+ this.showJunctionMarker(junctionPoint);
+
+ this.showMessage('Abzweigung von Leitung: Rasterpunkte klicken, dann Ziel-Terminal', 'info');
+ },
+
+ // Finish drawing a wire that ends at a junction on an existing connection
+ // (Terminal -> Wire junction)
+ finishWireToJunction: function(connId, event) {
+ var self = this;
+ var conn = this.connections.find(function(c) { return String(c.id) === String(connId); });
+ if (!conn) return;
+
+ // Get click position in SVG coordinates
+ var svg = this.svgElement;
+ var pt = svg.createSVGPoint();
+ pt.x = event.clientX;
+ pt.y = event.clientY;
+ var svgPt = pt.matrixTransform(svg.getScreenCTM().inverse());
+
+ // Find nearest point on the connection path
+ var junctionPoint = this.findNearestPointOnConnection(conn, svgPt.x, svgPt.y);
+ if (!junctionPoint) {
+ this.showMessage('Konnte Verbindungspunkt nicht berechnen', 'error');
+ return;
+ }
+
+ // Add junction point to path
+ this.wireDrawPoints.push({x: junctionPoint.x, y: junctionPoint.y});
+
+ // Build path string from points
+ var pathData = '';
+ for (var i = 0; i < this.wireDrawPoints.length; i++) {
+ pathData += (i === 0 ? 'M' : 'L') + ' ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y + ' ';
+ }
+
+ // Show dialog to configure the connection
+ // Note: cleanup is done inside the dialog handlers (save/cancel)
+ this.showTerminalToJunctionDialog(
+ this.wireDrawSourceEq,
+ this.wireDrawSourceTerm,
+ connId,
+ junctionPoint,
+ pathData.trim()
+ );
+ },
+
+ // Dialog for creating a connection from terminal to junction on existing wire
+ showTerminalToJunctionDialog: function(sourceEqId, sourceTermId, targetConnId, junctionPoint, pathData) {
+ var self = this;
+
+ // Remove any existing dialog first
+ $('#schematic-conn-dialog').remove();
+
+ var sourceEq = this.equipment.find(function(e) { return String(e.id) === String(sourceEqId); });
+ var targetConn = this.connections.find(function(c) { return String(c.id) === String(targetConnId); });
+
+ var targetLabel = targetConn ? (targetConn.output_label || targetConn.connection_type || 'Leitung') : 'Leitung';
+
+ var html = '
';
+ html += '
';
+ html += '
Verbindung zur Leitung
';
+ html += '×
';
+ html += '
';
+
+ html += '
';
+ html += '' + this.escapeHtml((sourceEq ? sourceEq.label || sourceEq.type_label_short : 'Gerät')) + '';
+ html += ' ';
+ html += ' ' + this.escapeHtml(targetLabel) + '';
+ html += '
';
+
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ html += '
';
+ html += '';
+ html += '
';
+
+ var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P'];
+ typePresets.forEach(function(t) {
+ var color = self.PHASE_COLORS[t] || self.COLORS.connection;
+ html += '';
+ });
+
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ html += '
';
+ html += '';
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+ html += '
';
+
+ // Bundled terminals checkbox - only show if source equipment has >1 terminal
+ var sourceTerminalCount = sourceEq ? this.getTerminalCount(sourceEq) : 1;
+ if (sourceTerminalCount > 1) {
+ html += '
';
+ html += '';
+ html += '
';
+ }
+
+ html += '
';
+ html += '
';
+
+ $('body').append(html);
+
+ // Type preset buttons
+ $('.conn-type-btn').on('click', function() {
+ $('.conn-type-btn').css('outline', 'none');
+ $(this).css('outline', '2px solid #fff');
+ $('#conn-type').val($(this).data('type'));
+ $('#conn-color').val($(this).data('color'));
+ });
+
+ $('#conn-save').on('click', function() {
+ var label = $('#conn-label').val().trim();
+ var connType = $('#conn-type').val();
+ var color = $('#conn-color').val();
+ var medium = $('#conn-medium').val();
+ var length = $('#conn-length').val();
+ var bundleAll = $('#conn-bundle-all').is(':checked');
+
+ // Create connection: terminal -> junction on wire
+ self.saveJunctionConnection({
+ anlage_id: self.anlageId,
+ fk_source: sourceEqId,
+ source_terminal_id: sourceTermId,
+ fk_target: null, // No target equipment
+ target_terminal_id: null,
+ output_label: label,
+ connection_type: connType,
+ color: color,
+ medium_type: medium,
+ medium_length: length,
+ path_data: pathData,
+ bundled_terminals: bundleAll ? 'all' : '',
+ junction_connection_id: targetConnId,
+ junction_x: junctionPoint.x,
+ junction_y: junctionPoint.y
+ });
+
+ $('#schematic-conn-dialog').remove();
+ // Cleanup after dialog closes - keep draw mode active
+ self.cleanupWireDrawState(true);
+ });
+
+ $('#conn-cancel, .kundenkarte-modal-close').on('click', function() {
+ $('#schematic-conn-dialog').remove();
+ // Cleanup after dialog closes - keep draw mode active
+ self.cleanupWireDrawState(true);
+ });
+ },
+
+ // Find nearest point on a connection path
+ findNearestPointOnConnection: function(conn, clickX, clickY) {
+ var pathData = conn.path_data;
+ if (!pathData) {
+ // No manual path - calculate from terminals
+ var sourceEq = conn.fk_source ? this.equipment.find(function(e) { return String(e.id) === String(conn.fk_source); }) : null;
+ var targetEq = conn.fk_target ? this.equipment.find(function(e) { return String(e.id) === String(conn.fk_target); }) : null;
+
+ // For outputs (no target), create path from source going down/up
+ if (sourceEq && !targetEq) {
+ var sourceTerminals = this.getTerminals(sourceEq);
+ var sourcePos = this.getTerminalPosition(sourceEq, conn.source_terminal_id || 't2', sourceTerminals);
+ if (!sourcePos) return null;
+
+ // Create a simple vertical path for output
+ var direction = sourcePos.position === 'top' ? -1 : 1;
+ var endY = sourcePos.y + (direction * 60);
+ pathData = 'M ' + sourcePos.x + ' ' + sourcePos.y + ' L ' + sourcePos.x + ' ' + endY;
+ } else if (sourceEq && targetEq) {
+ var sourceTerminals = this.getTerminals(sourceEq);
+ var targetTerminals = this.getTerminals(targetEq);
+ var sourcePos = this.getTerminalPosition(sourceEq, conn.source_terminal_id || 't2', sourceTerminals);
+ var targetPos = this.getTerminalPosition(targetEq, conn.target_terminal_id || 't1', targetTerminals);
+ if (!sourcePos || !targetPos) return null;
+
+ pathData = this.createOrthogonalPath(sourcePos, targetPos, 0, sourceEq, targetEq);
+ } else {
+ return null;
+ }
+ }
+
+ // Parse path data to get segments
+ var points = this.parsePathToPoints(pathData);
+ if (points.length < 2) return null;
+
+ // Find nearest point on any segment
+ var nearestPoint = null;
+ var minDist = Infinity;
+
+ for (var i = 0; i < points.length - 1; i++) {
+ var p1 = points[i];
+ var p2 = points[i + 1];
+ var closest = this.nearestPointOnSegment(p1, p2, clickX, clickY);
+ var dist = Math.sqrt(Math.pow(closest.x - clickX, 2) + Math.pow(closest.y - clickY, 2));
+
+ if (dist < minDist) {
+ minDist = dist;
+ nearestPoint = closest;
+ }
+ }
+
+ // Snap to grid
+ if (nearestPoint) {
+ var gridSize = 5;
+ nearestPoint.x = Math.round(nearestPoint.x / gridSize) * gridSize;
+ nearestPoint.y = Math.round(nearestPoint.y / gridSize) * gridSize;
+ }
+
+ return nearestPoint;
+ },
+
+ // Parse SVG path data to array of points
+ parsePathToPoints: function(pathData) {
+ var points = [];
+ var commands = pathData.match(/[ML]\s*[\d.-]+\s*[\d.-]+/gi) || [];
+
+ commands.forEach(function(cmd) {
+ var parts = cmd.match(/[\d.-]+/g);
+ if (parts && parts.length >= 2) {
+ points.push({
+ x: parseFloat(parts[0]),
+ y: parseFloat(parts[1])
+ });
+ }
+ });
+
+ return points;
+ },
+
+ // Find nearest point on a line segment
+ nearestPointOnSegment: function(p1, p2, px, py) {
+ var dx = p2.x - p1.x;
+ var dy = p2.y - p1.y;
+ var lengthSq = dx * dx + dy * dy;
+
+ if (lengthSq === 0) return {x: p1.x, y: p1.y};
+
+ var t = Math.max(0, Math.min(1, ((px - p1.x) * dx + (py - p1.y) * dy) / lengthSq));
+
+ return {
+ x: p1.x + t * dx,
+ y: p1.y + t * dy
+ };
+ },
+
+ // Show visual marker at junction point
+ showJunctionMarker: function(point) {
+ var svgNS = 'http://www.w3.org/2000/svg';
+ var existing = this.svgElement.querySelector('.junction-marker');
+ if (existing) existing.remove();
+
+ var marker = document.createElementNS(svgNS, 'g');
+ marker.setAttribute('class', 'junction-marker');
+
+ // Pulsing circle
+ var circle = document.createElementNS(svgNS, 'circle');
+ circle.setAttribute('cx', point.x);
+ circle.setAttribute('cy', point.y);
+ circle.setAttribute('r', '8');
+ circle.setAttribute('fill', '#f39c12');
+ circle.setAttribute('stroke', '#fff');
+ circle.setAttribute('stroke-width', '2');
+ circle.style.animation = 'junctionPulse 1s infinite';
+ marker.appendChild(circle);
+
+ // Add pulse animation style if not exists (opacity + stroke only - NO scale to avoid position shift)
+ if (!document.getElementById('junction-pulse-style')) {
+ var style = document.createElement('style');
+ style.id = 'junction-pulse-style';
+ style.textContent = '@keyframes junctionPulse { 0%, 100% { opacity: 1; stroke-width: 2px; } 50% { opacity: 0.6; stroke-width: 4px; } }';
+ document.head.appendChild(style);
+ }
+
+ this.svgElement.appendChild(marker);
+ },
+
showWireGrid: function() {
- console.log('showWireGrid called, svgElement:', this.svgElement);
+ this.log('showWireGrid called');
if (!this.svgElement) {
- console.error('No SVG element found!');
+ this.log('No SVG element found!');
return;
}
@@ -9027,44 +10236,59 @@
}
// Routing-Bereich ober- und unterhalb der Blöcke
- this.carriers.forEach(function(carrier) {
+ // Sortiere Carriers nach Y-Position für Zwischen-Berechnung
+ var sortedCarriers = this.carriers.slice().sort(function(a, b) {
+ return (a._y || 0) - (b._y || 0);
+ });
+
+ sortedCarriers.forEach(function(carrier, carrierIdx) {
if (typeof carrier._y === 'undefined') return;
var railCenterY = carrier._y + self.RAIL_HEIGHT / 2;
var blockTop = railCenterY - self.BLOCK_HEIGHT / 2;
var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2;
- // Raster oberhalb (Sammelschienen-Bereich)
- for (var offsetY = 20; offsetY < 150; offsetY += 15) {
+ // Raster oberhalb (Sammelschienen-Bereich) - erweitert für Outputs oben
+ for (var offsetY = 20; offsetY < self.OUTPUT_ZONE_HEIGHT + self.ROUTING_ZONE_HEIGHT; offsetY += 15) {
var routeY = blockTop - offsetY;
if (routeY > 20 && yPositions.indexOf(routeY) === -1) yPositions.push(routeY);
}
- // Raster unterhalb (Abgang-Bereich)
- for (var offsetY2 = 20; offsetY2 < 150; offsetY2 += 15) {
+
+ // Raster unterhalb (Abgang-Bereich) - erweitert für Outputs unten
+ for (var offsetY2 = 20; offsetY2 < self.OUTPUT_ZONE_HEIGHT + self.ROUTING_ZONE_HEIGHT; offsetY2 += 15) {
var routeY2 = blockBottom + offsetY2;
if (routeY2 < svgHeight - 20 && yPositions.indexOf(routeY2) === -1) yPositions.push(routeY2);
}
+
+ // Routing-Zone ZWISCHEN zwei aufeinanderfolgenden Carriern
+ if (carrierIdx < sortedCarriers.length - 1) {
+ var nextCarrier = sortedCarriers[carrierIdx + 1];
+ if (typeof nextCarrier._y !== 'undefined') {
+ var nextBlockTop = nextCarrier._y + self.RAIL_HEIGHT / 2 - self.BLOCK_HEIGHT / 2;
+ var routingZoneStart = blockBottom + 10;
+ var routingZoneEnd = nextBlockTop - 10;
+
+ // Fülle die Zone zwischen den Carriern mit Grid-Punkten
+ for (var zoneY = routingZoneStart; zoneY < routingZoneEnd; zoneY += 15) {
+ if (yPositions.indexOf(zoneY) === -1) yPositions.push(zoneY);
+ }
+ }
+ }
});
// Sort positions
xPositions.sort(function(a, b) { return a - b; });
yPositions.sort(function(a, b) { return a - b; });
- // Draw grid points at terminal-aligned positions
+ // PERFORMANCE: Only store snap points, don't create visual DOM elements
+ // This reduces from potentially 5000+ DOM elements to just a data array
xPositions.forEach(function(x) {
yPositions.forEach(function(y) {
- var circle = document.createElementNS(svgNS, 'circle');
- circle.setAttribute('cx', x);
- circle.setAttribute('cy', y);
- circle.setAttribute('r', '1.5');
- circle.setAttribute('fill', '#888');
- circle.setAttribute('opacity', '0.4');
- gridLayer.appendChild(circle);
self.wireGridPoints.push({x: x, y: y});
pointCount++;
});
});
- console.log('Wire grid: ' + xPositions.length + ' x-positions, ' + yPositions.length + ' y-positions');
+ self.log('Wire grid: ' + xPositions.length + ' x-positions, ' + yPositions.length + ' y-positions = ' + pointCount + ' snap points');
// Create magnetic cursor indicator (shows nearest grid point)
var magnetCursor = document.createElementNS(svgNS, 'circle');
@@ -9077,9 +10301,14 @@
magnetCursor.setAttribute('pointer-events', 'none');
gridLayer.appendChild(magnetCursor);
- // Append grid layer at the end of SVG (on top of everything)
- this.svgElement.appendChild(gridLayer);
- console.log('Wire grid created with ' + pointCount + ' points');
+ // Insert grid layer BEFORE terminals layer so terminals remain clickable
+ var terminalsLayer = this.svgElement.querySelector('.schematic-terminals-layer');
+ if (terminalsLayer) {
+ this.svgElement.insertBefore(gridLayer, terminalsLayer);
+ } else {
+ this.svgElement.appendChild(gridLayer);
+ }
+ self.log('Wire grid created with ' + pointCount + ' snap points (no DOM elements)');
// Setup magnetic snap behavior
this.setupMagneticSnap();
@@ -9209,7 +10438,7 @@
$(this.svgElement).off('mousemove.magneticSnap');
this.magnetSnappedPos = null;
this.wireGridPoints = null;
- console.log('Wire grid hidden');
+ this.log('Wire grid hidden');
},
updateWirePreview: function() {
@@ -9237,7 +10466,7 @@
d += ' L ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y;
}
preview.setAttribute('d', d);
- console.log('Wire preview updated:', d);
+ this.log('Wire preview updated:', d);
},
updateWirePreviewCursor: function(x, y) {
@@ -9292,22 +10521,14 @@
},
cancelWireDrawing: function() {
- this.wireDrawSourceEq = null;
- this.wireDrawSourceTerm = null;
- this.wireDrawPoints = [];
-
- // Remove all preview elements using native DOM
- var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line');
- elements.forEach(function(el) { el.remove(); });
-
- // Reset terminal highlight
- $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', '');
-
+ this.cleanupWireDrawState();
this.showMessage('Zeichnung abgebrochen', 'info');
},
finishWireDrawing: function(targetEqId, targetTermId) {
var self = this;
+ this.log('finishWireDrawing called - targetEqId:', targetEqId, 'targetTermId:', targetTermId);
+ this.log('State: wireExtendMode:', this.wireExtendMode, 'wireExtendConnId:', this.wireExtendConnId);
// Get target terminal position
var eq = this.equipment.find(function(e) { return String(e.id) === String(targetEqId); });
@@ -9315,51 +10536,249 @@
var terminals = this.getTerminals(eq);
var termPos = this.getTerminalPosition(eq, targetTermId, terminals);
if (termPos) {
- // Add orthogonal bend if needed before target
- if (this.wireDrawPoints.length > 0) {
- var lastPt = this.wireDrawPoints[this.wireDrawPoints.length - 1];
- // If not aligned, add intermediate point for 90° bend
- if (lastPt.x !== termPos.x && lastPt.y !== termPos.y) {
- // Go vertical first, then horizontal
- this.wireDrawPoints.push({x: lastPt.x, y: termPos.y});
- }
- }
this.wireDrawPoints.push({x: termPos.x, y: termPos.y});
}
}
- // Build orthogonal path - ensure all segments are horizontal or vertical
- var pathData = '';
- for (var i = 0; i < this.wireDrawPoints.length; i++) {
- var pt = this.wireDrawPoints[i];
- if (i === 0) {
- pathData += 'M ' + pt.x + ' ' + pt.y + ' ';
- } else {
- var prevPt = this.wireDrawPoints[i - 1];
- // If diagonal, add intermediate point
- if (prevPt.x !== pt.x && prevPt.y !== pt.y) {
- pathData += 'L ' + prevPt.x + ' ' + pt.y + ' ';
- }
- pathData += 'L ' + pt.x + ' ' + pt.y + ' ';
+ // Handle EXTEND mode - update existing connection
+ if (this.wireExtendMode && this.wireExtendConnId) {
+ // Combine existing points with new points (skip first point of new since it's the join point)
+ var allPoints = this.wireExtendExistingPoints.concat(this.wireDrawPoints.slice(1));
+
+ // Build combined path
+ var pathData = '';
+ for (var i = 0; i < allPoints.length; i++) {
+ pathData += (i === 0 ? 'M' : 'L') + ' ' + allPoints[i].x + ' ' + allPoints[i].y + ' ';
}
+
+ // Update the existing connection with new path and target
+ this.updateExtendedConnection(this.wireExtendConnId, targetEqId, targetTermId, pathData.trim());
+ // Extend mode: don't keep draw mode
+ this.cleanupWireDrawState(false);
+ return;
}
- // Show connection dialog to set labels and save
- this.showConnectionLabelDialogWithPath(
- this.wireDrawSourceEq,
- this.wireDrawSourceTerm,
- targetEqId,
- targetTermId,
- pathData.trim()
- );
+ // Build path string from points
+ var pathData = '';
+ for (var i = 0; i < this.wireDrawPoints.length; i++) {
+ pathData += (i === 0 ? 'M' : 'L') + ' ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y + ' ';
+ }
- // Cleanup
+ // Handle junction source vs terminal source
+ if (this.wireDrawFromJunction) {
+ // Junction source - inherit from source connection
+ this._inheritFromConnection = this.wireDrawJunctionConn;
+ this._pendingPathData = pathData.trim();
+ // Junction connections have no source equipment (fk_source = NULL)
+ this.showConnectionLabelDialog(null, null, targetEqId, targetTermId);
+ } else {
+ // Normal terminal source
+ this._pendingPathData = pathData.trim();
+ this.showConnectionLabelDialog(
+ this.wireDrawSourceEq,
+ this.wireDrawSourceTerm,
+ targetEqId,
+ targetTermId
+ );
+ }
+
+ // Cleanup - keep draw mode active for drawing multiple connections
+ this.cleanupWireDrawState(true);
+ },
+
+ cleanupWireDrawState: function(keepDrawMode) {
this.wireDrawSourceEq = null;
this.wireDrawSourceTerm = null;
+ this.wireDrawFromJunction = false;
+ this.wireDrawJunctionConn = null;
+ this.wireDrawJunctionPoint = null;
this.wireDrawPoints = [];
- var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line');
+ // Extend mode cleanup
+ this.wireExtendMode = false;
+ this.wireExtendConnId = null;
+ this.wireExtendExistingPoints = [];
+
+ // Remove preview elements
+ var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line, .junction-marker, .extend-start-marker');
elements.forEach(function(el) { el.remove(); });
- $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', '');
+
+ // Reset wire draw mode only if not keeping it
+ if (!keepDrawMode) {
+ this.wireDrawMode = false;
+ var $btn = $('.schematic-wire-draw-toggle');
+ $btn.removeClass('active').css('background', '');
+ $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2');
+ $(this.svgElement).find('.schematic-terminal').css('cursor', '');
+ $(this.svgElement).find('.schematic-connection-hitarea').css('cursor', '');
+ } else {
+ // Keep draw mode active - just reset terminal highlights
+ $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2');
+ this.showMessage('Zeichenmodus aktiv - nächste Verbindung zeichnen', 'info');
+ }
+ },
+
+ // Update an existing connection with extended path
+ updateExtendedConnection: function(connId, newTargetEqId, newTargetTermId, newPathData) {
+ var self = this;
+
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php',
+ method: 'POST',
+ data: {
+ action: 'update',
+ connection_id: connId,
+ fk_target: newTargetEqId,
+ target_terminal_id: newTargetTermId,
+ path_data: newPathData,
+ token: $('input[name="token"]').val()
+ },
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ self.showMessage('Leitung verlängert', 'success');
+ self.loadConnections();
+ } else {
+ self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error');
+ }
+ },
+ error: function() {
+ self.showMessage('Netzwerkfehler', 'error');
+ }
+ });
+ },
+
+ // Dialog for creating a connection from a junction point
+ showJunctionConnectionDialog: function(sourceConnId, junctionPoint, targetEqId, targetTermId, pathData) {
+ var self = this;
+
+ // Get source connection info for context
+ var sourceConn = this.connections.find(function(c) { return String(c.id) === String(sourceConnId); });
+ var targetEq = this.equipment.find(function(e) { return String(e.id) === String(targetEqId); });
+
+ var sourceLabel = sourceConn ? (sourceConn.output_label || sourceConn.connection_type || 'Leitung') : 'Leitung';
+
+ var html = '
';
+ html += '
';
+ html += '
Abzweigung erstellen
';
+ html += '×
';
+ html += '
';
+
+ html += '
';
+ html += ' Abzweig von: ' + this.escapeHtml(sourceLabel) + '';
+ html += ' ';
+ html += '' + this.escapeHtml((targetEq ? targetEq.label || targetEq.type_label_short : 'Gerät')) + '';
+ html += '
';
+
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ html += '
';
+ html += '';
+ html += '
';
+
+ var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P'];
+ // Pre-select the same type as source connection
+ var defaultType = sourceConn ? sourceConn.connection_type : 'L1N';
+
+ typePresets.forEach(function(t) {
+ var color = self.PHASE_COLORS[t] || self.COLORS.connection;
+ var selected = (t === defaultType) ? 'outline:2px solid #fff;' : '';
+ html += '';
+ });
+
+ html += '
';
+ html += '';
+ html += '';
+ html += '
';
+
+ html += '
';
+ html += '
';
+
+ $('body').append(html);
+
+ // Event handlers
+ $('.kundenkarte-modal-close, #conn-cancel').on('click', function() {
+ $('#schematic-conn-dialog').remove();
+ });
+
+ $('.conn-type-btn').on('click', function() {
+ $('.conn-type-btn').css('outline', 'none');
+ $(this).css('outline', '2px solid #fff');
+ $('#conn-type').val($(this).data('type'));
+ $('#conn-color').val($(this).data('color'));
+ });
+
+ $('#conn-save').on('click', function() {
+ var label = $('#conn-label').val().trim();
+ var connType = $('#conn-type').val();
+ var color = $('#conn-color').val();
+
+ // Create connection via AJAX - junction connections have no fk_source
+ // but we store junction info in path_data and source_connection_id
+ self.saveJunctionConnection({
+ anlage_id: self.anlageId,
+ fk_source: null, // No source equipment
+ source_terminal_id: null,
+ fk_target: targetEqId,
+ target_terminal_id: targetTermId,
+ output_label: label,
+ connection_type: connType,
+ color: color,
+ path_data: pathData,
+ junction_connection_id: sourceConnId,
+ junction_x: junctionPoint.x,
+ junction_y: junctionPoint.y
+ });
+
+ $('#schematic-conn-dialog').remove();
+ });
+ },
+
+ saveJunctionConnection: function(data) {
+ var self = this;
+
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php',
+ method: 'POST',
+ data: {
+ action: 'create',
+ fk_source: data.fk_source,
+ source_terminal_id: data.source_terminal_id,
+ fk_target: data.fk_target,
+ target_terminal_id: data.target_terminal_id,
+ connection_type: data.connection_type || 'L1N',
+ color: data.color || self.COLORS.connection,
+ output_label: data.output_label || '',
+ medium_type: data.medium_type || '',
+ medium_length: data.medium_length || '',
+ path_data: data.path_data || '',
+ bundled_terminals: data.bundled_terminals || '',
+ // Junction-specific fields
+ junction_connection_id: data.junction_connection_id || '',
+ junction_x: data.junction_x || '',
+ junction_y: data.junction_y || '',
+ token: $('input[name="token"]').val()
+ },
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ self.showMessage('Abzweigung erstellt', 'success');
+ self.loadConnections();
+ } else {
+ self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error');
+ }
+ },
+ error: function() {
+ self.showMessage('Verbindungsfehler', 'error');
+ }
+ });
},
showConnectionLabelDialogWithPath: function(sourceEqId, sourceTermId, targetEqId, targetTermId, pathData) {
@@ -9375,6 +10794,30 @@
var sourceEq = this.equipment.find(function(e) { return e.id == sourceEqId; });
var targetEq = this.equipment.find(function(e) { return e.id == targetEqId; });
+ // Check if we should inherit from a specific connection (clicked wire)
+ var existingConn = this._inheritFromConnection || null;
+ this._inheritFromConnection = null; // Clear after use
+
+ // If no specific connection, look for existing connections to target
+ if (!existingConn && targetEqId && this.connections && this.connections.length > 0) {
+ // Look for connections where target is this equipment (incoming)
+ existingConn = this.connections.find(function(c) {
+ return c.fk_target == targetEqId;
+ });
+ // If not found, look for connections from target (outgoing)
+ if (!existingConn) {
+ existingConn = this.connections.find(function(c) {
+ return c.fk_source == targetEqId;
+ });
+ }
+ }
+
+ // Default values or inherited from existing connection
+ var defaultType = existingConn && existingConn.connection_type ? existingConn.connection_type : 'L1N';
+ var defaultColor = existingConn && existingConn.color ? existingConn.color : this.COLORS.connection;
+ var defaultMedium = existingConn && existingConn.medium_type ? existingConn.medium_type : '';
+ var defaultLength = existingConn && existingConn.medium_length ? existingConn.medium_length : '';
+
var html = '
';
html += '
';
html += '
Verbindung erstellen
';
@@ -9399,24 +10842,37 @@
var typePresets = ['L1', 'L2', 'L3', 'N', 'PE', 'L1N', '3P'];
typePresets.forEach(function(t) {
var color = self.PHASE_COLORS[t] || self.COLORS.connection;
+ var isSelected = (t === defaultType);
+ var outlineStyle = isSelected ? 'outline:2px solid #fff;' : '';
html += '';
});
html += '
';
- html += '';
- html += '';
+ html += '';
+ html += '';
html += '
';
html += '
';
html += '';
html += '
';
- html += '';
- html += '';
+ html += '';
+ html += '';
html += '
';
html += '
';
+ // Bundled terminals checkbox - only show if source equipment has >1 terminal
+ var sourceTerminalCount = sourceEq ? this.getTerminalCount(sourceEq) : 1;
+ if (sourceTerminalCount > 1) {
+ html += '