11# -*- coding: utf-8 -*-
22from collections import deque
3- from pyrevit import HOST_APP , revit , forms , script , traceback
3+ from math import pi , atan , sqrt
4+ from pyrevit import HOST_APP , revit , forms , script
45from pyrevit import UI , DB
56from pyrevit .framework import System
67from Autodesk .Revit .Exceptions import InvalidOperationException
910
1011doc = HOST_APP .doc
1112uidoc = revit .uidoc
13+ # Length
1214length_format_options = doc .GetUnits ().GetFormatOptions (DB .SpecTypeId .Length )
1315length_unit = length_format_options .GetUnitTypeId ()
1416length_unit_label = DB .LabelUtils .GetLabelForUnit (length_unit )
1517length_unit_symbol = length_format_options .GetSymbolTypeId ()
1618length_unit_symbol_label = None
1719if not length_unit_symbol .Empty ():
1820 length_unit_symbol_label = DB .LabelUtils .GetLabelForSymbol (length_unit_symbol )
21+ # Slope
22+ slope_format_options = doc .GetUnits ().GetFormatOptions (DB .SpecTypeId .Slope )
23+ slope_unit = slope_format_options .GetUnitTypeId ()
24+ slope_unit_label = DB .LabelUtils .GetLabelForUnit (slope_unit )
25+ slope_unit_symbol = slope_format_options .GetSymbolTypeId ()
26+ slope_unit_symbol_label = None
27+ if not slope_unit_symbol .Empty ():
28+ slope_unit_symbol_label = DB .LabelUtils .GetLabelForSymbol (slope_unit_symbol )
1929
2030# Global variables
2131measure_window = None
3444
3545WINDOW_POSITION = "measure_window_pos"
3646
47+
3748def calculate_distances (point1 , point2 ):
38- """Calculate dx, dy, dz and diagonal distance between two points.
49+ """Calculate dx, dy, dz, diagonal distance, and slope angle between two points.
3950
4051 Args:
4152 point1 (DB.XYZ): First point
4253 point2 (DB.XYZ): Second point
4354
4455 Returns:
45- tuple: (dx, dy, dz, diagonal) all in internal units (feet)
56+ tuple: (dx, dy, dz, diagonal, slope ) all in internal units (feet, rad )
4657 """
4758 dx = abs (point2 .X - point1 .X )
4859 dy = abs (point2 .Y - point1 .Y )
4960 dz = abs (point2 .Z - point1 .Z )
5061 diagonal = point1 .DistanceTo (point2 )
5162
52- return dx , dy , dz , diagonal
63+ horizontal = sqrt (dx ** 2 + dy ** 2 )
64+
65+ if horizontal == 0 :
66+ slope = pi / 2.0 # 90 degrees (vertical)
67+ else :
68+ slope = atan (dz / horizontal )
69+
70+ return dx , dy , dz , diagonal , slope
5371
5472
55- def format_distance (value_in_feet ):
73+ def format_distance (value_internal ):
5674 return DB .UnitFormatUtils .Format (
5775 doc .GetUnits (),
5876 DB .SpecTypeId .Length ,
59- value_in_feet ,
77+ value_internal ,
78+ False ,
79+ )
80+
81+
82+ def format_slope (value_internal ):
83+ return DB .UnitFormatUtils .Format (
84+ doc .GetUnits (),
85+ DB .SpecTypeId .Slope ,
86+ value_internal ,
6087 False ,
6188 )
6289
@@ -148,7 +175,7 @@ def validate_3d_view():
148175 forms .alert (
149176 "Please activate a 3D view before using the 3D Measure tool." ,
150177 title = "3D View Required" ,
151- exitscript = True
178+ exitscript = True ,
152179 )
153180 return False
154181 return True
@@ -159,7 +186,7 @@ def perform_measurement():
159186 # Add 3D view validation
160187 if not validate_3d_view ():
161188 return
162-
189+
163190 try :
164191 with forms .WarningBar (title = "Pick first point" ):
165192 point1 = revit .pick_elementpoint (world = True )
@@ -180,26 +207,32 @@ def perform_measurement():
180207 dc3d_server .meshes = existing_meshes + new_meshes
181208 uidoc .RefreshActiveView ()
182209
183- dx , dy , dz , diagonal = calculate_distances (point1 , point2 )
210+ dx , dy , dz , diagonal , slope = calculate_distances (point1 , point2 )
184211
185212 measure_window .point1_text .Text = "Point 1: {}" .format (format_point (point1 ))
186213 measure_window .point2_text .Text = "Point 2: {}" .format (format_point (point2 ))
187- measure_window .dx_text .Text = "ΔX: {}" .format (format_distance (dx ))
188- measure_window .dy_text .Text = "ΔY: {}" .format (format_distance (dy ))
189- measure_window .dz_text .Text = "ΔZ: {}" .format (format_distance (dz ))
214+ measure_window .dx_text .Text = "ΔX: {:>15 }" .format (format_distance (dx ))
215+ measure_window .dy_text .Text = "ΔY: {:>15 }" .format (format_distance (dy ))
216+ measure_window .dz_text .Text = "ΔZ: {:>15 }" .format (format_distance (dz ))
190217 measure_window .diagonal_text .Text = "Diagonal: {}" .format (
191218 format_distance (diagonal )
192219 )
220+ measure_window .slope_text .Text = "Slope: {}" .format (format_slope (slope ))
193221
194222 # Add to history
195- history_entry = "Measurement {}:\n P1: {}\n P2: {}\n ΔX: {}\n ΔY: {}\n ΔZ: {}\n Diagonal: {}\n " .format (
196- len (measurement_history ) + 1 ,
197- format_point (point1 ),
198- format_point (point2 ),
199- format_distance (dx ),
200- format_distance (dy ),
201- format_distance (dz ),
202- format_distance (diagonal ),
223+ history_entry = (
224+ "Measurement {}:\n P1: {}\n P2: {}\n "
225+ "ΔX: {:>15}\n ΔY: {:>15}\n ΔZ: {:>15}\n "
226+ "Diagonal: {}\n Slope: {}" .format (
227+ len (measurement_history ) + 1 ,
228+ format_point (point1 ),
229+ format_point (point2 ),
230+ format_distance (dx ),
231+ format_distance (dy ),
232+ format_distance (dz ),
233+ format_distance (diagonal ),
234+ format_slope (slope ),
235+ )
203236 )
204237 measurement_history .append (history_entry )
205238
@@ -214,13 +247,15 @@ def perform_measurement():
214247 logger .error ("InvalidOperationException during measurement: {}" .format (ex ))
215248 forms .alert (
216249 "Measurement cancelled due to invalid operation. Please try again." ,
217- title = "Measurement Error"
250+ title = "Measurement Error" ,
218251 )
219252 except Exception as ex :
220- logger .error ("Error during measurement: {}\n {}" .format (ex , traceback .format_exc ()))
253+ logger .exception (
254+ "Error during measurement: {}" .format (ex )
255+ )
221256 forms .alert (
222257 "An unexpected error occurred during measurement. Check the log for details." ,
223- title = "Measurement Error"
258+ title = "Measurement Error" ,
224259 )
225260
226261
@@ -253,6 +288,7 @@ def __init__(self, xaml_file_name):
253288 self .dy_text .Text = "ΔY: -"
254289 self .dz_text .Text = "ΔZ: -"
255290 self .diagonal_text .Text = "Diagonal: -"
291+ self .slope_text .Text = "Slope: -"
256292 self .history_text .Text = "No measurements yet"
257293
258294 if not length_unit_symbol_label :
@@ -261,48 +297,53 @@ def __init__(self, xaml_file_name):
261297 "Length Units (adjust in Project Units): \n " + length_unit_label
262298 )
263299 self .Height = self .Height + 20
300+ if not slope_unit_symbol_label :
301+ self .show_element (self .project_unit_text )
302+ self .project_unit_text .Text = self .project_unit_text .Text + (
303+ "\n Slope Units (adjust in Project Units): \n " + slope_unit_label
304+ )
305+ self .Height = self .Height + 20
264306
265307 # Handle window close event
266308 self .Closed += self .window_closed
267309
268310 try :
269311 pos = script .load_data (WINDOW_POSITION , this_project = False )
270312 all_bounds = [s .WorkingArea for s in System .Windows .Forms .Screen .AllScreens ]
271- x , y = pos [' Left' ], pos [' Top' ]
313+ x , y = pos [" Left" ], pos [" Top" ]
272314 visible = any (
273- (b .Left <= x <= b .Right and b .Top <= y <= b .Bottom )
274- for b in all_bounds
315+ (b .Left <= x <= b .Right and b .Top <= y <= b .Bottom ) for b in all_bounds
275316 )
276317 if not visible :
277318 raise Exception
278319 self .WindowStartupLocation = System .Windows .WindowStartupLocation .Manual
279- self .Left = pos .get (' Left' , 200 )
280- self .Top = pos .get (' Top' , 150 )
320+ self .Left = pos .get (" Left" , 200 )
321+ self .Top = pos .get (" Top" , 150 )
281322 except Exception :
282- self .WindowStartupLocation = System .Windows .WindowStartupLocation .CenterScreen
323+ self .WindowStartupLocation = (
324+ System .Windows .WindowStartupLocation .CenterScreen
325+ )
283326
284327 self .Show ()
285-
328+
286329 # Automatically start the first measurement
287330 measure_handler_event .Raise ()
288331
289332 def window_closed (self , sender , args ):
290333 """Handle window close event - copy history to clipboard, cleanup DC3D server and visual aids."""
291334 global dc3d_server
292- new_pos = {' Left' : self .Left , ' Top' : self .Top }
335+ new_pos = {" Left" : self .Left , " Top" : self .Top }
293336 script .store_data (WINDOW_POSITION , new_pos , this_project = False )
294-
337+
295338 # Copy measurement history to clipboard before cleanup
296339 try :
297340 if measurement_history :
298341 history_text = "\n " .join (measurement_history )
299342 script .clipboard_copy (history_text )
300- logger .info ("Measurement history copied to clipboard" )
301- else :
302- logger .info ("No measurements to copy to clipboard" )
343+ forms .toast ("Measurements copied to Clipboard!" , title = "Measure 3D" )
303344 except Exception as ex :
304345 logger .error ("Error copying to clipboard: {}" .format (ex ))
305-
346+
306347 try :
307348 # Delete all visual aids
308349 if dc3d_server :
0 commit comments