#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## 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/.
##
"""Toolbars for the pyFormex GUI.
This module defines the functions for creating the pyFormex window toolbars.
"""
import pyformex as pf
from pyformex import utils
from pyformex.gui import QtGui, QPixmap
from pyformex.gui import widgets
############################## functions for the pick button ############
[docs]def pick(mode, *, tool='pix', filter=None, oneshot=False,
func=None, pickable=None, prompt=None, _rect=None, minobj=0,
**kargs):
"""Enter interactive picking mode and return selection.
See :func:`gui.qtcanvas.Canvas.pick` for more details.
This function differs in that it provides an extra interactive interface
in the statusbar: OK/Cancel buttons to stop the picking operation,
and comboboxes to change the picking tool and filters.
Parameters
----------
mode: str
Defines what to pick : one of 'actor', 'element' or 'point'.
'actor' picks complete actors. 'element' picks elements from one or
more actor(s). 'point' picks points of Formices or nodes of Meshes.
tool: str
Defines what picking tool to use. One of 'pix', 'any' or 'all'.
With 'pix' items are picked if they have any visible pixels in the
pick rectangle. With 'any', items are picked if any of their points
are inside the pick rectangle. With 'all', items are picked if all
their points are inside the rectangle.
filter: str
The picking filter that is activated on entering the pick mode.
It should be one of the Canvas.selection_filters: 'none', 'single',
'closest', 'connected'.
The active filter can be changed from a combobox in the statusbar.
The available filters depend on the picking tool.
oneshot: bool.
If True, the function returns as soon as the user ends
a picking operation. The default is to let the user
modify his selection and to return only after an explicit
cancel (ESC or right mouse button).
func: callable, optional
If specified, this function will be called after each
atomic pick operation. The Collection with the currently selected
objects is passed as an argument. This can e.g. be used to highlight
the selected objects during picking.
pickable: list of Actors, optional
List of Actors from which can be picked. The default is to use
a list with all Actors having the pickable=True attribute (which is
the default for newly constructed Actors).
prompt: str
The text printed to prompt the user to start picking. If None,
a default prompt is printed. Specify an empty string to avoid printing
a prompt.
minobj: int
Returns
-------
Collection
A (possibly empty) Collection with the picked items.
After return, the value of the pf.canvas.selection_accepted variable
can be tested to find how the picking operation was exited:
True means accepted (right mouse click, ENTER key, or OK button),
False means canceled (ESC key, or Cancel button). In the latter case,
the returned Collection is always empty. Ther returned Collection
also remains available in pf.canvas.selection until a new pick is
started.
"""
from pyformex.gui import draw
def _tool_filter_choices(tool):
"""Return the possible selection filters for the tool"""
if mode == 'element':
filters = pf.canvas.selection_filters
else:
filters = pf.canvas.selection_filters[:3]
return filters
def _set_selection_filter(item):
"""Set the selection filter mode
This function is used to change the selection filter from the
selection InputCombo widget.
s is one of the strings in selection_filters.
"""
s = item.value()
if pf.canvas.pick_mode is not None and s in pf.canvas.selection_filters:
# filter changed during picking: restart
pf.canvas.start_selection(None, pf.canvas.pick_tool, s)
def _set_pick_tool(item):
"""Set the value of the pick tool (first 3 chars)"""
nonlocal filters, filter_combo, tool
pf.canvas.pick_tool = item.value()[:3].lower()
filters = _tool_filter_choices(tool)
#print("CHANGE FILTERS",tool,filters)
filter_combo.setChoices(filters)
filter_combo.setValue(filters[0])
if pf.canvas.pick_mode is not None:
draw.warning("You need to finish the previous picking operation first!")
return
if mode not in pf.canvas.pick_modes:
draw.warning(f"Invalid picking {mode=}. "
f"Expected one of {pf.canvas.pick_modes=}")
return
pick_buttons = widgets.ButtonBox('Selection:', [
('Cancel', pf.canvas.cancel_selection),
('OK', pf.canvas.accept_selection)])
# combobox for filter selection
filters = _tool_filter_choices(tool)
filter_combo = widgets.InputCombo(
'Filter:', None, choices=filters, func=_set_selection_filter)
if filter is not None and filter in filters:
filter_combo.setValue(filter)
# combobox for tool switching
pick_tools = {'pix': 'Pixels', 'any': 'Any Point', 'all': 'All Points'}
txt = pick_tools[tool]
tool_combo = widgets.InputCombo(
'Pick by ', txt, choices=list(pick_tools.values()),
func=_set_pick_tool)
if prompt is None:
prompt = f"Pick: Mode {mode}; Tool {tool}; Filter {filter}"
if prompt:
print(prompt)
pf.GUI.statusbar.addWidget(pick_buttons)
pf.GUI.statusbar.addWidget(filter_combo)
pf.GUI.statusbar.addWidget(tool_combo)
try:
if pf.debugon(pf.DEBUG.PICK):
print(f"PICK {mode=}, {tool=}, {func=}, {filter=}, "
f"{pickable=}, {_rect=}, {minobj=}")
sel = pf.canvas.pick(mode, tool, oneshot, func,
filter, pickable, _rect, minobj)
finally:
# cleanup
if pf.canvas.pick_mode is not None:
pf.canvas.finish_selection()
pf.GUI.statusbar.removeWidget(pick_buttons)
pf.GUI.statusbar.removeWidget(filter_combo)
pf.GUI.statusbar.removeWidget(tool_combo)
return sel
[docs]def picksel(obj_type, **kargs):
"""Pick and print selection"""
sel = pick(obj_type, **kargs)
print(sel)
def report_func(self):
from pyformex.plugins import tools
self.removeHighlight()
self.highlightSelection(self.picked)
print(tools.report(self.picked))
[docs]def query(obj_type, **kargs):
"""Enter interactive query mode.
Enters a continuous picking mode where the user can pick objects
and the picked objects are reported in detail. This is usually called
from the toolbar Query buttons. The query mode stays active until
the user cancels pick mode (with ESC or right mouse button). distan
Parameters
----------
obj_type: str
Defines what to pick : one of 'actor', 'element' or 'point'
**kargs:
Extra parameters to be passed to :func:`pick`
"""
return pick(obj_type, func=report_func, **kargs)
# TODO: we could have a mode that shows distance on every point selected
[docs]def query_distance(colorid=0, colorstep=1, show=True, prec=None):
"""Enter interactive distance query mode
In distance query mode, the user picks subsequent single points.
For every second point picked, the distance to the previous point
is shown. The distances are shown and printed with subsequent colors
from the canvas default colormap or from a custom colormap in
pf.cfg['draw/querypalette'].
"""
import numpy as np
from pyformex.formex import Formex
from pyformex.gui import draw
if prec is None:
prec = np.get_printoptions()['precision']
def every2_showdistance(self):
nonlocal points, colorid, drawn
if not self.picked:
return
k, i = next(self.picked.singles())
self.actors[k].addHighlightPoints(np.array([i]))
P = self.actors[k].object.points()[i]
print(f"Actor {k}, point {i}, coords {P}")
points.append([k, i, P])
if len(points) % 2 == 0:
P0, P1 = (points[i][2] for i in range(-2, 0))
d = P1.distanceFromPoint(P0)
draw.printc(f"Distance: {d:.{prec}}", color=colorid)
if show:
lines = draw.draw(
Formex([[P0, P1]]), linewidth=2, color=colorid,
rendertype=4, ontop=True, bbox='last', view=None)
marks = draw.drawMarks(
[0.5 * (P0 + P1)], [f"={d:.{prec}}"], size=20, color=colorid)
pf.canvas.update()
drawn.extend([lines, marks])
colorid += colorstep
points=[]
drawn = []
print("Pick single points, every 2nd shows distance")
print(f"{pf.cfg['draw/querypalette']=}")
with draw.TempPalette(pf.cfg['draw/querypalette']):
pick('point', filter='closest', func=every2_showdistance, prompt='')
return {'points': points, 'drawn': drawn}
[docs]def query_angle(colorid=0, colorstep=1, show=True, prec=None):
"""Enter interactive angle query mode
In angle query mode, the user picks subsequent single points.
For every third point picked, the angle between the vectors from
first to second and from secont to third is shown.
The angles are shown and printed with subsequent colors
from the canvas default colormap or from a custom colormap in
pf.cfg['draw/querypalette'].
"""
import numpy as np
from pyformex.formex import Formex
from pyformex.gui import draw
from pyformex import geomtools as gt
if prec is None:
prec = np.get_printoptions()['precision']
def angle_report(P0, P1, P2, color):
angle, n = gt.rotationAngle(P0-P1, P2-P1)
angle, n = angle[0], n[0]
draw.printc(f"Angle: {angle:.{prec}} degrees, axis: {n}",
color=color)
lines = draw.draw(Formex([[P0, P1],[P1, P2]]), linewidth=3, color=color,
rendertype=4, ontop=True, bbox='last', view=None)
marks = draw.drawMarks([(P0+P1+P2)/3], [f"{angle:.{prec}}"], size=20,
color=color, gravity='')
pf.canvas.update()
return [lines, marks]
def every3_showangle(self):
nonlocal points, colorid, drawn, temp
if not self.picked:
return
k, i = next(self.picked.singles())
self.actors[k].addHighlightPoints(np.array([i]))
P = self.actors[k].object.points()[i]
print(f"Actor {k}, point {i}, coords {P}")
points.append([k, i, P])
if len(points) % 3 == 2:
P0, P1 = (points[i][2] for i in range(-2, 0))
temp = draw.draw(Formex([[P0, P1]]), linewidth=3,
color=colorid, rendertype=4, ontop=True,
bbox='last', view=None)
elif len(points) % 3 == 0:
P0, P1, P2 = (points[i][2] for i in range(-3, 0))
drawn += angle_report(P0, P1, P2, colorid)
colorid += colorstep
draw.undraw(temp)
temp = None
temp = None
points = []
drawn = []
print("Pick single points, every 3rd shows angle at middle point")
with draw.TempPalette(pf.cfg['draw/querypalette']):
pick('point', filter='closest', func=every3_showangle, prompt='')
draw.undraw(temp)
return {'points': points, 'drawn': drawn}
################### General Button Functions ###########
################### Main toolbar ###########
pickmenudata ={
'title': 'Pick tools',
'items' : [
('Pick point', picksel, {'data':'point', 'icon':'pick-point'}),
('Pick element', picksel, {'data':'element', 'icon':'pick-element'}),
('Pick actor', picksel, {'data':'actor', 'icon':'pick-actor'}),
],
'default': 'Pick element',
}
querymenudata = {
'title': 'Query tools',
'items' : [
('Query point', query, {'data':'point', 'icon':'query-point'}),
('Query element', query, {'data':'element', 'icon':'query-element'}),
('Query actor', query, {'data':'actor', 'icon':'query-actor'}),
('Query distance', query_distance, {'icon':'query-dist'}),
('Query angle', query_angle, {'icon':'query-angle'}),
],
'default': 'Query element',
}
################# Camera action toolbar ###############
#######################################################################
# Canvas Toggle buttons #
#########################
[docs]class ViewportToggle(widgets.ToggleToolButton):
"""A toolbar button that toggles the state of a Viewpor attribute
attr: one of 'perspective',
"""
def __init__(self, toolbar, icons, attr, checked=False, tooltip=''):
self._vp = toolbar.parent().viewports
self.attr = attr
super().__init__(
toolbar, icons, func=self.setstate, status=self.getstate,
checked=self.getstate(), # THIS DOES NOT WORK
#checked=False,
tooltip=tooltip)
[docs] def getstate(self):
"""Get the current state of the viewport attribute."""
vp = self._vp.current
return False if vp is None else vp.getToggle(self.attr)
[docs] def setstate(self, onoff=None):
"""Toggle the state of the viewport attribute."""
vp = self._vp.current
if onoff is None:
onoff = not vp.getToggle(self.attr)
else:
onoff = bool(onoff)
vp.setToggle(self.attr, onoff)
vp.update()
pf.app.processEvents()
def updateViewportButtons(vp):
if vp.focus:
transparency_button.update_status()
light_button.update_status()
normals_button.update_status()
perspective_button.update_status()
wire_button.update_status()
################# Wire Button ###############
# TODO: this is special: toggles an int between + and -
wire_button = None # the toggle wire button
def addWireButton(toolbar):
global wire_button
def wire_button_getstate():
vp = toolbar.parent().viewports.current
return False if vp is None else vp.settings['wiremode'] > 0
wire_button = widgets.ToggleToolButton(
toolbar, icons=('wirenone', 'wireall'),
func=wire_button_setstate, status=wire_button_getstate,
checked=False, tooltip='Toggle Wire Mode')
def wire_button_setstate():
vp = pf.GUI.viewports.current
vp.setWireMode()
vp.update()
pf.app.processEvents()
################# Transparency Button ###############
transparency_button = None # the toggle transparency button
def addTransparencyButton(toolbar):
global transparency_button
transparency_button = ViewportToggle(
toolbar, icons=('transparent', 'transparent'), attr='alphablend',
checked=False, tooltip='Toggle Transparent Mode')
################# Lights Button ###############
light_button = None
def addLightButton(toolbar):
global light_button
light_button = ViewportToggle(
toolbar, icons=('lamp', 'lamp-on'), attr='lighting',
checked=False, tooltip='Toggle Lights')
################# Normals Button ###############
normals_button = None
def addNormalsButton(toolbar):
global normals_button
normals_button = ViewportToggle(
toolbar, icons=('normals-ind', 'normals-avg'), attr='avgnormals',
checked=False, tooltip='Toggle Normals Mode')
################# Perspective Button ###############
perspective_button = None
def addPerspectiveButton(toolbar):
global perspective_button
perspective_button = ViewportToggle(
toolbar, icons=('project', 'perspect'), attr='perspective',
checked=True, tooltip='Toggle Perspective/Projective Mode')
def setPerspective():
perspective_button.setstate(True)
def setProjection():
perspective_button.setstate(False)
################# Timeout Button ###############
timeout_button = None # the timeout toggle button
def toggleTimeout(onoff=None):
if onoff is None:
onoff = widgets.input_timeout < 0
if onoff:
timeout = pf.cfg['gui/timeoutvalue']
else:
timeout = -1
widgets.setInputTimeout(timeout)
onoff = widgets.input_timeout > 0
if onoff:
# THIS SUSPENDS ALL WAITING! WE SHOULD IMPLEMENT A TIMEOUT!
# BY FORCING ALL INDEFINITE PAUSES TO A WAIT TIME EQUAL TO
# WIDGET INPUT TIMEOUT
pf.debug("FREEING the draw lock")
pf.GUI.drawlock.free()
else:
pf.debug("ALLOWING the draw lock")
pf.GUI.drawlock.allow()
return onoff
[docs]def timeout(onoff=None):
"""Programmatically toggle the timeout button"""
if timeout_button is not None:
timeout_button.setChecked(toggleTimeout(onoff))
# End