@hayro_o7/labyrinth 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
goalNodes?: string[];
|
|
9
9
|
nodeRadius?: number;
|
|
10
10
|
autoPlay?: boolean;
|
|
11
|
-
buttons?: boolean;
|
|
12
|
-
legend?: boolean;
|
|
13
|
-
stepCount?: boolean;
|
|
14
11
|
animationSpeed?: number;
|
|
15
12
|
colors?: ColorScheme;
|
|
16
13
|
showMultiGoal?: boolean;
|
|
@@ -23,9 +20,6 @@
|
|
|
23
20
|
goalNodes = [],
|
|
24
21
|
nodeRadius = 20,
|
|
25
22
|
autoPlay = false,
|
|
26
|
-
buttons = true,
|
|
27
|
-
legend = true,
|
|
28
|
-
stepCount = true,
|
|
29
23
|
animationSpeed = 100,
|
|
30
24
|
colors,
|
|
31
25
|
showMultiGoal = false,
|
|
@@ -134,10 +128,13 @@
|
|
|
134
128
|
const result = findOptimalMultiGoalPath(graph, computedStartNode, computedGoalNodes);
|
|
135
129
|
optimalPath = result.optimalPath;
|
|
136
130
|
|
|
137
|
-
// Generate visualization steps for
|
|
131
|
+
// Generate visualization steps by running BFS for each segment
|
|
138
132
|
steps = [];
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
const tour = [computedStartNode, ...computedGoalNodes];
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < tour.length - 1; i++) {
|
|
136
|
+
const segmentResult = bidirectionalBFS(graph, tour[i], tour[i + 1]);
|
|
137
|
+
steps.push(...segmentResult.steps);
|
|
141
138
|
}
|
|
142
139
|
} else {
|
|
143
140
|
const result = bidirectionalBFS(graph, computedStartNode, computedGoalNodes[0]);
|
|
@@ -246,8 +243,12 @@
|
|
|
246
243
|
onControls?.(controlApi);
|
|
247
244
|
});
|
|
248
245
|
|
|
249
|
-
const svgWidth =
|
|
250
|
-
|
|
246
|
+
const svgWidth = $derived(
|
|
247
|
+
Math.max(...Array.from(graph.nodes.values()).map(n => n.x)) + 100
|
|
248
|
+
);
|
|
249
|
+
const svgHeight = $derived(
|
|
250
|
+
Math.max(...Array.from(graph.nodes.values()).map(n => n.y)) + 100
|
|
251
|
+
);
|
|
251
252
|
</script>
|
|
252
253
|
|
|
253
254
|
<div class="graph-container" style={cssVars}>
|
|
@@ -306,69 +307,6 @@
|
|
|
306
307
|
{/each}
|
|
307
308
|
</svg>
|
|
308
309
|
|
|
309
|
-
<div class="controls">
|
|
310
|
-
{#if buttons}
|
|
311
|
-
<button onclick={play} disabled={isPlaying}>Play</button>
|
|
312
|
-
<button onclick={pause} disabled={!isPlaying}>Pause</button>
|
|
313
|
-
<button onclick={reset}>Reset</button>
|
|
314
|
-
<button onclick={stepBackward} disabled={isPlaying || currentStepIndex === 0}>
|
|
315
|
-
Step Back
|
|
316
|
-
</button>
|
|
317
|
-
<button onclick={stepForward} disabled={isPlaying || currentStepIndex === steps.length}>
|
|
318
|
-
Step Forward
|
|
319
|
-
</button>
|
|
320
|
-
{/if}
|
|
321
|
-
{#if stepCount}
|
|
322
|
-
<span class="step-counter">
|
|
323
|
-
Step: {currentStepIndex} / {steps.length}
|
|
324
|
-
</span>
|
|
325
|
-
{/if}
|
|
326
|
-
</div>
|
|
327
|
-
|
|
328
|
-
{#if legend}
|
|
329
|
-
<div class="legend">
|
|
330
|
-
<div class="legend-item">
|
|
331
|
-
<div class="legend-color" style="background-color: {colorScheme.start};"></div>
|
|
332
|
-
<span>Start</span>
|
|
333
|
-
</div>
|
|
334
|
-
<div class="legend-item">
|
|
335
|
-
<div class="legend-color" style="background-color: {colorScheme.end};"></div>
|
|
336
|
-
<span>Goal{computedGoalNodes.length > 1 ? 's' : ''}</span>
|
|
337
|
-
</div>
|
|
338
|
-
<div class="legend-item">
|
|
339
|
-
<div class="legend-color" style="background-color: {colorScheme.current};"></div>
|
|
340
|
-
<span>Forward</span>
|
|
341
|
-
</div>
|
|
342
|
-
<div class="legend-item">
|
|
343
|
-
<div class="legend-color" style="background-color: #f97316;"></div>
|
|
344
|
-
<span>Backward</span>
|
|
345
|
-
</div>
|
|
346
|
-
<div class="legend-item">
|
|
347
|
-
<div class="legend-color" style="background-color: {colorScheme.visiting};"></div>
|
|
348
|
-
<span>Forward Frontier</span>
|
|
349
|
-
</div>
|
|
350
|
-
<div class="legend-item">
|
|
351
|
-
<div class="legend-color" style="background-color: #fed7aa;"></div>
|
|
352
|
-
<span>Backward Frontier</span>
|
|
353
|
-
</div>
|
|
354
|
-
<div class="legend-item">
|
|
355
|
-
<div class="legend-color" style="background-color: {colorScheme.visited};"></div>
|
|
356
|
-
<span>Forward Visited</span>
|
|
357
|
-
</div>
|
|
358
|
-
<div class="legend-item">
|
|
359
|
-
<div class="legend-color" style="background-color: #fecaca;"></div>
|
|
360
|
-
<span>Backward Visited</span>
|
|
361
|
-
</div>
|
|
362
|
-
<div class="legend-item">
|
|
363
|
-
<div class="legend-color" style="background-color: #a855f7;"></div>
|
|
364
|
-
<span>Intersection</span>
|
|
365
|
-
</div>
|
|
366
|
-
<div class="legend-item">
|
|
367
|
-
<div class="legend-color" style="background-color: {colorScheme.path};"></div>
|
|
368
|
-
<span>Path</span>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
{/if}
|
|
372
310
|
</div>
|
|
373
311
|
|
|
374
312
|
<style>
|
|
@@ -377,7 +315,6 @@
|
|
|
377
315
|
flex-direction: column;
|
|
378
316
|
gap: 1rem;
|
|
379
317
|
align-items: center;
|
|
380
|
-
padding: 1rem;
|
|
381
318
|
}
|
|
382
319
|
|
|
383
320
|
.graph-svg {
|
|
@@ -386,64 +323,4 @@
|
|
|
386
323
|
border: 2px solid #e5e7eb;
|
|
387
324
|
border-radius: 0.5rem;
|
|
388
325
|
}
|
|
389
|
-
|
|
390
|
-
.controls {
|
|
391
|
-
display: flex;
|
|
392
|
-
gap: 0.5rem;
|
|
393
|
-
align-items: center;
|
|
394
|
-
flex-wrap: wrap;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
.controls button {
|
|
398
|
-
padding: 0.5rem 1rem;
|
|
399
|
-
border: none;
|
|
400
|
-
border-radius: 0.375rem;
|
|
401
|
-
cursor: pointer;
|
|
402
|
-
font-weight: 500;
|
|
403
|
-
transition: background-color 0.2s;
|
|
404
|
-
background-color: var(--graph-buttons);
|
|
405
|
-
color: var(--graph-buttonstext);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.controls button:hover:not(:disabled) {
|
|
409
|
-
background-color: var(--graph-buttonshover);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
.controls button:disabled {
|
|
413
|
-
background-color: var(--graph-buttonsdisabled);
|
|
414
|
-
cursor: not-allowed;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.step-counter {
|
|
418
|
-
padding: 0.5rem 1rem;
|
|
419
|
-
border-radius: 0.375rem;
|
|
420
|
-
font-weight: 500;
|
|
421
|
-
background-color: var(--graph-legend);
|
|
422
|
-
color: var(--graph-legend-text);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.legend {
|
|
426
|
-
display: flex;
|
|
427
|
-
gap: 1rem;
|
|
428
|
-
flex-wrap: wrap;
|
|
429
|
-
padding: 0.75rem;
|
|
430
|
-
border-radius: 0.5rem;
|
|
431
|
-
background-color: var(--graph-legend);
|
|
432
|
-
color: var(--graph-legend-text);
|
|
433
|
-
max-width: 650px;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
.legend-item {
|
|
437
|
-
display: flex;
|
|
438
|
-
align-items: center;
|
|
439
|
-
gap: 0.5rem;
|
|
440
|
-
font-size: 0.875rem;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.legend-color {
|
|
444
|
-
width: 1.5rem;
|
|
445
|
-
height: 1.5rem;
|
|
446
|
-
border-radius: 0.25rem;
|
|
447
|
-
border: 1px solid var(--graph-legend-text);
|
|
448
|
-
}
|
|
449
326
|
</style>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { GeneralGraph } from './types';
|
|
2
2
|
export interface GraphGeneratorOptions {
|
|
3
3
|
nodeCount: number;
|
|
4
|
-
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
maxConnectionsPerNode?: number;
|
|
7
|
+
connectionRadius?: number;
|
|
8
|
+
minNodeDistance?: number;
|
|
5
9
|
seed?: number;
|
|
6
10
|
}
|
|
7
11
|
export declare function generateRandomGraph(options: GraphGeneratorOptions): GeneralGraph;
|
|
12
|
+
export declare function findOppositeCornerNodes(graph: GeneralGraph): {
|
|
13
|
+
topLeft: string;
|
|
14
|
+
bottomRight: string;
|
|
15
|
+
};
|
package/dist/graph-generator.js
CHANGED
|
@@ -6,17 +6,31 @@ function seededRandom(seed) {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
export function generateRandomGraph(options) {
|
|
9
|
-
const { nodeCount,
|
|
9
|
+
const { nodeCount, width = 600, height = 600, maxConnectionsPerNode = 5, connectionRadius = 150, minNodeDistance = 30, seed } = options;
|
|
10
10
|
const random = seed !== undefined ? seededRandom(seed) : Math.random;
|
|
11
11
|
const nodes = new Map();
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const centerY = 300;
|
|
12
|
+
const padding = 50;
|
|
13
|
+
const nodePositions = [];
|
|
14
|
+
// Generate node positions with minimum distance constraint
|
|
16
15
|
for (let i = 0; i < nodeCount; i++) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
16
|
+
let x, y;
|
|
17
|
+
let attempts = 0;
|
|
18
|
+
const maxAttempts = 100;
|
|
19
|
+
do {
|
|
20
|
+
x = padding + random() * (width - 2 * padding);
|
|
21
|
+
y = padding + random() * (height - 2 * padding);
|
|
22
|
+
attempts++;
|
|
23
|
+
// Check if position is far enough from existing nodes
|
|
24
|
+
const tooClose = nodePositions.some(pos => {
|
|
25
|
+
const dx = pos.x - x;
|
|
26
|
+
const dy = pos.y - y;
|
|
27
|
+
return Math.sqrt(dx * dx + dy * dy) < minNodeDistance;
|
|
28
|
+
});
|
|
29
|
+
if (!tooClose || attempts >= maxAttempts) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
} while (true);
|
|
33
|
+
nodePositions.push({ x, y });
|
|
20
34
|
nodes.set(`n${i}`, {
|
|
21
35
|
id: `n${i}`,
|
|
22
36
|
x,
|
|
@@ -24,34 +38,88 @@ export function generateRandomGraph(options) {
|
|
|
24
38
|
neighbors: []
|
|
25
39
|
});
|
|
26
40
|
}
|
|
27
|
-
// Connect nodes
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const connectedNode = Array.from(connected)[Math.floor(random() * connected.size)];
|
|
34
|
-
const unconnectedNode = Array.from(unconnected)[Math.floor(random() * unconnected.size)];
|
|
35
|
-
nodes.get(connectedNode).neighbors.push(unconnectedNode);
|
|
36
|
-
nodes.get(unconnectedNode).neighbors.push(connectedNode);
|
|
37
|
-
connected.add(unconnectedNode);
|
|
38
|
-
unconnected.delete(unconnectedNode);
|
|
39
|
-
}
|
|
40
|
-
// Add additional edges to reach target average degree
|
|
41
|
-
const targetEdges = Math.floor((nodeCount * avgDegree) / 2);
|
|
42
|
-
const currentEdges = nodeCount - 1; // from spanning tree
|
|
43
|
-
const edgesToAdd = targetEdges - currentEdges;
|
|
44
|
-
for (let i = 0; i < edgesToAdd; i++) {
|
|
45
|
-
const node1Id = nodeIds[Math.floor(random() * nodeIds.length)];
|
|
46
|
-
const node2Id = nodeIds[Math.floor(random() * nodeIds.length)];
|
|
47
|
-
if (node1Id === node2Id)
|
|
41
|
+
// Connect nodes based on proximity
|
|
42
|
+
const nodeArray = Array.from(nodes.values());
|
|
43
|
+
for (let i = 0; i < nodeArray.length; i++) {
|
|
44
|
+
const node = nodeArray[i];
|
|
45
|
+
// Skip if already at max connections
|
|
46
|
+
if (node.neighbors.length >= maxConnectionsPerNode)
|
|
48
47
|
continue;
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// Find nearby nodes
|
|
49
|
+
const distances = [];
|
|
50
|
+
for (let j = 0; j < nodeArray.length; j++) {
|
|
51
|
+
if (i === j)
|
|
52
|
+
continue;
|
|
53
|
+
const other = nodeArray[j];
|
|
54
|
+
// Skip if other node is at max connections
|
|
55
|
+
if (other.neighbors.length >= maxConnectionsPerNode)
|
|
56
|
+
continue;
|
|
57
|
+
// Skip if already connected
|
|
58
|
+
if (node.neighbors.includes(other.id))
|
|
59
|
+
continue;
|
|
60
|
+
const dx = node.x - other.x;
|
|
61
|
+
const dy = node.y - other.y;
|
|
62
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
63
|
+
if (distance <= connectionRadius) {
|
|
64
|
+
distances.push({ nodeId: other.id, distance });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Sort by distance and connect to closest nodes
|
|
68
|
+
distances.sort((a, b) => a.distance - b.distance);
|
|
69
|
+
const connectionsNeeded = Math.min(maxConnectionsPerNode - node.neighbors.length, distances.length);
|
|
70
|
+
for (let k = 0; k < connectionsNeeded; k++) {
|
|
71
|
+
const targetId = distances[k].nodeId;
|
|
72
|
+
const targetNode = nodes.get(targetId);
|
|
73
|
+
// Only connect if target isn't at max
|
|
74
|
+
if (targetNode.neighbors.length < maxConnectionsPerNode) {
|
|
75
|
+
node.neighbors.push(targetId);
|
|
76
|
+
targetNode.neighbors.push(node.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Ensure all nodes have at least one connection
|
|
81
|
+
for (const node of nodeArray) {
|
|
82
|
+
if (node.neighbors.length === 0) {
|
|
83
|
+
// Find closest node that can accept a connection
|
|
84
|
+
let closestNode = null;
|
|
85
|
+
let closestDistance = Infinity;
|
|
86
|
+
for (const other of nodeArray) {
|
|
87
|
+
if (other.id === node.id)
|
|
88
|
+
continue;
|
|
89
|
+
if (other.neighbors.length >= maxConnectionsPerNode)
|
|
90
|
+
continue;
|
|
91
|
+
const dx = node.x - other.x;
|
|
92
|
+
const dy = node.y - other.y;
|
|
93
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
94
|
+
if (distance < closestDistance) {
|
|
95
|
+
closestDistance = distance;
|
|
96
|
+
closestNode = other;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (closestNode) {
|
|
100
|
+
node.neighbors.push(closestNode.id);
|
|
101
|
+
closestNode.neighbors.push(node.id);
|
|
102
|
+
}
|
|
54
103
|
}
|
|
55
104
|
}
|
|
56
105
|
return { nodes };
|
|
57
106
|
}
|
|
107
|
+
export function findOppositeCornerNodes(graph) {
|
|
108
|
+
const nodeArray = Array.from(graph.nodes.values());
|
|
109
|
+
let topLeftNode = nodeArray[0];
|
|
110
|
+
let bottomRightNode = nodeArray[0];
|
|
111
|
+
for (const node of nodeArray) {
|
|
112
|
+
// Top-left: minimize x + y
|
|
113
|
+
if (node.x + node.y < topLeftNode.x + topLeftNode.y) {
|
|
114
|
+
topLeftNode = node;
|
|
115
|
+
}
|
|
116
|
+
// Bottom-right: maximize x + y
|
|
117
|
+
if (node.x + node.y > bottomRightNode.x + bottomRightNode.y) {
|
|
118
|
+
bottomRightNode = node;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
topLeft: topLeftNode.id,
|
|
123
|
+
bottomRight: bottomRightNode.id
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { generateLabyrinth } from './labyrinth-generator';
|
|
2
|
-
export { generateRandomGraph } from './graph-generator';
|
|
2
|
+
export { generateRandomGraph, findOppositeCornerNodes } from './graph-generator';
|
|
3
3
|
export { dijkstra } from './algorithms/dijkstra';
|
|
4
4
|
export { astar } from './algorithms/astar';
|
|
5
5
|
export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { generateLabyrinth } from './labyrinth-generator';
|
|
2
|
-
export { generateRandomGraph } from './graph-generator';
|
|
2
|
+
export { generateRandomGraph, findOppositeCornerNodes } from './graph-generator';
|
|
3
3
|
export { dijkstra } from './algorithms/dijkstra';
|
|
4
4
|
export { astar } from './algorithms/astar';
|
|
5
5
|
export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
|