Source code for madcad.mesh.container

# This file is part of pymadcad,  distributed under license LGPL v3

from copy import copy, deepcopy
from random import random
import numpy as np
import numpy.lib.recfunctions as rfn
from array import array
from collections import OrderedDict, Counter
from numbers import Integral, Real
import math

from ..mathutils import *
from ..asso import Asso
from .. import hashing
from .. import settings


[docs]class MeshError(Exception): ''' Inconsistent data in mesh ''' pass
class NMesh(object): ''' Common methods for points container (typically Mesh, Web, Wire) ''' # BEGIN --- data management --- def own(self, **kwargs) -> 'Self': ''' Return a copy of the current mesh, which attributes are referencing the original data or duplicates if demanded Example: >>> b = a.own(points=True, faces=False) >>> b.points is a.points False >>> b.faces is a.faces True ''' new = copy(self) for name, required in kwargs.items(): if required: setattr(new, name, deepcopy(getattr(self, name))) return new def option(self, **kwargs) -> 'self': ''' Update the internal options with the given dictionary and the keywords arguments. This is only a shortcut to set options in a method style. ''' self.options.update(kwargs) return self def transform(self, trans) -> 'Self': ''' Apply the transform to the points of the mesh, returning the new transformed mesh''' trans = transformer(trans) transformed = copy(self) transformed.points = typedlist((trans(p) for p in self.points), dtype=vec3) return transformed def mergeclose(self, limit=None) -> dict: ''' Merge points below the specified distance, or below the precision return a dictionary of points remapping {src index: dst index} O(n) implementation thanks to hashing ''' if limit is None: limit = self.precision() merges = {} points = hashing.PointSet(limit) for i in range(len(self.points)): used = points.add(self.points[i]) if used != i: merges[i] = used self.points = points.points self.mergepoints(merges) return merges def stripgroups(self) -> list: ''' Remove groups that are used by no faces. return the reindex list. ''' self.groups, self.tracks, reindex = striplist(self.groups, self.tracks) return reindex def mergegroups(self, defs=None, merges=None) -> 'self': ''' Merge the groups according to the merge dictionary The new groups associated can be specified with defs The former unused groups are not removed from the buffer and the new ones are appended If merges is not provided, all groups are merged, and defs is the data associated to the only group after the merge ''' if merges is None: self.groups = [defs] self.tracks = typedlist.full(0, len(self.tracks), 'I') else: if defs: l = len(self.groups) self.groups.extend(defs) else: l = 0 for i,t in enumerate(self.tracks): if t in merges: self.tracks[i] = merges[t]+l return self def finish(self) -> 'self': ''' Finish and clean the mesh note that this operation can cost as much as other transformation operation job done - mergeclose - strippoints - stripgroups - check ''' self.mergeclose() self.strippoints() self.stripgroups() self.check() return self # END BEGIN --- verification methods --- def isvalid(self): ''' Return true if the internal data is consistent (all indices referes to actual points and groups) ''' try: self.check() except MeshError: return False else: return True # END BEGIN --- selection methods --- def pointat(self, point: vec3, neigh=NUMPREC) -> int: ''' Return the index of the first point at the given location, or None ''' for i,p in enumerate(self.points): if distance(p,point) <= neigh: return i def pointnear(self, point: vec3) -> int: ''' Return the nearest point the the given location ''' return min( range(len(self.points)), lambda i: distance(self.points[i], point)) def qualify(self, *quals, select=None, replace=False) -> 'self': ''' Set a new qualifier for the given groups Parameters: quals: the qualifiers to enable for the selected mesh groups select (iterable): if specified, only the groups having all those qualifiers will be added the new qualifiers replace (bool): if True, the qualifiers in select will be removed Example: >>> pool = meshb.qualify('part-a') + meshb.qualify('part-b') >>> set(meshb.faces) == set(pool.group('part-b').faces) True >>> chamfer(mesh, ...).qualify('my-group', select='chamfer', replace=True) >>> mesh.group('my-group') <Mesh ...> ''' if select is None: it = range(len(self.groups)) else: it = self.qualified_groups(select) for i in it: group = self.groups[i] if group is None: self.groups[i] = group = {} if not hasattr(group, '__setitem__'): raise TypeError('cannot qualify a group that is not a dictionary') if replace: for key in select: if key in group: del group[key] for key in quals: group[key] = None return self def qualified_indices(self, quals): ''' Yield the faces indices when their associated group are matching the requirements ''' if isinstance(quals, int): yield from self.qualified_indices({quals}) elif isinstance(quals, str): yield from self.qualified_indices([quals]) elif not quals: yield from range(len(self.tracks)) elif isinstance(next(iter(quals)), int): if not isinstance(quals, set): quals = set(quals) for i,t in enumerate(self.tracks): if t in quals: yield i else: inclusive = None in quals seen = {} for i,t in enumerate(self.tracks): if t in seen: ok = seen[t] elif self.groups[t]: ok = seen[t] = all(k in self.groups[t] for k in quals) else: ok = inclusive if ok: yield i def qualified_groups(self, quals): ''' Yield the groups indices when they are matching the requirements ''' if isinstance(quals, int): yield quals elif isinstance(quals, str): yield from self.qualified_groups([quals]) elif not quals: yield from range(len(self.groups)) elif isinstance(next(iter(quals)), int): yield from quals else: inclusive = None in quals for i, group in enumerate(self.groups): if group and all(key in group for key in quals) or inclusive: yield i # END BEGIN --- extraction methods --- def maxnum(self) -> float: ''' Maximum numeric value of the mesh, use this to get an hint on its size or to evaluate the numeric precision ''' m = 0 for p in self.points: for v in p: a = abs(v) if a > m: m = a return m def precision(self, propag=3) -> float: ''' Numeric coordinate precision of operations on this mesh, allowed by the floating point precision ''' return self.maxnum() * NUMPREC * (2**propag) def usepointat(self, point, neigh=NUMPREC) -> int: ''' Return the index of the first point in the mesh at the location. If none is found, insert it and return the index ''' i = self.pointat(point, neigh=neigh) if i is None: i = len(self.points) self.points.append(point) return i def box(self) -> Box: ''' Return the extreme coordinates of the mesh (vec3, vec3) ''' if not self.points: return Box() p = next(filter(isfinite, self.points)) max = deepcopy(p) min = deepcopy(p) for pt in self.points: for i in range(3): if pt[i] < min[i]: min[i] = pt[i] elif pt[i] > max[i]: max[i] = pt[i] return Box(min, max) def barycenter_points(self) -> vec3: ''' Barycenter of points used ''' return sum(self.points[i] for i in self.indices) / len(self.indices) # END
[docs]def numpy_to_typedlist(array: 'ndarray', dtype) -> 'typedlist': ''' Convert a numpy.ndarray into a typedlist with the given dtype, if the conversion is possible term to term ''' ndtype = np.array(typedlist(dtype)).dtype if ndtype.fields: return typedlist(rfn.unstructured_to_structured(array).astype(ndtype, copy=False), dtype) else: return typedlist(array.astype(ndtype, copy=False), dtype)
[docs]def typedlist_to_numpy(array: 'typedlist', dtype) -> 'ndarray': ''' Convert a typedlist to a numpy.ndarray with the given dtype, if the conversion is possible term to term ''' tmp = np.array(array, copy=False) if tmp.dtype.fields: return rfn.structured_to_unstructured(tmp, dtype) else: return tmp.astype(dtype)
[docs]def ensure_typedlist(obj, dtype): ''' Return a typedlist with the given dtype, create it from whatever is in obj if needed ''' if isinstance(obj, typedlist) and obj.dtype == dtype: return obj else: return typedlist(obj, dtype)
# ------ connectivity tools -------
[docs]def edgekey(a,b): ''' Return a key for a non-directional edge ''' if a < b: return (a,b) else: return (b,a)
[docs]def facekeyo(a,b,c): ''' Return a key for an oriented face ''' if a < b and b < c: return (a,b,c) elif a < b: return (c,a,b) else: return (b,c,a)
[docs]def arrangeface(f, p): ''' Return the face indices rotated the way the `p` is the first one ''' if p == f[1]: return f[1],f[2],f[0] elif p == f[2]: return f[2],f[0],f[1] else: return f
[docs]def arrangeedge(e, p): if p == e[1]: return e[1], e[0] else: return e
[docs]def connpp(ngons): ''' Point to point connectivity input is a list of ngons (tuple of 2 to n indices) ''' conn = {} for loop in ngons: for i in range(len(loop)): for a,b in ((loop[i-1],loop[i]), (loop[i],loop[i-1])): if a not in conn: conn[a] = [b] elif b not in conn[a]: conn[a].append(b) return conn
[docs]def connef(faces): ''' Connectivity dictionary, from oriented edge to face ''' conn = {} for i,f in enumerate(faces): for e in ((f[0],f[1]), (f[1],f[2]), (f[2],f[0])): conn[e] = i return conn
[docs]def connpe(edges): conn = Asso() for i,edge in enumerate(edges): for p in edge: conn.add(p,i) return conn
[docs]def connexity(links): ''' Return the number of links referencing each point as a dictionary {point: num links} ''' reach = {} for l in links: for p in l: reach[p] = reach.get(p,0) +1 return reach
[docs]def suites(lines, oriented=True, cut=True, loop=False): ''' Return a list of the suites that can be formed with lines. `lines` is an iterable of edges Parameters: oriented: specifies that (a,b) and (c,b) will not be assembled cut: cut suites when they are crossing each others Return a list of the sequences that can be formed ''' lines = list(lines) # get contiguous suite of points suites = [] while lines: suite = list(lines.pop()) found = True while found: found = False for i,edge in enumerate(lines): if edge[-1] == suite[0]: suite[0:1] = edge elif edge[0] == suite[-1]: suite[-1:] = edge # for unoriented lines elif not oriented and edge[0] == suite[0]: suite[0:1] = reversed(edge) elif not oriented and edge[-1] == suite[-1]: suite[-1:] = reversed(edge) else: continue lines.pop(i) found = True break if loop and suite[-1] == suite[0]: break suites.append(suite) # cut at suite intersections (sub suites or crossing suites) if cut: reach = {} for suite in suites: for p in suite: reach[p] = reach.get(p,0) + 1 for suite in suites: for i in range(1,len(suite)-1): if reach[suite[i]] > 1: suites.append(suite[i:]) suite[i+1:] = [] break return suites
# ----- internal helpers ------ def reprarray(array, name): content = ', '.join((repr(e) for e in array)) return '['+content+']' def striplist(points, indices): used = [False] * len(points) for index in indices: if isinstance(index, int): used[index] = True else: for p in index: used[p] = True optimized = typedlist(dtype=points.dtype) if isinstance(points, typedlist) else [] reindices = typedlist(dtype=indices.dtype) reindex = typedlist(dtype='i', reserve=len(points)) for p, u in zip(points, used): if u: reindex.append(len(optimized)) optimized.append(p) else: reindex.append(-1) for index in indices: if isinstance(index, int): reindices.append(reindex[index]) else: reindices.append([reindex[i] for i in index]) return optimized, reindices, reindex