Browse Source

Added script based component

master
Kenneth Bruen 2 years ago
parent
commit
fc815ed879
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 2
      lib/models/component.dart
  2. 62
      lib/models/component.freezed.dart
  3. 2
      lib/models/component.g.dart
  4. 119
      lib/pages/edit_component.dart
  5. 1
      lib/state/project.dart
  6. 58
      lib/state/script.dart
  7. 19
      lib/utils/simulation.dart

2
lib/models/component.dart

@ -20,6 +20,8 @@ class ComponentEntry with _$ComponentEntry {
required bool visualDesigned,
@JsonKey(defaultValue: [])
required List<String> dependencies,
@JsonKey(defaultValue: false)
required bool scriptBased,
}) = _ComponentEntry;
factory ComponentEntry.fromJson(Map<String, Object?> json) => _$ComponentEntryFromJson(json);

62
lib/models/component.freezed.dart

@ -34,6 +34,8 @@ mixin _$ComponentEntry {
bool get visualDesigned => throw _privateConstructorUsedError;
@JsonKey(defaultValue: [])
List<String> get dependencies => throw _privateConstructorUsedError;
@JsonKey(defaultValue: false)
bool get scriptBased => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -55,7 +57,8 @@ abstract class $ComponentEntryCopyWith<$Res> {
@JsonKey(includeIfNull: false) List<String>? truthTable,
@JsonKey(includeIfNull: false) List<String>? logicExpression,
@JsonKey(defaultValue: false) bool visualDesigned,
@JsonKey(defaultValue: []) List<String> dependencies});
@JsonKey(defaultValue: []) List<String> dependencies,
@JsonKey(defaultValue: false) bool scriptBased});
}
/// @nodoc
@ -78,6 +81,7 @@ class _$ComponentEntryCopyWithImpl<$Res>
Object? logicExpression = freezed,
Object? visualDesigned = freezed,
Object? dependencies = freezed,
Object? scriptBased = freezed,
}) {
return _then(_value.copyWith(
componentId: componentId == freezed
@ -116,6 +120,10 @@ class _$ComponentEntryCopyWithImpl<$Res>
? _value.dependencies
: dependencies // ignore: cast_nullable_to_non_nullable
as List<String>,
scriptBased: scriptBased == freezed
? _value.scriptBased
: scriptBased // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@ -136,7 +144,8 @@ abstract class _$$_ComponentEntryCopyWith<$Res>
@JsonKey(includeIfNull: false) List<String>? truthTable,
@JsonKey(includeIfNull: false) List<String>? logicExpression,
@JsonKey(defaultValue: false) bool visualDesigned,
@JsonKey(defaultValue: []) List<String> dependencies});
@JsonKey(defaultValue: []) List<String> dependencies,
@JsonKey(defaultValue: false) bool scriptBased});
}
/// @nodoc
@ -161,6 +170,7 @@ class __$$_ComponentEntryCopyWithImpl<$Res>
Object? logicExpression = freezed,
Object? visualDesigned = freezed,
Object? dependencies = freezed,
Object? scriptBased = freezed,
}) {
return _then(_$_ComponentEntry(
componentId: componentId == freezed
@ -199,6 +209,10 @@ class __$$_ComponentEntryCopyWithImpl<$Res>
? _value._dependencies
: dependencies // ignore: cast_nullable_to_non_nullable
as List<String>,
scriptBased: scriptBased == freezed
? _value.scriptBased
: scriptBased // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@ -215,7 +229,8 @@ class _$_ComponentEntry implements _ComponentEntry {
@JsonKey(includeIfNull: false) final List<String>? truthTable,
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
@JsonKey(defaultValue: false) required this.visualDesigned,
@JsonKey(defaultValue: []) required final List<String> dependencies})
@JsonKey(defaultValue: []) required final List<String> dependencies,
@JsonKey(defaultValue: false) required this.scriptBased})
: _inputs = inputs,
_outputs = outputs,
_truthTable = truthTable,
@ -277,9 +292,13 @@ class _$_ComponentEntry implements _ComponentEntry {
return EqualUnmodifiableListView(_dependencies);
}
@override
@JsonKey(defaultValue: false)
final bool scriptBased;
@override
String toString() {
return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned, dependencies: $dependencies)';
return 'ComponentEntry(componentId: $componentId, componentName: $componentName, componentDescription: $componentDescription, inputs: $inputs, outputs: $outputs, truthTable: $truthTable, logicExpression: $logicExpression, visualDesigned: $visualDesigned, dependencies: $dependencies, scriptBased: $scriptBased)';
}
@override
@ -302,7 +321,9 @@ class _$_ComponentEntry implements _ComponentEntry {
const DeepCollectionEquality()
.equals(other.visualDesigned, visualDesigned) &&
const DeepCollectionEquality()
.equals(other._dependencies, _dependencies));
.equals(other._dependencies, _dependencies) &&
const DeepCollectionEquality()
.equals(other.scriptBased, scriptBased));
}
@JsonKey(ignore: true)
@ -317,7 +338,8 @@ class _$_ComponentEntry implements _ComponentEntry {
const DeepCollectionEquality().hash(_truthTable),
const DeepCollectionEquality().hash(_logicExpression),
const DeepCollectionEquality().hash(visualDesigned),
const DeepCollectionEquality().hash(_dependencies));
const DeepCollectionEquality().hash(_dependencies),
const DeepCollectionEquality().hash(scriptBased));
@JsonKey(ignore: true)
@override
@ -332,20 +354,17 @@ class _$_ComponentEntry implements _ComponentEntry {
abstract class _ComponentEntry implements ComponentEntry {
const factory _ComponentEntry(
{required final String componentId,
required final String componentName,
@JsonKey(includeIfNull: false)
final String? componentDescription,
required final List<String> inputs,
required final List<String> outputs,
@JsonKey(includeIfNull: false)
final List<String>? truthTable,
@JsonKey(includeIfNull: false)
final List<String>? logicExpression,
@JsonKey(defaultValue: false)
required final bool visualDesigned,
@JsonKey(defaultValue: [])
required final List<String> dependencies}) = _$_ComponentEntry;
{required final String componentId,
required final String componentName,
@JsonKey(includeIfNull: false) final String? componentDescription,
required final List<String> inputs,
required final List<String> outputs,
@JsonKey(includeIfNull: false) final List<String>? truthTable,
@JsonKey(includeIfNull: false) final List<String>? logicExpression,
@JsonKey(defaultValue: false) required final bool visualDesigned,
@JsonKey(defaultValue: []) required final List<String> dependencies,
@JsonKey(defaultValue: false) required final bool scriptBased}) =
_$_ComponentEntry;
factory _ComponentEntry.fromJson(Map<String, dynamic> json) =
_$_ComponentEntry.fromJson;
@ -374,6 +393,9 @@ abstract class _ComponentEntry implements ComponentEntry {
@JsonKey(defaultValue: [])
List<String> get dependencies => throw _privateConstructorUsedError;
@override
@JsonKey(defaultValue: false)
bool get scriptBased => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$$_ComponentEntryCopyWith<_$_ComponentEntry> get copyWith =>
throw _privateConstructorUsedError;

2
lib/models/component.g.dart

@ -26,6 +26,7 @@ _$_ComponentEntry _$$_ComponentEntryFromJson(Map<String, dynamic> json) =>
?.map((e) => e as String)
.toList() ??
[],
scriptBased: json['scriptBased'] as bool? ?? false,
);
Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
@ -47,5 +48,6 @@ Map<String, dynamic> _$$_ComponentEntryToJson(_$_ComponentEntry instance) {
writeNotNull('logicExpression', instance.logicExpression);
val['visualDesigned'] = instance.visualDesigned;
val['dependencies'] = instance.dependencies;
val['scriptBased'] = instance.scriptBased;
return val;
}

119
lib/pages/edit_component.dart

@ -1,6 +1,8 @@
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:logic_circuits_simulator/components/logic_expression_field.dart';
@ -14,10 +16,13 @@ import 'package:logic_circuits_simulator/pages_arguments/edit_component.dart';
import 'package:logic_circuits_simulator/state/component.dart';
import 'package:logic_circuits_simulator/state/project.dart';
import 'package:logic_circuits_simulator/state/projects.dart';
import 'package:logic_circuits_simulator/state/script.dart';
import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
import 'package:logic_circuits_simulator/utils/logic_expressions.dart';
import 'package:logic_circuits_simulator/utils/logic_operators.dart';
import 'package:logic_circuits_simulator/utils/provider_hook.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
@ -44,6 +49,7 @@ class EditComponentPage extends HookWidget {
: List<LogicExpression?>.generate(logicExpressions.value!.length, (index) => null),
);
final visualDesigned = useState(ce().visualDesigned);
final scriptBased = useState(ce().scriptBased);
final inputs = useState(ce().inputs.toList());
final outputs = useState(ce().outputs.toList());
final componentNameEditingController = useTextEditingController(text: ce().componentName);
@ -64,7 +70,7 @@ class EditComponentPage extends HookWidget {
// Don't allow saving empty outputs
return false;
}
if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) {
if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value && !scriptBased.value) {
// Don't allow saving components without functionality
return false;
}
@ -91,6 +97,9 @@ class EditComponentPage extends HookWidget {
if (visualDesigned.value != ce().visualDesigned) {
return true;
}
if (scriptBased.value != ce().scriptBased) {
return true;
}
return false;
},
[
@ -107,6 +116,8 @@ class EditComponentPage extends HookWidget {
visualDesigned.value,
ce().visualDesigned,
logicExpressionsParsed.value,
ce().scriptBased,
scriptBased.value,
],
);
@ -413,7 +424,7 @@ class EditComponentPage extends HookWidget {
textAlign: TextAlign.center,
),
)
else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value) ...[
else if (truthTable.value == null && logicExpressions.value == null && !visualDesigned.value && !scriptBased.value) ...[
SliverToBoxAdapter(
child: Column(
children: [
@ -462,11 +473,13 @@ class EditComponentPage extends HookWidget {
child: const Text('Visual Designer'),
),
),
const Padding(
padding: EdgeInsets.all(8.0),
Padding(
padding: const EdgeInsets.all(8.0),
child: OutlinedButton(
onPressed: null,
child: Text('Script'),
onPressed: () async {
scriptBased.value = true;
},
child: const Text('Script'),
),
),
],
@ -579,7 +592,7 @@ class EditComponentPage extends HookWidget {
try {
await Provider.of<ComponentState>(context, listen: false).setCurrentComponent(
project: projectState.currentProject!,
component: component,
component: ce(),
onDependencyNeeded: (String projectId, String componentId) async {
if (projectId == 'self') {
final maybeComponent = projectState.index.components.where((c) => c.componentId == componentId).firstOrNull;
@ -622,7 +635,95 @@ class EditComponentPage extends HookWidget {
),
],
),
)
),
],
if (scriptBased.value) ...[
SliverToBoxAdapter(
child: Column(
children: [
Text(
"Script Component",
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
HookBuilder(
builder: (context) {
final stateListenable = useState(
ScriptState(
project: projectState.currentProject!,
component: ce(),
),
);
final scriptState = useListenable(stateListenable.value);
if (!scriptState.loaded) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final picked = await FilePicker.platform.pickFiles(
dialogTitle: 'Select Script',
// allowedExtensions: ['ht', 'txt'],
allowMultiple: false,
type: FileType.any,
);
if (picked == null) {
return;
}
final file = File(picked.files.first.path!);
if (!await file.exists()) {
scaffoldMessenger.showSnackBar(
const SnackBar(
content: Text('The selected file does not exist'),
),
);
return;
}
await scriptState.setScriptContents(await file.readAsString());
},
child: Text(scriptState.scriptExists ? 'Replace Script' : 'Select Script'),
),
),
),
if (scriptState.scriptContent != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
scriptState.scriptContent!,
softWrap: true,
style: TextStyle(
inherit: true,
fontFamily: 'JetBrains Mono',
fontFamilyFallback: [
'JetBrains Mono',
'Ubuntu Mono',
'Menlo',
'Cascadia Code',
'Courier New',
'Courier',
],
),
),
),
],
);
},
),
],
),
),
],
const SliverPadding(
padding: EdgeInsets.only(bottom: 56 + 16 + 16),
@ -640,9 +741,9 @@ class EditComponentPage extends HookWidget {
truthTable: truthTable.value,
logicExpression: logicExpressions.value,
visualDesigned: visualDesigned.value,
scriptBased: scriptBased.value,
));
anySave.value = true;
// TODO: Implement saving
},
tooltip: 'Save Component',
child: const Icon(Icons.save),

