#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see http://www.gnu.org/licenses/.
##
"""Interactive OpenGL Canvas embedded in a Qt widget.
This module implements user interaction with the OpenGL canvas defined in
module :mod:`canvas`.
`QtCanvas` is a single interactive OpenGL canvas, while `MultiCanvas`
implements a dynamic array of multiple canvases.
"""
import math
import numpy as np
import pyformex as pf
from pyformex import arraytools as at
from pyformex.formex import Formex
from pyformex.mesh import Mesh
from pyformex.collection import Collection
from pyformex.coords import Coords
from pyformex.opengl import canvas
from pyformex.opengl.gl import GL
from pyformex.plugins import imagearray
from pyformex.gui import QtCore, QtGui, QtOpenGL, QtWidgets
from pyformex.gui import qtutils
from pyformex.gui import qtgl
from pyformex.gui.signals import Signal
from pyformex.gui import OpenGLWidget
# Some 2D vector operations
# We could do this with the general functions of coords.py,
# but that would be overkill for this simple 2D vectors
[docs]def dotpr(v, w):
"""Return the dot product of vectors v and w"""
return v[0]*w[0] + v[1]*w[1]
[docs]def length(v):
"""Return the length of the vector v"""
return math.sqrt(dotpr(v, v))
[docs]def projection(v, w):
"""Return the (signed) length of the projection of vector v on vector w."""
return dotpr(v, w)/length(w)
################# Constants for event handlers #########################
# keys
QKEY = QtCore.Qt.Key
ESC = QKEY.Key_Escape
RETURN = QKEY.Key_Return # Normal Enter
ENTER = QKEY.Key_Enter # Num Keypad Enter
# mouse actions
PRESS = 0
MOVE = 1
RELEASE = 2
# mouse buttons
QBUTTON = QtCore.Qt.MouseButton
LEFT = QBUTTON.LeftButton
MIDDLE = QBUTTON.MiddleButton
RIGHT = QBUTTON.RightButton
# modifiers
QMOD = QtCore.Qt.KeyboardModifier
NONE = QMOD.NoModifier
SHIFT = QMOD.ShiftModifier
CTRL = QMOD.ControlModifier
ALT = QMOD.AltModifier
META = QMOD.MetaModifier
ALLMODS = SHIFT | CTRL | ALT | META
_modifier = {
'NONE': NONE,
'SHIFT': SHIFT,
'CTRL': CTRL,
'ALT': ALT,
'META': META,
}
# mouse modifiers used during picking actions
_PICK_MOVE = [_modifier[i] for i in pf.cfg['gui/mouse_mod_move']]
_PICK_SET = _modifier[pf.cfg['gui/mouse_mod_set']]
_PICK_ADD = _modifier[pf.cfg['gui/mouse_mod_add']]
_PICK_REMOVE = _modifier[pf.cfg['gui/mouse_mod_remove']]
CURSORSHAPE = QtCore.Qt.CursorShape
################# Canvas Mouse Event Handler #########################
def custom_cursor(base):
cbfile = pf.cfg['icondir'] / base + '-cb.xpm'
cmfile = pf.cfg['icondir'] / base + '-cm.xpm'
cb = QtGui.QPixmap(cbfile)
cm = QtGui.QPixmap(cmfile)
return QtGui.QCursor(cb, cm)
# class CursorShapeHandler():
# """A class for handling the mouse cursor shape on the Canvas.
# """
# cursor_shape = {'default': QtCore.Qt.ArrowCursor,
# 'pick': QtCore.Qt.CrossCursor,
# 'busy': QtCore.Qt.BusyCursor,
# }
# custom_cursors = ['mouse-pick']
# def __init__(self, widget):
# """Create a CursorHandler for the specified widget."""
# self.widget = widget
# def setCursorShape(self, shape):
# """Set the cursor shape to shape"""
# if shape in custom_cursors:
# cursor = custom_cursor(shape),
# else:
# if shape not in QtCanvas.cursor_shape:
# shape = 'default'
# cursor = QtCanvas.cursor_shape[shape]
# self.setCursor(cursor)
# def setCursorShapeFromFunc(self, func):
# """Set the cursor shape to shape"""
# if func in [self.mouse_rectangle]:
# shape = 'mouse-pick'
# else:
# shape = 'default'
# self.setCursorShape(shape)
[docs]class MouseHandler():
"""A class for handling the mouse events on the Canvas.
mousefunc keeps track of the installed mouse functions.
For each combination of mouse button and modifier key we keep a list
of functions. Installing a function adds it at the start
of the list. The first of the list is the active function.
Reset pops the first off the list, making the next active.
"""
buttons = [None, LEFT, MIDDLE, RIGHT] # None is relevant for mouse tracking
modifiers = [NONE, SHIFT, CTRL, ALT, META]
cursor_shape = {'default': CURSORSHAPE.ArrowCursor,
'cross': CURSORSHAPE.CrossCursor,
'draw': CURSORSHAPE.CrossCursor,
'busy': CURSORSHAPE.BusyCursor,
}
custom_cursor_shape = {'pick': 'mouse-pick'}
def __init__(self, canvas):
self.canvas = canvas
self.mousefnc = {}
for button in MouseHandler.buttons:
self.mousefnc[button] = {}
for mod in MouseHandler.modifiers:
self.mousefnc[button][int(mod)] = []
def set(self, button, mod, func):
self.mousefnc[button][int(mod)].append(func)
def reset(self, button, mod):
try:
self.mousefnc[button][int(mod)].pop()
except IndexError:
pass
[docs] def get(self, button, mod):
"""Return the mouse function bound to button and mod"""
try:
return self.mousefnc[button][int(mod)][-1]
except IndexError:
return None
[docs] def setCursorShape(self, shape):
"""Set the cursor shape to shape"""
if shape in MouseHandler.custom_cursor_shape:
cursor = custom_cursor(MouseHandler.custom_cursor_shape[shape])
else:
if shape not in MouseHandler.cursor_shape:
shape = 'default'
cursor = MouseHandler.cursor_shape[shape]
self.canvas.setCursor(cursor)
if pf.debugon(pf.DEBUG.MOUSE):
print(f"{cursor=}")
################# Single Interactive OpenGL Canvas ###############
[docs]class QtCanvas(OpenGLWidget, canvas.Canvas):
"""A canvas for OpenGL rendering.
This class provides interactive functionality for the OpenGL canvas
provided by the :class:`canvas.Canvas` class.
Interactivity is highly dependent on Qt. Putting the interactive
functions in a separate class makes it esier to use the Canvas class
in non-interactive situations or combining it with other GUI toolsets.
The QtCanvas constructor may have positional and keyword arguments. The
positional arguments are passed to the QtOpenGL.QGLWidget constructor,
while the keyword arguments are passed to the canvas.Canvas constructor.
"""
_exclude_members_ = ['Communicate']
pick_filters = ['none', 'single', 'closest', 'connected', 'connected_',
'conn0', 'conn1', 'conn2', 'conn0_', 'conn1_', 'conn2_']
# private signal class
class Communicate(QtCore.QObject):
RECTANGLE = Signal()
CANCEL = Signal()
DONE = Signal()
def __init__(self, *args, **kargs):
"""Initialize an empty canvas."""
OpenGLWidget.__init__(self, *args)
if pf.debugon(pf.DEBUG.OPENGL):
fmt = qtgl.OpenGLFormat(self.format())
print(f"QtCanvas.__init__:\n{fmt}")
# TODO: In case of multisample, report the number of samples here
# Define our private signals
self.signals = self.Communicate()
self.setMinimumSize(32, 32)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
canvas.Canvas.__init__(self, **kargs)
self.mousehandler = MouseHandler(self)
# Initial mouse funcs are dynamic handling
# Also some modifier keys are bound to mouse movement operations
# These can be used during picking operations
for mod in set(_PICK_MOVE):
self.mousehandler.set(LEFT, mod, self.dynarot)
self.mousehandler.set(MIDDLE, mod, self.dynapan)
self.mousehandler.set(RIGHT, mod, self.dynazoom)
self.mousehandler.setCursorShape('default')
self.button = None
self.mod = NONE
self.dynamouse = True # dynamic mouse action works on mouse move
self.dynamic = None # what action on mouse move
self.pick_modes = ['actor', 'element', 'point']
# TODO: implement picking of 'face' and 'edge'
self.pick_tools = ['pix', 'any', 'all']
self.pick_mode = None
self.pick_tool = pf.cfg['draw/picktool']
self.pick_filter = 'none'
self.selection = Collection()
self.trackfunc = None
self.picked = None
self.pickable = None
self.drawmode = None
self.drawing_mode = None
self.drawing = None
self.events = []
# Drawing options
self.resetOptions()
# def setCursorShape(self, shape):
# """Set the cursor shape to shape"""
# if shape in MouseHandler.custom_cursor_shape:
# cursor = custom_cursor(MouseHandler.custom_cursor_shape[shape])
# else:
# if shape not in MouseHandler.cursor_shape:
# shape = 'default'
# cursor = MouseHandler.cursor_shape[shape]
# self.canvas.setCursor(cursor)
# def setCursorShapeFromFunc(self, func):
# """Set the cursor shape according to the specified function"""
# if func in [self.mouse_rectangle]:
# shape = 'pick' if self.canvas.pick_mode == 'point' else 'cross'
# elif func == self.mouse_draw:
# shape = 'draw'
# else:
# shape = 'default'
# self.mousehandler.setCursorShape(shape)
[docs] def getSize(self):
"""Return the size of this canvas"""
return qtutils.Size(self)
[docs] def saneSize(self, width=-1, height=-1):
"""Return a cleverly resized canvas size.
Computes a new size for the canvas, while trying to keep its
current aspect ratio. Specified positive values are returned
unchanged.
Parameters
----------
width: int
Requested width of the canvas. If <=0, it is automatically
computed from height and canvas aspect ratio, or set equal to
canvas width.
height: int
Requested height of the canvas. If <=0, it is automatically
computed from width and canvas aspect ratio, or set equal to
canvas height.
Returns
-------
width: int
Adjusted canvas width.
height: int
Adjusted canvas height.
"""
if width <= 0 or height <= 0:
wc, hc = self.getSize()
if height > 0:
width = round(float(height)/hc*wc)
elif width > 0:
height = round(float(width)/wc*hc)
else:
width, height = wc, hc
return width, height
# TODO: negative sizes should probably resize all viewports
# OR we need to implement frames
[docs] def changeSize(self, width, height):
"""Resize the canvas to (width x height).
If a negative value is given for either width or height,
the corresponding size is set equal to the maximum visible size
(the size of the central widget of the main window).
Note that this may not have the expected result when multiple
viewports are used.
"""
if width < 0 or height < 0:
w, h = pf.GUI.maxCanvasSize()
if width < 0:
width = w
if height < 0:
height = h
self.resize(width, height)
[docs] def image(self, *, resize=None, picking=None, remove_alpha=True):
"""Return the current OpenGL rendering in an image format.
Parameters
----------
resize: tuple of int, optional
A tuple (width, height) with the requested image size. If either
of these values is <= 0, it will be set from the other and the
canvas aspect ratio. If not provided or both values are <= 0,
the current canvas size will be used.
remove_alpha: bool
If True (default), the alpha channel is removed from the image.
Returns
-------
qim: QImage
The current OpenGL rendering as a QImage of the specified size.
Notes
-----
The returned image can be written directly to an image file with
``qim.save(filename)``.
See Also
--------
rgb: returns the canvas rendering as a numpy ndarray
"""
self.makeCurrent()
w, h = self.getSize()
if resize:
wc, hc = w, h
w, h = self.saneSize(*resize)
vcanvas = QtOpenGL.QGLFramebufferObject(
w, h, QtOpenGL.QGLFramebufferObject.Depth)
# With new FrameBufferObject
# vcanvas = QtGui.QOpenGLFramebufferObject(
# w, h, QtGui.QOpenGLFramebufferObject.Depth)
# print("IMAGE", vcanvas.attachment())
vcanvas.bind()
if resize:
self.resize(w, h)
if picking:
self.renderpick(picking)
else:
self.display()
self.glFinish()
qim = vcanvas.toImage()
vcanvas.release()
if resize:
self.resize(wc, hc)
self.glFinish()
del vcanvas
if picking:
# restore non-picking mode
self.picking = False
self.display()
self.update()
#imagearray.removeAlpha(qim).save('pick_original.png')
if remove_alpha:
qim = imagearray.removeAlpha(qim)
return qim
[docs] def rgb(self, resize=None, remove_alpha=True, picking=False):
"""Return the current OpenGL rendering in an array format.
Parameters
----------
resize: tuple of int, optional
A tuple (width, height) with the requested image size. If either
of these values is <= 0, it will be set from the other and the
canvas aspect ratio. If not provided or both values are <= 0,
the current canvas size will be used.
remove_alpha: bool
If True (default), the alpha channel is removed from the image.
picking: bool
This argument is for internal use only.
Returns
-------
ar: array
The current OpenGL rendering as a numpy array of type uint8.
Its shape is (w,h,3) if remove_alpha is True (default)
or (w,h,4) if remove_alpha is False.
See Also
--------
image: return the current rendering as an image
"""
qim = self.image(resize=resize, remove_alpha=False, picking=picking)
ar, cm = imagearray.qimage2numpy(qim)
if remove_alpha:
ar = ar[..., :3]
return ar
[docs] def split_pickids(self, ids, obj_type='element'):
"""Convert picked pixel ids to element Collection"""
K = Collection(obj_type=obj_type)
key = 0
for start, end in zip(self.pick_nitems[:-1], self.pick_nitems[1:]):
mine = (ids >= start) * (ids < end)
if obj_type == 'actor' and ids[mine].any():
K.add([key], -1)
elif obj_type == 'element':
K.add(ids[mine] - start, key)
else: # obj_type = 'point'
oids = ids[mine] - start
actor = self.actors[key]
if isinstance(actor.object, Formex):
oids = actor._translate_mesh_points_formex(oids)
K.add(oids, key)
key += 1
return K
[docs] def insideRect(self, rect=None, obj_type='element'):
"""Find collection of elements inside a rectangle"""
if rect is None:
rect = self.getRectangle()
x0, y0, x1, y1 = rect
h = self.height()
qim = self.image(picking=obj_type)
if pf.debugon(pf.DEBUG.PICK):
qimmy = qim.copy(x0+1, h-y1+1, x1-x0-1, y1-y0-1) # qt has y down
savefile = pf.preffile.parent / 'pick_debug.png'
imagearray.removeAlpha(qimmy).save(savefile)
crop = imagearray.qimage2numpy(qim, indexed=False)
if (x0, y0) == (x1, y1):
# No movement: pick pixel under mouse
crop = crop[y0, x0]
else:
x1, y1 = max(x1, x0+2), max(y1, y0+2)
crop = crop[y0+1:y1, x0+1:x1]
crop = crop.reshape(-1, 4)
uniq = at.uniqueRows(crop)
crop = crop[uniq]
ids = crop.view(np.uint32).reshape(-1)
return self.split_pickids(ids, obj_type=obj_type)
[docs] def outline(self, size=(0, 0), profile='lightness', level=0.5, bgcolor=None,
nproc=None):
"""Return the outline of the current rendering
Parameters
----------
size: tuple
A tuple of ints (w,h) specifying the size of the image to
be used in outline detection. A non-positive value will be set
automatically from the current canvas size or aspect ratio.
profile: callable
The function to be used to translate pixel colors into a single
value. The default is to use the lightness of the pixel color.
Also available: 'luminance'.
level: float
The isolevel at which to construct the outline.
bgcolor: color_like
A color that is to be interpreted as background color
and will get a pixel value -0.5. This is currently experimental.
nproc: int
The number of processors to be used in the image processing.
Default is to use as many as available.
Returns
-------
Formex:
The outline as a Formex of plexitude 2.
"""
from pyformex.plugins.isosurface import isoline
from pyformex.formex import Formex
from pyformex.color import luminance, lightness, Color
self.camera.lock()
w, h = self.saneSize(*size)
rgb = self.rgb((w, h)) / 255
shape = rgb.shape[:2]
data = luminance(rgb.reshape(-1, 3)).reshape(shape)
if profile == 'lightness':
data = lightness(data)
if bgcolor:
bgcolor = Color(bgcolor).RGB
print("bgcolor = %s" % (bgcolor,))
bg = (rgb == bgcolor).all(axis=-1)
print(bg)
data += 0.5
data[bg] = -0.5
rng = data.max() - data.min()
bbox = self.bbox
ctr = self.camera.project((bbox[0]+bbox[1])*.05)
axis = at.unitVector(self.camera.eye-self.camera.focus)
#
# NOTE: the + [0.5,0.5] should be checked and then be
# moved inside the isoline function!
#
seg = isoline(data, data.min()+level*rng, nproc=nproc) + [0.5, 0.5]
if size is not None:
wc, hc = self.getSize()
sx = float(wc)/w
sy = float(hc)/h
# print(f"Post scaling {sx}, {sy}")
seg[..., 0] *= sx
seg[..., 1] *= sy
X = Coords(seg).trl([0., 0., ctr[0][2]])
shape = X.shape
X = self.camera.unproject(X.reshape(-1, 3)).reshape(shape)
self.camera.unlock()
F = Formex(X)
F.attrib(axis=axis)
return F
####################### MOUSE RECTANGLE ############################
def clip_coords(self, x, y):
w, h = self.width(), self.height()
x = 0 if x < 0 else w if x > w else x
y = 0 if y < 0 else h if y > h else y
return x, y
[docs] def draw_state_line(self, x, y):
"""Store the pos and draw a rectangle to it."""
self.state = self.clip_coords(x, y)
canvas.drawLine(self.statex, self.statey, *self.state)
[docs] def draw_state_rect(self, x, y):
"""Store the pos and draw a line to it."""
self.state = self.clip_coords(x, y)
canvas.drawRect(self.statex, self.statey, *self.state)
[docs] def wait_interaction(self):
"""Wait for the user to finish some interaction."""
timer = QtCore.QThread
self.interaction_busy = True
while self.interaction_busy:
# This allows us to push mouse rectangle picking events
if self.events:
self.emit_events(self.events.pop(0))
timer.msleep(20)
pf.app.processEvents()
def start_rectangle(self, func=None):
self.rectangle = None
self.rectangle_func = func
self.mousehandler.set(LEFT, NONE, self.mouse_rectangle)
self.mousehandler.setCursorShape('cross')
if func is None:
func = self.finish_rectangle
self.signals.RECTANGLE.connect(func)
def finish_rectangle(self):
self.mousehandler.reset(LEFT, NONE)
self.mousehandler.setCursorShape('default')
self.update()
[docs] def mouse_rectangle(self, x, y, action):
"""Draw a rectangle during mouse move.
On PRESS, record the mouse position.
On MOVE, show a rectangle.
On RELEASE, store the picked rectangle and possibly execute a function
"""
if action == PRESS:
self.makeCurrent()
self.update()
if self.trackfunc:
self.camera.setTracking(True)
x, y, z = self.camera.focus
self.zplane = self.project(x, y, z, True)[2]
self.trackfunc(x, y, self.zplane)
self.begin_2D_drawing()
GL.glEnable(GL.GL_COLOR_LOGIC_OP)
GL.glLogicOp(GL.GL_INVERT) # An alternative is GL_XOR #
self.draw_state_rect(x, y) # Draw rectangle
self.swapBuffers()
elif action == MOVE:
if self.trackfunc:
self.trackfunc(x, y, self.zplane)
self.draw_state_rect(*self.state) # Remove old rectangle
self.draw_state_rect(x, y) # Draw new rectangle
self.swapBuffers()
elif action == RELEASE:
self.draw_state_rect(*self.state) # Remove old rectangle
GL.glDisable(GL.GL_COLOR_LOGIC_OP)
self.swapBuffers()
self.end_2D_drawing()
x0 = max(min(self.statex, x), 0)
y0 = max(min(self.statey, y), 0)
x1 = min(max(self.statex, x), self.width())
y1 = min(max(self.statey, y), self.height())
self.rectangle = x0, y0, x1, y1
self.interaction_busy = False
[docs] def mouse_line(self, x, y, action):
"""Draw a line during mouse move.
On PRESS, record the mouse position.
On MOVE, draw a line from initial to new position.
On RELEASE, store the final point
(self.statex, self.statey) is the start point
self.state is the current end point
"""
if action == PRESS:
self.makeCurrent()
self.update()
if self.trackfunc:
self.camera.setTracking(True)
x, y, z = self.camera.focus
self.zplane = self.project(x, y, z, True)[2]
self.trackfunc(x, y, self.zplane)
self.begin_2D_drawing()
GL.glEnable(GL.GL_COLOR_LOGIC_OP)
GL.glLogicOp(GL.GL_INVERT) # An alternative is GL_XOR #
self.draw_state_line(x, y) # Draw line
self.swapBuffers()
elif action == MOVE:
if self.trackfunc:
self.trackfunc(x, y, self.zplane)
self.draw_state_line(*self.state) # Remove oldline
self.draw_state_line(x, y) # Draw new line
self.swapBuffers()
elif action == RELEASE:
self.draw_state_line(*self.state) # Remove line
GL.glDisable(GL.GL_COLOR_LOGIC_OP)
self.swapBuffers()
self.end_2D_drawing()
self.drawn = self.unproject(x, y, self.zplane)
self.interaction_busy = False
[docs] def getRectangle(self, yup=True):
"""Let the user pick a rectangle.
Returns: x0, y0, x1, y1 where x0<x1, y0<y1
If yup is False, y values are downward
"""
self.start_rectangle()
self.wait_interaction()
self.finish_rectangle()
if yup:
return self.rectangle
else:
h = self.height()
x0, y0, x1, y1 = self.rectangle
return x0, h-y1, x1, h-y0
def zoom_rectangle(self):
self.zoomRectangle(*self.getRectangle())
####################### INTERACTIVE PICKING ############################
[docs] def start_pick_mode(self, mode):
"""Start an interactive picking mode.
If selection mode was already started, mode is disregarded and
this can be used to change the tool or filter.
"""
if pf.debugon(pf.DEBUG.PICK):
print(f"PICK: Start selection {mode=}")
self.pick_mode = mode
self.mousehandler.set(LEFT, NONE, self.mouse_rectangle)
self.mousehandler.set(LEFT, SHIFT, self.mouse_rectangle)
self.mousehandler.set(LEFT, CTRL, self.mouse_rectangle)
self.mousehandler.set(RIGHT, NONE, self.emit_done)
self.mousehandler.set(RIGHT, SHIFT, self.emit_cancel)
self.mousehandler.setCursorShape(
'pick' if self.pick_mode == 'point' else 'cross')
self.signals.DONE.connect(self.accept_selection)
self.signals.CANCEL.connect(self.cancel_selection)
self.selection.clear()
self.selection.obj_type = self.pick_mode
if pf.debugon(pf.DEBUG.PICK):
print(f"PICK started: {self.pick_mode=}, {self.selection}")
self.removeHighlight()
[docs] def finish_selection(self):
"""End interactive picking mode."""
if pf.debugon(pf.DEBUG.PICK):
print("Finish selection")
self.mousehandler.reset(LEFT, NONE)
self.mousehandler.reset(LEFT, SHIFT)
self.mousehandler.reset(LEFT, CTRL)
self.mousehandler.reset(RIGHT, NONE)
self.mousehandler.reset(RIGHT, SHIFT)
self.mousehandler.setCursorShape('default')
self.signals.DONE.disconnect(self.accept_selection)
self.signals.CANCEL.disconnect(self.cancel_selection)
self.pick_mode = None
self.pickable = None
[docs] def accept_selection(self, clear=False):
"""Accept or cancel an interactive picking mode.
If clear == True, the current selection is cleared.
"""
if pf.debugon(pf.DEBUG.PICK):
print("Accept selection")
self.selection_accepted = True
if clear:
self.selection.clear()
self.selection_accepted = False
self.pick_canceled = True
self.interaction_busy = False
[docs] def cancel_selection(self):
"""Cancel an interactive picking mode and clear the selection."""
self.accept_selection(clear=True)
def emit_events(self, events):
# pf.logger.debug("sending events %s" % events)
for event in events:
pf.app.sendEvent(self, event)
[docs] def pick_pixels(self):
"""Set the list of actor parts inside the pick_window.
This implements the 'pix' picking tool.
The picked object numbers are stored in self.picked.
"""
if pf.debugon(pf.DEBUG.PICK):
print(f"PICK_PIXELS {self.pick_mode}")
# With pick_pixels, pickable can only be set on the actors.
# TODO: Can we enforce a pickable list through the filters?
# if self.pickable is None:
# pickable = [a for a in self.actors if a.pickable]
# else:
# pickable = self.pickable
self.picked = self.insideRect(self.rectangle, self.pick_mode)
if pf.debugon(pf.DEBUG.PICK):
print(f"self.picked={self.picked}")
[docs] def pick_parts(self):
"""Set the list of actor parts inside the pick_window.
This implements the 'any' and 'all' picking tool.
The picked object numbers are stored in self.picked.
"""
if pf.debugon(pf.DEBUG.PICK):
print(f"PICK_PARTS {self.pick_mode=}, {self.pick_tool=}")
# Allow a different pickable list than the pickable actors.
# This is used in the draw2d plugin.
if self.pickable is None:
pickable = [a for a in self.actors if a.pickable]
else:
pickable = self.pickable
self.picked = Collection(self.pick_mode)
x0, y0, x1, y1 = self.rectangle
x, y = 0.5 * (x0 + x1), 0.5 * (y0 + y1)
w, h = x1 - x0, y1 - y0
if w <= 1 or h <= 1:
w, h = pf.cfg['draw/picksize']
vp = GL.glGetIntegerv(GL.GL_VIEWPORT)
self.pick_window = (x, y, w, h, vp)
if pf.debugon(pf.DEBUG.PICK):
print(f"{self.pick_window=}")
# Make sure we always return Actor index from self.actors
for i, a in enumerate(self.actors):
if a in pickable:
picked = a.inside(
self.camera, rect=self.pick_window[:4], mode=self.pick_mode,
sel=self.pick_tool, return_depth=False)
if self.pick_mode == 'actor':
if picked:
self.picked.add([i], key=-1)
else:
self.picked.add(picked, key=i)
[docs] def filter_closest(self, picked, single):
"""Narrow a Collection to item(s) closest to the camera plane
If single is True, only a single item is kept. Else all the items
on the actor with the closest item.
"""
if not picked:
return picked
imin = -1
jmin = None
dmin = None
if picked.obj_type == 'actor':
for i in picked[-1]:
o = self.actors[i].object
# we use normal toward objects to have positive distances
d = o.points().distanceFromPlane(
self.camera.eye, -self.camera.axis)
d = d.min()
if imin < 0 or d < dmin:
imin, dmin = i, d
picked.clear()
picked.add([imin], key=-1)
picked.depth = dmin
elif picked.obj_type == 'point':
for i in picked:
v = picked[i]
o = self.actors[i].object
d = o.points()[v].distanceFromPlane(
self.camera.eye, -self.camera.axis)
j = d.argmin()
if imin < 0 or d[j] < dmin:
imin, jmin, dmin = i, v[j], d[j]
picked.clear()
picked.add([jmin], key=imin)
picked.depth = dmin
elif picked.obj_type == 'element':
for i in picked:
v = picked[i]
o = self.actors[i].object
if isinstance(o, Formex):
X = o.coords[v]
elif isinstance(o, Mesh):
X = o.coords[o.elems[v]]
d = X.points().distanceFromPlane(
self.camera.eye, -self.camera.axis)
j = d.argmin()
k = j // X.shape[1]
if not single:
picked[i] = v[k:k+1]
if imin < 0 or d[j] < dmin:
imin, jmin, dmin = i, v[k], d[j]
if single:
picked.clear()
picked.add([jmin], key=imin)
picked.depth = dmin
[docs] def filter_connected(self, picked, level):
"""Narrow a Collection to the items connected to self.selection"""
if not picked or picked.obj_type != 'element':
return
if not self.selection:
# initialize selection to closest picked item
self.selection = picked.copy()
self.filter_closest(self.selection, single=self.single_actor)
for i in picked:
if i in self.selection:
o = self.actors[i].object
if isinstance(o, Mesh):
start = self.selection[i]
new = self.picked[i]
# TODO: should connectedElements always add start
# to test and return intersect? would make this simpler
test = np.union1d(start, new)
ok = o.connectedElements(start, test, level)
picked[i] = np.intersect1d(ok, new)
else:
del picked[i]
else:
del picked[i]
[docs] def modify_selection(self):
"""Modify the current selection.
This method is intended for use in the `func` of the :meth:`pick`
method, to update the selection after each atomic pick.
It modifies the selection depending on the used filters and on the
modifier key pressed when doing the pick. Default is:
- None: add to the selection
- SHIFT: set as the selection (forgetting previous picks)
- CTRL: remove from the selection
Without filter, all the items in the last pick are involved.
With a filter only a subset may be involved.
"""
if self.mod == _PICK_SET:
self.selection.set(self.picked)
elif self.mod == _PICK_ADD:
self.selection.add(self.picked)
elif self.mod == _PICK_REMOVE:
self.selection.remove(self.picked)
if self.pick_filter == 'single':
self.filter_closest(self.selection, single=True)
[docs] def modify_and_highlight(self):
"""Modify selection and highlight updated selection.
This method is the default `func` used in the pick method
after each atomic pick. It modifies the selection according to
the modifiers and filters, and highlights the resulting selection.
"""
self.modify_selection()
self.highlightSelection(self.selection)
[docs] def pick(self, mode, tool='pix', oneshot=False, func=None,
filter=None, pickable=None, _rect=None, minobj=0):
"""Interactively pick objects from the canvas.
Parameters
----------
mode: str
Defines what to pick: one of ``actor``, ``element``, ``point``.
oneshot: bool
If True, the function returns as soon as the user ends
an atomic picking operation (left mouse press and release).
If False (default) the user can modify his selection until
he explicitely accepts (right mouse button press or ENTER)
or cancels (ESC) the pick operation.
func: callable
If provided, this function is called after each atomic pick
operation (from mouse button press to mouse button release).
The canvas self is passed as an argument. The last atomic pick
is then available as `self.picked` and the previously collected
selection (if collection is done) is in self.selection. This is
commonly used to highlight the picked items, collect picked items,
report picked items, compute and display features of picked
items. If not provided, the default function
:meth:`modify_and_highlight` is used. See there for details.
filter: str
Defines a filter to retain only some of the picked items in
the selection. If not provided, all the picked items are retained.
Available filters:
- single: keeps only a single item
- closest: keeps only the item closest to the user.
- conn*: these filters only work when picking mode is 'element' and
for objects of type Mesh. They keep only the items connected to
the already selected items or to the closest picked item if
nothing has been selected yet. Any filter name starting with
'conn' will activate the connected filter.
If 'conn' is followed by a '0', '1' or '2', that number defines
the level of the connectors (point, edge, face). The default level
is 1 (edge). By default the connected filters restrict the
selection to a single actor. Adding a '_' at the end of the
filter name allows simultaneous picking of connected elements
on multiple actors.
_rect: tuple
A tuple (x0, y0, x1, y1) speciying the rectangular part on the
canvas that will be picked. Allows simulated picking.
Returns
-------
Collection:
A (possibly empty) Collection with the picked items.
After return, the value of the selection_accepted
attribute can be tested to find how the picking operation was
exited:
- True: the selection was accepted (right mouse click, ENTER key,
or OK button),
- False: the selection was canceled (ESC key, or Cancel button).
In the latter case, the returned Collection is always empty.
It is also possible to test on the length of the selection.
"""
if self.pick_mode is None and mode not in self.pick_modes:
raise ValueError(f"Invalid pick mode {mode}")
self.setFocus()
self.pick_canceled = False
self.start_pick_mode(mode)
self.set_pick_tools(tool, filter, pickable)
if not callable(func):
func = QtCanvas.modify_and_highlight
self.events = []
if _rect:
# create events for programmed pick
self.events.extend(self.mouse_rect_pick_events(_rect))
try:
while not self.pick_canceled:
self.wait_interaction() # wait for user to pick a rectangle
if not self.pick_canceled:
if self.pick_tool == 'pix':
self.pick_pixels() # pick by pixels
else:
self.pick_parts() # pick by points
if self.pick_filter in ['single', 'closest']:
self.filter_closest(self.picked,
single=self.single_actor)
elif self.pick_filter[:4] == 'conn':
try:
connlevel = int(self.pick_filter[4:5])
except Exception:
connlevel = -1
self.filter_connected(self.picked, connlevel)
func(self)
self.update()
if oneshot or (
minobj > 0 and self.selection.total() >= minobj):
self.accept_selection()
finally:
self.finish_selection()
return self.selection
# TODO: can we flatten the return list and that of next function
[docs] def mouse_click_events(self, xy, button='left'):
"""Create the events for a mouse click.
Parameters
----------
xy: (int, int)
A tuple (x,y) specifying the mouse position where the click
events are to be generated.
Values are in pixels relative to the canvas widget.
button: 'left' | 'right'
The mouse button be be clicked at the given point.
Returns
-------
list
A nested list of events. The list contains one list with two
events: a mouse button press at the given point,
and a mouse button release.
"""
button = getattr(QtCore.Qt, button.capitalize()+'Button')
event1 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonPress, QtCore.QPoint(*xy),
button, button, QtCore.Qt.NoModifier)
event2 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonRelease, QtCore.QPoint(*xy),
button, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
return [[event1, event2]]
[docs] def mouse_rect_pick_events(self, rect=None):
"""Create the events for a mouse rectangle pick.
Parameters
----------
rect: tuple of ints, optional
A tuple (x0,y0,x1,y1) specifying the top left corner and the
bottom right corner of the rectangular are to be picked. Values
are in pixels relative to the canvas widget.
If not provided, the whole canvas area will be picked.
Returns
-------
list
A nested list of events. The list contains two sublists. The first
holds the events to make the rectangle pick:
- Press the left button mouse at (x0,y0).
- Move the mouse while holding the left button pressed to (x1,y1).
- Release the left mouse button at (x1,y1).
The second sublist holds the events to accept the picked items:
- Press the right mouse button at (x1,y1).
- Release the right mouse button at (x1,y1).
"""
if rect is None:
x0, y0 = 0, 0
x1, y1 = self.getSize()
else:
x0, y0, x1, y1 = rect
event1 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonPress, QtCore.QPoint(x0, y0),
QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
event2 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseMove, QtCore.QPoint(x1, y1),
QtCore.Qt.NoButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
event3 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonRelease, QtCore.QPoint(x1, y1),
QtCore.Qt.LeftButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
event4 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonPress, QtCore.QPoint(x1, y1),
QtCore.Qt.RightButton, QtCore.Qt.RightButton, QtCore.Qt.NoModifier)
event5 = QtGui.QMouseEvent(
QtCore.QEvent.Type.MouseButtonRelease, QtCore.QPoint(x1, y1),
QtCore.Qt.RightButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
return [[event1, event2, event3], [event4, event5]]
#################### Interactive drawing ####################################
[docs] def mouse_draw(self, x, y, action):
"""Process mouse events during interactive drawing.
On PRESS, do nothing.
On MOVE, do nothing.
On RELEASE, compute the unprojected point
"""
if action == PRESS:
self.makeCurrent()
self.update()
if self.preview_timer:
self.preview_timer.start()
self.preview_last = self.preview = (x, y)
if self.trackfunc:
self.camera.setTracking(True)
elif action == MOVE:
if self.trackfunc:
self.trackfunc(x, y, self.zplane)
if self.preview_timer:
self.preview = (x, y)
elif action == RELEASE:
if self.preview_timer:
self.preview_timer.stop()
self.drawn = self.unproject(x, y, self.zplane)
self.interaction_busy = False
def add_point(self):
from pyformex.gui import guicore as pg
self.drawing = self.drawing.append(self.drawn)
self.removeHighlight()
pg.draw(self.drawing, highlight=True, color=0, marksize=10,
ontop=True)
def on_mouse_move(self):
if self.preview != self.preview_last:
self.drawn = self.unproject(*self.preview, self.zplane)
self.previewfunc()
self.preview_last = self.preview
[docs] def idraw(self, mode='point', *, npoints=-1, zplane=0.,
func=None, preview=None, coords=None, mouseline=False):
"""Interactively draw on the canvas.
This function allows the user to interactively create points in 2.5D
space and collects the subsequent points in a Coords object. The
interpretation of these points is left to the caller.
The drawing operation is finished when the number of requested points
has been reached, or when the user clicks the right mouse button or
hits 'ENTER' or presses the ESC-button.
Parameters
----------
mode: str
One of the drawing modes, specifying the kind of objects you
want to draw. This is passed to the specified `func`.
npoints: int
Specifies how many points can be created before returning.
If < 0, the continuous drawing mode has to be ended explicitely
with an accept or cancel.
zplane: float
The depth of the z-plane on which the 2D drawing is done.
func: callable
A function that is called after each atomic drawing operation.
It is typically used to accumulate the drawn points in a single
set of points and draw a preview of the drawing. If not provided,
the default will just do that.
The function is passed the canvas as a parameter, from which the
following data are available:
- canvas.drawn: the newly drawn point,
- canvas.drawing: the accumulated set of points
- canvas.drawmode: the current drawing mode
preview: callable
A function that is called during mouse movement with pressed
button. Parameters as for ``func``. The canvas.drawn is set to
the point currently pointed at. This allows for interactively
previewing what will be constructed if the mouse b utton would
be released at that point.
coords: Coords
An initial set of coordinates to which the newly created
points should be added. This can be used to continue a previous
idraw operation. If provided, `npoints` also counts these
initial points.
Returns
-------
Coords (npts, 3)
The Coordinates of the created points. On return
canvas.draw_accepted will be True if the function returned
because the number of points was reached or the result was
accepted with a right mouse click or ENTER key;
it will be False if the ESC button was hit.
"""
from pyformex.gui.timer import Timer
from functools import partial
self.setFocus()
self.draw_canceled = False
self.start_draw(mode, zplane, coords, mouseline)
if not callable(func):
func = QtCanvas.add_point
if callable(preview):
self.previewfunc = partial(preview, self)
self.preview_timer = Timer(
0.05, self.on_mouse_move, repeat=True, start=False)
else:
self.preview_timer = None
self.events = []
try:
while not self.draw_canceled:
self.wait_interaction()
if not self.draw_canceled:
func(self)
self.update()
if npoints > 0 and len(self.drawing) >= npoints:
self.accept_draw()
except Exception as e:
print(e)
finally:
self.finish_draw()
return self.drawing
[docs] def start_draw(self, mode, zplane, coords, mouseline):
"""Start an interactive drawing mode."""
# self.perspective(False)
self.camera.lock()
if mouseline:
self.mousehandler.set(LEFT, NONE, self.mouse_line)
self.mousehandler.set(None, NONE, self.mouse_line)
self.mousehandler.setCursorShape('default')
else:
self.mousehandler.set(LEFT, NONE, self.mouse_draw)
self.mousehandler.setCursorShape('draw')
self.mousehandler.set(RIGHT, NONE, self.emit_done)
self.mousehandler.set(RIGHT, SHIFT, self.emit_cancel)
self.signals.DONE.connect(self.accept_draw)
self.signals.CANCEL.connect(self.cancel_draw)
self.drawmode = mode
self.zplane = float(zplane)
self.drawing = Coords(coords)
[docs] def finish_draw(self):
"""End an interactive drawing mode."""
self.mousehandler.reset(None, NONE)
self.mousehandler.reset(LEFT, NONE)
self.mousehandler.reset(RIGHT, NONE)
self.mousehandler.reset(RIGHT, SHIFT)
self.mousehandler.setCursorShape('default')
self.signals.DONE.disconnect(self.accept_draw)
self.signals.CANCEL.disconnect(self.cancel_draw)
self.drawmode = None
# self.perspective(original_perspective)
self.camera.unlock() # should unlock only if it wasn't locked before
[docs] def accept_draw(self, clear=False):
"""Cancel an interactive drawing mode.
If clear == True, the current drawing is cleared.
"""
self.draw_accepted = True
if clear:
self.drawing = Coords()
self.draw_accepted = False
self.draw_canceled = True
self.interaction_busy = False
[docs] def cancel_draw(self):
"""Cancel an interactive drawing mode and clear the drawing."""
self.accept_draw(clear=True)
##########################################################################
# line drawing mode: currently disabled.
# Stores 2d int coords. should convert to Coords??
# def drawLinesInter(self, mode='line', oneshot=False, func=None):
# """Interactively draw lines on the canvas.
# - oneshot: if True, the function returns as soon as the user ends
# a drawing operation. The default is to let the user
# draw multiple lines and only to return after an explicit
# cancel (ESC or right mouse button).
# - func: if specified, this function will be called after each
# atomic drawing operation. The current drawing is passed as
# an argument. This can e.g. be used to show the drawing.
# When the drawing operation is finished, the drawing is returned.
# The return value is a (n,2,2) shaped array.
# """
# self.setFocus()
# self.drawing_canceled = False
# self.start_drawing(mode)
# while not self.drawing_canceled:
# self.wait_drawing()
# if not self.drawing_canceled:
# if self.edit_mode: # edit mode from the edit combo was clicked
# if self.edit_mode == 'undo' and self.drawing.size != 0:
# self.drawing = self.drawing[:-1]
# elif self.edit_mode == 'clear':
# self.drawing = empty((0, 2, 2), dtype=int)
# elif self.edit_mode == 'close' and self.drawing.size != 0:
# line = asarray([self.drawing[-1, -1], self.drawing[0, 0]])
# self.drawing = append(self.drawing, line.reshape(-1, 2, 2), 0)
# self.edit_mode = None
# else: # a line was drawn interactively
# self.drawing = append(self.drawing, self.drawn.reshape(-1, 2, 2), 0)
# if func:
# func(self.drawing)
# if oneshot:
# self.accept_drawing()
# if func and not self.drawing_accepted:
# func(self.drawing)
# self.finish_drawing()
# return self.drawing
# def start_drawing(self, mode):
# """Start an interactive line drawing mode."""
# if pf.debugon(pf.DEBUG.GUI):
# print("START DRAWING MODE")
# self.mousehandler.set(LEFT, NONE, self.mouse_draw_line)
# self.mousehandler.set(RIGHT, NONE, self.emit_done)
# self.mousehandler.set(RIGHT, SHIFT, self.emit_cancel)
# self.mousehandler.setCursorShape('default')
# self.signals.DONE.connect(self.accept_drawing)
# self.signals.CANCEL.connect(self.cancel_drawing)
# self.drawing_mode = mode
# self.edit_mode = None
# self.drawing = empty((0, 2, 2), dtype=int)
# def wait_drawing(self):
# """Wait for the user to interactively draw a line."""
# self.drawing_timer = QtCore.QThread
# self.drawing_busy = True
# while self.drawing_busy:
# self.drawing_timer.msleep(20)
# pf.app.processEvents()
# def finish_drawing(self):
# """End an interactive drawing mode."""
# if pf.debugon(pf.DEBUG.GUI):
# print("END DRAWING MODE")
# self.mousehandler.reset(LEFT, NONE)
# self.mousehandler.reset(RIGHT, NONE)
# self.mousehandler.reset(RIGHT, SHIFT)
# self.mousehandler.setCursorShape('default')
# self.signals.DONE.disconnect(self.accept_drawing)
# self.signals.CANCEL.disconnect(self.cancel_drawing)
# self.drawing_mode = None
# def accept_drawing(self, clear=False):
# """Cancel an interactive drawing mode.
# If clear == True, the current drawing is cleared.
# """
# if pf.debugon(pf.DEBUG.GUI):
# print("CANCEL DRAWING MODE")
# self.drawing_accepted = True
# if clear:
# self.drawing = empty((0, 2, 2), dtype=int)
# self.drawing_accepted = False
# self.drawing_canceled = True
# self.drawing_busy = False
# def cancel_drawing(self):
# """Cancel an interactive drawing mode and clear the drawing."""
# self.accept_drawing(clear=True)
# def edit_drawing(self, mode):
# """Edit an interactive drawing."""
# self.edit_mode = mode
# self.drawing_busy = False
######## QtOpenGL interface ##############################
[docs] def initializeGL(self):
self.glinit()
self.initCamera()
self.resizeGL(self.width(), self.height())
self.makeCurrent()
# self.setCamera()
[docs] def resizeGL(self, w, h):
self.setSize(w, h)
[docs] def paintGL(self):
if not self.mode2D:
self.display()
####### MOUSE EVENT HANDLERS ############################
# Mouse functions can be bound to any of the mouse buttons
# LEFT, MIDDLE or RIGHT.
# Each mouse function should accept three possible actions:
# PRESS, MOVE, RELEASE.
# On a mouse button PRESS, the mouse screen position and the pressed
# button are always saved in self.statex,self.statey,self.button.
# The mouse function does not need to save these and can directly use
# their values.
# On a mouse button RELEASE, self.button is cleared, to avoid further
# move actions.
# ATTENTION! The y argument is positive upward, as in normal OpenGL
# operations!
[docs] def dynarot(self, x, y, action):
"""Perform dynamic rotation operation.
This function processes mouse button events controlling a dynamic
rotation operation. The action is one of PRESS, MOVE or RELEASE.
"""
if action == PRESS:
w, h = self.getSize()
self.state = [self.statex-w/2, self.statey-h/2]
self.stated = length(self.state) < 0.35 * length([w, h])
elif action == MOVE:
w, h = self.getSize()
# set all three rotations from mouse movement
# tangential movement sets twist,
# but only if initial vector is big enough
x0 = self.state # initial vector
d = length(x0)
x1 = [x-w/2, y-h/2] # new vector
if d > h/8:
a0 = math.atan2(x0[0], x0[1])
a1 = math.atan2(x1[0], x1[1])
an = (a1-a0) / math.pi * 180
ds = at.stuur(d, [-h/4, h/8, h/4], [-1, 0, 1], 2)
twist = - an*ds
self.camera.rotate(twist, 0., 0., 1.)
self.state = x1
# radial movement rotates around vector in lens plane
x0 = [self.statex-w/2, self.statey-h/2] # initial vector
if x0 == [0., 0.]:
x0 = [1., 0.]
dx = [x-self.statex, y-self.statey] # movement
b = projection(dx, x0)
if abs(b) > 5: # only process when the movement is large enough
if self.stated: # mouse action did not start in the corners
val = at.stuur(b, [-2*h, 0, 2*h], [-180, 0, +180], 1)
rot = [abs(val), -dx[1], dx[0], 0]
self.camera.rotate(*rot)
self.statex, self.statey = (x, y)
self.update()
elif action == RELEASE:
self.update()
[docs] def dynapan(self, x, y, action):
"""Perform dynamic pan operation.
This function processes mouse button events controlling a dynamic
pan operation. The action is one of PRESS, MOVE or RELEASE.
"""
if action == PRESS:
pass
elif action == MOVE:
w, h = self.getSize()
dx, dy = float(self.statex-x)/w, float(self.statey-y)/h
self.camera.transArea(dx, dy)
self.statex, self.statey = (x, y)
self.update()
elif action == RELEASE:
self.update()
[docs] def dynazoom(self, x, y, action):
"""Perform dynamic zoom operation.
This function processes mouse button events controlling a dynamic
zoom operation. The action is one of PRESS, MOVE or RELEASE.
"""
if action == PRESS:
# TODO: make this a tuple?
self.state = [self.camera.dist, self.camera.area.tolist(),
pf.cfg['gui/dynazoom']]
elif action == MOVE:
w, h = self.getSize()
# dx, dy = float(self.statex-x)/w, float(self.statey-y)/h
for method, state, value, size in zip(
self.state[2], [self.statex, self.statey], [x, y], [w, h]):
if method == 'area':
d = float(state-value)/size
f = math.exp(4*d)
self.camera.zoomArea(f, area=np.asarray(
self.state[1]).reshape(2, 2))
elif method == 'dolly':
d = at.stuur(value, [0, state, size], [5, 1, 0.2], 1.2)
self.camera.dist = d*self.state[0]
self.update()
elif action == RELEASE:
self.update()
[docs] def wheel_zoom(self, delta):
"""Zoom by rotating a wheel over an angle delta"""
f = 2**(delta/120.*pf.cfg['gui/wheelzoomfactor'])
if pf.cfg['gui/wheelzoom'] == 'area':
self.camera.zoomArea(f)
elif pf.cfg['gui/wheelzoom'] == 'lens':
self.camera.zoom(f)
else:
self.camera.dolly(f)
self.update()
[docs] def emit_done(self, x, y, action):
"""Emit a DONE event by clicking the mouse.
This is equivalent to pressing the ENTER button."""
if action == RELEASE:
self.signals.DONE.emit()
[docs] def emit_cancel(self, x, y, action):
"""Emit a CANCEL event by clicking the mouse.
This is equivalent to pressing the ESC button."""
if action == RELEASE:
self.signals.CANCEL.emit()
@classmethod
def has_modifier(clas, e, mod):
return (e.modifiers() & mod) == mod
[docs] def mousePressEvent(self, e):
"""Process a mouse press event."""
# Make the clicked viewport the current one
pf.GUI.viewports.setCurrent(self)
# on PRESS, always remember mouse position and button
self.statex, self.statey = e.x(), self.height()-e.y()
self.button = e.button()
self.mod = e.modifiers() & ALLMODS
func = self.mousehandler.get(self.button, self.mod)
if func:
func(self.statex, self.statey, PRESS)
e.accept()
[docs] def mouseMoveEvent(self, e):
"""Process a mouse move event."""
# the MOVE event does not identify a button, use the saved one
func = self.mousehandler.get(self.button, self.mod)
if func:
func(e.x(), self.height()-e.y(), MOVE)
e.accept()
[docs] def mouseReleaseEvent(self, e):
"""Process a mouse release event."""
func = self.mousehandler.get(self.button, self.mod)
self.button = None # clear the stored button
if func:
func(e.x(), self.height()-e.y(), RELEASE)
e.accept()
[docs] def wheelEvent(self, e):
"""Process a wheel event."""
func = self.wheel_zoom
if func:
func(e.delta())
e.accept()
# Any keypress with focus in the canvas generates a GUI WAKEUP signal.
# This is used to break out of a wait status.
# Events not handled here could also be handled by the toplevel
# event handler.
[docs] def keyPressEvent(self, e):
# Make the clicked viewport the current one
pf.GUI.signals.WAKEUP.emit()
if e.key() == ESC:
self.signals.CANCEL.emit()
e.accept()
elif e.key() == ENTER or e.key() == RETURN:
self.signals.DONE.emit()
e.accept()
else:
e.ignore()
# End