#
##
## 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),
('>S 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['>S functions'].setEnabled(pf.External.has('gts-extra') != '')
menu['&Instant Meshes'].setEnabled(pf.External.has('instant-meshes') != '')
# End