mirror of
https://github.com/dancojocaru2000/logic-circuits-simulator.git
synced 2025-02-22 09:09:35 +02:00
Added connecting wires
This commit is contained in:
parent
76c911e697
commit
a86a5e6aec
2 changed files with 220 additions and 33 deletions
|
@ -11,10 +11,28 @@ class VisualComponent extends HookWidget {
|
||||||
final Map<String, Color?> inputColors;
|
final Map<String, Color?> inputColors;
|
||||||
final Map<String, Color?> outputColors;
|
final Map<String, Color?> outputColors;
|
||||||
final bool isNextToSimulate;
|
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 ?? {}
|
: inputColors = inputColors ?? {}
|
||||||
, outputColors = outputColors ?? {};
|
, outputColors = outputColors ?? {}
|
||||||
|
, assert((onInputHovered == null) == (onInputUnhovered == null))
|
||||||
|
, assert((onOutputHovered == null) == (onOutputUnhovered == null));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -40,12 +58,20 @@ class VisualComponent extends HookWidget {
|
||||||
|
|
||||||
final hovered = useState(false);
|
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 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));
|
final outputsWidth = outputs.map((output) => IOLabel.getNeededWidth(context, output)).fold<double>(0, (previousValue, element) => max(previousValue, element));
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (event) => hovered.value = true,
|
onEnter: onInputHovered == null && onOutputHovered == null ? (event) => hovered.value = true : null,
|
||||||
onExit: (event) => hovered.value = false,
|
onExit: onInputUnhovered == null && onOutputUnhovered== null ? (event) => hovered.value = false : null,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
|
@ -59,6 +85,9 @@ class VisualComponent extends HookWidget {
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: inputColors[input] ?? Colors.black,
|
: inputColors[input] ?? Colors.black,
|
||||||
width: inputsWidth,
|
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
|
? Theme.of(context).colorScheme.primary
|
||||||
: outputColors[output] ?? Colors.black,
|
: outputColors[output] ?? Colors.black,
|
||||||
width: outputsWidth,
|
width: outputsWidth,
|
||||||
|
onHovered: onOutputHovered == null ? null : () => onOutputHovered!(output),
|
||||||
|
onUnhovered: onOutputUnhovered == null ? null : () => onOutputUnhovered!(output),
|
||||||
|
flashing: onOutputHovered != null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -143,16 +175,58 @@ class IOComponent extends HookWidget {
|
||||||
final double circleDiameter;
|
final double circleDiameter;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final void Function()? onTap;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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);
|
final hovered = useState(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (onHovered != null) {
|
||||||
|
hovered.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [onHovered]);
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (event) => hovered.value = true,
|
onEnter: (event) => onHovered != null ? onHovered!() : hovered.value = true,
|
||||||
onExit: (event) => hovered.value = false,
|
onExit: (event) => onUnhovered != null ? onUnhovered!() : hovered.value = false,
|
||||||
hitTestBehavior: HitTestBehavior.translucent,
|
hitTestBehavior: HitTestBehavior.translucent,
|
||||||
opaque: false,
|
opaque: false,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
@ -161,6 +235,11 @@ class IOComponent extends HookWidget {
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final lineColor = hovered.value ? Theme.of(context).colorScheme.primary : color ?? Colors.black;
|
final lineColor = hovered.value ? Theme.of(context).colorScheme.primary : color ?? Colors.black;
|
||||||
|
final animLineColor = Color.lerp(
|
||||||
|
lineColor,
|
||||||
|
Colors.blue,
|
||||||
|
flashingAnimProgress,
|
||||||
|
)!;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
@ -169,7 +248,7 @@ class IOComponent extends HookWidget {
|
||||||
width: circleDiameter,
|
width: circleDiameter,
|
||||||
height: circleDiameter,
|
height: circleDiameter,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: lineColor),
|
border: Border.all(color: animLineColor),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
|
@ -179,7 +258,7 @@ class IOComponent extends HookWidget {
|
||||||
child: IOLabel(
|
child: IOLabel(
|
||||||
label: name,
|
label: name,
|
||||||
input: !input,
|
input: !input,
|
||||||
lineColor: lineColor,
|
lineColor: animLineColor,
|
||||||
width: width - circleDiameter,
|
width: width - circleDiameter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -187,7 +266,7 @@ class IOComponent extends HookWidget {
|
||||||
width: circleDiameter,
|
width: circleDiameter,
|
||||||
height: circleDiameter,
|
height: circleDiameter,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: lineColor),
|
border: Border.all(color: animLineColor),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
|
@ -205,35 +284,78 @@ class IOComponent extends HookWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IOLabel extends StatelessWidget {
|
class IOLabel extends HookWidget {
|
||||||
final bool input;
|
final bool input;
|
||||||
final String label;
|
final String label;
|
||||||
final Color lineColor;
|
final Color lineColor;
|
||||||
final double width;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
final flashingAnimation = useAnimationController(
|
||||||
width: width,
|
duration: const Duration(milliseconds: 500),
|
||||||
height: 20,
|
initialValue: 0.0,
|
||||||
decoration: BoxDecoration(
|
lowerBound: 0.0,
|
||||||
border: Border(
|
upperBound: 1.0,
|
||||||
bottom: BorderSide(color: lineColor),
|
);
|
||||||
|
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(
|
||||||
child: Align(
|
alignment: input ? Alignment.bottomRight : Alignment.bottomLeft,
|
||||||
alignment: input ? Alignment.bottomRight : Alignment.bottomLeft,
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
child: Text(
|
||||||
child: Text(
|
label,
|
||||||
label,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
inherit: true,
|
||||||
inherit: true,
|
fontFeatures: [
|
||||||
fontFeatures: [
|
FontFeature.tabularFigures(),
|
||||||
FontFeature.tabularFigures(),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'package:logic_circuits_simulator/utils/iterable_extension.dart';
|
||||||
import 'package:logic_circuits_simulator/utils/provider_hook.dart';
|
import 'package:logic_circuits_simulator/utils/provider_hook.dart';
|
||||||
import 'package:logic_circuits_simulator/utils/stack_canvas_controller_hook.dart';
|
import 'package:logic_circuits_simulator/utils/stack_canvas_controller_hook.dart';
|
||||||
import 'package:stack_canvas/stack_canvas.dart';
|
import 'package:stack_canvas/stack_canvas.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
Key canvasKey = GlobalKey();
|
Key canvasKey = GlobalKey();
|
||||||
Key pickerKey = GlobalKey();
|
Key pickerKey = GlobalKey();
|
||||||
|
@ -44,6 +45,8 @@ class DesignComponentPage extends HookWidget {
|
||||||
final deleteOnDrop = useState<bool>(false);
|
final deleteOnDrop = useState<bool>(false);
|
||||||
final designSelection = useState<String?>(null);
|
final designSelection = useState<String?>(null);
|
||||||
final wireToDelete = useState<String?>(null);
|
final wireToDelete = useState<String?>(null);
|
||||||
|
final sourceToConnect = useState<String?>(null);
|
||||||
|
final hoveredIO = useState<String?>(null);
|
||||||
|
|
||||||
final widgets = useMemoized(() => [
|
final widgets = useMemoized(() => [
|
||||||
for (final subcomponent in componentState.designDraft.components)
|
for (final subcomponent in componentState.designDraft.components)
|
||||||
|
@ -117,6 +120,18 @@ class DesignComponentPage extends HookWidget {
|
||||||
: Colors.black,
|
: Colors.black,
|
||||||
} : null,
|
} : null,
|
||||||
isNextToSimulate: isSimulating.value && componentState.partialVisualSimulation!.nextToSimulate.contains(subcomponent.instanceId),
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -169,6 +184,13 @@ class DesignComponentPage extends HookWidget {
|
||||||
onTap: isSimulating.value ? () {
|
onTap: isSimulating.value ? () {
|
||||||
componentState.partialVisualSimulation!.toggleInput(input.name);
|
componentState.partialVisualSimulation!.toggleInput(input.name);
|
||||||
} : null,
|
} : 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -217,6 +239,13 @@ class DesignComponentPage extends HookWidget {
|
||||||
: componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == false ? Colors.red
|
: componentState.partialVisualSimulation!.inputsValues['self/${output.name}'] == false ? Colors.red
|
||||||
: null)
|
: null)
|
||||||
: 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -362,7 +391,7 @@ class DesignComponentPage extends HookWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
], [componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues]);
|
], [componentState.designDraft, isSimulating.value, componentState.partialVisualSimulation!.outputsValues, designSelection.value, sourceToConnect.value]);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final wList = widgets;
|
final wList = widgets;
|
||||||
canvasController.addCanvasObjects(wList);
|
canvasController.addCanvasObjects(wList);
|
||||||
|
@ -409,6 +438,41 @@ class DesignComponentPage extends HookWidget {
|
||||||
hw(update.delta.dx, update.delta.dy);
|
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(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
StackCanvas(
|
StackCanvas(
|
||||||
|
@ -463,6 +527,7 @@ class DesignComponentPage extends HookWidget {
|
||||||
designSelection.value = selection;
|
designSelection.value = selection;
|
||||||
if (selection != 'wiring') {
|
if (selection != 'wiring') {
|
||||||
wireToDelete.value = null;
|
wireToDelete.value = null;
|
||||||
|
sourceToConnect.value = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -716,10 +781,10 @@ class ComponentPicker extends HookWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue