diff --git a/lib/components/visual_component.dart b/lib/components/visual_component.dart new file mode 100644 index 0000000..6d96a17 --- /dev/null +++ b/lib/components/visual_component.dart @@ -0,0 +1,296 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class VisualComponent extends HookWidget { + final String name; + final List inputs; + final List outputs; + final Map inputColors; + final Map outputColors; + + VisualComponent({super.key, required this.name, required this.inputs, required this.outputs, Map? inputColors, Map? outputColors}) + : inputColors = inputColors ?? {} + , outputColors = outputColors ?? {}; + + @override + Widget build(BuildContext context) { + final hovered = useState(false); + + final inputsWidth = inputs.map((input) => IOLabel.getNeededWidth(context, input)).fold(0, (previousValue, element) => max(previousValue, element)); + final outputsWidth = outputs.map((output) => IOLabel.getNeededWidth(context, output)).fold(0, (previousValue, element) => max(previousValue, element)); + + return MouseRegion( + onEnter: (event) => hovered.value = true, + onExit: (event) => hovered.value = false, + child: Row( + children: [ + Column( + children: [ + for (final input in inputs) Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: IOLabel( + label: input, + input: true, + lineColor: hovered.value + ? Theme.of(context).colorScheme.primary + : inputColors[input] ?? Colors.black, + width: inputsWidth, + ), + ), + ], + ), + Container( + width: 100, + decoration: BoxDecoration( + border: Border.all( + color: hovered.value ? Theme.of(context).colorScheme.primary : Colors.black, + ), + ), + child: Center( + child: Text( + name, + softWrap: true, + ), + ), + ), + Column( + children: [ + for (final output in outputs) Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: IOLabel( + label: output, + input: false, + lineColor: hovered.value + ? Theme.of(context).colorScheme.primary + : outputColors[output] ?? Colors.black, + width: outputsWidth, + ), + ), + ], + ), + ], + ), + ); + } + + static double getNeededWidth(BuildContext context, List inputs, List outputs, [TextStyle? textStyle]) { + final inputsWidth = inputs.map((input) => IOLabel.getNeededWidth(context, input, textStyle)).fold(0, (previousValue, element) => max(previousValue, element)); + final outputsWidth = outputs.map((output) => IOLabel.getNeededWidth(context, output, textStyle)).fold(0, (previousValue, element) => max(previousValue, element)); + return inputsWidth + outputsWidth + 100; + } + + static double getHeightOfIO(BuildContext context, List options, int index, [TextStyle? textStyle]) { + assert(index < options.length); + getHeightOfText(String text) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: (textStyle ?? DefaultTextStyle.of(context).style).merge( + const TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return textPainter.height; + } + var result = 0.0; + for (var i = 0; i < index; i++) { + result += 5.0 + getHeightOfText(options[i]) + 5.0; + } + result += 5.0 + getHeightOfText(options[index]); + return result; + } +} + +class IOComponent extends HookWidget { + final bool input; + final String name; + final double width; + final double circleDiameter; + final Color? color; + final void Function()? onTap; + + const IOComponent({super.key, required this.name, required this.input, this.width = 100, this.circleDiameter = 20, this.color, this.onTap}); + + @override + Widget build(BuildContext context) { + final hovered = useState(false); + + return MouseRegion( + onEnter: (event) => hovered.value = true, + onExit: (event) => hovered.value = false, + hitTestBehavior: HitTestBehavior.translucent, + child: GestureDetector( + onTap: onTap, + child: Builder( + builder: (context) { + final lineColor = hovered.value ? Theme.of(context).colorScheme.primary : color ?? Colors.black; + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (input) Container( + width: circleDiameter, + height: circleDiameter, + decoration: BoxDecoration( + border: Border.all(color: lineColor), + shape: BoxShape.circle, + color: color, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: circleDiameter - 2), + child: IOLabel( + label: name, + input: !input, + lineColor: lineColor, + width: width - circleDiameter, + ), + ), + if (!input) Container( + width: circleDiameter, + height: circleDiameter, + decoration: BoxDecoration( + border: Border.all(color: lineColor), + shape: BoxShape.circle, + color: color, + ), + ), + ], + ); + } + ), + ), + ); + } + + static double getNeededWidth(BuildContext context, String name, {double circleDiameter = 20, TextStyle? textStyle}) { + return circleDiameter + IOLabel.getNeededWidth(context, name, textStyle); + } +} + +class IOLabel extends StatelessWidget { + final bool input; + final String label; + final Color lineColor; + final double width; + + const IOLabel({super.key, required this.lineColor, required this.label, required this.input, this.width = 80}); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: 20, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: lineColor), + ), + ), + child: Align( + alignment: input ? Alignment.bottomRight : Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + label, + style: const TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ); + } + + static double getNeededWidth(BuildContext context, String text, [TextStyle? textStyle]) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: (textStyle ?? DefaultTextStyle.of(context).style).merge( + const TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return textPainter.width + 10; + } +} + +class WireWidget extends StatelessWidget { + final Offset from; + final Offset to; + final Color color; + + const WireWidget({ + required this.from, + required this.to, + this.color = Colors.black, + super.key, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _WireCustomPainter( + color: color, + primaryDiagonal: + (from.dx < to.dx && from.dy < to.dy) || + (from.dx > to.dx && from.dy > to.dy), + ), + child: SizedBox( + height: (to - from).dy.abs(), + width: (to - from).dx.abs(), + ), + ); + } +} + +class _WireCustomPainter extends CustomPainter { + final Color color; + final bool primaryDiagonal; + + const _WireCustomPainter({required this.color, required this.primaryDiagonal}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color; + if (primaryDiagonal) { + canvas.drawLine( + Offset.zero, + Offset(size.width, size.height), + paint, + ); + } + else { + canvas.drawLine( + Offset(size.width, 0), + Offset(0, size.height), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/models.dart b/lib/models.dart index 577d79b..19393f1 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -1,4 +1,5 @@ -export 'package:logic_circuits_simulator/models/projects.dart'; -export 'package:logic_circuits_simulator/models/project.dart'; export 'package:logic_circuits_simulator/models/component.dart'; +export 'package:logic_circuits_simulator/models/design.dart'; +export 'package:logic_circuits_simulator/models/project.dart'; +export 'package:logic_circuits_simulator/models/projects.dart'; export 'package:logic_circuits_simulator/models/wiring.dart'; diff --git a/lib/models/design.dart b/lib/models/design.dart new file mode 100644 index 0000000..93da726 --- /dev/null +++ b/lib/models/design.dart @@ -0,0 +1,60 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'design.freezed.dart'; +part 'design.g.dart'; + +@freezed +class Design with _$Design { + const factory Design({ + required List components, + required List wires, + required List inputs, + required List outputs, + }) = _Design; + + factory Design.fromJson(Map json) => _$DesignFromJson(json); +} + +@freezed +class DesignComponent with _$DesignComponent { + const factory DesignComponent({ + required String instanceId, + required double x, + required double y, + }) = _DesignComponent; + + factory DesignComponent.fromJson(Map json) => _$DesignComponentFromJson(json); +} + +@freezed +class DesignWire with _$DesignWire { + const factory DesignWire({ + required String wireId, + required double x, + required double y, + }) = _DesignWire; + + factory DesignWire.fromJson(Map json) => _$DesignWireFromJson(json); +} + +@freezed +class DesignInput with _$DesignInput { + const factory DesignInput({ + required String name, + required double x, + required double y, + }) = _DesignInput; + + factory DesignInput.fromJson(Map json) => _$DesignInputFromJson(json); +} + +@freezed +class DesignOutput with _$DesignOutput { + const factory DesignOutput({ + required String name, + required double x, + required double y, + }) = _DesignOutput; + + factory DesignOutput.fromJson(Map json) => _$DesignOutputFromJson(json); +} diff --git a/lib/models/design.freezed.dart b/lib/models/design.freezed.dart new file mode 100644 index 0000000..4da482d --- /dev/null +++ b/lib/models/design.freezed.dart @@ -0,0 +1,908 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'design.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +Design _$DesignFromJson(Map json) { + return _Design.fromJson(json); +} + +/// @nodoc +mixin _$Design { + List get components => throw _privateConstructorUsedError; + List get wires => throw _privateConstructorUsedError; + List get inputs => throw _privateConstructorUsedError; + List get outputs => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DesignCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DesignCopyWith<$Res> { + factory $DesignCopyWith(Design value, $Res Function(Design) then) = + _$DesignCopyWithImpl<$Res>; + $Res call( + {List components, + List wires, + List inputs, + List outputs}); +} + +/// @nodoc +class _$DesignCopyWithImpl<$Res> implements $DesignCopyWith<$Res> { + _$DesignCopyWithImpl(this._value, this._then); + + final Design _value; + // ignore: unused_field + final $Res Function(Design) _then; + + @override + $Res call({ + Object? components = freezed, + Object? wires = freezed, + Object? inputs = freezed, + Object? outputs = freezed, + }) { + return _then(_value.copyWith( + components: components == freezed + ? _value.components + : components // ignore: cast_nullable_to_non_nullable + as List, + wires: wires == freezed + ? _value.wires + : wires // ignore: cast_nullable_to_non_nullable + as List, + inputs: inputs == freezed + ? _value.inputs + : inputs // ignore: cast_nullable_to_non_nullable + as List, + outputs: outputs == freezed + ? _value.outputs + : outputs // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +abstract class _$$_DesignCopyWith<$Res> implements $DesignCopyWith<$Res> { + factory _$$_DesignCopyWith(_$_Design value, $Res Function(_$_Design) then) = + __$$_DesignCopyWithImpl<$Res>; + @override + $Res call( + {List components, + List wires, + List inputs, + List outputs}); +} + +/// @nodoc +class __$$_DesignCopyWithImpl<$Res> extends _$DesignCopyWithImpl<$Res> + implements _$$_DesignCopyWith<$Res> { + __$$_DesignCopyWithImpl(_$_Design _value, $Res Function(_$_Design) _then) + : super(_value, (v) => _then(v as _$_Design)); + + @override + _$_Design get _value => super._value as _$_Design; + + @override + $Res call({ + Object? components = freezed, + Object? wires = freezed, + Object? inputs = freezed, + Object? outputs = freezed, + }) { + return _then(_$_Design( + components: components == freezed + ? _value._components + : components // ignore: cast_nullable_to_non_nullable + as List, + wires: wires == freezed + ? _value._wires + : wires // ignore: cast_nullable_to_non_nullable + as List, + inputs: inputs == freezed + ? _value._inputs + : inputs // ignore: cast_nullable_to_non_nullable + as List, + outputs: outputs == freezed + ? _value._outputs + : outputs // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_Design implements _Design { + const _$_Design( + {required final List components, + required final List wires, + required final List inputs, + required final List outputs}) + : _components = components, + _wires = wires, + _inputs = inputs, + _outputs = outputs; + + factory _$_Design.fromJson(Map json) => + _$$_DesignFromJson(json); + + final List _components; + @override + List get components { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_components); + } + + final List _wires; + @override + List get wires { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_wires); + } + + final List _inputs; + @override + List get inputs { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_inputs); + } + + final List _outputs; + @override + List get outputs { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_outputs); + } + + @override + String toString() { + return 'Design(components: $components, wires: $wires, inputs: $inputs, outputs: $outputs)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Design && + const DeepCollectionEquality() + .equals(other._components, _components) && + const DeepCollectionEquality().equals(other._wires, _wires) && + const DeepCollectionEquality().equals(other._inputs, _inputs) && + const DeepCollectionEquality().equals(other._outputs, _outputs)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_components), + const DeepCollectionEquality().hash(_wires), + const DeepCollectionEquality().hash(_inputs), + const DeepCollectionEquality().hash(_outputs)); + + @JsonKey(ignore: true) + @override + _$$_DesignCopyWith<_$_Design> get copyWith => + __$$_DesignCopyWithImpl<_$_Design>(this, _$identity); + + @override + Map toJson() { + return _$$_DesignToJson(this); + } +} + +abstract class _Design implements Design { + const factory _Design( + {required final List components, + required final List wires, + required final List inputs, + required final List outputs}) = _$_Design; + + factory _Design.fromJson(Map json) = _$_Design.fromJson; + + @override + List get components => throw _privateConstructorUsedError; + @override + List get wires => throw _privateConstructorUsedError; + @override + List get inputs => throw _privateConstructorUsedError; + @override + List get outputs => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_DesignCopyWith<_$_Design> get copyWith => + throw _privateConstructorUsedError; +} + +DesignComponent _$DesignComponentFromJson(Map json) { + return _DesignComponent.fromJson(json); +} + +/// @nodoc +mixin _$DesignComponent { + String get instanceId => throw _privateConstructorUsedError; + double get x => throw _privateConstructorUsedError; + double get y => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DesignComponentCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DesignComponentCopyWith<$Res> { + factory $DesignComponentCopyWith( + DesignComponent value, $Res Function(DesignComponent) then) = + _$DesignComponentCopyWithImpl<$Res>; + $Res call({String instanceId, double x, double y}); +} + +/// @nodoc +class _$DesignComponentCopyWithImpl<$Res> + implements $DesignComponentCopyWith<$Res> { + _$DesignComponentCopyWithImpl(this._value, this._then); + + final DesignComponent _value; + // ignore: unused_field + final $Res Function(DesignComponent) _then; + + @override + $Res call({ + Object? instanceId = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_value.copyWith( + instanceId: instanceId == freezed + ? _value.instanceId + : instanceId // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +abstract class _$$_DesignComponentCopyWith<$Res> + implements $DesignComponentCopyWith<$Res> { + factory _$$_DesignComponentCopyWith( + _$_DesignComponent value, $Res Function(_$_DesignComponent) then) = + __$$_DesignComponentCopyWithImpl<$Res>; + @override + $Res call({String instanceId, double x, double y}); +} + +/// @nodoc +class __$$_DesignComponentCopyWithImpl<$Res> + extends _$DesignComponentCopyWithImpl<$Res> + implements _$$_DesignComponentCopyWith<$Res> { + __$$_DesignComponentCopyWithImpl( + _$_DesignComponent _value, $Res Function(_$_DesignComponent) _then) + : super(_value, (v) => _then(v as _$_DesignComponent)); + + @override + _$_DesignComponent get _value => super._value as _$_DesignComponent; + + @override + $Res call({ + Object? instanceId = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_$_DesignComponent( + instanceId: instanceId == freezed + ? _value.instanceId + : instanceId // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_DesignComponent implements _DesignComponent { + const _$_DesignComponent( + {required this.instanceId, required this.x, required this.y}); + + factory _$_DesignComponent.fromJson(Map json) => + _$$_DesignComponentFromJson(json); + + @override + final String instanceId; + @override + final double x; + @override + final double y; + + @override + String toString() { + return 'DesignComponent(instanceId: $instanceId, x: $x, y: $y)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DesignComponent && + const DeepCollectionEquality() + .equals(other.instanceId, instanceId) && + const DeepCollectionEquality().equals(other.x, x) && + const DeepCollectionEquality().equals(other.y, y)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(instanceId), + const DeepCollectionEquality().hash(x), + const DeepCollectionEquality().hash(y)); + + @JsonKey(ignore: true) + @override + _$$_DesignComponentCopyWith<_$_DesignComponent> get copyWith => + __$$_DesignComponentCopyWithImpl<_$_DesignComponent>(this, _$identity); + + @override + Map toJson() { + return _$$_DesignComponentToJson(this); + } +} + +abstract class _DesignComponent implements DesignComponent { + const factory _DesignComponent( + {required final String instanceId, + required final double x, + required final double y}) = _$_DesignComponent; + + factory _DesignComponent.fromJson(Map json) = + _$_DesignComponent.fromJson; + + @override + String get instanceId => throw _privateConstructorUsedError; + @override + double get x => throw _privateConstructorUsedError; + @override + double get y => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_DesignComponentCopyWith<_$_DesignComponent> get copyWith => + throw _privateConstructorUsedError; +} + +DesignWire _$DesignWireFromJson(Map json) { + return _DesignWire.fromJson(json); +} + +/// @nodoc +mixin _$DesignWire { + String get wireId => throw _privateConstructorUsedError; + double get x => throw _privateConstructorUsedError; + double get y => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DesignWireCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DesignWireCopyWith<$Res> { + factory $DesignWireCopyWith( + DesignWire value, $Res Function(DesignWire) then) = + _$DesignWireCopyWithImpl<$Res>; + $Res call({String wireId, double x, double y}); +} + +/// @nodoc +class _$DesignWireCopyWithImpl<$Res> implements $DesignWireCopyWith<$Res> { + _$DesignWireCopyWithImpl(this._value, this._then); + + final DesignWire _value; + // ignore: unused_field + final $Res Function(DesignWire) _then; + + @override + $Res call({ + Object? wireId = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_value.copyWith( + wireId: wireId == freezed + ? _value.wireId + : wireId // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +abstract class _$$_DesignWireCopyWith<$Res> + implements $DesignWireCopyWith<$Res> { + factory _$$_DesignWireCopyWith( + _$_DesignWire value, $Res Function(_$_DesignWire) then) = + __$$_DesignWireCopyWithImpl<$Res>; + @override + $Res call({String wireId, double x, double y}); +} + +/// @nodoc +class __$$_DesignWireCopyWithImpl<$Res> extends _$DesignWireCopyWithImpl<$Res> + implements _$$_DesignWireCopyWith<$Res> { + __$$_DesignWireCopyWithImpl( + _$_DesignWire _value, $Res Function(_$_DesignWire) _then) + : super(_value, (v) => _then(v as _$_DesignWire)); + + @override + _$_DesignWire get _value => super._value as _$_DesignWire; + + @override + $Res call({ + Object? wireId = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_$_DesignWire( + wireId: wireId == freezed + ? _value.wireId + : wireId // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_DesignWire implements _DesignWire { + const _$_DesignWire({required this.wireId, required this.x, required this.y}); + + factory _$_DesignWire.fromJson(Map json) => + _$$_DesignWireFromJson(json); + + @override + final String wireId; + @override + final double x; + @override + final double y; + + @override + String toString() { + return 'DesignWire(wireId: $wireId, x: $x, y: $y)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DesignWire && + const DeepCollectionEquality().equals(other.wireId, wireId) && + const DeepCollectionEquality().equals(other.x, x) && + const DeepCollectionEquality().equals(other.y, y)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(wireId), + const DeepCollectionEquality().hash(x), + const DeepCollectionEquality().hash(y)); + + @JsonKey(ignore: true) + @override + _$$_DesignWireCopyWith<_$_DesignWire> get copyWith => + __$$_DesignWireCopyWithImpl<_$_DesignWire>(this, _$identity); + + @override + Map toJson() { + return _$$_DesignWireToJson(this); + } +} + +abstract class _DesignWire implements DesignWire { + const factory _DesignWire( + {required final String wireId, + required final double x, + required final double y}) = _$_DesignWire; + + factory _DesignWire.fromJson(Map json) = + _$_DesignWire.fromJson; + + @override + String get wireId => throw _privateConstructorUsedError; + @override + double get x => throw _privateConstructorUsedError; + @override + double get y => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_DesignWireCopyWith<_$_DesignWire> get copyWith => + throw _privateConstructorUsedError; +} + +DesignInput _$DesignInputFromJson(Map json) { + return _DesignInput.fromJson(json); +} + +/// @nodoc +mixin _$DesignInput { + String get name => throw _privateConstructorUsedError; + double get x => throw _privateConstructorUsedError; + double get y => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DesignInputCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DesignInputCopyWith<$Res> { + factory $DesignInputCopyWith( + DesignInput value, $Res Function(DesignInput) then) = + _$DesignInputCopyWithImpl<$Res>; + $Res call({String name, double x, double y}); +} + +/// @nodoc +class _$DesignInputCopyWithImpl<$Res> implements $DesignInputCopyWith<$Res> { + _$DesignInputCopyWithImpl(this._value, this._then); + + final DesignInput _value; + // ignore: unused_field + final $Res Function(DesignInput) _then; + + @override + $Res call({ + Object? name = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_value.copyWith( + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +abstract class _$$_DesignInputCopyWith<$Res> + implements $DesignInputCopyWith<$Res> { + factory _$$_DesignInputCopyWith( + _$_DesignInput value, $Res Function(_$_DesignInput) then) = + __$$_DesignInputCopyWithImpl<$Res>; + @override + $Res call({String name, double x, double y}); +} + +/// @nodoc +class __$$_DesignInputCopyWithImpl<$Res> extends _$DesignInputCopyWithImpl<$Res> + implements _$$_DesignInputCopyWith<$Res> { + __$$_DesignInputCopyWithImpl( + _$_DesignInput _value, $Res Function(_$_DesignInput) _then) + : super(_value, (v) => _then(v as _$_DesignInput)); + + @override + _$_DesignInput get _value => super._value as _$_DesignInput; + + @override + $Res call({ + Object? name = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_$_DesignInput( + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_DesignInput implements _DesignInput { + const _$_DesignInput({required this.name, required this.x, required this.y}); + + factory _$_DesignInput.fromJson(Map json) => + _$$_DesignInputFromJson(json); + + @override + final String name; + @override + final double x; + @override + final double y; + + @override + String toString() { + return 'DesignInput(name: $name, x: $x, y: $y)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DesignInput && + const DeepCollectionEquality().equals(other.name, name) && + const DeepCollectionEquality().equals(other.x, x) && + const DeepCollectionEquality().equals(other.y, y)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(name), + const DeepCollectionEquality().hash(x), + const DeepCollectionEquality().hash(y)); + + @JsonKey(ignore: true) + @override + _$$_DesignInputCopyWith<_$_DesignInput> get copyWith => + __$$_DesignInputCopyWithImpl<_$_DesignInput>(this, _$identity); + + @override + Map toJson() { + return _$$_DesignInputToJson(this); + } +} + +abstract class _DesignInput implements DesignInput { + const factory _DesignInput( + {required final String name, + required final double x, + required final double y}) = _$_DesignInput; + + factory _DesignInput.fromJson(Map json) = + _$_DesignInput.fromJson; + + @override + String get name => throw _privateConstructorUsedError; + @override + double get x => throw _privateConstructorUsedError; + @override + double get y => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_DesignInputCopyWith<_$_DesignInput> get copyWith => + throw _privateConstructorUsedError; +} + +DesignOutput _$DesignOutputFromJson(Map json) { + return _DesignOutput.fromJson(json); +} + +/// @nodoc +mixin _$DesignOutput { + String get name => throw _privateConstructorUsedError; + double get x => throw _privateConstructorUsedError; + double get y => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DesignOutputCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DesignOutputCopyWith<$Res> { + factory $DesignOutputCopyWith( + DesignOutput value, $Res Function(DesignOutput) then) = + _$DesignOutputCopyWithImpl<$Res>; + $Res call({String name, double x, double y}); +} + +/// @nodoc +class _$DesignOutputCopyWithImpl<$Res> implements $DesignOutputCopyWith<$Res> { + _$DesignOutputCopyWithImpl(this._value, this._then); + + final DesignOutput _value; + // ignore: unused_field + final $Res Function(DesignOutput) _then; + + @override + $Res call({ + Object? name = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_value.copyWith( + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +abstract class _$$_DesignOutputCopyWith<$Res> + implements $DesignOutputCopyWith<$Res> { + factory _$$_DesignOutputCopyWith( + _$_DesignOutput value, $Res Function(_$_DesignOutput) then) = + __$$_DesignOutputCopyWithImpl<$Res>; + @override + $Res call({String name, double x, double y}); +} + +/// @nodoc +class __$$_DesignOutputCopyWithImpl<$Res> + extends _$DesignOutputCopyWithImpl<$Res> + implements _$$_DesignOutputCopyWith<$Res> { + __$$_DesignOutputCopyWithImpl( + _$_DesignOutput _value, $Res Function(_$_DesignOutput) _then) + : super(_value, (v) => _then(v as _$_DesignOutput)); + + @override + _$_DesignOutput get _value => super._value as _$_DesignOutput; + + @override + $Res call({ + Object? name = freezed, + Object? x = freezed, + Object? y = freezed, + }) { + return _then(_$_DesignOutput( + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + x: x == freezed + ? _value.x + : x // ignore: cast_nullable_to_non_nullable + as double, + y: y == freezed + ? _value.y + : y // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_DesignOutput implements _DesignOutput { + const _$_DesignOutput({required this.name, required this.x, required this.y}); + + factory _$_DesignOutput.fromJson(Map json) => + _$$_DesignOutputFromJson(json); + + @override + final String name; + @override + final double x; + @override + final double y; + + @override + String toString() { + return 'DesignOutput(name: $name, x: $x, y: $y)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DesignOutput && + const DeepCollectionEquality().equals(other.name, name) && + const DeepCollectionEquality().equals(other.x, x) && + const DeepCollectionEquality().equals(other.y, y)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(name), + const DeepCollectionEquality().hash(x), + const DeepCollectionEquality().hash(y)); + + @JsonKey(ignore: true) + @override + _$$_DesignOutputCopyWith<_$_DesignOutput> get copyWith => + __$$_DesignOutputCopyWithImpl<_$_DesignOutput>(this, _$identity); + + @override + Map toJson() { + return _$$_DesignOutputToJson(this); + } +} + +abstract class _DesignOutput implements DesignOutput { + const factory _DesignOutput( + {required final String name, + required final double x, + required final double y}) = _$_DesignOutput; + + factory _DesignOutput.fromJson(Map json) = + _$_DesignOutput.fromJson; + + @override + String get name => throw _privateConstructorUsedError; + @override + double get x => throw _privateConstructorUsedError; + @override + double get y => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$$_DesignOutputCopyWith<_$_DesignOutput> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/design.g.dart b/lib/models/design.g.dart new file mode 100644 index 0000000..dc37bc9 --- /dev/null +++ b/lib/models/design.g.dart @@ -0,0 +1,85 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'design.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_Design _$$_DesignFromJson(Map json) => _$_Design( + components: (json['components'] as List) + .map((e) => DesignComponent.fromJson(e as Map)) + .toList(), + wires: (json['wires'] as List) + .map((e) => DesignWire.fromJson(e as Map)) + .toList(), + inputs: (json['inputs'] as List) + .map((e) => DesignInput.fromJson(e as Map)) + .toList(), + outputs: (json['outputs'] as List) + .map((e) => DesignOutput.fromJson(e as Map)) + .toList(), + ); + +Map _$$_DesignToJson(_$_Design instance) => { + 'components': instance.components, + 'wires': instance.wires, + 'inputs': instance.inputs, + 'outputs': instance.outputs, + }; + +_$_DesignComponent _$$_DesignComponentFromJson(Map json) => + _$_DesignComponent( + instanceId: json['instanceId'] as String, + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + ); + +Map _$$_DesignComponentToJson(_$_DesignComponent instance) => + { + 'instanceId': instance.instanceId, + 'x': instance.x, + 'y': instance.y, + }; + +_$_DesignWire _$$_DesignWireFromJson(Map json) => + _$_DesignWire( + wireId: json['wireId'] as String, + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + ); + +Map _$$_DesignWireToJson(_$_DesignWire instance) => + { + 'wireId': instance.wireId, + 'x': instance.x, + 'y': instance.y, + }; + +_$_DesignInput _$$_DesignInputFromJson(Map json) => + _$_DesignInput( + name: json['name'] as String, + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + ); + +Map _$$_DesignInputToJson(_$_DesignInput instance) => + { + 'name': instance.name, + 'x': instance.x, + 'y': instance.y, + }; + +_$_DesignOutput _$$_DesignOutputFromJson(Map json) => + _$_DesignOutput( + name: json['name'] as String, + x: (json['x'] as num).toDouble(), + y: (json['y'] as num).toDouble(), + ); + +Map _$$_DesignOutputToJson(_$_DesignOutput instance) => + { + 'name': instance.name, + 'x': instance.x, + 'y': instance.y, + }; diff --git a/lib/pages/design_component.dart b/lib/pages/design_component.dart index cc4a841..a4fe7a5 100644 --- a/lib/pages/design_component.dart +++ b/lib/pages/design_component.dart @@ -1,12 +1,19 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:logic_circuits_simulator/components/visual_component.dart'; import 'package:logic_circuits_simulator/models.dart'; import 'package:logic_circuits_simulator/pages_arguments/design_component.dart'; import 'package:logic_circuits_simulator/state/component.dart'; +import 'package:logic_circuits_simulator/utils/future_call_debounce.dart'; +import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; import 'package:logic_circuits_simulator/utils/provider_hook.dart'; import 'package:logic_circuits_simulator/utils/stack_canvas_controller_hook.dart'; import 'package:stack_canvas/stack_canvas.dart'; +Key canvasKey = GlobalKey(); + class DesignComponentPage extends HookWidget { final ComponentEntry component; @@ -19,51 +26,378 @@ class DesignComponentPage extends HookWidget { @override Widget build(BuildContext context) { final componentState = useProvider(); - final canvasController = useStackCanvasController(); - final widgets = useState(>[]); + final canvasController = useStackCanvasController( + offsetReference: Reference.Center, + ); + + // Simulation vars + final isSimulating = useState(false); + final simulatePartially = useState(false); + + useListenable(componentState.partialVisualSimulation!); + + final movingWidgetUpdater = useState(null); + final movingWidget = useState(null); + final widgets = useMemoized(() => [ + for (final subcomponent in componentState.designDraft.components) + CanvasObject( + dx: subcomponent.x, + dy: subcomponent.y, + width: VisualComponent.getNeededWidth( + context, + componentState + .getMetaByInstance(subcomponent.instanceId) + .item2 + .inputs, + componentState + .getMetaByInstance(subcomponent.instanceId) + .item2 + .outputs, + Theme.of(context).textTheme.bodyMedium, + ), + height: max( + componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs.length, + componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs.length, + ) * 30 + 10, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + final debouncer = FutureCallDebounce>( + futureCall: (xyList) { + final dx = xyList[0]; + final dy = xyList[1]; + return componentState.updateDesign(componentState.designDraft.copyWith( + components: componentState.designDraft.components.map( + (e) => e.instanceId == subcomponent.instanceId ? e.copyWith( + x: subcomponent.x + dx, + y: subcomponent.y + dy, + ) : e, + ).toList(), + ), commit: false); + }, + combiner: (oldParams, newParams) { + return oldParams.zipWith([newParams], (deltas) => deltas.fold(0.0, (prev, elem) => prev + elem)).toList(); + }, + ); + movingWidgetUpdater.value = (dx, dy) { + debouncer.call([dx, dy]); + }; + movingWidget.value = subcomponent; + }, + onPointerUp: (event) { + componentState.updateDesign(componentState.designDraft); + movingWidgetUpdater.value = null; + movingWidget.value = null; + }, + child: MouseRegion( + cursor: movingWidget.value == subcomponent ? SystemMouseCursors.move : MouseCursor.defer, + child: VisualComponent( + name: componentState.getMetaByInstance(subcomponent.instanceId).item2.componentName, + inputs: componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs, + outputs: componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs, + inputColors: isSimulating.value ? { + for (final input in componentState.getMetaByInstance(subcomponent.instanceId).item2.inputs) + input: componentState.partialVisualSimulation!.inputsValues['${subcomponent.instanceId}/$input'] == true ? Colors.green + : componentState.partialVisualSimulation!.inputsValues['${subcomponent.instanceId}/$input'] == false ? Colors.red + : Colors.black, + } : null, + outputColors: isSimulating.value ? { + for (final output in componentState.getMetaByInstance(subcomponent.instanceId).item2.outputs) + output: componentState.partialVisualSimulation!.outputsValues['${subcomponent.instanceId}/$output'] == true ? Colors.green + : componentState.partialVisualSimulation!.outputsValues['${subcomponent.instanceId}/$output'] == false ? Colors.red + : Colors.black, + } : null, + ), + ), + ), + ), + for (final input in componentState.designDraft.inputs) + CanvasObject( + dx: input.x, + dy: input.y, + width: IOComponent.getNeededWidth(context, input.name, textStyle: Theme.of(context).textTheme.bodyMedium), + height: 40, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + final debouncer = FutureCallDebounce>( + futureCall: (xyList) { + final dx = xyList[0]; + final dy = xyList[1]; + return componentState.updateDesign(componentState.designDraft.copyWith( + inputs: componentState.designDraft.inputs.map( + (e) => e.name == input.name ? e.copyWith( + x: input.x + dx, + y: input.y + dy, + ) : e, + ).toList(), + ), commit: false); + }, + combiner: (oldParams, newParams) { + return oldParams.zipWith([newParams], (deltas) => deltas.fold(0.0, (prev, elem) => prev + elem)).toList(); + }, + ); + movingWidgetUpdater.value = (dx, dy) { + debouncer.call([dx, dy]); + }; + }, + onPointerUp: (event) { + componentState.updateDesign(componentState.designDraft); + movingWidgetUpdater.value = null; + }, + child: IOComponent( + input: true, + name: input.name, + width: IOComponent.getNeededWidth(context, input.name, textStyle: Theme.of(context).textTheme.bodyMedium), + color: isSimulating.value + ? (componentState.partialVisualSimulation!.outputsValues['self/${input.name}']! + ? Colors.green + : Colors.red) + : null, + onTap: isSimulating.value ? () { + componentState.partialVisualSimulation!.toggleInput(input.name); + } : null, + ), + ), + ), + for (final output in componentState.designDraft.outputs) + CanvasObject( + dx: output.x, + dy: output.y, + width: IOComponent.getNeededWidth(context, output.name, textStyle: Theme.of(context).textTheme.bodyMedium), + height: 40, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + final debouncer = FutureCallDebounce>( + futureCall: (xyList) { + final dx = xyList[0]; + final dy = xyList[1]; + return componentState.updateDesign(componentState.designDraft.copyWith( + outputs: componentState.designDraft.outputs.map( + (e) => e.name == output.name ? e.copyWith( + x: output.x + dx, + y: output.y + dy, + ) : e, + ).toList(), + ), commit: false); + }, + combiner: (oldParams, newParams) { + return oldParams.zipWith([newParams], (deltas) => deltas.fold(0.0, (prev, elem) => prev + elem)).toList(); + }, + ); + movingWidgetUpdater.value = (dx, dy) { + debouncer.call([dx, dy]); + }; + }, + onPointerUp: (event) { + componentState.updateDesign(componentState.designDraft); + movingWidgetUpdater.value = null; + }, + child: IOComponent( + input: false, + name: output.name, + width: IOComponent.getNeededWidth(context, output.name, textStyle: Theme.of(context).textTheme.bodyMedium), + color: isSimulating.value + ? (componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == true ? Colors.green + : componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == false ? Colors.red + : null) + : null, + ), + ), + ), + for (final wire in componentState.wiring.wires) + (() { + const ioCircleDiameter = 20; + + Offset from, to; + if (wire.output.split('/')[0] == 'self') { + // It's a component input + + // Find input + final inputName = wire.output.split('/')[1]; + final design = componentState.designDraft.inputs.where((i) => i.name == inputName).first; + + from = Offset( + // Take into account widget length + design.x + + IOComponent.getNeededWidth( + context, + inputName, + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + design.y + ioCircleDiameter + 1, + ); + } + else { + // It's a subcomponent output + + // Find subcomponent + final split = wire.output.split('/'); + final subcomponentId = split[0]; + final outputName = split[1]; + final design = componentState.designDraft.components.where((c) => c.instanceId == subcomponentId).first; + final subcomponent = componentState.getMetaByInstance(subcomponentId).item2; + + from = Offset( + // Take into account widget length + design.x + + VisualComponent.getNeededWidth( + context, + subcomponent.inputs, + subcomponent.outputs, + Theme.of(context).textTheme.bodyMedium, + ), + design.y + + VisualComponent.getHeightOfIO( + context, + subcomponent.outputs, + subcomponent.outputs.indexOf(outputName), + Theme.of(context).textTheme.bodyMedium, + ), + ); + } + + if (wire.input.split('/')[0] == 'self') { + // It's a component output + + // Find output + final outputName = wire.input.split('/')[1]; + final design = componentState.designDraft.outputs.where((o) => o.name == outputName).first; + + to = Offset( + design.x, + design.y + ioCircleDiameter + 1, + ); + } + else { + // It's a subcomponent input + + // Find subcomponent + final split = wire.input.split('/'); + final subcomponentId = split[0]; + final inputName = split[1]; + final design = componentState.designDraft.components.where((c) => c.instanceId == subcomponentId).first; + final subcomponent = componentState.getMetaByInstance(subcomponentId).item2; + + to = Offset( + // Take into account widget length + design.x, + design.y + + VisualComponent.getHeightOfIO( + context, + subcomponent.inputs, + subcomponent.inputs.indexOf(inputName), + Theme.of(context).textTheme.bodyMedium, + ), + ); + } + + var wireColor = Colors.black; + if (isSimulating.value) { + final wireValue = componentState.partialVisualSimulation!.outputsValues[wire.output]; + if (wireValue == true) { + wireColor = Colors.green; + } + else if (wireValue == false) { + wireColor = Colors.red; + } + } + + return CanvasObject( + dx: min(from.dx, to.dx), + dy: min(from.dy, to.dy), + width: (to - from).dx.abs(), + height: (to - from).dy.abs(), + child: IgnorePointer( + child: WireWidget( + from: from, + to: to, + color: wireColor, + ), + ), + ); + })(), + ], [componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues]); useEffect(() { - canvasController.addCanvasObjects(widgets.value); + final wList = widgets; + canvasController.addCanvasObjects(wList); return () { // Cleanup - canvasController.clearCanvas(); + for (final obj in wList) { + canvasController.removeCanvasObject(obj); + } }; }, [widgets]); + useEffect(() { + if (isSimulating.value && !simulatePartially.value && componentState.partialVisualSimulation!.nextToSimulate.isNotEmpty) { + componentState.partialVisualSimulation!.nextStep(); + } + return null; + }, [componentState.partialVisualSimulation!.outputsValues.entries.map((e) => '${e.key}:${e.value}').join(';'), simulatePartially.value, isSimulating.value]); return Scaffold( appBar: AppBar( centerTitle: true, - title: Text('Design - ${component.componentName}'), + title: Text('${isSimulating.value ? 'Simulation' : 'Design'} - ${component.componentName}'), + actions: [ + IconButton( + icon: Icon(isSimulating.value ? Icons.stop : Icons.start), + tooltip: isSimulating.value ? 'Stop Simulation' : 'Start Simulation', + onPressed: () { + isSimulating.value = !isSimulating.value; + }, + ), + ], ), - body: OrientationBuilder( - builder: (context, orientation) { - if (orientation == Orientation.portrait) { - return Column( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: StackCanvas( - canvasController: canvasController, - backgroundColor: Theme.of(context).colorScheme.background, - ), - ), - ], - ); + body: GestureDetector( + onPanUpdate: (update) { + final hw = movingWidgetUpdater.value; + if (hw == null || isSimulating.value) { + canvasController.offset = canvasController.offset.translate(update.delta.dx, update.delta.dy); } else { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: StackCanvas( - canvasController: canvasController, + hw(update.delta.dx, update.delta.dy); + } + }, + child: OrientationBuilder( + builder: (context, orientation) { + if (orientation == Orientation.portrait) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: StackCanvas( + key: canvasKey, + canvasController: canvasController, + animationDuration: const Duration(milliseconds: 50), + // disposeController: false, + backgroundColor: Theme.of(context).colorScheme.background, + ), ), - ), - ], - ); + ], + ); + } + else { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: StackCanvas( + key: canvasKey, + canvasController: canvasController, + animationDuration: const Duration(milliseconds: 50), + // disposeController: false, + backgroundColor: Theme.of(context).colorScheme.background, + ), + ), + ], + ); + } } - } + ), ), ); } -} +} \ No newline at end of file diff --git a/lib/state/component.dart b/lib/state/component.dart index b4a476b..6e85261 100644 --- a/lib/state/component.dart +++ b/lib/state/component.dart @@ -12,13 +12,41 @@ class ComponentState extends ChangeNotifier { ProjectEntry? _currentProject; ComponentEntry? _currentComponent; Wiring _wiring = const Wiring(instances: [], wires: []); + Wiring? _wiringDraft; + Design _design = const Design(components: [], wires: [], inputs: [], outputs: []); + Design? _designDraft; SimulatedComponent? _simulatedComponent; + PartialVisualSimulation? _partialVisualSimulation; final Map> _dependenciesMap = {}; ProjectEntry? get currentProject => _currentProject; ComponentEntry? get currentComponent => _currentComponent; Wiring get wiring => _wiring; + Wiring get wiringDraft => _wiringDraft ?? _wiring; + Design get design => _design; + Design get designDraft => _designDraft ?? _design; + PartialVisualSimulation? get partialVisualSimulation => _partialVisualSimulation; + + Future _onRequiredDependency(String depId) async { + final t = _dependenciesMap[depId]!; + final proj = t.item1; + final comp = t.item2; + final state = comp.visualDesigned ? ComponentState() : null; + if (state != null) { + await state.setCurrentComponent( + project: proj, + component: comp, + onDependencyNeeded: (projId, compId) async => _dependenciesMap['$projId/$compId'], + ); + } + return SimulatedComponent( + project: proj, + component: comp, + onRequiredDependency: _onRequiredDependency, + state: state, + ); + } Future _getComponentDir() async { if (_currentProject == null) { @@ -40,6 +68,11 @@ class ComponentState extends ChangeNotifier { return result; } + Future _getDesignFile() async { + final result = File(path.join((await _getComponentDir()).path, 'design.json')); + return result; + } + Future _loadComponentFiles() async { final wiringFile = await _getWiringFile(); if (!await wiringFile.exists()) { @@ -49,6 +82,17 @@ class ComponentState extends ChangeNotifier { else { _wiring = Wiring.fromJson(jsonDecode(await wiringFile.readAsString())); } + _wiringDraft = null; + + final designFile = await _getDesignFile(); + if (!await designFile.exists()) { + _design = const Design(components: [], wires: [], inputs: [], outputs: []); + await designFile.writeAsString(jsonEncode(_design)); + } + else { + _design = Design.fromJson(jsonDecode(await designFile.readAsString())); + } + _designDraft = null; } Future setCurrentComponent({ @@ -78,7 +122,18 @@ class ComponentState extends ChangeNotifier { throw DependenciesNotSatisfiedException(dependencies: unsatisfiedDependencies); } - return _loadComponentFiles().then((_) => notifyListeners()); + await _loadComponentFiles(); + + if (component.visualDesigned) { + _partialVisualSimulation = await PartialVisualSimulation.init( + project: project, + component: component, + state: this, + onRequiredDependency: _onRequiredDependency, + ); + } + + notifyListeners(); } void noComponent() { @@ -86,41 +141,63 @@ class ComponentState extends ChangeNotifier { _currentProject = null; _currentComponent = null; _wiring = const Wiring(instances: [], wires: []); + _design = const Design(components: [], wires: [], inputs: [], outputs: []); + _wiringDraft = _designDraft = null; _simulatedComponent = null; + _partialVisualSimulation = null; notifyListeners(); } - Future> simulate(Map inputs) async { - Future onRequiredDependency(String depId) async { - final t = _dependenciesMap[depId]!; - final proj = t.item1; - final comp = t.item2; - final state = comp.visualDesigned ? ComponentState() : null; - if (state != null) { - await state.setCurrentComponent( - project: proj, - component: comp, - onDependencyNeeded: (projId, compId) async => _dependenciesMap['$projId/$compId'], - ); + Tuple2 getMetaByInstance(String instanceId) { + for (final instance in wiring.instances) { + if (instance.instanceId == instanceId) { + return _dependenciesMap[instance.componentId]!; } - return SimulatedComponent( - project: proj, - component: comp, - onRequiredDependency: onRequiredDependency, - state: state, - ); } + throw Exception('Instance $instanceId not found in the dependencies map'); + } + + Future> simulate(Map inputs) async { + _simulatedComponent ??= SimulatedComponent( project: _currentProject!, component: _currentComponent!, - onRequiredDependency: onRequiredDependency, + onRequiredDependency: _onRequiredDependency, state: this, ); return _simulatedComponent!.simulate(inputs); } + + Future updateDesign(Design newDesign, {bool commit = true}) async { + if (commit) { + _design = newDesign; + _designDraft = null; + final designFile = await _getDesignFile(); + await designFile.writeAsString(jsonEncode(newDesign)); + } + else { + _designDraft = newDesign; + } + notifyListeners(); + return designDraft; + } + + Future updateWiring(Wiring newWiring, {bool commit = true}) async { + if (commit) { + _wiring = newWiring; + _wiringDraft = null; + final wiringFile = await _getWiringFile(); + await wiringFile.writeAsString(jsonEncode(newWiring)); + } + else { + _wiringDraft = newWiring; + } + notifyListeners(); + return wiringDraft; + } } class DependenciesNotSatisfiedException with Exception { diff --git a/lib/utils/future_call_debounce.dart b/lib/utils/future_call_debounce.dart new file mode 100644 index 0000000..737d823 --- /dev/null +++ b/lib/utils/future_call_debounce.dart @@ -0,0 +1,22 @@ +class FutureCallDebounce { + TParams? _params; + Future? _awaited; + final Future Function(TParams) futureCall; + final TParams Function(TParams oldParams, TParams newParams) combiner; + + static TParams _defaultCombiner(TParams _, TParams newParams) => newParams; + + FutureCallDebounce({required this.futureCall, required this.combiner}); + FutureCallDebounce.replaceCombiner({required this.futureCall}) : combiner = _defaultCombiner; + + void call(TParams newParams) { + if (_params != null) { + _params = combiner(_params!, newParams); + } + else { + _params = newParams; + } + + _awaited ??= futureCall(_params!).then((value) => _awaited = null); + } +} diff --git a/lib/utils/simulation.dart b/lib/utils/simulation.dart index ea20206..173da94 100644 --- a/lib/utils/simulation.dart +++ b/lib/utils/simulation.dart @@ -1,3 +1,5 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:logic_circuits_simulator/models.dart'; import 'package:logic_circuits_simulator/state/component.dart'; import 'package:logic_circuits_simulator/utils/iterable_extension.dart'; @@ -10,19 +12,18 @@ class SimulatedComponent { final Future Function(String depId) onRequiredDependency; final _instances = {}; - SimulatedComponent({ - required this.project, - required this.component, - required this.onRequiredDependency, - this.state - }); + SimulatedComponent( + {required this.project, + required this.component, + required this.onRequiredDependency, + this.state}); - Future _getInstance(String instanceId, String? depId) async { + Future _getInstance( + String instanceId, String? depId) async { if (!_instances.containsKey(instanceId)) { if (depId != null) { _instances[instanceId] = await onRequiredDependency(depId); - } - else { + } else { throw Exception('Attempted to get instance of unknown component'); } } @@ -31,39 +32,32 @@ class SimulatedComponent { Future> simulate(Map inputs) async { final input = int.parse( - component.inputs.map((input) => inputs[input]! ? '1' : '0').join(), + component.inputs.map((input) => inputs[input]! ? '1' : '0').join(), radix: 2, ); if (component.truthTable != null) { final output = component.truthTable![input]; - return { - for (final it in component.outputs.indexedMap( - (index, outName) => [outName, output[index]] - )) - it[0] : it[1] == '1' + return { + for (final it in component.outputs + .indexedMap((index, outName) => [outName, output[index]])) + it[0]: it[1] == '1' }; - } - else if (component.logicExpression != null) { + } else if (component.logicExpression != null) { // Somehow? // A truth table should be automatically generated for every logic expression component. // Might as well handle cases where that isn't done anyway. final results = component.outputs.zipWith( - [component.logicExpression!], + [component.logicExpression!], (zips) { final output = zips[0]; final le = LogicExpression.parse(zips[1]); return [output, le.evaluate(inputs)]; }, ); - return { - for (final it in results) - it[0] as String : it[1] as bool - }; - } - else if (state == null) { + return {for (final it in results) it[0] as String: it[1] as bool}; + } else if (state == null) { throw Exception('Cannot simulate designed component without its state'); - } - else { + } else { // Create instances final wiring = state!.wiring; for (final instance in wiring.instances) { @@ -75,8 +69,7 @@ class SimulatedComponent { ...component.outputs.map((output) => 'self/$output'), ]; final knownSources = { - for (final entry in inputs.entries) - 'self/${entry.key}': entry.value + for (final entry in inputs.entries) 'self/${entry.key}': entry.value }; final knownSinks = {}; @@ -86,37 +79,40 @@ class SimulatedComponent { if (knownSinks.containsKey(sink)) { // Requirement satisfied continue; - } - else { + } else { // Find wire that provides sink final wire = wiring.wires.where((wire) => wire.input == sink).first; if (knownSources.containsKey(wire.output)) { - // If we know the output provided through the wire, + // If we know the output provided through the wire, // we know the input provided to the sink knownSinks[sink] = knownSources[wire.output]!; - } - else { + } else { // The instance providing the source for the wire has not been simulated. // See if all its sinks are known: final instanceId = wire.output.split('/')[0]; final instance = await _getInstance(instanceId, null); - final depSinks = instance.component.inputs.map((input) => '$instanceId/$input').toList(); - if (depSinks.map((depSink) => !knownSinks.containsKey(depSink)).where((cond) => cond).isEmpty) { + final depSinks = instance.component.inputs + .map((input) => '$instanceId/$input') + .toList(); + if (depSinks + .map((depSink) => !knownSinks.containsKey(depSink)) + .where((cond) => cond) + .isEmpty) { // If so, then simulate final results = await instance.simulate({ for (final depSink in depSinks) - depSink.split('/')[1] : knownSinks[depSink]! + depSink.split('/')[1]: knownSinks[depSink]! }); knownSources.addAll({ for (final result in results.entries) - '$instanceId/${result.key}' : result.value + '$instanceId/${result.key}': result.value }); // And resolve needed sink knownSinks[sink] = knownSources[wire.output]!; - } - else { + } else { // Otherwise, require the sinks and reschedule the current one - requiredSinks.addAll(depSinks.where((depSink) => !knownSinks.containsKey(depSink))); + requiredSinks.addAll(depSinks + .where((depSink) => !knownSinks.containsKey(depSink))); requiredSinks.add(sink); } } @@ -125,8 +121,162 @@ class SimulatedComponent { return { for (final output in component.outputs) - output : knownSinks['self/$output']! + output: knownSinks['self/$output']! }; } } -} \ No newline at end of file +} + +class PartialVisualSimulation with ChangeNotifier { + final Map _outputsValues = {}; + final List nextToSimulate = []; + final List _alreadySimulated = []; + + UnmodifiableMapView get outputsValues => UnmodifiableMapView(_outputsValues); + UnmodifiableMapView get inputsValues => UnmodifiableMapView({ + for (final entry in outputsValues.entries) + if (entry.value != null) + for (final wire in state.wiringDraft.wires.where((w) => w.output == entry.key)) + wire.input: entry.value + }); + + final ProjectEntry project; + final ComponentEntry component; + final ComponentState state; + final Future Function(String depId) onRequiredDependency; + final _instances = {}; + + PartialVisualSimulation._( + {required this.project, + required this.component, + required this.state, + required this.onRequiredDependency}); + + Future _getInstance( + String instanceId, String? depId) async { + if (!_instances.containsKey(instanceId)) { + if (depId != null) { + _instances[instanceId] = await onRequiredDependency(depId); + } else { + throw Exception('Attempted to get instance of unknown component'); + } + } + return _instances[instanceId]!; + } + + static Future init({ + required ProjectEntry project, + required ComponentEntry component, + required ComponentState state, + required Future Function(String depId) onRequiredDependency, + Map? inputs, + }) async { + final sim = PartialVisualSimulation._(project: project, component: component, state: state, onRequiredDependency: onRequiredDependency); + + // Create instances + final wiring = state.wiring; + for (final instance in wiring.instances) { + await sim._getInstance(instance.instanceId, instance.componentId); + } + + // Populate inputs + inputs ??= {}; + for (final input in component.inputs) { + if (!inputs.containsKey(input)) { + inputs[input] = false; + } + } + await sim.provideInputs(inputs); + + return sim; + } + + Future toggleInput(String inputName) { + final inputValue = _outputsValues['self/$inputName']!; + return modifyInput(inputName, !inputValue); + } + + Future modifyInput(String inputName, bool newValue) { + _outputsValues['self/$inputName'] = newValue; + for (final key in _outputsValues.keys.toList()) { + if (!key.startsWith('self/')) { + _outputsValues.remove(key); + } + } + _alreadySimulated.clear(); + return reset(); + } + + Future provideInputs(Map inputs) { + _alreadySimulated.clear(); + _outputsValues.clear(); + for (final entry in inputs.entries) { + _outputsValues['self/${entry.key}'] = entry.value; + } + return reset(); + } + + Future reset() async { + nextToSimulate.clear(); + + final neededToBeNext = >{}; + + for (final wire in state.wiringDraft.wires) { + if (_outputsValues.containsKey(wire.output)) { + final subcomponentId = wire.input.split('/')[0]; + + // Ignore component outputs, they require no computation + if (subcomponentId == 'self') { + continue; + } + + // Skip already simulated subcomponents + if (_alreadySimulated.contains(subcomponentId)) { + continue; + } + + if (neededToBeNext.containsKey(subcomponentId)) { + neededToBeNext[subcomponentId]!.remove(wire.input.split('/')[1]); + if (neededToBeNext[subcomponentId]!.isEmpty) { + nextToSimulate.add(subcomponentId); + } + } + else { + neededToBeNext[subcomponentId] = + (await _getInstance(subcomponentId, null)) + .component + .inputs + .whereNot((e) => e == wire.input.split('/')[1]) + .toList(); + if (neededToBeNext[subcomponentId]!.isEmpty) { + nextToSimulate.add(subcomponentId); + } + } + } + } + + notifyListeners(); + } + + Future nextStep() async { + if (nextToSimulate.isEmpty) { + return; + } + + final currentlySimulating = nextToSimulate.toList(); + + for (final subcomponentId in currentlySimulating) { + final sim = await _getInstance(subcomponentId, null); + final outputs = await sim.simulate({ + for (final input in sim.component.inputs) + input: inputsValues['$subcomponentId/$input']! + }); + for (final entry in outputs.entries) { + _outputsValues['$subcomponentId/${entry.key}'] = entry.value; + } + _alreadySimulated.add(subcomponentId); + } + + return reset(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9d7b467..631564c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -633,9 +633,11 @@ packages: stack_canvas: dependency: "direct main" description: - name: stack_canvas - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: HEAD + resolved-ref: "83e1032940e9424572c60ddad397f38320ee9cd4" + url: "https://github.com/dancojocaru2000/stack_canvas.git" + source: git version: "0.2.0+2" stack_trace: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index a5f613f..be56be3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,8 @@ dependencies: archive: ^3.3.0 file_picker: ^4.6.1 share_plus: ^4.0.8 - stack_canvas: ^0.2.0+2 + stack_canvas: + git: https://github.com/dancojocaru2000/stack_canvas.git tuple: ^2.0.0 dev_dependencies: