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