A Voronoi Diagram viewer with an editable canvas.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

427 lines
11 KiB

"""
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()