Skip to content

Commit bccd088

Browse files
committed
update: move and fix the bezier handle to avoid weird meshing behavior
1 parent 50810f9 commit bccd088

1 file changed

Lines changed: 25 additions & 65 deletions

File tree

src/bsr/geometry/primitives/pipe.py

Lines changed: 25 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import bpy
1616
import numpy as np
1717
from numpy.typing import NDArray
18+
from scipy.interpolate import interp1d
1819

1920
from bsr.geometry.protocol import BlenderMeshInterfaceProtocol, SplineDataType
2021
from bsr.tools.keyframe_mixin import KeyFrameControlMixin
@@ -35,16 +36,7 @@ class BezierSplinePipe(KeyFrameControlMixin):
3536
positions : NDArray
3637
The position of the spline control points. Shape: (3, n)
3738
radii : NDArray
38-
The radius at each control point. Shape: (n,)
39-
handle_type : str, optional
40-
Handle type for Bezier points. Options: "AUTO", "VECTOR", "SMOOTH"
41-
Default: "AUTO" (recommended for smooth pipes without tip distortion)
42-
43-
Notes
44-
-----
45-
- "AUTO" handles provide the smoothest pipe appearance
46-
- "VECTOR" handles can cause tip distortion and bevel compression at corners
47-
- "SMOOTH" handles provide manual control but require more setup
39+
The radius at each control point. Shape: (n-1,)
4840
"""
4941

5042
input_states = {"positions", "radii"}
@@ -54,7 +46,6 @@ def __init__(
5446
self,
5547
positions: NDArray,
5648
radii: NDArray,
57-
handle_type: str = "AUTO",
5849
downsample_num_element: int | None = None,
5950
**kwargs: Any,
6051
) -> None:
@@ -67,9 +58,6 @@ def __init__(
6758
The position of the spline object. (3, n)
6859
radii : NDArray
6960
The radius of the spline object. (n,)
70-
handle_type : str, optional
71-
The handle type for Bezier points. Options: "AUTO", "VECTOR", "SMOOTH"
72-
Default is "AUTO" for smoother pipe appearance.
7361
downsample_num_element : int | None, optional
7462
If provided, downsample the positions and radii to this number of elements.
7563
Default is None (no downsampling).
@@ -78,7 +66,7 @@ def __init__(
7866
downsample_num_element is None or downsample_num_element >= 2
7967
), "downsample_num_element must be at least 2 to include both endpoints"
8068
self.downsample_num_element = downsample_num_element
81-
self._obj = self._create_bezier_spline(radii.size, handle_type)
69+
self._obj = self._create_bezier_spline(min(positions.shape[1], downsample_num_element))
8270
self._obj.name = self.name
8371
self.update_states(positions, radii)
8472

@@ -91,7 +79,6 @@ def __init__(
9179
def create(
9280
cls,
9381
states: SplineDataType,
94-
handle_type: str = "AUTO",
9582
downsample_num_element: int | None = None,
9683
) -> "BezierSplinePipe":
9784
"""
@@ -101,9 +88,6 @@ def create(
10188
----------
10289
states : SplineDataType
10390
Dictionary containing 'positions', 'radii'
104-
handle_type : str, optional
105-
The handle type for Bezier points. Options: "AUTO", "VECTOR", "SMOOTH"
106-
Default is "AUTO" for smoother pipe appearance.
10791
downsample_num_element : int | None, optional
10892
If provided, downsample the positions and radii to this number of elements.
10993
Default is None (no downsampling).
@@ -118,7 +102,6 @@ def create(
118102
return cls(
119103
states["positions"],
120104
states["radii"],
121-
handle_type,
122105
downsample_num_element,
123106
)
124107

@@ -182,60 +165,34 @@ def update_states(
182165
If the shape of the position or radius is incorrect, or if the data is NaN.
183166
"""
184167

185-
# Apply downsampling if configured
186-
if self.downsample_num_element is not None:
187-
positions, radii = self._downsample_data(positions, radii)
188-
189168
spline = self.object.data.splines[0]
190169
if positions is not None:
191170
_validate_position(positions)
171+
positions = self._downsample_data(positions, self.downsample_num_element)
192172
for i, point in enumerate(spline.bezier_points):
193173
x, y, z = positions[:, i]
194174
point.co = (x, y, z)
195175
if radii is not None:
196176
_validate_radii(radii)
177+
_radii = np.concatenate([radii, [0]])
178+
_radii[1:] += radii
179+
_radii[1:-1] /= 2.0
180+
radii = self._downsample_data(_radii, self.downsample_num_element)
197181
for i, point in enumerate(spline.bezier_points):
198182
point.radius = radii[i]
199183

200184
def _downsample_data(
201-
self, positions: NDArray, radii: NDArray
202-
) -> tuple[NDArray, NDArray]:
203-
"""
204-
Downsamples the positions and radii arrays to the specified number of elements.
205-
Always includes both endpoints (first and last points).
206-
207-
Parameters
208-
----------
209-
positions : NDArray
210-
The position array to downsample. Shape: (3, n)
211-
radii : NDArray
212-
The radius array to downsample. Shape: (n,)
213-
num_elements : int
214-
The target number of elements after downsampling.
215-
216-
Returns
217-
-------
218-
tuple[NDArray, NDArray]
219-
Downsampled positions and radii arrays.
220-
"""
221-
num_elements = self.downsample_num_element
222-
if positions.shape[1] <= num_elements:
223-
# No downsampling needed
224-
# TODO: should we just raise error, or interpolate the positions and radii?
225-
return positions, radii
185+
self, vector: NDArray, num_elements: int
186+
) -> NDArray:
187+
if vector.shape[-1] <= num_elements:
188+
return vector
226189

227190
t = np.linspace(0, 1, num_elements)
228-
t_old = np.linspace(0, 1, positions.shape[1])
229-
new_positions = np.empty((3, num_elements))
230-
new_positions[0, :] = np.interp(t, t_old, positions[0, :])
231-
new_positions[1, :] = np.interp(t, t_old, positions[1, :])
232-
new_positions[2, :] = np.interp(t, t_old, positions[2, :])
233-
new_radii = np.interp(t, t_old, radii)
234-
235-
return new_positions, new_radii
191+
t_old = np.linspace(0, 1, vector.shape[-1])
192+
return interp1d(t_old, vector, axis=-1)(t)
236193

237194
def _create_bezier_spline(
238-
self, number_of_points: int, handle_type: str = "AUTO"
195+
self, number_of_points: int,
239196
) -> bpy.types.Object:
240197
"""
241198
Creates a new pipe object.
@@ -244,9 +201,6 @@ def _create_bezier_spline(
244201
----------
245202
number_of_points : int
246203
The number of points in the pipe.
247-
handle_type : str, optional
248-
The handle type for Bezier points. Options: "AUTO", "VECTOR", "SMOOTH"
249-
Default is "AUTO" for smoother pipe appearance.
250204
"""
251205
# Create a new curve
252206
curve_data = bpy.data.curves.new(name="spline_curve", type="CURVE")
@@ -260,12 +214,12 @@ def _create_bezier_spline(
260214
# Set the spline points and radii
261215
for i in range(number_of_points):
262216
point = spline.bezier_points[i]
263-
point.handle_left_type = point.handle_right_type = handle_type
217+
point.handle_left_type = point.handle_right_type = "FREE"
264218

265219
# Create a new object with the curve data
266220
curve_object = bpy.data.objects.new("spline_curve_object", curve_data)
267221
curve_object.data.resolution_u = 1
268-
curve_object.data.render_resolution_u = 1
222+
# curve_object.data.render_resolution_u = 1
269223
bpy.context.collection.objects.link(curve_object)
270224

271225
# Create a bevel object for the pipe profile
@@ -279,7 +233,7 @@ def _create_bezier_spline(
279233
bevel_circle = bpy.context.object
280234
bevel_circle.name = "bevel_circle"
281235
# Set resolution for smoother bevel profile
282-
bevel_circle.data.resolution_u = 1
236+
bevel_circle.data.resolution_u = 8
283237
# Hide the bevel circle object in the viewport and render
284238
bevel_circle.hide_viewport = True
285239
bevel_circle.hide_render = True
@@ -300,8 +254,14 @@ def update_keyframe(self, keyframe: int) -> None:
300254
keyframe : int
301255
"""
302256
spline = self.object.data.splines[0]
257+
303258
for i, point in enumerate(spline.bezier_points):
304-
point.keyframe_insert(data_path="co", frame=keyframe)
259+
# This is to reset the left/right handle for each bezier curve points
260+
point.handle_left_type = "AUTO"
261+
point.handle_right_type = "AUTO"
262+
point.keyframe_insert(data_path="handle_left", frame=keyframe)
263+
point.keyframe_insert(data_path="handle_right", frame=keyframe)
264+
point.keyframe_insert(data_path="co", frame=keyframe) # Coordinate
305265
point.keyframe_insert(data_path="radius", frame=keyframe)
306266
self.material.keyframe_insert(data_path="diffuse_color", frame=keyframe)
307267

0 commit comments

Comments
 (0)