Skip to content

Commit

Permalink
Merge pull request #2142 from mikedh/fix/nan
Browse files Browse the repository at this point in the history
Release: NaN exports + Update ruff
  • Loading branch information
mikedh authored Feb 2, 2024
2 parents 20b4f17 + d554f93 commit 11cd6f8
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 46 deletions.
14 changes: 7 additions & 7 deletions 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.2"
version = "4.1.3"
authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
license = {file = "LICENSE.md"}
description = "Import, export, process, analyze and view triangular meshes."
Expand Down Expand Up @@ -112,7 +112,12 @@ all = ["trimesh[easy,recommend,test]"]

[tool.ruff]
target-version = "py37"
line-length = 90


# See https://github.com/charliermarsh/ruff#rules for error code definitions.

[tool.ruff.lint]
select = [
# "ANN", # annotations
"B", # bugbear
Expand All @@ -121,7 +126,7 @@ select = [
"F", # flakes
"I", # import sorting
"RUF100", # meta
"U", # upgrade
"UP", # upgrade
"W", # style warnings
"YTT", # sys.version
]
Expand All @@ -133,9 +138,4 @@ ignore = [
"E501", # Line too long ({width} > {limit} characters)
"B904", # raise ... from err
"B905", # zip() without an explicit strict= parameter
"ANN101", # type hint for `self`
"ANN002", # type hint for *args
"ANN003", # type hint for **kwargs
]
line-length = 90

2 changes: 1 addition & 1 deletion tests/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ def check_fuze(fuze):
fuze.visual.material.copy()


def wrapload(exported, file_type, **kwargs):
def roundtrip(exported, file_type, **kwargs):
"""
Reload an exported byte blob into a mesh.
Expand Down
4 changes: 2 additions & 2 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def test_export(self):
def test_obj(self):
m = g.get_mesh("textured_tetrahedron.obj", process=False)
export = m.export(file_type="obj")
reconstructed = g.wrapload(export, file_type="obj", process=False)
reconstructed = g.roundtrip(export, file_type="obj", process=False)
# test that we get at least the same number of normals and texcoords out;
# the loader may reorder vertices, so we shouldn't check direct
# equality
Expand Down Expand Up @@ -243,7 +243,7 @@ def test_scene(self):
export = source.export(file_type="glb")

# re- load the file as a trimesh.Scene object again
loaded = g.wrapload(export, file_type="glb")
loaded = g.roundtrip(export, file_type="glb")

# the scene should be identical after export-> import cycle
assert g.np.allclose(loaded.extents / source.extents, 1.0)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_gltf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,30 @@ def post(tree):
# export with a postprocessor
s.export(file_type="glb", tree_postprocessor=post)

def test_unitize_normals_null_values(self):
# Create the mesh
mesh = g.trimesh.Trimesh(
vertices=[[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1]],
faces=[[0, 1, 2], [1, 3, 2], [0, 1, 4]],
)

# Set the normal of the first vertex to null
modified_normals = mesh.vertex_normals.copy()
modified_normals[0] = [0, 0, 0]

mesh.vertex_normals = modified_normals

# Export the mesh
export = mesh.export(file_type="glb", unitize_normals=True)
reimported_mesh = list(
g.trimesh.load(
g.trimesh.util.wrap_as_stream(export), file_type="glb"
).geometry.values()
)[0]

# Check that the normals are still null
assert g.np.allclose(reimported_mesh.vertex_normals[0], [0, 0, 0])


if __name__ == "__main__":
g.trimesh.util.attach_to_log()
Expand Down
12 changes: 6 additions & 6 deletions tests/test_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_rabbit(self):
# this should test the non-vectorized load path
m = g.get_mesh("rabbit.obj")
assert len(m.faces) == 1252
rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
assert g.np.isclose(m.area, rec.area)

def test_no_img(self):
Expand All @@ -25,14 +25,14 @@ def test_no_img(self):
assert m.visual.uv.min() > -1e-5
# check to make sure it's not all zeros
assert m.visual.uv.ptp() > 0.5
rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
assert g.np.isclose(m.area, rec.area)

def test_trailing(self):
# test files with texture and trailing slashes
m = g.get_mesh("jacked.obj")
assert len(m.visual.uv) == len(m.vertices)
rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
assert g.np.isclose(m.area, rec.area)

def test_obj_groups(self):
Expand Down Expand Up @@ -66,7 +66,7 @@ def test_obj_quad(self):

assert mesh.is_watertight
assert mesh.is_winding_consistent
rec = g.wrapload(mesh.export(file_type="obj"), file_type="obj")
rec = g.roundtrip(mesh.export(file_type="obj"), file_type="obj")
assert g.np.isclose(mesh.area, rec.area)

def test_obj_multiobj(self):
Expand Down Expand Up @@ -242,7 +242,7 @@ def test_empty_or_pointcloud(self):
raise ValueError("cannot export empty")
elif "points" in empty_file:
export = e.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")

# result should be a point cloud instance
assert isinstance(e, g.trimesh.PointCloud)
Expand All @@ -258,7 +258,7 @@ def test_backslash_continuation_character(self):

def test_no_uv(self):
mesh = g.get_mesh("box.obj")
rec = g.wrapload(mesh.export(file_type="obj"), file_type="obj")
rec = g.roundtrip(mesh.export(file_type="obj"), file_type="obj")
assert g.np.isclose(mesh.area, rec.area)

def test_no_uv_but_mtl(self):
Expand Down
16 changes: 8 additions & 8 deletions tests/test_ply.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_ply(self):
assert m.visual.face_colors.ptp(axis=0).max() > 0

export = m.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")

assert reconstructed.visual.kind == "face"

Expand All @@ -31,7 +31,7 @@ def test_ply(self):
assert m.visual.vertex_colors.ptp(axis=0).max() > 0

export = m.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")
assert reconstructed.visual.kind == "vertex"

assert g.np.allclose(reconstructed.visual.vertex_colors, m.visual.vertex_colors)
Expand Down Expand Up @@ -90,7 +90,7 @@ def test_vertex_attributes(self):
m.vertex_attributes["test_nd_attribute"] = test_nd_attribute

export = m.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")

vertex_attributes = reconstructed.metadata["_ply_raw"]["vertex"]["data"]
result_1d = vertex_attributes["test_1d_attribute"]
Expand All @@ -110,7 +110,7 @@ def test_face_attributes(self):
m.face_attributes["test_nd_attribute"] = test_nd_attribute

export = m.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")

face_attributes = reconstructed.metadata["_ply_raw"]["face"]["data"]
result_1d = face_attributes["test_1d_attribute"]
Expand All @@ -133,19 +133,19 @@ def test_cases(self):

def test_ascii_color(self):
mesh = g.trimesh.creation.box()
en = g.wrapload(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
en = g.roundtrip(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
assert en.visual.kind is None

color = [255, 0, 0, 255]
mesh.visual.vertex_colors = color

# try exporting and reloading raw
eb = g.wrapload(mesh.export(file_type="ply"), file_type="ply")
eb = g.roundtrip(mesh.export(file_type="ply"), file_type="ply")

assert g.np.allclose(eb.visual.vertex_colors[0], color)
assert eb.visual.kind == "vertex"

ea = g.wrapload(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
ea = g.roundtrip(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
assert g.np.allclose(ea.visual.vertex_colors, color)
assert ea.visual.kind == "vertex"

Expand All @@ -171,7 +171,7 @@ def test_empty_or_pointcloud(self):
elif "points" in empty_file:
# create export
export = e.export(file_type="ply")
reconstructed = g.wrapload(export, file_type="ply")
reconstructed = g.roundtrip(export, file_type="ply")

# result should be a point cloud instance
assert isinstance(e, g.trimesh.PointCloud)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def test_ply(self):
assert hash(p.visual) != initial

# test exporting a pointcloud to a PLY file
r = g.wrapload(p.export(file_type="ply"), file_type="ply")
r = g.roundtrip(p.export(file_type="ply"), file_type="ply")
assert r.vertices.shape == p.vertices.shape
# make sure colors survived the round trip
assert g.np.allclose(r.colors, p.colors)
Expand Down
12 changes: 10 additions & 2 deletions trimesh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
creation,
curvature,
decomposition,
exceptions,
geometry,
graph,
grouping,
Expand Down Expand Up @@ -59,9 +60,15 @@
from . import path
except BaseException as E:
# raise a useful error if path hasn't loaded
from .exceptions import ExceptionWrapper
path = exceptions.ExceptionWrapper(E)


try:
from . import voxel
except BaseException as E:
# requires non-minimal imports
voxel = exceptions.ExceptionWrapper(E)

path = ExceptionWrapper(E)

__all__ = [
"PointCloud",
Expand Down Expand Up @@ -108,4 +115,5 @@
"units",
"utilScene",
"voxel",
"exceptions",
]
39 changes: 21 additions & 18 deletions trimesh/exchange/gltf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"""

import base64
import collections
import json
from collections import OrderedDict, defaultdict, deque

import numpy as np

from .. import rendering, resources, transformations, util, visual
from ..caching import hash_fast
from ..constants import log, tol
from ..typed import NDArray
from ..util import unique_name
from ..visual.gloss import specular_to_pbr

Expand Down Expand Up @@ -484,7 +485,7 @@ def _buffer_append(ordered, data):
Parameters
----------
od : collections.OrderedDict
od : OrderedDict
Keyed like { hash : data }
data : bytes
To be stored
Expand All @@ -505,19 +506,19 @@ def _buffer_append(ordered, data):
return len(ordered) - 1


def _data_append(acc, buff, blob, data):
def _data_append(acc: OrderedDict, buff: OrderedDict, blob: dict, data: NDArray):
"""
Append a new accessor to an OrderedDict.
Parameters
------------
acc : collections.OrderedDict
acc
Collection of accessors, will be mutated in-place
buff : collections.OrderedDict
buff
Collection of buffer bytes, will be mutated in-place
blob : dict
blob
Candidate accessor
data : numpy.array
data
Data to fill in details to blob
Returns
Expand Down Expand Up @@ -562,6 +563,11 @@ def _data_append(acc, buff, blob, data):
# see if we're an array, matrix, etc
kind = blob["type"]

if tol.strict:
# in unit tests make sure everything we're trying to export
# is finite, which also checks for accidental NaN values
assert np.isfinite(data).all()

if kind == "SCALAR":
# is probably (n, 1)
blob["count"] = int(np.prod(data.shape))
Expand Down Expand Up @@ -665,7 +671,7 @@ def _create_gltf_structure(
"scene": 0,
"scenes": [{"nodes": [0]}],
"asset": {"version": "2.0", "generator": "https://github.com/mikedh/trimesh"},
"accessors": collections.OrderedDict(),
"accessors": OrderedDict(),
"meshes": [],
"images": [],
"textures": [],
Expand All @@ -689,7 +695,7 @@ def _create_gltf_structure(
# store materials as {hash : index} to avoid duplicates
mat_hashes = {}
# store data from geometries
buffer_items = collections.OrderedDict()
buffer_items = OrderedDict()

# map the name of each mesh to the index in tree['meshes']
mesh_index = {}
Expand Down Expand Up @@ -922,10 +928,7 @@ def _append_mesh(
):
# store vertex normals if requested
if unitize_normals:
normals = mesh.vertex_normals.copy()
norms = np.linalg.norm(normals, axis=1)
if not util.allclose(norms, 1.0, atol=1e-4):
normals /= norms.reshape((-1, 1))
normals = util.unitize(mesh.vertex_normals)
else:
# we don't have to copy them since
# they aren't being altered
Expand Down Expand Up @@ -978,7 +981,7 @@ def _build_views(buffer_items):
Parameters
--------------
buffer_items : collections.OrderedDict
buffer_items : OrderedDict
Buffers to build views for
Returns
Expand Down Expand Up @@ -1461,9 +1464,9 @@ def _read_buffers(
else:
materials = _parse_materials(header, views=views, resolver=resolver)

mesh_prim = collections.defaultdict(list)
mesh_prim = defaultdict(list)
# load data from accessors into Trimesh objects
meshes = collections.OrderedDict()
meshes = OrderedDict()

# keep track of how many times each name has been attempted to
# be inserted to avoid a potentially slow search through our
Expand Down Expand Up @@ -1661,9 +1664,9 @@ def _read_buffers(
names[base_frame] = base_frame

# visited, kwargs for scene.graph.update
graph = collections.deque()
graph = deque()
# unvisited, pairs of node indexes
queue = collections.deque()
queue = deque()

if "scene" in header:
# specify the index of scenes if specified
Expand Down
3 changes: 2 additions & 1 deletion trimesh/voxel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import creation
from .base import VoxelGrid

__all__ = ["VoxelGrid"]
__all__ = ["VoxelGrid", "creation"]

0 comments on commit 11cd6f8

Please sign in to comment.