Source code for gui.menus.Surface

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

Surface operations menu for pyFormex. This menu contains specific options
for TriSurface objects. The options from the Geometry and Mesh menus are
also applicable to these objects.
"""
import numpy as np
import pyformex as pf
from pyformex.trisurface import TriSurface
import pyformex.gui.guicore as pg
from pyformex.plugins import plot2d
from pyformex.plugins.tools import Plane
from pyformex.gui.menus.Geometry import clipSelection, clipAtPlane
_I = pg._I
_G = pg._G
_C = pg._C


##################### surface operations ##########################


[docs]def fixNormals(method): """Fix the normals of the selected surfaces.""" pm = pf.pmgr() SL = pm.get_sel(clas=TriSurface) if SL: pm.set_sel(SL.fixNormals(method=method))
[docs]def reverseNormals(): """Reverse the normals of the selected surfaces.""" pm = pf.pmgr() SL = pm.get_sel(clas=TriSurface) if SL: pm.set_sel(SL.reverse())
[docs]def removeNonManifold(): """Remove the nonmanifold edges.""" pm = pf.pmgr() SL = pm.get_sel(clas=TriSurface) if SL: pm.set_sel(SL.removeNonManifold())
[docs]def createPointsOnSurface(): """Interactively create points on the selected TriSurface. """ from pyformex.gui.menus import Tools pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: P = Tools.createPoints2D(surface=S) if P is not None: pname = pf.autoName('coords').peek() res = pg.askItems([ _I('name', pname, text='Name for storing the object'), ]) if res: name = res['name'] if name == pname: next(pf.autoName('coords')) pf.PF.update({name: P}) return P
# # Operations with surface type, border, ... # def showBorder(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: pm.draw_sel(color=0) border = S.borderMeshes() if border: print("The border consists of %s parts" % len(border)) print("The sorted border edges are: ") print('\n'.join([" %s: %s" % (i, b.elems) for i, b in enumerate(border)])) coloredB = [b.compact().setProp(i+1) for i, b in enumerate(border)] pg.draw(coloredB, linewidth=3) for i, b in enumerate(coloredB): c = np.roll(pf.canvas.settings.colormap, i+1, axis=0) pg.drawText(str(i), b.center(), color=c, size=18) # ontop=True) pf.PF.update({'border': coloredB}) else: pf.warning("The surface %s does not have a border" % pf.PM.selection[0]) pf.PF.forget('border') return S def fillBorders(): _name = 'Fill Borders' S = showBorder() try: B = pf.PF['border'] except KeyError: return print("Got Border") pm = pf.pmgr() pg.clear() pg.draw(B) if B: props = [b.prop[0] for b in B] dia = pg.Dialog(caption=_name, store=_name+'_data', items=[ _I('Fill which borders', itemtype='radio', choices=['All', 'One']), _I('Filling method', itemtype='radio', choices=['radial', 'border']), _I('merge', False, text='Merge fills into current surface'), ]) res = dia.getResults() if res: if res['Fill which borders'] == 'One': B = B[:1] fills = {} for i, b in enumerate(B): fills[f'fill-{i}'] = pf.fillBorder( b, method=res['Filling method']).setProp(props[i]) if res['merge']: # name = pm.selection[0] for f in fills: S += fills[f] # pf.PF.update({name: S}) pm.draw_sel() else: pf.PF.update(fills) pg.draw(fills)
[docs]def deleteTriangles(): """Interactively delete triangles from a TriSurface""" pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: picked = pg.pick('element') if picked: picked = picked[0] if len(picked) > 0: pm.set_sel([S.cclip(picked)])
# Selectable values for display/histogram # key is a description of a result # value is a tuple of: # - function to calculate the values # - domain to display: True to display on edges, False to display on elements SelectableStatsValues = { 'Quality': (TriSurface.quality, False), 'Aspect ratio': (TriSurface.aspectRatio, False), 'Facet Area': (TriSurface.areas, False), 'Facet Perimeter': (TriSurface.perimeters, False), 'Smallest altitude': (TriSurface.smallestAltitude, False), 'Longest edge': (TriSurface.longestEdge, False), 'Shortest edge': (TriSurface.shortestEdge, False), 'Number of node adjacent elements': (TriSurface.nNodeAdjacent, False), 'Number of edge adjacent elements': (TriSurface.nEdgeAdjacent, False), 'Edge angle': (TriSurface.edgeAngles, True), 'Number of connected elements': (TriSurface.nEdgeConnected, True), 'Curvature': (TriSurface.curvature, False), } # Drawable options for curvature. CurvatureValues = [ 'Shape index S', 'Curvedness C', 'Gaussian curvature K', 'Mean curvature H', 'First principal curvature k1', 'Second principal curvature k2', 'First principal direction d1', 'Second principal direction d2', ] def showHistogram(key, val, cumulative): y, x = plot2d.createHistogram(val, cumulative=cumulative) plot2d.showHistogram(x, y, key) _stat_dia = None
[docs]def showStatistics(key=None, domain=True, dist=False, cumdist=False, clip=None, vmin=None, vmax=None, percentile=False): """Show the values corresponding with key in the specified mode. key is one of the keys of SelectableStatsValues mode is one of ['On Domain','Histogram','Cumulative Histogram'] """ pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: func, onEdges = SelectableStatsValues[key] kargs = {} if key == 'Curvature': kargs['neigh'] = _stat_dia.results['neigh'] val = func(S, **kargs) as_vectors = False if key == 'Curvature': key = _stat_dia.results['curval'] ind = key.split()[-1] if ind in ['d1', 'd2']: val = val['d1'], val['d2'] as_vectors = True else: val = val[ind] val = val[S.elems] # !! THIS SHOULD BE IMPLEMENTED AS A GENERAL VALUE CLIPPER # !! e.g popping up when clicking the legend # !! and the values should be changeable if clip: clip = clip.lower() if percentile: try: from scipy.stats.stats import scoreatpercentile except Exception: pf.warning(""".. **The **percentile** clipping option is not available. Most likely because 'python-scipy' is not installed on your system.""") return Q1 = scoreatpercentile(val, vmin) Q3 = scoreatpercentile(val, vmax) factor = 3 if vmin: vmin = Q1-factor*(Q3-Q1) if vmax: vmax = Q3+factor*(Q3-Q1) if clip == 'top': val = val.clip(max=vmax) elif clip == 'bottom': val = val.clip(min=vmin) else: val = val.clip(vmin, vmax) if domain: pg.clear() if as_vectors: # TODO: this can become a drawTensor function siz = 0.5*S.edgeLengths().mean() pg.drawVectors(S.coords, val[0], size=siz, color='red') pg.drawVectors(S.coords, val[1], size=siz, color='darkgreen') pg.lights(True) pg.draw(S, mode='smooth') else: pg.lights(False) showSurfaceValue(S, key, val, onEdges) if dist: showHistogram(key, val, cumulative=False) if cumdist: showHistogram(key, val, cumulative=True)
def _show_stats(domain, dist): if not _stat_dia.validate(): return res = _stat_dia.results key = res['Value'] if dist and res['Cumulative Distribution']: cumdist = True dist = False else: cumdist = False clip = res['clip'] if clip == 'None': clip = None percentile = res['Clip Mode'] != 'Range' minval = res['Bottom'] maxval = res['Top'] showStatistics(key, domain, dist, cumdist, clip=clip, vmin=minval, vmax=maxval, percentile=percentile) def _show_domain(): _show_stats(True, False) def _show_dist(): _show_stats(False, True) def _close_stats_dia(): global _stat_dia # close any created 2d plots plot2d.closeAllPlots() # close the dialog if _stat_dia: try: _stat_dia.close() # this may fail if fialog closed by window manager except Exception: pass _stat_dia = None def showStatisticsDialog(): global _stat_dia if _stat_dia: _close_stats_dia() keys = list(SelectableStatsValues.keys()) _stat_dia = pg.Dialog( caption='Surface Statistics', items=[ _C('', [ _I('Value', itemtype='vradio', choices=keys), _I('neigh', text='Curvature Neighbourhood', value=1), _I('curval', text='Curvature Value', itemtype='vradio', choices=CurvatureValues), ]), _C('', [ _I('clip', itemtype='hradio', choices=['None', 'Top', 'Bottom', 'Both']), _I('Clip Mode', itemtype='hradio', choices=['Range', 'Percentile']), _G('Clip Values', check=True, items=[ _I('Top', 1.0), _I('Bottom', 0.0), ]), _I('Cumulative Distribution', False), ]), ], actions=[ ('Close', _close_stats_dia), ('Distribution', _show_dist), ('Show on domain', _show_domain)], default='Show on domain' ) _stat_dia.show()
[docs]def showSurfaceValue(S, txt, val, onEdges, legend=True): """Display a scalar value on the surface or its edges""" if onEdges: M = pf.Mesh(S.coords, S.edges) else: M = S fld = pf.Field(M, 'elemc', val) pg.drawField(fld, legend=legend)
# drawText(txt, (10, 240), size=18) def partitionByAngle(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: res = pg.askItems([ _I('angle', 60.), _I('firstprop', 1), _I('sort by', choices=['number', 'area', 'none']), ]) if res: pm.proj.remember(pm.selection) with pf.Timing() as t: p = S.partitionByAngle(angle=res['angle'], sort=res['sort by']) S.setProp(p + res['firstprop']) nprops = len(S.propSet()) print(f"Partitioned in {nprops} parts ({t.mem:.6f} sec.)") for p in S.propSet(): print(" p: %s; n: %s" % (p, (S.prop == p).sum())) pm.draw_sel() def showFeatureEdges(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: pm.draw_sel(color=0) res = pg.askItems([ _I('angle', 60.), _I('minangle', -60.), _I('ontop', False), ]) if res: ontop = res.pop('ontop') p = S.featureEdges(**res) M = pf.Mesh(S.coords, S.edges[p]) pg.draw(M, color='red', linewidth=3, bbox='last', nolight=True, ontop=ontop) ############################################################################# # Transformation of the vertex coordinates (based on Coords)
[docs]def cutWithPlane(): """Cut the selection with a plane.""" pm = pf.pmgr() FL = pm.get_sel(clas=TriSurface) if not FL: return dsize = pf.bbox(FL).dsize() esize = 10 ** (pf.at.niceLogSize(dsize)-5) res = pg.askItems(caption='Define the cutting plane', items=[ _I('p', [0.0, 0.0, 0.0], itemtype='point', text='Point'), _I('n', [1.0, 0.0, 0.0], itemtype='point', text='Normal'), _I('newprops', [1, 2, 2, 3, 4, 5, 6], text='New props'), _I('side', 'positive', itemtype='radio', text='Side', choices=['positive', 'negative', 'both']), _I('atol', esize, text='Tolerance'), ]) if res: parts = FL.cutWithPlane(**res) if res['side'] == 'both': parts_pos, parts_neg = zip(*parts) names_pos = [name + '/pos' for name in pm.selection] names_neg = [name + '/neg' for name in pm.selection] pf.PF.update2(names_pos, parts_pos) pf.PF.update2(names_neg, parts_neg) pm.set_sel(names_pos + names_neg) else: pm.replace_sel(parts)
[docs]def cutSelectionByPlanes(): """Cut the selection with one or more planes, which are already created.""" pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if not S: return name = pm.selection[0] planes = pm.get_sel(clas=Plane) if not planes: return res = pg.askItems(caption='Cutting parameters', items=[ _I('Tolerance', 0.), _I('Color by', 'side', itemtype='radio', choices=['side', 'element type']), _I('Side', 'both', itemtype='radio', choices=['positive', 'negative', 'both']), ]) if not res: return p = [plane.P for plane in planes] n = [plane.n for plane in planes] atol = res['Tolerance'] color = res['Color by'] side = res['Side'] if color == 'element type': newprops = [1, 2, 2, 3, 4, 5, 6] else: newprops = None fcuts = S.cutWithPlane(p, n, newprops=newprops, side=side, atol=atol) if side == 'both': Spos, Sneg = fcuts elif side == 'positive': Spos = fcuts Sneg = pf.TriSurface() elif side == 'negative': Sneg = fcuts Spos = pf.TriSurface() if color == 'side': Spos.setProp(2) Sneg.setProp(3) pm.set_sel([Spos, Sneg], [name+"/pos", name+"/neg"])
[docs]def intersectWithPlane(): """Intersect the selection with a plane.""" pm = pf.pmgr() FL = pm.get_sel(clas=TriSurface) if not FL: return res = pg.askItems(caption='Define the cutting plane', items=[ _I('Name suffix', 'intersect'), _I('Point', (0.0, 0.0, 0.0)), _I('Normal', (1.0, 0.0, 0.0)), ]) if res: suffix = res['Name suffix'] P = res['Point'] N = res['Normal'] M = [S.intersectionWithPlane(P, N) for S in FL] # TODO: fix from here pg.draw(M, color='red') pf.PF.update(dict([('%s/%s' % (n, suffix), m) for (n, m) in zip(pf.PM.selection, M)]))
[docs]def slicer(): """Slice the surface to a sequence of cross sections.""" pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if not S: return res = pg.askItems(caption='Define the slicing planes', items=[ _I('Direction', [1., 0., 0.]), _I('# slices', 20), ]) if res: axis = res['Direction'] nslices = res['# slices'] with pg.busyCursor(), pf.Timing("Sliced", auto=True): slices = S.slice(dir=axis, nplanes=nslices) print([s.nelems() for s in slices]) pm.set_sel(slices, suffix='/slices')
[docs]def spliner(): """Slice the surface to a sequence of cross sections.""" from pyformex import olist from pyformex.curve import BezierSpline pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if not S: return res = pg.askItems(caption='Define the slicing planes', items=[ _I('Direction', [1., 0., 0.]), _I('# slices', 20), # _I('remove_invalid', False), ]) if res: # TODO: this needs checking and cleanup axis = res['Direction'] nslices = res['# slices'] # remove_cruft = res['remove_invalid'] with pg.busyCursor(): slices = S.slice(dir=axis, nplanes=nslices) print([s.nelems() for s in slices]) split = [s.splitProp() for s in slices if s.nelems() > 0] split = olist.flatten(split) hasnan = [np.isnan(s.coords).any() for s in split] print(hasnan) print(sum(hasnan)) # print [s.closed for s in split] pf.PF.update({'%s/split' % pf.PM.selection[0]: split}) pg.draw(split, color='blue', bbox='last', view=None) splines = [BezierSpline(s.coords[s.elems[:, 0]], closed=True) for s in split] pg.draw(splines, color='red', bbox='last', view=None) pf.PF.update({'%s/splines' % pf.PM.selection[0]: splines})
def refine(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: res = pg.askItems(caption='Refine parameters', items=[ _I('max_edges', -1), _I('min_cost', -1.0), ]) if res: if res['max_edges'] <= 0: res['max_edges'] = None if res['min_cost'] <= 0: res['min_cost'] = None pm.replace_sel([S.refine(**res)])
[docs]def create_volume(): """Generate a volume tetraeder mesh inside a surface.""" pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: M = S.tetgen(quality=True) # TODO: set filter of pm to 'Mesh' pm.set_sel([M], ['tetmesh']) print("Created and selected tetraeder mesh 'tetmesh'")
################################################################### ########### The following functions are in need of a make-over # TODO: these should be implemented on TriSurface # def trim_border(elems, nodes, nb, visual=False): # """Removes the triangles with nb or more border edges. # Returns an array with the remaining elements. # """ # b = border(elems) # b = b.sum(axis=1) # trim = np.where(b>=nb)[0] # keep = np.where(b<nb)[0] # nelems = elems.shape[0] # ntrim = trim.shape[0] # nkeep = keep.shape[0] # print("Selected %s of %s elements, leaving %s" % (ntrim, nelems, nkeep)) # if visual and ntrim > 0: # prop = np.zeros(shape=(F.nelems(),), dtype=int32) # prop[trim] = 2 # red # prop[keep] = 1 # yellow # F = pf.Formex(nodes[elems], prop) # pg.clear() # pg.draw(F, view='left') # return elems[keep] # def trim_surface(): # check_surface() # res = pg.askItems(caption='Trim surface', items=[ # _I('Number of trim rounds', 1), # _I('Minimum number of border edges', 1), # ]) # n = int(res['Number of trim rounds']) # nb = int(res['Minimum number of border edges']) # print("Initial number of elements: %s" % elems.shape[0]) # for i in range(n): # elems = trim_border(elems, nodes, nb) # print("Number of elements after border removal: %s" % elems.shape[0]) # def read_tetgen(surface=True, volume=True): # """Read a tetgen model from files fn.node, fn.ele, fn.smesh.""" # from pyformex.gui.menus import tetgen # ftype = '' # if surface: # ftype += ' *.smesh' # if volume: # ftype += ' *.ele' # fn = pf.askFilename(pf.cfg['workdir'], "Tetgen files (%s)" % ftype) # nodes = elems =surf = None # if fn: # pf.chdir(fn) # project = utils.projectName(fn) # # set_project(project) # nodes, nodenrs = tetgen.readNodes(project+'.node') # # print("Read %d nodes" % nodes.shape[0]) # if volume: # elems, elemnrs, elemattr = tetgen.readElems(project+'.ele') # print("Read %d tetraeders" % elems.shape[0]) # pf.PF['volume'] = (nodes, elems) # if surface: # surf = tetgen.readSurface(project+'.smesh') # print("Read %d triangles" % surf.shape[0]) # pf.PF['surface'] = (nodes, surf) # if surface: # show_surface() # else: # show_volume() # def read_tetgen_surface(): # read_tetgen(volume=False) # def read_tetgen_volume(): # read_tetgen(surface=False) ################### Operations using gts library ######################## def check(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: sta, out = S.check() print((sta, out)) if sta == 3: pg.clear() pg.draw(S.select(out), color='red') pg.draw(S, color='black') def split(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: print(S.split(pf.PM.selection[0], verbose=True)) def coarsen(): pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if S: res = pg.askItems(caption='Coarsen surface', items=[ _I('min_edges', -1), _I('max_cost', -1.0), _I('mid_vertex', False), _I('length_cost', False), _I('max_fold', 1.0), _I('volume_weight', 0.5), _I('boundary_weight', 0.5), _I('shape_weight', 0.0), _I('progressive', False), _I('log', False), _I('verbose', False), ]) if res: if res['min_edges'] <= 0: res['min_edges'] = None if res['max_cost'] <= 0: res['max_cost'] = None pm.replace_sel([S.coarsen(**res)])
[docs]def boolean(): """Boolean operation on two surfaces. op is one of '+' : union, '-' : difference, '*' : interesection """ pm = pf.pmgr() S = pm.get_sel(clas=TriSurface) if len(S) != 2: pf.error("You should select exactly two surfaces!") return names = pm.selection ops = ['+ (Union)', '- (Difference)', '* (Intersection)'] res = pg.askItems(caption='Boolean Operation', items=[ _I('surface 1', choices=names, default=names[0]), _I('surface 2', choices=names, default=names[1]), _I('operation', choices=ops), _I('check self intersection', False), _I('verbose', False), ]) if res: name0, name1 = res['surface 1'], res['surface 2'] SA = pf.PF[name0] SB = pf.PF[name1] SC = SA.boolean(SB, op=res['operation'].strip()[0], check=res['check self intersection'], verbose=res['verbose']) if not SC: pf.error("The boolean operation on (name0, name1) failed") return name = next(pf.autoName('trisurface')) pm.set_sel([SC], [name], fname='TriSurface')
[docs]def intersection(): """Intersection curve of two surfaces.""" names = pf.PF.contents(clas=TriSurface) if len(names) == 0: pf.warning("You currently have no exported surfaces!") return res = pg.askItems(caption='Intersection Curve', items=[ _I('surface 1', choices=names), _I('surface 2', choices=names), _I('check self intersection', False), _I('verbose', False), ]) if res: SA = pf.PF[res['surface 1']] SB = pf.PF[res['surface 2']] SC = SA.intersection(SB, check=res['check self intersection'], verbose=res['verbose']) pf.PF.update({'__intersection_curve__': SC}) pg.draw(SC, color='red', linewidth=3)
[docs]def voxelize(): """Voxelize""" pass
################### Operations using instant-meshes ########################
[docs]def remesh(): """Remesh a TriSurface to a quality Tri3 and/or Quad4 Mesh. Shows a Dialog to set the parameters for the remeshing. Then creates and shows the new mesh. """ pm = pf.pmgr() S = pm.get_sel(clas=TriSurface, single=True) if not S: return # ask parameters _name = 'Remesh parameters' name = pm.selection[0] methods = ['acvd'] for m in ['instant-meshes']: if pf.External.has(m): methods.append(m) # use instant-meshes as default if available method = 'instant-meshes' if 'instant-meshes' in methods else methods[0] vertices = S.ncoords() faces = S.nelems() edglen = S.edgeLengths() scale = 0.5*(edglen.min()+edglen.max()) # smooth = 2 boundaries = not S.isClosedManifold() crease = 90 intrinsic = False res = pg.askItems(caption=_name, store=_name+'_data', items=[ _I('name', next(pf.NameSequence(name))), _I('method', method, choices=methods), _G('a', text='Parameters for acvd', items=[ _I('npoints', vertices), _I('ndiv', 3), ]), _G('i', text='Parameters for instant-meshes', items=[ _I('posy', 6), _I('rosy', 6), _I('resolution', choices=['vertices', 'faces', 'scale'], itemtype='combo', value='vertices'), _I('vertices', vertices), _I('faces', faces), _I('scale', scale), _I('smooth', 2), _I('boundaries', boundaries), _I('intrinsic', intrinsic), _I('crease', crease), ]), ], enablers=[ ('method', 'acvd', 'a'), ('method', 'instant-meshes', 'i'), ('resolution', 'vertices', 'vertices'), ('resolution', 'faces', 'faces'), ('resolution', 'scale', 'scale'), ]) # infile: :term:`path_like` # An .obj file containing a pure tri3 mesh. # outfile: :term:`path_like` # The output file with the quad (or quad dominated) Mesh. # It can be a .obj or .ply file. If not provided, it is generated # from the input file with the '.obj' suffix replaced 'with _quad.obj'. # threads: int # Number of threads to use in parallel computations. # deterministic: bool # If True, prefer (slower) deterministic algorithms. # crease: float # Dihedral angle threshold for creases. # smooth: int # Number of smoothing & ray tracing reprojection steps (default: 2). # dominant: bool # If True, generate a quad dominant mesh instead of a pure quad mesh. # The output may contain some triangles and pentagones as well. # intrinsic: bool # If True, use intrinsic mode (extrinsic is the default). # boundaries: bool # If True, align the result on the boundaries. # Only applies when the surface is not closed. # posy: 3 | 4 | 6 # Specifies the position symmetry type. # rosy: 2 | 4 | 6 # Specifies the orientation symmetry type. # Combinations: # posy rosy result # 3 6 quality tri3 # 4 4 quality quad4 if res: if pf.verbosity(2): print(f"Remeshing {name}") qname = res['name'] res.pop('name', None) for k in ['scale', 'faces', 'vertices']: if k != res['resolution']: res.pop(k, None) res.pop('resolution', None) with pf.Timing('Remeshing') as t: mesh = S.remesh(**res) if mesh: if pf.verbosity(2): print(f"Converted {name} to {mesh.elName()} Mesh" f" named {qname} in {t.mem:.6f} sec.") print(mesh) pm.set_sel([mesh], [qname]) else: if pf.verbosity(2): print("Conversion failed")
[docs]def tri2quad_auto(suffix='_quad4'): """Autoconvert a set of TriSurfaces to quad4 Meshes The outputs are stored with names equal to the input names plus the suffix. If a suffix '' is given, the input names are used and the input objects will be cleared from memory. """ pm = pf.pmgr() FL = pm.get_sel(clas=TriSurface) if FL: self.set_sel(Fl.remesh(), suffix=suffix, fname='Mesh')
########## The menu ########## menu_items = [ ("&Fix Normals", fixNormals, {'data': 'internal'}), ("&Fix Normals (admesh)", fixNormals, {'data': 'admesh'}), ("&Reverse Normals", reverseNormals), ("&Remove NonManifold Edges", removeNonManifold), ("&Statistics", showStatisticsDialog), ("&Refine", refine), ('&Remesh', remesh), ("&Create Points on Surface", createPointsOnSurface), ("&Partition By Angle", partitionByAngle), ("&Show Feature Edges", showFeatureEdges), ("&Show Border", showBorder), ("&Fill Border", fillBorders), ("&Delete Triangles", deleteTriangles), ("---", None), ("&Clip/Cut", [ ("&Clip", clipSelection), ("&Clip At Plane", clipAtPlane), ("&Cut With Plane", cutWithPlane), ("&Multiple Cut", cutSelectionByPlanes), ("&Intersection With Plane", intersectWithPlane), ("&Slicer", slicer), ("&Spliner", spliner), ]), ("---", None), ('&GTS functions', [ ('&Check surface', check), ('&Split surface', split), ("&Coarsen surface", coarsen), ("&Refine", refine), ("&Boolean operation on two surfaces", boolean), ("&Intersection curve of two surfaces", intersection), # ("&Voxelize the volume inside a surface", voxelize), ]), ('&Instant Meshes', [ ('&Quality remesh surface to tri3 or quad4 mesh', remesh), ('&Auto-convert multiple surfaces to quad4', tri2quad_auto), ]), # ("&Trim border",trim_surface), ("&Create volume mesh", create_volume), ("---", None), ] def menu_setup(menu): # Enable menus needing optional external software menu['&GTS functions'].setEnabled(pf.External.has('gts-extra') != '') menu['&Instant Meshes'].setEnabled(pf.External.has('instant-meshes') != '') # End