Skip to content

Commit aa3cb5c

Browse files
committed
add support for aligning 2d cropbox to 3d sectionbox
1 parent 43bba1d commit aa3cb5c

File tree

2 files changed

+207
-32
lines changed

2 files changed

+207
-32
lines changed

extensions/pyRevitTools.extension/pyRevit.tab/Modify.panel/3D.pulldown/Section Box Navigator.pushbutton/script.py

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
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+
)
2026
from sectionbox_actions import toggle, hide, align_to_face
2127
from 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):
14861584
if __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
)

extensions/pyRevitTools.extension/pyRevit.tab/Modify.panel/3D.pulldown/Section Box Navigator.pushbutton/sectionbox_utils.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
from pyrevit import DB
2+
from pyrevit.compat import get_elementid_value_func, get_elementid_from_value_func
3+
from pyrevit.coreutils import math
24

5+
get_elementid_value = get_elementid_value_func()
6+
get_elementid_from_value = get_elementid_from_value_func()
37

4-
def is_2d_view(view):
8+
9+
def is_2d_view(view, only_plan=False):
510
"""Check if a view is a 2D view (plan, elevation, section)."""
611
view_type = view.ViewType
7-
return view_type in [
12+
if only_plan:
13+
return view_type in (
14+
DB.ViewType.FloorPlan,
15+
DB.ViewType.CeilingPlan,
16+
)
17+
18+
return view_type in (
819
DB.ViewType.FloorPlan,
920
DB.ViewType.CeilingPlan,
1021
DB.ViewType.Section,
1122
DB.ViewType.Elevation,
12-
]
23+
)
1324

1425

1526
def get_view_range_and_crop(view, doc):
@@ -59,3 +70,71 @@ def get_view_range_and_crop(view, doc):
5970
}
6071

6172
return None
73+
74+
75+
def get_crop_element(doc, view):
76+
vid = get_elementid_value(view.Id)
77+
expected_name = view.get_Parameter(DB.BuiltInParameter.VIEW_NAME).AsString()
78+
cid = vid + 2
79+
crop_el = doc.GetElement(get_elementid_from_value(cid))
80+
if crop_el:
81+
param = crop_el.get_Parameter(DB.BuiltInParameter.VIEW_NAME)
82+
if param and param.AsString() == expected_name:
83+
return crop_el
84+
85+
86+
def compute_rotation_angle(section_box, view):
87+
# 3D box X-axis projected to XY
88+
bx = section_box.Transform.BasisX
89+
angle_box = math.atan2(bx.Y, bx.X)
90+
91+
# View X-axis in world
92+
vx = view.CropBox.Transform.BasisX
93+
angle_view = math.atan2(vx.Y, vx.X)
94+
95+
# rotation needed to align view to box
96+
return angle_box - angle_view
97+
98+
99+
def apply_plan_viewrange_from_sectionbox(doc, view, section_box):
100+
vr = view.GetViewRange()
101+
if not vr:
102+
return
103+
104+
# ---- 1. Collect all level Z coordinates ----
105+
def lvl_z(plane):
106+
lvl = doc.GetElement(vr.GetLevelId(plane))
107+
return lvl.Elevation if lvl else 0.0
108+
109+
z_bottom_lvl = lvl_z(DB.PlanViewPlane.BottomClipPlane)
110+
z_cut_lvl = lvl_z(DB.PlanViewPlane.CutPlane)
111+
z_top_lvl = lvl_z(DB.PlanViewPlane.TopClipPlane)
112+
z_depth_lvl = lvl_z(DB.PlanViewPlane.ViewDepthPlane)
113+
114+
# ---- 2. Transform box coords into world space ----
115+
tf = section_box.Transform
116+
world_min = tf.OfPoint(section_box.Min)
117+
world_max = tf.OfPoint(section_box.Max)
118+
119+
new_bottom_z = world_min.Z
120+
new_top_z = world_max.Z
121+
122+
# ---- 3. Compute offsets relative to each plane's level ----
123+
bottom_offset = new_bottom_z - z_bottom_lvl
124+
top_offset = new_top_z - z_top_lvl
125+
126+
# Safe cut plane: middle of range
127+
cut_z = (new_bottom_z + new_top_z) / 2.0
128+
cut_offset = cut_z - z_cut_lvl
129+
130+
# Safe view depth: slightly below bottom
131+
depth_z = new_bottom_z - 3.0
132+
depth_offset = depth_z - z_depth_lvl
133+
134+
# ---- 4. Apply all offsets ----
135+
vr.SetOffset(DB.PlanViewPlane.BottomClipPlane, bottom_offset)
136+
vr.SetOffset(DB.PlanViewPlane.CutPlane, cut_offset)
137+
vr.SetOffset(DB.PlanViewPlane.TopClipPlane, top_offset)
138+
vr.SetOffset(DB.PlanViewPlane.ViewDepthPlane, depth_offset)
139+
140+
view.SetViewRange(vr)

0 commit comments

Comments
 (0)