1
lib/state/project.dart

@ -77,6 +77,7 @@ class ProjectState extends ChangeNotifier {
outputs: [],
visualDesigned: false,
dependencies: [],
scriptBased: false,
);
await _updateIndex(index.copyWith(components: index.components + [newComponent]));
return newComponent;

58
lib/state/script.dart

@ -0,0 +1,58 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logic_circuits_simulator/models.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class ScriptState extends ChangeNotifier {
bool? _scriptExists;
String? _scriptContent;
bool get loaded => _scriptExists != null;
bool get scriptExists => _scriptExists ?? false;
String? get scriptContent => _scriptContent;
final ProjectEntry project;
final ComponentEntry component;
ScriptState({required this.project, required this.component, bool invokeInit = true}) {
if (invokeInit) {
init();
}
}
Future<File> _getScriptFile() async {
final appDir = await getApplicationDocumentsDirectory();
final componentDir = Directory(path.join(appDir.path, 'LogicCircuitsSimulator', 'projects', project.projectId, 'components', component.componentId));
if (!await componentDir.exists()) {
await componentDir.create(recursive: true);
}
return File(path.join(componentDir.path, 'script.ht'));
}
Future<void> init() async {
final scriptFile = await _getScriptFile();
_scriptExists = await scriptFile.exists();
if (scriptExists) {
_scriptContent = await scriptFile.readAsString();
}
notifyListeners();
}
Future<void> setScriptContents(String newContents) async {
final scriptFile = await _getScriptFile();
await scriptFile.writeAsString(newContents);
_scriptContent = newContents;
_scriptExists = true;
notifyListeners();
}
Future<void> deleteScript() async {
final scriptFile = await _getScriptFile();
await scriptFile.delete();
_scriptContent = null;
_scriptExists = false;
notifyListeners();
}
}

19
lib/utils/simulation.dart

@ -1,7 +1,9 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hetu_script/hetu_script.dart';
import 'package:logic_circuits_simulator/models.dart';
import 'package:logic_circuits_simulator/state/component.dart';
import 'package:logic_circuits_simulator/state/script.dart';
import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
import 'package:logic_circuits_simulator/utils/logic_expressions.dart';
@ -55,6 +57,23 @@ class SimulatedComponent {
},
);
return {for (final it in results) it[0] as String: it[1] as bool};
} else if (component.scriptBased) {
final state = ScriptState(
component: component,
project: project,
invokeInit: false,
);
await state.init();
if (!state.scriptExists) {
throw Exception('Script for component ${project.projectId}/${component.componentId} does not exist');
}
final hetu = Hetu();
hetu.init();
final result = hetu.eval(state.scriptContent!, invokeFunc: 'simulate', positionalArgs: [inputs]);
return {
for (final output in component.outputs)
output: result[output]
};
} else if (state == null) {
throw Exception('Cannot simulate designed component without its state');
} else {

Loading…
Cancel
Save