@@ -302,6 +302,7 @@ def swept(
302302 cross_section ,
303303 trajectory ,
304304 cross_section_normal = None ,
305+ anchor = "auto" ,
305306 set_on_trajectory = False ,
306307 rotation_adaption = None ,
307308):
@@ -329,6 +330,13 @@ def swept(
329330 cross_section_normal : np.array
330331 Normal vector of the cross-section
331332 Default is [0, 0, 1]
333+ anchor : str
334+ Strategy used to determine which point of the cross-section is
335+ placed on the trajectory. Supported values are:
336+ ``"auto"``, ``"parametric"``, ``"control_box"``,
337+ and ``"geometry_box"``.
338+ ``"auto"`` uses ``"geometry_box"`` for curve cross-sections and
339+ ``"parametric"`` for surface cross-sections.
332340 set_on_trajectory : bool
333341 If True, the cross-section will be placed at the evaluation
334342 points of the trajectory's knots. If False, the cross-section
@@ -376,6 +384,8 @@ def swept(
376384 raise ValueError ("trajectory must have a parametric dimension of 1" )
377385 if not isinstance (set_on_trajectory , bool ):
378386 raise TypeError ("set_on_trajectory must be a boolean" )
387+ if not isinstance (anchor , str ):
388+ raise TypeError ("anchor must be a string" )
379389
380390 if cross_section_normal is not None and not len (cross_section_normal ) == 3 :
381391 raise ValueError (
@@ -387,6 +397,17 @@ def swept(
387397 # add debug message
388398 _log .debug ("No cross_section_normal given. Defaulting to [0, 0, 1]." )
389399
400+ anchor = anchor .lower ()
401+ if anchor == "auto" :
402+ anchor = (
403+ "geometry_box" if cross_section .para_dim == 1 else "parametric"
404+ )
405+ if anchor not in {"parametric" , "control_box" , "geometry_box" }:
406+ raise ValueError (
407+ "anchor must be one of 'auto', 'parametric', "
408+ "'control_box', or 'geometry_box'"
409+ )
410+
390411 ### STARTING CALCULATIONS ###
391412
392413 # make copies so we can work on it inplace
@@ -401,7 +422,13 @@ def swept(
401422
402423 # tangent vector 'e1' of trajectory at parameter value 0
403424 e1 = trajectory .derivative ([par_value [0 ]], [1 ])
404- e1 = (e1 / _np .linalg .norm (e1 )).ravel ()
425+ e1_norm = _np .linalg .norm (e1 )
426+ if e1_norm < _settings .TOLERANCE :
427+ raise ValueError (
428+ "Cannot determine initial sweep direction from trajectory "
429+ "because the first tangent is too small."
430+ )
431+ e1 = (e1 / e1_norm ).ravel ()
405432
406433 # evaluating a vector normal to e1
407434 vec = [- e1 [1 ], e1 [0 ], - e1 [2 ]]
@@ -429,14 +456,22 @@ def swept(
429456 # calculation according to NURBS Book, eq. 10.27
430457 # tangent vector e1 on trajectory at parameter value i
431458 e1 = trajectory .derivative ([par_value [i ]], [1 ])
432- if _np .linalg .norm (e1 ) < _settings .TOLERANCE :
433- e1 = tang_collection [- 1 ]
459+ e1_norm = _np .linalg .norm (e1 )
460+ if e1_norm < _settings .TOLERANCE :
461+ if tang_collection :
462+ e1 = tang_collection [- 1 ]
463+ e1_norm = _np .linalg .norm (e1 )
464+ else :
465+ raise ValueError (
466+ "Cannot determine sweep direction because the first "
467+ "trajectory tangent is too small."
468+ )
434469 # add debug message
435470 _log .debug (
436471 f"Division by zero occurred. Applying an approximation "
437472 f"by using the previous tangent e1 for parametric value { par_value [i ]} "
438473 )
439- e1 = (e1 / _np . linalg . norm ( e1 ) ).ravel ()
474+ e1 = (e1 / e1_norm ).ravel ()
440475 # collecting tangent vectors for later use
441476 tang_collection .append (e1 )
442477
@@ -458,15 +493,20 @@ def swept(
458493 T .append (_np .vstack ((e1 , e2 , e3 )))
459494
460495 # array of transformation matrices from local to global coordinates
461- A .append (_np . linalg . inv ( T [i ]) )
496+ A .append (T [i ]. T )
462497
463498 # separate procedure, if trajectory is closed and B[0] != B[-1]
464499 # recalculate B-vector and middle the values between B and B_rec
465500 # according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483
466- is_trajectory_closed = _np .array_equal (
467- trajectory .evaluate ([[0 ]]), trajectory .evaluate ([par_value [- 1 ]])
501+ is_trajectory_closed = _np .allclose (
502+ trajectory .evaluate ([[0 ]]),
503+ trajectory .evaluate ([par_value [- 1 ]]),
504+ atol = _settings .TOLERANCE ,
505+ rtol = 1e-8 ,
506+ )
507+ is_B_start_equal_B_end = _np .allclose (
508+ B [0 ], B [- 1 ], atol = _settings .TOLERANCE , rtol = 1e-8
468509 )
469- is_B_start_equal_B_end = _np .array_equal (B [0 ], B [- 1 ])
470510
471511 if is_trajectory_closed and not is_B_start_equal_B_end :
472512 # reset transformation matrices
@@ -503,7 +543,7 @@ def swept(
503543 T .append (_np .vstack ((tang_collection [i ], e2 , e3 )))
504544
505545 # array of transformation matrices from local to global coordinates
506- A .append (_np . linalg . inv ( T [i ]) )
546+ A .append (T [i ]. T )
507547
508548 # check if the beginning and the end of the B-vector are the same
509549 if not _np .allclose (B_rec [0 ], B_rec [- 1 ], rtol = 1e-3 ):
@@ -561,38 +601,58 @@ def swept(
561601 ### SWEEPING PROCESS ###
562602
563603 # evaluate center of cross-section and translate to origin
564- cross_para_center = _np .mean (cross_section .parametric_bounds , axis = 0 )
565- cs_center = cross_section .evaluate (
566- cross_para_center .reshape (- 1 , cross_section .para_dim )
567- ).ravel ()
568- cross_section .control_points -= cs_center
604+ if anchor == "parametric" :
605+ cross_para_center = _np .mean (cross_section .parametric_bounds , axis = 0 )
606+ cs_center = cross_section .evaluate (
607+ cross_para_center .reshape (- 1 , cross_section .para_dim )
608+ ).ravel ()
609+ elif anchor == "control_box" :
610+ cs_min = cross_section .control_points .min (axis = 0 )
611+ cs_max = cross_section .control_points .max (axis = 0 )
612+ cs_center = 0.5 * (cs_min + cs_max )
613+ else :
614+ if cross_section .para_dim == 1 :
615+ sample_resolution = max (
616+ 101 , 4 * cross_section .control_points .shape [0 ]
617+ )
618+ else :
619+ sample_resolution = int (
620+ _np .ceil (625 ** (1 / cross_section .para_dim ))
621+ )
622+ sample_resolution = max (5 , min (25 , sample_resolution ))
623+
624+ sample_axes = [
625+ _np .linspace (
626+ cross_section .parametric_bounds [0 , i ],
627+ cross_section .parametric_bounds [1 , i ],
628+ sample_resolution ,
629+ )
630+ for i in range (cross_section .para_dim )
631+ ]
632+ sample_queries = _np .stack (
633+ _np .meshgrid (* sample_axes , indexing = "ij" ),
634+ axis = - 1 ,
635+ ).reshape (- 1 , cross_section .para_dim )
636+ sampled_points = cross_section .evaluate (sample_queries )
637+ cs_min = sampled_points .min (axis = 0 )
638+ cs_max = sampled_points .max (axis = 0 )
639+ cs_center = 0.5 * (cs_min + cs_max )
640+
641+ centered_cross_section_cps = cross_section .control_points - cs_center
569642
570643 # set cross-section control points along trajectory
571644 swept_spline_cps = []
645+ rotated_cross_section_cps = centered_cross_section_cps @ R .T
572646 for i , par_val in enumerate (par_value ):
573- temp_csp = []
574647 # evaluate trajectory if user wants to set cross-section on trajectory.
575648 if set_on_trajectory :
576- evals = trajectory .evaluate ([par_val ]).ravel ()
577- # place every control point of cross-section separately
578- for cscp in cross_section .control_points :
579- # rotate cross-section in trajectory direction
580- normal_cscp = _np .matmul (R , cscp )
581- # transform cross-section to global coordinates
582- normal_cscp = _np .matmul (A [i ], normal_cscp )
583- # check if user wants to place cross-section at
584- # evaluation points or control points of trajectory
585- if set_on_trajectory :
586- # translate cross-section to trajectory evaluation point
587- normal_cscp += evals
588- else :
589- # translate cross-section to trajectory control point
590- normal_cscp += trajectory .control_points [i ]
591- # append control point to list
592- temp_csp .append (normal_cscp )
649+ section_origin = trajectory .evaluate ([par_val ]).ravel ()
650+ else :
651+ section_origin = trajectory .control_points [i ]
593652
594- # collect all control points
595- swept_spline_cps .append (_np .array (temp_csp ))
653+ swept_spline_cps .append (
654+ rotated_cross_section_cps @ A [i ].T + section_origin
655+ )
596656
597657 # create spline dictionary
598658 dict_swept_spline = {
0 commit comments