Adding ability to control state machine.

This commit is contained in:
Luigi Rosso
2021-03-11 20:21:35 -08:00
parent 45e536ccaa
commit 462faf20ff
24 changed files with 523 additions and 81 deletions

BIN
example/assets/rocket.riv Normal file

Binary file not shown.

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
class ExampleAnimation extends StatefulWidget {
const ExampleAnimation({Key key}) : super(key: key);
@override
_ExampleAnimationState createState() => _ExampleAnimationState();
}
class _ExampleAnimationState extends State<ExampleAnimation> {
void _togglePlay() {
setState(() => _controller.isActive = !_controller.isActive);
}
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard _riveArtboard;
RiveAnimationController _controller;
@override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('assets/dino.riv').then(
(data) async {
final file = RiveFile();
// Load the RiveFile from the binary data.
if (file.import(data)) {
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
// Add a controller to play back a known animation on the main/default
// artboard. We store a reference to it so we can toggle playback.
artboard.addController(_controller = SimpleAnimation('Run'));
setState(() => _riveArtboard = artboard);
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animation Example'),
),
body: Center(
child: _riveArtboard == null
? const SizedBox()
: Rive(artboard: _riveArtboard),
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
tooltip: isPlaying ? 'Pause' : 'Play',
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
class ExampleStateMachine extends StatefulWidget {
const ExampleStateMachine({Key key}) : super(key: key);
@override
_ExampleStateMachineState createState() => _ExampleStateMachineState();
}
class _ExampleStateMachineState extends State<ExampleStateMachine> {
void _togglePlay() {
setState(() => _controller.isActive = !_controller.isActive);
}
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard _riveArtboard;
StateMachineController _controller;
StateMachineInput<bool> _hoverInput;
StateMachineInput<bool> _pressInput;
@override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('assets/rocket.riv').then(
(data) async {
final file = RiveFile();
// Load the RiveFile from the binary data.
if (file.import(data)) {
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
var controller =
StateMachineController.fromArtboard(artboard, 'Button');
if (controller != null) {
artboard.addController(controller);
_hoverInput = controller.findInput('Hover');
_pressInput = controller.findInput('Press');
}
setState(() => _riveArtboard = artboard);
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: const Text('State Machine Example'),
),
body: Center(
child: _riveArtboard == null
? const SizedBox()
: MouseRegion(
onEnter: (_) => _hoverInput.value = true,
onExit: (_) => _hoverInput.value = false,
child: GestureDetector(
onTapDown: (_) => _pressInput.value = true,
onTapCancel: () => _pressInput.value = false,
onTapUp: (_) => _pressInput.value = false,
child: SizedBox(
width: 250,
height: 250,
child: Rive(
artboard: _riveArtboard,
),
),
),
),
),
);
}
}

View File

@ -1,77 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
import 'package:rive_example/example_animation.dart';
import 'package:rive_example/example_state_machine.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _togglePlay() {
setState(() => _controller.isActive = !_controller.isActive);
}
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard _riveArtboard;
RiveAnimationController _controller;
@override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('assets/off_road_car.riv').then(
(data) async {
final file = RiveFile();
// Load the RiveFile from the binary data.
if (file.import(data)) {
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
// Add a controller to play back a known animation on the main/default
// artboard.We store a reference to it so we can toggle playback.
artboard.addController(_controller = SimpleAnimation('idle'));
setState(() => _riveArtboard = artboard);
}
},
);
}
void main() => runApp(MaterialApp(
title: 'Navigation Basics',
home: Home(),
));
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _riveArtboard == null
? const SizedBox()
: Rive(artboard: _riveArtboard),
appBar: AppBar(
title: const Text('Rive Examples'),
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
tooltip: isPlaying ? 'Pause' : 'Play',
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: const Text('Animation'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const ExampleAnimation(),
),
);
},
),
const SizedBox(
height: 10,
),
ElevatedButton(
child: const Text('State Machine'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const ExampleStateMachine(),
),
);
},
),
],
),
),
);

View File

@ -18,3 +18,5 @@ export 'package:rive/src/rive_core/shapes/paint/radial_gradient.dart';
export 'package:rive/src/rive_core/runtime/runtime_header.dart'
show riveVersion;
export 'package:rive/src/extensions.dart';
export 'package:rive/src/rive_core/animation/state_machine.dart';
export 'package:rive/src/state_machine_controller.dart';

View File

