mirror of
https://github.com/dancojocaru2000/logic-circuits-simulator.git
synced 2025-06-19 10:32:28 +03:00
Compare commits
3 commits
8abd6b3ca8
...
a86a5e6aec
Author | SHA1 | Date | |
---|---|---|---|
a86a5e6aec | |||
76c911e697 | |||
e74c2f58ed |
4 changed files with 567 additions and 114 deletions
|
@ -11,10 +11,28 @@ class VisualComponent extends HookWidget {
|
|||
final Map<String, Color?> inputColors;
|
||||
final Map<String, Color?> outputColors;
|
||||
final bool isNextToSimulate;
|
||||
final void Function(String)? onInputHovered;
|
||||
final void Function(String)? onInputUnhovered;
|
||||
final void Function(String)? onOutputHovered;
|
||||
final void Function(String)? onOutputUnhovered;
|
||||
|
||||
VisualComponent({super.key, required this.name, required this.inputs, required this.outputs, Map<String, Color?>? inputColors, Map<String, Color?>? outputColors, this.isNextToSimulate = false})
|
||||
VisualComponent({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.inputs,
|
||||
required this.outputs,
|
||||
Map<String, Color?>? inputColors,
|
||||
Map<String, Color?>? outputColors,
|
||||
this.isNextToSimulate = false,
|
||||
this.onInputHovered,
|
||||
this.onInputUnhovered,
|
||||
this.onOutputHovered,
|
||||
this.onOutputUnhovered,
|
||||
})
|
||||
: inputColors = inputColors ?? {}
|
||||
, outputColors = outputColors ?? {};
|
||||
, outputColors = outputColors ?? {}
|
||||
, assert((onInputHovered == null) == (onInputUnhovered == null))
|
||||
, assert((onOutputHovered == null) == (onOutputUnhovered == null));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -40,12 +58,20 @@ class VisualComponent extends HookWidget {
|
|||
|
||||
final hovered = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
if (onInputHovered != null || onOutputHovered != null) {
|
||||
hovered.value = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [onInputHovered, onOutputHovered]);
|
||||
|
||||
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,
|
||||
onEnter: onInputHovered == null && onOutputHovered == null ? (event) => hovered.value = true : null,
|
||||
onExit: onInputUnhovered == null && onOutputUnhovered== null ? (event) => hovered.value = false : null,
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
|
@ -59,6 +85,9 @@ class VisualComponent extends HookWidget {
|
|||
? Theme.of(context).colorScheme.primary
|
||||
: inputColors[input] ?? Colors.black,
|
||||
width: inputsWidth,
|
||||
onHovered: onInputHovered == null ? null : () => onInputHovered!(input),
|
||||
onUnhovered: onInputUnhovered == null ? null : () => onInputUnhovered!(input),
|
||||
flashing: onInputHovered != null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -92,6 +121,9 @@ class VisualComponent extends HookWidget {
|
|||
? Theme.of(context).colorScheme.primary
|
||||
: outputColors[output] ?? Colors.black,
|
||||
width: outputsWidth,
|
||||
onHovered: onOutputHovered == null ? null : () => onOutputHovered!(output),
|
||||
onUnhovered: onOutputUnhovered == null ? null : () => onOutputUnhovered!(output),
|
||||
flashing: onOutputHovered != null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -143,22 +175,71 @@ class IOComponent extends HookWidget {
|
|||
final double circleDiameter;
|
||||
final Color? color;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHovered;
|
||||
final void Function()? onUnhovered;
|
||||
final bool flashing;
|
||||
|
||||
const IOComponent({super.key, required this.name, required this.input, this.width = 100, this.circleDiameter = 20, this.color, this.onTap});
|
||||
const IOComponent({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.input,
|
||||
this.width = 100,
|
||||
this.circleDiameter = 20,
|
||||
this.color,
|
||||
this.onTap,
|
||||
this.onHovered,
|
||||
this.onUnhovered,
|
||||
this.flashing = false,
|
||||
}) : assert((onHovered == null) == (onUnhovered == null));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flashingAnimation = useAnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
initialValue: 0.0,
|
||||
lowerBound: 0.0,
|
||||
upperBound: 1.0,
|
||||
);
|
||||
useEffect(() {
|
||||
if (flashing) {
|
||||
flashingAnimation.repeat(
|
||||
reverse: true,
|
||||
);
|
||||
}
|
||||
else {
|
||||
flashingAnimation.reset();
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [flashing]);
|
||||
final flashingAnimProgress = useAnimation(flashingAnimation);
|
||||
|
||||
final hovered = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
if (onHovered != null) {
|
||||
hovered.value = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [onHovered]);
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (event) => hovered.value = true,
|
||||
onExit: (event) => hovered.value = false,
|
||||
onEnter: (event) => onHovered != null ? onHovered!() : hovered.value = true,
|
||||
onExit: (event) => onUnhovered != null ? onUnhovered!() : hovered.value = false,
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
opaque: false,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final lineColor = hovered.value ? Theme.of(context).colorScheme.primary : color ?? Colors.black;
|
||||
final animLineColor = Color.lerp(
|
||||
lineColor,
|
||||
Colors.blue,
|
||||
flashingAnimProgress,
|
||||
)!;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
@ -167,7 +248,7 @@ class IOComponent extends HookWidget {
|
|||
width: circleDiameter,
|
||||
height: circleDiameter,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: lineColor),
|
||||
border: Border.all(color: animLineColor),
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
|
@ -177,7 +258,7 @@ class IOComponent extends HookWidget {
|
|||
child: IOLabel(
|
||||
label: name,
|
||||
input: !input,
|
||||
lineColor: lineColor,
|
||||
lineColor: animLineColor,
|
||||
width: width - circleDiameter,
|
||||
),
|
||||
),
|
||||
|
@ -185,7 +266,7 @@ class IOComponent extends HookWidget {
|
|||
width: circleDiameter,
|
||||
height: circleDiameter,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: lineColor),
|
||||
border: Border.all(color: animLineColor),
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
|
@ -203,35 +284,78 @@ class IOComponent extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class IOLabel extends StatelessWidget {
|
||||
class IOLabel extends HookWidget {
|
||||
final bool input;
|
||||
final String label;
|
||||
final Color lineColor;
|
||||
final double width;
|
||||
final void Function()? onHovered;
|
||||
final void Function()? onUnhovered;
|
||||
final bool flashing;
|
||||
|
||||
const IOLabel({super.key, required this.lineColor, required this.label, required this.input, this.width = 80});
|
||||
const IOLabel({super.key, required this.lineColor, required this.label, required this.input, this.width = 80, this.onHovered, this.onUnhovered, this.flashing = false,})
|
||||
: assert((onHovered == null) == (onUnhovered == null));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: lineColor),
|
||||
final flashingAnimation = useAnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
initialValue: 0.0,
|
||||
lowerBound: 0.0,
|
||||
upperBound: 1.0,
|
||||
);
|
||||
useEffect(() {
|
||||
if (flashing) {
|
||||
flashingAnimation.repeat(
|
||||
reverse: true,
|
||||
);
|
||||
}
|
||||
else {
|
||||
flashingAnimation.reset();
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [flashing]);
|
||||
final flashingAnimProgress = useAnimation(flashingAnimation);
|
||||
|
||||
final hovered = useState(false);
|
||||
|
||||
return MouseRegion(
|
||||
hitTestBehavior: onHovered != null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild,
|
||||
onEnter: onHovered == null ? null : (_) {
|
||||
hovered.value = true;
|
||||
onHovered!();
|
||||
},
|
||||
onExit: onUnhovered == null ? null : (_) {
|
||||
hovered.value = false;
|
||||
onUnhovered!();
|
||||
},
|
||||
child: Container(
|
||||
width: width,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Color.lerp(
|
||||
lineColor,
|
||||
Colors.blue,
|
||||
flashingAnimProgress,
|
||||
)!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
],
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -6,13 +6,17 @@ 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/state/project.dart';
|
||||
import 'package:logic_circuits_simulator/state/projects.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';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
Key canvasKey = GlobalKey();
|
||||
Key pickerKey = GlobalKey();
|
||||
|
||||
class DesignComponentPage extends HookWidget {
|
||||
final ComponentEntry component;
|
||||
|
@ -38,6 +42,12 @@ class DesignComponentPage extends HookWidget {
|
|||
|
||||
final movingWidgetUpdater = useState<void Function(double dx, double dy)?>(null);
|
||||
final movingWidget = useState<dynamic>(null);
|
||||
final deleteOnDrop = useState<bool>(false);
|
||||
final designSelection = useState<String?>(null);
|
||||
final wireToDelete = useState<String?>(null);
|
||||
final sourceToConnect = useState<String?>(null);
|
||||
final hoveredIO = useState<String?>(null);
|
||||
|
||||
final widgets = useMemoized(() => [
|
||||
for (final subcomponent in componentState.designDraft.components)
|
||||
CanvasObject(
|
||||
|
@ -90,6 +100,8 @@ class DesignComponentPage extends HookWidget {
|
|||
movingWidget.value = null;
|
||||
},
|
||||
child: MouseRegion(
|
||||
opaque: false,
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
cursor: movingWidget.value == subcomponent ? SystemMouseCursors.move : MouseCursor.defer,
|
||||
child: VisualComponent(
|
||||
name: componentState.getMetaByInstance(subcomponent.instanceId).item2.componentName,
|
||||
|
@ -108,6 +120,18 @@ class DesignComponentPage extends HookWidget {
|
|||
: Colors.black,
|
||||
} : null,
|
||||
isNextToSimulate: isSimulating.value && componentState.partialVisualSimulation!.nextToSimulate.contains(subcomponent.instanceId),
|
||||
onInputHovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? (input) {
|
||||
hoveredIO.value = '${subcomponent.instanceId}/$input';
|
||||
} : null,
|
||||
onInputUnhovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? (input) {
|
||||
hoveredIO.value = null;
|
||||
} : null,
|
||||
onOutputHovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? (output) {
|
||||
hoveredIO.value = '${subcomponent.instanceId}/$output';
|
||||
} : null,
|
||||
onOutputUnhovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? (output) {
|
||||
hoveredIO.value = null;
|
||||
} : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -141,10 +165,12 @@ class DesignComponentPage extends HookWidget {
|
|||
movingWidgetUpdater.value = (dx, dy) {
|
||||
debouncer.call([dx, dy]);
|
||||
};
|
||||
movingWidget.value = input;
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
componentState.updateDesign(componentState.designDraft);
|
||||
movingWidgetUpdater.value = null;
|
||||
movingWidget.value = null;
|
||||
},
|
||||
child: IOComponent(
|
||||
input: true,
|
||||
|
@ -158,6 +184,13 @@ class DesignComponentPage extends HookWidget {
|
|||
onTap: isSimulating.value ? () {
|
||||
componentState.partialVisualSimulation!.toggleInput(input.name);
|
||||
} : null,
|
||||
onHovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? () {
|
||||
hoveredIO.value = 'self/${input.name}';
|
||||
} : null,
|
||||
onUnhovered: designSelection.value == 'wiring' && sourceToConnect.value == null ? () {
|
||||
hoveredIO.value = null;
|
||||
} : null,
|
||||
flashing: designSelection.value == 'wiring' && sourceToConnect.value == null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -190,10 +223,12 @@ class DesignComponentPage extends HookWidget {
|
|||
movingWidgetUpdater.value = (dx, dy) {
|
||||
debouncer.call([dx, dy]);
|
||||
};
|
||||
movingWidget.value = output;
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
componentState.updateDesign(componentState.designDraft);
|
||||
movingWidgetUpdater.value = null;
|
||||
movingWidget.value = null;
|
||||
},
|
||||
child: IOComponent(
|
||||
input: false,
|
||||
|
@ -204,6 +239,13 @@ class DesignComponentPage extends HookWidget {
|
|||
: componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == false ? Colors.red
|
||||
: null)
|
||||
: null,
|
||||
onHovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? () {
|
||||
hoveredIO.value = 'self/${output.name}';
|
||||
} : null,
|
||||
onUnhovered: designSelection.value == 'wiring' && sourceToConnect.value != null ? () {
|
||||
hoveredIO.value = null;
|
||||
} : null,
|
||||
flashing: designSelection.value == 'wiring' && sourceToConnect.value != null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -310,16 +352,46 @@ class DesignComponentPage extends HookWidget {
|
|||
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,
|
||||
child: MouseRegion(
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
opaque: false,
|
||||
onEnter: (_) {
|
||||
if (designSelection.value == 'wiring') {
|
||||
wireToDelete.value = wire.wireId;
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
wireToDelete.value = null;
|
||||
},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
if (designSelection.value == 'wiring') {
|
||||
if (wireToDelete.value != wire.wireId) {
|
||||
wireToDelete.value = wire.wireId;
|
||||
}
|
||||
else {
|
||||
// Delete the wire
|
||||
await componentState.updateDesign(componentState.designDraft.copyWith(
|
||||
wires: componentState.designDraft.wires.where((w) => w.wireId != wireToDelete.value).toList(),
|
||||
));
|
||||
await componentState.updateWiring(componentState.wiringDraft.copyWith(
|
||||
wires: componentState.wiringDraft.wires.where((w) => w.wireId != wireToDelete.value).toList(),
|
||||
));
|
||||
wireToDelete.value = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: WireWidget(
|
||||
from: from,
|
||||
to: to,
|
||||
color: wireToDelete.value == wire.wireId ? Colors.red : wireColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})(),
|
||||
], [componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues]);
|
||||
], [componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues, designSelection.value, sourceToConnect.value]);
|
||||
useEffect(() {
|
||||
final wList = widgets;
|
||||
canvasController.addCanvasObjects(wList);
|
||||
|
@ -344,95 +416,173 @@ class DesignComponentPage extends HookWidget {
|
|||
title: Text('${isSimulating.value ? 'Simulation' : 'Design'} - ${component.componentName}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(isSimulating.value ? Icons.stop : Icons.start),
|
||||
icon: Icon(isSimulating.value ? Icons.stop : Icons.play_arrow),
|
||||
tooltip: isSimulating.value ? 'Stop Simulation' : 'Start Simulation',
|
||||
onPressed: () {
|
||||
isSimulating.value = !isSimulating.value;
|
||||
designSelection.value = null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
onPanUpdate: (update) {
|
||||
final hw = movingWidgetUpdater.value;
|
||||
if (hw == null || isSimulating.value) {
|
||||
canvasController.offset = canvasController.offset.translate(update.delta.dx, update.delta.dy);
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
final stackCanvas = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPanUpdate: (update) {
|
||||
final hw = movingWidgetUpdater.value;
|
||||
if (hw == null || isSimulating.value) {
|
||||
canvasController.offset = canvasController.offset.translate(update.delta.dx, update.delta.dy);
|
||||
}
|
||||
else {
|
||||
hw(update.delta.dx, update.delta.dy);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (designSelection.value == 'wiring') {
|
||||
// Handle wire creation
|
||||
if (hoveredIO.value == null) {
|
||||
// If clicking on something not hovered, ignore
|
||||
return;
|
||||
}
|
||||
else if (sourceToConnect.value == null) {
|
||||
sourceToConnect.value = hoveredIO.value;
|
||||
hoveredIO.value = null;
|
||||
} else {
|
||||
// Create wire only if sink is not already connected
|
||||
if (componentState.wiringDraft.wires.where((w) => w.input == hoveredIO.value).isNotEmpty) {
|
||||
// Sink already connected
|
||||
final sinkType = hoveredIO.value!.startsWith('self/') ? 'component output' : 'subcomponent input';
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Wire already connected to that $sinkType.'),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
componentState.updateWiring(componentState.wiringDraft.copyWith(
|
||||
wires: componentState.wiringDraft.wires + [
|
||||
WiringWire(
|
||||
wireId: const Uuid().v4(),
|
||||
output: sourceToConnect.value!,
|
||||
input: hoveredIO.value!,
|
||||
),
|
||||
],
|
||||
));
|
||||
sourceToConnect.value = null;
|
||||
hoveredIO.value = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
StackCanvas(
|
||||
key: canvasKey,
|
||||
canvasController: canvasController,
|
||||
animationDuration: const Duration(milliseconds: 50),
|
||||
// disposeController: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
if (!isSimulating.value)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: MouseRegion(
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
opaque: false,
|
||||
onEnter: (_) {
|
||||
deleteOnDrop.value = true;
|
||||
},
|
||||
onExit: (_) {
|
||||
deleteOnDrop.value = false;
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
color: movingWidget.value != null && deleteOnDrop.value ? Colors.red : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final debuggingButtons = DebuggingButtons(
|
||||
partialSimulation: simulatePartially.value,
|
||||
onPartialSimulationToggle: () {
|
||||
simulatePartially.value = !simulatePartially.value;
|
||||
},
|
||||
onReset: simulatePartially.value ? () {
|
||||
componentState.partialVisualSimulation!.restart();
|
||||
} : null,
|
||||
onNextStep: simulatePartially.value && componentState.partialVisualSimulation!.nextToSimulate.isNotEmpty ? () {
|
||||
componentState.partialVisualSimulation!.nextStep();
|
||||
} : null,
|
||||
);
|
||||
|
||||
final componentPicker = ComponentPicker(
|
||||
key: pickerKey,
|
||||
onSeletionUpdate: (selection) {
|
||||
designSelection.value = selection;
|
||||
if (selection != 'wiring') {
|
||||
wireToDelete.value = null;
|
||||
sourceToConnect.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (orientation == Orientation.portrait) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
stackCanvas,
|
||||
if (isSimulating.value)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: debuggingButtons,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isSimulating.value)
|
||||
componentPicker,
|
||||
],
|
||||
);
|
||||
}
|
||||
else {
|
||||
hw(update.delta.dx, update.delta.dy);
|
||||
}
|
||||
},
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
final stackCanvas = StackCanvas(
|
||||
key: canvasKey,
|
||||
canvasController: canvasController,
|
||||
animationDuration: const Duration(milliseconds: 50),
|
||||
// disposeController: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
);
|
||||
|
||||
final debuggingButtons = DebuggingButtons(
|
||||
partialSimulation: simulatePartially.value,
|
||||
onPartialSimulationToggle: () {
|
||||
simulatePartially.value = !simulatePartially.value;
|
||||
},
|
||||
onReset: simulatePartially.value ? () {
|
||||
componentState.partialVisualSimulation!.restart();
|
||||
} : null,
|
||||
onNextStep: simulatePartially.value && componentState.partialVisualSimulation!.nextToSimulate.isNotEmpty ? () {
|
||||
componentState.partialVisualSimulation!.nextStep();
|
||||
} : null,
|
||||
);
|
||||
|
||||
if (orientation == Orientation.portrait) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
stackCanvas,
|
||||
if (isSimulating.value)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: debuggingButtons,
|
||||
),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
stackCanvas,
|
||||
if (isSimulating.value)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: debuggingButtons,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
else {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
stackCanvas,
|
||||
if (isSimulating.value)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: debuggingButtons,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
if (!isSimulating.value)
|
||||
componentPicker,
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -477,4 +627,168 @@ class DebuggingButtons extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentPicker extends HookWidget {
|
||||
const ComponentPicker({required this.onSeletionUpdate, super.key});
|
||||
|
||||
final void Function(String? selection) onSeletionUpdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final projectsState = useProvider<ProjectsState>();
|
||||
final tickerProvider = useSingleTickerProvider();
|
||||
final selection = useState<String?>(null);
|
||||
final tabBarControllerState = useState<TabController?>(null );
|
||||
useEffect(() {
|
||||
selection.addListener(() {
|
||||
onSeletionUpdate(selection.value);
|
||||
});
|
||||
|
||||
tabBarControllerState.value = TabController(
|
||||
length: 1 + projectsState.projects.length,
|
||||
vsync: tickerProvider,
|
||||
initialIndex: 1,
|
||||
);
|
||||
|
||||
tabBarControllerState.value!.addListener(() {
|
||||
if (tabBarControllerState.value!.index == 0) {
|
||||
selection.value = 'wiring';
|
||||
}
|
||||
else {
|
||||
selection.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
return () {
|
||||
tabBarControllerState.value?.dispose();
|
||||
};
|
||||
}, []);
|
||||
final tabBarController = tabBarControllerState.value!;
|
||||
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return SizedBox(
|
||||
height: orientation == Orientation.portrait ? 200 : null,
|
||||
width: orientation == Orientation.landscape ? 300 : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: tabBarController,
|
||||
tabs: [
|
||||
const Tab(
|
||||
text: 'Wiring',
|
||||
),
|
||||
for (final project in projectsState.projects)
|
||||
Tab(
|
||||
text: project.projectName,
|
||||
),
|
||||
],
|
||||
isScrollable: true,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabBarController,
|
||||
children: [
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'To create wires, click a source and then click a sink to link them.',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'To remove wires, click them or tap them twice.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
for (final project in projectsState.projects)
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final projectState = useFuture(() async {
|
||||
final projectState = ProjectState();
|
||||
await projectState.setCurrentProject(project);
|
||||
return projectState;
|
||||
}());
|
||||
|
||||
if (projectState.data == null) {
|
||||
return Container();
|
||||
}
|
||||
final components = projectState.data!.index.components;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text('To add a component, select it below and then click on the canvas to place it.'),
|
||||
),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
scrollbarOrientation: orientation == Orientation.portrait ? ScrollbarOrientation.bottom : ScrollbarOrientation.right,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: orientation == Orientation.portrait ? Axis.horizontal : Axis.vertical,
|
||||
child: Wrap(
|
||||
direction: orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final component in components)
|
||||
IntrinsicWidth(
|
||||
child: Card(
|
||||
color: selection.value == '${project.projectId}/${component.componentId}' ? Theme.of(context).colorScheme.primaryContainer : null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (selection.value != '${project.projectId}/${component.componentId}') {
|
||||
selection.value = '${project.projectId}/${component.componentId}';
|
||||
}
|
||||
else {
|
||||
selection.value = null;
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
component.componentName,
|
||||
style: selection.value == '${project.projectId}/${component.componentId}'
|
||||
? TextStyle(
|
||||
inherit: true,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -275,6 +275,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
hetu_script:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hetu_script
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.12"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -492,6 +499,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -41,6 +41,7 @@ dependencies:
|
|||
stack_canvas:
|
||||
git: https://github.com/dancojocaru2000/stack_canvas.git
|
||||
tuple: ^2.0.0
|
||||
hetu_script: ^0.3.12
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Add table
Reference in a new issue