1616 get_next_level_below ,
1717 find_next_grid_in_direction ,
1818)
19- from sectionbox_utils import is_2d_view , get_view_range_and_crop
19+ from sectionbox_utils import (
20+ is_2d_view ,
21+ get_view_range_and_crop ,
22+ get_crop_element ,
23+ compute_rotation_angle ,
24+ apply_plan_viewrange_from_sectionbox ,
25+ )
2026from sectionbox_actions import toggle , hide , align_to_face
2127from sectionbox_geometry import (
2228 get_section_box_info ,
@@ -242,6 +248,11 @@ def update_info(self):
242248 try :
243249 self .current_view = doc .ActiveView
244250
251+ if is_2d_view (self .current_view ):
252+ self .btnAlignToView .Content = "Align with 3D View"
253+ elif isinstance (self .current_view , DB .View3D ):
254+ self .btnAlignToView .Content = "Align with 2D View"
255+
245256 if (
246257 not isinstance (self .current_view , DB .View3D )
247258 or not self .current_view .IsSectionBoxActive
@@ -404,16 +415,18 @@ def execute_action(self, params):
404415 self .do_align_to_face ()
405416 elif action_type == "expand_shrink" :
406417 self .do_expand_shrink (params )
407- elif action_type == "align_to_view" :
408- self .do_align_to_view (params )
418+ elif action_type == "align_to_2d_view" :
419+ self .do_align_to_2d_view (params )
420+ elif action_type == "align_to_3d_view" :
421+ self .do_align_to_3d_view (params )
409422 elif action_type == "grid_move" :
410423 self .do_grid_move (params )
411424
412425 # Update info after action
413426 self .Dispatcher .Invoke (System .Action (self .update_info ))
414427
415428 except Exception as ex :
416- logger .error ("Error executing action: {}" .format (ex ))
429+ logger .exception ("Error executing action: {}" .format (ex ))
417430
418431 def do_level_move (self , params ):
419432 """Move section box to level or by nudge amount."""
@@ -808,7 +821,7 @@ def do_grid_move(self, params):
808821 "success" ,
809822 )
810823
811- def do_align_to_view (self , params ):
824+ def do_align_to_2d_view (self , params ):
812825 """Align section box to a 2D view's range and crop."""
813826 view_data = params .get ("view_data" )
814827 if not view_data :
@@ -882,6 +895,62 @@ def do_align_to_view(self, params):
882895 "success" ,
883896 )
884897
898+ def do_align_to_3d_view (self , params ):
899+ view_data = params .get ("view_data" )
900+ if not view_data :
901+ return
902+
903+ vt = view_data .get ("view_type" , None )
904+ section_box = view_data .get ("section_box" , None )
905+
906+ # Has to be a seperate Transaction for rotate_crop_element to find the bbox
907+ with revit .Transaction ("Activate CropBox" ):
908+ if not self .current_view .CropBoxActive :
909+ self .current_view .CropBoxActive = True
910+
911+ if not self .current_view .CropBoxVisible :
912+ self .current_view .CropBoxVisible = True
913+
914+ with revit .Transaction ("Align 2D View to 3D Section Box" ):
915+ if vt == DB .ViewType .FloorPlan or vt == DB .ViewType .CeilingPlan :
916+ self .current_view .CropBox = section_box
917+ crop_el = get_crop_element (doc , self .current_view )
918+ if crop_el :
919+ # --- 1. Compute 3D section box centroid in world coordinates ---
920+ tf = section_box .Transform
921+ sb_min = tf .OfPoint (section_box .Min )
922+ sb_max = tf .OfPoint (section_box .Max )
923+ sb_centroid = DB .XYZ (
924+ (sb_min .X + sb_max .X ) / 2.0 ,
925+ (sb_min .Y + sb_max .Y ) / 2.0 ,
926+ 0 # Z is ignored for plan rotation
927+ )
928+
929+ # --- 2. Compute current crop element centroid in view coordinates ---
930+ crop_box = crop_el .get_BoundingBox (self .current_view )
931+ crop_centroid = DB .XYZ (
932+ (crop_box .Min .X + crop_box .Max .X ) / 2.0 ,
933+ (crop_box .Min .Y + crop_box .Max .Y ) / 2.0 ,
934+ 0
935+ )
936+
937+ # --- 3. Translate crop element so centroids align (XY only) ---
938+ translation = sb_centroid - crop_centroid
939+ DB .ElementTransformUtils .MoveElement (doc , crop_el .Id , translation )
940+
941+ # --- 4. Rotate crop element around vertical axis through its centroid ---
942+ angle = compute_rotation_angle (section_box , self .current_view )
943+ axis = DB .Line .CreateBound (
944+ DB .XYZ (sb_centroid .X , sb_centroid .Y , 0 ),
945+ DB .XYZ (sb_centroid .X , sb_centroid .Y , 1 )
946+ )
947+ DB .ElementTransformUtils .RotateElement (doc , crop_el .Id , axis , angle )
948+ apply_plan_viewrange_from_sectionbox (doc , self .current_view , section_box )
949+
950+ else :
951+ self .show_status_message (1 , "Unsupported view type." , "warning" )
952+ return
953+
885954 def do_toggle (self ):
886955 """Toggle section box."""
887956 was_active = self .current_view .IsSectionBoxActive
@@ -1168,29 +1237,58 @@ def btn_expansion_top_down_click(self, sender, e):
11681237 )
11691238
11701239 def btn_align_box_to_view_click (self , sender , e ):
1171- """Align section box to a selected 2D view."""
1172- # Select a 2D view
1173- selected_view = forms .select_views (
1174- multiple = False ,
1175- filterfunc = is_2d_view ,
1176- title = "Select 2D View for Section Box" ,
1177- )
1240+ """Align section box to a selected view."""
1241+ self .current_view = doc .ActiveView
1242+ # Select the view to align
1243+ if isinstance (self .current_view , DB .View3D ):
1244+ selected_view = forms .select_views (
1245+ multiple = False ,
1246+ filterfunc = is_2d_view ,
1247+ title = "Select 2D View for Section Box" ,
1248+ )
11781249
1179- if not selected_view :
1180- return
1250+ if not selected_view :
1251+ return
1252+
1253+ # Get view range and crop information
1254+ view_data = get_view_range_and_crop (selected_view , doc )
1255+ self .pending_action = {
1256+ "action" : "align_to_2d_view" ,
1257+ "view_data" : view_data ,
1258+ }
11811259
1182- # Get view range and crop information
1183- view_data = get_view_range_and_crop (selected_view , doc )
1260+ elif is_2d_view (self .current_view , only_plan = True ):
1261+
1262+ selected_view = forms .select_views (
1263+ multiple = False ,
1264+ filterfunc = lambda v : isinstance (v , DB .View3D ),
1265+ title = "Select 3D View to Copy From" ,
1266+ )
1267+ if not selected_view :
1268+ return
1269+
1270+ info = get_section_box_info (selected_view , DATAFILENAME )
1271+ section_box = info .get ("box" )
1272+ if not section_box :
1273+ self .show_status_message (1 , "3D view has no section box." , "error" )
1274+ return
1275+
1276+ view_data = {
1277+ "view_type" : self .current_view .ViewType ,
1278+ "section_box" : section_box ,
1279+ }
1280+ self .pending_action = {
1281+ "action" : "align_to_3d_view" ,
1282+ "view_data" : view_data ,
1283+ }
1284+
1285+ else :
1286+ return
11841287
11851288 if not view_data :
11861289 self .show_status_message (1 , "Could not extract view information." , "error" )
11871290 return
11881291
1189- # Queue the action to be executed in Revit context
1190- self .pending_action = {
1191- "action" : "align_to_view" ,
1192- "view_data" : view_data ,
1193- }
11941292 self .event_handler .parameters = self .pending_action
11951293 self .ext_event .Raise ()
11961294
@@ -1486,14 +1584,12 @@ def form_closed(self, sender, args):
14861584if __name__ == "__main__" :
14871585 try :
14881586 # Check if section box is active
1489- if not isinstance ( active_view , DB . View3D ) or not active_view .IsSectionBoxActive :
1587+ if not active_view .IsSectionBoxActive :
14901588 try :
1491- view_boxes = script .load_data (DATAFILENAME )
1492- view_id_value = get_elementid_value (active_view .Id )
1493- if view_id_value not in view_boxes :
1494- raise KeyError ("View not found in stored boxes" )
1495- bbox_data = view_boxes [view_id_value ]
1496- restored_bbox = revit .deserialize (bbox_data )
1589+ info = get_section_box_info (active_view , DATAFILENAME )
1590+ restored_bbox = info .get ("box" )
1591+ if not restored_bbox :
1592+ raise Exception
14971593
14981594 # Ask user if they want to restore
14991595 if forms .alert (
@@ -1505,7 +1601,7 @@ def form_closed(self, sender, args):
15051601 active_view .SetSectionBox (restored_bbox )
15061602 except Exception :
15071603 forms .alert (
1508- "The current view isn't 3D or doesn't have an active section box." ,
1604+ "The current view doesn't have an active or stored section box." ,
15091605 title = "No Section Box" ,
15101606 exitscript = True ,
15111607 )
0 commit comments