swcgeom 0.13.2__py3-none-any.whl → 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of swcgeom might be problematic. Click here for more details.

swcgeom/utils/sdf.py CHANGED
@@ -1,6 +1,11 @@
1
1
  """Signed distance functions.
2
2
 
3
3
  Refs: https://iquilezles.org/articles/distfunctions/
4
+
5
+ Note
6
+ ----
7
+ This module has been deprecated since v0.14.0, and will be removed in
8
+ the future, use `sdflit` instead.
4
9
  """
5
10
 
6
11
  import warnings
@@ -10,7 +15,18 @@ from typing import Iterable, Tuple
10
15
  import numpy as np
11
16
  import numpy.typing as npt
12
17
 
13
- __all__ = ["SDF", "SDFCompose", "SDFRoundCone"]
18
+ from swcgeom.utils.solid_geometry import project_vector_on_plane
19
+
20
+ __all__ = [
21
+ "SDF",
22
+ "SDFUnion",
23
+ "SDFIntersection",
24
+ "SDFDifference",
25
+ "SDFCompose",
26
+ "SDFSphere",
27
+ "SDFFrustumCone",
28
+ "SDFRoundCone",
29
+ ]
14
30
 
15
31
  # Axis-aligned bounding box, tuple of array of shape (3,)
16
32
  AABB = Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
@@ -26,7 +42,7 @@ class SDF(ABC):
26
42
 
27
43
  @abstractmethod
28
44
  def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
29
- """Calc signed distance.
45
+ """Calculate signed distance.
30
46
 
31
47
  Parmeters
32
48
  ---------
@@ -69,15 +85,12 @@ class SDF(ABC):
69
85
  return is_in
70
86
 
71
87
 
72
- class SDFCompose(SDF):
73
- """Compose multiple SDFs."""
88
+ class SDFUnion(SDF):
89
+ """Union multiple SDFs."""
74
90
 
75
- def __init__(self, sdfs: Iterable[SDF]) -> None:
76
- sdfs = list(sdfs)
91
+ def __init__(self, *sdfs: SDF) -> None:
77
92
  assert len(sdfs) != 0, "must combine at least one SDF"
78
-
79
- if len(sdfs) == 1:
80
- warnings.warn("compose only one SDF, use SDFCompose.compose instead")
93
+ super().__init__()
81
94
 
82
95
  self.sdfs = sdfs
83
96
 
@@ -92,18 +105,162 @@ class SDFCompose(SDF):
92
105
  return np.min([sdf(p) for sdf in self.sdfs], axis=0)
93
106
 
94
107
  def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
108
+ p = np.array(p, dtype=np.float32)
109
+ assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
110
+
95
111
  in_box = self.is_in_bounding_box(p)
96
- is_in = np.stack([sdf.is_in(p[in_box]) for sdf in self.sdfs])
112
+ p_in_box = p[in_box]
113
+ is_in = np.stack([sdf.is_in(p_in_box) for sdf in self.sdfs])
97
114
  flags = np.full_like(in_box, False, dtype=np.bool_)
98
115
  flags[in_box] = np.any(is_in, axis=0)
99
116
  return flags
100
117
 
118
+
119
+ class SDFIntersection(SDF):
120
+ def __init__(self, *sdfs: SDF) -> None:
121
+ assert len(sdfs) != 0, "must intersect at least one SDF"
122
+ super().__init__()
123
+ self.sdfs = sdfs
124
+
125
+ bounding_boxes = [sdf.bounding_box for sdf in self.sdfs if sdf.bounding_box]
126
+ if len(bounding_boxes) == len(self.sdfs):
127
+ self.bounding_box = (
128
+ np.max(np.stack([box[0] for box in bounding_boxes]).T, axis=1),
129
+ np.min(np.stack([box[1] for box in bounding_boxes]).T, axis=1),
130
+ )
131
+
132
+ def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
133
+ distances = np.stack([sdf.distance(p) for sdf in self.sdfs])
134
+ return np.max(distances, axis=1)
135
+
136
+ def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
137
+ p = np.array(p, dtype=np.float32)
138
+ assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
139
+
140
+ in_box = self.is_in_bounding_box(p)
141
+ p_in_box = p[in_box]
142
+ is_in = np.stack([sdf.is_in(p_in_box) for sdf in self.sdfs])
143
+ flags = np.full_like(in_box, False, dtype=np.bool_)
144
+ flags[in_box] = np.all(is_in, axis=0)
145
+ return flags
146
+
147
+
148
+ class SDFDifference(SDF):
149
+ """Difference of two SDFs A-B."""
150
+
151
+ def __init__(self, sdf_a: SDF, sdf_b: SDF) -> None:
152
+ super().__init__()
153
+ self.sdf_a = sdf_a
154
+ self.sdf_b = sdf_b
155
+
156
+ self.bounding_box = sdf_a.bounding_box
157
+
158
+ def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
159
+ da = self.sdf_a.distance(p)
160
+ db = self.sdf_b.distance(p)
161
+ return np.maximum(da, -db)
162
+
163
+ def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
164
+ p = np.array(p, dtype=np.float32)
165
+ assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
166
+
167
+ in_box = self.is_in_bounding_box(p)
168
+ p_in_box = p[in_box]
169
+ is_in_a = self.sdf_a.is_in(p_in_box)
170
+ is_in_b = self.sdf_b.is_in(p_in_box)
171
+ flags = np.full_like(in_box, False, dtype=np.bool_)
172
+ flags[in_box] = np.logical_and(is_in_a, np.logical_not(is_in_b))
173
+ return flags
174
+
175
+
176
+ class SDFCompose(SDFUnion):
177
+ """Compose multiple SDFs."""
178
+
179
+ def __init__(self, sdfs: Iterable[SDF]) -> None:
180
+ warnings.warn(
181
+ "`SDFCompose` has been replace by `SDFUnion` since v0.14.0, "
182
+ "and will be removed in next version",
183
+ DeprecationWarning,
184
+ )
185
+ sdfs = list(sdfs)
186
+ if len(sdfs) == 1:
187
+ warnings.warn("compose only one SDF, use SDFCompose.compose instead")
188
+
189
+ super().__init__(*sdfs)
190
+
101
191
  @staticmethod
