1515import bpy
1616import numpy as np
1717from numpy .typing import NDArray
18+ from scipy .interpolate import interp1d
1819
1920from bsr .geometry .protocol import BlenderMeshInterfaceProtocol , SplineDataType
2021from 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