-

Objects

+

Components

- Triangle + Transformer One output only
- Square - Multiple outputs + Node + Cable Box or Pole
-

Connection Mode

+

Cable Mode

diff --git a/readme.md b/readme.md index e69de29..6a410b2 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,157 @@ +# Power System Designer + +A web-based graphical interface for designing electrical power distribution systems. This application allows you to create and connect electrical components including transformers, cable boxes, poles, and various types of power cables. + +## Components + +### 🔺 Transformer + +- **Purpose**: Steps down voltage from high to low voltage +- **Data Fields**: + - Number: Unique identifier + - Name: Custom name for the transformer + - Upper Voltage (V): Input voltage level + - Bottom Voltage (V): Output voltage level + - Power (kVA): Power rating in kilovolt-amperes + - Description: Additional notes +- **Constraints**: Only one transformer allowed per design, can have only one output connection + +### ⬛ Nodes (Cable Boxes, Poles, and End Connections) + +- **Cable Box Types**: Hand-input description field for custom box types +- **Pole Types**: Hand-input description field for custom pole types +- **End Connection Types**: Terminal connections for overhead lines +- **Data Fields**: + - Number: Unique identifier + - Name: Custom name + - Node Type: Cable box, pole, or end connection + - Type Description: Hand-input field for specific subtype + - Power Rating (kW): Power capacity for boxes and end connections (not applicable to poles) + - Description: Additional notes +- **Constraints**: + - Cable boxes and poles: One input, multiple outputs + - End connections: Multiple inputs, no outputs (terminal points) + +### 🔌 Power Cables + +Four types of power cables are supported: + +#### Ground Cables + +- **YAKY**: Underground power cable +- **NA2XY-J**: Underground power cable with improved insulation + +#### Overhead Cables + +- **AL**: Aluminum overhead conductor +- **AsXSn**: Aluminum conductor with steel reinforcement + +**Data Fields**: + +- Label: Display name for the cable +- Cable Type: One of the four types above +- Cross Section (mm²): Cable conductor cross-sectional area +- Length (m): Cable length in meters +- Description: Additional specifications + +**Constraints**: + +- Overhead cables (AL, AsXSn) can only connect to poles or end connections +- Ground cables (YAKY, NA2XY-J) can connect to any nodes +- All cables enforce electrical flow direction (no reverse connections) +- End connections are terminal points (no outgoing connections allowed) + +## Features + +### Visual Design Interface + +- **Drag & Drop**: Place components by clicking on palette items +- **Grid Snapping**: All components automatically snap to a 20px grid for precise alignment +- **Connection Mode**: Click "Connect with Cable" then click two components to connect them +- **Selection Mode**: Click components or cables to select and edit properties +- **Visual Differentiation**: + - Transformers appear as green triangles + - Poles appear as dark gray squares with ⬆ symbol + - Cable boxes appear as blue squares with ⬛ symbol + - End connections appear as red squares with ⬤ symbol + - Power ratings displayed below boxes and end connections + - Custom type descriptions shown below each node +- **Enhanced Grid**: Major grid lines every 80px, minor grid lines every 20px for better visual guidance + +### Smart Validation + +- **Component Limits**: Only one transformer per design +- **Connection Rules**: Overhead cables automatically validate pole/end connection requirements +- **Flow Direction**: Prevents invalid electrical connections +- **Duplicate Prevention**: No duplicate connections between same components +- **End Connection Rules**: Terminal points cannot have outgoing connections + +### Data Management + +- **Properties Panel**: Edit all component and cable specifications +- **Export/Import**: Save and load complete designs as JSON files +- **Real-time Updates**: Changes immediately reflected in visual design + +## Usage Instructions + +1. **Adding Components**: + + - Click on "Transformer" or "Node" in the Components panel + - Components automatically snap to the grid for precise alignment + - For nodes, select type (Cable Box, Pole, or End Connection) in properties + - Enter custom type descriptions and power ratings as needed + - Components will appear on the canvas and can be moved by dragging (with grid snapping) + +2. **Connecting Components**: + + - Click "Connect with Cable" button + - Click on the source component (transformer or node) + - Click on the destination component to create a cable connection + - Switch back to "Select Mode" to edit properties + +3. **Editing Properties**: + + - Click on any component or cable in Select Mode + - Use the Properties panel on the right to edit specifications + - Changes are saved automatically + +4. **Validating Design**: + + - Overhead cables will show warnings if not connected to poles or end connections + - End connections automatically prevent outgoing connections + - Invalid connections are prevented with error messages + - Visual indicators show connection status and power ratings + +5. **Saving/Loading**: + - Use "Export Data" to save your design as a JSON file + - Use "Import Data" to load a previously saved design + - "Clear All" removes all components and connections + +## Technical Specifications + +- **Built with**: HTML5 Canvas, CSS3, Vanilla JavaScript +- **File Format**: JSON for data persistence +- **Browser Support**: Modern browsers with Canvas support +- **Responsive**: Adapts to different screen sizes + +## File Structure + +``` +web/ +├── index.html # Main application interface +├── styles.css # CSS styling and layout +├── script.js # Core application logic +└── README.md # This documentation +``` + +## Power System Rules Enforced + +1. **Electrical Flow**: Power flows from transformer to nodes only +2. **Voltage Levels**: Transformer steps down from high to low voltage +3. **Cable Types**: Different cables for underground vs overhead installation +4. **Infrastructure**: Overhead cables require pole infrastructure or end connections +5. **Connection Limits**: Realistic electrical connection constraints +6. **Safety Standards**: Prevents invalid electrical configurations +7. **Power Management**: Track power ratings for distribution points and end connections + +This application is designed for educational purposes and basic power system planning. For actual electrical installations, consult qualified electrical engineers and follow local electrical codes. diff --git a/script.js b/script.js index 290fd4e..e0233d8 100644 --- a/script.js +++ b/script.js @@ -11,6 +11,12 @@ class ObjectFlowDesigner { this.dragOffset = { x: 0, y: 0 }; this.isDragging = false; this.nextId = 1; + this.gridSize = 20; // Grid size for snapping + + // Scrolling/panning support + this.scrollOffset = { x: 0, y: 0 }; + this.isPanning = false; + this.panStart = { x: 0, y: 0 }; this.setupEventListeners(); this.resizeCanvas(); @@ -24,6 +30,15 @@ class ObjectFlowDesigner { this.canvas.addEventListener("mouseup", this.handleMouseUp.bind(this)); this.canvas.addEventListener("click", this.handleClick.bind(this)); + // Scrolling and panning + this.canvas.addEventListener("wheel", this.handleWheel.bind(this), { + passive: false, + }); + this.canvas.addEventListener( + "contextmenu", + this.handleContextMenu.bind(this) + ); + // Palette events document.querySelectorAll(".palette-item").forEach((item) => { item.addEventListener("click", this.handlePaletteClick.bind(this)); @@ -103,24 +118,59 @@ class ObjectFlowDesigner { createObject(type, x, y) { const id = this.nextId++; - const obj = { - id, - type, - x, - y, - width: type === "triangle" ? 40 : 50, - height: type === "triangle" ? 40 : 50, - data: { - name: `${type.charAt(0).toUpperCase() + type.slice(1)} ${id}`, - description: "", - value: "", - metadata: {}, - }, - connections: { - inputs: [], - outputs: [], - }, - }; + + // Snap coordinates to grid + const snappedPos = this.snapToGrid(x, y); + + let obj; + + if (type === "triangle") { + // Transformer + obj = { + id, + type, + x: snappedPos.x, + y: snappedPos.y, + width: 50, + height: 50, + data: { + number: `T${id}`, + name: `Transformer ${id}`, + upperVoltage: 20000, // V + bottomVoltage: 400, // V + powerKVA: 630, // kVA + description: "", + metadata: {}, + }, + connections: { + inputs: [], + outputs: [], + }, + }; + } else { + // Square - Cable box or Pole + obj = { + id, + type, + x: snappedPos.x, + y: snappedPos.y, + width: 50, + height: 50, + data: { + number: `N${id}`, + name: `Node ${id}`, + nodeType: "cable_box", // cable_box, pole, or end_connection + boxPoleType: "", // Hand input field + powerKW: 0, // Power rating in kW + description: "", + metadata: {}, + }, + connections: { + inputs: [], + outputs: [], + }, + }; + } this.objects.push(obj); this.render(); @@ -132,12 +182,23 @@ class ObjectFlowDesigner { handleMouseDown(event) { const rect = this.canvas.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + const worldPos = this.screenToWorld(screenX, screenY); + const x = worldPos.x; + const y = worldPos.y; const clickedObject = this.getObjectAt(x, y); const clickedConnection = this.getConnectionAt(x, y); + // Check for right-click panning + if (event.button === 2) { + // Right mouse button + this.isPanning = true; + this.panStart = { x: screenX, y: screenY }; + return; + } + if (this.mode === "select") { if (clickedObject) { this.selectedObject = clickedObject; @@ -178,28 +239,89 @@ class ObjectFlowDesigner { handleMouseMove(event) { const rect = this.canvas.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; + const screenX = event.clientX - rect.left; + const screenY = event.clientY - rect.top; + + // Handle panning + if (this.isPanning) { + const deltaX = screenX - this.panStart.x; + const deltaY = screenY - this.panStart.y; + + this.scrollOffset.x += deltaX; + this.scrollOffset.y += deltaY; + + this.panStart = { x: screenX, y: screenY }; + this.render(); + return; + } + + const worldPos = this.screenToWorld(screenX, screenY); + const x = worldPos.x; + const y = worldPos.y; if (this.isDragging && this.selectedObject) { - this.selectedObject.x = x - this.dragOffset.x; - this.selectedObject.y = y - this.dragOffset.y; + const newX = x - this.dragOffset.x; + const newY = y - this.dragOffset.y; + + // Snap to grid during dragging + const snappedPos = this.snapToGrid(newX, newY); + this.selectedObject.x = snappedPos.x; + this.selectedObject.y = snappedPos.y; this.render(); } // Show tooltip const hoveredObject = this.getObjectAt(x, y); - this.showTooltip(hoveredObject, x, y); + this.showTooltip(hoveredObject, screenX, screenY); } handleMouseUp() { this.isDragging = false; + this.isPanning = false; } handleClick(event) { // Handle click events that don't require dragging } + handleWheel(event) { + event.preventDefault(); + + // Scroll sensitivity + const scrollSpeed = 1; + + // Update scroll offset + this.scrollOffset.x -= event.deltaX * scrollSpeed; + this.scrollOffset.y -= event.deltaY * scrollSpeed; + + // Optional: Limit scrolling bounds + // You can uncomment these lines to limit how far users can scroll + // this.scrollOffset.x = Math.max(-2000, Math.min(2000, this.scrollOffset.x)); + // this.scrollOffset.y = Math.max(-2000, Math.min(2000, this.scrollOffset.y)); + + this.render(); + } + + handleContextMenu(event) { + event.preventDefault(); // Prevent context menu from appearing + } + + // Transform screen coordinates to world coordinates + screenToWorld(screenX, screenY) { + return { + x: screenX - this.scrollOffset.x, + y: screenY - this.scrollOffset.y, + }; + } + + // Transform world coordinates to screen coordinates + worldToScreen(worldX, worldY) { + return { + x: worldX + this.scrollOffset.x, + y: worldY + this.scrollOffset.y, + }; + } + getObjectAt(x, y) { for (let i = this.objects.length - 1; i >= 0; i--) { const obj = this.objects[i]; @@ -299,31 +421,58 @@ class ObjectFlowDesigner { fromObj.type === "triangle" && fromObj.connections.outputs.length >= 1 ) { - this.showError("Triangle can only have one outgoing connection!"); + this.showError("Transformer can only have one outgoing connection!"); return; } - // Square can only have one input from the left - if (toObj.type === "square" && toObj.connections.inputs.length >= 1) { - this.showError("Square can only have one incoming connection!"); + // Square can only have one input from the left (except end connections) + if ( + toObj.type === "square" && + toObj.data.nodeType !== "end_connection" && + toObj.connections.inputs.length >= 1 + ) { + this.showError("Node can only have one incoming connection!"); + return; + } + + // End connections can have multiple inputs but no outputs + if ( + toObj.type === "square" && + toObj.data.nodeType === "end_connection" && + toObj.connections.outputs.length > 0 + ) { + this.showError("End connections cannot have outgoing connections!"); + return; + } + + // Cannot connect from end connections + if ( + fromObj.type === "square" && + fromObj.data.nodeType === "end_connection" + ) { + this.showError("Cannot connect from end connections!"); return; } // Only allow triangle->square or square->square connections if (fromObj.type === "square" && toObj.type === "triangle") { - this.showError("Cannot connect from square to triangle!"); + this.showError("Cannot connect from node to transformer!"); return; } + // Validate overhead cable connections (only to poles or end connections) + // This will be checked later when cable type is selected + const connection = { id: this.nextId++, from: fromObj, to: toObj, data: { - label: `Connection ${this.nextId - 1}`, + label: `Cable ${this.nextId - 1}`, + cableType: "YAKY", // YAKY, NA2XY-J (ground), AL, AsXSn (overhead) + crossSection: 50, // mm² + length: 100, // meters description: "", - weight: 1, - dataType: "default", metadata: {}, }, }; @@ -332,13 +481,49 @@ class ObjectFlowDesigner { fromObj.connections.outputs.push(connection); toObj.connections.inputs.push(connection); + // Validate cable type after creation + this.validateCableConnection(connection); + this.render(); } + validateCableConnection(connection) { + const isOverheadCable = + connection.data.cableType === "AL" || + connection.data.cableType === "AsXSn"; + + if (isOverheadCable) { + // Check if both ends are connected to poles or end connections + const fromIsPole = + connection.from.type === "square" && + (connection.from.data.nodeType === "pole" || + connection.from.data.nodeType === "end_connection"); + const toIsPole = + connection.to.type === "square" && + (connection.to.data.nodeType === "pole" || + connection.to.data.nodeType === "end_connection"); + const fromIsTransformer = connection.from.type === "triangle"; + + if (!((fromIsPole || fromIsTransformer) && toIsPole)) { + this.showError( + "Overhead cables can only connect to poles or end connections!" + ); + // Optionally remove the invalid connection + this.deleteConnection(connection.id); + return false; + } + } + return true; + } + render() { // Clear canvas this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + // Save context and apply scroll transform + this.ctx.save(); + this.ctx.translate(this.scrollOffset.x, this.scrollOffset.y); + // Draw grid this.drawGrid(); @@ -348,28 +533,61 @@ class ObjectFlowDesigner { // Draw objects this.objects.forEach((obj) => this.drawObject(obj)); - // Draw connection preview + // Restore context for UI elements that shouldn't scroll + this.ctx.restore(); + + // Draw connection preview (in screen coordinates) if (this.mode === "connect" && this.connectionStart) { this.drawConnectionPreview(); } } drawGrid() { - const gridSize = 20; - this.ctx.strokeStyle = "#f0f0f0"; - this.ctx.lineWidth = 1; + // Calculate visible area based on scroll offset + const startX = + Math.floor(-this.scrollOffset.x / this.gridSize) * this.gridSize; + const startY = + Math.floor(-this.scrollOffset.y / this.gridSize) * this.gridSize; + const endX = startX + this.canvas.width + this.gridSize * 2; + const endY = startY + this.canvas.height + this.gridSize * 2; - for (let x = 0; x < this.canvas.width; x += gridSize) { + this.ctx.strokeStyle = "#e8e8e8"; + this.ctx.lineWidth = 0.5; + + // Draw major grid lines every 4 squares (80px) + for (let x = startX; x < endX; x += this.gridSize * 4) { + this.ctx.strokeStyle = "#d0d0d0"; + this.ctx.lineWidth = 1; this.ctx.beginPath(); - this.ctx.moveTo(x, 0); - this.ctx.lineTo(x, this.canvas.height); + this.ctx.moveTo(x, startY); + this.ctx.lineTo(x, endY); this.ctx.stroke(); } - for (let y = 0; y < this.canvas.height; y += gridSize) { + for (let y = startY; y < endY; y += this.gridSize * 4) { + this.ctx.strokeStyle = "#d0d0d0"; + this.ctx.lineWidth = 1; this.ctx.beginPath(); - this.ctx.moveTo(0, y); - this.ctx.lineTo(this.canvas.width, y); + this.ctx.moveTo(startX, y); + this.ctx.lineTo(endX, y); + this.ctx.stroke(); + } + + // Draw minor grid lines + this.ctx.strokeStyle = "#f0f0f0"; + this.ctx.lineWidth = 0.5; + + for (let x = startX; x < endX; x += this.gridSize) { + this.ctx.beginPath(); + this.ctx.moveTo(x, startY); + this.ctx.lineTo(x, endY); + this.ctx.stroke(); + } + + for (let y = startY; y < endY; y += this.gridSize) { + this.ctx.beginPath(); + this.ctx.moveTo(startX, y); + this.ctx.lineTo(endX, y); this.ctx.stroke(); } } @@ -415,31 +633,120 @@ class ObjectFlowDesigner { // Label this.ctx.fillStyle = "white"; - this.ctx.font = "12px Arial"; + this.ctx.font = "16px Arial"; this.ctx.textAlign = "center"; - this.ctx.fillText("▲", centerX, centerY + 4); + this.ctx.fillText("▲", centerX, centerY + 6); + + // Power display below transformer + this.ctx.font = "14px Arial"; + this.ctx.fillStyle = isSelected ? "#2d3748" : "#4a5568"; + this.ctx.fillText( + `${obj.data.powerKVA}kVA`, + centerX, + obj.y + obj.height + 18 + ); } drawSquare(obj, isSelected, isConnectionStart) { + const isPole = obj.data.nodeType === "pole"; + const isCableBox = obj.data.nodeType === "cable_box"; + const isEndConnection = obj.data.nodeType === "end_connection"; + + // Different colors for different node types + let fillColor, borderColor; + + if (isPole) { + fillColor = isConnectionStart ? "#2d3748" : "#4a5568"; // Darker gray for poles + borderColor = "#1a202c"; + } else if (isEndConnection) { + fillColor = isConnectionStart ? "#c53030" : "#e53e3e"; // Red for end connections + borderColor = "#9b2c2c"; + } else { + fillColor = isConnectionStart ? "#2b6cb0" : "#4299e1"; // Blue for cable boxes + borderColor = "#2c5282"; + } + // Fill - this.ctx.fillStyle = isConnectionStart ? "#2b6cb0" : "#4299e1"; + this.ctx.fillStyle = fillColor; this.ctx.fillRect(obj.x, obj.y, obj.width, obj.height); // Border if (isSelected) { - this.ctx.strokeStyle = "#2d3748"; + this.ctx.strokeStyle = "#fbb040"; // Orange border for selected this.ctx.lineWidth = 3; } else { - this.ctx.strokeStyle = "#2c5282"; + this.ctx.strokeStyle = borderColor; this.ctx.lineWidth = 2; } this.ctx.strokeRect(obj.x, obj.y, obj.width, obj.height); - // Label + // Different symbols for different types this.ctx.fillStyle = "white"; - this.ctx.font = "12px Arial"; + this.ctx.font = "16px Arial"; this.ctx.textAlign = "center"; - this.ctx.fillText("■", obj.x + obj.width / 2, obj.y + obj.height / 2 + 4); + + if (isPole) { + // Draw pole symbol + this.ctx.fillText("⬆", obj.x + obj.width / 2, obj.y + obj.height / 2 + 6); + } else if (isEndConnection) { + // Draw end connection symbol + this.ctx.fillText("⬤", obj.x + obj.width / 2, obj.y + obj.height / 2 + 6); + } else { + // Draw cable box symbol + this.ctx.fillText( + "⬛", + obj.x + obj.width / 2, + obj.y + obj.height / 2 + 6 + ); + } + + // Add detailed information below nodes + this.ctx.font = "11px Arial"; + this.ctx.fillStyle = isSelected ? "#fbb040" : "#2d3748"; + this.ctx.textAlign = "center"; + + let yOffset = obj.y + obj.height + 16; + + // 1. Number + this.ctx.fillText(`#${obj.data.number}`, obj.x + obj.width / 2, yOffset); + yOffset += 13; + + // 2. Node power (for boxes and end connections) + if (!isPole && obj.data.powerKW > 0) { + this.ctx.fillText( + `${obj.data.powerKW}kW`, + obj.x + obj.width / 2, + yOffset + ); + yOffset += 13; + } + + // 3. Total downstream power + const downstreamPower = this.calculateDownstreamPower(obj); + if (downstreamPower > 0) { + this.ctx.fillText( + `Σ${downstreamPower}kW`, + obj.x + obj.width / 2, + yOffset + ); + yOffset += 13; + } + + // 4. Distance from transformer + const distance = this.calculateNodeDistance(obj); + if (distance > 0) { + this.ctx.fillText(`D${distance}`, obj.x + obj.width / 2, yOffset); + yOffset += 13; + } + + // Type description (moved down to accommodate other info) + if (obj.data.boxPoleType) { + this.ctx.fillText( + obj.data.boxPoleType.substr(0, 8), + obj.x + obj.width / 2, + yOffset + ); + } } drawConnection(connection) { @@ -456,6 +763,18 @@ class ObjectFlowDesigner { this.ctx.lineWidth = isSelected ? 4 : 3; this.ctx.lineCap = "round"; + // Set line style based on cable type + const isOverheadCable = + connection.data.cableType === "AL" || + connection.data.cableType === "AsXSn"; + if (isOverheadCable) { + // Overhead cables: continuous line + this.ctx.setLineDash([]); + } else { + // Underground cables: dashed line + this.ctx.setLineDash([10, 5]); + } + // Draw curved line this.ctx.beginPath(); this.ctx.moveTo(fromPoint.x, fromPoint.y); @@ -475,6 +794,9 @@ class ObjectFlowDesigner { ); this.ctx.stroke(); + // Reset line dash for arrowhead (always solid) + this.ctx.setLineDash([]); + // Draw arrowhead this.drawArrowhead( toPoint.x, @@ -483,46 +805,72 @@ class ObjectFlowDesigner { isSelected ); - // Draw connection label if it exists - if (connection.data.label) { - const midPoint = this.getBezierPoint( - fromPoint.x, - fromPoint.y, - controlX1, - controlY1, - controlX2, - controlY2, - toPoint.x, - toPoint.y, - 0.5 - ); + // Draw connection information + const midPoint = this.getBezierPoint( + fromPoint.x, + fromPoint.y, + controlX1, + controlY1, + controlX2, + controlY2, + toPoint.x, + toPoint.y, + 0.5 + ); - this.ctx.fillStyle = "white"; - this.ctx.strokeStyle = "#4a5568"; - this.ctx.lineWidth = 2; + // Prepare the text lines to display + const textLines = []; + if (connection.data.label) { + textLines.push(connection.data.label); + } + + // Format cable type display + const isOverhead = + connection.data.cableType === "AL" || + connection.data.cableType === "AsXSn"; + const cableTypeDisplay = `${connection.data.cableType} ${ + isOverhead ? "(OH)" : "(UG)" + }`; + + textLines.push(`${cableTypeDisplay} ${connection.data.crossSection}mm²`); + textLines.push(`${connection.data.length}m`); + + if (textLines.length > 0) { this.ctx.font = "12px Arial"; this.ctx.textAlign = "center"; - const textWidth = this.ctx.measureText(connection.data.label).width; - const padding = 4; + // Calculate dimensions for background + const lineHeight = 16; + const padding = 5; + let maxWidth = 0; + + // Measure all text lines to find the maximum width + textLines.forEach((line) => { + const width = this.ctx.measureText(line).width; + if (width > maxWidth) maxWidth = width; + }); + + const totalHeight = textLines.length * lineHeight; // Draw background - this.ctx.fillRect( - midPoint.x - textWidth / 2 - padding, - midPoint.y - 8 - padding, - textWidth + padding * 2, - 16 + padding * 2 - ); - this.ctx.strokeRect( - midPoint.x - textWidth / 2 - padding, - midPoint.y - 8 - padding, - textWidth + padding * 2, - 16 + padding * 2 - ); + this.ctx.fillStyle = "rgba(255, 255, 255, 0.95)"; + this.ctx.strokeStyle = "#4a5568"; + this.ctx.lineWidth = 1; - // Draw text - this.ctx.fillStyle = "#4a5568"; - this.ctx.fillText(connection.data.label, midPoint.x, midPoint.y + 4); + const bgX = midPoint.x - maxWidth / 2 - padding; + const bgY = midPoint.y - totalHeight / 2 - padding; + const bgWidth = maxWidth + padding * 2; + const bgHeight = totalHeight + padding * 2; + + this.ctx.fillRect(bgX, bgY, bgWidth, bgHeight); + this.ctx.strokeRect(bgX, bgY, bgWidth, bgHeight); + + // Draw text lines + this.ctx.fillStyle = "#2d3748"; + textLines.forEach((line, index) => { + const yOffset = (index - (textLines.length - 1) / 2) * lineHeight; + this.ctx.fillText(line, midPoint.x, midPoint.y + yOffset + 4); + }); } this.ctx.restore(); @@ -532,19 +880,23 @@ class ObjectFlowDesigner { if (!this.connectionStart) return; const rect = this.canvas.getBoundingClientRect(); - const mouseX = event?.clientX - rect.left || 0; - const mouseY = event?.clientY - rect.top || 0; + const screenX = event?.clientX - rect.left || 0; + const screenY = event?.clientY - rect.top || 0; + const worldMouse = this.screenToWorld(screenX, screenY); + + // Apply scroll transform for this preview + this.ctx.save(); + this.ctx.translate(this.scrollOffset.x, this.scrollOffset.y); const fromPoint = this.getConnectionPoint(this.connectionStart, "output"); - this.ctx.save(); this.ctx.strokeStyle = "#a0aec0"; this.ctx.lineWidth = 2; this.ctx.setLineDash([5, 5]); this.ctx.beginPath(); this.ctx.moveTo(fromPoint.x, fromPoint.y); - this.ctx.lineTo(mouseX, mouseY); + this.ctx.lineTo(worldMouse.x, worldMouse.y); this.ctx.stroke(); this.ctx.restore(); @@ -597,108 +949,248 @@ class ObjectFlowDesigner { updateObjectProperties(panel) { const obj = this.selectedObject; - panel.innerHTML = ` -
-

