Source code for plugins.draw2d

#
##
##  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