commit
					336ab6dc13
				
				 22 changed files with 2409 additions and 0 deletions
			
			
		| @ -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 | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | { | ||||||
|  |     "python.pythonPath": "venv/bin/python3.7", | ||||||
|  |     "python.linting.flake8Enabled": true, | ||||||
|  |     "python.linting.mypyEnabled": true | ||||||
|  | } | ||||||
| @ -0,0 +1,30 @@ | |||||||
|  | # Voronoi View | ||||||
|  | 
 | ||||||
|  | Some simple software to explore the generation of Voronoi Diagrams with a drawable canvas. | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
|  | ## 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. | ||||||
| After Width: | Height: | Size: 92 KiB | 
| @ -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) | ||||||
| @ -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 | ||||||
| @ -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 | ||||||
| @ -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() | ||||||
| @ -0,0 +1,368 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <ui version="4.0"> | ||||||
|  |  <class>MainWindow</class> | ||||||
|  |  <widget class="QMainWindow" name="MainWindow"> | ||||||
|  |   <property name="geometry"> | ||||||
|  |    <rect> | ||||||
|  |     <x>0</x> | ||||||
|  |     <y>0</y> | ||||||
|  |     <width>1280</width> | ||||||
|  |     <height>720</height> | ||||||
|  |    </rect> | ||||||
|  |   </property> | ||||||
|  |   <property name="sizePolicy"> | ||||||
|  |    <sizepolicy hsizetype="Maximum" vsizetype="Minimum"> | ||||||
|  |     <horstretch>0</horstretch> | ||||||
|  |     <verstretch>0</verstretch> | ||||||
|  |    </sizepolicy> | ||||||
|  |   </property> | ||||||
|  |   <property name="minimumSize"> | ||||||
|  |    <size> | ||||||
|  |     <width>1280</width> | ||||||
|  |     <height>720</height> | ||||||
|  |    </size> | ||||||
|  |   </property> | ||||||
|  |   <property name="maximumSize"> | ||||||
|  |    <size> | ||||||
|  |     <width>1280</width> | ||||||
|  |     <height>720</height> | ||||||
|  |    </size> | ||||||
|  |   </property> | ||||||
|  |   <property name="windowTitle"> | ||||||
|  |    <string>Voronoi View</string> | ||||||
|  |   </property> | ||||||
|  |   <widget class="QWidget" name="centralwidget"> | ||||||
|  |    <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|  |     <item> | ||||||
|  |      <widget class="QOpenGLWidget" name="opengl_widget"> | ||||||
|  |       <property name="sizePolicy"> | ||||||
|  |        <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> | ||||||
|  |         <horstretch>0</horstretch> | ||||||
|  |         <verstretch>0</verstretch> | ||||||
|  |        </sizepolicy> | ||||||
|  |       </property> | ||||||
|  |       <property name="maximumSize"> | ||||||
|  |        <size> | ||||||
|  |         <width>900</width> | ||||||
|  |         <height>16777215</height> | ||||||
|  |        </size> | ||||||
|  |       </property> | ||||||
|  |      </widget> | ||||||
|  |     </item> | ||||||
|  |     <item> | ||||||
|  |      <layout class="QVBoxLayout" name="verticalLayout"> | ||||||
|  |       <item> | ||||||
|  |        <widget class="QGroupBox" name="groupBox"> | ||||||
|  |         <property name="sizePolicy"> | ||||||
|  |          <sizepolicy hsizetype="Maximum" vsizetype="Minimum"> | ||||||
|  |           <horstretch>0</horstretch> | ||||||
|  |           <verstretch>0</verstretch> | ||||||
|  |          </sizepolicy> | ||||||
|  |         </property> | ||||||
|  |         <property name="minimumSize"> | ||||||
|  |          <size> | ||||||
|  |           <width>100</width> | ||||||
|  |           <height>0</height> | ||||||
|  |          </size> | ||||||
|  |         </property> | ||||||
|  |         <property name="maximumSize"> | ||||||
|  |          <size> | ||||||
|  |           <width>200</width> | ||||||
|  |           <height>200</height> | ||||||
|  |          </size> | ||||||
|  |         </property> | ||||||
|  |         <property name="title"> | ||||||
|  |          <string>Point List</string> | ||||||
|  |         </property> | ||||||
|  |         <layout class="QGridLayout" name="gridLayout"> | ||||||
|  |          <item row="0" column="0"> | ||||||
|  |           <widget class="QListWidget" name="point_list_widget"> | ||||||
|  |            <property name="sizePolicy"> | ||||||
|  |             <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> | ||||||
|  |              <horstretch>0</horstretch> | ||||||
|  |              <verstretch>0</verstretch> | ||||||
|  |             </sizepolicy> | ||||||
|  |            </property> | ||||||
|  |            <property name="minimumSize"> | ||||||
|  |             <size> | ||||||
|  |              <width>100</width> | ||||||
|  |              <height>0</height> | ||||||
|  |             </size> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |         </layout> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |        <widget class="QGroupBox" name="groupBox_3"> | ||||||
|  |         <property name="title"> | ||||||
|  |          <string>Solver</string> | ||||||
|  |         </property> | ||||||
|  |         <layout class="QFormLayout" name="formLayout"> | ||||||
|  |          <item row="0" column="0"> | ||||||
|  |           <widget class="QPushButton" name="voronoi_button"> | ||||||
|  |            <property name="enabled"> | ||||||
|  |             <bool>false</bool> | ||||||
|  |            </property> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string>Generate Voronoi Diagram</string> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |          <item row="1" column="0"> | ||||||
|  |           <widget class="QPushButton" name="reset_button"> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string>Reset</string> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |         </layout> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |        <spacer name="verticalSpacer_2"> | ||||||
|  |         <property name="orientation"> | ||||||
|  |          <enum>Qt::Vertical</enum> | ||||||
|  |         </property> | ||||||
|  |         <property name="sizeType"> | ||||||
|  |          <enum>QSizePolicy::Fixed</enum> | ||||||
|  |         </property> | ||||||
|  |         <property name="sizeHint" stdset="0"> | ||||||
|  |          <size> | ||||||
|  |           <width>20</width> | ||||||
|  |           <height>20</height> | ||||||
|  |          </size> | ||||||
|  |         </property> | ||||||
|  |        </spacer> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |        <widget class="QGroupBox" name="groupBox_2"> | ||||||
|  |         <property name="title"> | ||||||
|  |          <string>Canvas Information</string> | ||||||
|  |         </property> | ||||||
|  |         <layout class="QGridLayout" name="gridLayout_2"> | ||||||
|  |          <item row="0" column="0"> | ||||||
|  |           <widget class="QLabel" name="label"> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string>Mouse Position:</string> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |          <item row="0" column="2"> | ||||||
|  |           <spacer name="horizontalSpacer"> | ||||||
|  |            <property name="orientation"> | ||||||
|  |             <enum>Qt::Horizontal</enum> | ||||||
|  |            </property> | ||||||
|  |            <property name="sizeType"> | ||||||
|  |             <enum>QSizePolicy::Fixed</enum> | ||||||
|  |            </property> | ||||||
|  |            <property name="sizeHint" stdset="0"> | ||||||
|  |             <size> | ||||||
|  |              <width>20</width> | ||||||
|  |              <height>20</height> | ||||||
|  |             </size> | ||||||
|  |            </property> | ||||||
|  |           </spacer> | ||||||
|  |          </item> | ||||||
|  |          <item row="1" column="0"> | ||||||
|  |           <widget class="QLabel" name="label_3"> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string>Number of Points:</string> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |          <item row="3" column="0"> | ||||||
|  |           <spacer name="verticalSpacer"> | ||||||
|  |            <property name="orientation"> | ||||||
|  |             <enum>Qt::Vertical</enum> | ||||||
|  |            </property> | ||||||
|  |            <property name="sizeHint" stdset="0"> | ||||||
|  |             <size> | ||||||
|  |              <width>20</width> | ||||||
|  |              <height>20</height> | ||||||
|  |             </size> | ||||||
|  |            </property> | ||||||
|  |           </spacer> | ||||||
|  |          </item> | ||||||
|  |          <item row="0" column="3"> | ||||||
|  |           <widget class="QLabel" name="mouse_position_label"> | ||||||
|  |            <property name="sizePolicy"> | ||||||
|  |             <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> | ||||||
|  |              <horstretch>0</horstretch> | ||||||
|  |              <verstretch>0</verstretch> | ||||||
|  |             </sizepolicy> | ||||||
|  |            </property> | ||||||
|  |            <property name="minimumSize"> | ||||||
|  |             <size> | ||||||
|  |              <width>100</width> | ||||||
|  |              <height>0</height> | ||||||
|  |             </size> | ||||||
|  |            </property> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string/> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |          <item row="1" column="2"> | ||||||
|  |           <spacer name="horizontalSpacer_2"> | ||||||
|  |            <property name="orientation"> | ||||||
|  |             <enum>Qt::Horizontal</enum> | ||||||
|  |            </property> | ||||||
|  |            <property name="sizeType"> | ||||||
|  |             <enum>QSizePolicy::Fixed</enum> | ||||||
|  |            </property> | ||||||
|  |            <property name="sizeHint" stdset="0"> | ||||||
|  |             <size> | ||||||
|  |              <width>20</width> | ||||||
|  |              <height>20</height> | ||||||
|  |             </size> | ||||||
|  |            </property> | ||||||
|  |           </spacer> | ||||||
|  |          </item> | ||||||
|  |          <item row="1" column="3"> | ||||||
|  |           <widget class="QLabel" name="number_of_points_label"> | ||||||
|  |            <property name="text"> | ||||||
|  |             <string/> | ||||||
|  |            </property> | ||||||
|  |           </widget> | ||||||
|  |          </item> | ||||||
|  |         </layout> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |      </layout> | ||||||
|  |     </item> | ||||||
|  |    </layout> | ||||||
|  |   </widget> | ||||||
|  |   <widget class="QMenuBar" name="menubar"> | ||||||
|  |    <property name="geometry"> | ||||||
|  |     <rect> | ||||||
|  |      <x>0</x> | ||||||
|  |      <y>0</y> | ||||||
|  |      <width>1280</width> | ||||||
|  |      <height>22</height> | ||||||
|  |     </rect> | ||||||
|  |    </property> | ||||||
|  |    <property name="nativeMenuBar"> | ||||||
|  |     <bool>true</bool> | ||||||
|  |    </property> | ||||||
|  |    <widget class="QMenu" name="menu_file"> | ||||||
|  |     <property name="title"> | ||||||
|  |      <string>File</string> | ||||||
|  |     </property> | ||||||
|  |     <addaction name="action_load_point_configuration"/> | ||||||
|  |     <addaction name="action_save_point_configuration"/> | ||||||
|  |     <addaction name="separator"/> | ||||||
|  |     <addaction name="action_exit"/> | ||||||
|  |    </widget> | ||||||
|  |    <widget class="QMenu" name="menu_help"> | ||||||
|  |     <property name="title"> | ||||||
|  |      <string>Help</string> | ||||||
|  |     </property> | ||||||
|  |    </widget> | ||||||
|  |    <addaction name="menu_file"/> | ||||||
|  |    <addaction name="menu_help"/> | ||||||
|  |   </widget> | ||||||
|  |   <widget class="QStatusBar" name="status_bar"/> | ||||||
|  |   <widget class="QToolBar" name="tool_bar"> | ||||||
|  |    <property name="windowTitle"> | ||||||
|  |     <string>toolBar</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="movable"> | ||||||
|  |     <bool>false</bool> | ||||||
|  |    </property> | ||||||
|  |    <attribute name="toolBarArea"> | ||||||
|  |     <enum>LeftToolBarArea</enum> | ||||||
|  |    </attribute> | ||||||
|  |    <attribute name="toolBarBreak"> | ||||||
|  |     <bool>false</bool> | ||||||
|  |    </attribute> | ||||||
|  |    <addaction name="action_generate_random_points"/> | ||||||
|  |    <addaction name="action_add_points"/> | ||||||
|  |    <addaction name="action_move_points"/> | ||||||
|  |    <addaction name="action_edit_points"/> | ||||||
|  |    <addaction name="action_delete_points"/> | ||||||
|  |    <addaction name="separator"/> | ||||||
|  |    <addaction name="action_clear_canvas"/> | ||||||
|  |   </widget> | ||||||
|  |   <action name="action_add_points"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Add Points</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="toolTip"> | ||||||
|  |     <string>Enables point adding mode.</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="shortcut"> | ||||||
|  |     <string>Ctrl+A</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_edit_points"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Edit Points</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="toolTip"> | ||||||
|  |     <string>Enables point editing mode.</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="shortcut"> | ||||||
|  |     <string>Ctrl+E</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_delete_points"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Delete Points</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="toolTip"> | ||||||
|  |     <string>Enables point deletion mode.</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="shortcut"> | ||||||
|  |     <string>Ctrl+D</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_solve"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Solve</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="toolTip"> | ||||||
|  |     <string>Opens the solve dialog to choose a solving solution.</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="shortcut"> | ||||||
|  |     <string>Ctrl+S</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_move_points"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Move Points</string> | ||||||
|  |    </property> | ||||||
|  |    <property name="toolTip"> | ||||||
|  |     <string>Enables the movement of a selection of points.</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_save_point_configuration"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Save Point Configuration</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_load_point_configuration"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Load Point Configuration</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_exit"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Exit</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_generate_random_points"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Generate Random Points</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_clear_canvas"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Clear Canvas</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |  </widget> | ||||||
|  |  <resources/> | ||||||
|  |  <connections/> | ||||||
|  | </ui> | ||||||
| @ -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) | ||||||
|  | } | ||||||
| @ -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() | ||||||
| @ -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 | ||||||
| @ -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 | ||||||
| @ -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) | ||||||
| @ -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 = "<POINT " | ||||||
|  |         s += f"X: {self._x} | Y: {self._y} | " | ||||||
|  |         s += f"SIZE: {self._point_size} | " | ||||||
|  |         s += f"COLOR: {self._color} | " | ||||||
|  |         s += f"WEIGHT: {self._weight} | " | ||||||
|  |         s += f"VIEWPORT_WIDTH: {self._viewport_width} | " | ||||||
|  |         s += f"VIEWPORT_HEIGHT: {self._viewport_height}" | ||||||
|  |         s += ">" | ||||||
|  | 
 | ||||||
