mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-07-12 18:43:44 +00:00
build(*): Add Netlify support
This commit is contained in:
@@ -1,144 +1,168 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, afterUpdate, tick } from 'svelte';
|
||||
import { networkStore, loadNetworkData } from '../../stores/networkStore';
|
||||
import { get } from 'svelte/store';
|
||||
import MachineCard from './MachineCard.svelte';
|
||||
import DeviceCard from './DeviceCard.svelte';
|
||||
|
||||
import type { NetworkData, Port } from '../../lib/types';
|
||||
import { astar } from '../utils/pathfinding';
|
||||
import type { Point } from '../utils/pathfinding';
|
||||
|
||||
export let jsonPath: string = '/data/network.json';
|
||||
|
||||
let data: NetworkData;
|
||||
let data: NetworkData | undefined = undefined;
|
||||
|
||||
// We'll store all port references in one big map for easy access
|
||||
let lines: any[] = [];
|
||||
// DEBUG: Log when component mounts
|
||||
onMount(() => {
|
||||
// Async block for data loading and initial connection
|
||||
(async () => {
|
||||
console.log('NetworkDiagram mounted');
|
||||
await loadNetworkData(jsonPath);
|
||||
data = get(networkStore);
|
||||
console.log('Loaded network data:', data);
|
||||
await tick(); // Wait for DOM update
|
||||
const container = document.querySelector('.diagram-container');
|
||||
if (container) {
|
||||
updateSVGConnections();
|
||||
} else {
|
||||
console.warn('Container not found, skipping updateSVGConnections');
|
||||
}
|
||||
})();
|
||||
|
||||
onMount(async () => {
|
||||
await loadNetworkData(jsonPath);
|
||||
data = get(networkStore);
|
||||
// Optionally, update connections on window resize
|
||||
const handleResize = () => {
|
||||
const container = document.querySelector('.diagram-container');
|
||||
if (container) updateSVGConnections();
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Wait for DOM to render fully
|
||||
setTimeout(() => {
|
||||
createLines();
|
||||
}, 0);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Called after child components have rendered.
|
||||
* We gather all port references and draw lines using global LeaderLine
|
||||
*/
|
||||
async function createLines() {
|
||||
// Use global UMD version from window
|
||||
const LeaderLine = (window as any).LeaderLine;
|
||||
if (!LeaderLine) {
|
||||
console.error('LeaderLine is not loaded.');
|
||||
// SVG-based connection rendering
|
||||
let svgWidth = 0;
|
||||
let svgHeight = 0;
|
||||
let svgOffsetX = 0;
|
||||
let svgOffsetY = 0;
|
||||
let svgConnections: { points: [number, number][]; color: string; label: string }[] = [];
|
||||
|
||||
const GRID_SIZE = 40; // px per grid cell
|
||||
|
||||
function updateSVGConnections() {
|
||||
const container = document.querySelector('.diagram-container') as HTMLElement;
|
||||
if (!container) {
|
||||
console.warn('No container element found in updateSVGConnections');
|
||||
return;
|
||||
}
|
||||
const rect = container.getBoundingClientRect();
|
||||
svgWidth = rect.width;
|
||||
svgHeight = rect.height;
|
||||
svgOffsetX = rect.left;
|
||||
svgOffsetY = rect.top;
|
||||
|
||||
console.log('updateSVGConnections called');
|
||||
|
||||
const portDataMap: Map<string, { element: HTMLDivElement; port: Port }> = new Map();
|
||||
// Gather all port elements
|
||||
if (!data) {
|
||||
console.warn('No data loaded in updateSVGConnections');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous lines
|
||||
for (const ln of lines) {
|
||||
ln.remove();
|
||||
}
|
||||
lines = [];
|
||||
|
||||
type PortInfo = {
|
||||
parentName: string;
|
||||
port: Port;
|
||||
element: HTMLDivElement;
|
||||
};
|
||||
|
||||
const portDataMap: Map<string, PortInfo> = new Map();
|
||||
|
||||
// GATHER MACHINE PORTS
|
||||
for (const machine of data.machines) {
|
||||
if (machine.ports) {
|
||||
for (const p of machine.ports) {
|
||||
const key = `${machine.machineName}-${p.portName}`;
|
||||
const elem = document.querySelector(`[data-port-key="${key}"]`) as HTMLDivElement;
|
||||
if (elem) {
|
||||
portDataMap.set(key, {
|
||||
parentName: machine.machineName,
|
||||
port: p,
|
||||
element: elem
|
||||
});
|
||||
}
|
||||
if (elem) portDataMap.set(key, { element: elem, port: p });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GATHER DEVICE PORTS
|
||||
for (const dev of data.devices) {
|
||||
if (dev.ports) {
|
||||
for (const p of dev.ports) {
|
||||
const key = `${dev.name}-${p.portName}`;
|
||||
const elem = document.querySelector(`[data-port-key="${key}"]`) as HTMLDivElement;
|
||||
if (elem) {
|
||||
portDataMap.set(key, {
|
||||
parentName: dev.name,
|
||||
port: p,
|
||||
element: elem
|
||||
});
|
||||
for (const dev of data.devices) {
|
||||
if (dev.ports) {
|
||||
for (const p of dev.ports) {
|
||||
const key = `${dev.name}-${p.portName}`;
|
||||
const elem = document.querySelector(`[data-port-key="${key}"]`) as HTMLDivElement;
|
||||
if (elem) portDataMap.set(key, { element: elem, port: p });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DRAW CONNECTIONS
|
||||
svgConnections = [];
|
||||
// Pathfinding: mark all cards as obstacles
|
||||
const cols = Math.ceil(svgWidth / GRID_SIZE);
|
||||
const rows = Math.ceil(svgHeight / GRID_SIZE);
|
||||
const cardElements = document.querySelectorAll('.device-card, .machine-card');
|
||||
const grid: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0));
|
||||
for (const el of cardElements) {
|
||||
const r = el.getBoundingClientRect();
|
||||
const left = Math.floor((r.left - svgOffsetX) / GRID_SIZE);
|
||||
const top = Math.floor((r.top - svgOffsetY) / GRID_SIZE);
|
||||
const right = Math.ceil((r.right - svgOffsetX) / GRID_SIZE);
|
||||
const bottom = Math.ceil((r.bottom - svgOffsetY) / GRID_SIZE);
|
||||
for (let y = top; y < bottom; y++) {
|
||||
for (let x = left; x < right; x++) {
|
||||
if (x >= 0 && y >= 0 && x < cols && y < rows) grid[y][x] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svgConnections = [];
|
||||
for (const [key, info] of portDataMap.entries()) {
|
||||
const localPort = info.port;
|
||||
if (!localPort.connectedTo) continue;
|
||||
|
||||
const remoteKey = localPort.connectedTo;
|
||||
const remoteInfo = portDataMap.get(remoteKey);
|
||||
const remoteInfo = portDataMap.get(localPort.connectedTo);
|
||||
if (!remoteInfo) continue;
|
||||
if (key > localPort.connectedTo) continue; // Avoid duplicates
|
||||
|
||||
if (key > remoteKey) continue; // Avoid duplicates
|
||||
const a = info.element.getBoundingClientRect();
|
||||
const b = remoteInfo.element.getBoundingClientRect();
|
||||
const PADDING = 8;
|
||||
const isRightward = (b.left + b.width / 2) > (a.left + a.width / 2);
|
||||
const startX = isRightward ? a.right - svgOffsetX + PADDING : a.left - svgOffsetX - PADDING;
|
||||
const endX = isRightward ? b.left - svgOffsetX - PADDING : b.right - svgOffsetX + PADDING;
|
||||
const startY = a.top + a.height / 2 - svgOffsetY;
|
||||
const endY = b.top + b.height / 2 - svgOffsetY;
|
||||
|
||||
const localSpeed = localPort.speedGbps ?? 1;
|
||||
const remoteSpeed = remoteInfo.port.speedGbps ?? 1;
|
||||
const cableSpeed = Math.min(localSpeed, remoteSpeed);
|
||||
const color = getLineColor(cableSpeed);
|
||||
// Source/target grid points
|
||||
const start: Point = [Math.floor(startX / GRID_SIZE), Math.floor(startY / GRID_SIZE)];
|
||||
const end: Point = [Math.floor(endX / GRID_SIZE), Math.floor(endY / GRID_SIZE)];
|
||||
// Temporarily clear source/target cells
|
||||
grid[start[1]][start[0]] = 0;
|
||||
grid[end[1]][end[0]] = 0;
|
||||
|
||||
// Calculate the best sockets for the line to exit/enter
|
||||
const localRect = info.element.getBoundingClientRect();
|
||||
const remoteRect = remoteInfo.element.getBoundingClientRect();
|
||||
const dx = remoteRect.left - localRect.left;
|
||||
const dy = remoteRect.top - localRect.top;
|
||||
|
||||
let startSocket = 'right';
|
||||
let endSocket = 'left';
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// More horizontal distance
|
||||
startSocket = dx > 0 ? 'right' : 'left';
|
||||
endSocket = dx > 0 ? 'left' : 'right';
|
||||
let path = astar(grid, start, end);
|
||||
let points: [number, number][] = [];
|
||||
if (path.length > 0) {
|
||||
points = path.map(([gx, gy]) => [
|
||||
gx * GRID_SIZE + GRID_SIZE / 2,
|
||||
gy * GRID_SIZE + GRID_SIZE / 2
|
||||
]);
|
||||
// Ensure start and end points are exactly at the card edges
|
||||
points[0] = [startX, startY];
|
||||
points[points.length - 1] = [endX, endY];
|
||||
} else {
|
||||
// More vertical distance
|
||||
startSocket = dy > 0 ? 'bottom' : 'top';
|
||||
endSocket = dy > 0 ? 'top' : 'bottom';
|
||||
// fallback: simple L-bend
|
||||
points = [
|
||||
[startX, startY],
|
||||
[endX, startY],
|
||||
[endX, endY]
|
||||
];
|
||||
}
|
||||
|
||||
const line = new LeaderLine(info.element, remoteInfo.element, {
|
||||
path: 'grid',
|
||||
startPlug: 'behind',
|
||||
endPlug: 'behind',
|
||||
startSocket,
|
||||
endSocket,
|
||||
color,
|
||||
size: 4,
|
||||
middleLabel: LeaderLine.captionLabel({
|
||||
text: `${cableSpeed}GbE`,
|
||||
fontSize: 12,
|
||||
color: 'white',
|
||||
outlineColor: 'black',
|
||||
outlineSize: 2
|
||||
})
|
||||
});
|
||||
|
||||
lines.push(line);
|
||||
const speed = Math.min(localPort.speedGbps ?? 1, remoteInfo.port.speedGbps ?? 1);
|
||||
const color = getLineColor(speed);
|
||||
const label = `${speed}GbE`;
|
||||
svgConnections.push({ points, color, label });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getLineColor(speed: number): string {
|
||||
if (speed >= 10) return 'orange';
|
||||
if (speed >= 2.5) return 'green';
|
||||
@@ -149,7 +173,33 @@
|
||||
|
||||
<main>
|
||||
{#if data}
|
||||
<div class="diagram-container">
|
||||
<div class="diagram-container" style="position:relative;">
|
||||
<svg
|
||||
class="diagram-svg"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
style="position:absolute;top:0;left:0;z-index:0;pointer-events:none;"
|
||||
>
|
||||
{#each svgConnections as conn}
|
||||
<polyline
|
||||
points={conn.points.map(([x, y]) => `${x},${y}`).join(' ')}
|
||||
stroke={conn.color}
|
||||
stroke-width="4"
|
||||
fill="none"
|
||||
/>
|
||||
<!-- Optionally render a label at the midpoint -->
|
||||
<text
|
||||
x={(conn.points[1][0] + conn.points[2][0]) / 2}
|
||||
y={(conn.points[1][1] + conn.points[2][1]) / 2 - 6}
|
||||
font-size="12"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
stroke-width="2"
|
||||
paint-order="stroke"
|
||||
text-anchor="middle">{conn.label}</text
|
||||
>
|
||||
{/each}
|
||||
</svg>
|
||||
{#each data.machines as machine}
|
||||
<MachineCard {machine} />
|
||||
{/each}
|
||||
@@ -166,6 +216,9 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
position: relative; /* required for leader-line positioning */
|
||||
position: relative;
|
||||
}
|
||||
.diagram-svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Simple grid-based A* pathfinding for orthogonal routing
|
||||
// Returns a list of [x, y] points from start to end, or [] if no path found
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
interface Node {
|
||||
x: number;
|
||||
y: number;
|
||||
g: number;
|
||||
h: number;
|
||||
f: number;
|
||||
parent?: Node;
|
||||
}
|
||||
|
||||
export function astar(grid: number[][], start: Point, end: Point): Point[] {
|
||||
const height = grid.length;
|
||||
const width = grid[0].length;
|
||||
const open: Node[] = [];
|
||||
const closed: boolean[][] = Array.from({ length: height }, () => Array(width).fill(false));
|
||||
|
||||
function heuristic([x, y]: Point): number {
|
||||
// Manhattan distance
|
||||
return Math.abs(x - end[0]) + Math.abs(y - end[1]);
|
||||
}
|
||||
|
||||
open.push({ x: start[0], y: start[1], g: 0, h: heuristic(start), f: heuristic(start) });
|
||||
|
||||
while (open.length > 0) {
|
||||
// Get node with lowest f
|
||||
open.sort((a, b) => a.f - b.f);
|
||||
const current = open.shift()!;
|
||||
if (current.x === end[0] && current.y === end[1]) {
|
||||
// Reconstruct path
|
||||
const path: Point[] = [];
|
||||
let node: Node | undefined = current;
|
||||
while (node) {
|
||||
path.push([node.x, node.y]);
|
||||
node = node.parent;
|
||||
}
|
||||
return path.reverse();
|
||||
}
|
||||
closed[current.y][current.x] = true;
|
||||
// Explore neighbors (orthogonal only)
|
||||
for (const [dx, dy] of [[1,0], [-1,0], [0,1], [0,-1]]) {
|
||||
const nx = current.x + dx;
|
||||
const ny = current.y + dy;
|
||||
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
||||
if (grid[ny][nx] === 1 || closed[ny][nx]) continue; // Obstacle or closed
|
||||
const g = current.g + 1;
|
||||
const h = heuristic([nx, ny]);
|
||||
const existing = open.find(n => n.x === nx && n.y === ny);
|
||||
if (!existing) {
|
||||
open.push({ x: nx, y: ny, g, h, f: g + h, parent: current });
|
||||
} else if (g < existing.g) {
|
||||
existing.g = g;
|
||||
existing.f = g + h;
|
||||
existing.parent = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user