From 336ab6dc1339e7311b3c354520f609ddd56109c1 Mon Sep 17 00:00:00 2001 From: Taylor Bockman Date: Sun, 2 Feb 2020 18:26:25 -0800 Subject: [PATCH] Init --- .gitignore | 97 ++++++++ .vscode/settings.json | 5 + README.md | 30 +++ example.PNG | Bin 0 -> 93804 bytes main_window.py | 263 ++++++++++++++++++++++ requirements-dev.txt | 8 + requirements.txt | 5 + setup.cfg | 2 + voronoiview.py | 18 ++ voronoiview.ui | 368 +++++++++++++++++++++++++++++++ voronoiview/__init__.py | 0 voronoiview/colors.py | 27 +++ voronoiview/debug.py | 9 + voronoiview/exceptions.py | 57 +++++ voronoiview/mode.py | 16 ++ voronoiview/point_manager.py | 62 ++++++ voronoiview/points.py | 391 +++++++++++++++++++++++++++++++++ voronoiview/ui/__init__.py | 0 voronoiview/ui/mode_handlers.py | 378 +++++++++++++++++++++++++++++++ voronoiview/ui/opengl_widget.py | 427 ++++++++++++++++++++++++++++++++++++ voronoiview/ui/point_list_widget.py | 55 +++++ voronoiview_ui.py | 191 ++++++++++++++++ 22 files changed, 2409 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 example.PNG create mode 100644 main_window.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 voronoiview.py create mode 100644 voronoiview.ui create mode 100644 voronoiview/__init__.py create mode 100644 voronoiview/colors.py create mode 100644 voronoiview/debug.py create mode 100644 voronoiview/exceptions.py create mode 100644 voronoiview/mode.py create mode 100644 voronoiview/point_manager.py create mode 100644 voronoiview/points.py create mode 100644 voronoiview/ui/__init__.py create mode 100644 voronoiview/ui/mode_handlers.py create mode 100644 voronoiview/ui/opengl_widget.py create mode 100644 voronoiview/ui/point_list_widget.py create mode 100644 voronoiview_ui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b93c1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +.mypy_cache diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..320806e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.pythonPath": "venv/bin/python3.7", + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c6a88c --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Voronoi View + +Some simple software to explore the generation of Voronoi Diagrams with a drawable canvas. + +![](example.PNG) + +## Usage + +First install the necessary packages: + +`pip install -r requirements.txt` + +Then launch Voronoi View using: + +`python voronoiview.py` + +from the root directory. + +## Development + +Make sure to install the development requirements using `pip install -r requirements-dev.txt`. This will install +all main requirements as well as useful testing and linting tools. + +### Regenerating the UI + +After modifying the `*.ui` file in Qt Designer run + +`pyuic5 voronoiview.ui -o voronoiview.py` + +to regenerate the UI python file. diff --git a/example.PNG b/example.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a41adf9ab2126a29f06acb88af1ad93dee0ea7e8 GIT binary patch literal 93804 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�b}M_B*;IReYkh;x^1TNvHL6eZvS2=Nr!YUsdgp#XJ*A#6EX>*`TZ$N%gW97k(+Dh%3?mrlFC1% zid+26v15Gb_-_mgxVYPk$k1ir?e2K{jKsE~+p`|8R*%tf$|MUlHMO+}?0#x}#Mkv- zxAYWd@2*Ikqx)&#RpIHk`0xA=hQspqjyLaLQvNU62Aq3;8ESu^j9otA$sFppZ`%I5 zjKc}OPl>S0?p;61D}H;{VI@EPn=HhAkCy=PbDy_JCi<7Te$C*W|FTkOvC{s5fr0Oz z(uw>YzC9iC@0uTQx?voW7YIR@r4hdbm3N*i#z5QgwLMvVq4uu=8TZR!O6+3ex}B6u zmZ8UL_qX0xceO#*x601^%d|G2U<}pMq{p^S{1*IA(f3c0xZOyJ9a#hon!la#OY-yk z-J0?9)f7h?Te#o6S5DXX;Vl9(S|<;5<4vkn8A8l6an~Ft7DY0fwyGA-c}o_PG`pL( z4RzJ%+#d^B&A0V_ypqZ>`1;e^{A8hp6E0FBhK0u!4N0%;KmVZljwL zPd}_r-x@fpryG0Ch*|As^=mi5=-H1Ig_nx+*~6>ll3%-7feCfR{@;ZDZ-@TBcu3r9 pK}XzUX{2{}n6Aa-&5!W!!isein{y(_e^w9tS literal 0 HcmV?d00001 diff --git a/main_window.py b/main_window.py new file mode 100644 index 0000000..291e9b2 --- /dev/null +++ b/main_window.py @@ -0,0 +1,263 @@ +from functools import partial + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QErrorMessage, QFileDialog, QInputDialog, QMainWindow + +from voronoiview.exceptions import handle_exceptions +from voronoiview.mode import Mode +from voronoiview.ui.mode_handlers import (MODE_HANDLER_MAP, + ogl_keypress_handler, + refresh_point_list, + reset_colors, + generate_random_points) +from voronoiview.ui.opengl_widget import (clear_selection, initialize_gl, + mouse_leave, paint_gl, resize_gl, + set_drawing_context) +from voronoiview.points import PointSet +from voronoiview.point_manager import PointManager +from voronoiview.ui.point_list_widget import item_click_handler +from voronoiview_ui import Ui_MainWindow + + +class MainWindow(QMainWindow, Ui_MainWindow): + """ + A wrapper class for handling creating a window based + on the `voronoiview_ui.py` code generated from + `voronoiview.ui`. + """ + + # This is a static mode variable since there will only ever + # be one MainWindow. + _mode = Mode.OFF + + def __init__(self, parent=None): + super(MainWindow, self).__init__(parent) + self.setupUi(self) + + # Size of point for drawing + self._point_size = 8 + + # TODO: THESE ARE HARD CODED TO THE CURRENT QT WIDGET SIZES + # FIX THIS PROPERLY WITH A RESIZE EVENT DETECT. + # PointManager is a class that is filled with static methods + # designed for managing state. + self._viewport_width = 833 + self._viewport_height = 656 + + PointManager.point_set = PointSet(self._point_size, + self._viewport_width, + self._viewport_height) + + self.voronoi_button.setEnabled(False) + + # We only need to set the context in our OpenGL state machine + # wrapper once here since the window is fixed size. + # If we allow resizing of the window, the context must be updated + # each resize so that coordinates are converted from screen (x, y) + # to OpenGL coordinates properly. + set_drawing_context(self) + + # Enables mouse tracking on the viewport so mouseMoveEvents are + # tracked and fired properly. + self.opengl_widget.setMouseTracking(True) + + # Enable keyboard input capture on the OpenGL Widget + self.opengl_widget.setFocusPolicy(Qt.StrongFocus) + + # Here we partially apply the key press handler with self to + # create a new function that only expects the event `keyPressEvent` + # expects. In this way, we've snuck the state of the opengl_widget + # into the function so that we can modify it as we please. + self.opengl_widget.keyPressEvent = partial(ogl_keypress_handler, self) + + # Same story here but this time with the itemClicked event + # so that when an element is clicked on in the point list it will + # highlight. + self.point_list_widget.itemClicked.connect(partial(item_click_handler, + self)) + + self.voronoi_button.clicked.connect(self._voronoi) + + self.reset_button.clicked.connect(self._reset) + + # ----------------------------------------------- + # OpenGL Graphics Handlers are set + # here and defined in voronoiview.opengl_widget. + # ----------------------------------------------- + self.opengl_widget.initializeGL = initialize_gl + self.opengl_widget.paintGL = paint_gl + self.opengl_widget.resizeGL = resize_gl + self.opengl_widget.leaveEvent = partial(mouse_leave, self) + + # ------------------------------------- + # UI Handlers + # ------------------------------------- + self.action_add_points.triggered.connect(self._add_points) + self.action_edit_points.triggered.connect(self._edit_points) + self.action_delete_points.triggered.connect(self._delete_points) + self.action_move_points.triggered.connect(self._move_points) + self.action_clear_canvas.triggered.connect(self._clear_canvas) + + (self.action_generate_random_points + .triggered.connect(self._generate_random_points)) + + self.action_save_point_configuration.triggered.connect( + self._save_points_file) + + self.action_load_point_configuration.triggered.connect( + self._open_points_file) + + self.action_exit.triggered.connect(self._close_event) + + # Override handler for mouse press so we can draw points based on + # the OpenGL coordinate system inside of the OpenGL Widget. + self.opengl_widget.mousePressEvent = self._ogl_click_dispatcher + self.opengl_widget.mouseMoveEvent = self._ogl_click_dispatcher + self.opengl_widget.mouseReleaseEvent = self._ogl_click_dispatcher + + # Voronoi flag so it does not continue to run + self.voronoi_solved = False + + # ----------------------------------------------------------------- + # Mode changers - these will be used to signal the action in the + # OpenGL Widget. + # ----------------------------------------------------------------- + def _off_mode(self): + self._mode = Mode.OFF + self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) + self.status_bar.showMessage('') + clear_selection() + self.opengl_widget.update() + + def _add_points(self): + self._mode = Mode.ADD + self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor)) + self.status_bar.showMessage('ADD MODE') + clear_selection() + self.opengl_widget.update() + + def _edit_points(self): + self._mode = Mode.EDIT + self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor)) + self.status_bar.showMessage('EDIT MODE') + clear_selection() + self.opengl_widget.update() + + def _delete_points(self): + self._mode = Mode.DELETE + self.opengl_widget.setCursor(QCursor( + Qt.CursorShape.PointingHandCursor)) + self.status_bar.showMessage('DELETE MODE') + clear_selection() + self.opengl_widget.update() + + def _move_points(self): + self._mode = Mode.MOVE + self.opengl_widget.setCursor(QCursor(Qt.CursorShape.SizeAllCursor)) + self.status_bar.showMessage('MOVE MODE - PRESS ESC OR SWITCH MODES ' + + 'TO CANCEL SELECTION') + clear_selection() + self.opengl_widget.update() + + def _clear_canvas(self): + self._reset() + PointManager.point_set.clear_points() + refresh_point_list(self) + self.opengl_widget.update() + + def _voronoi(self): + if len(list(PointManager.point_set.points)) == 0: + error_dialog = QErrorMessage() + error_dialog.showMessage('Place points before generating the voronoi diagram.') + error_dialog.exec_() + return + + clear_selection() + self._mode = Mode.VORONOI + self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) + self.status_bar.showMessage('VORONOI DIAGRAM') + self.opengl_widget.update() + + def _reset(self): + self._off_mode() + self.voronoi_button.setEnabled(False) + self.voronoi_solved = False + PointManager.voronoi_regions = [] + + for point in PointManager.point_set.points: + point.weight = 1.0 + + reset_colors() + + def _generate_random_points(self): + value, ok = QInputDialog.getInt(self, 'Number of Points', + 'Number of Points:', 30, 30, 3000, 1) + + if ok: + self._mode = Mode.ADD + generate_random_points(value, + (self._viewport_width - self._point_size), + (self._viewport_height - self._point_size) + ) + self._mode = Mode.OFF + + self.opengl_widget.update() + + refresh_point_list(self) + + def _voronoi_enabled(self): + point_count = len(list(PointManager.point_set.points)) + self.voronoi_button.setEnabled(point_count > 0) + + @property + def mode(self): + """" + Function designed to be used from a context + to get the current mode. + """ + return self._mode + + @mode.setter + def mode(self, mode): + self._mode = mode + + def _close_event(self, event): + import sys + sys.exit(0) + + def _open_points_file(self): + ofile, _ = QFileDialog.getOpenFileName(self, + 'Open Point Configuration', + '', + 'JSON files (*.json)') + if ofile: + self._mode = Mode.LOADED + + PointManager.load(ofile) + + self.opengl_widget.update() + + refresh_point_list(self) + + def _save_points_file(self): + file_name, _ = (QFileDialog. + getSaveFileName(self, + 'Save Point Configuration', + '', + 'JSON Files (*.json)')) + if file_name: + PointManager.save(file_name) + + @handle_exceptions + def _ogl_click_dispatcher(self, event): + """ + Mode dispatcher for click actions on the OpenGL widget. + """ + # Map from Mode -> function + # where the function is a handler for the + # OpenGL event. The context passed to these functions allows + # them to modify on screen widgets such as the QOpenGLWidget and + # QListWidget. + self._voronoi_enabled() + MODE_HANDLER_MAP[self._mode](self, event) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9ddbe1d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +flake8==3.7.8 +mypy==0.730 +coverage==4.5.4 +pytest==5.0.1 +pytest-cov==2.7.1 +ipython==7.7.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05e5f0f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +PyOpenGL==3.1.0 +PyOpenGL-accelerate==3.1.3b1 +PyQt5==5.13.0 +PyQt5-sip==4.19.18 +scipy==1.4.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/voronoiview.py b/voronoiview.py new file mode 100644 index 0000000..25f218f --- /dev/null +++ b/voronoiview.py @@ -0,0 +1,18 @@ +import sys + +from PyQt5.QtWidgets import QApplication + +from main_window import MainWindow + + +def main(): + app = QApplication(sys.argv) + + window = MainWindow() + window.show() + + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() diff --git a/voronoiview.ui b/voronoiview.ui new file mode 100644 index 0000000..ee3c43c --- /dev/null +++ b/voronoiview.ui @@ -0,0 +1,368 @@ + + + MainWindow + + + + 0 + 0 + 1280 + 720 + + + + + 0 + 0 + + + + + 1280 + 720 + + + + + 1280 + 720 + + + + Voronoi View + + + + + + + + 0 + 0 + + + + + 900 + 16777215 + + + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 200 + 200 + + + + Point List + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + + + + Solver + + + + + + false + + + Generate Voronoi Diagram + + + + + + + Reset + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Canvas Information + + + + + + Mouse Position: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Number of Points: + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1280 + 22 + + + + true + + + + File + + + + + + + + + Help + + + + + + + + + toolBar + + + false + + + LeftToolBarArea + + + false + + + + + + + + + + + + Add Points + + + Enables point adding mode. + + + Ctrl+A + + + + + Edit Points + + + Enables point editing mode. + + + Ctrl+E + + + + + Delete Points + + + Enables point deletion mode. + + + Ctrl+D + + + + + Solve + + + Opens the solve dialog to choose a solving solution. + + + Ctrl+S + + + + + Move Points + + + Enables the movement of a selection of points. + + + + + Save Point Configuration + + + + + Load Point Configuration + + + + + Exit + + + + + Generate Random Points + + + + + Clear Canvas + + + + + + diff --git a/voronoiview/__init__.py b/voronoiview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/voronoiview/colors.py b/voronoiview/colors.py new file mode 100644 index 0000000..32f14c8 --- /dev/null +++ b/voronoiview/colors.py @@ -0,0 +1,27 @@ +from enum import Enum + + +class Color(str, Enum): + BLUE = 'BLUE' + BLACK = 'BLACK' + GREY = 'GREY' + RED = 'RED' + ORANGE = 'ORANGE' + PURPLE = 'PURPLE' + + @classmethod + def count(cls): + return len(cls.__members__) + + +# A simple map from Color -> RGBA 4-Tuple +# Note: The color values in the tuple are not RGB, but +# rather OpenGL percentage values for RGB. +COLOR_TO_RGBA = { + Color.GREY: (0.827, 0.827, 0.826, 0.0), + Color.BLUE: (0.118, 0.565, 1.0, 0.0), + Color.BLACK: (0.0, 0.0, 0.0, 0.0), + Color.RED: (1.0, 0.0, 0.0, 0.0), + Color.ORANGE: (0.98, 0.625, 0.12, 0.0), + Color.PURPLE: (0.60, 0.40, 0.70, 0.0) +} diff --git a/voronoiview/debug.py b/voronoiview/debug.py new file mode 100644 index 0000000..0ad0c5e --- /dev/null +++ b/voronoiview/debug.py @@ -0,0 +1,9 @@ +def debug_trace(): + """ + A wrapper for pdb that works with PyQt5. + """ + from PyQt5.QtCore import pyqtRemoveInputHook + + from pdb import set_trace + pyqtRemoveInputHook() + set_trace() diff --git a/voronoiview/exceptions.py b/voronoiview/exceptions.py new file mode 100644 index 0000000..f4cef26 --- /dev/null +++ b/voronoiview/exceptions.py @@ -0,0 +1,57 @@ +from PyQt5.QtWidgets import QErrorMessage + +from voronoiview.mode import Mode + + +class ExceededWindowBoundsError(Exception): + pass + + +class InvalidStateError(Exception): + pass + + +class InvalidModeError(Exception): + """ + An exception to specify an invalid mode has been provided. + """ + + def __init__(self, mode): + """ + Initializes the InvalidMode exception with a + mode. + """ + + if not isinstance(mode, Mode): + raise ValueError('Mode argument to InvalidMode must be of ' + + ' type mode') + + # Mode cases for invalid mode + if mode == Mode.OFF: + super().__init__('You must select a mode before continuing.') + + +def handle_exceptions(func): + """ + A decorator designed to make exceptions thrown + from a function easier to handle. + + The result will be that all exceptions coming from + the decorated function will be caught and displayed + as a error message box. + + Usage: + + @handle_exceptions + def my_qt_func(): + raises SomeException + """ + def wrapped(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + error_dialog = QErrorMessage() + error_dialog.showMessage(str(e)) + error_dialog.exec_() + + return wrapped diff --git a/voronoiview/mode.py b/voronoiview/mode.py new file mode 100644 index 0000000..6a960f9 --- /dev/null +++ b/voronoiview/mode.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class Mode(Enum): + """ + Class to make it easier to figure out what mode + we are operating in when the OpenGL window is + clicked. + """ + OFF = 0 + ADD = 1 + EDIT = 2 + MOVE = 3 + DELETE = 4 + LOADED = 5 + VORONOI = 6 diff --git a/voronoiview/point_manager.py b/voronoiview/point_manager.py new file mode 100644 index 0000000..1d07612 --- /dev/null +++ b/voronoiview/point_manager.py @@ -0,0 +1,62 @@ +import json + +from voronoiview.colors import Color +from voronoiview.points import PointSet + + +class PointManager(): + """ + A state class that represents the absolute state of the + world in regards to points. + """ + + point_set = None + + # Stores the direct results of running scipy's voronoi function. + voronoi_results = None + + @staticmethod + def load(location): + """ + Loads the JSON file from the location and populates point_set + with it's contents. + + @param location The location of the JSON file. + """ + with open(location) as json_file: + data = json.load(json_file) + + PointManager.point_set = PointSet(data['point_size'], + data['viewport_width'], + data['viewport_height']) + + for point in data['points']: + # We will need to cast the string representation of color + # back into a Color enum. + PointManager.point_set.add_point(point['x'], point['y'], + Color(point['color']), point['weight']) + + @staticmethod + def save(location): + """ + Persists the point_set as a JSON file at location. + + @param location The persistence location. + """ + + data = {} + data['point_size'] = PointManager.point_set.point_size + data['viewport_width'] = PointManager.point_set.viewport_width + data['viewport_height'] = PointManager.point_set.viewport_height + data['points'] = [] + + for p in PointManager.point_set.points: + data['points'].append({ + 'x': p.x, + 'y': p.y, + 'color': p.color, + 'weight': p.weight + }) + + with open(location, 'w') as out_file: + json.dump(data, out_file) diff --git a/voronoiview/points.py b/voronoiview/points.py new file mode 100644 index 0000000..af10dca --- /dev/null +++ b/voronoiview/points.py @@ -0,0 +1,391 @@ +from math import floor + +from voronoiview.colors import Color +from voronoiview.exceptions import ExceededWindowBoundsError + + +class Point(): + """ + A class representing a point. A point + has a point_size bounding box around + it. + """ + + def __init__(self, x, y, color, point_size, + viewport_width, viewport_height, weight=1.0): + """ + Initializes a new point with a point_size bounding box, viewport + awareness, and a color. + + Initialized with additional viewport data to make sure the + move function refuses to move a point outside the screen. + + @param x The x-coordinate. + @param y The y-coordinate. + @param color The color of the point. + @param point_size The size of the point in pixels. + @param viewport_width The width of the viewport. + @param viewport_height The height of the viewport. + """ + + if not isinstance(color, Color): + raise ValueError("Point must be initialized with a color of " + + "type Color.") + + self._point_size = point_size + + # Unfortunately, it appears decorated property methods are not + # inheirited and instead of redoing everything we will just repeat + # the properties here. + self._x = x + self._y = y + self._weight = weight + + self._color = color + + self._viewport_width = viewport_width + self._viewport_height = viewport_height + + self._calculate_hitbox() + + self._check_window_bounds(x, y) + + self._selected = False + + self._attributes = [] + + @property + def x(self): + return self._x + + @property + def y(self): + return self._y + + @property + def array(self): + """ + Returns an array representation of the point for use in generating a voronoi diagram. + """ + return [self._x, self._y] + + @property + def weight(self): + return self._weight + + @weight.setter + def weight(self, weight): + self._weight = weight + + @property + def point_size(self): + return self._point_size + + @property + def selected(self): + return self._selected + + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + if not isinstance(color, Color): + raise ValueError('Point color must be of type Color.') + + self._color = color + + @property + def attributes(self): + return self._attributes + + def add_attribute(self, attr): + self._attributes.append(attr) + + def _calculate_hitbox(self): + """ + Calculates the hit box for the point given the current + position (center) and the point size. + """ + half_point = floor(self.point_size / 2.0) + + self._top_left_corner = (self._x - half_point, + self._y + half_point) + + self._bottom_right_corner = (self._x + half_point, + self._y - half_point) + + def _check_window_bounds(self, x, y): + """ + Simple window bound check that raises an exception when + the point (x, y) exceeds the known viewport bounds. + + @param x The x-coordinate under test. + @param y The y-coordinate under test. + @raises ExceededWindowBoundsError If the viewport bounds are exceeded. + """ + half_point = floor(self.point_size / 2.0) + + # Screen size in pixels is always positive + # We need to include the half point here because + # the (x, y) for a point is the center of the square and we + # do not want the EDGES to exceed the viewport bounds. + if (x > self._viewport_width - half_point or + y > self._viewport_height - half_point or + x < half_point or + y < half_point): + + raise ExceededWindowBoundsError + + def move(self, dx, dy): + """ + Adds the deltas dx and dy to the point. + + @param dx The delta in the x direction. + @param dy The delta in the y direction. + """ + + self._check_window_bounds(self._x + dx, self._y + dy) + + self._x += dx + self._y += dy + + # It's important to note as we move the point we need to + # make sure we are constantly updating it's hitbox. + self._calculate_hitbox() + + def __eq__(self, other): + """ + Override for class equality. + + @param other The other object. + """ + return (self._x == other.x and + self._y == other.y and + self._color == other.color and + self._attributes == other.attributes and + self._point_size == other.point_size) + + def __repr__(self): + + # For some reason I had to split this instead of using one giant + # string chained with `+` inside of `()`. + s = "= self._top_left_corner[0] and + x <= self._bottom_right_corner[0] and + y <= self._top_left_corner[1] and + y >= self._bottom_right_corner[1]) + + +class Attribute: + + def __init__(self, name, value): + """ + Initializes an attribute. + """ + self._name = name + self._value = value + + +class PointSet: + """ + Useful container for points. Since points are not hashable (they are + modified in place by move) we are forced to back the PointSet with an + array. However, it is still a "set" in the "uniqueness among all points" + sense because `add_point` will reject a point with a duplicate center. + """ + + def __init__(self, point_size, viewport_width, viewport_height): + """ + Initializes a point container with points of size point_size. + + @param point_size The size of the points. + @param viewport_width The width of the viewport for bounds + calculations. + @param viewport_height The height of the viewport for bounds + calculations. + """ + self._points = [] + self._point_size = point_size + self._viewport_width = viewport_width + self._viewport_height = viewport_height + + def __eq__(self, other): + other_points = list(other.points) + + return (self._points == other_points and + self._point_size == other.point_size and + self._viewport_width == other.viewport_width and + self._viewport_height == other.viewport_height) + + def __repr__(self): + s = [] + + for p in self._points: + s.append(str(p)) + + return ",".join(s) + + def clear(self): + self._points = [] + + @property + def points(self): + """ + Getter for points. Returns a generator for + looping. + """ + for point in self._points: + yield point + + def clear_points(self): + self._points = [] + + @property + def point_size(self): + return self._point_size + + @property + def viewport_height(self): + return self._viewport_height + + @property + def viewport_width(self): + return self._viewport_width + + @viewport_height.setter + def viewport_height(self, height): + self._viewport_height = height + + @viewport_width.setter + def viewport_width(self, width): + self._viewport_width = width + + def empty(self): + return len(self._points) == 0 + + def clear_selection(self): + """ + Handy helper function to clear all selected points. + """ + for p in self._points: + p.unselect() + + def add_point(self, x, y, color, weight=1.0, attrs=[]): + """ + Adds a point in screen coordinates and an optional attribute to + the list. + + @param x The x-coordinate. + @param y The y-coordinate. + @param color The color of the point. + @param weight The point weight. + @param attr An optional attribute. + @raises ExceededWindowBoundsError If the point could not be constructed + because it would be outside the + window bounds. + """ + + if attrs != [] and not all(isinstance(x, Attribute) for x in attrs): + raise ValueError("Attributes in add_point must be an " + + "attribute array.") + + if not isinstance(color, Color): + raise ValueError("Point color must be a Color enum.") + + if not isinstance(weight, float): + raise ValueError("Point weight must be a float.") + + point = Point(x, y, color, self._point_size, + self._viewport_width, self._viewport_height, weight) + + for attr in attrs: + point.add_attribute(attr) + + if point in self._points: + # Silently reject a duplicate point (same center). + return + + self._points.append(point) + + def remove_point(self, x, y): + """ + Removes a point from the point set based on a bounding + box calculation. + + Removing a point is an exercise is determining which points + have been hit, and then pulling them out of the list. + + If two points have a section overlapping, and the user clicks + the overlapped section, both points will be removed. + + Currently O(n). + + @param x The x-coordinate. + @param y The y-coordinate. + """ + for p in self._points: + if p.hit(x, y): + self._points.remove(p) + + def groups(self): + """ + Returns a map from color to point representing each point's group + membership based on color. + """ + g = {} + + for p in self._points: + if p.color not in g: + # Create the key for the group color since it does + # not exist. + g[p.color] = [] + + g[p.color].append(p) + + return g diff --git a/voronoiview/ui/__init__.py b/voronoiview/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/voronoiview/ui/mode_handlers.py b/voronoiview/ui/mode_handlers.py new file mode 100644 index 0000000..b43c1d1 --- /dev/null +++ b/voronoiview/ui/mode_handlers.py @@ -0,0 +1,378 @@ +import random + +from PyQt5.QtCore import QEvent, Qt +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QErrorMessage, QInputDialog +from scipy.spatial import Voronoi + +from voronoiview.colors import Color +from voronoiview.exceptions import ExceededWindowBoundsError +from voronoiview.mode import Mode +from voronoiview.ui.opengl_widget import (set_drawing_event, set_move_bb_top_left, + set_move_bb_bottom_right, reset_move_bbs, + viewport_height, viewport_width) +from voronoiview.point_manager import PointManager + + +class _ClickFlag: + + # This is the first stage. On mouse release it goes to + # SELECTION_MOVE. + NONE = 0 + + # We are now in selection box mode. + SELECTION_BOX = 1 + + # Second stage - we have selected a number of points + # and now we are going to track the left mouse button + # to translate those points. After a left click + # this moves to SELECTED_MOVED. + SELECTION_MOVE = 2 + + # Any subsequent click in this mode will send it back + # to NONE - we are done. + SELECTED_MOVED = 3 + + +# GLOBALS + +# Canvas pixel border - empirical, not sure where this is stored officially +_CANVAS_BORDER = 1 + +# Module level flag for left click events (used to detect a left +# click hold drag) +_left_click_flag = _ClickFlag.NONE + +# Variable to track the mouse state during selection movement +_last_mouse_pos = None + +# Used to implement mouse dragging when clicked +_left_click_down = False + +# TODO: WHEN THE GROUPING ENDS AND THE USER CANCELS THE CENTROID COUNT +# SHOULD BE ZEROED AND REMAINING COLORS SHOULD BE REPOPULATED. +# Count of centroids for comparison with the spin widget +_centroid_count = 0 +_remaining_colors = [c for c in Color if c not in [Color.BLUE, Color.GREY]] + + +def refresh_point_list(ctx): + """ + Refreshes the point list display. + + @param ctx A handle to the window context. + """ + # In order to make some guarantees and avoid duplicate + # data we will clear the point list widget and re-populate + # it using the current _point_set. + ctx.point_list_widget.clear() + + for p in PointManager.point_set.points: + ctx.point_list_widget.addItem(f"({p.x}, {p.y}) | Weight: {p.weight}") + + ctx.point_list_widget.update() + + num_of_points = len(list(PointManager.point_set.points)) + + ctx.number_of_points_label.setText(str(num_of_points)) + + +def _handle_add_point(ctx, event): + """ + Event handler for the add point mode. + + Sets the drawing mode for the OpenGL Widget using + `set_drawing_mode`, converts a point to our point + representation, and adds it to the list. + + @param ctx A context handle to the main window. + @param event The click event. + """ + + # Update information as needed + _handle_info_updates(ctx, event) + + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): + + # At this point we can be sure resize_gl has been called + # at least once, so set the viewport properties of the + # point set so it knows the canvas bounds. + PointManager.point_set.viewport_width = viewport_width() + PointManager.point_set.viewport_height = viewport_height() + + # Clear any existing selections + PointManager.point_set.clear_selection() + + try: + # No attribute at the moment, default point color is Color.GREY. + PointManager.point_set.add_point(event.x(), event.y(), Color.GREY) + except ExceededWindowBoundsError: + # The user tried to place a point whos edges would be + # on the outside of the window. We will just ignore it. + return + + refresh_point_list(ctx) + + set_drawing_event(event) + + ctx.opengl_widget.update() + ctx.point_list_widget.update() + + +def _handle_edit_point(ctx, event): + _handle_info_updates(ctx, event) + PointManager.point_set.clear_selection() + + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): + + # See if a point was hit + point = None + + for p in PointManager.point_set.points: + if p.hit(event.x(), event.y()): + point = p + break + + # Get point weight from user and assign it to the point. + if point is not None: + value, ok = QInputDialog.getDouble(None, 'Weight', 'Weight(Float): ', 1, 1, 3000, 1) + + if ok: + if not isinstance(value, float): + error_dialog = QErrorMessage() + error_dialog.showMessage('Point weight must be a floating point value.') + error_dialog.exec_() + + else: + point.weight = value + + # Store old x, y from event + set_drawing_event(event) + ctx.update() + refresh_point_list(ctx) + + +def ogl_keypress_handler(ctx, event): + """ + A keypress handler attached to the OpenGL widget. + + It primarily exists to allow the user to cancel selection. + + Also allows users to escape from modes. + + @param ctx A handle to the window context. + @param event The event associated with this handler. + """ + global _left_click_flag + global _last_mouse_pos + + if event.key() == Qt.Key_Escape: + if ctx.mode is Mode.MOVE: + if _left_click_flag is not _ClickFlag.NONE: + + _last_mouse_pos = None + + _left_click_flag = _ClickFlag.NONE + PointManager.point_set.clear_selection() + reset_move_bbs() + refresh_point_list(ctx) + + elif ctx.mode is not Mode.OFF: + ctx.mode = Mode.OFF + + # Also change the mouse back to normal + ctx.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) + ctx.status_bar.showMessage("") + + ctx.opengl_widget.update() + + +def _handle_move_points(ctx, event): + """ + A relatively complicated state machine that handles the process of + selection, clicking, and dragging. + + @param ctx The context to the window. + @param event The event. + """ + + global _left_click_flag + global _left_mouse_down + global _last_mouse_pos + + set_drawing_event(event) + + _handle_info_updates(ctx, event) + + # If we release the mouse, we want to quickly alert drag mode. + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonRelease): + + _left_mouse_down = False + + # This if statement block is used to set the bounding box for + # drawing and call the selection procedure. + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): + + _left_mouse_down = True + + if _left_click_flag is _ClickFlag.NONE: + _left_click_flag = _ClickFlag.SELECTION_BOX + + set_move_bb_top_left(event.x(), event.y()) + + elif (_left_click_flag is _ClickFlag.SELECTION_BOX + and _left_mouse_down): + # We are now in the click-and-hold to signal move + # tracking and translation + _left_click_flag = _ClickFlag.SELECTION_MOVE + _last_mouse_pos = (event.x(), event.y()) + + # Post-selection handlers + if (_left_click_flag is _ClickFlag.SELECTION_BOX + and event.type() == QEvent.MouseMove): + + set_move_bb_bottom_right(event.x(), event.y()) + + elif (_left_click_flag is _ClickFlag.SELECTION_MOVE + and _last_mouse_pos is not None + and _left_mouse_down + and event.type() == QEvent.MouseMove): + + dx = abs(_last_mouse_pos[0] - event.x()) + dy = abs(_last_mouse_pos[1] - event.y()) + + for p in PointManager.point_set.points: + if p.selected: + # Use the deltas to decide what direction to move. + # We only want to move in small unit increments. + # If we used the deltas directly the points would + # fly off screen quickly as we got farther from our + # start. + try: + if event.x() < _last_mouse_pos[0]: + p.move(-dx, 0) + if event.y() < _last_mouse_pos[1]: + p.move(0, -dy) + if event.x() > _last_mouse_pos[0]: + p.move(dx, 0) + if event.y() > _last_mouse_pos[1]: + p.move(0, dy) + + except ExceededWindowBoundsError: + # This point has indicated a move would exceed + # it's bounds, so we'll just go to the next + # point. + continue + + _last_mouse_pos = (event.x(), event.y()) + + elif (_left_click_flag is not _ClickFlag.NONE and + event.type() == QEvent.MouseButtonRelease): + + if _left_click_flag is _ClickFlag.SELECTION_BOX: + + set_move_bb_bottom_right(event.x(), event.y()) + + # Satisfy the post condition by resetting the bounding box + reset_move_bbs() + + ctx.opengl_widget.update() + + +def _handle_delete_point(ctx, event): + + _handle_info_updates(ctx, event) + + if (event.button() == Qt.LeftButton and + event.type() == QEvent.MouseButtonPress): + + set_drawing_event(event) + + PointManager.point_set.remove_point(event.x(), event.y()) + + refresh_point_list(ctx) + + ctx.opengl_widget.update() + ctx.point_list_widget.update() + + +def _handle_info_updates(ctx, event): + """ + Updates data under the "information" header. + + @param ctx The context to the main window. + @param event The event. + """ + if event.type() == QEvent.MouseMove: + ctx.mouse_position_label.setText(f"{event.x(), event.y()}") + + +def reset_colors(): + global _remaining_colors + + _remaining_colors = [c for c in Color if c not in [Color.BLUE, Color.GREY]] + + for point in PointManager.point_set.points: + point.color = Color.GREY + + +def generate_random_points(point_count, x_bound, y_bound): + """ + Using the random module of python generate a unique set of xs and ys + to use as points, bounded by the canvas edges. + + @param point_count The count of points to generate. + @param x_bound The width bound. + @param y_bound The height bound. + """ + + # TODO: The window size should be increased slightly to + # accomodate 3000 points (the maximum) given the point size. + # Work out an algorithm and limit the number of points + # selectable based on the maximum amount of points on the screen + # given the point size. + + # First clear the point set + PointManager.point_set.clear() + + point_size = PointManager.point_set.point_size + + # Sample without replacement so points are not duplicated. + xs = random.sample(range(point_size, x_bound), point_count) + + ys = random.sample(range(point_size, y_bound), point_count) + + points = list(zip(xs, ys)) + + for point in points: + PointManager.point_set.add_point(point[0], point[1], Color.GREY) + + +def _handle_voronoi(ctx, _): + + if not ctx.voronoi_solved: + points = list(PointManager.point_set.points) + + point_arr = [point.array for point in points] + + PointManager.voronoi_results = Voronoi(point_arr) + + ctx.opengl_widget.update() + ctx.voronoi_solved = True + + +# Simple dispatcher to make it easy to dispatch the right mode +# function when the OpenGL window is acted on. +MODE_HANDLER_MAP = { + Mode.OFF: _handle_info_updates, + Mode.LOADED: _handle_info_updates, + Mode.ADD: _handle_add_point, + Mode.EDIT: _handle_edit_point, + Mode.MOVE: _handle_move_points, + Mode.DELETE: _handle_delete_point, + Mode.VORONOI: _handle_voronoi +} diff --git a/voronoiview/ui/opengl_widget.py b/voronoiview/ui/opengl_widget.py new file mode 100644 index 0000000..db9ddc2 --- /dev/null +++ b/voronoiview/ui/opengl_widget.py @@ -0,0 +1,427 @@ +""" +This module defines functions that need to be overwritten +in order for OpenGL to work with the main window. This +module is named the same as the actual widget in order +to make namespacing consistent. + +To be clear, the actual widget is defined in the UI +generated code - `voronoiview_ui.py`. The functions +here are imported as overrides to the OpenGL functions of +that widget. + +It should be split up into a few more separate files eventually... +Probably even into it's own module folder. +""" + +import math +from typing import List + +from OpenGL.GL import (glBegin, glClearColor, glColor3f, glColor4f, + glEnable, glEnd, GL_LINES, GL_LINE_LOOP, GL_LINE_SMOOTH, + GL_POINTS, glPointSize, glVertex3f, + glViewport) + +from voronoiview.colors import Color, COLOR_TO_RGBA +from voronoiview.exceptions import (handle_exceptions, + InvalidStateError) +from voronoiview.mode import Mode +from voronoiview.point_manager import PointManager + +# Constants set based on the size of the window. +__BOTTOM_LEFT = (0, 0) +__WIDTH = None +__HEIGHT = None + +# State variables for a move selection bounding box. +# There are always reset to None after a selection has been made. +__move_bb_top_left = None +__move_bb_bottom_right = None + +# Module-global state variables for our drawing +# state machine. +# +# Below functions have to mark these as `global` so +# the interpreter knows that the variables are not +# function local. +__current_context = None +__current_event = None + + +# TODO: This should live inside of a class as static methods with the +# globals moved into the static scope to make this nicer...once you +# get it running before doing kmeans make this modification. + +def set_drawing_context(ctx): + """ + Sets the drawing context so that drawing functions can properly + interact with the widget. + """ + global __current_context + + __current_context = ctx + + +def set_drawing_event(event): + """ + State machine event management function. + + @param event The event. + """ + global __current_context + global __current_event + + if __current_context is None: + raise InvalidStateError('Drawing context must be set before setting ' + + 'drawing mode') + + if event is not None: + __current_event = event + + +def mouse_leave(ctx, event): + """ + The leave event for the OpenGL widget to properly reset the mouse + position label. + + @param ctx The context. + @param event The event. + """ + ctx.mouse_position_label.setText('') + + +def set_move_bb_top_left(x, y): + """ + Called to set the move bounding box's top left corner. + + @param x The x-coordinate. + @param y The y-coordinate. + """ + global __move_bb_top_left + + __move_bb_top_left = (x, y) + + +def set_move_bb_bottom_right(x, y): + """ + Called to set the move bounding box's bottom right corner. + + @param x The x-coordinate. + @param y The y-coordinate. + """ + global __move_bb_bottom_right + + __move_bb_bottom_right = (x, y) + + +def get_bb_top_left(): + return __move_bb_top_left + + +def get_bb_bottom_right(): + return __move_bb_bottom_right + + +def reset_move_bbs(): + global __move_bb_top_left + global __move_bb_bottom_right + + __move_bb_top_left = None + __move_bb_bottom_right = None + + +def initialize_gl(): + """ + Initializes the OpenGL context on the Window. + """ + + # Set white background + glClearColor(255, 255, 255, 0) + + +def resize_gl(w, h): + """ + OpenGL resize handler used to get the current viewport size. + + @param w The new width. + @param h The new height. + """ + global __WIDTH + global __HEIGHT + + __WIDTH = __current_context.opengl_widget.width() + __HEIGHT = __current_context.opengl_widget.height() + + +def viewport_width(): + return __WIDTH + + +def viewport_height(): + return __HEIGHT + + +@handle_exceptions +def paint_gl(): + """ + Stock PaintGL function from OpenGL that switches + on the current mode to determine what action to + perform on the current event. + """ + if(__current_context.mode is Mode.OFF and + not PointManager.point_set.empty()): + + # We want to redraw on any change to Mode.OFF so points are preserved - + # without this, any switch to Mode.OFF will cause a blank screen to + # render. + draw_points(PointManager.point_set) + + if (__current_context.mode in [Mode.ADD, Mode.EDIT, + Mode.MOVE, Mode.DELETE] and + __current_event is None and PointManager.point_set.empty()): + return + + if (__current_context.mode in [Mode.ADD, Mode.EDIT, Mode.DELETE] and + PointManager.point_set.empty()): + return + + if (__current_context.mode is Mode.ADD or + __current_context.mode is Mode.DELETE or + __current_context.mode is Mode.EDIT or + __current_context.mode is Mode.LOADED or + __current_context.mode is Mode.VORONOI): + + draw_points(PointManager.point_set) + + if (__current_context.mode is Mode.VORONOI and + __current_context.voronoi_solved): + + draw_voronoi_diagram() + + elif __current_context.mode is Mode.MOVE: + # We have to repeatedly draw the points while we are showing the + # move box. + if not PointManager.point_set.empty(): + draw_points(PointManager.point_set) + + draw_selection_box(Color.BLACK) + + if (__move_bb_top_left is not None and + __move_bb_bottom_right is not None): + + # Mark points that are selected in the bounding box + # and draw them using the normal function + highlight_selection() + draw_points(PointManager.point_set) + + +def __clamp_x(x): + """ + X-coordinate clamping function that goes from mouse coordinates to + OpenGL coordinates. + + @param x The x-coordinate to clamp. + @returns The clamped x coordinate. + """ + x_w = (x / (__WIDTH / 2.0) - 1.0) + return x_w + + +def __clamp_y(y): + """ + Y-coordinate clamping function that goes from mouse coordinates to + OpenGL coordinates. + + @param y The y-coordinate to clamp. + @returns The clamped y coordinate. + """ + y_w = -1.0 * (y / (__HEIGHT / 2.0) - 1.0) + return y_w + + +def box_hit(tx, ty, x1, y1, x2, y2): + """ + Calculates whether or not a given point collides with the given bounding + box. + + @param tx The target x. + @param ty The target y. + @param x1 The top left x. + @param y1 The top left y. + @param x2 The bottom left x. + @param y2 The bottom left y. + """ + + # The box in this case is flipped - the user started at the bottom right + # corner. Pixel-wise top left is (0, 0) and bottom right is + # (screen_x, screen_y) + if x1 > x2 and y1 > y2: + return (tx <= x1 and + tx >= x2 and + ty <= y1 and + ty >= y2) + + # The box in this case started from the top right + if x1 > x2 and y1 < y2: + return (tx <= x1 and + tx >= x2 and + ty >= y1 and + ty <= y2) + + # The box in this case started from the bottom left + if x1 < x2 and y1 > y2: + return (tx >= x1 and + tx <= x2 and + ty <= y1 and + ty >= y2) + + # Normal condition: Box starts from the top left + return (tx >= x1 and + tx <= x2 and + ty >= y1 and + ty <= y2) + + +def highlight_selection(): + """ + Given the current move bounding box, highlights any points inside it. + """ + + top_left = get_bb_top_left() + bottom_right = get_bb_bottom_right() + + for point in PointManager.point_set.points: + if box_hit(point.x, point.y, top_left[0], top_left[1], + bottom_right[0], bottom_right[1]): + + point.select() + else: + point.unselect() + + +def draw_selection_box(color): + """ + When the move bounding box state is populated and the mode is set + to MODE.Move this function will draw the selection bounding box. + + @param color The color Enum. + """ + global __current_context + + if __current_context is None: + raise InvalidStateError('Drawing context must be set before setting ' + + 'drawing mode') + + if not isinstance(color, Color): + raise ValueError('Color must exist in the Color enumeration') + + if __move_bb_top_left is None or __move_bb_bottom_right is None: + # Nothing to draw. + return + + ct = COLOR_TO_RGBA[color] + + glViewport(0, 0, __WIDTH, __HEIGHT) + + # Top right corner has the same x as the bottom right + # and same y as the top left. + top_right_corner = (__move_bb_bottom_right[0], __move_bb_top_left[1]) + + # Bottom left corner has the same x as the top left and + # same y as the bottom right. + bottom_left_corner = (__move_bb_top_left[0], __move_bb_bottom_right[1]) + + glBegin(GL_LINE_LOOP) + glColor3f(ct[0], ct[1], ct[2]) + + glVertex3f(__clamp_x(__move_bb_top_left[0]), + __clamp_y(__move_bb_top_left[1]), + 0.0) + + glVertex3f(__clamp_x(top_right_corner[0]), + __clamp_y(top_right_corner[1]), + 0.0) + + glVertex3f(__clamp_x(__move_bb_bottom_right[0]), + __clamp_y(__move_bb_bottom_right[1]), + 0.0) + + glVertex3f(__clamp_x(bottom_left_corner[0]), + __clamp_y(bottom_left_corner[1]), + 0.0) + + glEnd() + + +def clear_selection(): + """ + A helper designed to be called from the main window + in order to clear the selection internal to the graphics + and mode files. This way you dont have to do something + before the selection clears. + """ + if not PointManager.point_set.empty(): + PointManager.point_set.clear_selection() + + +def draw_points(point_set): + """ + Simple point drawing function. + + Given a coordinate (x, y), and a Color enum this + function will draw the given point with the given + color. + + @param point_set The PointSet to draw. + @param color The Color Enum. + """ + global __current_context + + if __current_context is None: + raise InvalidStateError('Drawing context must be set before setting ' + + 'drawing mode') + + glViewport(0, 0, __WIDTH, __HEIGHT) + glPointSize(PointManager.point_set.point_size) + + glBegin(GL_POINTS) + for point in point_set.points: + + if point.selected: + blue = COLOR_TO_RGBA[Color.BLUE] + glColor3f(blue[0], blue[1], blue[2]) + else: + ct = COLOR_TO_RGBA[point.color] + glColor3f(ct[0], ct[1], ct[2]) + + glVertex3f(__clamp_x(point.x), + __clamp_y(point.y), + 0.0) # Z is currently fixed to 0 + glEnd() + + +def draw_voronoi_diagram(): + """ + Draws the voronoi regions to the screen. Uses the global point manager to draw the points. + """ + + results = PointManager.voronoi_results + + vertices = results.vertices + + color = COLOR_TO_RGBA[Color.BLACK] + + for region_indices in results.regions: + # The region index is out of bounds + if -1 in region_indices: + continue + + glBegin(GL_LINE_LOOP) + for idx in region_indices: + vertex = vertices[idx] + + glColor3f(color[0], color[1], color[2]) + glVertex3f(__clamp_x(vertex[0]), + __clamp_y(vertex[1]), + 0.0) # Z is currently fixed to 0 + + glEnd() diff --git a/voronoiview/ui/point_list_widget.py b/voronoiview/ui/point_list_widget.py new file mode 100644 index 0000000..4504ad2 --- /dev/null +++ b/voronoiview/ui/point_list_widget.py @@ -0,0 +1,55 @@ +""" +Similar to the opengl_widget module, this module defines +helper functions for the point_list_widget. It is named +the same for convenience. The actual point_list_widget +is defined in the voronoiview_ui.py file. +""" + +from voronoiview.point_manager import PointManager + + +def _string_point_to_point(str_point): + """ + In the QListWidget points are stored as strings + because of the way Qt has list items defined. + + @param str_point The string of the form (x, y) to convert. + """ + + # 1. Split + point_side = str_point.split("|")[0] # First element is the point + point_side = point_side.strip() + elems = point_side.split(",") + + # 2. Take elements "(x" and "y)" and remove their first and + # last characters, respectively. Note that for y this + # function expects there to be a space after the comma. + x = elems[0][1:] + y = elems[1][1:-1] + + return (int(x), int(y)) + + +def item_click_handler(ctx, item): + """ + Handles an item becoming clicked in the list. + + This function is designed to be partially applied with the + main_window context in order to be able to trigger an opengl_widget + refresh. + + @param ctx The context. + @param item The clicked item. + """ + point = _string_point_to_point(item.text()) + + # TODO: Super slow linear search, should write a find_point function + # on the point_set in order to speed this up since PointSet + # is backed by a set anyway. + for p in PointManager.point_set.points: + if p.x == point[0] and p.y == point[1]: + p.select() + else: + p.unselect() + + ctx.opengl_widget.update() diff --git a/voronoiview_ui.py b/voronoiview_ui.py new file mode 100644 index 0000000..9eee756 --- /dev/null +++ b/voronoiview_ui.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'voronoiview.ui' +# +# Created by: PyQt5 UI code generator 5.13.0 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1280, 720) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) + MainWindow.setSizePolicy(sizePolicy) + MainWindow.setMinimumSize(QtCore.QSize(1280, 720)) + MainWindow.setMaximumSize(QtCore.QSize(1280, 720)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.opengl_widget = QtWidgets.QOpenGLWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.opengl_widget.sizePolicy().hasHeightForWidth()) + self.opengl_widget.setSizePolicy(sizePolicy) + self.opengl_widget.setMaximumSize(QtCore.QSize(900, 16777215)) + self.opengl_widget.setObjectName("opengl_widget") + self.horizontalLayout.addWidget(self.opengl_widget) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.groupBox = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.groupBox.setMinimumSize(QtCore.QSize(100, 0)) + self.groupBox.setMaximumSize(QtCore.QSize(200, 200)) + self.groupBox.setObjectName("groupBox") + self.gridLayout = QtWidgets.QGridLayout(self.groupBox) + self.gridLayout.setObjectName("gridLayout") + self.point_list_widget = QtWidgets.QListWidget(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.point_list_widget.sizePolicy().hasHeightForWidth()) + self.point_list_widget.setSizePolicy(sizePolicy) + self.point_list_widget.setMinimumSize(QtCore.QSize(100, 0)) + self.point_list_widget.setObjectName("point_list_widget") + self.gridLayout.addWidget(self.point_list_widget, 0, 0, 1, 1) + self.verticalLayout.addWidget(self.groupBox) + self.groupBox_3 = QtWidgets.QGroupBox(self.centralwidget) + self.groupBox_3.setObjectName("groupBox_3") + self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) + self.formLayout.setObjectName("formLayout") + self.voronoi_button = QtWidgets.QPushButton(self.groupBox_3) + self.voronoi_button.setEnabled(False) + self.voronoi_button.setObjectName("voronoi_button") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.voronoi_button) + self.reset_button = QtWidgets.QPushButton(self.groupBox_3) + self.reset_button.setObjectName("reset_button") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.reset_button) + self.verticalLayout.addWidget(self.groupBox_3) + spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + self.verticalLayout.addItem(spacerItem) + self.groupBox_2 = QtWidgets.QGroupBox(self.centralwidget) + self.groupBox_2.setObjectName("groupBox_2") + self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2) + self.gridLayout_2.setObjectName("gridLayout_2") + self.label = QtWidgets.QLabel(self.groupBox_2) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem1, 0, 2, 1, 1) + self.label_3 = QtWidgets.QLabel(self.groupBox_2) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_2.addItem(spacerItem2, 3, 0, 1, 1) + self.mouse_position_label = QtWidgets.QLabel(self.groupBox_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.mouse_position_label.sizePolicy().hasHeightForWidth()) + self.mouse_position_label.setSizePolicy(sizePolicy) + self.mouse_position_label.setMinimumSize(QtCore.QSize(100, 0)) + self.mouse_position_label.setText("") + self.mouse_position_label.setObjectName("mouse_position_label") + self.gridLayout_2.addWidget(self.mouse_position_label, 0, 3, 1, 1) + spacerItem3 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem3, 1, 2, 1, 1) + self.number_of_points_label = QtWidgets.QLabel(self.groupBox_2) + self.number_of_points_label.setText("") + self.number_of_points_label.setObjectName("number_of_points_label") + self.gridLayout_2.addWidget(self.number_of_points_label, 1, 3, 1, 1) + self.verticalLayout.addWidget(self.groupBox_2) + self.horizontalLayout.addLayout(self.verticalLayout) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 22)) + self.menubar.setNativeMenuBar(True) + self.menubar.setObjectName("menubar") + self.menu_file = QtWidgets.QMenu(self.menubar) + self.menu_file.setObjectName("menu_file") + self.menu_help = QtWidgets.QMenu(self.menubar) + self.menu_help.setObjectName("menu_help") + MainWindow.setMenuBar(self.menubar) + self.status_bar = QtWidgets.QStatusBar(MainWindow) + self.status_bar.setObjectName("status_bar") + MainWindow.setStatusBar(self.status_bar) + self.tool_bar = QtWidgets.QToolBar(MainWindow) + self.tool_bar.setMovable(False) + self.tool_bar.setObjectName("tool_bar") + MainWindow.addToolBar(QtCore.Qt.LeftToolBarArea, self.tool_bar) + self.action_add_points = QtWidgets.QAction(MainWindow) + self.action_add_points.setObjectName("action_add_points") + self.action_edit_points = QtWidgets.QAction(MainWindow) + self.action_edit_points.setObjectName("action_edit_points") + self.action_delete_points = QtWidgets.QAction(MainWindow) + self.action_delete_points.setObjectName("action_delete_points") + self.action_solve = QtWidgets.QAction(MainWindow) + self.action_solve.setObjectName("action_solve") + self.action_move_points = QtWidgets.QAction(MainWindow) + self.action_move_points.setObjectName("action_move_points") + self.action_save_point_configuration = QtWidgets.QAction(MainWindow) + self.action_save_point_configuration.setObjectName("action_save_point_configuration") + self.action_load_point_configuration = QtWidgets.QAction(MainWindow) + self.action_load_point_configuration.setObjectName("action_load_point_configuration") + self.action_exit = QtWidgets.QAction(MainWindow) + self.action_exit.setObjectName("action_exit") + self.action_generate_random_points = QtWidgets.QAction(MainWindow) + self.action_generate_random_points.setObjectName("action_generate_random_points") + self.action_clear_canvas = QtWidgets.QAction(MainWindow) + self.action_clear_canvas.setObjectName("action_clear_canvas") + self.menu_file.addAction(self.action_load_point_configuration) + self.menu_file.addAction(self.action_save_point_configuration) + self.menu_file.addSeparator() + self.menu_file.addAction(self.action_exit) + self.menubar.addAction(self.menu_file.menuAction()) + self.menubar.addAction(self.menu_help.menuAction()) + self.tool_bar.addAction(self.action_generate_random_points) + self.tool_bar.addAction(self.action_add_points) + self.tool_bar.addAction(self.action_move_points) + self.tool_bar.addAction(self.action_edit_points) + self.tool_bar.addAction(self.action_delete_points) + self.tool_bar.addSeparator() + self.tool_bar.addAction(self.action_clear_canvas) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Voronoi View")) + self.groupBox.setTitle(_translate("MainWindow", "Point List")) + self.groupBox_3.setTitle(_translate("MainWindow", "Solver")) + self.voronoi_button.setText(_translate("MainWindow", "Generate Voronoi Diagram")) + self.reset_button.setText(_translate("MainWindow", "Reset")) + self.groupBox_2.setTitle(_translate("MainWindow", "Canvas Information")) + self.label.setText(_translate("MainWindow", "Mouse Position:")) + self.label_3.setText(_translate("MainWindow", "Number of Points:")) + self.menu_file.setTitle(_translate("MainWindow", "File")) + self.menu_help.setTitle(_translate("MainWindow", "Help")) + self.tool_bar.setWindowTitle(_translate("MainWindow", "toolBar")) + self.action_add_points.setText(_translate("MainWindow", "Add Points")) + self.action_add_points.setToolTip(_translate("MainWindow", "Enables point adding mode.")) + self.action_add_points.setShortcut(_translate("MainWindow", "Ctrl+A")) + self.action_edit_points.setText(_translate("MainWindow", "Edit Points")) + self.action_edit_points.setToolTip(_translate("MainWindow", "Enables point editing mode.")) + self.action_edit_points.setShortcut(_translate("MainWindow", "Ctrl+E")) + self.action_delete_points.setText(_translate("MainWindow", "Delete Points")) + self.action_delete_points.setToolTip(_translate("MainWindow", "Enables point deletion mode.")) + self.action_delete_points.setShortcut(_translate("MainWindow", "Ctrl+D")) + self.action_solve.setText(_translate("MainWindow", "Solve")) + self.action_solve.setToolTip(_translate("MainWindow", "Opens the solve dialog to choose a solving solution.")) + self.action_solve.setShortcut(_translate("MainWindow", "Ctrl+S")) + self.action_move_points.setText(_translate("MainWindow", "Move Points")) + self.action_move_points.setToolTip(_translate("MainWindow", "Enables the movement of a selection of points.")) + self.action_save_point_configuration.setText(_translate("MainWindow", "Save Point Configuration")) + self.action_load_point_configuration.setText(_translate("MainWindow", "Load Point Configuration")) + self.action_exit.setText(_translate("MainWindow", "Exit")) + self.action_generate_random_points.setText(_translate("MainWindow", "Generate Random Points")) + self.action_clear_canvas.setText(_translate("MainWindow", "Clear Canvas"))