Object Properties

- - - - - - -
- -
-

Object Info

-

Type: ${obj.type}

-

ID: ${obj.id}

-

Position: (${Math.round( - obj.x - )}, ${Math.round(obj.y)})

-

Inputs: ${obj.connections.inputs.length}

-

Outputs: ${ - obj.connections.outputs.length - }

-
- -
-

Actions

- -
- `; - // Add event listeners for property changes - document.getElementById("objName").addEventListener("input", (e) => { - obj.data.name = e.target.value; - }); + if (obj.type === "triangle") { + // Transformer properties + panel.innerHTML = ` +
+

Transformer Properties

+ + + + + + + + + + + + +
+ +
+

Transformer Info

+

Type: Transformer

+

ID: ${obj.id}

+

Position: (${Math.round(obj.x)}, ${Math.round(obj.y)})

+

Ratio: ${obj.data.upperVoltage}V / ${ + obj.data.bottomVoltage + }V

+

Outputs: ${obj.connections.outputs.length}/1

+
+ +
+

Actions

+ +
+ `; - document.getElementById("objDescription").addEventListener("input", (e) => { - obj.data.description = e.target.value; - }); + // Add event listeners + document.getElementById("objNumber").addEventListener("input", (e) => { + obj.data.number = e.target.value; + }); + document.getElementById("objName").addEventListener("input", (e) => { + obj.data.name = e.target.value; + }); + document + .getElementById("objUpperVoltage") + .addEventListener("input", (e) => { + obj.data.upperVoltage = parseInt(e.target.value) || 0; + }); + document + .getElementById("objBottomVoltage") + .addEventListener("input", (e) => { + obj.data.bottomVoltage = parseInt(e.target.value) || 0; + }); + document.getElementById("objPowerKVA").addEventListener("input", (e) => { + obj.data.powerKVA = parseInt(e.target.value) || 0; + }); + document + .getElementById("objDescription") + .addEventListener("input", (e) => { + obj.data.description = e.target.value; + }); + } else { + // Square (Node) properties + panel.innerHTML = ` +
+

