Skip to content

Commit

Permalink
Merge pull request #2209 from mikedh/feat/sample_original
Browse files Browse the repository at this point in the history
Feature: Include Original
  • Loading branch information
mikedh authored Apr 16, 2024
2 parents 2e2c787 + 088f46e commit ff1983e
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 14 deletions.
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.3.0"
version = "4.3.1"
authors = [{name = "Michael Dawson-Haggerty", email = "[email protected]"}]
license = {file = "LICENSE.md"}
description = "Import, export, process, analyze and view triangular meshes."
Expand Down
21 changes: 21 additions & 0 deletions tests/test_path_sample.py
Original file line number Diff line number Diff line change
@@ -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()
78 changes: 65 additions & 13 deletions trimesh/path/traversal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -263,7 +264,28 @@ 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, 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)
Expand All @@ -275,15 +297,35 @@ def sample(self, distances):
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]
Expand All @@ -304,7 +346,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
Expand All @@ -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):
Expand All @@ -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

Expand Down

0 comments on commit ff1983e

Please sign in to comment.