#
##
## This file is part of pyFormex 1.0.7 (Mon Jun 17 12:20:39 CEST 2019)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: http://pyformex.org
## Project page: http://savannah.nongnu.org/projects/pyformex/
## Copyright 2004-2019 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
## 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/.
##
"""Write geometry to file in a whole number of formats.
This module defines bothe the basic routines to write geometrical data
to a file and the specialized exporters to write files in a number of
well known standardized formats.
The basic routines are very versatile as well as optimized (using the version
in the pyFormex C-library) and allow to easily create new exporters for
other formats.
"""
from __future__ import absolute_import, division, print_function
import numpy as np
import pyformex as pf
from pyformex import Path
from pyformex.arraytools import checkArray
from pyformex import utils
if pf.PY3:
# We can not use the accelerated functions
from pyformex.lib.misc_e import tofile_int32, tofile_float32
else:
from pyformex.lib import misc
tofile_int32 = misc.tofile_int32
tofile_float32 = misc.tofile_float32
#
# DEVS: Do not use Int and Float here, but np.int32 and np.float32
#
[docs]def writeData(fil, data, sep='', fmt=None, end=''):
"""Write an array of numerical data to an open file.
Parameters:
- `fil`: an open file object
- `data`: a numerical array of int or float type
- `sep`: a string to be used as separator in case no `fmt` is specified.
If an empty string, the data are written in binary mode. This is the
default. For any other string, the data are written in ascii mode with
the specified string inserted as separator between any two items, and a
newline appended at the end.
In both cases, the data are written using the `numpy.tofile`
function.
- `fmt`: a format string compatible with the array data type.
If specified, the `sep` argument is ignored and the data are written
according to the specified format. This uses the pyFormex functions
`misc.tofile_int32` or `misc.tofile_float32`, which have accelerated
versions in the pyFormex C library. This also means that the data arrays
will be forced to type `float32` or `int32` before writing.
The format string should contain a valid format converter for a
a single data item in both Python and C. They should also contain
the necessary spacing or separator. Examples are '%5i ' for int data
and '%f,' or '%10.3e' for float data. The array will be converted
to a 2D array, keeping the length of the last axis. Then all elements
will be written row by row using the specified format string, and the
`end` string will be added after each row.
- `end`: a string to be written at the end of the data block (if no `fmt`)
or at the end of each row (with `fmt`). The default
value is a newline character.
"""
kind = data.dtype.kind
if isinstance(fmt, dict):
fmt = fmt.get(kind, None)
if not fmt or '%' not in fmt:
data.tofile(fil, sep)
else:
val = data.reshape(-1, data.shape[-1])
if kind == 'i':
tofile_int32(val.astype(np.int32), fil, fmt)
elif kind == 'f':
tofile_float32(val.astype(np.float32), fil, fmt)
else:
raise ValueError("Can not write data fo type %s" % data.dtype)
if end:
fil.write(end)
[docs]def writeIData(data, fil, fmt, ind=1):
"""Write an indexed array of numerical data to an open file.
ind = i: autoindex from i
array: use these indices
"""
kind = data.dtype.kind
val = data.reshape(-1, data.shape[-1])
nrows = val.shape[0]
if isinstance(ind, int):
ind = ind + np.arange(nrows)
else:
ind = ind.reshape(-1)
if ind.shape[0] != nrows:
raise ValueError("Index should have same length as data")
if kind == 'i':
raise RuntimeError("This is not implemented yet")
misc.tofile_iint32(val.astype(np.int32), fil, fmt)
elif kind == 'f':
misc.tofile_ifloat32(ind.astype(np.int32), val.astype(np.float32), fil, fmt)
else:
raise ValueError("Can not write data fo type %s" % data.dtype)
# Output of mesh file formats
[docs]def writeOFF(fn, mesh):
"""Write a mesh of polygons to a file in OFF format.
Parameters:
- `fn`: file name, by preference ending on '.off'
- `mesh`: a Mesh
"""
coords = mesh.coords
elems = mesh.elems
fil = open(fn, 'w')
fil.write("OFF\n")
fil.write("%d %d 0\n" % (coords.shape[0], elems.shape[0]))
writeData(fil, coords, fmt='%f ')
nelems = np.zeros_like(elems[:, :1])
nelems.fill(elems.shape[1])
elemdata = np.column_stack([nelems, elems])
writeData(fil, elemdata, fmt='%i ')
fil.close()
[docs]def writeOBJ(fn, mesh, name=None):
"""Write a mesh of polygons to a file in OBJ format.
Parameters:
- `fn`: file name, by preference ending on '.obj'
- `mesh`: a Mesh
- `name`: name of the Mesh to be written into the file. If None, and the
Mesh has an .attrib.name, that name will be used.
"""
from pyformex.plugins.export import ObjFile
if name is None and hasattr(mesh, 'attrib'):
name = mesh.attrib.name
with ObjFile(fn) as fil:
fil.write(mesh, name)
[docs]def writePLY(fn, mesh, comment=None):
"""Write a mesh to a file in PLY format.
Parameters:
- `fn`: file name, by preference ending on '.ply'
- `mesh`: a Mesh
- `comment`: an extra comment to add in the file header.
"""
from pyformex.plugins.export import PlyFile
with PlyFile(fn) as fil:
fil.write(mesh, comment)
# Output of surface file formats
[docs]def writeGTS(fn, coords, edges, faces):
"""Write a mesh of triangles to a file in GTS format.
Parameters:
- `fn`: file name, by preference ending on '.gts'
- `coords`: float array with shape (ncoords,3), with the coordinates of
`ncoords` vertices
- `edges`: int array with shape (nedges,2), with the definition of
`nedges` edges in function of the vertex indices
- `faces`: int array with shape (nfaces,3), with the definition of
`nfaces` triangles in function of the edge indices
"""
if coords.dtype.kind != 'f' or coords.ndim != 2 or coords.shape[1] != 3 or edges.dtype.kind != 'i' or edges.ndim != 2 or edges.shape[1] != 2 or faces.dtype.kind != 'i' or faces.ndim != 2 or faces.shape[1] != 3:
raise RuntimeError("Invalid type or shape of argument(s)")
fil = open(fn, 'w')
fil.write("%d %d %d\n" % (coords.shape[0], edges.shape[0], faces.shape[0]))
writeData(fil, coords, fmt='%f ')
writeData(fil, edges+1, fmt='%i ')
writeData(fil, faces+1, fmt='%i ')
fil.write("#GTS file written by %s\n" % pf.Version())
fil.close()
# Output of surface file formats
[docs]def writeSTL(f, x, n=None, binary=False, color=None):
"""Write a collection of triangles to an STL file.
Parameters:
- `fn`: file name, by preference ending with '.stl' or '.stla'
- `x`: (ntriangles,3,3) shaped array with the vertices of the
triangles
- `n`: (ntriangles,3) shaped array with the normals of the
triangles. If not specified, they will be calculated.
- `binary`: if True, the output file format will be a binary STL.
The default is an ascii STL. Note that creation of a binary STL
requires the extermal program 'admesh'.
- `color`: a single color can be passed to a binary STL and will be
stpored in the header.
"""
if not x.shape[1:] == (3, 3):
raise ValueError("Expected an (ntri,3,3) array, got %s" % x.shape)
if n is None:
from pyformex import geomtools
a, n = geomtools.areaNormals(x)
degen = geomtools.degenerate(a, n)
print("The model contains %d degenerate triangles" % degen.shape[0])
x = np.column_stack([n.reshape(-1, 1, 3), x])
if binary:
write_stl_bin(f, x, color)
else:
write_stl_asc(f, x)
[docs]def write_stl_bin(fn, x, color=None):
"""Write a binary stl.
Parameters:
- `x`: (ntri,4,3) float array describin ntri triangles.
The first item of each triangle is the normal, the other
three are the vertices.
- `color`: (4,) int array with values in the range 0..255. These are
the red, green, blue and alpha components of the color. This is a
single color for all the triangles, and will be stored in the header
of the STL file.
"""
fn = Path(fn)
x = checkArray(x, shape=(-1, 4, 3), kind='f')
if color is not None:
#color = checkArray(color, shape=(4,), kind='i').astype(np.uint8)
color = checkArray(color, shape=(4,), kind='u', allow='i').astype(np.uint8)
def addTriangle(i):
x[i].tofile(fil)
fil.write('\x00\x00')
print("Writing binary STL %s" % fn)
ver = pf.fullVersion()
if len(ver) > 50:
ver = ver[:50]
if color is None:
color = ''
else:
color = "COLOR=%4s" % color.tostring()
print("Adding %s to the header" % color)
with fn.open('wb') as fil:
head = "%-50s%-30s" % (ver, color)
fil.write(head)
ntri = x.shape[0]
print("Number of triangles: %s" % ntri)
np.array(ntri).astype(np.int32).tofile(fil)
x = x.astype(np.float32)
[addTriangle(i) for i in range(ntri)]
print("Finished writing binary STL, %s bytes" % fn.size())
[docs]def write_stl_asc(fn, x):
"""Write a collection of triangles to an ascii .stl file.
Parameters:
- `fn`: file name, by preference ending with '.stl' or '.stla'
- `x`: (ntriangles,3,3) shaped array with the vertices of the
triangles
"""
fn = Path(fn)
if not x.shape[1:] == (4, 3):
raise ValueError("Expected an (ntri,4,3) array, got %s" % x.shape)
print("Writing ascii STL %s" % fn)
with fn.open('w') as fil:
fil.write("solid Created by %s\n" % pf.fullVersion())
for e in x:
fil.write(" facet normal %s %s %s\n" % tuple(e[0]))
fil.write(" outer loop\n")
for p in e[1:]:
fil.write(" vertex %s %s %s\n" % tuple(p))
fil.write(" endloop\n")
fil.write(" endfacet\n")
fil.write("endsolid\n")
print("Finished writing ascii STL, %s bytes" % fn.size())
# End