Node Properties

+ + + + + + + + + ${ + obj.data.nodeType !== "pole" + ? ` + + + ` + : "" + } + + +
+ +
+

Node Info

+

Type: ${ + obj.data.nodeType === "cable_box" + ? "Cable Box" + : obj.data.nodeType === "pole" + ? "Pole" + : "End Connection" + }

+

ID: ${obj.id}

+

Position: (${Math.round(obj.x)}, ${Math.round(obj.y)})

+ ${ + obj.data.nodeType !== "pole" && obj.data.powerKW > 0 + ? `

Power: ${obj.data.powerKW} kW

` + : "" + } +

Inputs: ${obj.connections.inputs.length}${ + obj.data.nodeType === "end_connection" ? "" : "/1" + }

+

Outputs: ${obj.connections.outputs.length}${ + obj.data.nodeType === "end_connection" ? "/0" : "" + }

+
+ +
+

Actions

+ +
+ `; - document.getElementById("objValue").addEventListener("input", (e) => { - obj.data.value = e.target.value; - }); + // Add event listeners + document.getElementById("objNumber").addEventListener("input", (e) => { + obj.data.number = e.target.value; + }); + document.getElementById("objName").addEventListener("input", (e) => { + obj.data.name = e.target.value; + }); + document.getElementById("objNodeType").addEventListener("change", (e) => { + obj.data.nodeType = e.target.value; + // Reset power for poles + if (e.target.value === "pole") { + obj.data.powerKW = 0; + } + this.updatePropertiesPanel(); // Refresh to update options + this.render(); // Re-render to update visual representation + }); + document + .getElementById("objBoxPoleType") + .addEventListener("input", (e) => { + obj.data.boxPoleType = e.target.value; + this.render(); // Re-render to update visual representation + }); + + // Only add power listener if the field exists (not for poles) + const powerField = document.getElementById("objPowerKW"); + if (powerField) { + powerField.addEventListener("input", (e) => { + obj.data.powerKW = parseFloat(e.target.value) || 0; + }); + } + + document + .getElementById("objDescription") + .addEventListener("input", (e) => { + obj.data.description = e.target.value; + }); + } } updateConnectionProperties(panel) { const conn = this.selectedConnection; + const isOverheadCable = + conn.data.cableType === "AL" || conn.data.cableType === "AsXSn"; + panel.innerHTML = ` -
-

