#
##
## This file is part of pyFormex 2.0 (Mon Sep 14 12:29:05 CEST 2020)
## 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-2020 (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 both 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 optimizedand allow to
easily create new exporters for other formats.
"""
import sys
import numpy as np
import pyformex as pf
from pyformex.path import Path
from pyformex.formex import Formex
from pyformex.mesh import Mesh # noqa: F401 (used in doctests)
from pyformex import arraytools as at
from pyformex import utils
__all__ = ['writeOFF', 'writeOBJ', 'writePLY', 'writeGTS', 'writeSTL',
'writeData', 'writeIData']
[docs]def writeOFF(fn, mesh):
"""Write a mesh of polygons to a file in OFF format.
Parameters
----------
fn: :term:`path_like`
The output file name, commonly having a suffix '.off'.
mesh: Mesh
The Mesh to write to the file.
Notes
-----
See https://en.wikipedia.org/wiki/OFF_(file_format).
Examples
--------
>>> f = Path('test_filewrite.off')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writeOFF(f, M)
>>> print(f.read_text())
OFF
4 2 0
0.0 0.0 0.0
1.0 0.0 0.0
1.0 1.0 0.0
0.0 1.0 0.0
3 0 1 2
3 2 3 0
<BLANKLINE>
>>> f.remove()
"""
fn = Path(fn)
pf.verbose(1, f"Write OFF file {fn}")
coords = mesh.coords
elems = mesh.elems
with utils.File(fn, 'wb') as fil:
fil.write(b"OFF\n")
fil.write(b"%d %d 0\n" % (coords.shape[0], elems.shape[0]))
writeData(fil, coords, fmt='%s', sep=' ')
nelems = np.full_like(elems[:, :1], elems.shape[1])
elemdata = np.column_stack([nelems, elems])
writeData(fil, elemdata, fmt='%i', sep=' ')
pf.verbose(2, f"File size: {fn.size()} bytes")
[docs]def writeOBJ(fn, mesh, name=None):
"""Write a mesh of polygons to a file in OBJ format.
Parameters
----------
fn: :term:`path_like`
The output file name, commonly having a suffix '.obj'.
mesh: Mesh
The Mesh to write to the file.
name: str, optional
Name of the Mesh to be written into the file. If not provided None
and the Mesh has an .attrib.name, that name will be used.
Notes
-----
See https://en.wikipedia.org/wiki/OBJ_(file_format).
Examples
--------
>>> f = Path('test_filewrite.obj')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writeOBJ(f, M, name='test')
>>> print(f.read_text())
# .obj file written by pyFormex ...
o test
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 1.0 1.0 0.0
v 0.0 1.0 0.0
f 1 2 3
f 3 4 1
# End
<BLANKLINE>
>>> f.remove()
"""
fn = Path(fn)
pf.verbose(1, f"Write OBJ file {fn.absolute()}")
coords = mesh.coords
elems = mesh.elems
if name is None and hasattr(mesh, 'attrib'):
name = mesh.attrib.name
with utils.File(fn, 'w') as fil:
fil.write("# .obj file written by %s\n" % pf.Version())
if name is not None:
fil.write("o %s\n" % str(name))
for v in coords:
fil.write("v %s %s %s\n" % tuple(v))
# element code: p(oint), l(ine) or f(ace)
nplex = mesh.nplex()
code = {1: 'p', 2: 'l'}.get(nplex, 'f')
s = code+(' %s'*nplex)+'\n'
for e in elems+1: # .obj format starts at 1
fil.write(s % tuple(e))
fil.write('# End\n')
pf.verbose(2, f"File size: {fn.size()} bytes")
[docs]def writePLY(fn, mesh, comment=None, color_table=None, binary=False):
"""Write a mesh to a file in PLY format.
Parameters
----------
fn: :term:`path_like`
The output file name, commonly having a suffix '.ply'.
mesh: Mesh
The Mesh to write to the file.
comment: str, optional
An extra comment to add in the file header.
Notes
-----
See `<https://en.wikipedia.org/wiki/PLY_(file_format)>`_).
Examples
--------
>>> f = Path('test_filewrite.ply')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writePLY(f, M, comment='This is a test')
>>> print(f.read_text())
ply
format ascii 1.0
comment .ply file written by pyFormex ...
comment This is a test
element vertex 4
property float x
property float y
property float z
element face 2
property list uchar int vertex_indices
end_header
0.0 0.0 0.0
1.0 0.0 0.0
1.0 1.0 0.0
0.0 1.0 0.0
3 0 1 2
3 2 3 0
<BLANKLINE>
>>> f.remove()
"""
fn = Path(fn)
pf.verbose(1, f"Write PLY file {fn.absolute()}")
binary = False # currently only ascii
color_table = None # broken anyway
coords = mesh.coords
elems = mesh.elems
with utils.File(fn, 'w') as fil:
if binary:
fmt = 'binary_' + sys.byteorder + '_endian 1.0'
else:
fmt = 'ascii 1.0'
fil.write(f"""ply
format {fmt}
comment .ply file written by {pf.Version()}
""")
if comment is not None:
fil.write("comment %s\n" % str(comment))
nplex = mesh.nplex()
el_type = {2: 'edge'}.get(nplex, 'face')
vcol_type = ecol_type = ''
if el_type == 'edge':
eprop_type = "property int vertex1\nproperty int vertex2"
else:
eprop_type = "property list uchar int vertex_indices"
if color_table is not None:
# color_table currently is a list of 2 elements. The first entry
# is a string that can assume 2 values 'v' or 'e', to indicate
# whether the color table represents nodal or element
# values. The second entry is an array of shape (ncoords,3) or
# (nelems,3) of interger RGB values between 0 and 255. If RGB
# values are passed as float between 0 and 1, they will be
# converted to RGB integers.
color_location, color_table = color_table
if not(np.issubclass_(color_table.dtype.type, np.integer)):
color_table = (255*color_table.clip(0., 1.)).astype(np.integer)
# float can be added but they are implemented yet
cprop_type = {}.get(color_table.dtype, 'uchar')
cprop_types = f"""\
property {cprop_type} red
property {cprop_type} green
property {cprop_type} blue
"""
if color_location == 'v':
vcol_type = cprop_types
coords = np.hstack([coords, color_table])
if color_location == 'e':
ecol_type = cprop_types
elems = np.hstack([elems, color_table])
fil.write(f"""\
element vertex {mesh.ncoords()}
property float x
property float y
property float z
{vcol_type}element {el_type} {mesh.nelems()}
{eprop_type}{ecol_type}
end_header
""")
writeData(fil, coords, fmt='%s', sep=' ')
nelems = np.full_like(elems[:, :1], mesh.elems.shape[1])
elemdata = np.column_stack([nelems, elems])
writeData(fil, elemdata, fmt='%i', sep=' ')
pf.verbose(2, f"File size: {fn.size()} bytes")
# Output of surface file formats
[docs]def writeGTS(fn, surf):
"""Write a TriSurface to a file in GTS format.
Parameters
----------
fn: :term:`path_like`
The output file name, commonly having a suffix '.gts'.
surf: TriSurface
The TriSurface to write to the file.
Examples
--------
>>> f = Path('test_filewrite.gts')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writeGTS(f, M.toSurface())
>>> print(f.read_text())
4 5 2
0.000000 0.000000 0.000000
1.000000 0.000000 0.000000
1.000000 1.000000 0.000000
0.000000 1.000000 0.000000
1 2
2 3
3 1
3 4
4 1
1 2 3
4 5 3
#GTS file written by pyFormex ...
<BLANKLINE>
>>> f.remove()
"""
fn = Path(fn)
from .trisurface import TriSurface
if not isinstance(surf, TriSurface):
raise ValueError("Expected TriSurface as second argument'")
pf.verbose(1, f"Write GTS file {fn.absolute()}")
coords = surf.coords
edges = surf.getEdges()
faces = surf.getElemEdges()
with utils.File(fn, 'wb') as fil:
fil.write(f"{coords.shape[0]} {edges.shape[0]} "
f"{faces.shape[0]}\n".encode('latin1'))
writeData(fil, coords, fmt='%f', sep=' ')
writeData(fil, edges+1, fmt='%i', sep=' ')
writeData(fil, faces+1, fmt='%i', sep=' ')
fil.write(f"#GTS file written by {pf.Version()}\n".encode('latin1'))
pf.verbose(2, f"File size: {fn.size()} bytes")
[docs]def writeSTL(fn, x, n=None, binary=False, color=None):
"""Write a collection of triangles to an STL file.
Parameters
----------
fn: :term:`path_like`
The output file name, commonly having a suffix '.stl' or
'.stla' (for ascii output) or '.stlb' (for binary output).
x: Coords | Formex
A Coords or Formex with shape (ntriangles,3,3) holding the coordinates of
the vertices of the triangles to write to the file.
n: Coords, optional
A Coords with shape (ntriangles,3) holding the normal vectors to the
triangles. If not specified, they will be calculated.
binary: bool
If True, the output file format will be a binary STL.
The default is an ascii STL.
color: (4,) int array
An int array with 4 values in the range 0..255. These are
the red, green, blue and alpha components of a single color for all
the triangles. It will be stored in the header of a binary
STL file.
Note
----
The color can only be used with a binary STL format, and
is not recognized by all STL processing software.
Warning
-------
The STL format stores a loose collection of triangles and does
not include connectivity information between the triangles.
Therefore the use of this format for intermediate storage is
**strongly discouraged**, as many processing algorithms will need
to build the connectivity information over and again, which may
lead to different results depending on round-off errors.
The STL format should only be used as a **final export** to
e.g. visualisation methods or machining processes.
Examples
--------
>>> f = Path('test_filewrite.stl')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writeSTL(f, M.toFormex())
>>> print(f.read_text())
solid Created by pyFormex ...
facet normal 0.0 0.0 1.0
outer loop
vertex 0.0 0.0 0.0
vertex 1.0 0.0 0.0
vertex 1.0 1.0 0.0
endloop
endfacet
facet normal 0.0 0.0 1.0
outer loop
vertex 1.0 1.0 0.0
vertex 0.0 1.0 0.0
vertex 0.0 0.0 0.0
endloop
endfacet
endsolid
<BLANKLINE>
>>> f.remove()
"""
fn = Path(fn)
if isinstance(x, Formex):
x = x.coords
if not x.shape[1:] == (3, 3):
raise ValueError("Expected an (ntri,3,3) array, got %s" % x.shape)
stltype = 'binary' if binary else 'ascii'
pf.verbose(1, f"Write {stltype} STL file {fn.absolute()}")
if n is None:
from pyformex import geomtools
a, n = geomtools.areaNormals(x)
ndegen = geomtools.degenerate(a, n).shape[0]
if ndegen > 0:
pf.verbose(2, f"The model contains {ndegen} degenerate triangles")
x = np.column_stack([n.reshape(-1, 1, 3), x])
x = at.checkArray(x, shape=(-1, 4, 3), kind='f')
pf.verbose(2, f"Writing {x.shape[0]} triangles")
mode = 'wb' if binary else 'w'
with utils.File(fn, mode) as fil:
if binary:
write_stl_bin(fil, x, color)
else:
write_stl_asc(fil, x)
pf.verbose(2, f"File size: {fn.size()} bytes")
[docs]def write_stl_bin(fil, x, color=None):
"""Write a binary stl.
Note
----
This is a low level routine for use in writeSTL. It is not intended
to be used directly.
Parameters
----------
fil: :term:`file_like`
The file to write the data to. It can be any object supporting
the write(bytes) method, like a file opened in binary write mode.
x: (ntri,4,3) float array
Array with 1 normal and 3 vertices and 1 normal per triangle.
color: (4,) int array, optional
Four color components in the range 0..255: red, green, blue and alpha.
If specified, these will be stored in the header and **may** be
recognized by some other software.
Examples
--------
>>> f = Path('test_filewrite.stl')
>>> M = Mesh(eltype='quad4').convert('tri3-u')
>>> writeSTL(f, M.toFormex().coords, binary=True, color=[255,0,0,128])
>>> from .fileread import readSTL
>>> x, n, c = readSTL(f)
>>> print(x)
[[[ 0. 0. 0.]
[ 1. 0. 0.]
[ 1. 1. 0.]]
<BLANKLINE>
[[ 1. 1. 0.]
[ 0. 1. 0.]
[ 0. 0. 0.]]]
>>> print(n)
[[ 0. 0. 1.]
[ 0. 0. 1.]]
>>> print(c)
(1.0, 0.0, 0.0)
>>> f.remove()
"""
if color is not None:
color = at.checkArray(color, shape=(4,),
kind='u', allow='i').astype(np.uint8)
ver = pf.fullVersion().encode('latin1')
if len(ver) > 50:
ver = ver[:50]
if color is None:
color = b''
else:
color = b" COLOR=%c%c%c%c" % tuple(color)
pf.verbose(2, "Adding %s to the header" % color)
head = b"%-50s %-29s" % (ver, color)
fil.write(head)
ntri = x.shape[0]
np.array(ntri).astype(np.int32).tofile(fil)
x = x.astype(np.float32)
for i in range(ntri):
x[i].tofile(fil)
fil.write(b'\x00\x00')
[docs]def write_stl_asc(fil, x):
"""Write a collection of triangles to an ascii .stl file.
Note
----
This is a low level routine for use in writeSTL. It is not intended
to be used directly.
Parameters
----------
fil: :term:`file_like`
The file to write the data to. It can be any object supporting
the write(bytes) method, like a file opened in binary write mode.
x: (ntri,4,3) float array
Array with 1 normal and 3 vertices and 1 normal per triangle.
"""
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")
[docs]def writeData(fil, data, fmt=None, sep='', end='\n'):
"""Write an array of numerical data to an open file.
Parameters
----------
fil: :term:`file_like`
The file to write the data to. It can be any object supporting
the write(bytes) method, like a file opened in binary write mode.
data: :term:`array_like`
A numerical array of int or float type. For output, the array
will be reshaped to a 2D array, keeping the length of the last axis.
fmt: str, optional
A format string compatible with the array data type.
If not provided, the data are written using :func:`numpy.tofile`.
If provided, the data are written in text mode using
:func:`numpy.savetxt` with the specified `fmt`. The `sep` string
is inserted as delimiter between individual items and the `end`
string is inserted at the end of each row.
The format string should contain a valid format converter for a
a single data item. It may also contain the necessary spacing or
separator. Examples are '%5i ' for int data and '%f,' or '%10.3e'
for float data.
sep: str, optional
A string to be used as separator between single items.
If an empty string and no `fmt` is provided, 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.
end: str, optional
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.
Examples
--------
>>> i = np.eye(3)
>>> f = Path('test_filewrite.out')
>>> with f.open('w') as fil:
... writeData(fil,i,sep=' ')
>>> f.size()
35
>>> print(f.read_text())
1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
>>> with f.open('w') as fil:
... writeData(fil,i,fmt='%.4f',sep=' ')
>>> f.size()
63
>>> print(f.read_text())
1.0000 0.0000 0.0000
0.0000 1.0000 0.0000
0.0000 0.0000 1.0000
<BLANKLINE>
>>> f.remove()
"""
if fmt is None:
data.tofile(fil, sep)
else:
np.savetxt(fil, data, fmt=fmt, delimiter=sep, newline=end, header='',
footer='', comments='# ') # REMOVED encoding=None)
[docs]def writeIData(fil, data, fmt, ind=1, sep=' ', end='\n'):
"""Write an indexed array of numerical data to an open file.
Parameters
----------
fil: :term:`file_like`
The file to write the data to. It can be any object supporting
the write(bytes) method, like a file opened in binary write mode.
data: :term:`array_like`
A numerical array of int or float type. For output, the array
will be reshaped to a 2D array, keeping the length of the last axis.
fmt: str
A format string compatible with the array data type.
The data are written in text mode using
:func:`numpy.savetxt` with the specified `fmt`. The `sep` string
is inserted as delimiter between individual items and the `end`
string is inserted at the end of each row.
The format string should contain a valid format converter for a
a single data item. It may also contain the necessary spacing or
separator. Examples are '%5i ' for int data and '%f,' or '%10.3e'
for float data.
ind: int or int :term:`array_like`
The row indices to write with the data. If an array, its length
should be equal to the numbe of rows in the (2D-reshaped) `data`
array. If a single int, it specifies the index for the first row,
and the value will be automatically incremented for the other rows.
sep: str, optional
A string to be used as separator between single items.
If an empty string and no `fmt` is provided, 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.
end: str, optional
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.
Examples
--------
>>> i = np.eye(3)
>>> f = Path('test_filewrite.out')
>>> with f.open('w') as fil:
... writeIData(fil,i,fmt='%.4f',sep=' ')
>>> f.size()
72
>>> print(f.read_text())
1 1.0000 0.0000 0.0000
2 0.0000 1.0000 0.0000
3 0.0000 0.0000 1.0000
<BLANKLINE>
>>> f.remove()
"""
val = data.reshape(-1, data.shape[-1])
nrows = val.shape[0]
if at.isInt(ind):
ind = ind + np.arange(nrows)
else:
ind = ind.reshape(-1)
if ind.shape[0] != nrows:
raise ValueError("Index should have same length as data")
# We need to write row by row
for i in range(nrows):
fil.write(f"{ind[i]:d} ")
np.savetxt(fil, val[i].reshape(1, -1), fmt=fmt,
delimiter=sep, newline=end)
# End