diff --git a/flare_dart/lib/actor_component.dart b/flare_dart/lib/actor_component.dart index 12ea89d..86790c7 100644 --- a/flare_dart/lib/actor_component.dart +++ b/flare_dart/lib/actor_component.dart @@ -31,6 +31,10 @@ abstract class ActorComponent { return _name; } + set name(String name) { + _name = name; + } + void resolveComponentIndices(List components) { ActorNode node = components[_parentIdx] as ActorNode; if (node != null) { diff --git a/flare_dart/lib/dependency_sorter.dart b/flare_dart/lib/dependency_sorter.dart index 98df48d..d05350e 100644 --- a/flare_dart/lib/dependency_sorter.dart +++ b/flare_dart/lib/dependency_sorter.dart @@ -1,4 +1,7 @@ import "dart:collection"; + +import "package:graphs/graphs.dart"; + import "actor_component.dart"; class DependencySorter { @@ -24,7 +27,7 @@ class DependencySorter { return true; } if (_temp.contains(n)) { - print("Dependency cycle!"); + // cycle detected! return false; } @@ -44,3 +47,60 @@ class DependencySorter { return true; } } + +/// Sorts dependencies for Actors even when cycles are present +/// +/// Any nodes that form part of a cycle can be found in `cycleNodes` after +/// `sort`. NOTE: Nodes isolated by cycles will not be found in `_order` or +/// `cycleNodes` e.g. `A -> B <-> C -> D` isolates D when running a sort based +/// on A +class TarjansDependencySorter extends DependencySorter { + HashSet _cycleNodes; + HashSet get cycleNodes => _cycleNodes; + + TarjansDependencySorter() { + _perm = HashSet(); + _temp = HashSet(); + _cycleNodes = HashSet(); + } + + @override + bool visit(ActorComponent n) { + if (cycleNodes.contains(n)) { + // skip any nodes on a known cycle. + return true; + } + + return super.visit(n); + } + + @override + List sort(ActorComponent root) { + _order = []; + + if (!visit(root)) { + // if we detect cycles, go find them all + _perm.clear(); + _temp.clear(); + _cycleNodes.clear(); + _order.clear(); + + var cycles = stronglyConnectedComponents( + [root], (ActorComponent node) => node.dependents); + + cycles.forEach((cycle) { + // cycles of len 1 are not cycles. + if (cycle.length > 1) { + cycle.forEach((cycleMember) { + _cycleNodes.add(cycleMember); + }); + } + }); + + // revisit the tree, skipping nodes on any cycle. + visit(root); + } + + return _order; + } +} diff --git a/flare_dart/pubspec.yaml b/flare_dart/pubspec.yaml index 172bc97..6f46ee7 100644 --- a/flare_dart/pubspec.yaml +++ b/flare_dart/pubspec.yaml @@ -5,4 +5,7 @@ author: "Rive Team " homepage: https://github.com/2d-inc/Flare-Flutter environment: sdk: ">=2.1.0 <3.0.0" - \ No newline at end of file +dependencies: + graphs: ^0.2.0 +dev_dependencies: + test: ^1.9.4 \ No newline at end of file diff --git a/flare_dart/test/dependency_sorter_test.dart b/flare_dart/test/dependency_sorter_test.dart new file mode 100644 index 0000000..701f3be --- /dev/null +++ b/flare_dart/test/dependency_sorter_test.dart @@ -0,0 +1,259 @@ +import 'dart:typed_data'; + +import 'package:flare_dart/actor_artboard.dart'; +import 'package:flare_dart/actor_color.dart'; +import 'package:flare_dart/actor_component.dart'; +import 'package:flare_dart/actor_drop_shadow.dart'; +import 'package:flare_dart/actor_inner_shadow.dart'; +import 'package:flare_dart/actor_layer_effect_renderer.dart'; +import 'package:flare_dart/actor_node.dart'; +import 'package:flare_dart/actor.dart'; +import 'package:flare_dart/dependency_sorter.dart'; + +import 'package:test/test.dart'; + +class DummyActor extends Actor { + @override + Future loadAtlases(List rawAtlases) { + throw UnimplementedError(); + } + + @override + ColorFill makeColorFill() { + throw UnimplementedError(); + } + + @override + ColorStroke makeColorStroke() { + throw UnimplementedError(); + } + + @override + ActorDropShadow makeDropShadow() { + throw UnimplementedError(); + } + + @override + GradientFill makeGradientFill() { + throw UnimplementedError(); + } + + @override + GradientStroke makeGradientStroke() { + throw UnimplementedError(); + } + + @override + ActorInnerShadow makeInnerShadow() { + throw UnimplementedError(); + } + + @override + ActorLayerEffectRenderer makeLayerEffectRenderer() { + throw UnimplementedError(); + } + + @override + RadialGradientFill makeRadialFill() { + throw UnimplementedError(); + } + + @override + RadialGradientStroke makeRadialStroke() { + throw UnimplementedError(); + } + + @override + Future readOutOfBandAsset(String filename, context) { + throw UnimplementedError(); + } +} + +String orderString(List order) { + String output = order.fold( + '', + (previousValue, actorComponent) => + previousValue + ' ' + actorComponent.name); + return output.trim(); +} + +void main() { + group("Simple Cycle:", () { + ActorArtboard artboard; + final actor = DummyActor(); + artboard = ActorArtboard(actor); + final nodeA = ActorNode()..name = 'A'; + final nodeB = ActorNode()..name = 'B'; + final nodeC = ActorNode()..name = 'C'; + final nodeD = ActorNode()..name = 'D'; + + /// + /// [root] <- [A] <- [B] <- [D] + /// A | A + /// | +------+ + /// [C] + artboard.addDependency(nodeA, artboard.root); + artboard.addDependency(nodeB, nodeA); + artboard.addDependency(nodeC, nodeA); + artboard.addDependency(nodeD, nodeB); + artboard.addDependency(nodeB, nodeD); + + test("DependencySorter cannot order", () { + expect(DependencySorter().sort(artboard.root), equals(null)); + }); + + test("TarjansDependencySorter orders", () { + var order = TarjansDependencySorter().sort(artboard.root); + expect(order.length, equals(3)); + expect(orderString(order), equals('Unnamed A C')); + }); + }); + group("No cycle:", () { + final actor = DummyActor(); + final artboard = ActorArtboard(actor); + final nodeA = ActorNode()..name = 'A'; + final nodeB = ActorNode()..name = 'B'; + final nodeC = ActorNode()..name = 'C'; + final nodeD = ActorNode()..name = 'D'; + + /// + /// [root] <- [A] <- [B] <- [D] + /// A A + /// | | + /// [C]-----+ + artboard.addDependency(nodeA, artboard.root); + artboard.addDependency(nodeB, nodeA); + artboard.addDependency(nodeC, nodeA); + artboard.addDependency(nodeD, nodeB); + artboard.addDependency(nodeC, nodeB); + + test("DependencySorter orders", () { + var order = DependencySorter().sort(artboard.root); + expect(order, isNotNull); + expect(orderString(order), 'Unnamed A B C D'); + }); + + test("DependencySorter orders", () { + var order = TarjansDependencySorter().sort(artboard.root); + expect(order, isNotNull); + expect(orderString(order), 'Unnamed A B C D'); + }); + }); + + group("Complex Cycle A:", () { + final actor = DummyActor(); + final artboard = ActorArtboard(actor); + final nodeA = ActorNode()..name = 'A'; + final nodeB = ActorNode()..name = 'B'; + final nodeC = ActorNode()..name = 'C'; + final nodeD = ActorNode()..name = 'D'; + + /// + /// +------+ + /// | v + /// [root] <- [A] <- [B] <- [D] + /// A | + /// | | + /// [C]<-----------+ + /// + artboard.addDependency(nodeA, artboard.root); + artboard.addDependency(nodeB, nodeA); + artboard.addDependency(nodeD, nodeB); + artboard.addDependency(nodeB, nodeD); + artboard.addDependency(nodeC, nodeA); + artboard.addDependency(nodeD, nodeC); + + test("DependencySorter cannot order", () { + expect(DependencySorter().sort(artboard.root), equals(null)); + }); + + test("TarjansDependencySorter orders", () { + var order = TarjansDependencySorter().sort(artboard.root); + expect(order, isNotNull); + expect(orderString(order), equals('Unnamed A C')); + }); + }); + + group("Complex Cycle B, F is isolated:", () { + ActorArtboard artboard; + + final actor = DummyActor(); + artboard = ActorArtboard(actor); + final nodeA = ActorNode()..name = 'A'; + final nodeB = ActorNode()..name = 'B'; + final nodeC = ActorNode()..name = 'C'; + final nodeD = ActorNode()..name = 'D'; + final nodeE = ActorNode()..name = 'E'; + final nodeF = ActorNode()..name = 'F'; + + /// + /// +-------------+ + /// | | + /// | [F] | + /// | v v + /// [root] <- [A] <- [B] <- [C] <- [D] + /// A | + /// | | + /// [E]<-----------+ + /// + artboard.addDependency(nodeA, artboard.root); + artboard.addDependency(nodeB, nodeA); + artboard.addDependency(nodeC, nodeB); + artboard.addDependency(nodeD, nodeC); + artboard.addDependency(nodeB, nodeD); + artboard.addDependency(nodeE, nodeA); + artboard.addDependency(nodeC, nodeE); + artboard.addDependency(nodeF, nodeC); + + test("TarjansDependencySorter orders", () { + var dependencySorter = TarjansDependencySorter(); + var order = dependencySorter.sort(artboard.root); + expect(orderString(order), equals('Unnamed A E')); + expect(dependencySorter.cycleNodes, containsAll([nodeB, nodeC, nodeD])); + expect(dependencySorter.cycleNodes, contains(nodeF), + skip: "Node F is isolated by a cycle, and does not " + " exist in 'order' or in 'cycleNodes'"); + }); + }); + + group("Complex Cycle C, F is not isolated:", () { + ActorArtboard artboard; + + final actor = DummyActor(); + artboard = ActorArtboard(actor); + final nodeA = ActorNode()..name = 'A'; + final nodeB = ActorNode()..name = 'B'; + final nodeC = ActorNode()..name = 'C'; + final nodeD = ActorNode()..name = 'D'; + final nodeE = ActorNode()..name = 'E'; + final nodeF = ActorNode()..name = 'F'; + + /// + /// +---------------+ + /// | | + /// | [F]---+ | + /// | v | v + /// [root] <- [A] <- [B] <- [C] <-+- [D] + /// A | | + /// | | | + /// [E]<-----------+ | + /// A | + /// +------------------+ + artboard.addDependency(nodeA, artboard.root); + artboard.addDependency(nodeB, nodeA); + artboard.addDependency(nodeC, nodeB); + artboard.addDependency(nodeD, nodeC); + artboard.addDependency(nodeB, nodeD); + artboard.addDependency(nodeE, nodeA); + artboard.addDependency(nodeC, nodeE); + artboard.addDependency(nodeF, nodeC); + artboard.addDependency(nodeF, nodeE); + + test("TarjansDependencySorter orders", () { + var dependencySorter = TarjansDependencySorter(); + var order = dependencySorter.sort(artboard.root); + expect(orderString(order), equals('Unnamed A E F')); + expect(dependencySorter.cycleNodes, containsAll([nodeB, nodeC, nodeD])); + }); + }); +}