diff --git a/README.md b/README.md index 3839248..f88feca 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ If you have pip (Python 3.5+) you can simply type `$> pip install persimmon` +To execute use. + +`$> python -m persimmon` + For windows self-contained executables can be found on the [releases page]. diff --git a/docs/images/final_aspect.png b/docs/images/final_aspect.png index 574de3c..d98a4c6 100644 Binary files a/docs/images/final_aspect.png and b/docs/images/final_aspect.png differ diff --git a/docs/src/introduction.md b/docs/src/introduction.md index 276cc45..5f140d7 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -5,7 +5,7 @@ On this chapter Persimmon is introduced, along its main objectives and motivations. It also includes a section about topics that are related but beyond the scope of this project. -Finally, it includes an overviewof the project report structure. +Finally, it includes an overview of the project report structure. Description diff --git a/docs/template.tex b/docs/template.tex index 55fbe56..92c4ce8 100644 --- a/docs/template.tex +++ b/docs/template.tex @@ -216,11 +216,10 @@ $endif$ % Make sections great again -\usepackage{titlesec} - -\titleformat*{\section}{\sffamily\LARGE\bfseries} -\titleformat*{\subsection}{\sffamily\Large\bfseries} -\titleformat*{\subsubsection}{\sffamily\large\bfseries} +%\usepackage{titlesec} +%\titleformat*{\section}{\sffamily\LARGE\bfseries} +%\titleformat*{\subsection}{\sffamily\Large\bfseries} +%\titleformat*{\subsubsection}{\sffamily\large\bfseries} % Better abstract \renewenvironment{abstract}{ diff --git a/persimmon/background.png b/persimmon/background.png new file mode 100644 index 0000000..38031b0 Binary files /dev/null and b/persimmon/background.png differ diff --git a/persimmon/border.png b/persimmon/border.png index c0a04b6..b48d6c1 100644 Binary files a/persimmon/border.png and b/persimmon/border.png differ diff --git a/persimmon/circuit_board.png b/persimmon/circuit_board.png deleted file mode 100644 index 4cea12c..0000000 Binary files a/persimmon/circuit_board.png and /dev/null differ diff --git a/persimmon/connections.png b/persimmon/connections.png deleted file mode 100644 index ac8b920..0000000 Binary files a/persimmon/connections.png and /dev/null differ diff --git a/persimmon/view/blocks/block.kv b/persimmon/view/blocks/block.kv index d860d28..eecdcd2 100644 --- a/persimmon/view/blocks/block.kv +++ b/persimmon/view/blocks/block.kv @@ -1,33 +1,29 @@ : - # Fixed options - size_hint: None, None - size: '200dp', '75dp' - drag_rectangle: self.x, self.y, self.width, self.height - drag_timeout: 10000000 - drag_distance: 0 + # Fixed options + size_hint: None, None + width: '200dp' + drag_rectangle: self.x, self.y, self.width, self.height + drag_timeout: 10000000 + drag_distance: 0 + label: label canvas: Color: - rgb: .2, .2, .2 - Rectangle: + rgba: .15, .15, .15, .8 + RoundedRectangle: pos: self.pos size: self.size - #canvas.before: - #Color: - #rgb: 1, 1, 1 - #BorderImage: - #pos: self.x - 5, self.y - 5 - #size: self.width + 10, self.height + 10 - #source: 'tex2.png' - # Children - Label: - pos: root.x, root.y + root.height - 20 - size_hint: None, None - height: '20dp' + # Children + Label: + id: label + pos: root.x, root.y + root.height - 20 + size_hint: None, None + height: '20dp' width: root.width text: root.title - canvas.before: - Color: - rgb: root.block_color - Rectangle: - pos: self.pos - size: self.size + canvas.before: + Color: + rgba: [*root.block_color, 0.8] + RoundedRectangle: + pos: self.pos + size: self.size + radius: [(10, 10), (10, 10), (0, 0), (0, 0)] diff --git a/persimmon/view/blocks/block.py b/persimmon/view/blocks/block.py index 1fa281b..b858526 100644 --- a/persimmon/view/blocks/block.py +++ b/persimmon/view/blocks/block.py @@ -5,11 +5,12 @@ from kivy.uix.behaviors import DragBehavior from kivy.properties import ListProperty, StringProperty, ObjectProperty from kivy.lang import Builder -from kivy.graphics import BorderImage, Color +from kivy.graphics import BorderImage, Color, RoundedRectangle from kivy.uix.image import Image # Types are fun from typing import Optional from abc import abstractmethod +from functools import partial Builder.load_file('view/blocks/block.kv') @@ -17,6 +18,7 @@ class Block(DragBehavior, FloatLayout, metaclass=AbstractWidget): block_color = ListProperty([1, 1, 1]) title = StringProperty() + label = ObjectProperty() inputs = ObjectProperty() outputs = ObjectProperty() input_pins = ListProperty() @@ -26,18 +28,32 @@ class Block(DragBehavior, FloatLayout, metaclass=AbstractWidget): def __init__(self, **kwargs): super().__init__(**kwargs) + if self.inputs: for pin in self.inputs.children: self.input_pins.append(pin) pin.block = self + self.gap = pin.width * 2 if self.outputs: for pin in self.outputs.children: self.output_pins.append(pin) pin.block = self + self.gap = pin.width * 2 self.tainted_msg = 'Block {} has unconnected inputs'.format(self.title) self._tainted = False self.kindled = None self.border_texture = Image(source='border.png').texture + # Make block taller if necessary + self.height = (max(len(self.output_pins), len(self.input_pins), 3) * + self.gap + self.label.height) + # Position pins nicely + y_origin = self.y + (self.height - self.label.height) + for i, in_pin in enumerate(list(self.input_pins[::-1]), 1): + self._bind_pin(self, (in_pin.x, in_pin.y), in_pin, i, False) + self.fbind('pos', self._bind_pin, pin=in_pin, i=i, output=False) + for i, out_pin in enumerate(list(self.output_pins[::-1]), 1): + self._bind_pin(self, (out_pin.x, out_pin.y), out_pin, i, True) + self.fbind('pos', self._bind_pin, pin=out_pin, i=i, output=True) @property def tainted(self): @@ -72,14 +88,14 @@ def function(self): raise NotImplementedError # Kivy touch events override - def on_touch_down(self, touch): + def on_touch_down(self, touch) -> bool: pin = self.in_pin(*touch.pos) if pin: # if touch is on pin let them handle return pin.on_touch_down(touch) else: # else default behavior (drag if collide) return super().on_touch_down(touch) - def on_touch_up(self, touch): + def on_touch_up(self, touch) -> bool: pin = self.in_pin(*touch.pos) if pin: result = pin.on_touch_up(touch) @@ -91,9 +107,9 @@ def kindle(self): """ Praise the sun \[T]/ """ with self.canvas.before: Color(1, 1, 1) - self.kindled = BorderImage(pos=(self.x - 5, self.y - 5), - size=(self.width + 10, - self.height + 10), + self.kindled = BorderImage(pos=(self.x - 2, self.y - 2), + size=(self.width + 4, + self.height + 4), texture=self.border_texture) self.fbind('pos', self._bind_border) @@ -109,4 +125,14 @@ def unkindle(self): # Auxiliary functions def _bind_border(self, block, new_pos): """ Bind border to position. """ - self.kindled.pos = new_pos[0] - 5, new_pos[1] - 5 + self.kindled.pos = new_pos[0] - 2, new_pos[1] - 2 + + def _bind_pin(self, block, new_pos, pin, i, output): + """ Keep pins on their respective places. """ + pin.y = (block.y + (block.height - block.label.height) - i * self.gap + + pin.height / 2) + if output: + pin.x = block.x + block.width - self.gap + else: + pin.x = block.x + 5 + diff --git a/persimmon/view/blocks/crossvalidationblock.kv b/persimmon/view/blocks/crossvalidationblock.kv index aac134c..aa23e05 100644 --- a/persimmon/view/blocks/crossvalidationblock.kv +++ b/persimmon/view/blocks/crossvalidationblock.kv @@ -11,24 +11,16 @@ id: inputs InputPin: id: data_input - x: root.x + 5 - y: root.y + root.height - 15 _type: root.t.DATAFRAME InputPin: id: estimator_input - x: root.x + 5 - y: root.y + root.height / 2 - 5 _type: root.t.CLASSIFICATOR InputPin: id: cross_val_input - x: root.x + 5 - y: root.y + 5 _type: root.t.CROSS_VALIDATOR Widget: id: outputs OutputPin: id: cross_out - x: root.x + root.width - 15 - y: root.y + root.height / 2 _type: root.t.ANY diff --git a/persimmon/view/blocks/csvinblock.kv b/persimmon/view/blocks/csvinblock.kv index b44fdb6..6d96176 100644 --- a/persimmon/view/blocks/csvinblock.kv +++ b/persimmon/view/blocks/csvinblock.kv @@ -12,6 +12,4 @@ id: outputs OutputPin: id: out_1 - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.DATAFRAME diff --git a/persimmon/view/blocks/csvoutblock.kv b/persimmon/view/blocks/csvoutblock.kv index 6e99fa4..5ae5734 100644 --- a/persimmon/view/blocks/csvoutblock.kv +++ b/persimmon/view/blocks/csvoutblock.kv @@ -12,6 +12,4 @@ id: inputs InputPin: id: in_1 - x: root.x + 5 - y: root.y + root.height / 2 _type: root.t.DATAFRAME diff --git a/persimmon/view/blocks/dictblock.kv b/persimmon/view/blocks/dictblock.kv index 463456f..e7471ff 100644 --- a/persimmon/view/blocks/dictblock.kv +++ b/persimmon/view/blocks/dictblock.kv @@ -8,8 +8,6 @@ id: outputs OutputPin: id: dict_out - x: root.x + root.width - 15 - y: root.y + root.height / 2 - 5 _type: root.t.STATE TextInput: id: tinput diff --git a/persimmon/view/blocks/fitblock.kv b/persimmon/view/blocks/fitblock.kv index 7d899c0..16acd63 100644 --- a/persimmon/view/blocks/fitblock.kv +++ b/persimmon/view/blocks/fitblock.kv @@ -10,18 +10,12 @@ id: inputs InputPin: id: data_in - x: root.x + 5 - y: root.y + root.height - 35 _type: root.t.DATAFRAME InputPin: id: est_in - x: root.x + 5 - y: root.y + 5 _type: root.t.CLASSIFICATOR Widget: id: outputs OutputPin: id: est_out - x: root.x + root.width - 15 - y: root.y + root.height / 2 - 5 _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/gridsearchblock.kv b/persimmon/view/blocks/gridsearchblock.kv index 09d419f..b67617b 100644 --- a/persimmon/view/blocks/gridsearchblock.kv +++ b/persimmon/view/blocks/gridsearchblock.kv @@ -12,28 +12,18 @@ id: inputs InputPin: id: data_in - x: root.x + 5 - y: root.y + root.height - 15 _type: root.t.DATAFRAME InputPin: id: est_in - x: root.x + 5 - y: root.y + root.height / 2 - 5 _type: root.t.CLASSIFICATOR InputPin: id: params_in - x: root.x + 5 - y: root.y + 5 _type: root.t.STATE Widget: id: outputs OutputPin: id: est_out - x: root.x + root.width - 15 - y: root.y + root.height - 35 _type: root.t.CLASSIFICATOR OutputPin: id: score_out - x: root.x + root.width - 15 - y: root.y + 5 _type: root.t.ANY diff --git a/persimmon/view/blocks/knnblock.kv b/persimmon/view/blocks/knnblock.kv index 29eecc6..8c6eacf 100644 --- a/persimmon/view/blocks/knnblock.kv +++ b/persimmon/view/blocks/knnblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: est_out - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/predictblock.kv b/persimmon/view/blocks/predictblock.kv index 1ff9dbc..c41b966 100644 --- a/persimmon/view/blocks/predictblock.kv +++ b/persimmon/view/blocks/predictblock.kv @@ -10,18 +10,13 @@ id: inputs InputPin: id: est_in - x: root.x + 5 - y: root.y + root.height - 35 _type: root.t.CLASSIFICATOR InputPin: id: data_in - x: root.x + 5 - y: root.y + 5 _type: root.t.DATAFRAME Widget: id: outputs OutputPin: id: plain_out - x: root.x + root.width - 15 - y: root.y + root.height / 2 - 5 _type: root.t.ANY + diff --git a/persimmon/view/blocks/printblock.kv b/persimmon/view/blocks/printblock.kv index 0182fd9..8830193 100644 --- a/persimmon/view/blocks/printblock.kv +++ b/persimmon/view/blocks/printblock.kv @@ -7,6 +7,4 @@ id: inputs InputPin: id: in_1 - x: root.x + 5 - y: root.y + root.height / 2 _type: root.t.ANY diff --git a/persimmon/view/blocks/randomforestblock.kv b/persimmon/view/blocks/randomforestblock.kv index 7ca63cc..42ec9c4 100644 --- a/persimmon/view/blocks/randomforestblock.kv +++ b/persimmon/view/blocks/randomforestblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: out_1 - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/sgdblock.kv b/persimmon/view/blocks/sgdblock.kv index 9e60958..85264c4 100644 --- a/persimmon/view/blocks/sgdblock.kv +++ b/persimmon/view/blocks/sgdblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: est_out - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/svmblock.kv b/persimmon/view/blocks/svmblock.kv index b2eca48..ec08f76 100644 --- a/persimmon/view/blocks/svmblock.kv +++ b/persimmon/view/blocks/svmblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: out_1 - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/tenfoldblock.kv b/persimmon/view/blocks/tenfoldblock.kv index 6724279..fa19f88 100644 --- a/persimmon/view/blocks/tenfoldblock.kv +++ b/persimmon/view/blocks/tenfoldblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: out_1 - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CROSS_VALIDATOR diff --git a/persimmon/view/blocks/tssplitblock.kv b/persimmon/view/blocks/tssplitblock.kv index 6e463e8..530ff71 100644 --- a/persimmon/view/blocks/tssplitblock.kv +++ b/persimmon/view/blocks/tssplitblock.kv @@ -7,6 +7,4 @@ id: outputs OutputPin: id: out_1 - x: root.x + root.width - 10 - 5 - y: root.y + root.height / 2 _type: root.t.CROSS_VALIDATOR diff --git a/persimmon/view/util/__init__.py b/persimmon/view/util/__init__.py index 46ac646..87ce838 100644 --- a/persimmon/view/util/__init__.py +++ b/persimmon/view/util/__init__.py @@ -1,9 +1,10 @@ +from .types import Type, BlockType, AbstractWidget from .circularbutton import CircularButton from .connection import Connection from .filedialog import FileDialog from .notification import Notification -from .types import Type, BlockType, AbstractWidget from .pin import Pin from .inpin import InputPin from .outpin import OutputPin + diff --git a/persimmon/view/util/connection.py b/persimmon/view/util/connection.py index 305da05..fe3041f 100644 --- a/persimmon/view/util/connection.py +++ b/persimmon/view/util/connection.py @@ -4,6 +4,8 @@ from kivy.properties import ObjectProperty, ListProperty from kivy.graphics import Color, Ellipse, Line from kivy.clock import Clock +# For type hinting +from kivy.input.motionevent import MotionEvent # Numpy for sin import numpy as np # Others @@ -11,22 +13,6 @@ import logging -""" -: - #lin: root.start_pos + root.end_pos - canvas.after: - Color: - rgb: root.color - Ellipse: - pos: root.start_pos - size: root.start.size - Ellipse: - pos: root.end_pos - size: root.end.size - #Line: - #points: root.lin -""" - logger = logging.getLogger(__name__) class Connection(Widget): @@ -34,163 +20,138 @@ class Connection(Widget): end = ObjectProperty(allownone=True) color = ListProperty() lin = ObjectProperty() - start_cr = ObjectProperty() - end_cr = ObjectProperty() def __init__(self, **kwargs): + """ On this initializer the connection has to check whether the + connection is being made forward or backwards. """ super().__init__(**kwargs) if self.start: self.forward = True + self.bez_start = self.start.center with self.canvas.before: Color(*self.color) - self.start_cr = Ellipse(pos=self.start.pos, - size=self.start.size) - self.lin = Line(points=self.start.center + self.start.center, - width=1.5) - self.start.fbind('pos', self.circle_bind) - self.start.fbind('pos', self.line_bind) + self.lin = Line(bezier=self.bez_start * 4, width=1.5) + self._bind_pin(self.start) else: self.forward = False + self.bez_end = self.end.center with self.canvas.before: Color(*self.color) - self.end_cr = Ellipse(pos=self.end.pos, size=self.end.size) - self.lin = Line(points=self.end.center + self.end.center, - width=1.5) - self.end.fbind('pos', self.circle_bind) - self.end.fbind('pos', self.line_bind) + self.lin = Line(bezier=self.bez_end * 4, width=1.5) + self._bind_pin(self.end) self.warned = False - self.it = None - def finish_connection(self, pin): + def finish_connection(self, pin: 'Pin'): """ This functions finishes a connection that has only start or end and is being currently dragged """ if self.forward: self.end = pin - with self.canvas.before: - self.end_cr = Ellipse(pos=self.end.pos, size=self.end.size) - self.rebind_pin(self.end, pin) + self._bind_pin(self.end) else: self.start = pin - with self.canvas.before: - self.start_cr = Ellipse(pos=self.start.pos, - size=self.start.size) - self.rebind_pin(self.start, pin) + self._bind_pin(self.start) + + # Kivy touch override + def on_touch_down(self, touch: MotionEvent): + """ On touch down on connection means we are modifying an already + existing connection, not creating a new one. """ + if self.start.collide_point(*touch.pos): + self.forward = False + # Remove start edge + self._unbind_pin(self.start) + self.start.on_connection_delete(self) + self.start = None + # This signals that we are dragging a connection + touch.ud['cur_line'] = self + return True + elif self.end.collide_point(*touch.pos): + # Same as before but with the other edge + self.forward = True + self._unbind_pin(self.end) + self.end.on_connection_delete(self) + self.end = None + touch.ud['cur_line'] = self + return True + else: + return False def follow_cursor(self, newpos, blackboard): """ This functions makes sure the current end being dragged follows the cursor. It also checks for type safety and changes the line color if needed.""" if self.forward: - self.lin.points = self.lin.points[:2] + [*newpos] fixed_edge = self.start + self.bez_end = [*newpos] + self._rebezier() else: - self.lin.points = [*newpos] + self.lin.points[2:] fixed_edge = self.end + self.bez_start = [*newpos] + self._rebezier() + # The conditionals are so complicated because it is necessary to check + # whether or not a pin in a block has been touched, and then check + # the typesafety. if (self.warned and (not blackboard.in_block(*newpos) or not blackboard.in_block(*newpos).in_pin(*newpos) or blackboard.in_block(*newpos).in_pin(*newpos).typesafe(fixed_edge))): - self.unwarn() + self._unwarn() elif (blackboard.in_block(*newpos) and blackboard.in_block(*newpos).in_pin(*newpos) and (not blackboard.in_block(*newpos).in_pin(*newpos).typesafe(fixed_edge))): - self.warn() + # This conditional represents that the cursor stepped out the pin + self._warn() - def delete_connection(self, parent): + def delete_connection(self): """ This function deletes both ends (if they exist) and the connection - itself. """ - parent.remove_widget(self) + itself. """ + self.parent.remove_widget(self) # Self-destruct if self.start: + self._unbind_pin(self.start) self.start.on_connection_delete(self) if self.end: + self._unbind_pin(self.end) self.end.on_connection_delete(self) - def on_touch_down(self, touch): - """ On touch down on connection means we are modifying an already - existing connection, not creating a new one """ - if self.start.collide_point(*touch.pos): - self.forward = False - self.unbind_pin(self.start) - self.uncircle_pin(self.start) - self.start.on_connection_delete(self) - touch.ud['cur_line'] = self - self.start = None - return True - elif self.end.collide_point(*touch.pos): - self.forward = True - self.unbind_pin(self.end) - self.uncircle_pin(self.end) - self.end.on_connection_delete(self) - touch.ud['cur_line'] = self - self.end = None - return True - else: - return False - - def rebind_pin(self, old, new): - """ Unbinds pin and circle pin, changes pin and rebinds. """ - old.funbind('pos', self.circle_bind) - old.funbind('pos', self.line_bind) - old = new - old.fbind('pos', self.circle_bind) - old.fbind('pos', self.line_bind) + def pulse(self): + """ Makes a connection appear to pulse by modifying its width + continuosly. """ + self.it = self._change_width() + next(self.it) + Clock.schedule_interval(lambda _: next(self.it), 0.05) # 20 FPS - def unbind_pin(self, finish): - finish.funbind('pos', self.circle_bind) - finish.funbind('pos', self.line_bind) + def stop_pulse(self): + """ Stops vibrating a connection. It will throw an execution if + the connection is not pulsing right now. """ + self.it.throw(StopIteration) - def uncircle_pin(self, pin): - if pin == self.start: - self.canvas.before.remove(self.start_cr) - elif pin == self.end: - self.canvas.before.remove(self.end_cr) - else: - logger.error('Attempted to uncircle pin without circle') + # Auxiliary methods + # Binding methods + def _unbind_pin(self, pin: 'Pin'): + """ Undos pin's circle and line binding. """ + pin.funbind('pos', self._line_bind) - def circle_bind(self, pin, new_pos): - if pin == self.start: - self.start_cr.pos = pin.pos - elif pin == self.end: - self.end_cr.pos = pin.pos - else: - logger.error('No circle associated with pin') + def _bind_pin(self, pin: 'Pin'): + """ Performs pin circle and line binding. """ + pin.fbind('pos', self._line_bind) - def line_bind(self, pin, new_pos): + def _line_bind(self, pin: 'Pin', new_pos: (float, float)): if pin == self.start: - self.lin.points = pin.center + self.lin.points[2:] + self.bez_start = pin.center + self._rebezier() elif pin == self.end: - self.lin.points = self.lin.points[:2] + pin.center + self.bez_end = pin.center + self._rebezier() else: logger.error('No line associated with pin') - def warn(self): - self.warned = True - self.canvas.before.remove(self.lin) - with self.canvas.before: - Color(1, 0, 0) - self.lin = Line(points=self.lin.points, width=3) - - def unwarn(self): - self.warned = False - self.canvas.before.remove(self.lin) - with self.canvas.before: - Color(*self.color) - self.lin = Line(points=self.lin.points, width=1.5) - - def pulse(self): - self.it = self._change_width() - next(self.it) - Clock.schedule_interval(lambda _: next(self.it), 0.05) # 20 FPS - - def stop_pulse(self): - self.it.throw(StopIteration) - + # Pulsing methods def _change_width(self): """ Ok, so let me explain what is going on, this generator/coroutine changes the width of the line continuosly using the width_gen generator. We use it by calling it 20 times per second. The tricky part is stopping the scheduled calls. The way to tell Kivy to stop calling is to return a False value, and to do that we need to call - this coroutine itself, which maybe execting or not at that moment. + this coroutine itself, which may be executing or not at that + precise moment. That is where throw comes in, allowing for exceptions to be thrown on during the execution, hijacking the current execution (like a @@ -210,5 +171,44 @@ def _width_gen(self): """ Infinity oscillating generator (between 2 and 4) """ val = 0 while True: - yield 2 * np.sin(val) + 4 + yield np.sin(val) + 3 val += pi / 20 + + # Warn methods + def _warn(self): + """ Changes the current line to a red thick connection. """ + self.warned = True + self.canvas.before.remove(self.lin) + with self.canvas.before: + Color(1, 0, 0) + self.lin = Line(points=self.lin.points, width=3) + self._rebezier() + + def _unwarn(self): + """ Returns the red thick connection to its normal state. """ + self.warned = False + self.canvas.before.remove(self.lin) + with self.canvas.before: + Color(*self.color) + self.lin = Line(points=self.lin.points, width=1.5) + self._rebezier() + + # Bezier refreshing + def _rebezier(self): + """ Refreshes bezier curve according to start and end. + It uses the arctan to force the bèzier curve always going a bit + forward before drifting.""" + arc_tan = np.arctan2(self.bez_start[1] - self.bez_end[1], + self.bez_start[0] - self.bez_end[0]) + abs_angle = np.abs(np.degrees(arc_tan)) + # We use the angle value plus a fixed amount to steer the line a bit + start_right = [self.bez_start[0] - 5 - 0.6 * abs_angle, + self.bez_start[1]] + end_left = [self.bez_end[0] + 5 + 0.6 * abs_angle, self.bez_end[1]] + # Y distance to mid point + dist = (min(self.bez_start[0], self.bez_end[0]) + + abs(self.bez_start[0] - self.bez_end[0]) / 2) + # This updates the bèzier curve graphics + self.lin.bezier = (self.bez_start + start_right + + [dist, self.bez_start[1]] + [dist, self.bez_end[1]] + + end_left + self.bez_end) diff --git a/persimmon/view/util/inpin.kv b/persimmon/view/util/inpin.kv new file mode 100644 index 0000000..ae76084 --- /dev/null +++ b/persimmon/view/util/inpin.kv @@ -0,0 +1,6 @@ +: + canvas.before: + Color: + rgb: root.color + Triangle: + points: (root.x + root.width * 0.75, root.y, root.x + root.width * 0.75, root.y + root.height, root.x + root.width * 1.55, root.y + root.height / 2) diff --git a/persimmon/view/util/inpin.py b/persimmon/view/util/inpin.py index 99f31e0..4c4e0e0 100644 --- a/persimmon/view/util/inpin.py +++ b/persimmon/view/util/inpin.py @@ -1,41 +1,71 @@ from persimmon.view.util import Pin, Connection from kivy.properties import ObjectProperty +from kivy.lang import Builder +from kivy.graphics import Ellipse, Color +from kivy.input.motionevent import MotionEvent + import logging +Builder.load_file('view/util/inpin.kv') logger = logging.getLogger(__name__) class InputPin(Pin): origin = ObjectProperty(allownone=True) - def on_touch_down(self, touch): - if self.collide_point(*touch.pos) and touch.button == 'left': + # Kivy touch methods override + def on_touch_down(self, touch: MotionEvent) -> bool: + if (self.collide_point(*touch.pos) and touch.button == 'left' and + not self.origin): logger.info('Creating connection') touch.ud['cur_line'] = Connection(start=self, color=self.color) self.origin = touch.ud['cur_line'] # Add to blackboard - self.block.parent.parent.parent.add_widget(touch.ud['cur_line']) + self.block.parent.parent.parent.connections.add_widget(touch.ud['cur_line']) + self._circle_pin() return True else: return False - def on_touch_up(self, touch): + def on_touch_up(self, touch: MotionEvent) -> bool: if ('cur_line' in touch.ud.keys() and touch.button == 'left' and self.collide_point(*touch.pos)): - if touch.ud['cur_line'].end and self.typesafe(touch.ud['cur_line'].end): + if (touch.ud['cur_line'].end and + self.typesafe(touch.ud['cur_line'].end)): logger.info('Establishing connection') touch.ud['cur_line'].finish_connection(self) self.origin = touch.ud['cur_line'] + self._circle_pin() else: logger.info('Deleting connection') - touch.ud['cur_line'].delete_connection(self.block.parent.parent) + touch.ud['cur_line'].delete_connection() return True else: return False def on_connection_delete(self, connection: Connection): - self.origin = None + if self.origin: + self.origin = None + # Undo pin circling + self.funbind('pos', self._bind_circle) + self.canvas.remove(self.circle) + del self.circle + else: + logger.error('Deleting connection not fully formed') def typesafe(self, other: Pin) -> bool: return super().typesafe(other) and self.origin == None + + def _circle_pin(self): + if hasattr(self, 'circle'): + logger.error('Circling pin twice') + return + with self.canvas: + Color(*self.color) + self.circle = Ellipse(pos=self.pos, size=self.size) + self.fbind('pos', self._bind_circle) + + def _bind_circle(self, instance, value): + self.circle.pos = self.pos + diff --git a/persimmon/view/util/outpin.kv b/persimmon/view/util/outpin.kv new file mode 100644 index 0000000..7382049 --- /dev/null +++ b/persimmon/view/util/outpin.kv @@ -0,0 +1,7 @@ +: + canvas.before: + Color: + rgb: root.color + Triangle: + points: (root.x + root.width * 0.75, root.y, root.x + root.width * 0.75, root.y + root.height, root.x + root.width * 1.55, root.y + root.height / 2) + diff --git a/persimmon/view/util/outpin.py b/persimmon/view/util/outpin.py index 1103c7c..9fe9025 100644 --- a/persimmon/view/util/outpin.py +++ b/persimmon/view/util/outpin.py @@ -1,43 +1,70 @@ from persimmon.view.util import Pin, Connection from kivy.properties import ObjectProperty, ListProperty +from kivy.lang import Builder +from kivy.graphics import Ellipse, Color + +from kivy.input.motionevent import MotionEvent import logging +Builder.load_file('view/util/outpin.kv') logger = logging.getLogger(__name__) class OutputPin(Pin): destinations = ListProperty() - def on_touch_down(self, touch): - if self.collide_point(*touch.pos) and touch.button == 'left': + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def on_touch_down(self, touch: MotionEvent) -> bool: + if (self.collide_point(*touch.pos) and touch.button == 'left' and + not self.destinations): logger.info('Creating connection') touch.ud['cur_line'] = Connection(end=self, color=self.color) self.destinations.append(touch.ud['cur_line']) # Add to blackboard - self.block.parent.parent.parent.add_widget(touch.ud['cur_line']) + self.block.parent.parent.parent.connections.add_widget(touch.ud['cur_line']) + self._circle_pin() return True else: return False - def on_touch_up(self, touch): + def on_touch_up(self, touch: MotionEvent) -> bool: if ('cur_line' in touch.ud.keys() and touch.button == 'left' and self.collide_point(*touch.pos)): - if touch.ud['cur_line'].start and self.typesafe(touch.ud['cur_line'].start): + if (touch.ud['cur_line'].start and + self.typesafe(touch.ud['cur_line'].start)): logger.info('Establishing connection') touch.ud['cur_line'].finish_connection(self) self.destinations.append(touch.ud['cur_line']) + self._circle_pin() else: logger.info('Deleting connection') - touch.ud['cur_line'].delete_connection(self.block.parent.parent) + touch.ud['cur_line'].delete_connection() return True else: return False - def on_connection_delete(self, connection): + def on_connection_delete(self, connection: Connection): if connection in self.destinations: self.destinations.remove(connection) - - def typesafe(self, other): - return super().typesafe(other) + # Undoing circling + self.funbind('pos', self._bind_circle) + self.canvas.remove(self.circle) + del self.circle + else: + logger.error('Deleting connection not fully formed') + + def _circle_pin(self): + if hasattr(self, 'circle'): + logger.error('Circling pin twice') + return + with self.canvas: + Color(*self.color) + self.circle = Ellipse(pos=self.pos, size=self.size) + self.fbind('pos', self._bind_circle) + + def _bind_circle(self, instance, value): + self.circle.pos = self.pos diff --git a/persimmon/view/util/pin.py b/persimmon/view/util/pin.py index cea652a..1619f49 100644 --- a/persimmon/view/util/pin.py +++ b/persimmon/view/util/pin.py @@ -1,8 +1,8 @@ from persimmon.view.util import CircularButton, Connection -#from persimmon.backend import Test from kivy.properties import ObjectProperty from kivy.lang import Builder from kivy.graphics import Color, Ellipse, Line +from kivy.input.motionevent import MotionEvent from persimmon.view.util import Type, AbstractWidget from abc import abstractmethod @@ -14,28 +14,32 @@ class Pin(CircularButton, metaclass=AbstractWidget): block = ObjectProperty() ellipse = ObjectProperty() line = ObjectProperty() - _type = ObjectProperty(Type.ANY) + _type = ObjectProperty(Type.ANY) - def on__type(self, instance, value): - """ If the kv lang was a bit smarted this would not be needed - """ - self.color = value.value - @abstractmethod - def on_touch_down(self, touch): + def on_touch_down(self, touch: MotionEvent) -> bool: raise NotImplementedError - + @abstractmethod - def on_touch_up(self, touch): + def on_touch_up(self, touch: MotionEvent) -> bool: raise NotImplementedError - + @abstractmethod def on_connection_delete(self, connection: Connection): raise NotImplementedError def typesafe(self, other: 'Pin') -> bool: + """ Tells if a relation between two pins is typesafe. """ if ((self._type == Type.ANY or other._type == Type.ANY) and self.block != other.block and self.__class__ != other.__class__): return True # Anything is possible with ANY else: - return self._type == other._type and self.block != other.block and self.__class__ != other.__class__ + return (self._type == other._type and self.block != other.block and + self.__class__ != other.__class__) + + # Hack + def on__type(self, instance, value): + """ If the kv lang was a bit smarted this would not be needed + """ + self.color = value.value + diff --git a/persimmon/view/view.kv b/persimmon/view/view.kv index 8da4438..cc0bdc3 100644 --- a/persimmon/view/view.kv +++ b/persimmon/view/view.kv @@ -3,6 +3,11 @@ FloatLayout: + canvas.before: + Rectangle: + pos: 0, 0 + size: app.background.size if app.background else [0, 0] + texture: app.background BlackBoard: id: blackboard pos: 0, 0 @@ -11,11 +16,6 @@ FloatLayout: do_scale: False do_translation: False auto_bring_to_front: False - canvas.before: - Rectangle: - pos: 0, 0 - size: self.size - texture: app.background Button: text: 'Execute' size_hint: None, None @@ -121,6 +121,8 @@ FloatLayout: : blocks: blocks + connections: connections + Widget: + id: connections Widget: id: blocks - diff --git a/persimmon/view/view.py b/persimmon/view/view.py index 95dd62a..d09964b 100644 --- a/persimmon/view/view.py +++ b/persimmon/view/view.py @@ -1,6 +1,7 @@ # Persimmon imports from persimmon.view import blocks from persimmon.view.util import Notification +from persimmon.view.blocks import Block import persimmon.backend as backend # Kivy imports from kivy.app import App @@ -23,7 +24,6 @@ from collections import deque import logging from typing import Optional -from persimmon.view.blocks import Block from itertools import chain @@ -34,15 +34,12 @@ class ViewApp(App): background = ObjectProperty() def build(self): - self.background = Image(source='connections.png').texture - self.background.wrap = 'repeat' - self.background.uvsize = 30, 30 - #self.background.uvsize = (Window.width / self.background.width, - # Window.height / self.background.height) + self.background = Image(source='background.png').texture return Builder.load_file('view/view.kv') class BlackBoard(ScatterLayout): blocks = ObjectProperty() + connections = ObjectProperty() popup = ObjectProperty(Notification()) def execute_graph(self): @@ -161,7 +158,7 @@ def on_touch_up(self, touch): # if no connection was made if 'cur_line' in touch.ud.keys() and touch.button == 'left': logger.info('Connection was not finished') - touch.ud['cur_line'].delete_connection(self) + touch.ud['cur_line'].delete_connection() return True # stop propagating if its within our bounds diff --git a/setup.py b/setup.py index b3ca6c4..0e50eff 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ license='MIT', packages=find_packages(), include_package_data=True, + python_requires='>=3.5', setup_requires=['Cython'], install_requires=['Cython', 'Kivy', 'scipy', 'scikit-learn', 'pandas', 'coloredlogs'],