-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcanvasLayoutManager.ts
More file actions
215 lines (176 loc) · 6.6 KB
/
canvasLayoutManager.ts
File metadata and controls
215 lines (176 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/**
* Canvas Layout Manager
* Handles grouping hosts by subnet and generating canvas node positions
*/
import { ParsedHost } from "./nmapParser";
import { getSubnetForIp } from "./subnetUtils";
export interface CanvasNode {
id: string;
type: "file";
file: string;
x: number;
y: number;
width: number;
height: number;
color?: string; // Obsidian canvas color: "1"-"6" for preset colors
}
export interface GroupedHosts {
[subnet: string]: ParsedHost[];
}
export class CanvasLayoutManager {
private nodeWidth: number;
private nodeHeight: number;
private nodeGap: number;
private groupPadding: number;
constructor(
nodeWidth: number = 475,
nodeHeight: number = 500,
nodeGap: number = 50,
groupPadding: number = 100
) {
this.nodeWidth = nodeWidth;
this.nodeHeight = nodeHeight;
this.nodeGap = nodeGap;
this.groupPadding = groupPadding;
}
/**
* Group hosts by their matching subnet
*/
groupHostsBySubnet(hosts: ParsedHost[], subnets: string[]): GroupedHosts {
const groups: GroupedHosts = {};
// Initialize groups for each subnet
for (const subnet of subnets) {
groups[subnet] = [];
}
// "Ungrouped" for hosts that don't match any subnet
groups["Ungrouped"] = [];
for (const host of hosts) {
let matched = false;
for (const subnet of subnets) {
if (getSubnetForIp(host.ipAddress, [subnet]) === subnet) {
groups[subnet].push(host);
matched = true;
break;
}
}
if (!matched) {
groups["Ungrouped"].push(host);
}
}
// Remove empty groups (except Ungrouped if it has items)
for (const key of Object.keys(groups)) {
if (groups[key].length === 0) {
delete groups[key];
}
}
return groups;
}
/**
* Generate canvas nodes with proper layout positions
*/
async generateCanvasNodes(
groupedHosts: GroupedHosts,
folder: string,
onCreateFile: (host: ParsedHost, filePath: string) => Promise<void>
): Promise<CanvasNode[]> {
const nodes: CanvasNode[] = [];
let groupY = 0;
// Sort subnet groups (Ungrouped last)
const sortedGroups = Object.keys(groupedHosts).sort((a, b) => {
if (a === "Ungrouped") return 1;
if (b === "Ungrouped") return -1;
return a.localeCompare(b);
});
// Generate random colors for each subnet (not for Ungrouped)
const subnetColors = this.generateSubnetColors(sortedGroups);
for (const subnet of sortedGroups) {
const hosts = groupedHosts[subnet];
if (hosts.length === 0) continue;
// Get color for this subnet (undefined for Ungrouped)
const subnetColor = subnetColors.get(subnet);
// Sort hosts by IP address (numeric sort)
hosts.sort((a, b) => this.compareIpAddresses(a.ipAddress, b.ipAddress));
// Calculate grid layout for this group
const nodesPerRow = Math.max(1, Math.ceil(Math.sqrt(hosts.length)));
let x = 0;
let y = groupY;
let maxHeightInRow = 0;
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
const fileName = `${host.ipAddress}.md`;
const filePath = folder ? `${folder}/${fileName}` : fileName;
// Create the file
await onCreateFile(host, filePath);
// Generate unique ID for the node
const nodeId = this.generateNodeId();
const node: CanvasNode = {
id: nodeId,
type: "file",
file: filePath,
x: x,
y: y,
width: this.nodeWidth,
height: this.nodeHeight,
};
// Apply color if subnet has one assigned
if (subnetColor) {
node.color = subnetColor;
}
nodes.push(node);
// Move to next position
x += this.nodeWidth + this.nodeGap;
maxHeightInRow = Math.max(maxHeightInRow, this.nodeHeight);
// Move to next row if needed
if ((i + 1) % nodesPerRow === 0) {
x = 0;
y += maxHeightInRow + this.nodeGap;
maxHeightInRow = 0;
}
}
// Calculate the height used by this group
const rowsUsed = Math.ceil(hosts.length / nodesPerRow);
const groupHeight = rowsUsed * (this.nodeHeight + this.nodeGap);
// Move to next group position
groupY += groupHeight + this.groupPadding;
}
return nodes;
}
/**
* Generate random colors for each subnet
* Obsidian canvas supports colors "1" through "6"
*/
private generateSubnetColors(subnets: string[]): Map<string, string> {
const colorMap = new Map<string, string>();
const availableColors = ["1", "2", "3", "4", "5", "6"];
// Shuffle colors for randomness
const shuffledColors = [...availableColors].sort(() => Math.random() - 0.5);
let colorIndex = 0;
for (const subnet of subnets) {
// Don't assign color to "Ungrouped"
if (subnet === "Ungrouped") continue;
// Assign color (cycle through if more subnets than colors)
colorMap.set(subnet, shuffledColors[colorIndex % shuffledColors.length]);
colorIndex++;
}
return colorMap;
}
/**
* Compare two IP addresses numerically
*/
private compareIpAddresses(a: string, b: string): number {
const partsA = a.split(".").map((n) => parseInt(n) || 0);
const partsB = b.split(".").map((n) => parseInt(n) || 0);
for (let i = 0; i < 4; i++) {
if (partsA[i] !== partsB[i]) {
return partsA[i] - partsB[i];
}
}
return 0;
}
/**
* Generate a unique node ID for canvas
*/
private generateNodeId(): string {
return Math.random().toString(36).substring(2, 18);
}
}