Taylor Bockman
5 years ago
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. |
||||
|
||||
![](example.PNG) |
||||
|
||||
## Usage |
||||
|
||||
First install the necessary packages: |
||||
|
||||
`pip install -r requirements.txt` |
||||
|
||||
Then launch Voronoi View using: |
||||
|
||||
`python voronoiview.py` |
||||
|
||||
from the root directory. |
||||
|
||||
## Development |
||||
|
||||
Make sure to install the development requirements using `pip install -r requirements-dev.txt`. This will install |
||||
all main requirements as well as useful testing and linting tools. |
||||
|
||||
### Regenerating the UI |
||||
|
||||
After modifying the `*.ui` file in Qt Designer run |
||||
|
||||
`pyuic5 voronoiview.ui -o voronoiview.py` |
||||
|
||||
to regenerate the UI python file. |
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