Browse Source

Implemented partial simulation and design*

- Implemented partial (step by step) simulation of visually designed
components
- Implemented moving components in design mode and simulating components
in simulation mode (click inputs to toggle)

TODO:
- add/remove subcomponents, wires via GUI
- add GUI for step by step simulation
master
Kenneth Bruen 2 years ago
parent
commit
c2d5d86554
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 296
      lib/components/visual_component.dart
  2. 5
      lib/models.dart
  3. 60
      lib/models/design.dart
  4. 908
      lib/models/design.freezed.dart
  5. 85
      lib/models/design.g.dart
  6. 392
      lib/pages/design_component.dart
  7. 117
      lib/state/component.dart
  8. 22
      lib/utils/future_call_debounce.dart
  9. 224
      lib/utils/simulation.dart
  10. 8
      pubspec.lock
  11. 3
      pubspec.yaml

296
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<String> inputs;
final List<String> outputs;
final Map<String, Color?> inputColors;
final Map<String, Color?> outputColors;
VisualComponent({super.key, required this.name, required this.inputs, required this.outputs, Map<String, Color?>? inputColors, Map<String, Color?>? outputColors})
: inputColors = inputColors ?? {}
, outputColors = outputColors ?? {};
@override
Widget build(BuildContext context) {
final hovered = useState(false);
final inputsWidth = inputs.map((input) => IOLabel.getNeededWidth(context, input)).fold<double>(0, (previousValue, element) => max(previousValue, element));
final outputsWidth = outputs.map((output) => IOLabel.getNeededWidth(context, output)).fold<double>(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<String> inputs, List<String> outputs, [TextStyle? textStyle]) {
final inputsWidth = inputs.map((input) => IOLabel.getNeededWidth(context, input, textStyle)).fold<double>(0, (previousValue, element) => max(previousValue, element));
final outputsWidth = outputs.map((output) => IOLabel.getNeededWidth(context, output, textStyle)).fold<double>(0, (previousValue, element) => max(previousValue, element));
return inputsWidth + outputsWidth + 100;
}
static double getHeightOfIO(BuildContext context, List<String> 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;
}
}

5
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';

60
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<DesignComponent> components,
required List<DesignWire> wires,
required List<DesignInput> inputs,
required List<DesignOutput> outputs,
}) = _Design;
factory Design.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => _$DesignOutputFromJson(json);
}

