Source code for plugins.gts_itf

#
##
##  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/.
##
"""Operations on triangulated surfaces using GTS functions.

This module provides access to GTS from inside pyFormex.
"""
import numpy as np
import re

import pyformex as pf
from pyformex import utils
from pyformex import arraytools as at
from pyformex.path import Path
from pyformex.coords import Coords, bboxIntersection
from pyformex.formex import Formex
from pyformex.mesh import Mesh
from pyformex.multi import multitask

#
# gts commands used:
#   in Debian package: stl2gts gts2stl gtscheck
#   not in Debian package: gtssplit gtscoarsen gtsrefine gtssmooth gtsinside
#

[docs]def read_intersectioncurve(fn): """Read the intersection curve of a boolean operation Parameters ---------- fn: :term:`path_like` The name of a file containing an intersection curve produced by :func:`gtsset`. Returns ------- Mesh A Mesh of eltype Line2 containing the line segments on the intersection curve. """ RE = re.compile(r"^VECT 1 2 0 2 0 (?P<data>.*)$") r = [] for line in open(fn, 'r'): m = RE.match(line) if m: r.append(m.group('data')) nelems = len(r) x = np.fromstring('\n'.join(r), sep=' ').reshape(-1, 2, 3) return Formex(x).toMesh()
[docs]def gtsset(surf1, surf2, op, filt='', ext='', curve=False, check=False, verbose=False): """Perform boolean/intersection methods on TriSurfaces. Boolean operations between two surfaces are a basic operation in free surface modeling. Both surfaces should be closed orientable non-intersecting manifolds. This means they represent an enclosed volume. :meth:`TriSurface.check` can be used to find out if the surface is ok. The boolean operations are set operations on the enclosed volumes: union('+'), difference('-') or intersection('*'). This uses the external program ``gtsset`` to do the actual computation. Parameters ---------- surf1: TriSurface The first TriSurface (should be a closed orientable non-intersecting manifold). surf2: TriSurface The second TriSurface (should be a closed orientable non-intersecting manifold). op: '+', '-', '*' or 'a' The boolean operation to perform: union('+'), difference('-') or intersection('*'), or 'a' to compute all parts of the intersection, allowing to compose different results. filt: str A filter command to be executed on the gtsset output. The string should start with a '|', and the filter will be run as a pipe on the gtsset output and should produce the filtered output on stdout. ext: str The extension to be added on the ``result`` file. curve: bool If True, an intersection curve is computed, else a new surface. Returns ------- TriSurface | dict | Mesh | None: - if curve is False and op is one of '+', '-' or '\\*': the resulting TriSurface; - if curve is False and op is 'a': a dict with four keys, each having a TriSurface as value: - 's1in2': the part of surf1 that is inside surf2, - 's1out2': the part of surf1 that is outside surf2, - 's2in1': the part of surf2 that is inside surf1, - 's2out1': the part of surf2 that is outside surf1; - if curve is True: a Mesh of eltype Line2, containing the intersection curve(s); - if surf1 and surf2 do not intersect: None. Note ---- The prefered way to invoke this function is by usint the :class:`TriSurface` methods: :meth:`TriSurface.gts_set`, :meth:`TriSurface.boolean and :meth:`TriSurface.intersection`. """ utils.External.require('gts-extra') # import here to avoid circular import from pyformex.trisurface import TriSurface op = { '+': 'union', '-': 'diff', '*': 'inter', 'a': 'all', }[op] options = '' if curve: options += '-i' if check: options += ' -s' if verbose: options += ' -v' with utils.TempDir() as tmpdir: tmp = tmpdir / 'surface1.gts' tmp1 = tmpdir / 'surface2.gts' tmp2 = tmpdir / ('result'+ext) #print("Writing temp file %s" % tmp) surf1.write(tmp, 'gts') #print("Writing temp file %s" % tmp1) surf2.write(tmp1, 'gts') print(f"Performing boolean operation {op}") cmd = f"cd {tmpdir} && gtsset {options} {op} {tmp} {tmp1} {filt}" P = utils.system( cmd, stdout=open(tmp2, 'w'), shell=True, verbose=verbose) if P.returncode or verbose: print(P.stdout) if P.returncode: print(P.stderr) print("""The boolean procedure didn't return any results.""") return None if curve: res = read_intersectioncurve(tmp2) elif op=='all': res = {} for k in ['s1in2', 's1out2', 's2in1', 's2out1']: res[k] = TriSurface.read(tmpdir / (k+'.gts')) else: res = TriSurface.read(tmp2) return res
[docs]def gtsinside(surf, pts, dir, keep=False): """Test whether points are inside a closed surface. This tests whether a point is inside a closed surface by shooting a ray from the point in a certain direction and tests whether the number of intersections with the surface is odd (inside) or even (outside). Shooting directions are limited to one of the global axes directions. Parameters ---------- surf: TriSurface The TriSurface (should be a closed orientable non-intersecting manifold). pts: :term:`coords_like` A Coords with shape (npts, 3) dir: int The global axis that will be used as shooting direction. Returns ------- array An int array with the indices of the points that are inside the surface. Note ---- This function is not intended to be used directly. False negatives or positives may occur when the ray passes precisely through the edge of triangles. Use :func:`inside` instead. """ # roll, because the external always shoots in x-direction S = surf.rollAxes(dir) P = pts.rollAxes(dir) with utils.TempDir(keep=keep) as tmpdir: if keep: print(f"Using temp dir {tmpdir}") tmp0 = tmpdir / 'surface.gts' tmp1 = tmpdir / 'points.dta' tmp2 = tmpdir / 'result.out' tmp3 = tmpdir / 'result.stderr' #print("Writing temp file %s" % tmp0) S.write(tmp0, 'gts') #print("Writing temp file %s" % tmp1) with open(tmp1, 'w') as f: P.tofile(f, sep=' ', format='%f') f.write('\n') #print("Performing inside testing") cmd = "gtsinside %s %s" % (tmp0, tmp1) from pyformex import process P = process.run(cmd, stdout=open(tmp2, 'w'), stderr=open(tmp3, 'w')) if P.returncode: print("An error occurred during the testing.\nSee file %s for more details." % tmp2) print(P.stdout) return None #print("Reading results from %s" % tmp2) ind = np.fromfile(tmp2, sep=' ', dtype=at.Int) return ind
[docs]def inside(surf, pts, atol='auto', multi=True, keep=False): """Test which of the points pts are inside the surface. This uses the ray shooting technique of gtsinside, but avoids (most of) the false negatives/positives. It does this by shooting in three directions (the three global axis directions) and returning the majority vote of the three outcomes. Parameters ---------- surf: TriSurface The TriSurface (should be a closed orientable non-intersecting manifold). pts: :term:`coords_like` A Coords with shape (npts, 3) or Formex with shape (npts, 1, 3). atol: float Tolerance used in restricting the points set to the surface's bounding box. multi: bool If True (default), rund the three shooting directions n parallel. This reduces the total processing time. keep: bool If True, the temporary directory with intermediate results is not erased. This may be useful for debugging purposes. Returns ------- array: An int array with the indices of the points that are inside the surface. See Also -------- TriSurface.inside: the prefered way to call this function """ utils.External.require('gts-extra') # We use a Formex so that we can use the prop for keeping the indices if not isinstance(pts, Formex): pts = Formex(pts) if atol == 'auto': atol = pts.dsize()*0.001 # determine bbox of common space of surface and points bb = bboxIntersection(surf, pts) if bb.size == 0: # No bbox intersection: no points inside return np.array([], dtype=at.Int) # Limit the points to the common part # Add point numbers as property, to allow return of original numbers pts.setProp(np.arange(pts.nelems())) pts = pts.clip(pts.testBbox(bb, atol=atol)) #print(f"Clipped points: {pts.prop}") ins = np.zeros((3, pts.nelems()), dtype=bool) if not multi: for i in range(3): #dirs = np.roll(np.arange(3), -i)[1:] # clip the surface perpendicular to the shooting direction # !! gtsinside seems to fail sometimes when using clipping #S = surf # .clip(testBbox(surf,bb,dirs=dirs,atol=atol),compact=False) # find inside points shooting in direction i ok = gtsinside(surf, pts.coords, i) # tmpdir = None if tmpdir is None else # Path(tmpdir)/f"gtsinside-{i}") ins[i, ok] = True else: tasks = [(gtsinside, (surf, pts.coords, i)) for i in range(3)] ind = multitask(tasks, 3) for i in range(3): ins[i, ind[i]] = True ok = np.where(ins.sum(axis=0) > 1)[0] return pts.prop[ok]
# End