Connection Properties

- - - - - - - - -
- -
-

Connection Info

-

ID: ${conn.id}

-

From: ${conn.from.data.name} (${ - conn.from.type - })

-

To: ${conn.to.data.name} (${ - conn.to.type - })

-

Flow Direction: ${conn.from.data.name} → ${ - conn.to.data.name +

+

Power Cable Properties

+ + + + + + + + + + +
+ +
+

Cable Info

+

ID: ${conn.id}

+

Type: ${conn.data.cableType} ${ + isOverheadCable ? "(Overhead)" : "(Ground)" }

-
- -
-

Actions

- -
- `; +

From: ${conn.from.data.name} (${ + conn.from.type === "triangle" + ? "Transformer" + : this.getNodeDisplayType(conn.from.data.nodeType) + })

+

To: ${conn.to.data.name} (${ + conn.to.type === "triangle" + ? "Transformer" + : this.getNodeDisplayType(conn.to.data.nodeType) + })

+

Capacity: ${conn.data.crossSection} mm² × ${ + conn.data.length + } m

+ ${ + isOverheadCable + ? '

⚠️ Overhead cables can only connect to poles or end connections!

' + : "" + } +
+ +
+

Actions

+ +
+ `; // Add event listeners for property changes document.getElementById("connLabel").addEventListener("input", (e) => { @@ -706,19 +1198,28 @@ class ObjectFlowDesigner { this.render(); // Re-render to show label changes }); + document.getElementById("connCableType").addEventListener("change", (e) => { + conn.data.cableType = e.target.value; + this.validateCableConnection(conn); // Re-validate connection + this.updatePropertiesPanel(); // Refresh to show warnings + this.render(); + }); + + document + .getElementById("connCrossSection") + .addEventListener("input", (e) => { + conn.data.crossSection = parseInt(e.target.value) || 1; + }); + + document.getElementById("connLength").addEventListener("input", (e) => { + conn.data.length = parseFloat(e.target.value) || 0.1; + }); + document .getElementById("connDescription") .addEventListener("input", (e) => { conn.data.description = e.target.value; }); - - document.getElementById("connWeight").addEventListener("input", (e) => { - conn.data.weight = parseFloat(e.target.value) || 0; - }); - - document.getElementById("connDataType").addEventListener("change", (e) => { - conn.data.dataType = e.target.value; - }); } deleteObject(id) { @@ -781,21 +1282,22 @@ class ObjectFlowDesigner { this.render(); } - showTooltip(obj, x, y) { + showTooltip(obj, screenX, screenY) { const tooltip = document.getElementById("tooltip"); if (obj) { tooltip.style.display = "block"; - tooltip.style.left = x + 10 + "px"; - tooltip.style.top = y - 30 + "px"; + tooltip.style.left = screenX + 10 + "px"; + tooltip.style.top = screenY - 30 + "px"; tooltip.textContent = `${obj.data.name} (${obj.type})`; } else { // Check if hovering over a connection - const connection = this.getConnectionAt(x, y); + const worldPos = this.screenToWorld(screenX, screenY); + const connection = this.getConnectionAt(worldPos.x, worldPos.y); if (connection) { tooltip.style.display = "block"; - tooltip.style.left = x + 10 + "px"; - tooltip.style.top = y - 30 + "px"; + tooltip.style.left = screenX + 10 + "px"; + tooltip.style.top = screenY - 30 + "px"; tooltip.textContent = `${connection.data.label || "Connection"} (${ connection.from.data.name } → ${connection.to.data.name})`; @@ -886,11 +1388,16 @@ class ObjectFlowDesigner { this.selectedConnection = null; this.connectionStart = null; - // Restore objects - this.objects = data.objects.map((obj) => ({ - ...obj, - connections: { inputs: [], outputs: [] }, - })); + // Restore objects and snap them to grid + this.objects = data.objects.map((obj) => { + const snappedPos = this.snapToGrid(obj.x, obj.y); + return { + ...obj, + x: snappedPos.x, + y: snappedPos.y, + connections: { inputs: [], outputs: [] }, + }; + }); // Restore connections data.connections.forEach((connData) => { @@ -922,6 +1429,77 @@ class ObjectFlowDesigner { // Reset file input event.target.value = ""; } + + getNodeDisplayType(nodeType) { + switch (nodeType) { + case "cable_box": + return "Cable Box"; + case "pole": + return "Pole"; + case "end_connection": + return "End Connection"; + default: + return "Node"; + } + } + + snapToGrid(x, y) { + return { + x: Math.round(x / this.gridSize) * this.gridSize, + y: Math.round(y / this.gridSize) * this.gridSize, + }; + } + + calculateNodeDistance(node) { + // Calculate distance from transformer (number of hops) + if (node.type === "triangle") return 0; + + const visited = new Set(); + const queue = [{ node, distance: 0 }]; + visited.add(node.id); + + while (queue.length > 0) { + const { node: currentNode, distance } = queue.shift(); + + // Check all input connections + for (const connection of currentNode.connections.inputs) { + const fromNode = connection.from; + + if (fromNode.type === "triangle") { + return distance + 1; + } + + if (!visited.has(fromNode.id)) { + visited.add(fromNode.id); + queue.push({ node: fromNode, distance: distance + 1 }); + } + } + } + + return -1; // Not connected to transformer + } + + calculateDownstreamPower(node) { + // Calculate total power of this node and all downstream nodes + let totalPower = node.data.powerKW || 0; + const visited = new Set(); + + const calculateRecursive = (currentNode) => { + if (visited.has(currentNode.id)) return 0; + visited.add(currentNode.id); + + let downstreamPower = 0; + for (const connection of currentNode.connections.outputs) { + const toNode = connection.to; + downstreamPower += toNode.data.powerKW || 0; + downstreamPower += calculateRecursive(toNode); + } + return downstreamPower; + }; + + totalPower += calculateRecursive(node); + return totalPower; + } } // Initialize the application diff --git a/styles.css b/styles.css index 992656b..376aac0 100644 --- a/styles.css +++ b/styles.css @@ -321,6 +321,47 @@ body { font-size: 0.85rem; } +/* Warning styling for overhead cable validation */ +.warning { + color: #d69e2e; + background-color: #fef5e7; + border: 1px solid #f6e05e; + border-radius: 4px; + padding: 0.5rem; + margin: 0.5rem 0; + font-size: 0.85rem; +} + +.warning strong { + color: #c05621; +} + +.pole-node { + background: #4a5568 !important; + border-color: #2d3748 !important; +} + +.cable-box-node { + background: #4299e1 !important; + border-color: #2c5282 !important; +} + +/* Power system specific styling */ +.transformer-preview { + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 25px solid #48bb78; +} + +.node-preview { + width: 30px; + height: 30px; + background: linear-gradient(45deg, #4299e1 50%, #4a5568 50%); + border-radius: 4px; +} + /* Responsive design */ @media (max-width: 1024px) { .main-content {