@ -7,10 +7,14 @@ import 'package:rive/src/rive_core/component.dart';
class ArtboardImporter extends ImportStackObject {
final RuntimeArtboard artboard;
ArtboardImporter(this.artboard);
final List<LinearAnimation> animations = [];
void addComponent(Core<CoreContext> object) => artboard.addObject(object);
void addAnimation(Animation animation) {
if (animation is LinearAnimation) {
animations.add(animation);
}
artboard.addObject(animation);
animation.artboard = artboard;
}

View File

@ -8,7 +8,7 @@ class LayerStateImporter extends ImportStackObject {
void addTransition(StateTransition transition) {
state.context.addObject(transition);
transition.stateFrom = state;
state.internalAddTransition(transition);
}
@override

View File

@ -1,10 +1,12 @@
import 'package:rive/src/core/core.dart';
import 'package:rive/src/rive_core/animation/animation_state.dart';
import 'package:rive/src/rive_core/animation/layer_state.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
class StateMachineLayerImporter extends ImportStackObject {
final StateMachineLayer layer;
StateMachineLayerImporter(this.layer);
final ArtboardImporter artboardImporter;
StateMachineLayerImporter(this.layer, this.artboardImporter);
final List<LayerState> importedStates = [];
@ -15,9 +17,19 @@ class StateMachineLayerImporter extends ImportStackObject {
layer.internalAddState(state);
}
bool _resolved = false;
@override
bool resolve() {
assert(!_resolved);
_resolved = true;
for (final state in importedStates) {
if (state is AnimationState) {
int artboardAnimationIndex = state.animationId;
assert(artboardAnimationIndex >= 0 &&
artboardAnimationIndex < artboardImporter.animations.length);
state.animationId =
artboardImporter.animations[artboardAnimationIndex].id;
}
for (final transition in state.transitions) {
// At import time the stateToId is an index relative to the entire layer
// (which state in this layer). We can use that to find the matching

View File

@ -3,13 +3,23 @@ import 'package:rive/src/rive_core/animation/state_transition.dart';
import 'package:rive/src/rive_core/animation/transition_condition.dart';
class StateTransitionImporter extends ImportStackObject {
final StateMachineImporter stateMachineImporter;
final StateTransition transition;
StateTransitionImporter(this.transition);
StateTransitionImporter(this.transition, this.stateMachineImporter);
void addCondition(TransitionCondition condition) {
transition.context.addObject(condition);
transition.internalAddCondition(condition);
}
@override
bool resolve() => true;
bool resolve() {
var inputs = stateMachineImporter.machine.inputs;
for (final condition in transition.conditions) {
var inputIndex = condition.inputId;
assert(inputIndex >= 0 && inputIndex < inputs.length);
condition.inputId = inputs[inputIndex].id;
}
return true;
}
}

View File

@ -4,6 +4,10 @@ import 'package:rive/src/generated/animation/keyframe_double_base.dart';
import 'package:rive/src/generated/rive_core_context.dart';
export 'package:rive/src/generated/animation/keyframe_double_base.dart';
double toDegrees(double rad) {
return rad / 3.14 * 180;
}
void _apply(
Core<CoreContext> object, int propertyKey, double mix, double value) {
if (mix == 1) {

View File

@ -5,6 +5,10 @@ class LinearAnimationInstance {
final LinearAnimation animation;
double _time = 0;
int _direction = 1;
bool _didLoop = false;
bool get didLoop => _didLoop;
double _spilledTime = 0;
double get spilledTime => _spilledTime;
LinearAnimationInstance(this.animation)
: _time =
(animation.enableWorkArea ? animation.workStart : 0).toDouble() /
@ -27,7 +31,9 @@ class LinearAnimationInstance {
double get startTime =>
(animation.enableWorkArea ? animation.workStart : 0).toDouble() /
animation.fps;
double get progress => (_time - startTime) / (endTime - startTime);
void reset() => _time = startTime;
bool get keepGoing => animation.loop != Loop.oneShot || !_didLoop;
bool advance(double elapsedSeconds) {
_time += elapsedSeconds * animation.speed * _direction;
double frames = _time * animation.fps;
@ -35,31 +41,41 @@ class LinearAnimationInstance {
var end = animation.enableWorkArea ? animation.workEnd : animation.duration;
var range = end - start;
bool keepGoing = true;
_didLoop = false;
_spilledTime = 0;
switch (animation.loop) {
case Loop.oneShot:
if (frames > end) {
keepGoing = false;
_spilledTime = (frames - end) / animation.fps;
frames = end.toDouble();
_time = frames / animation.fps;
_didLoop = true;
}
break;
case Loop.loop:
if (frames >= end) {
_spilledTime = (frames - end) / animation.fps;
frames = _time * animation.fps;
frames = start + (frames - start) % range;
_time = frames / animation.fps;
_didLoop = true;
}
break;
case Loop.pingPong:
while (true) {
if (_direction == 1 && frames >= end) {
_spilledTime = (frames - end) / animation.fps;
_direction = -1;
frames = end + (end - frames);
_time = frames / animation.fps;
_didLoop = true;
} else if (_direction == -1 && frames < start) {
_spilledTime = (start - frames) / animation.fps;
_direction = 1;
frames = start + (start - frames);
_time = frames / animation.fps;
_didLoop = true;
} else {
break;
}

View File

@ -4,4 +4,8 @@ export 'package:rive/src/generated/animation/state_machine_bool_base.dart';
class StateMachineBool extends StateMachineBoolBase {
@override
void valueChanged(bool from, bool to) {}
@override
bool isValidType<T>() => T == bool;
@override
dynamic get controllerValue => value;
}

View File

@ -4,4 +4,8 @@ export 'package:rive/src/generated/animation/state_machine_double_base.dart';
class StateMachineDouble extends StateMachineDoubleBase {
@override
void valueChanged(double from, double to) {}
@override
bool isValidType<T>() => T == double;
@override
dynamic get controllerValue => value;
}

View File

@ -8,4 +8,6 @@ abstract class StateMachineInput extends StateMachineInputBase {
@override
ListBase<StateMachineComponent> machineComponentList(StateMachine machine) =>
machine?.inputs;
bool isValidType<T>() => false;
dynamic get controllerValue => null;
}

View File

@ -11,4 +11,9 @@ class StateMachineTrigger extends StateMachineTriggerBase {
void reset() {
_triggered = false;
}
@override
bool isValidType<T>() => T == bool;
@override
dynamic get controllerValue => _triggered;
}

View File

@ -8,7 +8,6 @@ export 'package:rive/src/generated/animation/state_transition_base.dart';
class StateTransition extends StateTransitionBase {
final StateTransitionConditions conditions = StateTransitionConditions();
LayerState stateTo;
LayerState stateFrom;
@override
void onAdded() {}
@override

View File

@ -1,10 +1,16 @@
import 'dart:collection';
import 'package:rive/src/rive_core/animation/state_machine_bool.dart';
import 'package:rive/src/rive_core/animation/transition_condition.dart';
import 'package:rive/src/generated/animation/transition_bool_condition_base.dart';
export 'package:rive/src/generated/animation/transition_bool_condition_base.dart';
class TransitionBoolCondition extends TransitionBoolConditionBase {
@override
void valueChanged(bool from, bool to) {}
@override
bool validate() => input == null || input is StateMachineBool;
bool evaluate(HashMap<int, dynamic> values) {
var boolInput = input as StateMachineBool;
dynamic providedValue = values[input.id];
bool value = providedValue is bool ? providedValue : boolInput.value;
return (value && op == TransitionConditionOp.equal) ||
(!value && op == TransitionConditionOp.notEqual);
}
}

View File

@ -1,3 +1,4 @@
import 'dart:collection';
import 'package:rive/src/core/core.dart';
import 'package:rive/src/rive_core/animation/state_machine_input.dart';
import 'package:rive/src/rive_core/animation/state_transition.dart';
@ -36,6 +37,7 @@ abstract class TransitionCondition extends TransitionConditionBase {
input = inputId == null ? null : context.resolve(inputId);
}
bool evaluate(HashMap<int, dynamic> values);
@override
bool import(ImportStack importStack) {
var importer = importStack

View File

@ -1,4 +1,6 @@
import 'dart:collection';
import 'package:rive/src/rive_core/animation/state_machine_double.dart';
import 'package:rive/src/rive_core/animation/transition_condition.dart';
import 'package:rive/src/generated/animation/transition_double_condition_base.dart';
export 'package:rive/src/generated/animation/transition_double_condition_base.dart';
@ -7,4 +9,26 @@ class TransitionDoubleCondition extends TransitionDoubleConditionBase {
void valueChanged(double from, double to) {}
@override
bool validate() => input == null || input is StateMachineDouble;
@override
bool evaluate(HashMap<int, dynamic> values) {
var doubleInput = input as StateMachineDouble;
dynamic providedValue = values[input.id];
double inputValue =
providedValue is double ? providedValue : doubleInput.value;
switch (op) {
case TransitionConditionOp.equal:
return inputValue == value;
case TransitionConditionOp.notEqual:
return inputValue != value;
case TransitionConditionOp.lessThanOrEqual:
return inputValue <= value;
case TransitionConditionOp.lessThan:
return inputValue < value;
case TransitionConditionOp.greaterThanOrEqual:
return inputValue >= value;
case TransitionConditionOp.greaterThan:
return inputValue > value;
}
return false;
}
}

View File

@ -1,8 +1,21 @@
import 'dart:collection';
import 'package:rive/src/rive_core/animation/state_machine_trigger.dart';
import 'package:rive/src/generated/animation/transition_trigger_condition_base.dart';
export 'package:rive/src/generated/animation/transition_trigger_condition_base.dart';
class TransitionTriggerCondition extends TransitionTriggerConditionBase {
@override
bool validate() => input == null || input is StateMachineTrigger;
bool evaluate(HashMap<int, dynamic> values) {
dynamic providedValue = values[input.id];
if (providedValue is bool && providedValue) {
values[input.id] = false;
return true;
}
var triggerInput = input as StateMachineTrigger;
if (triggerInput.triggered) {
triggerInput.reset();
return true;
}
return false;
}
}

View File

@ -1,7 +1,9 @@
import 'package:rive/src/rive_core/animation/transition_condition.dart';
import 'package:rive/src/generated/animation/transition_value_condition_base.dart';
export 'package:rive/src/generated/animation/transition_value_condition_base.dart';
class TransitionValueCondition extends TransitionValueConditionBase {
abstract class TransitionValueCondition extends TransitionValueConditionBase {
TransitionConditionOp get op => TransitionConditionOp.values[opValue];
@override
void opValueChanged(int from, int to) {}
}

View File

@ -0,0 +1,141 @@
import 'dart:collection';
import 'package:rive/src/core/core.dart';
import 'package:rive/src/rive_core/animation/animation_state.dart';
import 'package:rive/src/rive_core/animation/layer_state.dart';
import 'package:rive/src/rive_core/animation/linear_animation_instance.dart';
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
import 'package:rive/src/rive_core/animation/state_transition.dart';
import 'package:rive/src/rive_core/rive_animation_controller.dart';
class LayerController {
final StateMachineLayer layer;
LayerState _currentState;
LinearAnimationInstance _animationInstanceFrom;
StateTransition _transition;
double _mix = 1.0;
LinearAnimationInstance _animationInstance;
LayerController(this.layer) {
_changeState(layer.entryState);
}
bool _changeState(LayerState state) {
if (state == _currentState) {
return false;
}
_currentState = state;
return true;
}
void dispose() {
_changeState(null);
}
bool apply(CoreContext core, double elapsedSeconds,
HashMap<int, dynamic> inputValues) {
if (_animationInstance != null) {
_animationInstance.advance(elapsedSeconds);
}
for (int i = 0; updateState(inputValues); i++) {
if (i == 100) {
print('StateMachineController.apply exceeded max iterations.');
return false;
}
}
if (_transition != null && _transition.duration != 0) {
_mix = (_mix + elapsedSeconds / (_transition.duration / 1000))
.clamp(0, 1)
.toDouble();
} else {
_mix = 1;
}
var keepGoing = _mix != 1;
if (_animationInstanceFrom != null && _mix < 1) {
_animationInstanceFrom.advance(elapsedSeconds);
_animationInstanceFrom.animation
.apply(_animationInstanceFrom.time, mix: 1 - _mix, coreContext: core);
}
if (_animationInstance != null) {
_animationInstance.animation
.apply(_animationInstance.time, mix: _mix, coreContext: core);
if (_animationInstance.keepGoing) {
keepGoing = true;
}
}
return keepGoing;
}
bool updateState(HashMap<int, dynamic> inputValues) {
if (layer.anyState != null && tryChangeState(layer.anyState, inputValues)) {
return true;
}
return tryChangeState(_currentState, inputValues);
}
bool tryChangeState(LayerState stateFrom, HashMap<int, dynamic> inputValues) {
for (final transition in stateFrom.transitions) {
bool valid = true;
for (final condition in transition.conditions) {
if (!condition.evaluate(inputValues)) {
valid = false;
break;
}
}
if (valid && _changeState(transition.stateTo)) {
_transition = transition;
if (_mix != 0) {
_animationInstanceFrom = _animationInstance;
}
if (_currentState is AnimationState) {
var spilledTime = _animationInstanceFrom?.spilledTime ?? 0;
_animationInstance = LinearAnimationInstance(
(_currentState as AnimationState).animation);
_animationInstance.advance(spilledTime);
_mix = 0;
} else {
_animationInstance = null;
}
return true;
}
}
return false;
}
}
class StateMachineController extends RiveAnimationController<CoreContext> {
final StateMachine stateMachine;
final inputValues = HashMap<int, dynamic>();
StateMachineController(this.stateMachine);
final layerControllers = <LayerController>[];
void _clearLayerControllers() {
for (final layer in layerControllers) {
layer.dispose();
}
layerControllers.clear();
}
@override
bool init(CoreContext core) {
_clearLayerControllers();
for (final layer in stateMachine.layers) {
layerControllers.add(LayerController(layer));
}
return super.init(core);
}
@override
void dispose() {
_clearLayerControllers();
super.dispose();
}
@override
void apply(CoreContext core, double elapsedSeconds) {
bool keepGoing = false;
for (final layerController in layerControllers) {
if (layerController.apply(core, elapsedSeconds, inputValues)) {
keepGoing = true;
}
}
isActive = keepGoing;
}
}

View File

@ -11,6 +11,7 @@ import 'package:rive/src/generated/animation/state_machine_base.dart';
import 'package:rive/src/rive_core/animation/keyed_property.dart';
import 'package:rive/src/rive_core/animation/layer_state.dart';
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/animation/state_machine_layer.dart';
import 'package:rive/src/rive_core/component.dart';
import 'package:rive/src/rive_core/runtime/runtime_header.dart';
import 'package:rive/src/rive_core/backboard.dart';
@ -21,6 +22,8 @@ import 'package:rive/src/rive_core/animation/keyed_object.dart';
import 'package:rive/src/rive_core/animation/linear_animation.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'rive_core/animation/state_transition.dart';
class RiveFile {
RuntimeHeader _header;
RuntimeHeader get header => _header;
@ -71,10 +74,6 @@ class RiveFile {
if (object == null) {
continue;
}
var previousOfType = importStack.latest(object.coreType);
if (previousOfType != null) {
previousOfType.resolve();
}
ImportStackObject stackObject;
var stackType = object.coreType;
@ -104,6 +103,16 @@ class RiveFile {
case StateMachineBase.typeKey:
stackObject = StateMachineImporter(object as StateMachine);
break;
case StateMachineLayerBase.typeKey:
{
// Needs artboard importer to resolve linear animations.
var artboardImporter =
importStack.latest<ArtboardImporter>(ArtboardBase.typeKey);
assert(artboardImporter != null);
stackObject = StateMachineLayerImporter(
object as StateMachineLayer, artboardImporter);
break;
}
case EntryStateBase.typeKey:
case AnyStateBase.typeKey:
case ExitStateBase.typeKey:
@ -111,6 +120,15 @@ class RiveFile {
stackObject = LayerStateImporter(object as LayerState);
stackType = LayerStateBase.typeKey;
break;
case StateTransitionBase.typeKey:
{
var stateMachineImporter = importStack
.latest<StateMachineImporter>(StateMachineBase.typeKey);
assert(stateMachineImporter != null);
stackObject = StateTransitionImporter(
object as StateTransition, stateMachineImporter);
break;
}
default:
if (object is Component) {
// helper = _ArtboardObjectImportHelper();

View File

@ -0,0 +1,52 @@
import 'package:rive/src/rive_core/animation/state_machine.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_core/state_machine_controller.dart' as core;
class StateMachineInput<T> {
final int id;
final StateMachineController controller;
StateMachineInput._(this.id, this.controller);
T get value => controller.inputValues[id] as T;
set value(T newValue) => change(newValue);
/// Change the value of the input, returns true if the value was changed the
/// and [StateMachineController] was activated.
bool change(T value) {
if (controller.inputValues[id] == value) {
return false;
}
controller.inputValues[id] = value;
controller.isActive = true;
return true;
}
}
/// An AnimationController which controls a StateMachine and provides access to
/// the inputs of the StateMachine.
class StateMachineController extends core.StateMachineController {
StateMachineController(StateMachine stateMachine) : super(stateMachine) {
isActive = true;
}
factory StateMachineController.fromArtboard(
Artboard artboard, String stateMachineName) {
for (final animation in artboard.animations) {
if (animation is StateMachine && animation.name == stateMachineName) {
return StateMachineController(animation);
}
}
return null;
}
StateMachineInput<T> findInput<T>(String name) {
for (final input in stateMachine.inputs) {
if (input.name == name && input.isValidType<T>()) {
inputValues[input.id] = input.controllerValue;
return StateMachineInput<T>._(input.id, this);
}
}
return null;
}
}