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