102
192
  def compose(sdfs: Iterable[SDF]) -> SDF:
103
193
  sdfs = list(sdfs)
104
194
  return SDFCompose(sdfs) if len(sdfs) != 1 else sdfs[0]
105
195
 
106
196
 
197
+ class SDFSphere(SDF):
198
+ """SDF of sphere."""
199
+
200
+ def __init__(self, center: npt.ArrayLike, radius: float) -> None:
201
+ super().__init__()
202
+ self.center = np.array(center)
203
+ self.radius = radius
204
+ assert tuple(self.center.shape) == (3,), "center should be vector of 3d"
205
+
206
+ self.bounding_box = (self.center - self.radius, self.center + self.radius)
207
+
208
+ def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
209
+ return np.linalg.norm(p - self.center, axis=1) - self.radius
210
+
211
+
212
+ class SDFFrustumCone(SDF):
213
+ """SDF of frustum cone."""
214
+
215
+ def __init__(
216
+ self, a: npt.ArrayLike, b: npt.ArrayLike, ra: float, rb: float
217
+ ) -> None:
218
+ super().__init__()
219
+ self.a = np.array(a)
220
+ self.b = np.array(b)
221
+ self.ra = ra
222
+ self.rb = rb
223
+ assert tuple(self.a.shape) == (3,), "a should be vector of 3d"
224
+ assert tuple(self.b.shape) == (3,), "b should be vector of 3d"
225
+
226
+ self.bounding_box = self.get_bounding_box()
227
+
228
+ def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
229
+ a, b, ra, rb = self.a, self.b, self.ra, self.rb
230
+
231
+ rba = rb - ra
232
+ baba = np.dot(b - a, b - a)
233
+ papa = np.einsum("ij,ij->i", p - a, p - a)
234
+ paba = np.dot(p - a, b - a) / baba
235
+ # maybe negative due to numerical error
236
+ x = np.sqrt(np.maximum(papa - paba * paba * baba, 0))
237
+ cax = np.maximum(0.0, x - np.where(paba < 0.5, ra, rb))
238
+ cay = np.abs(paba - 0.5) - 0.5
239
+ k = rba * rba + baba
240
+ f = np.clip((rba * (x - ra) + paba * baba) / k, 0.0, 1.0)
241
+ cbx = x - ra - f * rba
242
+ cby = paba - f
243
+ s = np.where(np.logical_and(cbx < 0.0, cay < 0.0), -1.0, 1.0)
244
+ return s * np.sqrt(
245
+ np.minimum(cax * cax + cay * cay * baba, cbx * cbx + cby * cby * baba)
246
+ )
247
+
248
+ def get_bounding_box(self) -> AABB | None:
249
+ a, b, ra, rb = self.a, self.b, self.ra, self.rb
250
+ up = a - b
251
+ vx = project_vector_on_plane((1, 0, 0), up)
252
+ vy = project_vector_on_plane((0, 1, 0), up)
253
+ vz = project_vector_on_plane((0, 0, 1), up)
254
+ a1 = a - ra * vx - ra * vy - ra * vz
255
+ a2 = a + ra * vx + ra * vy + ra * vz
256
+ b1 = b - rb * vx - rb * vy - rb * vz
257
+ b2 = b + rb * vx + rb * vy + rb * vz
258
+ return (
259
+ np.minimum(a1, b1).astype(np.float32),
260
+ np.maximum(a2, b2).astype(np.float32),
261
+ )
262
+
263
+
107
264
  class SDFRoundCone(SDF):
108
265
  """Round cone is made up of two balls and a cylinder."""
109
266
 
@@ -9,6 +9,8 @@ __all__ = [
9
9
  "find_unit_vector_on_plane",
10
10
  "find_sphere_line_intersection",
11
11
  "project_point_on_line",
12
+ "project_vector_on_vector",
13
+ "project_vector_on_plane",
12
14
  ]
13
15
 
14
16
 
@@ -68,3 +70,27 @@ def project_point_on_line(
68
70
  AP = P - A
69
71
  projection = A + np.dot(AP, n) / np.dot(n, n) * n
70
72
  return projection
73
+
74
+
75
+ def project_vector_on_vector(vec: npt.ArrayLike, target: npt.ArrayLike) -> npt.NDArray:
76
+ v = np.array(vec)
77
+ n = np.array(target)
78
+
79
+ n_normalized = n / np.linalg.norm(n)
80
+ projection_on_n = np.dot(v, n_normalized) * n_normalized
81
+ return projection_on_n
82
+
83
+
84
+ def project_vector_on_plane(
85
+ vec: npt.ArrayLike, plane_normal_vec: npt.ArrayLike
86
+ ) -> npt.NDArray:
87
+ v = np.array(vec)
88
+ n = np.array(plane_normal_vec)
89
+
90
+ # project v to n
91
+ projection_on_n = project_vector_on_vector(vec, n)
92
+
93
+ # project v to plane
94
+ projection_on_plane = v - projection_on_n
95
+
96
+ return projection_on_plane