Source code for gui.annotations

#
##
##  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/.
##
"""Annotations

Annotations are drawable decorations of the Geometry actors. They provide
extra information on the object and its components, like node numbers.
Annotations can be toggled on or off in the Geometry menu. There is a
predefined set of Annotations, but users can add their own.
"""
import numpy as np
import pyformex as pf


class _Annotations:

    def __init__(self):
        self.available = {}  # all the registered annotations
        self._active = set()  # set of active annotations (function names)
        self._names = []
        self._drawn = {}  # Drawn annotations (Drawables/Actors)
        # self._actors = []  # Drawn objects (Actors)

    def register(self, **kargs):
        """Register annotation function.

        An annotation function is a function that takes the name of an
        object as parameter and draws something related to that object.
        The annotations drawn by these functions can be toggled on and off.
        Annotation functions should silently ignore objects for which they
        can not draw the annotation.
        The Geometry menu has many examples of annotation functions.

        Parameters
        ----------
        kargs:
            A dict where each key is an annotation name and the value
            is an annotation function.
        """
        self.available.update(kargs)

    def active(self, f):
        """Return the status of annotation f (function or name)

        """
        if isinstance(f, str):
            f = self.available.get(f, None)
        return f in self._active


    def toggle(self, f, onoff=None):
        """Toggle the display of an annotation On or Off.

        If given, onoff is True or False.
        If no onoff is given, this works as a toggle.
        """
        print(f"toggleAnnotation {self} {f} {onoff}")
        if isinstance(f, str):
            f = self.available.get(f, None)
        if f is None:
            return
        if onoff is None:
            # toggle
            active = f not in self._active
        else:
            active = onoff
        if active:
            print(f"ACTIVATE {f}")
            self._active.add(f)
            self.drawAnnotation(f)
        else:
            print(f"DESACTIVATE {f}")
            self._active.discard(f)
            self.removeAnnotation(f)


    def drawAnnotation(self, f):
        """Draw some annotation for the specified names."""
        self._drawn[f] = [f(n) for n in self._names]


    # TODO: if names is None, use all object from Scene  or from pf.pmgr() ?
    def draw(self, names, annot=None):
        """Draw annotations on objects

        names: list of object names
        annot: list of annotation names. If not given, use all active.
        """
        self._names = [n for n in names if pf.is_drawable(pf.PF[n])]
        if annot is None:
            annot = self._active
        else:
            annot = [self.available[f] for f in annot]
        with pf.busyCursor():
            for f in annot:
                if pf.debugon(pf.DEBUG.DRAW):
                    print("Drawing ANNOTATION:", f)
                self.drawAnnotation(f)


    def removeAnnotation(self, f):
        """Remove the annotation f."""
        if f in self._drawn:
            # pf.canvas.removeAnnotation(self._annotations[f])
            # Use removeAny, because some annotations are not canvas
            # annotations but actors!
            pf.canvas.removeAny(self._drawn[f])
            pf.canvas.update()
            del self._drawn[f]


    def editAnnotations(self, ontop=None):
        """Edit the annotation properties

        Currently only changes the ontop attribute for all drawn
        annotations. Values: True, False or '' (toggle).
        Other values have no effect.
        """
        for annot in self._drawn.values():
            if ontop in (True, False, ''):
                if not isinstance(annot, list):
                    annot = [annot]
                for a in annot:
                    if ontop == '':
                        ontop = not a.ontop
                    print(a, ontop)
                    a.ontop = ontop


Annotations = _Annotations()
# print(f"ANNOTATIONS: {Annotations=}")


[docs]def annotation(name): """Decorator function to register an annotation""" def decorator(func): Annotations.available[name] = func return func return decorator
[docs]@annotation('Object name') def draw_object_name(n): """Draw the name of an object at its center.""" return pf.drawText(n, pf.PF[n].center())
[docs]@annotation('Element numbers') def draw_elem_numbers(n): """Draw the numbers of an object's elements.""" return pf.drawNumbers(pf.PF[n], color='red')
[docs]@annotation('Node marks') def draw_nodes(n): """Draw the nodes of an object.""" return pf.draw(pf.PF[n].coords, nolight=True, wait=False)
[docs]@annotation('Node numbers') def draw_node_numbers(n): """Draw the numbers of an object's nodes.""" return pf.drawNumbers(pf.PF[n].coords, color='black')
[docs]@annotation('Edge numbers') def draw_edge_numbers(n): """Draw the edge numbers of an object.""" O = pf.PF[n] if isinstance(O, pf.Mesh): E = pf.Formex(O.coords[O.edges]) return pf.drawNumbers(E, color='blue')
[docs]@annotation('Free edges') def draw_free_edges(n): """Draw the feature edges of an object.""" O = pf.PF[n] if isinstance(O, pf.Mesh): return pf.drawFreeEdges(O, color='red')
[docs]@annotation('Bounding box') def draw_bbox(n): """Draw the bbox of an object.""" return pf.drawBbox(pf.PF[n])
[docs]@annotation('Convex hull') def draw_convex_hull(n): """Draw the convex hull of a Geometry. """ pf.PF['_convex_hull_'] = H = pf.PF[n].convexHull() pf.draw(H, color='red')
[docs]@annotation('Convex hull 2D') def draw_convex_hull2D(n): """Draw the 2D convex hulls of a Geometry. """ pf.PF['_convex_hull_2D'] = H = [pf.PF[n].convexHull(i) for i in range(3)] pf.draw(H, color='green')
####### TriSurface annotation functions ########
[docs]@annotation('Surface Normals') def draw_normals(n, avg=False): """Draw the surface normals at centers or averaged normals at the nodes.""" S = pf.PF[n] if not isinstance(S, pf.TriSurface): return if avg: C = S.coords N = S.avgVertexNormals() else: C = S.centroids() N = S.normals() siz = pf.cfg['draw/normalsize'] if siz == 'area' and not avg: siz = np.sqrt(S.areas()).reshape(-1, 1) else: try: siz = float(siz) except ValueError: siz = 0.05 * C.dsize() if avg: color = 'orange' else: color = 'red' return pf.drawVectors(C, N, size=siz, color=color, wait=False)
@annotation('AVG Surface Normals') def draw_avg_normals(n): return draw_normals(n, True) # End