Repository: mikedh/trimesh
Pull request #2209 (2024-04-16 18:02:36)

Feature: Include Original
Add an `include_original` option to path resampling. 


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 52db3f63..27b7e0ff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
 [project]
 name = "trimesh"
 requires-python = ">=3.7"
-version = "4.3.0"
+version = "4.3.1"
 authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
 license = {file = "LICENSE.md"}
 description = "Import, export, process, analyze and view triangular meshes."
diff --git a/trimesh/path/traversal.py b/trimesh/path/traversal.py
index 20fb382e..192bee64 100644
--- a/trimesh/path/traversal.py
+++ b/trimesh/path/traversal.py
@@ -3,6 +3,7 @@ import copy
 import numpy as np
 
 from .. import constants, grouping, util
+from ..typed import ArrayLike, Integer, NDArray, Number, Optional
 from .util import is_ccw
 
 try:
@@ -246,7 +247,7 @@ def discretize_path(entities, vertices, path, scale=1.0):
 
 
 class PathSample:
-    def __init__(self, points):
+    def __init__(self, points: ArrayLike):
         # make sure input array is numpy
         self._points = np.array(points)
         # find the direction of each segment
@@ -263,7 +264,28 @@ class PathSample:
         # note that this is sorted
         self._cum_norm = np.cumsum(self._norms)
 
-    def sample(self, distances):
+    def sample(
+        self, distances: ArrayLike, include_original: bool = False
+    ) -> NDArray[np.float64]:
+        """
+        Return points at the distances along the path requested.
+
+        Parameters
+        ----------
+        distances
+          Distances along the path to sample at.
+        include_original
+          Include the original vertices even if they are not
+          specified in `distance`. Useful as this will return
+          a result with identical area and length, however
+          indexes of `distance` will not correspond with result.
+
+        Returns
+        --------
+        samples : (n, dimension)
+          Samples requested.
+          `n==len(distances)` if not `include_original`
+        """
         # return the indices in cum_norm that each sample would
         # need to be inserted at to maintain the sorted property
         positions = np.searchsorted(self._cum_norm, distances)
@@ -275,15 +297,35 @@ class PathSample:
         direction = self._unit_vec[positions]
         # find out which vertex we're offset from
         origin = self._points[positions]
+
         # just the parametric equation for a line
         resampled = origin + (direction * projection.reshape((-1, 1)))
 
+        if include_original:
+            # find the insertion index of the original positions
+            unique, index = np.unique(positions, return_index=True)
+            # see if we already have this point
+            ok = projection[index] > 1e-12
+
+            # insert the original vertices into the resampled array
+            resampled = np.insert(resampled, index[ok], self._points[unique[ok]], axis=0)
+
         return resampled
 
-    def truncate(self, distance):
+    def truncate(self, distance: Number) -> NDArray[np.float64]:
         """
         Return a truncated version of the path.
         Only one vertex (at the endpoint) will be added.
+
+        Parameters
+        ----------
+        distance
+          Distance along the path to truncate at.
+
+        Returns
+        ----------
+        path
+          Path clipped to `distance` requested.
         """
         position = np.searchsorted(self._cum_norm, distance)
         offset = distance - self._cum_norm[position - 1]
@@ -304,7 +346,13 @@ class PathSample:
         return truncated
 
 
-def resample_path(points, count=None, step=None, step_round=True):
+def resample_path(
+    points: ArrayLike,
+    count: Optional[Integer] = None,
+    step: Optional[Number] = None,
+    step_round: bool = True,
+    include_original: bool = False,
+) -> NDArray[np.float64]:
     """
     Given a path along (n,d) points, resample them such that the
     distance traversed along the path is constant in between each
@@ -320,18 +368,21 @@ def resample_path(points, count=None, step=None, step_round=True):
     Parameters
     ----------
     points:   (n, d) float
-        Points in space
+      Points in space
     count : int,
-        Number of points to sample evenly (aka np.linspace)
+      Number of points to sample evenly (aka np.linspace)
     step : float
-        Distance each step should take along the path (aka np.arange)
+      Distance each step should take along the path (aka np.arange)
+    step_round
+      Alter `step` to the nearest integer division of overall length.
+    include_original
+      Include the exact original points in the output.
 
     Returns
     ----------
     resampled : (j,d) float
         Points on the path
     """
-
     points = np.array(points, dtype=np.float64)
     # generate samples along the perimeter from kwarg count or step
     if (count is not None) and (step is not None):
@@ -351,12 +402,13 @@ def resample_path(points, count=None, step=None, step_round=True):
     elif step is not None:
         samples = np.arange(0, sampler.length, step)
 
-    resampled = sampler.sample(samples)
+    resampled = sampler.sample(samples, include_original=include_original)
 
-    check = util.row_norm(points[[0, -1]] - resampled[[0, -1]])
-    assert check[0] < constants.tol_path.merge
-    if count is not None:
-        assert check[1] < constants.tol_path.merge
+    if constants.tol.strict:
+        check = util.row_norm(points[[0, -1]] - resampled[[0, -1]])
+        assert check[0] < constants.tol_path.merge
+        if count is not None:
+            assert check[1] < constants.tol_path.merge
 
     return resampled
 


Test patch:
diff --git a/tests/test_path_sample.py b/tests/test_path_sample.py
new file mode 100644
index 00000000..75671a11
--- /dev/null
+++ b/tests/test_path_sample.py
@@ -0,0 +1,21 @@
+import numpy as np
+
+
+def test_resample_original():
+    # check to see if `include_original` works
+
+    from shapely.geometry import Polygon
+
+    from trimesh.path.traversal import resample_path
+
+    ori = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], dtype=np.float64)
+
+    re = resample_path(ori, step=0.25, include_original=True)
+
+    a, b = Polygon(ori), Polygon(re)
+    assert np.isclose(a.area, b.area)
+    assert np.isclose(a.length, b.length)
+
+
+if __name__ == "__main__":
+    test_resample_original()