Repository: mikedh/trimesh

Issue:
Unused setuptools dependency
I'm trying to figure out why setuptools is still in the list of "easy" dependencies.
The background is this bug report that I'm trying to solve: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1094339

From what I can see, it's only used during the packaging process, but there's nothing that would need setuptools at runtime.

Can you clarify where setuptools is used in the code, and if it can be safely removed from the runtime dependencies?

Hints from issue comments:
@mikedh: Thanks for the report! Yeah that's a legacy thing from before `pyproject.toml[build]` existed, I'll remove it in #2360 

Solution patch:
diff --git a/pyproject.toml b/pyproject.toml
index ab2721332..8ca451280 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
 [project]
 name = "trimesh"
 requires-python = ">=3.8"
-version = "4.6.2"
+version = "4.6.3"
 authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
 license = {file = "LICENSE.md"}
 description = "Import, export, process, analyze and view triangular meshes."
@@ -74,7 +74,6 @@ easy = [
     "networkx",
     "svg.path",
     "pycollada",
-    "setuptools",
     "shapely",
     "xxhash",
     "rtree",
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index cedf12ecb..1c361ca9c 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -1165,6 +1165,31 @@ def _append_path(path, name, tree, buffer_items):
         # add color to attributes
         current["primitives"][0]["attributes"]["COLOR_0"] = acc_color
 
+    # for each attribute with a leading underscore, assign them to path
+    # vertex_attributes
+    for key, attrib in path.vertex_attributes.items():
+        # Application specific attributes must be
+        # prefixed with an underscore
+        if not key.startswith("_"):
+            key = "_" + key
+
+        # GLTF has no floating point type larger than 32 bits so clip
+        # any float64 or larger to float32
+        if attrib.dtype.kind == "f" and attrib.dtype.itemsize > 4:
+            data = attrib.astype(np.float32)
+        else:
+            data = attrib
+
+        data = util.stack_lines(data).reshape((-1,))
+
+        # store custom vertex attributes
+        current["primitives"][0]["attributes"][key] = _data_append(
+            acc=tree["accessors"],
+            buff=buffer_items,
+            blob=_build_accessor(data),
+            data=data,
+        )
+
     tree["meshes"].append(current)
 
 
@@ -1525,6 +1550,13 @@ def _read_buffers(
 
                     kwargs["vertices"] = access[attr["POSITION"]]
                     kwargs["entities"] = [Line(points=np.arange(len(kwargs["vertices"])))]
+
+                    # custom attributes starting with a `_`
+                    custom = {
+                        a: access[attr[a]] for a in attr.keys() if a.startswith("_")
+                    }
+                    if len(custom) > 0:
+                        kwargs["vertex_attributes"] = custom
                 elif mode == _GL_POINTS:
                     kwargs["vertices"] = access[attr["POSITION"]]
                     visuals = None
diff --git a/trimesh/path/path.py b/trimesh/path/path.py
index a1e3714bd..ac3370ed2 100644
--- a/trimesh/path/path.py
+++ b/trimesh/path/path.py
@@ -78,6 +78,7 @@ def __init__(
         metadata: Optional[Dict] = None,
         process: bool = True,
         colors=None,
+        vertex_attributes: Optional[Dict] = None,
         **kwargs,
     ):
         """
@@ -105,6 +106,10 @@ def __init__(
         if isinstance(metadata, dict):
             self.metadata.update(metadata)
 
+        self.vertex_attributes = {}
+        if vertex_attributes is not None:
+            self.vertex_attributes.update(vertex_attributes)
+
         # cache will dump whenever self.crc changes
         self._cache = caching.Cache(id_function=self.__hash__)
 
@@ -523,6 +528,9 @@ def merge_vertices(self, digits=None):
 
         unique, inverse = grouping.unique_rows(self.vertices, digits=digits)
         self.vertices = self.vertices[unique]
+        self.vertex_attributes = {
+            key: np.array(value)[unique] for key, value in self.vertex_attributes.items()
+        }
 
         entities_ok = np.ones(len(self.entities), dtype=bool)
 
diff --git a/trimesh/visual/color.py b/trimesh/visual/color.py
index 85ebd0baa..7d4baa50f 100644
--- a/trimesh/visual/color.py
+++ b/trimesh/visual/color.py
@@ -70,13 +70,6 @@ def __init__(
         self._data = caching.DataStore()
         self._cache = caching.Cache(id_function=self._data.__hash__)
 
-        self.defaults = {
-            "material_diffuse": np.array([102, 102, 102, 255], dtype=np.uint8),
-            "material_ambient": np.array([64, 64, 64, 255], dtype=np.uint8),
-            "material_specular": np.array([197, 197, 197, 255], dtype=np.uint8),
-            "material_shine": 77.0,
-        }
-
         try:
             if face_colors is not None:
                 self.face_colors = face_colors
@@ -326,7 +319,7 @@ def _get_colors(self, name):
             if self.kind is None:
                 # no colors are defined, so create a (count, 4) tiled
                 # copy of the default color
-                colors = np.tile(self.defaults["material_diffuse"], (count, 1))
+                colors = np.tile(DEFAULT_MAT["material_diffuse"], (count, 1))
             elif self.kind == "vertex" and name == "face":
                 colors = vertex_to_face_color(
                     vertex_colors=self.vertex_colors, faces=self.mesh.faces
@@ -1106,3 +1099,9 @@ def color_to_uv(vertex_colors: ArrayLike):
 
 # set an arbitrary grey as the default color
 DEFAULT_COLOR = np.array([102, 102, 102, 255], dtype=np.uint8)
+DEFAULT_MAT = {
+    "material_diffuse": np.array([102, 102, 102, 255], dtype=np.uint8),
+    "material_ambient": np.array([64, 64, 64, 255], dtype=np.uint8),
+    "material_specular": np.array([197, 197, 197, 255], dtype=np.uint8),
+    "material_shine": 77.0,
+}


Test patch:
diff --git a/tests/generic.py b/tests/generic.py
index 09e5a062e..8b1080fb7 100644
--- a/tests/generic.py
+++ b/tests/generic.py
@@ -6,6 +6,7 @@
 
 import os
 import sys
+import io
 import json
 import copy
 import time
@@ -567,6 +568,47 @@ def roundtrip(exported, file_type, **kwargs):
     )
 
 
+def to_glb_bytes(geom: trimesh.Geometry) -> bytes:
+    """
+    Returns the content of the binary glTF file for the given geometry.
+
+    Parameters
+    ----------
+    geom : trimesh.Geometry
+      Geometry to export
+
+    Returns
+    -------
+    data : bytes
+      Content of the binary glTF file
+    """
+    data = trimesh.exchange.gltf.export_glb(geom)
+
+    return data
+
+
+def from_glb_bytes(data: bytes) -> trimesh.Scene:
+    """
+    Returns the scene from the given binary glTF file content.
+
+    Parameters
+    ----------
+    data : bytes
+      Content of the binary glTF file
+
+    Returns
+    -------
+    scene : trimesh.Scene
+      Scene
+    """
+    bytes_stream = io.BytesIO(data)
+    kwargs = trimesh.exchange.gltf.load_glb(bytes_stream)
+
+    scene = trimesh.exchange.load._load_kwargs(kwargs)
+
+    return scene
+
+
 # all the JSON files with truth data
 data = _load_data()
 
diff --git a/tests/test_paths.py b/tests/test_paths.py
index bffab2ea4..fb806919c 100644
--- a/tests/test_paths.py
+++ b/tests/test_paths.py
@@ -111,6 +111,26 @@ def test_discrete():
         d.process()
 
 
+def test_path():
+    path = g.trimesh.path.Path3D(
+        entities=[g.trimesh.path.entities.Line(points=[0, 1, 2])],
+        vertices=[[0, 0, 0], [1, 0, 0], [1, 1, 0]],
+        vertex_attributes={"_test": g.np.array([1, 2, 3], dtype=g.np.float32)},
+    )
+
+    glb_data = g.to_glb_bytes(path)
+    loaded_scene = g.from_glb_bytes(glb_data)
+
+    loaded_path = loaded_scene.geometry["geometry_0"]
+    loaded_path.process()
+
+    assert isinstance(loaded_path, g.trimesh.path.Path3D)
+
+    assert loaded_path.vertices.shape == (3, 3)
+    assert len(loaded_path.entities) == 1
+    assert len(loaded_path.vertex_attributes["_test"]) == 3
+
+
 def test_poly():
     p = g.get_mesh("2D/LM2.dxf")
     assert p.is_closed