|  |         return s | ||||||
|  | 
 | ||||||
|  |     def select(self): | ||||||
|  |         """ | ||||||
|  |         Selects the point. | ||||||
|  |         """ | ||||||
|  |         self._selected = True | ||||||
|  | 
 | ||||||
|  |     def unselect(self): | ||||||
|  |         """ | ||||||
|  |         Unselects the point. | ||||||
|  |         """ | ||||||
|  |         self._selected = False | ||||||
|  | 
 | ||||||
|  |     def hit(self, x, y): | ||||||
|  |         """ | ||||||
|  |         Determines if the point was hit inside of it's bounding box. | ||||||
|  | 
 | ||||||
|  |         The condition for hit is simple - consider the following | ||||||
|  |         bounding box: | ||||||
|  |                  ------------- | ||||||
|  |                 |              | | ||||||
|  |                 |    (x,y)     | | ||||||
|  |                 |              | | ||||||
|  |                  ------------- | ||||||
|  | 
 | ||||||
|  |         Where the clicked location is in the center. Then the top | ||||||
|  |         left corner is defined as (x - half_point_size, y + half_point_size) | ||||||
|  |         and the bottom corner is (x + half_point_size, y - half_point_size) | ||||||
|  | 
 | ||||||
|  |         So long as x and y are greater than the top left and less than the | ||||||
|  |         top right it is considered a hit. | ||||||
|  | 
 | ||||||
|  |         This function is necessary for properly deleting and selecting points. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         return (x >= 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 | ||||||
| @ -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 | ||||||
|  | } | ||||||
| @ -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() | ||||||
| @ -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() | ||||||
| @ -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")) | ||||||
					Loading…
					
					
				
		Reference in new issue