# This file is part of pymadcad, distributed under license LGPL v3
import numpy as np
import numpy.lib.recfunctions as rfn
import os, tempfile
from functools import wraps
from hashlib import md5
from .mathutils import vec3, glm, inf, typedlist
from .mesh import Mesh, Wire
def filetype(name, type=None):
''' Get the name for the file format, using the given forced type or the name extension '''
if not type:
type = name[name.rfind('.')+1:]
if not type:
raise FileFormatError('unable to guess the file type')
return type
[docs]def read(name: str, type=None, **opts) -> Mesh:
''' Load a mesh from a file, guessing its file type '''
type = filetype(name, type)
reader = globals().get(type+'_read')
if reader:
return reader(name, **opts)
else:
raise FileFormatError('no read function available for format '+type)
[docs]def write(mesh: Mesh, name: str, type=None, **opts):
''' Write a mesh to a file, guessing its file type '''
type = filetype(name, type)
writer = globals().get(type+'_write')
if writer:
return writer(mesh, name, **opts)
else:
raise FileFormatError('no write function available for format '+type)
cachedir = tempfile.gettempdir() + '/madcad-cache'
caches = {}
[docs]def cache(filename: str, create: callable=None, name=None, storage=None, **opts) -> Mesh:
''' Small cachefile system, it allows to dump objects to files and to get them when needed.
It's particularly useful when working with other processes. The cached files are reloaded only when the cache files are newer than the memory cache data.
If specified, create() is called to provide the data, in case it doesn't exist in memory neither as file.
If specified, name is the cache name used to index the file it defaults to the `filename`.
If specified, storage is the dictionary used to storage cache data, defaults to io.caches.
'''
if not storage: storage = caches
if not name: name = filename
# create the cache file if it doesn't exist
if os.path.exists(filename):
cachedate = storage[name][0] if name in storage else -inf
filedate = os.path.getmtime(filename)
if cachedate < filedate:
storage[name] = (filedate, read(filename, **opts))
# load reload the file content if it's newer that the data in memory
else:
if name in storage: obj = storage[name][1]
elif create: obj = create()
else:
raise IOError("the cache file doesn't exist")
write(obj, filename, **opts)
storage[name] = (os.path.getmtime(filename), obj)
return storage[name][1]
def cachefunc(f):
''' Decorator to cache a function results.
Use it if you want to cache their result associated with the argument set used
'''
@wraps(f)
def repl(*args, **kwargs):
if not os.path.exists(cachedir):
os.makedirs(cachedir)
key = '{}/{}{}-{}.pickle'.format(
cachedir,
f.__module__ + '.' if f.__module__ else '',
f.__name__,
hex(int.from_bytes(
md5(repr((
*args,
sorted(list(kwargs.items()))
)).encode())
.digest(),
'little')),
)
return cache(key, lambda: f(*args, **kwargs))
return repl
'''
pickle files are the standard python serialized files, they are absolutely not secure ! so do not use it for something else than your own caching.
'''
import pickle
def pickle_read(file, **opts):
return pickle.load(open(file, 'rb'))
def pickle_write(obj, file, **opts):
return pickle.dump(obj, open(file, 'wb'))
'''
PLY is loaded using plyfile module https://github.com/dranjan/python-plyfile
using the specifications from https://web.archive.org/web/20161221115231/http://www.cs.virginia.edu/~gfx/Courses/2001/Advanced.spring.01/plylib/Ply.txt
(also locally available in ply-description.txt)
'''
try:
from plyfile import PlyData, PlyElement
except ImportError: pass
else:
from . import triangulation
def ply_read(file, **opts):
mesh = Mesh()
data = PlyData.read(file)
if 'vertex' not in data: raise FileFormatError('file must have a vertex buffer')
if 'face' not in data: raise FileFormatError('file must have a face buffer')
# collect points
mesh.points = typedlist(data['vertex'].data.astype('f8, f8, f8'), dtype=vec3)
# collect faces
faces = data['face'].data
if faces.dtype.names[0] == 'vertex_indices':
for face in faces['vertex_indices']:
#print(' ', type(face), face, face.dtype, face.strides)
if len(face) == 3: # triangle
mesh.faces.append(face)
elif len(face) > 3: # quad or other extended face
mesh += triangulation.triangulation_outline(Wire(mesh.points, face))
else:
mesh.faces = numpy_to_typedlist(faces.astype('u4'), dtype=uvec3)
# collect tracks
if 'group' in faces.dtype.names:
mesh.tracks = typedlist(faces['group'].astype('u4'), dtype='I')
else:
mesh.tracks = typedlist.full(0, len(mesh.faces), 'I')
# create groups (TODO find a way to get it from the file, PLY doesn't support non-scalar types)
mesh.groups = [None] * (max(mesh.tracks, default=-1)+1)
return mesh
def ply_write(mesh, file, **opts):
vertices = np.array(mesh.points, copy=False).astype(np.dtype([('x', 'f4'), ('y', 'f4'), ('z', 'f4')]))
faces = np.empty(len(mesh.faces), dtype=[('vertex_indices', 'u4', (3,)), ('group', 'u2')])
faces['vertex_indices'] = typedlist_to_numpy(mesh.faces, 'u4')
faces['group'] = typedlist_to_numpy(mesh.tracks, 'u4')
ev = PlyElement.describe(vertices, 'vertex')
ef = PlyElement.describe(faces, 'face')
PlyData([ev,ef], opts.get('text', False)).write(file)
'''
STL is loaded using numpy-stl module https://github.com/WoLpH/numpy-stl
'''
try:
import stl
except ImportError: pass
else:
from .mathutils import *
from .mesh import numpy_to_typedlist, typedlist_to_numpy
def stl_read(file, **opts):
stlmesh = stl.mesh.Mesh.from_file(file, calculate_normals=False)
trinum = stlmesh.points.shape[0]
mesh = Mesh(
numpy_to_typedlist(stlmesh.points.reshape(trinum*3, 3), vec3),
typedlist(uvec3(i, i+1, i+2) for i in range(0, 3*trinum, 3)),
)
mesh.options['name'] = stlmesh.name
return mesh
def stl_write(mesh, file, **opts):
stlmesh = stl.mesh.Mesh(np.zeros(len(mesh.faces), dtype=stl.mesh.Mesh.dtype), name=mesh.options.get('name'))
stlmesh.vectors[:] = typedlist_to_numpy(mesh.points, 'f4')[typedlist_to_numpy(mesh.faces, 'i4')]
stlmesh.save(file)
'''
OBJ is loaded using the pywavefront module https://github.com/pywavefront/PyWavefront
using the specifications from https://en.wikipedia.org/wiki/Wavefront_.obj_file
'''
try:
import pywavefront
except ImportError: pass
else:
def obj_read(file, **opts):
scene = pywavefront.Wavefront(file, parse=True, collect_faces=True)
points = [vec3(v[:3]) for v in scene.vertices]
faces = []
for sub in scene.meshes.values():
faces.extend(( tuple(f[:3]) for f in sub.faces ))
mesh = Mesh(points, faces)
if len(scene.meshes) == 1:
mesh.options['name'] = next(iter(scene.meshes))
return mesh
# no write function available at this time
#def obj_write(mesh, file, **opts):
'''
JSON is loaded using the builtin json module
always using the official json specifications
it can store many object types, not only shapes
'''
import json
class JSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (vec2,vec3,vec4,mat2,mat3,mat4,quat)):
return {'type':type(obj).__name__, 'content':list(obj)}
elif isinstance(obj, np.ndarray):
return {'type':'ndarray', 'dtype':obj.dtype, 'content':list(obj)}
elif isinstance(obj, Mesh):
return {'type':'Mesh', 'points': [tuple(p) for p in obj.points], 'faces':obj.faces, 'tracks':obj.tracks, 'groups':obj.groups}
elif isinstance(obj, Web):
return {'type':'Web', 'points': [tuple(p) for p in obj.points], 'edges':obj.edges, 'tracks':obj.tracks, 'groups':obj.groups}
else:
return json.JSONEncoder.default(self, obj)
def jsondecode(obj):
if 'type' in obj:
t = obj['type']
if t in {'vec2','vec3','vec4','mat2','mat3','mat4','quat'}:
return vec3(obj['content'])
elif t == 'ndarray':
return np.array(obj['content'], dtype=obj['dtype'])
elif t == 'Mesh':
return Mesh([vec3(p) for p in obj['points']], [tuple(f) for f in obj['faces']], obj['tracks'], obj['groups'])
elif t == 'Web':
return Mesh([vec3(p) for p in obj['points']], [tuple(f) for f in obj['edges']], obj['tracks'], obj['groups'])
else:
raise FileFormatError('unable to load json for dumped type {}', t)
return obj
def json_read(file, **opts):
return json.load(open(file, 'r'), cls=JSONDecoder, **opts)
def json_write(objs, file, **opts):
return json.dump(open(file, 'w'), object_hook=jsondecode, **opts)