Browse Source

Added connecting wires

master
Kenneth Bruen 2 years ago
parent
commit
a86a5e6aec
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 182
      lib/components/visual_component.dart
  2. 71
      lib/pages/design_component.dart

182
lib/components/visual_component.dart

@ -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,16 +175,58 @@ 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(
@ -161,6 +235,11 @@ class IOComponent extends HookWidget {
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,
@ -169,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,
),
@ -179,7 +258,7 @@ class IOComponent extends HookWidget {
child: IOLabel(
label: name,
input: !input,
lineColor: lineColor,
lineColor: animLineColor,
width: width - circleDiameter,
),
),
@ -187,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,
),
@ -205,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(),
],
),
),
),
),

71
lib/pages/design_component.dart

@ -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/stack_canvas_controller_hook.dart';
import 'package:stack_canvas/stack_canvas.dart';
import 'package:uuid/uuid.dart';
Key canvasKey = GlobalKey();
Key pickerKey = GlobalKey();
@ -44,6 +45,8 @@ class DesignComponentPage extends HookWidget {
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)
@ -117,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,
),
),
),
@ -169,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,
),
),
),
@ -217,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,
),
),
),
@ -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(() {
final wList = widgets;
canvasController.addCanvasObjects(wList);
@ -409,6 +438,41 @@ class DesignComponentPage extends HookWidget {
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(
@ -463,6 +527,7 @@ class DesignComponentPage extends HookWidget {
designSelection.value = selection;
if (selection != 'wiring') {
wireToDelete.value = null;
sourceToConnect.value = null;
}
},
);
@ -716,10 +781,10 @@ class ComponentPicker extends HookWidget {
],
);
}
)
),
],
),
)
),
],
),
);

Loading…
Cancel
Save