From 32e4c293a473599e5472797ff9074faa96419a92 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Fri, 12 Apr 2024 16:42:33 -0400 Subject: [PATCH 1/3] skeleton of implementation --- trimesh/path/traversal.py | 53 +++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/trimesh/path/traversal.py b/trimesh/path/traversal.py index 20fb382e0..45900131c 100644 --- a/trimesh/path/traversal.py +++ b/trimesh/path/traversal.py @@ -3,6 +3,7 @@ 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,20 @@ def __init__(self, points): # note that this is sorted self._cum_norm = np.cumsum(self._norms) - def sample(self, distances): + def sample(self, distances: ArrayLike) -> NDArray[np.float64]: + """ + Return points at the distances along the path requested. + + Parameters + ---------- + distances + Distances along the path to sample at. + + Returns + -------- + samples : (len(distances), dimension) + Samples requested. + """ # 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) @@ -280,10 +294,20 @@ def sample(self, distances): 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 +328,13 @@ def truncate(self, distance): 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,11 +350,15 @@ 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 ---------- @@ -351,6 +385,11 @@ def resample_path(points, count=None, step=None, step_round=True): elif step is not None: samples = np.arange(0, sampler.length, step) + if include_original: + from IPython import embed + + embed() + resampled = sampler.sample(samples) check = util.row_norm(points[[0, -1]] - resampled[[0, -1]]) From 86232dd4f1a684da2fcfa2f4698709748a0ce956 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sat, 13 Apr 2024 01:52:14 -0400 Subject: [PATCH 2/3] implement include_original --- tests/test_path_sample.py | 21 +++++++++++++++++++++ trimesh/path/traversal.py | 39 ++++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 tests/test_path_sample.py diff --git a/tests/test_path_sample.py b/tests/test_path_sample.py new file mode 100644 index 000000000..75671a11b --- /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() diff --git a/trimesh/path/traversal.py b/trimesh/path/traversal.py index 45900131c..192bee64d 100644 --- a/trimesh/path/traversal.py +++ b/trimesh/path/traversal.py @@ -264,7 +264,9 @@ def __init__(self, points: ArrayLike): # note that this is sorted self._cum_norm = np.cumsum(self._norms) - def sample(self, distances: ArrayLike) -> NDArray[np.float64]: + def sample( + self, distances: ArrayLike, include_original: bool = False + ) -> NDArray[np.float64]: """ Return points at the distances along the path requested. @@ -272,11 +274,17 @@ def sample(self, distances: ArrayLike) -> NDArray[np.float64]: ---------- 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 : (len(distances), dimension) + 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 @@ -289,9 +297,19 @@ def sample(self, distances: ArrayLike) -> NDArray[np.float64]: 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: Number) -> NDArray[np.float64]: @@ -365,7 +383,6 @@ def resample_path( 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): @@ -385,17 +402,13 @@ def resample_path( elif step is not None: samples = np.arange(0, sampler.length, step) - if include_original: - from IPython import embed + resampled = sampler.sample(samples, include_original=include_original) - embed() - - resampled = sampler.sample(samples) - - 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 From 088f46e11e2360d1475494c543fa35dafda1f49d Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 16 Apr 2024 14:05:56 -0400 Subject: [PATCH 3/3] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52db3f63a..27b7e0ff1 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."