mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-29 18:38:36 +08:00
Remove main_graph
All of it has been incorproated into GitJournal.
This commit is contained in:
@ -1,561 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class MyExampleWidget extends StatefulWidget {
|
||||
@override
|
||||
_MyExampleWidgetState createState() => _MyExampleWidgetState();
|
||||
}
|
||||
|
||||
class _MyExampleWidgetState extends State<MyExampleWidget> {
|
||||
Graph graph;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
var a = Node("A", 30, 30);
|
||||
var b = Node("B", 200, 200);
|
||||
var c = Node("C", 200, 100);
|
||||
var d = Node("D", 300, 300);
|
||||
var e = Node("E", 300, 100);
|
||||
|
||||
var edges = <Edge>[
|
||||
Edge(a, b),
|
||||
Edge(a, c),
|
||||
Edge(b, d),
|
||||
Edge(b, e),
|
||||
Edge(e, c),
|
||||
];
|
||||
|
||||
graph = Graph();
|
||||
graph.nodes = [a, b, c, d, e];
|
||||
graph.edges = edges;
|
||||
|
||||
graph.assignRandomPositions(400, 650);
|
||||
graph.startLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GraphStackView(graph);
|
||||
/*
|
||||
return CanvasTouchDetector(
|
||||
builder: (context) {
|
||||
return CustomPaint(
|
||||
painter: MyPainter(context, graph),
|
||||
);
|
||||
},
|
||||
);*/
|
||||
}
|
||||
}
|
||||
|
||||
class MyPainter extends CustomPainter {
|
||||
final Graph graph;
|
||||
|
||||
MyPainter(this.graph) : super(repaint: graph);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Draw all the edges
|
||||
for (var edge in graph.edges) {
|
||||
var strokeWitdth = 2.5;
|
||||
if (edge.a.pressed || edge.b.pressed) {
|
||||
strokeWitdth *= 2;
|
||||
}
|
||||
|
||||
canvas.drawLine(
|
||||
Offset(edge.a.x, edge.a.y),
|
||||
Offset(edge.b.x, edge.b.y),
|
||||
Paint()
|
||||
..color = Colors.grey
|
||||
..strokeWidth = strokeWitdth,
|
||||
/*onPanUpdate: (detail) {
|
||||
print('Edge ${edge.a.label} -> ${edge.b.label} Swiped');
|
||||
},*/
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Node {
|
||||
String label = "";
|
||||
double x;
|
||||
double y;
|
||||
bool pressed = false;
|
||||
|
||||
double forceX = 0.0;
|
||||
double forceY = 0.0;
|
||||
|
||||
Node(this.label, this.x, this.y);
|
||||
|
||||
@override
|
||||
String toString() => "Node{$label, $x, $y}";
|
||||
}
|
||||
|
||||
class Edge {
|
||||
Node a;
|
||||
Node b;
|
||||
|
||||
Edge(this.a, this.b);
|
||||
}
|
||||
|
||||
class Graph extends ChangeNotifier {
|
||||
List<Node> nodes = [];
|
||||
List<Edge> edges = [];
|
||||
|
||||
Map<String, Set<int>> _neighbours = {};
|
||||
Map<String, int> _nodeIndexes;
|
||||
|
||||
void notify() {
|
||||
notifyListeners();
|
||||
startLayout();
|
||||
}
|
||||
|
||||
List<int> computeNeighbours(Node n) {
|
||||
if (_nodeIndexes == null) {
|
||||
_nodeIndexes = <String, int>{};
|
||||
for (var i = 0; i < this.nodes.length; i++) {
|
||||
var node = this.nodes[i];
|
||||
_nodeIndexes[node.label] = i;
|
||||
}
|
||||
}
|
||||
|
||||
var _nodes = _neighbours[n.label];
|
||||
if (_nodes != null) {
|
||||
return _nodes.union(computeOverlappingNodes(n)).toList();
|
||||
}
|
||||
|
||||
var nodes = <int>{};
|
||||
for (var edge in edges) {
|
||||
if (edge.a.label == n.label) {
|
||||
nodes.add(_nodeIndexes[edge.b.label]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (edge.b.label == n.label) {
|
||||
nodes.add(_nodeIndexes[edge.a.label]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_neighbours[n.label] = _nodes;
|
||||
return nodes.union(computeOverlappingNodes(n)).toList();
|
||||
}
|
||||
|
||||
// These nodes aren't actually neighbours, but we don't want nodes to
|
||||
// ever overlap, so I'm making the ones that are close by neighbours
|
||||
Set<int> computeOverlappingNodes(Node n) {
|
||||
var _nodes = <int>{};
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (node.label == n.label) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var dx = node.x - n.x;
|
||||
var dy = node.y - n.y;
|
||||
|
||||
var dist = sqrt((dx * dx) + (dy * dy));
|
||||
if (dist <= 60) {
|
||||
// print('${node.label} and ${n.label} are too close - $dist');
|
||||
_nodes.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return _nodes;
|
||||
}
|
||||
|
||||
void assignRandomPositions(int maxX, int maxY) {
|
||||
var random = Random(DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
for (var node in nodes) {
|
||||
node.x = random.nextInt(maxX).toDouble();
|
||||
node.y = random.nextInt(maxY).toDouble();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Timer layoutTimer;
|
||||
|
||||
void startLayout() {
|
||||
if (layoutTimer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = Duration(milliseconds: 25);
|
||||
layoutTimer = Timer.periodic(interval, (Timer t) {
|
||||
bool shouldStop = updateGraphPositions(this);
|
||||
print("shouldStop $shouldStop");
|
||||
if (shouldStop) {
|
||||
layoutTimer.cancel();
|
||||
layoutTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Timer(const Duration(seconds: 5), () {
|
||||
if (layoutTimer != null) {
|
||||
layoutTimer.cancel();
|
||||
layoutTimer = null;
|
||||
}
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
/// This Widget is the main application widget.
|
||||
class MyApp extends StatelessWidget {
|
||||
static const String _title = 'Graphs Experiments';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: _title,
|
||||
home: MyWidget(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyWidget extends StatelessWidget {
|
||||
MyWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: MyExampleWidget(),
|
||||
/*child: GraphView(
|
||||
children: <Widget>[
|
||||
Container(width: 50, height: 50, color: Colors.orange),
|
||||
Container(width: 50, height: 50, color: Colors.blue),
|
||||
Container(width: 50, height: 50, color: Colors.red),
|
||||
Container(width: 50, height: 50, color: Colors.yellow),
|
||||
],
|
||||
),*/
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Possibly use directed_graph library?
|
||||
|
||||
//
|
||||
// Basic Force Directed Layout
|
||||
//
|
||||
|
||||
const l = 150.0; // sping rest length
|
||||
const k_r = 10000.0; // repulsive force constant
|
||||
const k_s = 20; // spring constant
|
||||
const delta_t = 0.005; // time step
|
||||
const MAX_DISPLACEMENT_SQUARED = 16;
|
||||
const min_movement = 1.0;
|
||||
|
||||
bool updateGraphPositions(Graph g) {
|
||||
var numNodes = g.nodes.length;
|
||||
|
||||
// Initialize net forces
|
||||
for (var i = 0; i < numNodes; i++) {
|
||||
g.nodes[i].forceX = 0;
|
||||
g.nodes[i].forceY = 0;
|
||||
}
|
||||
|
||||
for (var i1 = 0; i1 < numNodes - 1; i1++) {
|
||||
var node1 = g.nodes[i1];
|
||||
|
||||
for (var i2 = i1 + 1; i2 < numNodes; i2++) {
|
||||
var node2 = g.nodes[i2];
|
||||
var dx = node2.x - node1.x;
|
||||
var dy = node2.y - node1.y;
|
||||
|
||||
if (dx != 0 || dy != 0) {
|
||||
var distSq = (dx * dx) + (dy * dy);
|
||||
var distance = sqrt(distSq);
|
||||
|
||||
var force = k_r / distSq;
|
||||
var fx = force * dx / distance;
|
||||
var fy = force * dy / distance;
|
||||
|
||||
node1.forceX -= fx;
|
||||
node1.forceY -= fy;
|
||||
|
||||
node2.forceX += fx;
|
||||
node2.forceY += fy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spring forces between adjacent pairs
|
||||
for (var i1 = 0; i1 < numNodes; i1++) {
|
||||
var node1 = g.nodes[i1];
|
||||
var node1Neighbours = g.computeNeighbours(node1);
|
||||
|
||||
for (var j = 0; j < node1Neighbours.length; j++) {
|
||||
var i2 = node1Neighbours[j];
|
||||
var node2 = g.nodes[i2];
|
||||
|
||||
if (i1 < i2) {
|
||||
var dx = node2.x - node1.x;
|
||||
var dy = node2.y - node1.y;
|
||||
|
||||
if (dx != 0 || dy != 0) {
|
||||
var distSq = (dx * dx) + (dy * dy);
|
||||
var distance = sqrt(distSq);
|
||||
|
||||
var force = k_s * (distance - l);
|
||||
var fx = force * dx / distance;
|
||||
var fy = force * dy / distance;
|
||||
|
||||
node1.forceX += fx;
|
||||
node1.forceY += fy;
|
||||
|
||||
node2.forceX -= fx;
|
||||
node2.forceY -= fy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update positions
|
||||
var allBelowThreshold = true;
|
||||
for (var i = 0; i < numNodes; i++) {
|
||||
var node = g.nodes[i];
|
||||
|
||||
// Skip Node which is current being controlled
|
||||
if (node.pressed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var dx = delta_t * node.forceX;
|
||||
var dy = delta_t * node.forceY;
|
||||
|
||||
var dispSq = (dx * dx) + (dy * dy);
|
||||
if (dispSq > MAX_DISPLACEMENT_SQUARED) {
|
||||
var s = sqrt(MAX_DISPLACEMENT_SQUARED / dispSq);
|
||||
|
||||
dx *= s;
|
||||
dy *= s;
|
||||
}
|
||||
|
||||
print('${node.label} $dx $dy');
|
||||
node.x += dx;
|
||||
node.y += dy;
|
||||
|
||||
if (dx.abs() > min_movement || dy.abs() > min_movement) {
|
||||
allBelowThreshold = false;
|
||||
}
|
||||
}
|
||||
print('------------------');
|
||||
|
||||
g.notify();
|
||||
return allBelowThreshold;
|
||||
}
|
||||
|
||||
class GraphRenderObjectParentData extends ContainerBoxParentData<RenderBox> {
|
||||
double x = -1;
|
||||
double y = -1;
|
||||
|
||||
GraphRenderObjectParentData();
|
||||
}
|
||||
|
||||
const maxX = 400;
|
||||
const maxY = 650;
|
||||
|
||||
class GraphRenderBox extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, GraphRenderObjectParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox,
|
||||
GraphRenderObjectParentData> {
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! GraphRenderObjectParentData) {
|
||||
child.parentData = GraphRenderObjectParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double computeDistanceToActualBaseline(TextBaseline baseline) =>
|
||||
defaultComputeDistanceToFirstActualBaseline(baseline);
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
print('performLayout');
|
||||
size = const Size(400, 650);
|
||||
|
||||
var random = Random(DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
// Assign each of the children their value
|
||||
var child = firstChild;
|
||||
while (child != null) {
|
||||
child.layout(constraints, parentUsesSize: true);
|
||||
// child.size gives size
|
||||
|
||||
final childParentData = child.parentData as GraphRenderObjectParentData;
|
||||
|
||||
if (childParentData.x == -1 && childParentData.y == -1) {
|
||||
var x = random.nextInt(maxX).toDouble();
|
||||
var y = random.nextInt(maxY).toDouble();
|
||||
childParentData.x = x;
|
||||
childParentData.y = y;
|
||||
}
|
||||
|
||||
childParentData.offset = Offset(childParentData.x, childParentData.y);
|
||||
print(childParentData.offset);
|
||||
|
||||
child = childParentData.nextSibling;
|
||||
|
||||
// FIXME: Do not let them intersect
|
||||
// Do not let them go over the max size
|
||||
// Computer a proper size
|
||||
}
|
||||
print('performLayout done');
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
//
|
||||
// Paint Edges
|
||||
//
|
||||
|
||||
var child = firstChild;
|
||||
final childParentData = child.parentData as GraphRenderObjectParentData;
|
||||
|
||||
var secondChild = childParentData.nextSibling;
|
||||
var secondChildParentData =
|
||||
secondChild.parentData as GraphRenderObjectParentData;
|
||||
|
||||
var paint = Paint();
|
||||
paint.color = Colors.grey;
|
||||
paint.strokeWidth = 3.0;
|
||||
|
||||
context.canvas.drawLine(
|
||||
childParentData.offset.translate(child.size.width / 2, child.size.height),
|
||||
secondChildParentData.offset
|
||||
.translate(child.size.width / 2, child.size.height),
|
||||
paint,
|
||||
);
|
||||
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
class GraphView extends MultiChildRenderObjectWidget {
|
||||
GraphView({
|
||||
Key key,
|
||||
List<Widget> children = const <Widget>[],
|
||||
}) : super(key: key, children: children);
|
||||
|
||||
@override
|
||||
GraphRenderBox createRenderObject(BuildContext context) {
|
||||
return GraphRenderBox();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// 1. Get this to accept a builder function to draw the widgets (instead of them always being drawn)
|
||||
// 2. Implement a getEdges function
|
||||
// 3. Place them in some initial order?
|
||||
// 4. Update their positions based on
|
||||
|
||||
// Maybe use CustomMultiChildLayout instead?
|
||||
|
||||
class GraphStackView extends StatefulWidget {
|
||||
final Graph graph;
|
||||
|
||||
GraphStackView(this.graph);
|
||||
|
||||
@override
|
||||
_GraphStackViewState createState() => _GraphStackViewState();
|
||||
}
|
||||
|
||||
class _GraphStackViewState extends State<GraphStackView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.graph.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var children = <Widget>[];
|
||||
|
||||
children.add(CustomPaint(painter: MyPainter(widget.graph)));
|
||||
|
||||
for (var node in widget.graph.nodes) {
|
||||
var w = Positioned(
|
||||
child: GestureDetector(
|
||||
child: NodeWidget(node),
|
||||
onPanStart: (details) {
|
||||
node.x = details.globalPosition.dx;
|
||||
node.y = details.globalPosition.dy;
|
||||
node.pressed = true;
|
||||
|
||||
widget.graph.notify();
|
||||
print("Pan start ${node.label} $details");
|
||||
},
|
||||
onPanEnd: (DragEndDetails details) {
|
||||
print("Pan end ${node.label} $details");
|
||||
node.pressed = false;
|
||||
widget.graph.notify();
|
||||
},
|
||||
onPanUpdate: (details) {
|
||||
node.x = details.globalPosition.dx;
|
||||
node.y = details.globalPosition.dy;
|
||||
|
||||
widget.graph.notify();
|
||||
print("Pan update ${node.label} ${details.globalPosition}");
|
||||
},
|
||||
),
|
||||
left: node.x - 25,
|
||||
top: node.y - 25,
|
||||
);
|
||||
children.add(w);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: children,
|
||||
fit: StackFit.expand,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NodeWidget extends StatelessWidget {
|
||||
final Node node;
|
||||
|
||||
NodeWidget(this.node);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Text(node.label),
|
||||
],
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user