From 210b95ff7bed39518bcce2b02fa40c8e9549d69c Mon Sep 17 00:00:00 2001 From: Isaiah-Narvaez-42 Date: Tue, 14 Oct 2025 14:23:31 -0400 Subject: [PATCH 1/6] Improved overall tool functionality Added offset and level change directly in UI --- .../ViewRange.pushbutton/script.py | 909 ++++++++++++++---- 1 file changed, 737 insertions(+), 172 deletions(-) diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py index c01ff93048..af905b09a7 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py @@ -2,7 +2,6 @@ from __future__ import print_function from pyrevit import script, forms, revit, HOST_APP -from pyrevit.revit import dc3dserver as d3d import traceback from Autodesk.Revit import DB, UI @@ -12,27 +11,29 @@ from System import EventHandler, Convert from System.Windows.Media import Color, SolidColorBrush - from System.Collections.Generic import List doc = revit.doc uidoc = revit.uidoc - logger = script.get_logger() output = script.get_output() PLANES = { - DB.PlanViewPlane.TopClipPlane: [0, 255, 0], - DB.PlanViewPlane.CutPlane: [255, 0, 0], - DB.PlanViewPlane.BottomClipPlane: [0, 0, 255], - DB.PlanViewPlane.ViewDepthPlane: [255, 127, 0] + DB.PlanViewPlane.TopClipPlane: ([0, 255, 0], "Top Clip Plane", "topplane"), + DB.PlanViewPlane.CutPlane: ([255, 0, 0], "Cut Plane", "cutplane"), + DB.PlanViewPlane.BottomClipPlane: ([0, 0, 255], "Bottom Clip Plane", "bottomplane"), + DB.PlanViewPlane.ViewDepthPlane: ([255, 127, 0], "View Depth Plane", "viewdepth"), } -class SimpleEventHandler(UI.IExternalEventHandler): - """ - Simple IExternalEventHandler sample - """ +PLANE_ORDER = [ + DB.PlanViewPlane.TopClipPlane, + DB.PlanViewPlane.CutPlane, + DB.PlanViewPlane.BottomClipPlane, + DB.PlanViewPlane.ViewDepthPlane, +] + +class SimpleEventHandler(UI.IExternalEventHandler): def __init__(self, do_this): self.do_this = do_this @@ -40,12 +41,34 @@ def Execute(self, uiapp): try: self.do_this(uiapp) except InvalidOperationException: - print('InvalidOperationException catched') + pass def GetName(self): return "SimpleEventHandler" +class UpdateViewRangeEventHandler(UI.IExternalEventHandler): + def __init__(self): + self.new_values = None + self.new_levels = None + self.context = None + + def Execute(self, uiapp): + try: + if self.context and self.new_values: + self.context._update_view_range_internal( + self.new_values, self.new_levels + ) + except Exception as e: + if hasattr(self.context, "view_model"): + self.context.view_model.warning_message = ( + "Error executing view range update: {}".format(str(e)) + ) + + def GetName(self): + return "UpdateViewRangeEventHandler" + + class Context(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, "instance"): @@ -55,12 +78,16 @@ def __new__(cls, *args, **kwargs): def __init__(self, view_model): self._active_view = None self._source_view = None - self.length_unit = (doc.GetUnits() - .GetFormatOptions(DB.SpecTypeId.Length) - .GetUnitTypeId()) - + self.length_unit = ( + doc.GetUnits().GetFormatOptions(DB.SpecTypeId.Length).GetUnitTypeId() + ) self.height_data = {} + self.offset_data = {} + self.level_data = {} + self.original_offset_data = {} + self.original_level_data = {} self.view_model = view_model + self._levels_populated = False # Track if levels have been populated view_model.unit_label = DB.LabelUtils.GetLabelForUnit(self.length_unit) @property @@ -68,6 +95,7 @@ def active_view(self): if self._active_view and not self._active_view.IsValidObject: self._active_view = None return self._active_view + @active_view.setter def active_view(self, value): if not compare_views(self._active_view, value): @@ -79,178 +107,502 @@ def source_view(self): if self._source_view and not self._source_view.IsValidObject: self._source_view = None return self._source_view + @source_view.setter def source_view(self, value): if not compare_views(self._source_view, value): self._source_view = value + self._levels_populated = False # Reset when view changes self.context_changed() + def update_view_range(self, new_values, new_levels=None): + if not self.source_view or not isinstance(self.source_view, DB.ViewPlan): + self.view_model.warning_message = "No valid plan view selected" + return False + if self.source_view.IsTemplate: + self.view_model.warning_message = ( + "Cannot modify view range - this is a view template" + ) + return False + + update_view_range_handler.new_values = new_values + update_view_range_handler.new_levels = new_levels or {} + update_view_range_handler.context = self + update_view_range_event.Raise() + return True + + def _update_view_range_internal(self, new_values, new_levels=None): + try: + if not self._validate_view_range_order(new_values, new_levels): + return False + + with revit.Transaction("Update View Range", doc=revit.doc): + view_range = self.source_view.GetViewRange() + + # First, update levels if provided + if new_levels: + for plane, new_level_id in new_levels.items(): + current_level_id = view_range.GetLevelId(plane) + + # Handle Unlimited (InvalidElementId) + if new_level_id == DB.ElementId.InvalidElementId: + # For View Depth, Unlimited is common - set to InvalidElementId + if current_level_id != DB.ElementId.InvalidElementId: + try: + # Note: Not all planes support InvalidElementId + # View Depth typically does, others may not + view_range.SetLevelId( + plane, DB.ElementId.InvalidElementId + ) + except Exception: + pass + # Handle regular level changes + elif new_level_id and current_level_id != new_level_id: + try: + view_range.SetLevelId(plane, new_level_id) + except Exception: + pass + + # Then, update offsets (only for planes that have valid levels) + for plane, offset_str in new_values.items(): + # Skip offset update if level is set to Unlimited + level_id = view_range.GetLevelId(plane) + if not level_id or level_id == DB.ElementId.InvalidElementId: + continue + + if ( + offset_str + and offset_str.strip() + and offset_str != "-" + and offset_str.upper() != "N/A" + ): + try: + offset_display = float(offset_str) + offset_internal = DB.UnitUtils.ConvertToInternalUnits( + offset_display, self.length_unit + ) + current_offset = view_range.GetOffset(plane) + + # Only update if different + if abs(current_offset - offset_internal) > 0.0001: + view_range.SetOffset(plane, offset_internal) + except ValueError: + return False + + # Apply the view range back to the view + self.source_view.SetViewRange(view_range) + self.context_changed() + self.view_model.warning_message = "View range updated successfully" + return True + + except Exception as e: + self.view_model.warning_message = "Error updating view range: {}".format( + str(e) + ) + return False + + def _validate_view_range_order(self, new_values, new_levels=None): + try: + elevations = {} + offset_values = {} + + for plane, offset_str in new_values.items(): + # Skip validation for N/A values (Unlimited levels) + if offset_str and offset_str.upper() == "N/A": + continue + + if offset_str and offset_str.strip() and offset_str != "-": + try: + offset_value = float(offset_str) + offset_values[plane] = offset_value + + # Use new level if provided, otherwise use current level + level = None + if new_levels and plane in new_levels: + level_id = new_levels[plane] + # Skip validation for Unlimited (InvalidElementId) + if ( + not level_id + or level_id == DB.ElementId.InvalidElementId + ): + continue + level = self.source_view.Document.GetElement(level_id) + + if not level: + level = self.level_data.get(plane) + + if level: + level_elevation = DB.UnitUtils.ConvertFromInternalUnits( + level.ProjectElevation, self.length_unit + ) + elevations[plane] = level_elevation + offset_value + except ValueError: + forms.alert( + "Invalid Input Format!\n\nThe value '{}' for {} is not a valid number.\n\n" + "Please enter a numeric value (e.g., 4.5, -2.0, 0)".format( + offset_str, PLANES[plane][1] + ), + title="Invalid Number Format", + warn_icon=True, + ) + self.view_model.warning_message = ( + "Invalid number format: {}".format(offset_str) + ) + return False + + # If any plane is set to Unlimited, skip full validation + if len(elevations) < 4: + return True + + # Check order: Top >= Cut >= Bottom >= ViewDepth + checks = [ + ( + DB.PlanViewPlane.TopClipPlane, + DB.PlanViewPlane.CutPlane, + "Top Clip Plane", + "Cut Plane", + ), + ( + DB.PlanViewPlane.CutPlane, + DB.PlanViewPlane.BottomClipPlane, + "Cut Plane", + "Bottom Clip Plane", + ), + ( + DB.PlanViewPlane.BottomClipPlane, + DB.PlanViewPlane.ViewDepthPlane, + "Bottom Clip Plane", + "View Depth Plane", + ), + ] + + for higher_plane, lower_plane, higher_name, lower_name in checks: + higher_elev = elevations.get(higher_plane) + lower_elev = elevations.get(lower_plane) + + if ( + higher_elev is not None + and lower_elev is not None + and higher_elev < lower_elev + ): + forms.alert( + "View Range Order Error!\n\n{} (offset: {:.2f}') must be greater than or equal to {} " + "(offset: {:.2f}').\n\nNote: These are offset values from the associated level.\n\n" + "Correct order (top to bottom):\n1. Top Clip Plane (highest offset)\n2. Cut Plane\n" + "3. Bottom Clip Plane\n4. View Depth Plane (lowest offset)".format( + higher_name, + offset_values.get(higher_plane, 0), + lower_name, + offset_values.get(lower_plane, 0), + ), + title="Invalid View Range", + warn_icon=True, + ) + self.view_model.warning_message = "{} must be >= {}".format( + higher_name, lower_name + ) + return False + + return True + + except Exception as e: + self.view_model.warning_message = "Error validating view range: {}".format( + str(e) + ) + return False + + def _populate_available_levels(self): + """Populate the list of available levels in the project""" + try: + # Get all levels in the project + level_collector = DB.FilteredElementCollector(doc).OfClass(DB.Level) + levels = list(level_collector) + + # Sort levels by elevation + levels.sort(key=lambda x: x.ProjectElevation) + + # Create a simple class for level items that WPF can bind to + class LevelItem(object): + def __init__(self, name, element_id, elevation=None, is_special=False): + self.Name = name + self.Id = element_id + # Store integer ID for WPF binding (WPF can't compare ElementId properly) + self.IdValue = element_id.IntegerValue if element_id else -1 + self.Elevation = elevation + self.IsSpecial = is_special + + # Create level items + level_items = [] + + # Add special "Unlimited" option (uses InvalidElementId) + level_items.append( + LevelItem("Unlimited", DB.ElementId.InvalidElementId, None, True) + ) + + # Add actual levels + for level in levels: + level_item = LevelItem( + level.Name, level.Id, level.ProjectElevation, False + ) + level_items.append(level_item) + + self.view_model.available_levels = level_items + self._levels_populated = True + + except Exception as e: + self.view_model.warning_message = "Error loading levels: {}".format(str(e)) + + def _set_current_level_selections(self, view_range): + """Set the current level selections based on the view range""" + try: + # Store current selections + stored_selections = {} + + # Set level selections for each plane + for plane in PLANES: + level_id = view_range.GetLevelId(plane) + + if plane == DB.PlanViewPlane.TopClipPlane: + if level_id and level_id != DB.ElementId.InvalidElementId: + stored_selections["top"] = level_id.IntegerValue + else: + stored_selections["top"] = -1 + + elif plane == DB.PlanViewPlane.CutPlane: + # For Cut Plane, show the level name as read-only text + if level_id and level_id != DB.ElementId.InvalidElementId: + level = self.active_view.Document.GetElement(level_id) + self.view_model.cutplane_level_name = ( + level.Name if level else "Unknown" + ) + else: + self.view_model.cutplane_level_name = "Unlimited" + + elif plane == DB.PlanViewPlane.BottomClipPlane: + if level_id and level_id != DB.ElementId.InvalidElementId: + stored_selections["bottom"] = level_id.IntegerValue + else: + stored_selections["bottom"] = -1 + + elif plane == DB.PlanViewPlane.ViewDepthPlane: + if level_id and level_id != DB.ElementId.InvalidElementId: + stored_selections["viewdepth"] = level_id.IntegerValue + else: + stored_selections["viewdepth"] = -1 + + # Force update the view model properties (reset first to force binding refresh) + self.view_model.topplane_level_id = None + self.view_model.bottomplane_level_id = None + self.view_model.viewdepth_level_id = None + + # Then set the actual values + self.view_model.topplane_level_id = stored_selections.get("top", -1) + self.view_model.bottomplane_level_id = stored_selections.get("bottom", -1) + self.view_model.viewdepth_level_id = stored_selections.get("viewdepth", -1) + + except Exception as e: + self.view_model.warning_message = ( + "Error setting level selections: {}".format(str(e)) + ) + def context_changed(self): server.uidoc = UI.UIDocument(self.active_view.Document) - self.view_model.topplane_elevation = "-" - self.view_model.cutplane_elevation = "-" - self.view_model.bottomplane_elevation = "-" - self.view_model.viewdepth_elevation = "-" + + # Reset all elevation and input values + for _, _, prefix in PLANES.values(): + setattr(self.view_model, prefix + "_elevation", "-") + setattr(self.view_model, prefix + "_new_value", "") + + self.view_model.warning_message = "" if not self.is_valid(): server.meshes = None refresh_event.Raise() - return - try: + try: + edges, triangles = [], [] - edges = [] - triangles = [] if isinstance(self.source_view, DB.ViewPlan): - - if self.active_view.get_Parameter( + if ( + self.active_view.get_Parameter( DB.BuiltInParameter.VIEWER_MODEL_CLIP_BOX_ACTIVE - ).AsInteger() == 1: - bbox = self.active_view.GetSectionBox() - - corners = corners_from_bb(bbox) + ).AsInteger() + == 1 + ): + corners = corners_from_bb(self.active_view.GetSectionBox()) else: - crop_bbox = self.source_view.CropBox - corners = corners_from_bb(crop_bbox) + corners = corners_from_bb(self.source_view.CropBox) view_range = self.source_view.GetViewRange() - for plane in PLANES: + # Only populate levels if not already populated + if not self._levels_populated: + self._populate_available_levels() - plane_level = self.source_view.Document.GetElement( - view_range.GetLevelId(plane) - ) + for plane in PLANE_ORDER: + color_rgb, name, prefix = PLANES[plane] + level_id = view_range.GetLevelId(plane) + + # Check if this plane is set to Unlimited + if not level_id or level_id == DB.ElementId.InvalidElementId: + self.height_data[plane] = "N/A" + self.offset_data[plane] = "N/A" + self.level_data[plane] = None + continue + + plane_level = self.source_view.Document.GetElement(level_id) if not plane_level: self.height_data[plane] = "N/A" + self.offset_data[plane] = "N/A" + self.level_data[plane] = None continue + + self.level_data[plane] = plane_level + plane_elevation = ( - plane_level.ProjectElevation - + view_range.GetOffset(plane) + plane_level.ProjectElevation + view_range.GetOffset(plane) ) - self.height_data[plane] = round( DB.UnitUtils.ConvertFromInternalUnits( - plane_elevation, - self.length_unit + plane_elevation, self.length_unit ), - 2 + 2, ) + offset_value = round( + DB.UnitUtils.ConvertFromInternalUnits( + view_range.GetOffset(plane), self.length_unit + ), + 2, + ) + self.offset_data[plane] = offset_value + + if plane not in self.original_offset_data: + self.original_offset_data[plane] = offset_value + + # Store original level data + if plane not in self.original_level_data: + self.original_level_data[plane] = level_id + cut_plane_vertices = [ DB.XYZ(c.X, c.Y, plane_elevation) for c in corners ] - color = get_color_from_plane(plane) + edges.extend(create_edges(cut_plane_vertices, color)) + triangles.extend(create_triangles(cut_plane_vertices, color)) + + # Set all view model properties + for plane in PLANE_ORDER: + _, _, prefix = PLANES[plane] + setattr( + self.view_model, + prefix + "_elevation", + str(self.height_data[plane]), + ) + setattr( + self.view_model, + prefix + "_new_value", + str(self.offset_data[plane]), + ) - edges.extend( - create_edges(cut_plane_vertices, color)) - - triangles.extend( - create_triangles(cut_plane_vertices, color)) - - self.view_model.topplane_elevation = str(self.height_data[ - DB.PlanViewPlane.TopClipPlane]) - self.view_model.cutplane_elevation = str(self.height_data[ - DB.PlanViewPlane.CutPlane]) - self.view_model.bottomplane_elevation = str(self.height_data[ - DB.PlanViewPlane.BottomClipPlane]) - self.view_model.viewdepth_elevation = str(self.height_data[ - DB.PlanViewPlane.ViewDepthPlane]) + # Set current level selections AFTER ensuring levels are populated + self._set_current_level_selections(view_range) else: crop_bbox = self.source_view.CropBox cut_plane_vertices = corners_from_bb(crop_bbox) - plane = DB.PlanViewPlane.ViewDepthPlane - - color = get_color_from_plane(plane) - - edges.extend( - create_edges(cut_plane_vertices, color)) - - triangles.extend( - create_triangles(cut_plane_vertices, color)) - - view_dir_transform = DB.Transform.CreateTranslation( - self.source_view.ViewDirection.Negate() - * self.source_view.CropBox.Min.Z - ) - cut_plane_vertices = [view_dir_transform.OfPoint(pt) - for pt in cut_plane_vertices] - plane = DB.PlanViewPlane.CutPlane - - color = get_color_from_plane(plane) - - edges.extend( - create_edges(cut_plane_vertices, color)) - - triangles.extend( - create_triangles(cut_plane_vertices, color)) - - - - mesh = revit.dc3dserver.Mesh( - edges, - triangles - ) - - server.meshes = [mesh] + for plane_type in [ + DB.PlanViewPlane.ViewDepthPlane, + DB.PlanViewPlane.CutPlane, + ]: + color = get_color_from_plane(plane_type) + edges.extend(create_edges(cut_plane_vertices, color)) + triangles.extend(create_triangles(cut_plane_vertices, color)) + + if plane_type == DB.PlanViewPlane.CutPlane: + view_dir_transform = DB.Transform.CreateTranslation( + self.source_view.ViewDirection.Negate() + * self.source_view.CropBox.Min.Z + ) + cut_plane_vertices = [ + view_dir_transform.OfPoint(pt) for pt in cut_plane_vertices + ] + + server.meshes = [revit.dc3dserver.Mesh(edges, triangles)] refresh_event.Raise() - except: + except Exception: print(traceback.format_exc()) - - def is_valid(self): if not can_use_view_as_source(self.source_view): - self.view_model.message = \ + self.view_model.message = ( "Please select a Plan or Section View in the Project Browser!" + ) + self.view_model.can_modify_view = False return False elif not isinstance(context.active_view, DB.View3D): self.view_model.message = "Please activate a 3D View!" + self.view_model.can_modify_view = False return False elif ( - not context.source_view.CropBoxActive and - not context.active_view.get_Parameter( - DB.BuiltInParameter.VIEWER_MODEL_CLIP_BOX_ACTIVE - ).AsInteger() == 1 + not context.source_view.CropBoxActive + and not context.active_view.get_Parameter( + DB.BuiltInParameter.VIEWER_MODEL_CLIP_BOX_ACTIVE + ).AsInteger() + == 1 ): - self.view_model.message = ("Please activate the \"Section Box\" " - "on the active view,\nor the " - "\"Crop View\" on the selected view!") - + self.view_model.message = ( + 'Please activate the "Section Box" on the active view,\n' + 'or the "Crop View" on the selected view!' + ) + self.view_model.can_modify_view = False else: - self.view_model.message = "Showing View Range of\n[{}]".format( - self.source_view.Name) + can_modify = ( + isinstance(self.source_view, DB.ViewPlan) + and not self.source_view.IsTemplate + ) + self.view_model.can_modify_view = can_modify + + if self.source_view.IsTemplate: + self.view_model.message = "Showing View Range of [{}]\n(View Template - Cannot Modify)".format( + self.source_view.Name + ) + else: + self.view_model.message = "Showing View Range of\n[{}]".format( + self.source_view.Name + ) return True class MainViewModel(forms.Reactive): - def __init__(self): self._message = None - self.topplane_brush = SolidColorBrush(Color.FromRgb( - *[Convert.ToByte(i) for i in PLANES[DB.PlanViewPlane.TopClipPlane]] - )) - self.cutplane_brush = SolidColorBrush(Color.FromRgb( - *[Convert.ToByte(i) for i in PLANES[DB.PlanViewPlane.CutPlane]] - )) - self.bottomplane_brush = SolidColorBrush(Color.FromRgb( - *[Convert.ToByte(i) for i in PLANES[DB.PlanViewPlane.BottomClipPlane]] - )) - self.viewdepth_brush = SolidColorBrush(Color.FromRgb( - *[Convert.ToByte(i) for i in PLANES[DB.PlanViewPlane.ViewDepthPlane]] - )) - self._topplane_elevation = "-" - self._cutplane_elevation = "-" - self._bottomplane_elevation = "-" - self._viewdepth_elevation = "-" + self._warning_message = "" + self._can_modify_view = False + + # Initialize level-related properties - use INTEGER values for WPF binding + self._available_levels = [] + self._topplane_level_id = -1 + self._bottomplane_level_id = -1 + self._viewdepth_level_id = -1 + self._cutplane_level_name = "Unknown" self.unit_label = "" + # Create brushes and initialize values + for plane, (rgb, name, prefix) in PLANES.items(): + setattr( + self, + prefix + "_brush", + SolidColorBrush(Color.FromRgb(*[Convert.ToByte(i) for i in rgb])), + ) + setattr(self, "_" + prefix + "_elevation", "-") + setattr(self, "_" + prefix + "_new_value", "") + @forms.reactive def message(self): return self._message @@ -259,6 +611,64 @@ def message(self): def message(self, value): self._message = value + @forms.reactive + def warning_message(self): + return self._warning_message + + @warning_message.setter + def warning_message(self, value): + self._warning_message = value + + @forms.reactive + def can_modify_view(self): + return self._can_modify_view + + @can_modify_view.setter + def can_modify_view(self, value): + self._can_modify_view = value + + # Level properties + @forms.reactive + def available_levels(self): + return self._available_levels + + @available_levels.setter + def available_levels(self, value): + self._available_levels = value + + @forms.reactive + def topplane_level_id(self): + return self._topplane_level_id + + @topplane_level_id.setter + def topplane_level_id(self, value): + self._topplane_level_id = value + + @forms.reactive + def bottomplane_level_id(self): + return self._bottomplane_level_id + + @bottomplane_level_id.setter + def bottomplane_level_id(self, value): + self._bottomplane_level_id = value + + @forms.reactive + def viewdepth_level_id(self): + return self._viewdepth_level_id + + @viewdepth_level_id.setter + def viewdepth_level_id(self, value): + self._viewdepth_level_id = value + + @forms.reactive + def cutplane_level_name(self): + return self._cutplane_level_name + + @cutplane_level_name.setter + def cutplane_level_name(self, value): + self._cutplane_level_name = value + + # Elevation properties @forms.reactive def topplane_elevation(self): return self._topplane_elevation @@ -291,6 +701,39 @@ def viewdepth_elevation(self): def viewdepth_elevation(self, value): self._viewdepth_elevation = value + # New value properties + @forms.reactive + def topplane_new_value(self): + return self._topplane_new_value + + @topplane_new_value.setter + def topplane_new_value(self, value): + self._topplane_new_value = value + + @forms.reactive + def cutplane_new_value(self): + return self._cutplane_new_value + + @cutplane_new_value.setter + def cutplane_new_value(self, value): + self._cutplane_new_value = value + + @forms.reactive + def bottomplane_new_value(self): + return self._bottomplane_new_value + + @bottomplane_new_value.setter + def bottomplane_new_value(self, value): + self._bottomplane_new_value = value + + @forms.reactive + def viewdepth_new_value(self): + return self._viewdepth_new_value + + @viewdepth_new_value.setter + def viewdepth_new_value(self, value): + self._viewdepth_new_value = value + class MainWindow(forms.WPFWindow): def __init__(self): @@ -299,28 +742,160 @@ def __init__(self): subscribe() server.add_server() - def window_closed(self, sender, args): server.remove_server() refresh_event.Raise() unsubscribe_event.Raise() + def apply_changes_click(self, sender, e): + try: + new_values = { + plane: getattr(self.DataContext, prefix + "_new_value") + for plane, (_, _, prefix) in PLANES.items() + } + + # Build new_levels dictionary - convert integer IDs back to ElementId + new_levels = {} + + # Get values from ViewModel (which now stores integers) + top_id_int = self.DataContext.topplane_level_id + bottom_id_int = self.DataContext.bottomplane_level_id + viewdepth_id_int = self.DataContext.viewdepth_level_id + + # Top Plane + if top_id_int == -1: + new_levels[DB.PlanViewPlane.TopClipPlane] = ( + DB.ElementId.InvalidElementId + ) + elif top_id_int is not None: + new_levels[DB.PlanViewPlane.TopClipPlane] = DB.ElementId(top_id_int) + + # Bottom Plane + if bottom_id_int == -1: + new_levels[DB.PlanViewPlane.BottomClipPlane] = ( + DB.ElementId.InvalidElementId + ) + elif bottom_id_int is not None: + new_levels[DB.PlanViewPlane.BottomClipPlane] = DB.ElementId( + bottom_id_int + ) + + # View Depth + if viewdepth_id_int == -1: + new_levels[DB.PlanViewPlane.ViewDepthPlane] = ( + DB.ElementId.InvalidElementId + ) + elif viewdepth_id_int is not None: + new_levels[DB.PlanViewPlane.ViewDepthPlane] = DB.ElementId( + viewdepth_id_int + ) + + context.update_view_range(new_values, new_levels) + except Exception as ex: + self.DataContext.warning_message = "Error applying changes: {}".format( + str(ex) + ) + + def reset_values_click(self, sender, e): + try: + # Reset offset values to original + for plane, (_, _, prefix) in PLANES.items(): + original_value = context.original_offset_data.get(plane, "") + setattr(self.DataContext, prefix + "_new_value", str(original_value)) + + # Reset level selections to original levels + if context.original_level_data: + # Reset to original level selections + for plane in PLANES: + original_level_id = context.original_level_data.get(plane) + + if plane == DB.PlanViewPlane.TopClipPlane: + if ( + original_level_id + and original_level_id != DB.ElementId.InvalidElementId + ): + self.DataContext.topplane_level_id = ( + original_level_id.IntegerValue + ) + else: + self.DataContext.topplane_level_id = -1 + + elif plane == DB.PlanViewPlane.CutPlane: + # For Cut Plane, show the original level name as read-only text + if ( + original_level_id + and original_level_id != DB.ElementId.InvalidElementId + ): + try: + level = context.active_view.Document.GetElement( + original_level_id + ) + self.DataContext.cutplane_level_name = ( + level.Name if level else "Unknown" + ) + except Exception: + self.DataContext.cutplane_level_name = "Unknown" + else: + self.DataContext.cutplane_level_name = "Unlimited" + + elif plane == DB.PlanViewPlane.BottomClipPlane: + if ( + original_level_id + and original_level_id != DB.ElementId.InvalidElementId + ): + self.DataContext.bottomplane_level_id = ( + original_level_id.IntegerValue + ) + else: + self.DataContext.bottomplane_level_id = -1 + + elif plane == DB.PlanViewPlane.ViewDepthPlane: + if ( + original_level_id + and original_level_id != DB.ElementId.InvalidElementId + ): + self.DataContext.viewdepth_level_id = ( + original_level_id.IntegerValue + ) + else: + self.DataContext.viewdepth_level_id = -1 + else: + # Fallback: Reset level selections to current view range levels if no original data + if hasattr(context, "source_view") and context.source_view: + view_range = context.source_view.GetViewRange() + context._set_current_level_selections(view_range) + + self.DataContext.warning_message = "" + except Exception as ex: + self.DataContext.warning_message = "Error resetting values: {}".format( + str(ex) + ) + + def subscribe(): try: ui_app = UI.UIApplication(HOST_APP.app) ui_app.ViewActivated += EventHandler[ViewActivatedEventArgs](view_activated) - ui_app.SelectionChanged += EventHandler[SelectionChangedEventArgs](selection_changed) - ui_app.Application.DocumentChanged += EventHandler[DocumentChangedEventArgs](doc_changed) - except: + ui_app.SelectionChanged += EventHandler[SelectionChangedEventArgs]( + selection_changed + ) + ui_app.Application.DocumentChanged += EventHandler[DocumentChangedEventArgs]( + doc_changed + ) + except Exception: print(traceback.format_exc()) def unsubscribe(uiapp): try: uiapp.ViewActivated -= EventHandler[ViewActivatedEventArgs](view_activated) - uiapp.SelectionChanged -= EventHandler[SelectionChangedEventArgs](selection_changed) - uiapp.Application.DocumentChanged -= EventHandler[DocumentChangedEventArgs](doc_changed) - except: + uiapp.SelectionChanged -= EventHandler[SelectionChangedEventArgs]( + selection_changed + ) + uiapp.Application.DocumentChanged -= EventHandler[DocumentChangedEventArgs]( + doc_changed + ) + except Exception: print(traceback.format_exc()) @@ -331,21 +906,19 @@ def refresh_active_view(uiapp): uidoc.ActiveView = context.active_view uidoc.RefreshActiveView() if context.source_view: - uidoc.Selection.SetElementIds( - List[DB.ElementId]([context.source_view.Id])) - except: + uidoc.Selection.SetElementIds(List[DB.ElementId]([context.source_view.Id])) + except Exception: print(traceback.format_exc()) def view_activated(sender, args): try: context.active_view = args.CurrentActiveView - except: + except Exception: print(traceback.format_exc()) def selection_changed(sender, args): - # only handle selections made in the Project Browser if not args.GetDocument().ActiveView.ViewType == DB.ViewType.ProjectBrowser: return @@ -358,19 +931,23 @@ def selection_changed(sender, args): context.source_view = sel return context.source_view = None - except: + except Exception: print(traceback.format_exc()) + def doc_changed(sender, args): try: - affected_ids = list(args.GetModifiedElementIds()) - affected_ids.extend(list(args.GetDeletedElementIds())) - if any([view.Id in affected_ids for view - in [context.source_view, context.active_view]]): + affected_ids = list(args.GetModifiedElementIds()) + list( + args.GetDeletedElementIds() + ) + if any( + view.Id in affected_ids + for view in [context.source_view, context.active_view] + ): context.context_changed() except AttributeError: context.context_changed() - except: + except Exception: print(traceback.format_exc()) @@ -379,39 +956,33 @@ def compare_views(view1, view2): return True elif not view1 or not view2: return False - if view1.Document.GetHashCode() != view2.Document.GetHashCode(): - return False - else: - return view1.Id == view2.Id + return ( + view1.Document.GetHashCode() == view2.Document.GetHashCode() + and view1.Id == view2.Id + ) def can_use_view_as_source(view): - return ( - isinstance(view, DB.ViewPlan) or - isinstance(view, DB.ViewSection) - ) + return isinstance(view, (DB.ViewPlan, DB.ViewSection)) def corners_from_bb(bbox): transform = bbox.Transform - corners = [ bbox.Min, bbox.Min + DB.XYZ.BasisX * (bbox.Max - bbox.Min).X, - bbox.Min + DB.XYZ.BasisX * (bbox.Max - bbox.Min).X + bbox.Min + + DB.XYZ.BasisX * (bbox.Max - bbox.Min).X + DB.XYZ.BasisY * (bbox.Max - bbox.Min).Y, - bbox.Min + DB.XYZ.BasisY * (bbox.Max - bbox.Min).Y + bbox.Min + DB.XYZ.BasisY * (bbox.Max - bbox.Min).Y, ] return [transform.OfPoint(c) for c in corners] def create_edges(vertices, color): return [ - revit.dc3dserver.Edge( - vertices[i-1], - vertices[i], - color - ) for i in range(len(vertices)) + revit.dc3dserver.Edge(vertices[i - 1], vertices[i], color) + for i in range(len(vertices)) ] @@ -422,39 +993,33 @@ def create_triangles(vertices, color): vertices[1], vertices[2], revit.dc3dserver.Mesh.calculate_triangle_normal( - vertices[0], - vertices[1], - vertices[2], + vertices[0], vertices[1], vertices[2] ), - color + color, ), revit.dc3dserver.Triangle( vertices[2], vertices[3], vertices[0], revit.dc3dserver.Mesh.calculate_triangle_normal( - vertices[2], - vertices[3], - vertices[0], + vertices[2], vertices[3], vertices[0] ), - color - ) + color, + ), ] def get_color_from_plane(plane): - return DB.ColorWithTransparency( - PLANES[plane][0], - PLANES[plane][1], - PLANES[plane][2], - 180 - ) + rgb = PLANES[plane][0] + return DB.ColorWithTransparency(rgb[0], rgb[1], rgb[2], 180) +# Initialize server = revit.dc3dserver.Server(register=False) - unsubscribe_event = UI.ExternalEvent.Create(SimpleEventHandler(unsubscribe)) refresh_event = UI.ExternalEvent.Create(SimpleEventHandler(refresh_active_view)) +update_view_range_handler = UpdateViewRangeEventHandler() +update_view_range_event = UI.ExternalEvent.Create(update_view_range_handler) vm = MainViewModel() context = Context(vm) From 0b0234563a62cb04879808daa2184c55f818dd2e Mon Sep 17 00:00:00 2001 From: Jean-Marc Couffin Date: Wed, 29 Oct 2025 10:40:04 +0100 Subject: [PATCH 2/6] Update extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py Co-authored-by: devloai[bot] <168258904+devloai[bot]@users.noreply.github.com> --- .../Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py index af905b09a7..1bd8a6169f 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py @@ -371,7 +371,7 @@ def _set_current_level_selections(self, view_range): elif plane == DB.PlanViewPlane.CutPlane: # For Cut Plane, show the level name as read-only text if level_id and level_id != DB.ElementId.InvalidElementId: - level = self.active_view.Document.GetElement(level_id) + level = self.source_view.Document.GetElement(level_id) self.view_model.cutplane_level_name = ( level.Name if level else "Unknown" ) From 1dd1e9608cde3574b713205d8a84796aaf8009f6 Mon Sep 17 00:00:00 2001 From: Jean-Marc Couffin Date: Wed, 29 Oct 2025 10:41:54 +0100 Subject: [PATCH 3/6] Update extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py Co-authored-by: devloai[bot] <168258904+devloai[bot]@users.noreply.github.com> --- .../Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py index 1bd8a6169f..0cbcacb3a8 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/script.py @@ -315,7 +315,7 @@ def _populate_available_levels(self): """Populate the list of available levels in the project""" try: # Get all levels in the project - level_collector = DB.FilteredElementCollector(doc).OfClass(DB.Level) + level_collector = DB.FilteredElementCollector(self.source_view.Document).OfClass(DB.Level) levels = list(level_collector) # Sort levels by elevation From 73e5ab54948e77da69aeaa4185bf85769549ddb8 Mon Sep 17 00:00:00 2001 From: Jean-Marc Couffin Date: Mon, 3 Nov 2025 14:00:48 +0100 Subject: [PATCH 4/6] Merging changes from PR #2866 - Enhance View Range Editor UI with new layout and input fields for plane elevations and associated levels. Added functionality for applying changes and resetting values, along with warning messages for user feedback. --- .../ViewRange.pushbutton/MainWindow.xaml | 140 +++++++++++++----- 1 file changed, 106 insertions(+), 34 deletions(-) diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/MainWindow.xaml b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/MainWindow.xaml index 8d80cef29d..8fff57a93f 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/MainWindow.xaml +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Toggles.panel/toggles3.stack/ViewRange.pushbutton/MainWindow.xaml @@ -1,37 +1,109 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Title="View Range Editor" Height="380" Width="680" ResizeMode="CanResize"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +