Skip to content

Commit

Permalink
Merge pull request #2133 from mikedh/feat/newline
Browse files Browse the repository at this point in the history
Release: check ascii exports for trailing newline
  • Loading branch information
mikedh authored Jan 31, 2024
2 parents 90a7ac5 + eb0a016 commit 021af1f
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 38 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim-bookworm AS base
FROM python:3.12-slim-bookworm AS base
LABEL maintainer="[email protected]"

# Install helper script to PATH.
Expand Down Expand Up @@ -63,7 +63,7 @@ COPY --chown=499 pyproject.toml .
COPY --chown=499 ./.git ./.git/

USER root
RUN trimesh-setup --install=test,gltf_validator,llvmpipe,binvox
RUN trimesh-setup --install=test,build,gltf_validator,llvmpipe,binvox
USER user

# install things like pytest
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
[project]
name = "trimesh"
requires-python = ">=3.7"
version = "4.1.0"
version = "4.1.1"
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
license = {file = "LICENSE.md"}
description = "Import, export, process, analyze and view triangular meshes."
Expand Down
1 change: 1 addition & 0 deletions tests/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ def check(item):
loaded = trimesh.load(os.path.join(dir_models, file_name))
except BaseException as E:
if raise_error:
log.error(f'failed to load {file_name}')
raise E
continue

Expand Down
8 changes: 8 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def test_export(self):

g.log.info("Export/import testing on %s", mesh.metadata["file_name"])

if isinstance(export, str):
assert export.endswith("\n"), f"{file_type} doesn't end with newline"

# if export is string or bytes wrap as pseudo file object
if isinstance(export, str) or isinstance(export, bytes):
file_obj = g.io_wrap(export)
Expand Down Expand Up @@ -180,6 +183,11 @@ def test_export(self):
assert len(r.vertices) == len(mesh.vertices)
assert len(r.faces) == len(mesh.faces)

if option.get("encoding", None) == "ascii":
with open(temp.name) as f:
exported = f.read()
assert exported.endswith("\n")

# manual cleanup
g.os.remove(temp.name)
g.os.remove(temp_off.name)
Expand Down
4 changes: 4 additions & 0 deletions trimesh/exchange/obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,10 @@ def export_obj(
if header is not None:
# add a created-with header to the top of the file
objects.appendleft(f"# {header}")

# add a trailing newline
objects.append("\n")

# combine elements into a single string
text = "\n".join(objects)

Expand Down
23 changes: 13 additions & 10 deletions trimesh/exchange/off.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import numpy as np

from .. import util
from ..geometry import triangulate_quads
from ..util import array_to_string, comment_strip, decode_text


def load_off(file_obj, **kwargs):
def load_off(file_obj, **kwargs) -> dict:
"""
Load an OFF file into the kwargs for a Trimesh constructor.
Expand All @@ -23,7 +23,7 @@ def load_off(file_obj, **kwargs):
text = file_obj.read()
# will magically survive weird encoding sometimes
# comment strip will handle all cases of commenting
text = util.comment_strip(util.decode_text(text)).strip()
text = comment_strip(decode_text(text)).strip()

# split the first key
_, header, raw = re.split("(COFF|OFF)", text, maxsplit=1)
Expand Down Expand Up @@ -58,7 +58,7 @@ def load_off(file_obj, **kwargs):
return kwargs


def export_off(mesh, digits=10):
def export_off(mesh, digits=10) -> str:
"""
Export a mesh as an OFF file, a simple text format
Expand All @@ -80,14 +80,17 @@ def export_off(mesh, digits=10):
faces_stacked = np.column_stack((np.ones(len(mesh.faces)) * 3, mesh.faces)).astype(
np.int64
)
export = "OFF\n"
# the header is vertex count, face count, another number
export += str(len(mesh.vertices)) + " " + str(len(mesh.faces)) + " 0\n"
export += (
util.array_to_string(mesh.vertices, col_delim=" ", row_delim="\n", digits=digits)
+ "\n"
export = "\n".join(
[
"OFF",
str(len(mesh.vertices)) + " " + str(len(mesh.faces)) + " 0",
array_to_string(mesh.vertices, col_delim=" ", row_delim="\n", digits=digits),
array_to_string(faces_stacked, col_delim=" ", row_delim="\n"),
"",
]
)
export += util.array_to_string(faces_stacked, col_delim=" ", row_delim="\n")

return export


Expand Down
28 changes: 18 additions & 10 deletions trimesh/exchange/ply.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,27 +342,35 @@ def export_ply(
_add_attributes_to_data_array(faces, mesh.face_attributes)

header.append(templates["outro"])
export = Template("".join(header)).substitute(header_params).encode("utf-8")
export = [Template("".join(header)).substitute(header_params).encode("utf-8")]

if encoding == "binary_little_endian":
if hasattr(mesh, "vertices"):
export += vertex.tobytes()
export.append(vertex.tobytes())
if hasattr(mesh, "faces"):
export += faces.tobytes()
export.append(faces.tobytes())
elif encoding == "ascii":
export_data = util.structured_array_to_string(
vertex, col_delim=" ", row_delim="\n"
export.append(
util.structured_array_to_string(vertex, col_delim=" ", row_delim="\n").encode(
"utf-8"
),
)

if hasattr(mesh, "faces"):
export_data += "\n"
export_data += util.structured_array_to_string(
faces, col_delim=" ", row_delim="\n"
export.extend(
[
b"\n",
util.structured_array_to_string(
faces, col_delim=" ", row_delim="\n"
).encode("utf-8"),
]
)
export += export_data.encode("utf-8")
export.append(b"\n")

else:
raise ValueError("encoding must be ascii or binary!")

return export
return b"".join(export)


def _parse_header(file_obj):
Expand Down
44 changes: 29 additions & 15 deletions trimesh/exchange/stl.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ def load_stl_ascii(file_obj):
"vertices": vertices.reshape((-1, 3)),
"face_normals": face_normals,
"faces": faces,
"metadata": {"name": name},
}

if len(kwargs) == 1:
Expand All @@ -239,17 +240,19 @@ def load_stl_ascii(file_obj):
return {"geometry": kwargs}


def export_stl(mesh):
def export_stl(mesh) -> bytes:
"""
Convert a Trimesh object into a binary STL file.
Parameters
---------
mesh: Trimesh object
mesh
Trimesh object to export.
Returns
---------
export: bytes, representing mesh in binary STL form
export
Represents mesh in binary STL form
"""
header = np.zeros(1, dtype=_stl_dtype_header)
if hasattr(mesh, "faces"):
Expand All @@ -265,7 +268,7 @@ def export_stl(mesh):
return export


def export_stl_ascii(mesh):
def export_stl_ascii(mesh) -> str:
"""
Convert a Trimesh object into an ASCII STL file.
Expand All @@ -275,8 +278,8 @@ def export_stl_ascii(mesh):
Returns
---------
export : str
Mesh represented as an ASCII STL file
export
Mesh represented as an ASCII STL file
"""

# move all the data that's going into the STL file into one array
Expand All @@ -285,17 +288,28 @@ def export_stl_ascii(mesh):
blob[:, 1:, :] = mesh.triangles

# create a lengthy format string for the data section of the file
format_string = "facet normal {} {} {}\nouter loop\n"
format_string += "vertex {} {} {}\n" * 3
format_string += "endloop\nendfacet\n"
format_string *= len(mesh.faces)
formatter = (
"\n".join(
[
"facet normal {} {} {}",
"outer loop",
"vertex {} {} {}\nvertex {} {} {}\nvertex {} {} {}",
"endloop",
"endfacet",
"",
]
)
) * len(mesh.faces)

# concatenate the header, data, and footer
export = "solid \n"
export += format_string.format(*blob.reshape(-1))
export += "endsolid"
# try applying the name from metadata if it exists
name = mesh.metadata.get("name", "")
if not isinstance(name, str):
name = ""
if len(name) > 80 or "\n" in name:
name = ""

return export
# concatenate the header, data, and footer, and a new line
return "\n".join(["solid {name}", formatter.format(*blob.reshape(-1)), "endsolid\n"])


_stl_loaders = {"stl": load_stl, "stl_ascii": load_stl}

0 comments on commit 021af1f

Please sign in to comment.