#
##
## 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 2D drawing in a 3D space
This pyFormex plugin provides some interactive 2D drawing functions.
While the drawing operations themselves are in 2D, they can be performed
on a plane with any orientation in space. The constructed geometry always
has 3D coordinates in the global cartesian coordinate system.
"""
import numpy as np
import pyformex as pf
from pyformex.opengl.decors import Grid
from pyformex.gui import guicore as pg
_I = pg._I
draw_modes = ('point', 'polyline', 'curve', 'nurbs', 'circle')
autoname = {
'point': pf.autoName('coords'),
'polyline': pf.autoName('polyline'),
'curve': pf.autoName('bezierspline'),
'nurbs': pf.autoName('nurbscurve'),
'circle': pf.autoName('circle'),
}
obj_params = {}
[docs]def draw2D(mode='point', npoints=-1, zvalue=0., zplane=None, func=None,
preview=False, coords=None, **kargs):
"""Enter interactive drawing mode and return the 2D drawing.
Drawing is done on a plane perpendicular to the camera axis, at a specified
z value. If zplane is specified, it is used directly. Else, it is computed
from projecting the point [0.,0.,zvalue]. Specifying zvalue is in
most cases easier for the user.
See meth:`QtCanvas.idraw` for more details.
This function differs in that it provides default displaying
during the drawing operation and a button to stop the drawing operation.
(TODO) The drawing can be edited using the methods 'undo', 'clear' and
'close', which are presented in a combobox.
"""
if pf.canvas.drawmode is not None:
pf.warning("You need to finish the previous drawing operation first!")
return
if func is None:
func = accept_point
if preview:
preview = preview_drawing
if zplane is None:
zplane = pf.canvas.project(0., 0., zvalue)[2]
return pf.canvas.idraw(mode=mode, npoints=npoints, zplane=zplane,
func=func, preview=preview, coords=coords)
[docs]def drawnObject(points, mode='point'):
"""Return the geometric object resulting from draw2D points"""
minor = None
if '_' in mode:
mode, minor = mode.split('_')
closed = minor == 'closed'
if mode == 'point':
return points
elif mode == 'polyline':
if points.ncoords() < 2:
return None
closed = obj_params.get('closed', None)
return pf.PolyLine(points, closed=closed)
elif mode == 'curve' and points.ncoords() > 1:
curl = obj_params.get('curl', None)
closed = obj_params.get('closed', None)
return pf.BezierSpline(points, curl=curl, closed=closed)
elif mode == 'nurbs':
degree = obj_params.get('degree', None)
if points.ncoords() <= degree:
return None
closed = obj_params.get('closed', None)
return pf.NurbsCurve(points, degree=degree, closed=closed)
elif mode == 'circle' and points.ncoords() % 3 == 0:
R, C, N = pf.gt.triangleCircumCircle(points.reshape(-1, 3, 3))
circles = [pf.simple.circle(r=r, c=c, n=n) for r, c, n in zip(R, C, N)]
if len(circles) == 1:
return circles[0]
else:
return circles
else:
return None
temp_draw = []
[docs]def highlight_drawing(canvas, coords, drawmode, numbered=False):
"""Highlight a temporary drawing on the canvas.
pts is an array of points.
"""
global temp_draw
canvas.removeHighlight()
PA = pf.draw(coords, bbox='last', marksize=8, highlight=True)
NA = pf.drawNumbers(coords, gravity='ne') if numbered else None
obj = drawnObject(coords, mode=drawmode)
OA = pf.draw(obj, bbox='last', highlight=True) if obj is not None else None
pf.undraw(temp_draw)
temp_draw = [a for a in (PA, NA, OA) if a]
canvas.update()
# TODO: these two can be merged
[docs]def preview_drawing(canvas):
"""Function executed during preview
Adds the point to a temporary drawing and then draws it
"""
temp_drawing = canvas.drawing.append(canvas.drawn)
highlight_drawing(canvas, temp_drawing, canvas.drawmode)
[docs]def accept_point(canvas):
"""Function to be executed when a new point is clicked
Adds the point to the accepted drawing and then draws it
"""
canvas.drawing = canvas.drawing.append(canvas.drawn)
highlight_drawing(canvas, canvas.drawing, canvas.drawmode)
[docs]def drawObject2D(mode, npoints=-1, zvalue=0., preview=False, coords=None):
"""Draw a 2D opbject in the xy-plane with given z-value"""
points = draw2D(mode, npoints=npoints, zvalue=zvalue, preview=preview,
coords=coords)
return drawnObject(points, mode=mode)
###################################
the_zvalue = 0.
def draw_object(points, mode):
# print("POINTS %s" % points)
obj = drawnObject(points, mode=mode)
if obj is None:
pf.canvas.removeHighlight()
return
# print("OBJECT IS %s:\n%s" % (mode, obj))
res = pf.askItems([
_I('name', autoname[mode].peek(), text='Name for storing the object'),
_I('color', 'blue', 'color', text='Color for the object'),
])
if not res:
return
name = res['name']
color = res['color']
if name == autoname[mode].peek():
next(autoname[mode])
pf.PF.update({name: obj})
pf.canvas.removeHighlight()
pf.draw(points, color='black', nolight=True)
if mode != 'point':
pf.draw(obj, color=color, nolight=True)
if mode == 'nurbs':
# print("DRAWING KNOTS")
pf.draw(obj.knotPoints(), color=color, marksize=5)
return name
def draw2d_dialog(mode, npoints=None, closed=None, curl=None, degree=None,
preview=None):
store = obj_params
items = []
if mode == 'point':
items.append(_I('npoints', store.get('npoints', -1)))
if mode == 'curve':
items.append(_I('curl', store.get('curl', 1/3)))
if mode == 'nurbs':
items.append(_I('degree', store.get('degree', 3)))
if mode in ('polyline', 'curve', 'nurbs'):
items.append(_I('closed', store.get('closed', False)))
if mode in ('polyline', 'curve', 'nurbs', 'circle'):
items.append(_I('preview', store.get('preview', False)))
res = pf.askItems(caption='draw2d dialog', items=items)
if res:
store.update(res)
return res
def draw_any(mode, **kargs):
obj_params.update(kargs)
res = draw2d_dialog(mode)
if res:
obj_params.update(res)
points = draw2D(mode, **obj_params)
if points is not None:
return draw_object(points, mode)
def draw_points(npoints=-1):
return draw_any('point', npoints=npoints)
def draw_polyline(closed=False, preview=False):
return draw_any('polyline', closed=closed, preview=preview)
def draw_curve(curl=1/3, closed=False, preview=False):
return draw_any('curve', curl=curl, closed=closed, preview=preview)
def draw_nurbs(degree=3, closed=False, preview=False):
return draw_any('nurbs', degree=degree, closed=closed, preview=preview)
def draw_circle(preview=False):
return draw_any('circle', preview=preview)
[docs]def objectName(actor):
"""Find the exported name corresponding to a canvas actor"""
if hasattr(actor, 'object'):
obj = actor.object
print("OBJECT", type(obj))
for name in pf.PF:
print(name)
print(pf.PF[name])
if pf.PF[name] is obj:
return name
return None
[docs]def splitPolyLine(c):
"""Interactively split the specified polyline"""
pf.options.debug = 1
XA = pf.draw(c.coords, clear=False, bbox='last', nolight=True)
k = pf.pick('point', filter='single', oneshot=True, pickable=[XA])
pf.canvas.pickable = None
pf.undraw(XA)
if 0 in k:
at = k[0]
print(at)
return c.split(at)
else:
return []
def split_curve():
k = pf.pick('actor', filter='single', oneshot=True)
if -1 not in k:
return
nr = k[-1][0]
print("Selecting actor %s" % nr)
actor = pf.canvas.actors[nr]
print("Actor", actor)
name = objectName(actor)
print("Enter a point to split %s" % name)
c = actor.object
print("Object", c)
cs = splitPolyLine(c)
if len(cs) == 2:
pf.draw(cs[0], color='red')
pf.draw(cs[1], color='green')
_grid_data = [
_I('autosize', False),
_I('dx', 1., text='Horizontal distance between grid lines'),
_I('dy', 1., text='Vertical distance between grid lines'),
_I('width', 100., text='Horizontal grid size'),
_I('height', 100., text='Vertical grid size'),
_I('point', [0., 0., 0.], text='Point in grid plane'),
_I('normal', [0., 0., 1.], text='Normal on the plane'),
_I('lcolor', 'black', 'color', text='Line color'),
_I('lwidth', 1.0, text='Line width'),
_I('showplane', False, text='Show backplane'),
_I('pcolor', 'white', 'color', text='Backplane color'),
_I('alpha', '0.3', text='Alpha transparency'),
]
[docs]def set_grid(*, autosize, dx, dy, width, height, point, normal,
lcolor, lwidth, showplane, pcolor, alpha, **kargs):
"""Show the grid with specified parameters"""
nx = int(np.ceil(width/dx))
ny = int(np.ceil(height/dy))
obj = None
if autosize:
obj = pf.pmgr().sel_values
if obj:
bb = pf.bbox(obj)
nx = ny = 20
dx = dy = bb.sizes().max() / nx * 2.
ox = (-nx*dx/2., -ny*dy/2., 0.)
if obj:
c = pf.bbox(obj).center()
ox = c + ox
planes = 'f' if showplane else 'n'
grid = Grid(nx=(nx, ny, 0), ox=ox, dx=(dx, dy, 0.), linewidth=lwidth,
linecolor=lcolor, planes=planes, planecolor=pcolor, alpha=0.3)
remove_grid()
pf.canvas._grid = pf.draw(grid)
[docs]def create_grid():
"""Interactively create the grid"""
_name = 'Draw2d Background Grid'
res = pf.askItems(caption=_name, store=_name + '_data', items=_grid_data)
if res:
set_grid(**res)
def remove_grid():
if hasattr(pf.canvas, '_grid'):
pf.undraw(pf.canvas._grid)
pf.canvas._grid = None
# End