908
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>(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<String, dynamic> json) {
return _Design.fromJson(json);
}
/// @nodoc
mixin _$Design {
List<DesignComponent> get components => throw _privateConstructorUsedError;
List<DesignWire> get wires => throw _privateConstructorUsedError;
List<DesignInput> get inputs => throw _privateConstructorUsedError;
List<DesignOutput> get outputs => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesignCopyWith<Design> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DesignCopyWith<$Res> {
factory $DesignCopyWith(Design value, $Res Function(Design) then) =
_$DesignCopyWithImpl<$Res>;
$Res call(
{List<DesignComponent> components,
List<DesignWire> wires,
List<DesignInput> inputs,
List<DesignOutput> 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<DesignComponent>,
wires: wires == freezed
? _value.wires
: wires // ignore: cast_nullable_to_non_nullable
as List<DesignWire>,
inputs: inputs == freezed
? _value.inputs
: inputs // ignore: cast_nullable_to_non_nullable
as List<DesignInput>,
outputs: outputs == freezed
? _value.outputs
: outputs // ignore: cast_nullable_to_non_nullable
as List<DesignOutput>,
));
}
}
/// @nodoc
abstract class _$$_DesignCopyWith<$Res> implements $DesignCopyWith<$Res> {
factory _$$_DesignCopyWith(_$_Design value, $Res Function(_$_Design) then) =
__$$_DesignCopyWithImpl<$Res>;
@override
$Res call(
{List<DesignComponent> components,
List<DesignWire> wires,
List<DesignInput> inputs,
List<DesignOutput> 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<DesignComponent>,
wires: wires == freezed
? _value._wires
: wires // ignore: cast_nullable_to_non_nullable
as List<DesignWire>,
inputs: inputs == freezed
? _value._inputs
: inputs // ignore: cast_nullable_to_non_nullable
as List<DesignInput>,
outputs: outputs == freezed
? _value._outputs
: outputs // ignore: cast_nullable_to_non_nullable
as List<DesignOutput>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_Design implements _Design {
const _$_Design(
{required final List<DesignComponent> components,
required final List<DesignWire> wires,
required final List<DesignInput> inputs,
required final List<DesignOutput> outputs})
: _components = components,
_wires = wires,
_inputs = inputs,
_outputs = outputs;
factory _$_Design.fromJson(Map<String, dynamic> json) =>
_$$_DesignFromJson(json);
final List<DesignComponent> _components;
@override
List<DesignComponent> get components {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_components);
}
final List<DesignWire> _wires;
@override
List<DesignWire> get wires {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_wires);
}
final List<DesignInput> _inputs;
@override
List<DesignInput> get inputs {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_inputs);
}
final List<DesignOutput> _outputs;
@override
List<DesignOutput> 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<String, dynamic> toJson() {
return _$$_DesignToJson(this);
}
}
abstract class _Design implements Design {
const factory _Design(
{required final List<DesignComponent> components,
required final List<DesignWire> wires,
required final List<DesignInput> inputs,
required final List<DesignOutput> outputs}) = _$_Design;
factory _Design.fromJson(Map<String, dynamic> json) = _$_Design.fromJson;
@override
List<DesignComponent> get components => throw _privateConstructorUsedError;
@override
List<DesignWire> get wires => throw _privateConstructorUsedError;
@override
List<DesignInput> get inputs => throw _privateConstructorUsedError;
@override
List<DesignOutput> get outputs => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$$_DesignCopyWith<_$_Design> get copyWith =>
throw _privateConstructorUsedError;
}
DesignComponent _$DesignComponentFromJson(Map<String, dynamic> json) {
return _DesignComponent.fromJson(json);
}
/// @nodoc
mixin _$DesignComponent {
String get instanceId => throw _privateConstructorUsedError;
double get x => throw _privateConstructorUsedError;
double get y => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesignComponentCopyWith<DesignComponent> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return _DesignWire.fromJson(json);
}
/// @nodoc
mixin _$DesignWire {
String get wireId => throw _privateConstructorUsedError;
double get x => throw _privateConstructorUsedError;
double get y => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesignWireCopyWith<DesignWire> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return _DesignInput.fromJson(json);
}
/// @nodoc
mixin _$DesignInput {
String get name => throw _privateConstructorUsedError;
double get x => throw _privateConstructorUsedError;
double get y => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesignInputCopyWith<DesignInput> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return _DesignOutput.fromJson(json);
}
/// @nodoc
mixin _$DesignOutput {
String get name => throw _privateConstructorUsedError;
double get x => throw _privateConstructorUsedError;
double get y => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DesignOutputCopyWith<DesignOutput> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

85
lib/models/design.g.dart

@ -0,0 +1,85 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'design.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$_Design _$$_DesignFromJson(Map<String, dynamic> json) => _$_Design(
components: (json['components'] as List<dynamic>)
.map((e) => DesignComponent.fromJson(e as Map<String, dynamic>))
.toList(),
wires: (json['wires'] as List<dynamic>)
.map((e) => DesignWire.fromJson(e as Map<String, dynamic>))
.toList(),
inputs: (json['inputs'] as List<dynamic>)
.map((e) => DesignInput.fromJson(e as Map<String, dynamic>))
.toList(),
outputs: (json['outputs'] as List<dynamic>)
.map((e) => DesignOutput.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$$_DesignToJson(_$_Design instance) => <String, dynamic>{
'components': instance.components,
'wires': instance.wires,
'inputs': instance.inputs,
'outputs': instance.outputs,
};
_$_DesignComponent _$$_DesignComponentFromJson(Map<String, dynamic> json) =>
_$_DesignComponent(
instanceId: json['instanceId'] as String,
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
);
Map<String, dynamic> _$$_DesignComponentToJson(_$_DesignComponent instance) =>
<String, dynamic>{
'instanceId': instance.instanceId,
'x': instance.x,
'y': instance.y,
};
_$_DesignWire _$$_DesignWireFromJson(Map<String, dynamic> json) =>
_$_DesignWire(
wireId: json['wireId'] as String,
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
);
Map<String, dynamic> _$$_DesignWireToJson(_$_DesignWire instance) =>
<String, dynamic>{
'wireId': instance.wireId,
'x': instance.x,
'y': instance.y,
};
_$_DesignInput _$$_DesignInputFromJson(Map<String, dynamic> json) =>
_$_DesignInput(
name: json['name'] as String,
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
);
Map<String, dynamic> _$$_DesignInputToJson(_$_DesignInput instance) =>
<String, dynamic>{
'name': instance.name,
'x': instance.x,
'y': instance.y,
};
_$_DesignOutput _$$_DesignOutputFromJson(Map<String, dynamic> json) =>
_$_DesignOutput(
name: json['name'] as String,
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
);
Map<String, dynamic> _$$_DesignOutputToJson(_$_DesignOutput instance) =>
<String, dynamic>{
'name': instance.name,
'x': instance.x,
'y': instance.y,
};

392
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,50 +26,377 @@ class DesignComponentPage extends HookWidget {
@override
Widget build(BuildContext context) {
final componentState = useProvider<ComponentState>();
final canvasController = useStackCanvasController();
final widgets = useState(<CanvasObject<Widget>>[]);
final canvasController = useStackCanvasController(
offsetReference: Reference.Center,
);
// Simulation vars
final isSimulating = useState(false);
final simulatePartially = useState(false);
useListenable(componentState.partialVisualSimulation!);
final movingWidgetUpdater = useState<void Function(double dx, double dy)?>(null);
final movingWidget = useState<dynamic>(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<List<double>>(
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<double>(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<List<double>>(
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<double>(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<List<double>>(
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<double>(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,
),
),
],
);
}
}
}
),
),
);
}

