#
##
## 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/.
##
"""This implements an OpenGL drawing widget for painting 3D scenes.
"""
import numpy as np
import pyformex as pf
from pyformex.opengl import gl
from pyformex.opengl.gl import GL, GLU
from pyformex import arraytools as at
from pyformex.coords import Coords
from pyformex.simple import cuboid2d
from pyformex.opengl.canvas_settings import (CanvasSettings, Light, LightProfile,
Rendermode, RenderProfiles)
from pyformex.opengl.scene import Scene
from pyformex.opengl.renderer import Renderer
from pyformex.opengl.actors import Actor
from pyformex.opengl.camera import Camera
from pyformex.opengl.decors import Grid2D
### Some (OLD) drawing functions ############################################
# But still kept because used!
[docs]def drawDot(x, y):
"""Draw a dot at canvas coordinates (x,y)."""
GL.glBegin(GL.GL_POINTS)
GL.glVertex2f(x, y)
GL.glEnd()
[docs]def drawLine(x1, y1, x2, y2):
"""Draw a straight line from (x1,y1) to (x2,y2) in canvas coordinates."""
GL.glBegin(GL.GL_LINES)
GL.glVertex2f(x1, y1)
GL.glVertex2f(x2, y2)
GL.glEnd()
[docs]def drawRect(x1, y1, x2, y2):
"""Draw the circumference of a rectangle."""
GL.glBegin(GL.GL_LINE_LOOP)
GL.glVertex2f(x1, y1)
GL.glVertex2f(x1, y2)
GL.glVertex2f(x2, y2)
GL.glVertex2f(x2, y1)
GL.glEnd()
[docs]def drawGrid(x1, y1, x2, y2, nx, ny):
"""Draw a rectangular grid of lines
The rectangle has (x1,y1) and and (x2,y2) as opposite corners.
There are (nx,ny) subdivisions along the (x,y)-axis. So the grid
has (nx+1) * (ny+1) lines. nx=ny=1 draws a rectangle.
nx=0 draws 1 vertical line (at x1). nx=-1 draws no vertical lines.
ny=0 draws 1 horizontal line (at y1). ny=-1 draws no horizontal lines.
"""
GL.glBegin(GL.GL_LINES)
ix = range(nx+1)
if nx == 0:
jx = [1]
nx = 1
else:
jx = ix[::-1]
for i, j in zip(ix, jx):
x = (i*x2+j*x1)/nx
GL.glVertex2f(x, y1)
GL.glVertex2f(x, y2)
iy = range(ny+1)
if ny == 0:
jy = [1]
ny = 1
else:
jy = iy[::-1]
for i, j in zip(iy, jy):
y = (i*y2+j*y1)/ny
GL.glVertex2f(x1, y)
GL.glVertex2f(x2, y)
GL.glEnd()
#############################################################################
#############################################################################
#
# The Canvas
#
#############################################################################
#############################################################################
[docs]class Canvas:
"""A canvas for OpenGL rendering.
The Canvas is a class holding all global data of an OpenGL scene rendering.
It always contains a Scene with the actors and decorations that are drawn
on the canvas. The canvas has a Camera holding the viewing parameters
needed to project the scene on the canvas.
Settings like colors, line types, rendering mode and the lighting
information are gathered in a CanvasSettings object.
There are attributes for some special purpose decorations (Triade, Grid)
that can not be handled by the standard drawing and Scene changing
functions.
The Canvas class does not however contain the viewport size and position.
The class is intended as a mixin to be used by some OpenGL widget class
that will hold this information (such as the QtCanvas class).
Important note: the Camera object is not initalized by the class
initialization. The derived class initialization should therefore
explicitely call the `initCamera` method before trying to draw anything
to the canvas. This is because initializing the camera requires a working
OpenGL format, which is only established by the derived OpenGL widget.
"""
def __init__(self, settings={}):
"""Initialize an empty canvas with default settings."""
# loadLibGL()
self.scene = Scene(self)
self.renderer = None
# self.highlights = ItemList(self)
self.camera = None
self.triade = None
self.grid = None
# self.setRenderMode(pf.cfg['draw/rendermode'])
self._rendermode = Rendermode.from_str(pf.prefcfg['draw/rendermode'])
self.settings = CanvasSettings(**settings)
self.mode2D = False
self.picking = False
self.resetLighting()
self._focus = None
self.focus = False
self.state = (0, 0)
if pf.debugon(pf.DEBUG.DRAW):
print("Canvas Settings:\n", self.settings)
self.makeCurrent() # we need correct OpenGL context
@property
def rendermode(self):
return self._rendermode.to_str()
@rendermode.setter
def rendermode(self, mode):
if mode not in RenderProfiles:
raise ValueError(f"Invalid render mode {mode}")
self._rendermode = Rendermode.from_str(mode)
[docs] def renderModes(self):
"""Return a list of predefined render profiles."""
return list(RenderProfiles)
[docs] def setRenderMode(self, mode):
"""Set the rendering mode.
This sets or changes the rendermode and lighting attributes.
If lighting is not specified, it is set depending on the rendermode.
If the canvas has not been initialized, this merely sets the
attributes self.rendermode and self.lighting.
If the canvas was already initialized (it has a camera), and one of
the specified settings is different from the existing, the new mode
is set, the canvas is re-initialized according to the newly set mode,
and everything is redrawn with the new mode.
"""
if mode != self.rendermode:
self.rendermode = mode
if self.camera:
self.scene.changeMode(self._rendermode)
self.reset()
@property
def lighting(self):
return self._rendermode.lighting
@lighting.setter
def lighting(self, onoff):
self._rendermode.lighting = bool(onoff)
@property
def focus(self):
return self._focus is not None
@focus.setter
def focus(self, onoff):
if onoff:
self.add_focus_rectangle()
else:
self.remove_focus_rectangle()
self.updateGL()
@property
def actors(self):
return self.scene.actors
@property
def bbox(self):
return self.scene.bbox
[docs] def sceneBbox(self):
"""Return the bbox of all actors in the scene"""
return pf.bbox(self.scene.actors)
## def enable_lighting(self, state):
## """Toggle OpenGL lighting on/off."""
## glLighting(state)
[docs] def resetDefaults(self, dict={}):
"""Return all the settings to their default values."""
self.settings.reset(dict)
self.resetLighting()
## self.resetLights()
[docs] def setAmbient(self, ambient):
"""Set the global ambient lighting for the canvas"""
self.lightprof.ambient = float(ambient)
[docs] def setMaterial(self, matname):
"""Set the default material light properties for the canvas"""
self.material = pf.GUI.materials[matname]
[docs] def resetLighting(self):
"""Change the light parameters"""
self.lightmodel = pf.cfg['render/lightmodel']
self.setMaterial(pf.cfg['render/material'])
self.lightset = pf.cfg['render/lights']
lights = [Light(**pf.cfg['light/' + light]) for light in self.lightset]
self.lightprof = LightProfile(pf.cfg['render/ambient'], lights)
[docs] def resetOptions(self):
"""Reset the Drawing options to default values"""
self.drawoptions = dict(
view=None, # Keep the current camera angles
bbox='auto', # Automatically zoom on the drawed object
clear_=False, # Clear on each drawing action
shrink=False,
shrink_factor=0.8,
wait=True,
silent=True,
single=False,
)
[docs] def setOptions(self, d):
"""Set the Drawing options to some values"""
# We rename the 'clear' to 'clear_', because we use a Dict
# to store these (in __init__.draw) and Dict does not allow
# a 'clear' key.
if 'clear' in d and isinstance(d['clear'], bool):
d['clear_'] = d.pop('clear')
self.drawoptions.update(d)
[docs] def setWireMode(self, mode=None):
"""Set the wiremode."""
oldmode = self._rendermode.wiremode
# print(f"SETWIREMODE {mode=}, {oldmode=}")
if oldmode is None:
return
if mode is None:
# just toggle
mode = 1 if oldmode == 0 else -oldmode
else:
if mode < -3 or mode > 3:
mode = 0
self._rendermode.wiremode = mode
# print(f"DO_WIREMODE {mode=}, {oldmode=}")
if mode > 0 or oldmode > 0:
# both <= 0 : off stays off
self.changedMode()
[docs] def getToggle(self, attr):
"""Get the value of a toggle attribute"""
if attr == 'perspective':
return self.camera.perspective
elif attr in ['lighting', 'avgnormals']:
return getattr(self._rendermode, attr)
else:
return getattr(self.settings, attr)
[docs] def setToggle(self, attr, state):
"""Set or toggle a boolean settings attribute
Furthermore, if a Canvas method do_ATTR is defined, it will be called
with the old and new toggle state as a parameter.
"""
oldstate = self.getToggle(attr)
if state not in [True, False]:
state = not oldstate
if attr == 'perspective':
self.camera.perspective = state
elif attr in ['lighting', 'avgnormals']:
setattr(self._rendermode, attr, state)
else:
self.settings[attr] = state
try:
func = getattr(self, 'do_'+attr)
func(state, oldstate)
except Exception:
pass
[docs] def changedMode(self):
"""Redisplay the scene after a mode change"""
self.scene.changeMode(self._rendermode)
self.display()
[docs] def do_alphablend(self, state, oldstate):
"""Toggle alphablend on/off."""
if state != oldstate:
self.changedMode()
[docs] def do_lighting(self, state, oldstate):
"""Toggle lights on/off."""
if state != oldstate:
self.enable_lighting(state)
self.changedMode()
def do_avgnormals(self, state, oldstate):
if state != oldstate and self.lighting:
self.changedMode()
[docs] def setLineWidth(self, lw):
"""Set the linewidth for line rendering."""
self.settings.linewidth = float(lw)
[docs] def setLineStipple(self, repeat, pattern):
"""Set the linestipple for line rendering."""
self.settings.update({'linestipple': (repeat, pattern)})
[docs] def setPointSize(self, sz):
"""Set the size for point drawing."""
self.settings.pointsize = float(sz)
[docs] def setBackground(self, color=None, image=None):
"""Set the color(s) and image.
Change the background settings according to the specified parameters
and set the canvas background accordingly. Only (and all) the specified
parameters get a new value.
Parameters
----------
color:
Either a single color, a list of two colors or a list of
four colors.
image:
An image to be set.
"""
self.scene.backgrounds.clear()
if color is not None:
self.settings.update(dict(bgcolor=color))
if image is not None:
self.settings.update(dict(bgimage=image))
color = self.settings.bgcolor
if color.ndim == 1 and not self.settings.bgimage:
if pf.debugon(pf.DEBUG.DRAW):
print("Clearing fancy background")
else:
self.createBackground()
[docs] def createBackground(self):
"""Create the background object."""
F = cuboid2d(xmin=[-1., -1.], xmax=[1., 1.])
# TODO: Currently need a Mesh for texture
F = F.toMesh()
image = None
if self.settings.bgimage:
from pyformex.plugins.imagearray import image2array
try:
image = image2array(self.settings.bgimage, mode='RGBA')
except Exception:
pass
actor = Actor(F, name='background', rendermode='smooth',
color=[self.settings.bgcolor], texture=image,
rendertype=3, opak=True, lighting=False, view='front',
sticky=True)
self.scene.addAny(actor)
self.update()
[docs] def setFgColor(self, color):
"""Set the default foreground color."""
self.settings.fgcolor = pf.Color(color)
[docs] def setSlColor(self, color):
"""Set the highlight color."""
self.settings.slcolor = pf.Color(color)
[docs] def setTriade(self, pos='lb', siz=100, triade=None):
"""Set the Triade for this canvas.
Display the Triade on the current viewport.
The Triade is a reserved Actor displaying the orientation of
the global axes. It has special methods to show/hide it.
See also: :meth:`removeTriade`, :meth:`hasTriade`
Parameters:
- `pos`: string of two characters. The characters define the horizontal
(one of 'l', 'c', or 'r') and vertical (one of 't', 'c', 'b') position
on the camera's viewport. Default is left-bottom.
- `siz`: float: intended size (in pixels) of the triade.
- `triade`: None or Geometry: defines the Geometry to be used for
representing the global axes.
If None, use the previously set triade, or set a default if no
previous.
If Geometry, use this to represent the axes. To be useful and properly
displayed, the Geometry's bbox should be around [(-1,-1,-1),(1,1,1)].
Drawing attributes may be set on the Geometry to influence
the appearance. This allows to fully customize the Triade.
"""
if self.triade:
self.removeTriade()
if triade:
import pyformex.gui.guicore as pg
self.triade = None
x, y, w, h = GL.glGetIntegerv(GL.GL_VIEWPORT)
if pos[0] == 'l':
x0 = x + siz
elif pos[0] == 'r':
x0 = x + w - siz
else:
x0 = x + w // 2
if pos[1] == 'b':
y0 = y + siz
elif pos[1] == 't':
y0 = y + h - siz
else:
y0 = y + h // 2
A = pg.draw(triade.scale(siz), rendertype=-2, single=True,
size=siz, x=x0, y=y0)
self.triade = A
elif self.triade:
self.addAny(self.triade)
[docs] def removeTriade(self):
"""Remove the Triade from the canvas.
Remove the Triade from the current viewport.
The Triade is a reserved Actor displaying the orientation of
the global axes. It has special methods to draw/undraw it.
See also: :meth:`setTriade`, :meth:`hasTriade`
"""
if self.hasTriade():
self.removeAny(self.triade)
[docs] def hasTriade(self):
"""Check if the canvas has a Triade displayed.
Return True if the Triade is currently displayed.
The Triade is a reserved Actor displaying the orientation of
the global axes.
See also: :meth:`setTriade`, :meth:`removeTriade`
"""
return self.triade is not None and self.triade in self.scene.decorations
[docs] def setGrid(self, grid=None):
"""Set the canvas Grid for this canvas.
Display the Grid on the current viewport.
The Grid is a 2D grid fixed to the canvas.
See also: :meth:`removeGrid`, :meth:`hasGrid`
Parameters:
- `grid`: None or Actor: The Actor to be displayed as grid. This will
normall be a Grid2D Actor.
"""
if self.grid:
self.removeGrid()
if grid:
self.grid = grid
if self.grid:
self.addAny(self.grid)
[docs] def removeGrid(self):
"""Remove the Grid from the canvas.
Remove the Grid from the current viewport.
The Grid is a 2D grid fixed to the canvas.
See also: :meth:`setGrid`, :meth:`hasGrid`
"""
if self.hasGrid():
self.removeAny(self.grid)
[docs] def hasGrid(self):
"""Check if the canvas has a Grid displayed.
Return True if the Grid is currently displayed.
The Grid is a 2D grid fixed to the canvas.
See also: :meth:`setGrid`, :meth:`removeGrid`
"""
return self.grid is not None and self.grid in self.scene.decorations
[docs] def initCamera(self, camera=None):
"""Initialize the current canvas camera
If a camera is provided, that camera is set. Else a new
default camera is constructed.
"""
self.makeCurrent() # we need correct OpenGL context for camera
if not isinstance(camera, Camera):
camera = Camera()
self.camera = camera
if self.renderer is None:
self.renderer = Renderer(self)
self.renderer.camera = self.camera
[docs] def clearCanvas(self):
"""Clear the canvas to the background color."""
color = self.settings.bgcolor
if color.ndim > 1:
color = color[0]
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
GL.glClearColor(*pf.Color(color).rgba)
self.setDefaults()
def setSize(self, w, h):
if h == 0: # prevent divide by zero
h = 1
GL.glViewport(0, 0, w, h)
self.aspect = float(w)/h
self.camera.setLens(aspect=self.aspect)
## if self.scene.background:
## # recreate the background to match the current size
## self.createBackground()
self.display()
[docs] def drawit(self, a):
"""_Perform the drawing of a single item"""
self.setDefaults()
a.draw(self)
[docs] def setDefaults(self):
"""Activate the canvas settings in the GL machine."""
self.settings.activate()
GL.glDepthFunc(GL.GL_LESS)
[docs] def overrideMode(self, mode):
"""Override some settings"""
settings = RenderProfiles[mode]
CanvasSettings.glOverride(settings, self.settings)
[docs] def reset(self):
"""Reset the rendering engine.
The rendering machine is initialized according to self.settings:
- self.rendermode
- self.lighting
"""
self.setDefaults()
self.setBackground(self.settings.bgcolor, self.settings.bgimage)
self.clearCanvas()
GL.glClearDepth(1.0) # Enables Clearing Of The Depth Buffer
GL.glEnable(GL.GL_DEPTH_TEST) # Enables Depth Testing
GL.glEnable(GL.GL_CULL_FACE)
if self.rendermode == 'wireframe':
gl.gl_polygonoffset(0.0)
else:
gl.gl_polygonoffset(1.0)
[docs] def glinit(self):
"""Initialize the rendering engine.
"""
self.reset()
[docs] def glFinish(self):
"""Flush all OpenGL commands, making sure the display is updated."""
GL.glFinish()
[docs] def display(self):
"""(Re)display all the actors in the scene.
This should e.g. be used when actors are added to the scene,
or after changing camera position/orientation or lens.
"""
self.makeCurrent()
if self.picking:
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
GL.glClearColor(0.0, 0.0, 0.0, 0.0)
gl.gl_flat()
GL.glDisable(GL.GL_BLEND)
else:
self.clearCanvas()
gl.gl_smooth()
gl.gl_fill()
self.renderer.render(self.scene, self.picking)
self.glFinish()
def create_pickitems(self, obj_type):
start = 1 # 0 is used to identify background pixels
self.pick_nitems = [start]
for a in self.actors:
if pf.debugon(pf.DEBUG.PICK):
print(f"Actor {a.name}, pickable: {a.pickable}")
if a.pickable:
start = a._add_pick(start, obj_type)
self.pick_nitems.append(start)
[docs] def renderpick(self, obj_type):
"""Show rendering for picking"""
# Create pickitems
self.create_pickitems(obj_type)
self.picking = True
self.display()
def zoom_2D(self, zoom=None):
if zoom is None:
zoom = (0, self.width(), 0, self.height())
GLU.gluOrtho2D(*zoom)
[docs] def begin_2D_drawing(self):
"""Set up the canvas for 2D drawing on top of 3D canvas.
The 2D drawing operation should be ended by calling end_2D_drawing.
It is assumed that you will not try to change/refresh the normal
3D drawing cycle during this operation.
"""
if self.mode2D:
return
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glPushMatrix()
GL.glLoadIdentity()
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glPushMatrix()
GL.glLoadIdentity()
self.zoom_2D()
GL.glDisable(GL.GL_DEPTH_TEST)
self.mode2D = True
[docs] def end_2D_drawing(self):
"""Cancel the 2D drawing mode initiated by begin_2D_drawing."""
if self.mode2D:
GL.glEnable(GL.GL_DEPTH_TEST)
GL.glMatrixMode(GL.GL_PROJECTION)
GL.glPopMatrix()
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glPopMatrix()
self.mode2D = False
# def addHighlight(self, itemlist):
# """Add a highlight or a list thereof to the 3D scene"""
# self.highlights.add(itemlist)
# def removeHighlight(self, itemlist=None):
# """Remove a highlight or a list thereof from the 3D scene.
# Without argument, removes all highlights from the scene.
# """
# if itemlist is None:
# itemlist = self.highlights[:]
# self.highlights.delete(itemlist)
[docs] def removeHighlight(self, actors=None):
"""Remove a highlight or a list thereof from the 3D scene.
Without argument, removes all highlights from the scene.
"""
self.scene.removeHighlight(actors)
# for actor in self.scene.actors:
# actor.removeHighlight()
def addAny(self, itemlist):
self.scene.addAny(itemlist)
addActor = addAnnotation = addDecoration = addAny
def removeAny(self, itemlist):
self.scene.removeAny(itemlist)
removeActor = removeAnnotation = removeDecoration = removeAny
def removeAll(self, sticky=False):
self.scene.clear(sticky)
# self.highlights.clear()
def dummy(self):
pass
redrawAll = dummy
def setBbox(self, bbox):
self.scene.bbox = bbox
[docs] def setCamera(self, bbox=None, angles=None, orient='xy'):
"""Sets the camera looking under angles at bbox.
This function sets the camera parameters to view the specified
bbox volume from the specified viewing direction.
Parameters:
- `bbox`: the bbox of the volume looked at
- `angles`: the camera angles specifying the viewing direction.
It can also be a string, the key of one of the predefined
camera directions
If no angles are specified, the viewing direction remains constant.
The scene center (camera focus point), camera distance, fovy and
clipping planes are adjusted to make the whole bbox viewed from the
specified direction fit into the screen.
If no bbox is specified, the following remain constant:
the center of the scene, the camera distance, the lens opening
and aspect ratio, the clipping planes. In other words the camera
is moving on a spherical surface and keeps focusing on the same
point.
If both are specified, then first the scene center is set,
then the camera angles, and finally the camera distance.
In the current implementation, the lens fovy and aspect are not
changed by this function. Zoom adjusting is performed solely by
changing the camera distance.
"""
#
# TODO: we should add the rectangle (digital) zooming to
# the docstring
from pyformex.gui import views
self.makeCurrent()
# set scene center
if bbox is not None:
if pf.debugon(pf.DEBUG.DRAW):
print("Setting {bbox=}")
self.setBbox(bbox)
X0, X1 = self.scene.bbox
self.camera.focus = 0.5*(X0+X1)
# set camera angles
if isinstance(angles, str):
angles, orient = views.getAngles(angles)
if angles is not None:
try:
axes = views.getAngleAxes(orient)
if orient == 'xz':
angles = angles + (-90.,)
axes = axes + ((1., 0., 0.),)
self.camera.setAngles(angles, axes)
except Exception:
raise ValueError(
f"Invalid view angles specified: {angles}, {orient}")
# set camera distance and clipping planes
if bbox is not None:
# Currently, we keep the default fovy/aspect
# and change the camera distance to focus
fovy = self.camera.fovy
self.camera.setLens(fovy, self.aspect)
# Default correction is sqrt(3)
correction = float(pf.cfg['gui/autozoomfactor'])
tf = at.tand(fovy/2.)
from pyformex import simple
bbix = simple.regularGrid(X0, X1, [1, 1, 1], swapaxes=True)
bbix = np.dot(bbix, self.camera.rot[:3, :3])
bbox = Coords(bbix).bbox()
dx, dy, dz = bbox[1] - bbox[0]
vsize = max(dx/self.aspect, dy)
dsize = bbox.dsize()
offset = dz
dist = (vsize/tf + offset) / correction
if dist == np.nan or dist == np.inf:
if pf.debugon(pf.DEBUG.DRAW):
print(f"Invalid {dist=}")
return
if dist <= 0.0:
dist = 1.0
self.camera.dist = dist
near, far = dist-2.0*dsize, dist+2.0*dsize
# We set this very extreme, because near and far are not changed
# in the zoom functions. TODO: fix this.
near = max(near, 0.001*vsize, 0.001*dist)
far = min(far, 10000.*vsize, 1000.*dist)
# make sure near is positive far > near
# Very small near gives rounding problems
near = max(near, 0.1)
# if near < 0.:
# near = np.finfo(at.Float).eps
if far <= near:
far = 2*near
self.camera.setClip(near, far)
self.camera.resetArea()
[docs] def project(self, x, y, z):
"""Map the object coordinates (x,y,z) to window coordinates."""
return self.camera.project((x, y, z))[0]
[docs] def unproject(self, x, y, z):
"""Map the window coordinates (x,y,z) to object coordinates."""
return self.camera.unproject((x, y, z))[0]
[docs] def zoom(self, f, dolly=True):
"""Dolly zooming.
Zooms in with a factor `f` by moving the camera closer
to the scene. This does not change the camera's FOV setting.
It will change the perspective view though.
"""
if dolly:
self.camera.dolly(f)
[docs] def zoomRectangle(self, x0, y0, x1, y1):
"""Rectangle zooming.
Zooms in/out by changing the area and position of the visible
part of the lens.
Unlike zoom(), this does not change the perspective view.
`x0,y0,x1,y1` are pixel coordinates of the lower left and upper right
corners of the area of the lens that will be mapped on the
canvas viewport.
Specifying values that lead to smaller width/height will zoom in.
"""
w, h = float(self.width()), float(self.height())
self.camera.setArea(x0/w, y0/h, x1/w, y1/h)
[docs] def zoomCentered(self, w, h, x=None, y=None):
"""Rectangle zooming with specified center.
This is like zoomRectangle, but the zoom rectangle is specified
by its center and size, which may be more appropriate when using
off-center zooming.
"""
self.zoomRectangle(x-w/2, y-h/2, x+w/2, y+w/2)
[docs] def zoomAll(self):
"""Zoom to make full scene visible."""
self.setCamera(bbox=self.sceneBbox())
[docs] def saveBuffer(self):
"""Save the current OpenGL buffer"""
self.save_buffer = GL.glGetIntegerv(GL.GL_DRAW_BUFFER)
[docs] def showBuffer(self):
"""Show the saved buffer"""
pass
[docs] def add_focus_rectangle(self, color=pf.color.pyformex_pink):
"""Draw the focus rectangle."""
if self._focus is None:
self._focus = Grid2D(-1., -1., 1., 1., color=color, linewidth=2, rendertype=3)
self._focus.sticky = True
if self._focus not in self.scene.backgrounds:
self.addAny(self._focus)
self.update()
def remove_focus_rectangle(self):
if self._focus:
self.removeAny(self._focus)
self._focus = None
[docs] def highlightSelection(self, K):
"""Highlight a selection of items on the canvas.
K is Collection of actors/items as returned by the pick() method.
"""
self.scene.removeHighlight()
if K.obj_type == 'actor':
for i in K.get(-1, []):
self.scene.actors[i].setHighlight()
else:
for i in K.keys():
if i >= 0:
if K.obj_type == 'element':
self.actors[i].addHighlightElements(K[i])
elif K.obj_type == 'point':
self.actors[i].addHighlightPoints(K[i])
### End