diff --git a/lib/algorithms/bfs.dart b/lib/algorithms/bfs.dart new file mode 100644 index 0000000..3e793eb --- /dev/null +++ b/lib/algorithms/bfs.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:visual_graphs/graph_editor/models/graph.dart'; +import 'package:visual_graphs/helpers/queue.dart'; + +class Pair { + final Vertex v; + final Edge? e; + + Pair(this.v, this.e); +} + +class BreadthFirstSearch { + final Graph graph; + final Vertex start; + + final QueueDS queue = QueueDS(); + + final Set visited = {}; + final Set seen = {}; + + BreadthFirstSearch({required this.graph, required this.start}); + + void search() async { + print("Starting BFS from ${start.label}"); + await see(Pair(start, null)); + + while (queue.isNotEmpty) { + await Future.delayed(const Duration(seconds: 1)); + + final pair = queue.dequeue(); + await visit(pair); + + var neighbours = pair.v.neighbours; + + for (final neighbour in neighbours.keys) { + if (!seen.contains(neighbour)) { + await see(Pair(neighbour, neighbours[neighbour]!.first)); + } + } + } + } + + Future see(Pair pair) async { + print("Seeing ${pair.v.label}"); + + queue.enqueue(pair); + + seen.add(pair.v); + + if (pair.e != null && pair.e?.component != null) { + pair.e?.component + ?..color = Colors.orange + ..hoverColor = Colors.orangeAccent + ..hoverOut(); + + await Future.delayed(const Duration(milliseconds: 100)); + } + + pair.v.component + ..color = Colors.orange + ..hoverColor = Colors.orangeAccent + ..onHoverExit(); + } + + Future visit(Pair pair) async { + print("Visiting ${pair.v.label} via ${pair.e}"); + + visited.add(pair.v); + + if (pair.e != null && pair.e?.component != null) { + pair.e?.component + ?..color = Colors.green + ..hoverColor = Colors.greenAccent + ..hoverOut(); + + await Future.delayed(const Duration(milliseconds: 100)); + } + + pair.v.component + ..color = Colors.green + ..hoverColor = Colors.greenAccent + ..onHoverExit(); + } +} diff --git a/lib/algorithms/dfs.dart b/lib/algorithms/dfs.dart new file mode 100644 index 0000000..065434e --- /dev/null +++ b/lib/algorithms/dfs.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:visual_graphs/graph_editor/models/graph.dart'; +import 'package:visual_graphs/helpers/stack.dart'; + +class DepthFirstSearch { + final Graph graph; + final Vertex start; + final StackDS stack = StackDS(); + + final Set visited = {}; + final Set seen = {}; + + DepthFirstSearch({required this.graph, required this.start}); + + void search() async { + print("Starting DFS from ${start.label}"); + see(start); + + while (stack.isNotEmpty) { + await Future.delayed(const Duration(seconds: 1)); + + final vertex = stack.pop(); + visit(vertex); + + var neighbours = vertex.neighbours.keys.toList() + ..sort((a, b) => a.id.compareTo(b.id)); + + await Future.delayed(const Duration(milliseconds: 100)); + for (final neighbour in neighbours) { + if (!seen.contains(neighbour)) { + see(neighbour); + } + } + } + } + + void see(Vertex vertex) { + print("Seeing ${vertex.label}"); + stack.push(vertex); + seen.add(vertex); + vertex.component.paint = Paint() + ..color = Colors.red + ..style = PaintingStyle.fill; + } + + void visit(Vertex vertex) { + print("Visiting ${vertex.label}"); + visited.add(vertex); + vertex.component.paint = Paint() + ..color = Colors.green + ..style = PaintingStyle.fill; + } +} diff --git a/lib/app.dart b/lib/app.dart index dad9b17..b610b8f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,4 @@ -import 'package:visual_graphs/game_screen.dart'; +import 'package:visual_graphs/graph_editor/graph_editor.dart'; import 'package:flutter/material.dart'; class App extends StatelessWidget { @@ -7,7 +7,7 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - home: const GameScreen(), + home: GraphEditorWidget(), title: "Visual Graphs", theme: ThemeData( useMaterial3: true, diff --git a/lib/game_screen.dart b/lib/game_screen.dart deleted file mode 100644 index caa3105..0000000 --- a/lib/game_screen.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/widgets.dart'; -import 'package:visual_graphs/components/graph_game.dart'; -import 'package:flame/game.dart'; -import 'package:flutter/material.dart'; -import 'package:visual_graphs/widgets/game_info_box.dart'; - -class GameScreen extends StatefulWidget { - const GameScreen({super.key}); - - @override - State createState() => _GameScreenState(); -} - -class _GameScreenState extends State { - late GraphGame game; - - @override - void initState() { - super.initState(); - game = GraphGame(); - game.parentWidgetState = this; - } - - @override - Widget build(BuildContext context) { - game.context = context; - bool smallScreen = MediaQuery.of(context).size.width < 600; - - return Scaffold( - body: Stack( - children: [ - GameWidget( - game: game, - ), - Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SegmentedButton( - segments: [ - ButtonSegment( - value: GameMode.defaultMode, - label: !smallScreen ? const Text("Default") : null, - icon: const Icon(Icons.mouse), - tooltip: smallScreen ? "Default Mode" : null, - ), - ButtonSegment( - value: GameMode.addVertex, - label: !smallScreen ? const Text("Add Vertex") : null, - icon: const Icon(Icons.add), - tooltip: smallScreen ? "Add Vertex" : null, - ), - ButtonSegment( - value: GameMode.addEdge, - label: !smallScreen ? const Text("Add Edge") : null, - icon: const Icon(Icons.linear_scale), - tooltip: smallScreen ? "Add Edge" : null, - ), - ButtonSegment( - value: GameMode.deleteComponent, - label: !smallScreen ? const Text("Delete") : null, - icon: const Icon(Icons.delete), - tooltip: smallScreen ? "Delete" : null, - ), - ], - style: SegmentedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Theme.of(context).colorScheme.primary, - selectedForegroundColor: - Theme.of(context).colorScheme.onPrimary, - selectedBackgroundColor: - Theme.of(context).colorScheme.primary, - ), - showSelectedIcon: false, - selected: {game.gameMode}, - onSelectionChanged: (p0) => setState(() { - game.gameMode = p0.first; - }), - ), - const SizedBox(height: 20), - ElevatedButton.icon( - onPressed: () => setState(() { - game.clearGraph(); - }), - label: const Text("Clear Graph"), - icon: const Icon(Icons.clear), - ), - Expanded(child: Container()), - IconButton( - onPressed: () => game.centerCamera(), - icon: const Icon( - Icons.center_focus_strong, - color: Colors.white, - ), - tooltip: "Center Camera", - ), - ], - ), - ), - ], - ), - floatingActionButton: FloatingActionButton.small( - tooltip: "Help", - onPressed: () { - showDialog( - context: context, - builder: (context) => const GameInfoBox(), - ); - }, - child: const Icon(Icons.help), - ), - ); - } -} diff --git a/lib/components/edge_component.dart b/lib/graph_editor/components/edge_component.dart similarity index 91% rename from lib/components/edge_component.dart rename to lib/graph_editor/components/edge_component.dart index 1f1b2c9..db04dac 100644 --- a/lib/components/edge_component.dart +++ b/lib/graph_editor/components/edge_component.dart @@ -1,28 +1,28 @@ import 'dart:math' as math; import 'dart:ui' as ui; -import 'package:visual_graphs/components/graph_game.dart'; -import 'package:visual_graphs/models/graph.dart'; +import 'package:visual_graphs/graph_editor/components/graph_game.dart'; +import 'package:visual_graphs/graph_editor/models/graph.dart'; import 'package:flame/components.dart'; import 'package:flutter/material.dart'; -import 'package:visual_graphs/widgets/edge_info_box.dart'; +import 'package:visual_graphs/graph_editor/widgets/edge_info_box.dart'; class EdgeComponent extends ShapeComponent with HasGameRef { Edge edge; Color color = Colors.white; Color hoverColor = Colors.white; - Color _color = const Color(0x00000000); + Path path = Path(); double labelTextSize = 14; List pathPoints = []; + Color _paintColor = const Color(0x00000000); + final PaintingStyle _paintPaintingStyle = PaintingStyle.stroke; + double _paintStrokeWidth = 2; + EdgeComponent(this.edge) { - _color = color; + _paintColor = color; anchor = Anchor.topLeft; position = edge.from.component.position; - paint = Paint() - ..color = _color - ..style = PaintingStyle.stroke - ..strokeWidth = 2; } @override @@ -61,25 +61,27 @@ class EdgeComponent extends ShapeComponent with HasGameRef { } void hoverIn() { - _color = + _paintColor = gameRef.gameMode == GameMode.deleteComponent ? Colors.red : hoverColor; - paint - ..color = _color - ..strokeWidth = 3; + _paintStrokeWidth = 3; + labelTextSize = 16; } void hoverOut() { - _color = color; - paint - ..color = _color - ..strokeWidth = 2; + _paintColor = color; + _paintStrokeWidth = 2; + labelTextSize = 14; } // rendering @override void render(Canvas canvas) { + paint = Paint() + ..color = _paintColor + ..style = _paintPaintingStyle + ..strokeWidth = _paintStrokeWidth; if (edge.isSelfEdge) { renderSelfEdge(canvas); } else { @@ -170,7 +172,7 @@ class EdgeComponent extends ShapeComponent with HasGameRef { (index + 1) * edge.from.component.radius * math.pow(0.975, index); path = Path(); - var center = gameRef.graph.center; + var center = gameRef.graph.geometricCenter; var centerOffset = Offset( center.x - edge.from.component.position.x, center.y - edge.from.component.position.y, @@ -232,7 +234,7 @@ class EdgeComponent extends ShapeComponent with HasGameRef { var paragraphFontSize = labelTextSize; var fontSize = paragraphFontSize + 2; var fontWeight = FontWeight.normal; - var fontColor = _color; + var fontColor = _paintColor; var text = edge.weight.toString(); final paragraphBuilder = ui.ParagraphBuilder( diff --git a/lib/components/graph_game.dart b/lib/graph_editor/components/graph_game.dart similarity index 62% rename from lib/components/graph_game.dart rename to lib/graph_editor/components/graph_game.dart index 387592b..8c7be13 100644 --- a/lib/components/graph_game.dart +++ b/lib/graph_editor/components/graph_game.dart @@ -1,13 +1,15 @@ -import 'package:visual_graphs/components/half_edge_component.dart'; -import 'package:visual_graphs/models/graph.dart'; +import 'package:visual_graphs/graph_editor/components/half_edge_component.dart'; +import 'package:visual_graphs/graph_editor/models/graph.dart'; import 'package:flame/components.dart'; import 'package:flame/events.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:visual_graphs/helpers/stack.dart'; enum GameMode { + lockedMode, defaultMode, addVertex, addEdge, @@ -18,35 +20,48 @@ class GraphGame extends FlameGame with PanDetector, TapCallbacks, MouseMovementDetector, KeyboardEvents { late Graph graph; - GameMode _gameMode = GameMode.defaultMode; + GameMode _gameMode = GameMode.lockedMode; GameMode get gameMode => _gameMode; + + bool addEdgeModeIsWeighted = false; + bool addEdgeModeIsDirected = false; + late BuildContext context; late State parentWidgetState; + SizedStackDS undoStack = SizedStackDS(10); + GraphGame() { graph = Graph(); graph.addVertex(Vertex(label: "0", position: Vector2(-100, -100))); graph.addVertex(Vertex(label: "1", position: Vector2(100, 100))); + graph.addVertex(Vertex(label: "2", position: Vector2(100, -100))); + graph.addVertex(Vertex(label: "3", position: Vector2(-100, 100))); + graph.addEdge(Edge( from: graph.vertices[0], to: graph.vertices[1], - weight: 10, + weight: 5, )); graph.addEdge(Edge( - from: graph.vertices[0], - to: graph.vertices[0], - weight: 5, + from: graph.vertices[1], + to: graph.vertices[2], + weight: 3, + isDirected: true)); + graph.addEdge(Edge( + from: graph.vertices[2], + to: graph.vertices[3], + weight: 1, )); graph.addEdge(Edge( - from: graph.vertices[0], + from: graph.vertices[3], to: graph.vertices[0], - weight: 10, isDirected: true, )); graph.addEdge(Edge( from: graph.vertices[0], - to: graph.vertices[1], - weight: 5, + to: graph.vertices[0], + weight: 2, isDirected: true, )); } @@ -99,6 +114,11 @@ class GraphGame extends FlameGame @override void onPanUpdate(DragUpdateInfo info) { + if (gameMode == GameMode.lockedMode) { + camera.viewfinder.position -= info.delta.global; + return; + } + if (gameMode == GameMode.defaultMode || gameMode == GameMode.deleteComponent) { if (graph.hoveredVertex != null) { @@ -119,6 +139,7 @@ class GraphGame extends FlameGame case GameMode.deleteComponent: for (var edge in graph.edges) { if (edge.component.checkHover(cursorPosition)) { + saveHistory(); graph.removeEdge(edge); refreshGraphComponents(); break; @@ -144,6 +165,8 @@ class GraphGame extends FlameGame } void addVertex(TapDownEvent event) { + saveHistory(); + // Convert the global tap position to the local position of the camera viewfinder var position = camera.viewfinder.globalToLocal(event.localPosition); graph.addVertex(Vertex( @@ -154,6 +177,8 @@ class GraphGame extends FlameGame } void deleteVertex(Vertex v) { + saveHistory(); + graph.removeVertex(v); refreshGraphComponents(); } @@ -164,9 +189,19 @@ class GraphGame extends FlameGame selectedVertex = v; world.add(HalfEdgeComponent(v.component)); } else { - graph.addEdge(Edge(from: selectedVertex!, to: v)); + saveHistory(); + + var edge = Edge( + from: selectedVertex!, + to: v, + isDirected: addEdgeModeIsDirected, + ); + graph.addEdge(edge); selectedVertex = null; refreshGraphComponents(); + if (addEdgeModeIsWeighted) { + edge.component.showInfo(context); + } } } @@ -180,6 +215,10 @@ class GraphGame extends FlameGame case GameMode.addEdge: mouseCursor = SystemMouseCursors.precise; break; + case GameMode.lockedMode: + mouseCursor = SystemMouseCursors.grab; + undoStack.clear(); + break; default: mouseCursor = SystemMouseCursors.basic; } @@ -187,57 +226,84 @@ class GraphGame extends FlameGame } void clearGraph() { + saveHistory(); graph.clear(); refreshGraphComponents(); } - void centerCamera() => camera.viewfinder.position = graph.center; + void saveHistory() { + // ignore: invalid_use_of_protected_member + parentWidgetState.setState(() {}); + undoStack.push(graph.clone()); + } + + void undo() { + if (undoStack.isNotEmpty) { + graph = undoStack.pop(); + refreshGraphComponents(); + } + } + + void centerCamera() { + camera.viewfinder.position = graph.geometricCenter; + } @override KeyEventResult onKeyEvent( KeyEvent event, Set keysPressed) { - if (keysPressed.contains(LogicalKeyboardKey.escape)) { - switch (gameMode) { - case GameMode.addVertex: - case GameMode.deleteComponent: - gameMode = GameMode.defaultMode; - break; - case GameMode.addEdge: - if (selectedVertex != null) { - selectedVertex = null; - refreshGraphComponents(); - } else { + if (gameMode != GameMode.lockedMode) { + if (keysPressed.contains(LogicalKeyboardKey.controlLeft) || + keysPressed.contains(LogicalKeyboardKey.controlRight)) { + if (keysPressed.contains(LogicalKeyboardKey.keyZ)) { + undo(); + return KeyEventResult.handled; + } + } + + if (keysPressed.contains(LogicalKeyboardKey.escape)) { + switch (gameMode) { + case GameMode.addVertex: + case GameMode.deleteComponent: gameMode = GameMode.defaultMode; - } - break; - default: + break; + case GameMode.addEdge: + if (selectedVertex != null) { + selectedVertex = null; + refreshGraphComponents(); + } else { + gameMode = GameMode.defaultMode; + } + break; + default: + } + // ignore: invalid_use_of_protected_member + parentWidgetState.setState(() {}); + return KeyEventResult.handled; + } + if (keysPressed.contains(LogicalKeyboardKey.keyE)) { + gameMode = GameMode.addEdge; + // ignore: invalid_use_of_protected_member + parentWidgetState.setState(() {}); + return KeyEventResult.handled; + } + if (keysPressed.contains(LogicalKeyboardKey.keyV)) { + gameMode = GameMode.addVertex; + // ignore: invalid_use_of_protected_member + parentWidgetState.setState(() {}); + return KeyEventResult.handled; + } + if (keysPressed.contains(LogicalKeyboardKey.keyX)) { + gameMode = GameMode.deleteComponent; + // ignore: invalid_use_of_protected_member + parentWidgetState.setState(() {}); + return KeyEventResult.handled; } - // ignore: invalid_use_of_protected_member - parentWidgetState.setState(() {}); - return KeyEventResult.handled; } + if (keysPressed.contains(LogicalKeyboardKey.keyC)) { centerCamera(); return KeyEventResult.handled; } - if (keysPressed.contains(LogicalKeyboardKey.keyE)) { - gameMode = GameMode.addEdge; - // ignore: invalid_use_of_protected_member - parentWidgetState.setState(() {}); - return KeyEventResult.handled; - } - if (keysPressed.contains(LogicalKeyboardKey.keyV)) { - gameMode = GameMode.addVertex; - // ignore: invalid_use_of_protected_member - parentWidgetState.setState(() {}); - return KeyEventResult.handled; - } - if (keysPressed.contains(LogicalKeyboardKey.keyX)) { - gameMode = GameMode.deleteComponent; - // ignore: invalid_use_of_protected_member - parentWidgetState.setState(() {}); - return KeyEventResult.handled; - } // camera movement if (keysPressed.contains(LogicalKeyboardKey.arrowUp) || diff --git a/lib/components/half_edge_component.dart b/lib/graph_editor/components/half_edge_component.dart similarity index 88% rename from lib/components/half_edge_component.dart rename to lib/graph_editor/components/half_edge_component.dart index ea166a9..5a7b42a 100644 --- a/lib/components/half_edge_component.dart +++ b/lib/graph_editor/components/half_edge_component.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'dart:ui'; -import 'package:visual_graphs/components/graph_game.dart'; -import 'package:visual_graphs/components/vertex_component.dart'; +import 'package:visual_graphs/graph_editor/components/graph_game.dart'; +import 'package:visual_graphs/graph_editor/components/vertex_component.dart'; import 'package:flame/components.dart'; class HalfEdgeComponent extends ShapeComponent with HasGameRef { diff --git a/lib/components/vertex_component.dart b/lib/graph_editor/components/vertex_component.dart similarity index 81% rename from lib/components/vertex_component.dart rename to lib/graph_editor/components/vertex_component.dart index d312f00..86f4bac 100644 --- a/lib/components/vertex_component.dart +++ b/lib/graph_editor/components/vertex_component.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'dart:ui' as ui; -import 'package:visual_graphs/components/graph_game.dart'; -import 'package:visual_graphs/models/graph.dart'; +import 'package:visual_graphs/graph_editor/components/graph_game.dart'; +import 'package:visual_graphs/graph_editor/models/graph.dart'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/events.dart'; import 'package:flutter/material.dart'; -import 'package:visual_graphs/widgets/vertex_info_box.dart'; +import 'package:visual_graphs/graph_editor/widgets/vertex_info_box.dart'; class VertexComponent extends ShapeComponent with @@ -19,23 +19,25 @@ class VertexComponent extends ShapeComponent CollisionCallbacks implements SizeProvider { + Vertex vertex; + double radius = 20; - double circleRadius = 20; + double _circleRadius = 20; + Color color = Colors.blue; Color hoverColor = Colors.lightBlue; - Vertex vertex; + + Color _paintColor = const Color(0x00000000); + final PaintingStyle _paintPaintingStyle = PaintingStyle.fill; VertexComponent(position, {required this.vertex}) : super( position: position, anchor: Anchor.center, ) { + _paintColor = color; width = radius * 2; height = radius * 2; - paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - // debugMode = true; } @override @@ -87,24 +89,18 @@ class VertexComponent extends ShapeComponent gameRef.graph.hoveredVertex = vertex; switch (gameRef.gameMode) { case GameMode.defaultMode: - paint = Paint() - ..color = hoverColor - ..style = PaintingStyle.fill; - circleRadius = radius + 2; + _paintColor = hoverColor; + _circleRadius = radius + 2; gameRef.mouseCursor = SystemMouseCursors.click; break; case GameMode.deleteComponent: - paint = Paint() - ..color = Colors.redAccent - ..style = PaintingStyle.fill; - circleRadius = radius + 2; + _paintColor = Colors.redAccent; + _circleRadius = radius + 2; gameRef.mouseCursor = SystemMouseCursors.click; break; case GameMode.addEdge: - paint = Paint() - ..color = hoverColor - ..style = PaintingStyle.fill; - circleRadius = radius + 2; + _paintColor = hoverColor; + _circleRadius = radius + 2; break; default: } @@ -121,20 +117,25 @@ class VertexComponent extends ShapeComponent break; default: } - circleRadius = radius; - paint = Paint() - ..color = color - ..style = PaintingStyle.fill; + _circleRadius = radius; + _paintColor = color; super.onHoverExit(); } // Rendering + void refreshPaint() { + paint = Paint() + ..color = _paintColor + ..style = _paintPaintingStyle; + } + @override void render(Canvas canvas) { + refreshPaint(); canvas.drawCircle( Offset(radius, radius), - circleRadius, + _circleRadius, paint, ); renderText(canvas); diff --git a/lib/graph_editor/graph_editor.dart b/lib/graph_editor/graph_editor.dart new file mode 100644 index 0000000..f9c2a47 --- /dev/null +++ b/lib/graph_editor/graph_editor.dart @@ -0,0 +1,223 @@ +// import 'package:visual_graphs/algorithms/bfs.dart'; +// import 'package:visual_graphs/algorithms/dfs.dart'; +import 'package:visual_graphs/graph_editor/components/graph_game.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:visual_graphs/graph_editor/widgets/game_info_box.dart'; + +final squareIconButtonStyle = ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), +); + +class GraphEditorWidget extends StatefulWidget { + GraphEditorWidget({super.key}) { + game = GraphGame(); + } + + @override + State createState() => _GraphEditorWidgetState(); + + late final GraphGame game; +} + +class _GraphEditorWidgetState extends State { + @override + void initState() { + super.initState(); + widget.game.parentWidgetState = this; + } + + @override + Widget build(BuildContext context) { + widget.game.context = context; + bool smallScreen = MediaQuery.of(context).size.width < 600; + + return Scaffold( + body: Stack( + children: [ + GameWidget( + game: widget.game, + ), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.game.gameMode == GameMode.lockedMode) + IconButton.filled( + onPressed: () => setState(() { + widget.game.gameMode = GameMode.defaultMode; + }), + icon: const Icon(Icons.edit), + style: squareIconButtonStyle, + ), + if (widget.game.gameMode != GameMode.lockedMode) + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton.filledTonal( + onPressed: () => setState(() { + widget.game.gameMode = GameMode.lockedMode; + }), + icon: const Icon(Icons.done), + style: squareIconButtonStyle, + ), + Expanded(child: Container()), + IconButton.filled( + icon: const Icon(Icons.undo), + onPressed: widget.game.undoStack.isEmpty + ? null + : () { + setState(() { + widget.game.undo(); + }); + }, + tooltip: "Undo", + disabledColor: Colors.grey, + ), + ], + ), + const SizedBox(height: 10), + SegmentedButton( + segments: [ + ButtonSegment( + value: GameMode.defaultMode, + label: !smallScreen ? const Text("Default") : null, + icon: const Icon(Icons.mouse), + tooltip: smallScreen ? "Default Mode" : null, + ), + ButtonSegment( + value: GameMode.addVertex, + label: + !smallScreen ? const Text("Add Vertex") : null, + icon: const Icon(Icons.add), + tooltip: smallScreen ? "Add Vertex" : null, + ), + ButtonSegment( + value: GameMode.addEdge, + label: !smallScreen ? const Text("Add Edge") : null, + icon: const Icon(Icons.linear_scale), + tooltip: smallScreen ? "Add Edge" : null, + ), + ButtonSegment( + value: GameMode.deleteComponent, + label: !smallScreen ? const Text("Delete") : null, + icon: const Icon(Icons.delete), + tooltip: smallScreen ? "Delete" : null, + ), + ], + style: SegmentedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.surface, + foregroundColor: + Theme.of(context).colorScheme.primary, + selectedForegroundColor: + Theme.of(context).colorScheme.onPrimary, + selectedBackgroundColor: + Theme.of(context).colorScheme.primary, + ), + showSelectedIcon: false, + selected: {widget.game.gameMode}, + onSelectionChanged: (p0) => setState(() { + widget.game.gameMode = p0.first; + }), + ), + const SizedBox(height: 10), + if (widget.game.gameMode == GameMode.addEdge) + Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Checkbox.adaptive( + visualDensity: VisualDensity.compact, + value: widget.game.addEdgeModeIsDirected, + onChanged: (value) { + setState(() { + widget.game.addEdgeModeIsDirected = + value!; + }); + }, + ), + const Text("Directed"), + const SizedBox(width: 20), + Checkbox.adaptive( + value: widget.game.addEdgeModeIsWeighted, + visualDensity: VisualDensity.compact, + onChanged: (value) { + setState(() { + widget.game.addEdgeModeIsWeighted = + value!; + }); + }, + ), + const Text("Weighted"), + ], + ), + ), + ), + if (widget.game.gameMode == GameMode.addEdge) + const SizedBox(height: 10), + ElevatedButton.icon( + onPressed: () => setState(() { + widget.game.clearGraph(); + }), + label: const Text("Clear Graph"), + icon: const Icon(Icons.clear), + ), + ], + ), + Expanded(child: Container()), + IconButton( + onPressed: () => widget.game.centerCamera(), + icon: const Icon( + Icons.center_focus_strong, + color: Colors.white, + ), + tooltip: "Center Camera", + ), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.small( + tooltip: "Help", + onPressed: () { + showDialog( + context: context, + builder: (context) => const GameInfoBox(), + ); + }, + child: const Icon(Icons.help), + ), + // floatingActionButton: FloatingActionButton( + // onPressed: () { + // var bfs = BreadthFirstSearch( + // graph: game.graph, start: game.graph.vertices.first); + // bfs.search(); + // }, + // child: const Icon(Icons.star), + // ), + ); + } +} diff --git a/lib/models/edge.dart b/lib/graph_editor/models/edge.dart similarity index 93% rename from lib/models/edge.dart rename to lib/graph_editor/models/edge.dart index 1182548..4e69056 100644 --- a/lib/models/edge.dart +++ b/lib/graph_editor/models/edge.dart @@ -47,7 +47,7 @@ class Edge extends _Edge { } double computePositionedIndex() { - var edgeList = component.gameRef.graph.edgesBetween(from, to); + var edgeList = component.gameRef.graph.getEdgesBetween(from, to); edgeList.sort((a, b) => a.id.compareTo(b.id)); var index = edgeList.indexOf(this); @@ -71,7 +71,7 @@ class Edge extends _Edge { } int computeIndex() { - var edgeList = component.gameRef.graph.edgesBetween(from, to); + var edgeList = component.gameRef.graph.getEdgesBetween(from, to); edgeList.sort((a, b) => a.id.compareTo(b.id)); var index = edgeList.indexOf(this); return index; diff --git a/lib/models/graph.dart b/lib/graph_editor/models/graph.dart similarity index 67% rename from lib/models/graph.dart rename to lib/graph_editor/models/graph.dart index 6aa1aab..1d94d8d 100644 --- a/lib/models/graph.dart +++ b/lib/graph_editor/models/graph.dart @@ -1,13 +1,23 @@ -import 'package:visual_graphs/components/edge_component.dart'; -import 'package:visual_graphs/components/vertex_component.dart'; +import 'package:visual_graphs/graph_editor/components/edge_component.dart'; +import 'package:visual_graphs/graph_editor/components/vertex_component.dart'; import 'package:flame/components.dart'; part "edge.dart"; part "vertex.dart"; class Graph { - final Set _vertices = {}; - final Set _edges = {}; + late final Set _vertices; + late final Set _edges; + late final Map>> _edgesBetween; + Vertex? hoveredVertex; + Map edgeIndexMap = {}; + Map edgePositionedIndexMap = {}; + + Graph() { + _vertices = {}; + _edges = {}; + _edgesBetween = {}; + } List get vertices => _vertices.toList(); List get edges => _edges.toList(); @@ -18,7 +28,8 @@ class Graph { void addEdge(Edge edge) { insertEdgeInCache(edge.from, edge.to, edge); - // If edge is not directed,add it in reverse too + + // If edge is not directed, add it in reverse too if (!edge.isDirected && !edge.isSelfEdge) { insertEdgeInCache(edge.to, edge.from, edge); } @@ -61,12 +72,23 @@ class Graph { if (_edgesBetween.containsKey(edge.from)) { if (_edgesBetween[edge.from]!.containsKey(edge.to)) { _edgesBetween[edge.from]![edge.to]!.remove(edge); + + // If there are no more edges between the two vertices, remove the key + if (_edgesBetween[edge.from]![edge.to]!.isEmpty) { + _edgesBetween[edge.from]!.remove(edge.to); + } } } + if (!edge.isDirected && !edge.isSelfEdge) { if (_edgesBetween.containsKey(edge.to)) { if (_edgesBetween[edge.to]!.containsKey(edge.from)) { _edgesBetween[edge.to]![edge.from]!.remove(edge); + + // If there are no more edges between the two vertices, remove the key + if (_edgesBetween[edge.to]![edge.from]!.isEmpty) { + _edgesBetween[edge.to]!.remove(edge.from); + } } } } @@ -77,10 +99,10 @@ class Graph { _edges.clear(); } - // Chache for the edges between two vertices - final Map>> _edgesBetween = {}; + Map> getNeighbours(Vertex vertex) => + _edgesBetween[vertex] ?? {}; - List edgesBetween(Vertex start, Vertex? end) { + List getEdgesBetween(Vertex start, Vertex? end) { if (end == null) return []; Set edges = {}; @@ -98,7 +120,7 @@ class Graph { return list; } - Vector2 get center { + Vector2 get geometricCenter { if (_vertices.isEmpty) { return Vector2.zero(); } @@ -113,10 +135,6 @@ class Graph { return Vector2(x / _vertices.length, y / _vertices.length); } - Vertex? hoveredVertex; - - Map edgeIndexMap = {}; - void computeEdgeIndex() { edgeIndexMap.clear(); for (var edge in _edges) { @@ -124,10 +142,30 @@ class Graph { } } - Map edgePositionedIndexMap = {}; void computeEdgePositionedIndex() { for (var edge in _edges) { edgePositionedIndexMap[edge] = edge.computePositionedIndex(); } } + + bool get isEmpty => _vertices.isEmpty; + bool get isNotEmpty => _vertices.isNotEmpty; + + Graph._from( + this._vertices, + this._edges, + this._edgesBetween, + this.edgeIndexMap, + this.edgePositionedIndexMap, + ); + + Graph clone() { + return Graph._from( + Set.from(_vertices), + Set.from(_edges), + Map.from(_edgesBetween), + Map.from(edgeIndexMap), + Map.from(edgePositionedIndexMap), + ); + } } diff --git a/lib/models/vertex.dart b/lib/graph_editor/models/vertex.dart similarity index 70% rename from lib/models/vertex.dart rename to lib/graph_editor/models/vertex.dart index 5624e65..6482b7a 100644 --- a/lib/models/vertex.dart +++ b/lib/graph_editor/models/vertex.dart @@ -20,4 +20,12 @@ class Vertex extends _Vertex { Vertex({super.label = '', Vector2? position}) { component = VertexComponent(position ?? Vector2.zero(), vertex: this); } + + Map> get neighbours { + return component.gameRef.graph.getNeighbours(this); + } + + List get neighboursList { + return neighbours.keys.toList(); + } } diff --git a/lib/widgets/edge_info_box.dart b/lib/graph_editor/widgets/edge_info_box.dart similarity index 97% rename from lib/widgets/edge_info_box.dart rename to lib/graph_editor/widgets/edge_info_box.dart index ea05964..5f2e857 100644 --- a/lib/widgets/edge_info_box.dart +++ b/lib/graph_editor/widgets/edge_info_box.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:visual_graphs/components/edge_component.dart'; +import 'package:visual_graphs/graph_editor/components/edge_component.dart'; class EdgeInfoBox extends StatefulWidget { final EdgeComponent edgeComponent; diff --git a/lib/widgets/game_info_box.dart b/lib/graph_editor/widgets/game_info_box.dart similarity index 100% rename from lib/widgets/game_info_box.dart rename to lib/graph_editor/widgets/game_info_box.dart diff --git a/lib/widgets/vertex_info_box.dart b/lib/graph_editor/widgets/vertex_info_box.dart similarity index 94% rename from lib/widgets/vertex_info_box.dart rename to lib/graph_editor/widgets/vertex_info_box.dart index 9256c51..3691f8b 100644 --- a/lib/widgets/vertex_info_box.dart +++ b/lib/graph_editor/widgets/vertex_info_box.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:visual_graphs/components/vertex_component.dart'; +import 'package:visual_graphs/graph_editor/components/vertex_component.dart'; class VertexInfoBox extends StatefulWidget { final VertexComponent vertexComponent; diff --git a/lib/helpers/dequeue.dart b/lib/helpers/dequeue.dart new file mode 100644 index 0000000..b86e392 --- /dev/null +++ b/lib/helpers/dequeue.dart @@ -0,0 +1,34 @@ +class Dequeue { + final List _dequeue = []; + + void addFirst(T item) { + _dequeue.insert(0, item); + } + + void addLast(T item) { + _dequeue.add(item); + } + + T removeFirst() { + return _dequeue.removeAt(0); + } + + T removeLast() { + return _dequeue.removeLast(); + } + + T peekFirst() { + return _dequeue.first; + } + + T peekLast() { + return _dequeue.last; + } + + bool get isEmpty => _dequeue.isEmpty; + bool get isNotEmpty => _dequeue.isNotEmpty; + + void clear() { + _dequeue.clear(); + } +} diff --git a/lib/helpers/queue.dart b/lib/helpers/queue.dart new file mode 100644 index 0000000..186aa64 --- /dev/null +++ b/lib/helpers/queue.dart @@ -0,0 +1,22 @@ +class QueueDS { + final List _queue = []; + + void enqueue(T item) { + _queue.add(item); + } + + T dequeue() { + return _queue.removeAt(0); + } + + T peek() { + return _queue.first; + } + + bool get isEmpty => _queue.isEmpty; + bool get isNotEmpty => _queue.isNotEmpty; + + void clear() { + _queue.clear(); + } +} diff --git a/lib/helpers/stack.dart b/lib/helpers/stack.dart new file mode 100644 index 0000000..7c10a3f --- /dev/null +++ b/lib/helpers/stack.dart @@ -0,0 +1,35 @@ +class StackDS { + final List _stack = []; + + void push(T item) { + _stack.add(item); + } + + T pop() { + return _stack.removeLast(); + } + + T peek() { + return _stack.last; + } + + bool get isEmpty => _stack.isEmpty; + bool get isNotEmpty => _stack.isNotEmpty; + + void clear() { + _stack.clear(); + } +} + +class SizedStackDS extends StackDS { + final int _maxSize; + SizedStackDS(this._maxSize); + + @override + void push(T item) { + if (_stack.length >= _maxSize) { + _stack.removeAt(0); + } + super.push(item); + } +}