117
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<String, Tuple2<ProjectEntry, ComponentEntry>> _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<SimulatedComponent> _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<Directory> _getComponentDir() async {
if (_currentProject == null) {
@ -40,6 +68,11 @@ class ComponentState extends ChangeNotifier {
return result;
}
Future<File> _getDesignFile() async {
final result = File(path.join((await _getComponentDir()).path, 'design.json'));
return result;
}
Future<void> _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<void> 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<Map<String, bool>> simulate(Map<String, bool> inputs) async {
Future<SimulatedComponent> 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<ProjectEntry, ComponentEntry> 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<Map<String, bool>> simulate(Map<String, bool> inputs) async {
_simulatedComponent ??= SimulatedComponent(
project: _currentProject!,
component: _currentComponent!,
onRequiredDependency: onRequiredDependency,
onRequiredDependency: _onRequiredDependency,
state: this,
);
return _simulatedComponent!.simulate(inputs);
}
Future<Design> 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<Wiring> 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 {

22
lib/utils/future_call_debounce.dart

@ -0,0 +1,22 @@
class FutureCallDebounce<TParams extends Object> {
TParams? _params;
Future? _awaited;
final Future Function(TParams) futureCall;
final TParams Function(TParams oldParams, TParams newParams) combiner;
static TParams _defaultCombiner<TParams>(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);
}
}

224
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<SimulatedComponent> Function(String depId) onRequiredDependency;
final _instances = <String, SimulatedComponent>{};
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<SimulatedComponent> _getInstance(String instanceId, String? depId) async {
Future<SimulatedComponent> _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');
}
}
@ -37,13 +38,11 @@ class SimulatedComponent {
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'
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.
@ -55,15 +54,10 @@ class SimulatedComponent {
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 = <String, bool>{};
@ -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,
// 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']!
};
}
}
}
class PartialVisualSimulation with ChangeNotifier {
final Map<String, bool?> _outputsValues = {};
final List<String> nextToSimulate = [];
final List<String> _alreadySimulated = [];
UnmodifiableMapView<String, bool?> get outputsValues => UnmodifiableMapView(_outputsValues);
UnmodifiableMapView<String, bool?> 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<SimulatedComponent> Function(String depId) onRequiredDependency;
final _instances = <String, SimulatedComponent>{};
PartialVisualSimulation._(
{required this.project,
required this.component,
required this.state,
required this.onRequiredDependency});
Future<SimulatedComponent> _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<PartialVisualSimulation> init({
required ProjectEntry project,
required ComponentEntry component,
required ComponentState state,
required Future<SimulatedComponent> Function(String depId) onRequiredDependency,
Map<String, bool>? 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<void> toggleInput(String inputName) {
final inputValue = _outputsValues['self/$inputName']!;
return modifyInput(inputName, !inputValue);
}
Future<void> 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<void> provideInputs(Map<String, bool> inputs) {
_alreadySimulated.clear();
_outputsValues.clear();
for (final entry in inputs.entries) {
_outputsValues['self/${entry.key}'] = entry.value;
}
return reset();
}
Future<void> reset() async {
nextToSimulate.clear();
final neededToBeNext = <String, List<String>>{};
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<void> 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();
}
}

8
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

3
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:

Loading…
Cancel
Save