├── img ├── apartment_layout.png ├── desk_and_chair_plan.png ├── desk_and_chair_loops.png ├── geosnoop_solids_versus_booleans.png ├── room_separator_using_2d_booleans.png ├── wall_width_loop_using_2d_booleans.png └── element_outline_four_selected_extrusion_analyser_svg_path.png ├── ElementOutline ├── JtWindowHandle.cs ├── ElementEqualityComparer.cs ├── App.cs ├── Cmd2dBoolean.cs ├── JtPlacement2dInt.cs ├── ElementOutline.addin ├── CmdRoomOuterOutline.cs ├── JtLoops.cs ├── Point2dInt.cs ├── JtLoop.cs ├── CmdExtrusionAnalyzer.cs ├── Properties │ └── AssemblyInfo.cs ├── EdgeLoopRetriever.cs ├── JtBoundingBox2dInt.cs ├── ElementOutline.csproj ├── JtLineCollection.cs ├── ContiguousCurveSorter.cs ├── GeoSnoop.cs ├── ClipperRvt.cs ├── Util.cs └── CmdUploadRooms.cs ├── LICENSE ├── clipper_library ├── Properties │ └── AssemblyInfo.cs └── clipper_library.csproj ├── ElementOutline.sln ├── .gitignore └── README.md /img/apartment_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/apartment_layout.png -------------------------------------------------------------------------------- /img/desk_and_chair_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/desk_and_chair_plan.png -------------------------------------------------------------------------------- /img/desk_and_chair_loops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/desk_and_chair_loops.png -------------------------------------------------------------------------------- /img/geosnoop_solids_versus_booleans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/geosnoop_solids_versus_booleans.png -------------------------------------------------------------------------------- /img/room_separator_using_2d_booleans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/room_separator_using_2d_booleans.png -------------------------------------------------------------------------------- /img/wall_width_loop_using_2d_booleans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/wall_width_loop_using_2d_booleans.png -------------------------------------------------------------------------------- /img/element_outline_four_selected_extrusion_analyser_svg_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/ElementOutline/HEAD/img/element_outline_four_selected_extrusion_analyser_svg_path.png -------------------------------------------------------------------------------- /ElementOutline/JtWindowHandle.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Diagnostics; 4 | using System.Windows.Forms; 5 | #endregion 6 | 7 | namespace ElementOutline 8 | { 9 | /// 10 | /// Wrapper class for converting 11 | /// IntPtr to IWin32Window. 12 | /// 13 | public class JtWindowHandle : IWin32Window 14 | { 15 | IntPtr _hwnd; 16 | 17 | public JtWindowHandle( IntPtr h ) 18 | { 19 | Debug.Assert( IntPtr.Zero != h, 20 | "expected non-null window handle" ); 21 | 22 | _hwnd = h; 23 | } 24 | 25 | public IntPtr Handle 26 | { 27 | get 28 | { 29 | return _hwnd; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ElementOutline/ElementEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using Autodesk.Revit.DB; 7 | #endregion 8 | 9 | namespace ElementOutline 10 | { 11 | /// 12 | /// Elements with the same element id equate to 13 | /// the same element. Without this, many, many, 14 | /// many duplicates. 15 | /// 16 | class ElementEqualityComparer 17 | : IEqualityComparer 18 | { 19 | public bool Equals( Element x, Element y ) 20 | { 21 | return x.Id.IntegerValue.Equals( 22 | y.Id.IntegerValue ); 23 | } 24 | 25 | public int GetHashCode( Element obj ) 26 | { 27 | return obj.Id.IntegerValue.GetHashCode(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ElementOutline/App.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using Autodesk.Revit.ApplicationServices; 7 | using Autodesk.Revit.Attributes; 8 | using Autodesk.Revit.DB; 9 | using Autodesk.Revit.UI; 10 | #endregion 11 | 12 | namespace ElementOutline 13 | { 14 | class App : IExternalApplication 15 | { 16 | /// 17 | /// Caption 18 | /// 19 | public const string Caption = "ElementOutline"; 20 | 21 | public Result OnStartup( UIControlledApplication a ) 22 | { 23 | //string path = "Z:\\j\\tmp"; // C:/tmp"; 24 | //Debug.Assert( File.Exists( path ), "expected access to tmp folder" ); 25 | return Result.Succeeded; 26 | } 27 | 28 | public Result OnShutdown( UIControlledApplication a ) 29 | { 30 | return Result.Succeeded; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jeremy Tammik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /clipper_library/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("clipper_library")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Angus Johnson")] 12 | [assembly: AssemblyProduct("clipper_library")] 13 | [assembly: AssemblyCopyright("Copyright © Angus Johnson 2010-14")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("51a6bdca-bc4e-4b2c-ae69-36e2497204f2")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ElementOutline/Cmd2dBoolean.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System.Collections.Generic; 3 | using Autodesk.Revit.ApplicationServices; 4 | using Autodesk.Revit.Attributes; 5 | using Autodesk.Revit.DB; 6 | using Autodesk.Revit.UI; 7 | #endregion 8 | 9 | namespace ElementOutline 10 | { 11 | [Transaction( TransactionMode.ReadOnly )] 12 | class Cmd2dBoolean : IExternalCommand 13 | { 14 | public Result Execute( 15 | ExternalCommandData commandData, 16 | ref string message, 17 | ElementSet elements ) 18 | { 19 | UIApplication uiapp = commandData.Application; 20 | UIDocument uidoc = uiapp.ActiveUIDocument; 21 | Application app = uiapp.Application; 22 | Document doc = uidoc.Document; 23 | 24 | if( null == doc ) 25 | { 26 | Util.ErrorMsg( "Please run this command in a valid" 27 | + " Revit project document." ); 28 | return Result.Failed; 29 | } 30 | 31 | ICollection ids 32 | = Util.GetSelectedElements( uidoc ); 33 | 34 | if( (null == ids) || (0 == ids.Count) ) 35 | { 36 | return Result.Cancelled; 37 | } 38 | 39 | // Third attempt: create the element 2D outline 40 | // from element solid faces and meshes in current 41 | // view by projecting them onto the XY plane and 42 | // executing 2d Boolean unions on them. 43 | 44 | View view = doc.ActiveView; 45 | 46 | Dictionary booleanLoops 47 | = ClipperRvt.GetElementLoops( view, ids ); 48 | 49 | JtWindowHandle hwnd = new JtWindowHandle( 50 | uiapp.MainWindowHandle ); 51 | 52 | Util.CreateOutput( "element_2d_boolean_outline", 53 | "2D Booleans", doc, hwnd, booleanLoops ); 54 | 55 | return Result.Succeeded; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ElementOutline.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.757 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElementOutline", "ElementOutline\ElementOutline.csproj", "{DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66} = {9B062971-A88E-4A3D-B3C9-12B78D15FA66} 9 | EndProjectSection 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "clipper_library", "clipper_library\clipper_library.csproj", "{9B062971-A88E-4A3D-B3C9-12B78D15FA66}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(SolutionProperties) = preSolution 29 | HideSolutionNode = FALSE 30 | EndGlobalSection 31 | GlobalSection(ExtensibilityGlobals) = postSolution 32 | SolutionGuid = {A6863220-470D-443F-97FD-1E656530D231} 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /ElementOutline/JtPlacement2dInt.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using Autodesk.Revit.DB; 4 | using System.Diagnostics; 5 | #endregion 6 | 7 | namespace ElementOutline 8 | { 9 | /// 10 | /// A 2D integer-based transformation, 11 | /// i.e. translation and rotation. 12 | /// 13 | class JtPlacement2dInt 14 | { 15 | /// 16 | /// Translation. 17 | /// 18 | public Point2dInt Translation { get; set; } 19 | 20 | /// 21 | /// Rotation in degrees. 22 | /// 23 | public int Rotation { get; set; } 24 | 25 | /// 26 | /// The family symbol UniqueId. 27 | /// 28 | public string SymbolId { get; set; } 29 | 30 | public JtPlacement2dInt( FamilyInstance fi ) 31 | { 32 | LocationPoint lp = fi.Location as LocationPoint; 33 | 34 | Debug.Assert( null != lp, 35 | "expected valid family instanace location point" ); 36 | 37 | Translation = new Point2dInt( lp.Point ); 38 | 39 | Rotation = Util.ConvertRadiansToDegrees( lp.Rotation ); 40 | 41 | SymbolId = fi.Symbol.UniqueId; 42 | } 43 | 44 | /// 45 | /// Create a dummy placement for a non-instance 46 | /// part, i.e. a nomral BIM element with a given 47 | /// unique id, just for GeoSnoop graphical 48 | /// debugging purposes. 49 | /// 50 | public JtPlacement2dInt( string uidPart ) 51 | { 52 | Translation = new Point2dInt( 0, 0 ); 53 | Rotation = 0; 54 | SymbolId = uidPart; 55 | } 56 | 57 | /// 58 | /// Return an SVG transform, 59 | /// either for native SVG or Raphael. 60 | /// 61 | public string SvgTransform 62 | { 63 | get 64 | { 65 | return string.Format( 66 | "R{2}T{0},{1}", 67 | //"translate({0},{1}) rotate({2})", 68 | Translation.X, 69 | Util.SvgFlipY( Translation.Y ), 70 | Util.SvgFlipY( Rotation ) ); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ElementOutline/ElementOutline.addin: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Element Outline Solid 5 | Determine element outline from geometry solids and ExtrusionAnalyzer 6 | ElementOutline.dll 7 | ElementOutline.CmdExtrusionAnalyzer 8 | cc311594-d26d-4f64-9dc7-71c43d27fbf6 9 | com.typepad.thebuildingcoder 10 | The Building Coder, http://thebuildingcoder.typepad.com 11 | 12 | 13 | Element Outline 2D Boolean 14 | Determine element outline from 2D Booleans of projected solid faces and meshes 15 | ElementOutline.dll 16 | ElementOutline.Cmd2dBoolean 17 | 2d2bca85-05f1-49ce-a309-ddd2fa14c795 18 | com.typepad.thebuildingcoder 19 | The Building Coder, http://thebuildingcoder.typepad.com 20 | 21 | 22 | Room Outer Outline 23 | Determine room outer outline including bounding elements from 2D Boolean union of projected solid faces and meshes 24 | ElementOutline.dll 25 | ElementOutline.CmdRoomOuterOutline 26 | 6cafb249-b7b9-496d-a825-d7b8906c76e1 27 | com.typepad.thebuildingcoder 28 | The Building Coder, http://thebuildingcoder.typepad.com 29 | 30 | 40 | 41 | -------------------------------------------------------------------------------- /ElementOutline/CmdRoomOuterOutline.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Autodesk.Revit.ApplicationServices; 5 | using Autodesk.Revit.Attributes; 6 | using Autodesk.Revit.DB; 7 | using Autodesk.Revit.DB.Architecture; 8 | using Autodesk.Revit.UI; 9 | #endregion 10 | 11 | namespace ElementOutline 12 | { 13 | [Transaction( TransactionMode.ReadOnly )] 14 | class CmdRoomOuterOutline : IExternalCommand 15 | { 16 | public Result Execute( 17 | ExternalCommandData commandData, 18 | ref string message, 19 | ElementSet elements ) 20 | { 21 | UIApplication uiapp = commandData.Application; 22 | UIDocument uidoc = uiapp.ActiveUIDocument; 23 | Application app = uiapp.Application; 24 | Document doc = uidoc.Document; 25 | 26 | if( null == doc ) 27 | { 28 | Util.ErrorMsg( "Please run this command in a valid" 29 | + " Revit project document." ); 30 | return Result.Failed; 31 | } 32 | 33 | IEnumerable ids 34 | = Util.GetSelectedRooms( uidoc ); 35 | 36 | if( (null == ids) || (0 == ids.Count()) ) 37 | { 38 | return Result.Cancelled; 39 | } 40 | 41 | View view = doc.ActiveView; 42 | 43 | SpatialElementBoundaryOptions seb_opt 44 | = new SpatialElementBoundaryOptions(); 45 | 46 | Dictionary booleanLoops 47 | = new Dictionary( 48 | ids.Count() ); 49 | 50 | foreach( ElementId id in ids ) 51 | { 52 | Room room = doc.GetElement( id ) as Room; 53 | 54 | JtLoops loops 55 | = ClipperRvt.GetRoomOuterBoundaryLoops( 56 | room, seb_opt, view ); 57 | 58 | if( null == loops ) // the room may not be bounded 59 | { 60 | continue; 61 | } 62 | booleanLoops.Add( id.IntegerValue, loops ); 63 | } 64 | 65 | JtWindowHandle hwnd = new JtWindowHandle( 66 | uiapp.MainWindowHandle ); 67 | 68 | Util.CreateOutput( "room_outer_outline", 69 | "Room Outer Outline", doc, hwnd, booleanLoops ); 70 | 71 | return Result.Succeeded; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /clipper_library/clipper_library.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {9B062971-A88E-4A3D-B3C9-12B78D15FA66} 9 | Library 10 | Properties 11 | ClipperLib 12 | clipper_library 13 | v4.0 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | -------------------------------------------------------------------------------- /ElementOutline/JtLoops.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Linq; 6 | using System.Text; 7 | #endregion 8 | 9 | namespace ElementOutline 10 | { 11 | /// 12 | /// A list of boundary loops. 13 | /// 14 | class JtLoops : List 15 | { 16 | public JtLoops( int capacity ) 17 | : base( capacity ) 18 | { 19 | } 20 | 21 | /// 22 | /// Unite two collections of boundary 23 | /// loops into one single one. 24 | /// 25 | public static JtLoops operator +( JtLoops a, JtLoops b ) 26 | { 27 | int na = a.Count; 28 | int nb = b.Count; 29 | JtLoops sum = new JtLoops( na + nb ); 30 | sum.AddRange( a ); 31 | sum.AddRange( b ); 32 | return sum; 33 | } 34 | 35 | /// 36 | /// Normalize the loops by ensuring that 37 | /// their minimal vertex comes first 38 | /// 39 | public void NormalizeLoops() 40 | { 41 | foreach( JtLoop loop in this ) 42 | { 43 | loop.Normalize(); 44 | } 45 | } 46 | 47 | /// 48 | /// Return a bounding box 49 | /// containing these loops. 50 | /// 51 | public JtBoundingBox2dInt BoundingBox 52 | { 53 | get 54 | { 55 | JtBoundingBox2dInt bb = new JtBoundingBox2dInt(); 56 | 57 | foreach( JtLoop loop in this ) 58 | { 59 | foreach( Point2dInt p in loop ) 60 | { 61 | bb.ExpandToContain( p ); 62 | } 63 | } 64 | return bb; 65 | } 66 | } 67 | 68 | /// 69 | /// Return suitable input for the .NET 70 | /// GraphicsPath.AddLines method to display the 71 | /// loops in a form. Note that a closing segment 72 | /// to connect the last point back to the first 73 | /// is added. 74 | /// 75 | public List GetGraphicsPathLines() 76 | { 77 | List loops 78 | = new List( Count ); 79 | 80 | foreach( JtLoop jloop in this ) 81 | { 82 | loops.Add( jloop.GetGraphicsPathLines() ); 83 | } 84 | return loops; 85 | } 86 | 87 | /// 88 | /// Return the concatenated SVG path 89 | /// specifications for all the loops. 90 | /// 91 | public string SvgPath 92 | { 93 | get 94 | { 95 | return string.Join( " ", 96 | this.Select( 97 | a => a.SvgPath ) ); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ElementOutline/Point2dInt.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using Autodesk.Revit.DB; 4 | #endregion 5 | 6 | namespace ElementOutline 7 | { 8 | /// 9 | /// An integer-based 2D point class. 10 | /// 11 | class Point2dInt : IComparable 12 | { 13 | public int X { get; set; } 14 | public int Y { get; set; } 15 | 16 | /// 17 | /// Initialise a 2D millimetre integer 18 | /// point to the given values. 19 | /// 20 | public Point2dInt( int x, int y ) 21 | { 22 | X = x; 23 | Y = y; 24 | } 25 | 26 | /// 27 | /// Convert a 2D Revit UV to a 2D millimetre 28 | /// integer point by scaling from feet to mm. 29 | /// 30 | public Point2dInt( UV p ) 31 | { 32 | X = Util.ConvertFeetToMillimetres( p.U ); 33 | Y = Util.ConvertFeetToMillimetres( p.V ); 34 | } 35 | 36 | /// 37 | /// Convert a 3D Revit XYZ to a 2D millimetre 38 | /// integer point by discarding the Z coordinate 39 | /// and scaling from feet to mm. 40 | /// 41 | public Point2dInt( XYZ p ) 42 | { 43 | X = Util.ConvertFeetToMillimetres( p.X ); 44 | Y = Util.ConvertFeetToMillimetres( p.Y ); 45 | } 46 | 47 | /// 48 | /// Convert Revit coordinates XYZ to a 2D 49 | /// millimetre integer point by scaling 50 | /// from feet to mm. 51 | /// 52 | public Point2dInt( double x, double y ) 53 | { 54 | X = Util.ConvertFeetToMillimetres( x ); 55 | Y = Util.ConvertFeetToMillimetres( y ); 56 | } 57 | 58 | /// 59 | /// Comparison with another point, important 60 | /// for dictionary lookup support. 61 | /// 62 | public int CompareTo( Point2dInt a ) 63 | { 64 | int d = X - a.X; 65 | 66 | if( 0 == d ) 67 | { 68 | d = Y - a.Y; 69 | } 70 | return d; 71 | } 72 | 73 | /// 74 | /// Distance from this point to another point. 75 | /// 76 | public double DistanceTo( Point2dInt a ) 77 | { 78 | int dy = a.Y - Y; 79 | var dx = a.X - X; 80 | return Math.Sqrt( dx * dx + dy * dy ); 81 | } 82 | 83 | /// 84 | /// Angle of line from this point to another point 85 | /// in the range (-pi,pi], just like Math.Atan2. 86 | /// 87 | public double AngleTo( Point2dInt a ) 88 | { 89 | int dy = a.Y - Y; 90 | int dx = a.X - X; 91 | return Math.Atan2( dy, dx ); 92 | } 93 | 94 | /// 95 | /// Display as a string. 96 | /// 97 | public override string ToString() 98 | { 99 | return string.Format( "({0},{1})", X, Y ); 100 | } 101 | 102 | /// 103 | /// Return a string suitable for use in an SVG 104 | /// path. For index i == 0, prefix with 'M', for 105 | /// i == 1 with 'L', and otherwise with nothing. 106 | /// 107 | public string SvgPath( int i ) 108 | { 109 | return string.Format( "{0}{1} {2}", 110 | ( 0 == i ? "M" : ( 1 == i ? "L" : "" ) ), 111 | X, Util.SvgFlipY( Y ) ); 112 | } 113 | 114 | /// 115 | /// Add two points, i.e. treat one of 116 | /// them as a translation vector. 117 | /// 118 | public static Point2dInt operator +( 119 | Point2dInt a, 120 | Point2dInt b ) 121 | { 122 | return new Point2dInt( 123 | a.X + b.X, a.Y + b.Y ); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ElementOutline/JtLoop.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | #endregion 6 | 7 | namespace ElementOutline 8 | { 9 | /// 10 | /// A closed or open polygon boundary loop. 11 | /// 12 | class JtLoop : List 13 | { 14 | public bool Closed { get; set; } 15 | 16 | /// 17 | /// Instantiate with a pre-initialised capacity. 18 | /// 19 | public JtLoop( int capacity ) 20 | : base( capacity ) 21 | { 22 | Closed = true; 23 | } 24 | 25 | /// 26 | /// Instantiate from an array of points. 27 | /// 28 | public JtLoop( Point2dInt[] pts ) 29 | : base( pts.Length ) 30 | { 31 | Closed = true; 32 | 33 | Add( pts ); 34 | } 35 | 36 | /// 37 | /// Add another point to the collection. 38 | /// If the new point is identical to the last, 39 | /// ignore it. This will automatically suppress 40 | /// really small boundary segment fragments. 41 | /// 42 | public new void Add( Point2dInt p ) 43 | { 44 | if( 0 == Count 45 | || 0 != p.CompareTo( this[Count - 1] ) ) 46 | { 47 | base.Add( p ); 48 | } 49 | } 50 | 51 | /// 52 | /// Add a point array to the collection. 53 | /// If the new point is identical to the last, 54 | /// ignore it. This will automatically suppress 55 | /// really small boundary segment fragments. 56 | /// 57 | public void Add( Point2dInt[] pts ) 58 | { 59 | foreach( Point2dInt p in pts ) 60 | { 61 | Add( p ); 62 | } 63 | } 64 | 65 | /// 66 | /// Normalize the loop by ensuring that 67 | /// the minimal vertex comes first 68 | /// 69 | public void Normalize() 70 | { 71 | Point2dInt pmin = this.Min(); 72 | int i = IndexOf( pmin ); 73 | int n = Count; 74 | Point2dInt[] a = new Point2dInt[ n ]; 75 | CopyTo( i, a, 0, n - i ); 76 | CopyTo( 0, a, n - i, i ); 77 | Clear(); 78 | AddRange( a ); 79 | } 80 | 81 | /// 82 | /// Return a bounding box 83 | /// containing this loop. 84 | /// 85 | public JtBoundingBox2dInt BoundingBox 86 | { 87 | get 88 | { 89 | JtBoundingBox2dInt bb = new JtBoundingBox2dInt(); 90 | 91 | foreach( Point2dInt p in this ) 92 | { 93 | bb.ExpandToContain( p ); 94 | } 95 | return bb; 96 | } 97 | } 98 | 99 | /// 100 | /// Display as a string. 101 | /// 102 | public override string ToString() 103 | { 104 | return string.Join( ", ", this ); 105 | } 106 | 107 | /// 108 | /// Return suitable input for the .NET 109 | /// GraphicsPath.AddLines method to display this 110 | /// loop in a form. Note that a closing segment 111 | /// to connect the last point back to the first 112 | /// is added. 113 | /// 114 | public Point[] GetGraphicsPathLines() 115 | { 116 | int i, n; 117 | 118 | n = Count; 119 | 120 | if( Closed ) { ++n; } 121 | 122 | Point[] loop = new Point[n]; 123 | 124 | i = 0; 125 | foreach( Point2dInt p in this ) 126 | { 127 | loop[i++] = new Point( p.X, p.Y ); 128 | } 129 | 130 | if( Closed ) { loop[i] = loop[0]; } 131 | 132 | return loop; 133 | } 134 | 135 | /// 136 | /// Return an SVG path specification, c.f. 137 | /// http://www.w3.org/TR/SVG/paths.html 138 | /// M [0] L [1] [2] ... [n-1] Z 139 | /// 140 | public string SvgPath 141 | { 142 | get 143 | { 144 | return 145 | string.Join( " ", 146 | this.Select( 147 | ( p, i ) => p.SvgPath( i ) ) ) 148 | + ( Closed ? "Z" : "" ); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ElementOutline/CmdExtrusionAnalyzer.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using Autodesk.Revit.ApplicationServices; 6 | using Autodesk.Revit.Attributes; 7 | using Autodesk.Revit.DB; 8 | using Autodesk.Revit.UI; 9 | using Autodesk.Revit.UI.Selection; 10 | using System.Linq; 11 | using System.IO; 12 | #endregion 13 | 14 | namespace ElementOutline 15 | { 16 | [Transaction( TransactionMode.ReadOnly )] 17 | public class CmdExtrusionAnalyzer : IExternalCommand 18 | { 19 | /// 20 | /// Retrieve plan view boundary loops from element 21 | /// solids using ExtrusionAnalyzer. 22 | /// 23 | static Dictionary GetSolidLoops( 24 | Document doc, 25 | ICollection ids ) 26 | { 27 | Dictionary solidLoops 28 | = new Dictionary(); 29 | 30 | int nFailures; 31 | 32 | foreach( ElementId id in ids ) 33 | { 34 | Element e = doc.GetElement( id ); 35 | 36 | if( e is Dimension ) 37 | { 38 | continue; 39 | } 40 | 41 | Debug.Print( e.Name + " " 42 | + id.IntegerValue.ToString() ); 43 | 44 | nFailures = 0; 45 | 46 | JtLoops loops 47 | = CmdUploadRooms.GetSolidPlanViewBoundaryLoops( 48 | e, false, ref nFailures ); 49 | 50 | if( 0 < nFailures ) 51 | { 52 | Debug.Print( "{0}: {1}", 53 | Util.ElementDescription( e ), 54 | Util.PluralString( nFailures, 55 | "extrusion analyser failure" ) ); 56 | } 57 | CmdUploadRooms.ListLoops( e, loops ); 58 | 59 | loops.NormalizeLoops(); 60 | 61 | solidLoops.Add( id.IntegerValue, loops ); 62 | } 63 | return solidLoops; 64 | } 65 | 66 | public Result Execute( 67 | ExternalCommandData commandData, 68 | ref string message, 69 | ElementSet elements ) 70 | { 71 | UIApplication uiapp = commandData.Application; 72 | UIDocument uidoc = uiapp.ActiveUIDocument; 73 | Application app = uiapp.Application; 74 | Document doc = uidoc.Document; 75 | 76 | JtWindowHandle hwnd = new JtWindowHandle( 77 | uiapp.MainWindowHandle ); 78 | 79 | if( null == doc ) 80 | { 81 | Util.ErrorMsg( "Please run this command in a valid" 82 | + " Revit project document." ); 83 | return Result.Failed; 84 | } 85 | 86 | // Ensure that output folder exists -- always fails 87 | 88 | //if( !File.Exists( _output_folder_path ) ) 89 | //{ 90 | // Util.ErrorMsg( string.Format( 91 | // "Please ensure that output folder '{0}' exists", 92 | // _output_folder_path ) ); 93 | // return Result.Failed; 94 | //} 95 | 96 | ICollection ids 97 | = Util.GetSelectedElements( uidoc ); 98 | 99 | if( (null == ids) || (0 == ids.Count) ) 100 | { 101 | return Result.Cancelled; 102 | } 103 | 104 | // First attempt: create element 2D outline from 105 | // element geometry solids using the ExtrusionAnalyzer; 106 | // unfortunately, some elements have no valid solid, 107 | // so this approach is not general enough. 108 | 109 | // Map element id to its solid outline loops 110 | 111 | Dictionary solidLoops = GetSolidLoops( 112 | doc, ids ); 113 | 114 | string filepath = Path.Combine( Util.OutputFolderPath, 115 | doc.Title + "_element_solid_outline.json" ); 116 | 117 | string caption = doc.Title + " solid extrusions"; 118 | 119 | Util.ExportLoops( filepath, hwnd, caption, doc, solidLoops ); 120 | 121 | // Second attempt: create element 2D outline from 122 | // element geometry edges in current view by 123 | // projecting them onto the XY plane and then 124 | // following the outer contour 125 | // counter-clockwise keeping to the right-most 126 | // edge until a closed loop is achieved. 127 | 128 | bool second_attempt = false; 129 | 130 | if( second_attempt ) 131 | { 132 | View view = doc.ActiveView; 133 | 134 | Options opt = new Options 135 | { 136 | View = view 137 | }; 138 | 139 | EdgeLoopRetriever edgeLooper 140 | = new EdgeLoopRetriever( opt, ids ); 141 | 142 | filepath = Path.Combine( Util.OutputFolderPath, 143 | doc.Title + "_element_edge_outline.json" ); 144 | 145 | caption = doc.Title + " curve outlines"; 146 | 147 | Util.ExportLoops( filepath, hwnd, caption, 148 | doc, edgeLooper.Loops ); 149 | } 150 | return Result.Succeeded; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ElementOutline/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle( "ElementOutline" )] 8 | [assembly: AssemblyDescription( "Revit C# .NET add-in to export 2D Element outlines" )] 9 | [assembly: AssemblyConfiguration( "" )] 10 | [assembly: AssemblyCompany( "Autodesk Inc." )] 11 | [assembly: AssemblyProduct( "ElementOutline Revit C# .NET Add-In" )] 12 | [assembly: AssemblyCopyright( "Copyright 2019 (C) Jeremy Tammik, Autodesk Inc." )] 13 | [assembly: AssemblyTrademark( "" )] 14 | [assembly: AssemblyCulture( "" )] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible( false )] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid( "321044f7-b0b2-4b1c-af18-e71a19252be0" )] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | // 35 | // History: 36 | // 37 | // 2019-07-23 2020.0.0.0 unchanged code extracted from RoomEditorApp 38 | // 2019-07-23 2020.0.0.1 implemented access to instance geometry, first successful run 39 | // 2019-07-23 2020.0.0.2 successful SvgPath text output 40 | // 2019-08-05 2020.0.0.3 renamed GetSolidPlanViewBoundaryLoops and solidLoops variable 41 | // 2019-08-05 2020.0.0.3 implemented GetSolidLoops and ExportLoops 42 | // 2019-08-05 2020.0.0.3 implemented GetEdgeLoops framework 43 | // 2019-08-05 2020.0.0.3 implemented EdgeLoopRetriever framework 44 | // 2019-08-07 2020.0.0.4 started working on EdgeLoopRetriever.GetLoops 45 | // 2019-08-15 2020.0.0.4 implemented JtLineCollection constructor from list of curves 46 | // 2019-08-15 2020.0.0.4 implemented JtLineCollection.GetOutline 47 | // 2019-08-16 2020.0.0.5 added use of Tessellate in JtLineCollection constructor 48 | // 2019-08-16 2020.0.0.5 debugging GetOutlineRecusive 49 | // 2019-08-17 2020.0.0.6 started implementing support for multiple loops 50 | // 2019-08-18 2020.0.0.6 started implementing code using clipper for 2d union approach 51 | // 2019-08-19 2020.0.0.6 implementing code using clipper for 2d union approach 52 | // 2019-08-20 2020.0.0.6 implementing code using clipper for 2d union approach 53 | // 2019-08-22 2020.0.0.7 completed first draft of 2d boolean union approach 54 | // 2019-08-22 2020.0.0.8 added curve support to 2d boolean union approach 55 | // 2019-08-23 2020.0.0.8 removed curve support to 2d boolean union approach 56 | // 2019-08-24 2020.0.0.9 removed call to ExporterIFCUtils.SortCurveLoops on solid face loops 57 | // 2019-08-25 2020.0.0.10 cast clipper PointInt coords from Int64 to int before constructing Point2dInt -- 2d Boolean loops work n ow 58 | // 2019-08-25 2020.0.0.10 implemented JtLoop.Normalize -- loops are ok but family instance is offset 59 | // 2019-08-25 2020.0.0.10 get instance geometry with identity transform -- solid and 2d boolean loops are identical 60 | // 2019-09-03 2020.0.0.11 added support for full circle, i.e., closed Arc -- successful test on intercom element 61 | // 2019-09-03 2020.0.0.12 deleted unused JtLine class 62 | // 2019-09-03 2020.0.0.12 implemented GeoSnoop.DisplayLoops 63 | // 2019-09-04 2020.0.0.13 reduce GeoSnoop.DisplayLoops target rectangle so edge lines remain visible 64 | // 2019-09-04 2020.0.0.13 added caption 65 | // 2019-09-04 2020.0.0.13 adjust bitmap edge size, not target image edge size 66 | // 2019-09-04 2020.0.0.13 increased form image and edge sizes 67 | // 2019-12-19 2020.0.1.0 implemented CmdRoomOuterOutline 68 | // 2019-12-19 2020.0.1.0 refactored Cmd2dBoolean and implemented GetElementLoops 69 | // 2019-12-19 2020.0.1.0 implemented GetSelectedRooms 70 | // 2019-12-19 2020.0.1.0 started fleshing out CmdRoomOuterOutline 71 | // 2019-12-19 2020.0.1.1 implemented AddToUnionRoom 72 | // 2019-12-30 2020.0.1.1 worked on GetRoomOuterBoundaryLoop 73 | // 2019-12-30 2020.0.1.1 implemented CreateOutput 74 | // 2020-01-01 2020.0.1.2 sucessfully tested GetRoomOuterBoundaryLoop 75 | // 2020-01-01 2020.0.1.2 skip room separator in GetRoomOuterBoundaryLoop 76 | // 2020-01-01 2020.0.1.2 successfully tested on both wall width and room separator sample models 77 | // 2020-01-05 2020.0.1.3 refactored and implemented ClipperRvt utility class 78 | // 2020-01-05 2020.0.1.3 renamed CmdExtrusionAnalyzer 79 | // 80 | [assembly: AssemblyVersion( "2020.0.1.3" )] 81 | [assembly: AssemblyFileVersion( "2020.0.1.3" )] 82 | -------------------------------------------------------------------------------- /ElementOutline/EdgeLoopRetriever.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Autodesk.Revit.DB; 5 | #endregion 6 | 7 | namespace ElementOutline 8 | { 9 | /// 10 | /// Retrieve plan view boundary loops from element 11 | /// geometry edges; create element 2D outline from 12 | /// element geometry edges by projecting them onto 13 | /// the XY plane and then following the outer contour 14 | /// counter-clockwise keeping to the right-most 15 | /// edge until a closed loop is achieved. 16 | /// 17 | class EdgeLoopRetriever 18 | { 19 | Dictionary _loops 20 | = new Dictionary(); 21 | 22 | /// 23 | /// Recursively retrieve all curves and solids 24 | /// from the given geometry 25 | /// 26 | static void AddCurvesAndSolids( 27 | GeometryElement geoElem, 28 | List curves, 29 | List solids ) 30 | { 31 | foreach( GeometryObject obj in geoElem ) 32 | { 33 | Curve curve = obj as Curve; 34 | if( null != curve ) 35 | { 36 | curves.Add( curve ); 37 | continue; 38 | } 39 | Solid solid = obj as Solid; 40 | if( null != solid ) 41 | { 42 | solids.Add( solid ); 43 | continue; 44 | } 45 | GeometryInstance inst = obj as GeometryInstance; 46 | if( null != inst ) 47 | { 48 | GeometryElement txGeoElem 49 | = inst.GetInstanceGeometry( 50 | inst.Transform ); 51 | 52 | AddCurvesAndSolids( txGeoElem, 53 | curves, solids ); 54 | continue; 55 | } 56 | Debug.Assert( false, 57 | "expected curve, solid or instance" ); 58 | } 59 | } 60 | 61 | /// 62 | /// Recursively all curves from the given solids 63 | /// 64 | static void AddCurvesFromSolids( 65 | List curves, 66 | List solids ) 67 | { 68 | foreach( Solid solid in solids ) 69 | { 70 | foreach( Edge e in solid.Edges ) 71 | { 72 | curves.Add( e.AsCurve() ); 73 | } 74 | } 75 | } 76 | 77 | //List GetCurves( Element e, Options opt ) 78 | //{ 79 | // GeometryElement geo = e.get_Geometry( opt ); 80 | 81 | // List curves = new List(); 82 | // List solids = new List(); 83 | 84 | // AddCurvesAndSolids( geo, curves, solids ); 85 | 86 | // return curves; 87 | //} 88 | 89 | //JtLoops GetLoops( Element e, Options opt ) 90 | //{ 91 | 92 | // List curves = GetCurves( e, opt ); 93 | // JtLoops loops = null; 94 | // return loops; 95 | //} 96 | 97 | /// 98 | /// Return loops for outer 2D outline 99 | /// of the given element ids. 100 | /// - Retrieve geometry curves from edges 101 | /// - Convert to linear segments and 2D integer coordinates 102 | /// - Convert to non-intersecting line segments 103 | /// - Start from left-hand bottom point 104 | /// - Go down, then right 105 | /// - Keep following right-mostconection until closed loop is found 106 | /// 107 | public EdgeLoopRetriever( 108 | Options opt, 109 | ICollection ids ) 110 | { 111 | Document doc = opt.View.Document; 112 | 113 | List curves = new List(); 114 | List solids = new List(); 115 | 116 | foreach( ElementId id in ids ) 117 | { 118 | curves.Clear(); 119 | solids.Clear(); 120 | 121 | // Retrieve element geometry 122 | 123 | Element e = doc.GetElement( id ); 124 | GeometryElement geo = e.get_Geometry( opt ); 125 | AddCurvesAndSolids( geo, curves, solids ); 126 | 127 | // Extract curves from solids 128 | 129 | AddCurvesFromSolids( curves, solids ); 130 | 131 | // Flatten and simplify to line unique segments 132 | // of non-zero length with 2D integer millimetre 133 | // coordinates 134 | 135 | JtLineCollection lines = new JtLineCollection( 136 | curves ); 137 | 138 | // Todo: Chop at each intersection, eliminating 139 | // all non-endpoint intersections 140 | 141 | // Contour following: 142 | // Regardless whether loop is closed or not, add it regardless. 143 | // Remove the line segments forming it and all contained within it. 144 | // If one endpoint is within and one outside, we have a relevant intersection. 145 | // Remove the line segment within, and shorten the line segment outside to the lloop edge. 146 | 147 | JtLoops loops = lines.GetOutline(); 148 | 149 | _loops.Add( id.IntegerValue, loops ); 150 | } 151 | } 152 | 153 | public Dictionary Loops 154 | { 155 | get { return _loops; } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /ElementOutline/JtBoundingBox2dInt.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | #endregion 6 | 7 | namespace ElementOutline 8 | { 9 | /// 10 | /// A bounding box for a collection 11 | /// of 2D integer points. 12 | /// 13 | class JtBoundingBox2dInt 14 | { 15 | /// 16 | /// Margin around graphics when 17 | /// exporting SVG view box. 18 | /// 19 | const int _margin = 10; 20 | 21 | /// 22 | /// Minimum and maximum X and Y values. 23 | /// 24 | int xmin, ymin, xmax, ymax; 25 | 26 | /// 27 | /// Initialise to infinite values, e.g. empty box. 28 | /// 29 | public JtBoundingBox2dInt() 30 | { 31 | Init(); 32 | } 33 | 34 | /// 35 | /// Initialise to infinite values, e.g. empty box. 36 | /// 37 | public void Init() 38 | { 39 | xmin = ymin = int.MaxValue; 40 | xmax = ymax = int.MinValue; 41 | } 42 | 43 | /// 44 | /// Return current lower left corner. 45 | /// 46 | public Point2dInt Min 47 | { 48 | get { return new Point2dInt( xmin, ymin ); } 49 | } 50 | 51 | /// 52 | /// Return current upper right corner. 53 | /// 54 | public Point2dInt Max 55 | { 56 | get { return new Point2dInt( xmax, ymax ); } 57 | } 58 | 59 | /// 60 | /// Return current center point. 61 | /// 62 | public Point2dInt MidPoint 63 | { 64 | get 65 | { 66 | return new Point2dInt( 67 | (int)(0.5 * ( xmin + xmax )), 68 | (int)(0.5 * ( ymin + ymax )) ); 69 | } 70 | } 71 | 72 | /// 73 | /// Return current width. 74 | /// 75 | public int Width 76 | { 77 | get { return xmax - xmin; } 78 | } 79 | 80 | /// 81 | /// Return current height. 82 | /// 83 | public int Height 84 | { 85 | get { return ymax - ymin; } 86 | } 87 | 88 | /// 89 | /// Return aspect ratio, i.e. Height/Width. 90 | /// 91 | public double AspectRatio 92 | { 93 | get 94 | { 95 | return (double) Height / (double) Width; 96 | } 97 | } 98 | 99 | /// 100 | /// Return a System.Drawing.Rectangle for this. 101 | /// 102 | public Rectangle Rectangle 103 | { 104 | get 105 | { 106 | return new Rectangle( xmin, ymin, 107 | Width, Height ); 108 | } 109 | } 110 | 111 | /// 112 | /// Grow or shrink bounding box in all directions 113 | /// by the given amount. 114 | /// 115 | public void AdjustBy( int d ) 116 | { 117 | xmin -= d; 118 | ymin -= d; 119 | xmax += d; 120 | ymax += d; 121 | } 122 | 123 | /// 124 | /// Expand bounding box to contain 125 | /// the given new point. 126 | /// 127 | public void ExpandToContain( Point2dInt p ) 128 | { 129 | if( p.X < xmin ) { xmin = p.X; } 130 | if( p.Y < ymin ) { ymin = p.Y; } 131 | if( p.X > xmax ) { xmax = p.X; } 132 | if( p.Y > ymax ) { ymax = p.Y; } 133 | } 134 | 135 | /// 136 | /// Expand bounding box to contain 137 | /// the given other bounding box. 138 | /// 139 | public void ExpandToContain( JtBoundingBox2dInt b ) 140 | { 141 | ExpandToContain( b.Min ); 142 | ExpandToContain( b.Max ); 143 | } 144 | 145 | ///// 146 | ///// Instantiate a new bounding box containing 147 | ///// the given loops. 148 | ///// 149 | //public JtBoundingBox2dInt( JtLoops loops ) 150 | //{ 151 | // foreach( JtLoop loop in loops ) 152 | // { 153 | // foreach( Point2dInt p in loop ) 154 | // { 155 | // ExpandToContain( p ); 156 | // } 157 | // } 158 | //} 159 | 160 | /// 161 | /// Return the four bounding box corners. 162 | /// 163 | public Point2dInt[] Corners 164 | { 165 | get 166 | { 167 | return new Point2dInt[] { 168 | Min, 169 | new Point2dInt( xmax, ymin ), 170 | Max, 171 | new Point2dInt( xmin, ymax ) 172 | }; 173 | } 174 | } 175 | 176 | /// 177 | /// Display as a string. 178 | /// 179 | public override string ToString() 180 | { 181 | return string.Format( "({0},{1})", Min, Max ); 182 | } 183 | 184 | /// 185 | /// Return the SVG viewBox 186 | /// of this bounding box. 187 | /// 188 | public string SvgViewBox 189 | { 190 | get 191 | { 192 | int left = xmin - _margin; 193 | int bottom = ymin - _margin; 194 | int w = Width + _margin + _margin; 195 | int h = Height + _margin + _margin; 196 | if( Util.SvgFlip ) 197 | { 198 | bottom = Util.SvgFlipY( bottom ) - h; 199 | } 200 | return string.Format( 201 | "{0} {1} {2} {3}", 202 | left, bottom, w, h ); 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /ElementOutline/ElementOutline.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | None 7 | 8 | 9 | 10 | 11 | Debug 12 | AnyCPU 13 | {DCB91AF8-6D19-42B1-9D16-E4DE3192E8F9} 14 | Library 15 | Properties 16 | ElementOutline 17 | ElementOutline 18 | v4.7 19 | 512 20 | 21 | 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | Program 30 | $(ProgramW6432)\Autodesk\Revit 2020\Revit.exe 31 | 32 | 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | Program 40 | $(ProgramW6432)\Autodesk\Revit 2020\Revit.exe 41 | 42 | 43 | 44 | 45 | $(ProgramW6432)\Autodesk\Revit 2020\RevitAPI.dll 46 | False 47 | 48 | 49 | C:\Program Files\Autodesk\Revit 2020\RevitAPIIFC.dll 50 | False 51 | 52 | 53 | $(ProgramW6432)\Autodesk\Revit 2020\RevitAPIUI.dll 54 | False 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | {9b062971-a88e-4a3d-b3c9-12b78d15fa66} 92 | clipper_library 93 | 94 | 95 | 96 | 97 | if exist "$(AppData)\Autodesk\REVIT\Addins\2020" copy "$(ProjectDir)*.addin" "$(AppData)\Autodesk\REVIT\Addins\2020" 98 | if exist "$(AppData)\Autodesk\REVIT\Addins\2020" copy "$(ProjectDir)$(OutputPath)*.dll" "$(AppData)\Autodesk\REVIT\Addins\2020" 99 | 100 | 101 | 102 | 103 | 104 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /ElementOutline/JtLineCollection.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using Autodesk.Revit.DB; 7 | #endregion 8 | 9 | namespace ElementOutline 10 | { 11 | /// 12 | /// Store line segment data twice over, with both 13 | /// endpoints entered as keys, pointing to a list 14 | /// of all the corresponding other endpoints 15 | /// 16 | class JtLineCollection : Dictionary> 17 | { 18 | static double _min_len = Util.ConvertMillimetresToFeet( 1 ); 19 | static double _step_len = Util.ConvertMillimetresToFeet( 5 ); 20 | 21 | /// 22 | /// Add a new segment to the collection, 23 | /// avoiding duplication 24 | /// 25 | void AddSegment2( 26 | Point2dInt p, 27 | Point2dInt q ) 28 | { 29 | if( !ContainsKey( p ) ) 30 | { 31 | Add( p, new List( 1 ) ); 32 | } 33 | if( !this[ p ].Contains( q ) ) 34 | { 35 | this[ p ].Add( q ); 36 | } 37 | } 38 | 39 | /// 40 | /// Add a new segment to the collection in both 41 | /// directions, so that both of its endpoints 42 | /// show up as dictionary keys. 43 | /// 44 | void AddSegment( 45 | Point2dInt a, 46 | Point2dInt b ) 47 | { 48 | AddSegment2( a, b ); 49 | AddSegment2( b, a ); 50 | } 51 | 52 | /// 53 | /// Initialise the collection of 2D integer 54 | /// millimetre coordinate line segments from 55 | /// the given Revit 3D curves in feet. 56 | /// 57 | public JtLineCollection( List curves ) 58 | { 59 | foreach( Curve c in curves ) 60 | { 61 | double len = c.Length; 62 | 63 | if( len < _min_len ) 64 | { 65 | continue; 66 | } 67 | 68 | Point2dInt a = new Point2dInt( 69 | c.GetEndPoint( 0 ) ); 70 | 71 | Point2dInt b; 72 | 73 | if( len < _step_len ) 74 | { 75 | b = new Point2dInt( c.GetEndPoint( 1 ) ); 76 | AddSegment( a, b ); 77 | } 78 | 79 | int nMaxSegments = (int) Math.Round( len / _step_len, 80 | MidpointRounding.AwayFromZero ); 81 | 82 | IList pts = c.Tessellate(); 83 | 84 | int n = pts.Count; 85 | 86 | if( n > nMaxSegments ) 87 | { 88 | double sp = c.GetEndParameter( 0 ); 89 | double ep = c.GetEndParameter( 1 ); 90 | double step = (ep - sp) / nMaxSegments; 91 | 92 | double t = sp + step; 93 | 94 | for( int i = 0; i < nMaxSegments; ++i, t += step ) 95 | { 96 | b = new Point2dInt( c.Evaluate( t, false ) ); 97 | 98 | if( 0 != a.CompareTo( b ) ) 99 | { 100 | AddSegment( a, b ); 101 | a = b; 102 | } 103 | } 104 | } 105 | else 106 | { 107 | for( int i = 1; i < n; ++i ) 108 | { 109 | b = new Point2dInt( pts[ i ] ); 110 | 111 | if( 0 != a.CompareTo( b ) ) 112 | { 113 | AddSegment( a, b ); 114 | a = b; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | /// 122 | /// Angle comparer class, taking into account the 123 | /// direction we are coming from 124 | /// 125 | class Point2dIntAngleComparer : IComparer 126 | { 127 | Point2dInt _current; 128 | double _current_angle; 129 | 130 | public Point2dIntAngleComparer( 131 | Point2dInt current, 132 | double current_angle ) 133 | { 134 | Debug.Assert( -Math.PI < current_angle, 135 | "expected current_angle in interval (-pi,pi]" ); 136 | 137 | Debug.Assert( current_angle <= Math.PI, 138 | "expected current_angle in interval (-pi,pi]" ); 139 | 140 | _current = current; 141 | _current_angle = current_angle; 142 | } 143 | 144 | /// 145 | /// Order the target points by angle. 146 | /// Right-most comes first, so small comes before large. 147 | /// However, current_angle defines the origin of comparison. 148 | /// Therefore, if ax is only slightly larger than current_angle 149 | /// and ay is slightly smaller, ay is considered ay + 2 * pi. 150 | /// If the angles are equal, the closer of the two point is 151 | /// considered smaller. 152 | /// 153 | public int Compare( Point2dInt x, Point2dInt y ) 154 | { 155 | double ax = _current.AngleTo( x ); // (-pi,pi] 156 | double ay = _current.AngleTo( y ); 157 | 158 | if( Util.IsEqual( ax, ay ) ) 159 | { 160 | double dx = _current.DistanceTo( x ); 161 | double dy = _current.DistanceTo( y ); 162 | return dx.CompareTo( dy ); 163 | } 164 | 165 | if( ax < _current_angle ) 166 | { 167 | ax += 2 * Math.PI; 168 | } 169 | if( ay < _current_angle ) 170 | { 171 | ay += 2 * Math.PI; 172 | } 173 | int d = ax.CompareTo( ay ); 174 | if( 0 == d ) 175 | { 176 | double dx = _current.DistanceTo( x ); 177 | double dy = _current.DistanceTo( y ); 178 | d = dx.CompareTo( dy ); 179 | } 180 | return d; 181 | } 182 | } 183 | 184 | public bool GetOutlineRecursion( 185 | List route ) 186 | { 187 | Point2dInt endpoint = route[ 0 ]; 188 | 189 | int n = route.Count; 190 | Point2dInt current = route[ n - 1 ]; 191 | List candidates = this[ current ]; 192 | if( candidates.Contains( endpoint ) ) 193 | { 194 | // A closed loop has been completed 195 | return true; 196 | } 197 | 198 | // At the left-most point, try going downwards, 199 | // else try the right-most possibility taking 200 | // the current direction into account 201 | 202 | double current_angle = (1 == n) 203 | ? -0.5 * Math.PI 204 | : route[ n - 2 ].AngleTo( current ); 205 | 206 | candidates.Sort( new Point2dIntAngleComparer( 207 | current, current_angle ) ); 208 | 209 | foreach( Point2dInt cand in candidates ) 210 | { 211 | route.Add( cand ); 212 | Debug.Assert( n + 1 == route.Count, 213 | "expected exactly one candidate added" ); 214 | 215 | Debug.Assert( this[ cand ].Contains( current ), 216 | "expected return path" ); 217 | this[ cand ].Remove( current ); 218 | 219 | if( GetOutlineRecursion( route ) ) 220 | { 221 | return true; 222 | } 223 | 224 | this[ cand ].Add( current ); 225 | 226 | Debug.Assert( n + 1 == route.Count, 227 | "expected exactly one candidate added" ); 228 | route.RemoveAt( n ); 229 | } 230 | 231 | // We tried all candidates and found 232 | // no closed loop, so retreat 233 | 234 | return false; 235 | } 236 | 237 | /// 238 | /// Return the outline loops of all the line segments 239 | /// 240 | public JtLoops GetOutline() 241 | { 242 | JtLoops loops = new JtLoops(1); 243 | 244 | while( 0 < Count ) 245 | { 246 | // Outline route taken so far 247 | 248 | List route = new List( 1 ); 249 | 250 | // Start at minimum point 251 | 252 | route.Add( Keys.Min() ); 253 | 254 | // Recursively search until a closed outline is found 255 | 256 | bool closed = GetOutlineRecursion( route ); 257 | 258 | loops.Add( new JtLoop( route.ToArray() ) ); 259 | 260 | if( closed ) 261 | { 262 | // Eliminate all line segments entirely enclosed. 263 | // Truncate line segments partially enclosed and remove the inner part. 264 | // A line segment might cut through the entire loop, with both endpoints outside. 265 | } 266 | } 267 | return loops; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /ElementOutline/ContiguousCurveSorter.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Autodesk.Revit.DB; 6 | using System.Diagnostics; 7 | #endregion 8 | 9 | namespace ElementOutline 10 | { 11 | static class CurveGetEnpointExtension 12 | { 13 | static public XYZ GetEndPoint( 14 | this Curve curve, 15 | int i ) 16 | { 17 | return curve.GetEndPoint( i ); 18 | } 19 | } 20 | 21 | /// 22 | /// Curve loop utilities supporting resorting and 23 | /// orientation of curves to form a contiguous 24 | /// closed loop. 25 | /// 26 | class CurveUtils 27 | { 28 | const double _inch = 1.0 / 12.0; 29 | const double _sixteenth = _inch / 16.0; 30 | 31 | public enum FailureCondition 32 | { 33 | Success, 34 | CurvesNotContigous, 35 | CurveLoopAboveTarget, 36 | NoIntersection 37 | }; 38 | 39 | /// 40 | /// Predicate to report whether the given curve 41 | /// type is supported by this utility class. 42 | /// 43 | /// The curve. 44 | /// True if the curve type is supported, 45 | /// false otherwise. 46 | public static bool IsSupported( 47 | Curve curve ) 48 | { 49 | return curve is Line || curve is Arc; 50 | } 51 | 52 | /// 53 | /// Create a new curve with the same 54 | /// geometry in the reverse direction. 55 | /// 56 | /// The original curve. 57 | /// The reversed curve. 58 | /// If the 59 | /// curve type is not supported by this utility. 60 | static Curve CreateReversedCurve( 61 | Autodesk.Revit.Creation.Application creapp, 62 | Curve orig ) 63 | { 64 | if( !IsSupported( orig ) ) 65 | { 66 | throw new NotImplementedException( 67 | "CreateReversedCurve for type " 68 | + orig.GetType().Name ); 69 | } 70 | 71 | if( orig is Line ) 72 | { 73 | return Line.CreateBound( 74 | orig.GetEndPoint( 1 ), 75 | orig.GetEndPoint( 0 ) ); 76 | } 77 | else if( orig is Arc ) 78 | { 79 | return Arc.Create( orig.GetEndPoint( 1 ), 80 | orig.GetEndPoint( 0 ), 81 | orig.Evaluate( 0.5, true ) ); 82 | } 83 | else 84 | { 85 | throw new Exception( 86 | "CreateReversedCurve - Unreachable" ); 87 | } 88 | } 89 | 90 | /// 91 | /// Sort a list of curves to make them correctly 92 | /// ordered and oriented to form a closed loop. 93 | /// 94 | public static void SortCurvesContiguous( 95 | Autodesk.Revit.Creation.Application creapp, 96 | IList curves, 97 | bool debug_output ) 98 | { 99 | int n = curves.Count; 100 | 101 | // Walk through each curve (after the first) 102 | // to match up the curves in order 103 | 104 | for( int i = 0; i < n; ++i ) 105 | { 106 | Curve curve = curves[i]; 107 | XYZ endPoint = curve.GetEndPoint( 1 ); 108 | 109 | if( debug_output ) 110 | { 111 | Debug.Print( "{0} endPoint {1}", i, 112 | Util.PointString( endPoint ) ); 113 | } 114 | 115 | XYZ p; 116 | 117 | // Find curve with start point = end point 118 | 119 | bool found = (i + 1 >= n); 120 | 121 | for( int j = i + 1; j < n; ++j ) 122 | { 123 | p = curves[j].GetEndPoint( 0 ); 124 | 125 | // If there is a match end->start, 126 | // this is the next curve 127 | 128 | if( _sixteenth > p.DistanceTo( endPoint ) ) 129 | { 130 | if( i + 1 == j ) 131 | { 132 | if( debug_output ) 133 | { 134 | Debug.Print( 135 | "{0} start point match, no need to swap", 136 | j, i + 1 ); 137 | } 138 | } 139 | else 140 | { 141 | if( debug_output ) 142 | { 143 | Debug.Print( 144 | "{0} start point, swap with {1}", 145 | j, i + 1 ); 146 | } 147 | Curve tmp = curves[i + 1]; 148 | curves[i + 1] = curves[j]; 149 | curves[j] = tmp; 150 | } 151 | found = true; 152 | break; 153 | } 154 | 155 | p = curves[j].GetEndPoint( 1 ); 156 | 157 | // If there is a match end->end, 158 | // reverse the next curve 159 | 160 | if( _sixteenth > p.DistanceTo( endPoint ) ) 161 | { 162 | if( i + 1 == j ) 163 | { 164 | if( debug_output ) 165 | { 166 | Debug.Print( 167 | "{0} end point, reverse {1}", 168 | j, i + 1 ); 169 | } 170 | 171 | curves[i + 1] = CreateReversedCurve( 172 | creapp, curves[j] ); 173 | } 174 | else 175 | { 176 | if( debug_output ) 177 | { 178 | Debug.Print( 179 | "{0} end point, swap with reverse {1}", 180 | j, i + 1 ); 181 | } 182 | 183 | Curve tmp = curves[i + 1]; 184 | curves[i + 1] = CreateReversedCurve( 185 | creapp, curves[j] ); 186 | curves[j] = tmp; 187 | } 188 | found = true; 189 | break; 190 | } 191 | } 192 | if( !found ) 193 | { 194 | throw new Exception( "SortCurvesContiguous:" 195 | + " non-contiguous input curves" ); 196 | } 197 | } 198 | } 199 | 200 | /// 201 | /// Return a list of curves which are correctly 202 | /// ordered and oriented to form a closed loop. 203 | /// 204 | /// The document. 205 | /// The list of curve element references which are the boundaries. 206 | /// The list of curves. 207 | public static IList GetContiguousCurvesFromSelectedCurveElements( 208 | Document doc, 209 | IList boundaries, 210 | bool debug_output ) 211 | { 212 | List curves = new List(); 213 | 214 | // Build a list of curves from the curve elements 215 | 216 | foreach( Reference reference in boundaries ) 217 | { 218 | CurveElement curveElement = doc.GetElement( 219 | reference ) as CurveElement; 220 | 221 | curves.Add( curveElement.GeometryCurve.Clone() ); 222 | } 223 | 224 | SortCurvesContiguous( doc.Application.Create, 225 | curves, debug_output ); 226 | 227 | return curves; 228 | } 229 | 230 | /// 231 | /// Identifies if the curve lies entirely in an XY plane (Z = constant) 232 | /// 233 | /// The curve. 234 | /// True if the curve lies in an XY plane, false otherwise. 235 | public static bool IsCurveInXYPlane( Curve curve ) 236 | { 237 | // quick reject - are endpoints at same Z 238 | 239 | double zDelta = curve.GetEndPoint( 1 ).Z 240 | - curve.GetEndPoint( 0 ).Z; 241 | 242 | if( Math.Abs( zDelta ) > 1e-05 ) 243 | return false; 244 | 245 | if( !( curve is Line ) && !curve.IsCyclic ) 246 | { 247 | // Create curve loop from curve and 248 | // connecting line to get plane 249 | 250 | List curves = new List(); 251 | curves.Add( curve ); 252 | 253 | //curves.Add(Line.CreateBound(curve.GetEndPoint(1), curve.GetEndPoint(0))); 254 | 255 | CurveLoop curveLoop = CurveLoop.Create( curves ); 256 | 257 | XYZ normal = curveLoop.GetPlane().Normal 258 | .Normalize(); 259 | 260 | if( !normal.IsAlmostEqualTo( XYZ.BasisZ ) 261 | && !normal.IsAlmostEqualTo( XYZ.BasisZ.Negate() ) ) 262 | { 263 | return false; 264 | } 265 | } 266 | return true; 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /ElementOutline/GeoSnoop.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.Drawing.Drawing2D; 7 | using System.Linq; 8 | using System.Windows.Forms; 9 | using ElementId = Autodesk.Revit.DB.ElementId; 10 | #endregion 11 | 12 | namespace ElementOutline 13 | { 14 | // based on 06972592 [How to get the shape of Structural Framing objects] 15 | // /a/j/adn/case/sfdc/06972592/src/FramingXsecAnalyzer/FramingXsecAnalyzer/GeoSnoop.cs 16 | 17 | /// 18 | /// Display a collection of loops in a .NET form. 19 | /// 20 | class GeoSnoop 21 | { 22 | #region Constants 23 | /// 24 | /// Width of the form to generate. 25 | /// 26 | const int _form_width = 300; 27 | 28 | /// 29 | /// Pen size. 30 | /// 31 | const int _pen_size = 1; 32 | 33 | /// 34 | /// Pen colour. 35 | /// 36 | static Color _pen_color = Color.Black; 37 | 38 | /// 39 | /// Margin around graphics between 40 | /// sheet and form edge. 41 | /// 42 | const int _margin = 20; 43 | 44 | /// 45 | /// Margin around graphics between 46 | /// BIM elements and viewport edge. 47 | /// 48 | const int _margin2 = 20; 49 | #endregion // Constants 50 | 51 | #region Pen 52 | /// 53 | /// Our one and only pen. 54 | /// 55 | static Pen _pen = null; 56 | 57 | /// 58 | /// Set up and return our one and only pen. 59 | /// 60 | static Pen Pen 61 | { 62 | get 63 | { 64 | if( null == _pen ) 65 | { 66 | _pen = new Pen( _pen_color, _pen_size ); 67 | } 68 | return _pen; 69 | } 70 | } 71 | #endregion // Pen 72 | 73 | #region DrawLoopsOnGraphics 74 | /// 75 | /// Draw loops on graphics with the specified 76 | /// transform and graphics attributes. 77 | /// 78 | static void DrawLoopsOnGraphics( 79 | Graphics graphics, 80 | List loops, 81 | Matrix transform ) 82 | { 83 | foreach( Point[] loop in loops ) 84 | { 85 | GraphicsPath path = new GraphicsPath(); 86 | 87 | transform.TransformPoints( loop ); 88 | 89 | path.AddLines( loop ); 90 | 91 | graphics.DrawPath( Pen, path ); 92 | } 93 | } 94 | #endregion // DrawLoopsOnGraphics 95 | 96 | #region DisplayRoom and DisplayLoops 97 | /// 98 | /// Display room and the furniture contained in it 99 | /// in a bitmap generated on the fly. 100 | /// 101 | /// Room boundary loops 102 | /// Family symbol geometry 103 | /// Family instances 104 | public static Bitmap DisplayRoom( 105 | JtLoops roomLoops, 106 | Dictionary geometryLoops, 107 | List familyInstances ) 108 | { 109 | JtBoundingBox2dInt bbFrom = roomLoops.BoundingBox; 110 | 111 | // Adjust target rectangle height to the 112 | // displayee loop height. 113 | 114 | int width = _form_width; 115 | int height = (int) (width * bbFrom.AspectRatio + 0.5); 116 | 117 | //SizeF fsize = new SizeF( width, height ); 118 | 119 | //SizeF scaling = new SizeF( 1, 1 ); 120 | //PointF translation = new PointF( 0, 0 ); 121 | 122 | //GetTransform( fsize, bbFrom, 123 | // ref scaling, ref translation, true ); 124 | 125 | //Matrix transform1 = new Matrix( 126 | // new Rectangle(0,0,width,height), 127 | // bbFrom.GetParallelogramPoints()); 128 | //transform1.Invert(); 129 | 130 | // the bounding box fills the rectangle 131 | // perfectly and completely, inverted and 132 | // non-uniformly distorted: 133 | 134 | //Point2dInt pmin = bbFrom.Min; 135 | //Rectangle rect = new Rectangle( 136 | // pmin.X, pmin.Y, bbFrom.Width, bbFrom.Height ); 137 | //Point[] parallelogramPoints = new Point [] { 138 | // new Point( 0, 0 ), // upper left 139 | // new Point( width, 0 ), // upper right 140 | // new Point( 0, height ) // lower left 141 | //}; 142 | 143 | // the bounding box fills the rectangle 144 | // perfectly and completely, inverted and 145 | // non-uniformly distorted: 146 | 147 | // Specify transformation target rectangle 148 | // including a margin. 149 | 150 | int bottom = height - (_margin + _margin); 151 | 152 | Point[] parallelogramPoints = new Point[] { 153 | new Point( _margin, bottom ), // upper left 154 | new Point( width - _margin, bottom ), // upper right 155 | new Point( _margin, _margin ) // lower left 156 | }; 157 | 158 | // Transform from native loop coordinate system 159 | // to target display coordinates. 160 | 161 | Matrix transform = new Matrix( 162 | bbFrom.Rectangle, parallelogramPoints ); 163 | 164 | Bitmap bmp = new Bitmap( width, height ); 165 | Graphics graphics = Graphics.FromImage( bmp ); 166 | 167 | graphics.Clear( System.Drawing.Color.White ); 168 | 169 | DrawLoopsOnGraphics( graphics, 170 | roomLoops.GetGraphicsPathLines(), transform ); 171 | 172 | if( null != familyInstances ) 173 | { 174 | List loops = new List( 1 ); 175 | loops.Add( new Point[] { } ); 176 | 177 | foreach( JtPlacement2dInt i in familyInstances ) 178 | { 179 | Point2dInt v = i.Translation; 180 | Matrix placement = new Matrix(); 181 | placement.Rotate( i.Rotation ); 182 | placement.Translate( v.X, v.Y, MatrixOrder.Append ); 183 | placement.Multiply( transform, MatrixOrder.Append ); 184 | loops[0] = geometryLoops[i.SymbolId] 185 | .GetGraphicsPathLines(); 186 | 187 | DrawLoopsOnGraphics( graphics, loops, placement ); 188 | } 189 | } 190 | return bmp; 191 | } 192 | 193 | /// 194 | /// Display a collection of loops in 195 | /// a bitmap generated on the fly. 196 | /// 197 | public static Bitmap DisplayLoops( 198 | ICollection loops ) 199 | { 200 | JtBoundingBox2dInt bbFrom = new JtBoundingBox2dInt(); 201 | foreach( JtLoops a in loops ) 202 | { 203 | bbFrom.ExpandToContain( a.BoundingBox ); 204 | } 205 | //bbFrom.AdjustBy(); 206 | 207 | // Adjust target rectangle height to the 208 | // displayee loop height. 209 | 210 | int width = _form_width; 211 | int height = (int) (width * bbFrom.AspectRatio + 0.5); 212 | 213 | // the bounding box fills the rectangle 214 | // perfectly and completely, inverted and 215 | // non-uniformly distorted. 216 | 217 | // Reduce target rectangle slightly so line 218 | // segments along the outer edge are visible. 219 | //width -= 6; 220 | //height -= 6; 221 | 222 | // Specify transformation target rectangle 223 | // including a margin. 224 | 225 | int bottom = height - (_margin + _margin); 226 | 227 | Point[] parallelogramPoints = new Point[] { 228 | new Point( _margin, bottom ), // upper left 229 | new Point( width - _margin, bottom ), // upper right 230 | new Point( _margin, _margin ) // lower left 231 | }; 232 | 233 | // Transform from native loop coordinate system 234 | // to target display coordinates. 235 | 236 | Matrix transform = new Matrix( 237 | bbFrom.Rectangle, parallelogramPoints ); 238 | 239 | //int edge_width = 4; 240 | 241 | Bitmap bmp = new Bitmap( width, height ); 242 | 243 | Graphics graphics = Graphics.FromImage( bmp ); 244 | 245 | graphics.Clear( System.Drawing.Color.White ); 246 | 247 | foreach( JtLoops a in loops ) 248 | { 249 | DrawLoopsOnGraphics( graphics, 250 | a.GetGraphicsPathLines(), transform ); 251 | } 252 | return bmp; 253 | } 254 | #endregion // DisplayRoom 255 | 256 | #region DisplayImageInForm 257 | /// 258 | /// Generate a form on the fly and display the 259 | /// given bitmap image in it in a picture box. 260 | /// 261 | /// Owner window 262 | /// Form caption 263 | /// Modal versus modeless 264 | /// Bitmap image to display 265 | public static void DisplayImageInForm( 266 | IWin32Window owner, 267 | string caption, 268 | bool modal, 269 | Bitmap bmp ) 270 | { 271 | Form form = new Form(); 272 | form.Text = caption; 273 | 274 | form.Size = new Size( bmp.Width + 7, 275 | bmp.Height + 13 ); 276 | 277 | form.FormBorderStyle = FormBorderStyle 278 | .FixedToolWindow; 279 | 280 | PictureBox pb = new PictureBox(); 281 | pb.Location = new System.Drawing.Point( 0, 0 ); 282 | pb.Dock = System.Windows.Forms.DockStyle.Fill; 283 | pb.Size = bmp.Size; 284 | pb.Parent = form; 285 | pb.Image = bmp; 286 | 287 | if( modal ) 288 | { 289 | form.ShowDialog( owner ); 290 | } 291 | else 292 | { 293 | form.Show( owner ); 294 | } 295 | } 296 | #endregion // DisplayImageInForm 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElementOutline 2 | 3 | Revit C# .NET add-in to export 2D outlines of RVT project `Element` instances. 4 | 5 | Table of contents: 6 | 7 | - [Task – 2D polygon representing birds-eye view of an element](#2) 8 | - [CmdExtrusionAnalyzer](#3) 9 | - [Alternative approaches to determine 2D element outline](#4) 10 | - [Cmd2dBoolean](#5) 11 | - [CmdRoomOuterOutline](#6) 12 | - [Author](#7) 13 | - [License](#8) 14 | 15 | The add-in implements three external commands: 16 | 17 | - [CmdExtrusionAnalyzer](#3) – generate element outline using `ExtrusionAnalyzer` 18 | - [Cmd2dBoolean](#5) – generate element outline using 2D Booleans 19 | - [CmdRoomOuterOutline](#6) – outer room outline using 2D Booleans 20 | 21 | All three generate element outlines of various types in various ways. 22 | 23 | The first uses the Revit API and 24 | the [`ExtrusionAnalyzer` class](https://www.revitapidocs.com/2020/ba9e3283-6868-8834-e8bf-2ea9e7358930.htm). 25 | 26 | The other two make use of 27 | the [Clipper integer coordinate based 2D Boolean operations library](http://angusj.com/delphi/clipper.php). 28 | 29 | The add-in also implements a bunch of utilities for converting Revit coordinates to 2D data in millimetre units and displaying the resulting element outlines in a Windows form. 30 | 31 | 32 | ## Task – 2D Polygon Representing Birds-Eye View of an Element 33 | 34 | The goal is to export the 2D outlines of Revit `Element` instances, i.e., for each element, associate its element id or unique id with the list of X,Y coordinates describing a polygon representing the visual birds-eye view look of its outline. 35 | 36 | Additional requirements: 37 | 38 | - Address family instances as well as elements that might be built as part of the construction, including wall, floor, railing, ceiling, mechanical duct, panel, plumbing pipe. 39 | - Generate a separate outline in place for each element, directly in its appropriate location and orientation. 40 | - Output the result in a simple text file. 41 | 42 | There is no need for a rendered view, just coordinates defining a 2D polygon around the element. 43 | 44 | The goal is: given an element id, retrieve a list of X,Y coordinates describing the birds-eye view look of an element. 45 | 46 | 61 | 62 | For instance, here is an apartment layout showing a birdseye view of bathtubs, doors, toilets and other accessories: 63 | 64 |
65 | Apartment layout showing birdseye view of accessories 66 |
67 | 68 | In end effect, we generate a dictionary mapping an element id or unique id to a list of space delimited pairs of X Y vertex coordinates in millimetres. 69 | 70 | 71 | ## CmdExtrusionAnalyzer 72 | 73 | This code was originally implemented as part of (and later extracted from) 74 | the [RoomEditorApp project](https://github.com/jeremytammik/RoomEditorApp). 75 | 76 | The approach implemented for the room editor is not based on the 2D view, but on the element geometry solids in the 3D view and the result of applying 77 | the [`ExtrusionAnalyzer` class](https://www.revitapidocs.com/2020/ba9e3283-6868-8834-e8bf-2ea9e7358930.htm) to them, 78 | creating a vertical projection of the 3D element shape onto the 2D XY plane. 79 | This approach is described in detail in the discussion on 80 | the [extrusion analyser and plan view boundaries](https://thebuildingcoder.typepad.com/blog/2013/04/extrusion-analyser-and-plan-view-boundaries.html). 81 | 82 | The [GeoSnoop .NET boundary curve loop visualisation](https://thebuildingcoder.typepad.com/blog/2013/04/geosnoop-net-boundary-curve-loop-visualisation.html) provides 83 | some example images of the resulting outlines. 84 | 85 | As you can see there, the outline generated is more precise and detailed than the standard 2D Revit representation. 86 | 87 | The standard plan view of the default desk and chair components look like this in Revit: 88 | 89 | Plan view of desk and chair in Revit 90 | 91 | The loops exported by the RoomEditorApp add-in for the same desk and chair look like this instead: 92 | 93 | Desk and chair loops in GeoSnoop 94 | 95 | E.g., for the desk, you notice the little bulges for the desk drawer handles sticking out a little bit beyond the desktop surface. 96 | 97 | For the chair, the arm rests are missing, because the solids used to model them do not make it through the extruson analyser, or maybe because the code ignores multiple disjunct loops. 98 | 99 | Here is a sample model with four elements highlighted in blue: 100 | 101 | Four elements selected 102 | 103 | For them, the CmdExtrusionAnalyzer command generates the following JSON file defining their outline polygon in SVG format: 104 | 105 |
106 | {"name":"pt2+20+7", "id":"576786", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cd12", "svg_path":"M-56862 -9150 L-56572 -9150 -56572 -14186 -56862 -14186Z"}
107 | {"name":"pt70/210", "id":"576925", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cd9d", "svg_path":"M-55672 -11390 L-55672 -11290 -55656 -11290 -55656 -11278 -55087 -11278 -55087 -11270 -55076 -11270 -55076 -11242 -55182 -11242 -55182 -11214 -55048 -11214 -55048 -11270 -55037 -11270 -55037 -11278 -54988 -11278 -54988 -11290 -54972 -11290 -54972 -11390Z"}
108 | {"name":"pt80/115", "id":"576949", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cdb5", "svg_path":"M-56572 -10580 L-56572 -9430 -55772 -9430 -55772 -10580Z"}
109 | {"name":"מנוע מזגן מפוצל", "id":"576972", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cdcc", "svg_path":"M-56753 -8031 L-56713 -8031 -56713 -8018 -56276 -8018 -56276 -8031 -56265 -8031 -56265 -8109 -56276 -8109 -56276 -8911 -56252 -8911 -56252 -8989 -56276 -8989 -56276 -9020 -56277 -9020 -56278 -9020 -56711 -9020 -56713 -9020 -56713 -8989 -56753 -8989 -56753 -8911 -56713 -8911 -56713 -8109 -56753 -8109Z"}
110 | 
111 | 112 | `M`, `L` and `Z` stand for `moveto`, `lineto` and `close`, respectively. Repetitions of `L` can be omitted. Nice and succinct. 113 | 114 | However, the extrusion analyser approach obviously fails for all elements that do not define any solids, e.g., 2D elements represented only by curves and meshes. 115 | 116 | Hence the continued research to find an alternative approach and the implementation of `Cmd2dBoolean` described below making use of the Clipper library and 2D Booleans instead. 117 | 118 | In July 2019, I checked with the development team and asked whether they could suggest a better way to retrieve the 2D outline of an element. 119 | 120 | They responded that my `ExtrusionAnalyzer` approach seems like the best (and maybe only) way to achieve this right now. 121 | 122 | Considering Cmd2dBoolean, I might add the caveat 'using the Revit API' to the last statement. 123 | 124 | 125 | ## Alternative Approaches to Determine 2D Element Outline 126 | 127 | The `ExtrusionAnalyzer` approach based on element solids does not successfully address the task of generating the 2D birds-eye view outline for all Revit elements. 128 | 129 | I therefore explored other avenues. 130 | 131 | Concave hull: 132 | 133 | - http://ubicomp.algoritmi.uminho.pt/local/concavehull.html 134 | - https://towardsdatascience.com/the-concave-hull-c649795c0f0f 135 | - https://github.com/kubkon/powercrust 136 | - https://adared.ch/concaveman-cpp-a-very-fast-2d-concave-hull-maybe-even-faster-with-c-and-python/ 137 | - https://www.codeproject.com/Articles/1201438/The-Concave-Hull-of-a-Set-of-Points 138 | - http://www.cs.ubc.ca/research/flann/ 139 | 140 | 2D outline: 141 | 142 | - https://github.com/eppz/Unity.Library.eppz.Geometry 143 | - https://github.com/eppz/Clipper 144 | - https://github.com/eppz/Triangle.NET 145 | - https://en.wikipedia.org/wiki/Sweep_line_algorithm 146 | - https://stackoverflow.com/questions/4213117/the-generalization-of-bentley-ottmann-algorithm 147 | - https://ggolikov.github.io/bentley-ottman/ 148 | - Joining unordered line segments – https://stackoverflow.com/questions/1436091/joining-unordered-line-segments 149 | - http://www3.cs.stonybrook.edu/~algorith/implement/sweep/implement.shtml 150 | - https://github.com/mikhaildubov/Computational-geometry/blob/master/2)%20Any%20segments%20intersection/src/ru/dubov/anysegmentsintersect/SegmentsIntersect.java 151 | - https://github.com/jeremytammik/wykobi/blob/master/wykobi_naive_group_intersections.inl 152 | 153 | Alpha shape: 154 | 155 | - https://en.wikipedia.org/wiki/Alpha_shape 156 | - https://pypi.org/project/alphashape/ 157 | - https://alphashape.readthedocs.io/ 158 | 159 | I determined that some elements have no solids, just meshes, hence the extrusion analyser approach cannot be used. 160 | 161 | Looked at the [alpha shape implementation here](https://pypi.org/project/alphashape). 162 | 163 | I worked on a 2D contour outline following algorithm, but it turned out quite complex. 164 | 165 | I had another idea for a much simpler approach using 2D Boolean operations, uniting all the solid faces and mesh faces into one single 2D polygon set. 166 | 167 | - Join all line segments into closed polygons 168 | - Union all the polygons using Clipper 169 | 170 | That seems to return robust results. 171 | 172 | 173 | ## Cmd2dBoolean 174 | 175 | I completed a new poly2d implementation using 2D Booleans instead of the solids and extrusion analyser. 176 | I expect it is significantly faster. 177 | 178 | The ElementOutline release 2020.0.0.10 exports outlines from both solids and 2D Booleans and generates identical results for both, so that is a good sign. 179 | 180 | Maybe meshes and solids cover all requirements. 181 | I am still experimenting and testing. 182 | What is missing besides meshes and solids? 183 | 184 | I tested successfully on an intercom element. 185 | It is not a mesh, just a circle, represented by a full closed arc. 186 | I implemented support to include circles as well as solids and meshes in the Boolean operation. 187 | 188 | I also implemented a utility `GeoSnoop` to display the loops generated in a temporary Windows form. 189 | 190 | Here is an image showing part of a sample Revit model in the middle including a wall, bathtub and intercom element and two GeoSnoop windows: 191 | 192 | GeoSnoop outlines generated from solids versus 2D Booleans 193 | 194 | The left GeoSnoop window shows the outline loops retrieved from the solids using the extrusion analyser. 195 | The right one shows the loops retrieved from the 2D Booleans, including closed arcs. 196 | Note the differences in the intercom and the bathtub drain. 197 | 198 | My target is to continue enhancing the 2D Booleans until they include all the solid loop information, so that we can then get rid of the solid and extrusion analyser code. 199 | 200 | Maybe all I need to do is to use LevelOfDetail = Fine? 201 | 202 | ``` 203 | Options opt = new Options 204 | { 205 | IncludeNonVisibleObjects = true, 206 | DetailLevel = ViewDetailLevel.Fine 207 | }; 208 | GeometryElement geomElem = element.get_Geometry(opt); 209 | ``` 210 | 211 | I might try again with fine detail level. 212 | However, the circle already looks pretty good to me. 213 | In fact, right now, I think all I need is there, in the combination of the two approaches. 214 | 215 | The first image was generated by capturing data from a 2D view. 216 | Capturing the 2D Booleans from a 3D view gives us all we need, I think. 217 | 218 | Tested a few use-cases and it seems to be working fine. 219 | 220 | Currently, the production pipeline uses an implementation in Python based on 221 | the [Shapely library](https://github.com/Toblerity/Shapely) for 222 | manipulation and analysis of geometric objects to `union()` the triangles. 223 | 224 | Since it is slower, it would be better to switch to Clipper. 225 | 226 | 227 | ## CmdRoomOuterOutline 228 | 229 | I implemented the third command `CmdRoomOuterOutline` after an unsuccessful attempt at generating the outer outline of a room including its bounding elements 230 | by [specifying a list of offsets to `CreateViaOffset`](https://thebuildingcoder.typepad.com/blog/2019/12/dashboards-createviaoffset-and-room-outline-algorithms.html#3). 231 | 232 | After that failure, I suggested a number of alternative approaches 233 | to [determine the room outline including surrounding walls](https://thebuildingcoder.typepad.com/blog/2019/12/dashboards-createviaoffset-and-room-outline-algorithms.html#4). 234 | 235 | **Question:** I started to look at the possibility of tracing the outside of the walls several weeks ago, when I was at a loss utilising `CreateViaOffset`. 236 | 237 | I was finding it difficult to create the closed loop necessary, and particularly how I would achieve this were the wall thickness changes across its length. 238 | 239 | Could you point me in the right direction, possibly some sample code that I could examine and see if I could get it to work to my requirements. 240 | 241 | **Answer:** I see several possible alternative approaches avoiding the use of `CreateViaOffset`, based on: 242 | 243 | - Room boundary curves and wall thicknesses 244 | - Room boundary curves and wall bottom face edges 245 | - Projection of 3D union of room and wall solids 246 | - 2D union of room and wall footprints 247 | 248 | The most immediate and pure Revit API approach would be to get the curves representing the room boundaries, determine the wall thicknesses, offset the wall boundary curves outwards by wall thickness plus minimum offset, and ensure that everything is well connected by adding small connecting segments in the gaps where the offset jumps. 249 | 250 | Several slightly more complex pure Revit API approaches could be designed by using the wall solids instead of just offsetting the room boundary curves based on the wall thickness. For instance, we could query the wall bottom face for its edges, determine and patch together all the bits of edge segments required to go around the outside of the wall instead of the inside. 251 | 252 | Slightly more complex still, and still pure Revit API: determine the room closed shell solid, unite it with all the wall solids, and make use of the extrusion analyser to project this union vertically onto the XY plane and grab its outside edge. 253 | 254 | Finally, making use of a minimalistic yet powerful 2D Boolean operation library, perform the projection onto the XY plane first, and unite the room footprint with all its surrounding wall footprints in 2D instead. Note that the 2D Booleans are integer based. To make use of those, I convert the geometry from imperial feet units using real numbers to integer-based millimetres. 255 | 256 | The two latter approaches are both implemented in 257 | my [ElementOutline add-in](https://github.com/jeremytammik/ElementOutline). 258 | 259 | I mentioned it here in two previous threads: 260 | 261 | - [Question regarding SVG data](https://forums.autodesk.com/t5/revit-api-forum/question-regarding-svg-data-from-revit/m-p/9106146) 262 | - [How do I get the outline and stakeout path of a built-in loft family](https://forums.autodesk.com/t5/revit-api-forum/how-do-i-get-the-outline-and-stakeout-path-of-a-built-in-loft/m-p/9148138) 263 | 264 | Probably all the pure Revit API approaches will run into various problematic exceptional cases, whereas the 2D Booleans seem fast, reliable and robust and may well be able to handle all the exceptional cases that can possibly occur, so I would recommend trying that out first. 265 | 266 | I ended up implementing my suggestion in the new external command `CmdRoomOuterOutline`. 267 | 268 | It makes use of the 2D Boolean outline generation functionality implemented for Cmd2dBoolean, adding code to generate a polygon for the room boundary and unite it with all the bounding elements. 269 | 270 | It successfully handles the wall width sample model: 271 | 272 | Wall width sample loop 273 | 274 | It also gracefully handles the room separator situation: 275 | 276 | Room separator sample loop 277 | 278 | 279 | ## Author 280 | 281 | Jeremy Tammik, [The Building Coder](http://thebuildingcoder.typepad.com), [ADN](http://www.autodesk.com/adn) [Open](http://www.autodesk.com/adnopen), [Autodesk Inc.](http://www.autodesk.com) 282 | 283 | 284 | ## License 285 | 286 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). 287 | Please see the [LICENSE](LICENSE) file for full details. 288 | 289 | -------------------------------------------------------------------------------- /ElementOutline/ClipperRvt.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using Autodesk.Revit.DB; 7 | using Autodesk.Revit.DB.Architecture; 8 | using Autodesk.Revit.DB.IFC; 9 | using ClipperLib; 10 | #endregion 11 | 12 | namespace ElementOutline 13 | { 14 | using Polygon = List; 15 | using Polygons = List>; 16 | using LineSegment = Tuple; 17 | 18 | class ClipperRvt 19 | { 20 | /// 21 | /// Map Point2dInt coordinates to 22 | /// Clipper IntPoint instances. 23 | /// The purpose of this is that depending on the 24 | /// precision used by the comparison operator, 25 | /// different Point2dInt input keys may actually 26 | /// map to the same IntPoint value. 27 | /// 28 | public class VertexLookup : Dictionary 29 | { 30 | public IntPoint GetOrAdd( XYZ p ) 31 | { 32 | Point2dInt q = new Point2dInt( p ); 33 | if( !ContainsKey( q ) ) 34 | { 35 | Add( q, new IntPoint { X = q.X, Y = q.Y } ); 36 | } 37 | return this[ q ]; 38 | } 39 | } 40 | 41 | /// 42 | /// Add the 2D projection of the given mesh triangles 43 | /// to the current element outline union 44 | /// 45 | static public bool AddToUnion( 46 | Polygons union, 47 | VertexLookup vl, 48 | Clipper c, 49 | Mesh m ) 50 | { 51 | int n = m.NumTriangles; 52 | 53 | Polygons triangles = new Polygons( n ); 54 | Polygon triangle = new Polygon( 3 ); 55 | 56 | for( int i = 0; i < n; ++i ) 57 | { 58 | MeshTriangle mt = m.get_Triangle( i ); 59 | 60 | triangle.Clear(); 61 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 0 ) ) ); 62 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 1 ) ) ); 63 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 2 ) ) ); 64 | triangles.Add( triangle ); 65 | } 66 | return c.AddPaths( triangles, PolyType.ptSubject, true ); 67 | } 68 | 69 | /// 70 | /// Add the 2D projection of the given face 71 | /// to the current element outline union 72 | /// 73 | static public bool AddToUnion( 74 | Polygons union, 75 | VertexLookup vl, 76 | Clipper c, 77 | Face f ) 78 | { 79 | IList loops = f.GetEdgesAsCurveLoops(); 80 | 81 | // ExporterIFCUtils class can also be used for 82 | // non-IFC purposes. The SortCurveLoops method 83 | // sorts curve loops (edge loops) so that the 84 | // outer loops come first. 85 | 86 | IList> sortedLoops 87 | = ExporterIFCUtils.SortCurveLoops( loops ); 88 | 89 | int n = loops.Count; 90 | 91 | Debug.Assert( 0 < n, 92 | "expected at least one face loop" ); 93 | 94 | Polygons faces = new Polygons( n ); 95 | Polygon face2d = new Polygon( loops[ 0 ].NumberOfCurves() ); 96 | 97 | //foreach( IList loops2 98 | // in sortedLoops ) 99 | 100 | foreach( CurveLoop loop in loops ) 101 | { 102 | // Outer curve loops are counter-clockwise 103 | 104 | if( loop.IsCounterclockwise( XYZ.BasisZ ) ) 105 | { 106 | face2d.Clear(); 107 | 108 | foreach( Curve curve in loop ) 109 | { 110 | IList pts = curve.Tessellate(); 111 | 112 | IntPoint a = vl.GetOrAdd( pts[ 0 ] ); 113 | 114 | face2d.Add( a ); 115 | 116 | n = pts.Count; 117 | 118 | for( int i = 1; i < n; ++i ) 119 | { 120 | IntPoint b = vl.GetOrAdd( pts[ i ] ); 121 | 122 | if( b != a ) 123 | { 124 | face2d.Add( b ); 125 | a = b; 126 | } 127 | } 128 | } 129 | faces.Add( face2d ); 130 | } 131 | } 132 | return c.AddPaths( faces, PolyType.ptSubject, true ); 133 | } 134 | 135 | /// 136 | /// Add the 2D projection of the given arc 137 | /// to the current element outline union 138 | /// 139 | static public bool AddToUnion( 140 | Polygons union, 141 | VertexLookup vl, 142 | Clipper c, 143 | Arc arc ) 144 | { 145 | IList pts = arc.Tessellate(); 146 | int n = pts.Count; 147 | 148 | Polygons faces = new Polygons( 1 ); 149 | Polygon face2d = new Polygon( n ); 150 | 151 | IntPoint a = vl.GetOrAdd( pts[ 0 ] ); 152 | 153 | face2d.Add( a ); 154 | 155 | for( int i = 1; i < n; ++i ) 156 | { 157 | IntPoint b = vl.GetOrAdd( pts[ i ] ); 158 | 159 | if( b != a ) 160 | { 161 | face2d.Add( b ); 162 | a = b; 163 | } 164 | } 165 | faces.Add( face2d ); 166 | 167 | return c.AddPaths( faces, PolyType.ptSubject, true ); 168 | } 169 | 170 | /// 171 | /// Return the union of all outlines projected onto 172 | /// the XY plane from the geometry solids and meshes 173 | /// 174 | static public bool AddToUnion( 175 | Polygons union, 176 | List curves, 177 | VertexLookup vl, 178 | Clipper c, 179 | GeometryElement geoElem ) 180 | { 181 | foreach( GeometryObject obj in geoElem ) 182 | { 183 | // Curve 184 | // Edge 185 | // Face 186 | // GeometryElement 187 | // GeometryInstance 188 | // Mesh 189 | // Point 190 | // PolyLine 191 | // Profile 192 | // Solid 193 | 194 | // Skip objects that contribute no 2D surface 195 | 196 | Curve curve = obj as Curve; 197 | if( null != curve ) 198 | { 199 | Arc arc = curve as Arc; 200 | 201 | if( null != arc && arc.IsCyclic ) 202 | { 203 | AddToUnion( union, vl, c, arc ); 204 | } 205 | else if( curve.IsBound ) 206 | { 207 | curves.Add( new LineSegment( 208 | vl.GetOrAdd( curve.GetEndPoint( 0 ) ), 209 | vl.GetOrAdd( curve.GetEndPoint( 1 ) ) ) ); 210 | } 211 | continue; 212 | } 213 | 214 | Solid solid = obj as Solid; 215 | if( null != solid ) 216 | { 217 | foreach( Face f in solid.Faces ) 218 | { 219 | // Skip pretty common case: vertical planar face 220 | 221 | if( f is PlanarFace 222 | && Util.IsHorizontal( ((PlanarFace) f).FaceNormal ) ) 223 | { 224 | continue; 225 | } 226 | AddToUnion( union, vl, c, f ); 227 | } 228 | continue; 229 | } 230 | 231 | Mesh mesh = obj as Mesh; 232 | if( null != mesh ) 233 | { 234 | AddToUnion( union, vl, c, mesh ); 235 | continue; 236 | } 237 | 238 | GeometryInstance inst = obj as GeometryInstance; 239 | if( null != inst ) 240 | { 241 | GeometryElement txGeoElem 242 | = inst.GetInstanceGeometry( 243 | Transform.Identity ); // inst.Transform 244 | 245 | AddToUnion( union, curves, vl, c, txGeoElem ); 246 | continue; 247 | } 248 | Debug.Assert( false, 249 | "expected only solid, mesh or instance" ); 250 | } 251 | return true; 252 | } 253 | 254 | /// 255 | /// Return the union of the outermost room boundary 256 | /// loop projected onto the XY plane. 257 | /// 258 | static public bool AddToUnionRoom( 259 | Polygons union, 260 | List curves, 261 | VertexLookup vl, 262 | Clipper c, 263 | IList> boundary ) 264 | { 265 | int n = boundary.Count; 266 | 267 | Debug.Assert( 0 < n, 268 | "expected at least one room boundary loop" ); 269 | 270 | Polygons faces = new Polygons( n ); 271 | Polygon face2d = new Polygon( boundary[ 0 ].Count ); 272 | 273 | foreach( IList loop in boundary ) 274 | { 275 | // Outer curve loops are counter-clockwise 276 | 277 | face2d.Clear(); 278 | 279 | foreach( BoundarySegment s in loop ) 280 | { 281 | IList pts = s.GetCurve().Tessellate(); 282 | 283 | IntPoint a = vl.GetOrAdd( pts[ 0 ] ); 284 | 285 | face2d.Add( a ); 286 | 287 | n = pts.Count; 288 | 289 | for( int i = 1; i < n; ++i ) 290 | { 291 | IntPoint b = vl.GetOrAdd( pts[ i ] ); 292 | 293 | if( b != a ) 294 | { 295 | face2d.Add( b ); 296 | a = b; 297 | } 298 | } 299 | faces.Add( face2d ); 300 | } 301 | } 302 | return c.AddPaths( faces, PolyType.ptSubject, true ); 303 | } 304 | 305 | //static List GetRoomBoundaryIds( 306 | // Room room, 307 | // SpatialElementBoundaryOptions seb_opt ) 308 | //{ 309 | // List ids = null; 310 | 311 | // IList> sloops 312 | // = room.GetBoundarySegments( seb_opt ); 313 | 314 | // if( null != sloops ) // the room may not be bounded 315 | // { 316 | // Debug.Assert( 1 == sloops.Count, "this add-in " 317 | // + "currently supports only rooms with one " 318 | // + "single boundary loop" ); 319 | 320 | // ids = new List(); 321 | 322 | // foreach( IList sloop in sloops ) 323 | // { 324 | // foreach( BoundarySegment s in sloop ) 325 | // { 326 | // ids.Add( s.ElementId ); 327 | // } 328 | 329 | // // Skip out after first segement loop - ignore 330 | // // rooms with holes and disjunct parts 331 | 332 | // break; 333 | // } 334 | // } 335 | // return ids; 336 | //} 337 | 338 | /// 339 | /// Create a JtLoop representing the 2D outline of 340 | /// the given room including all its bounding elements 341 | /// by creating the inner room boundary loop and 342 | /// uniting it with the bounding elements solid faces 343 | /// and meshes in the given view, projecting 344 | /// them onto the XY plane and executing 2D Boolean 345 | /// unions on them. 346 | /// 347 | public static JtLoops GetRoomOuterBoundaryLoops( 348 | Room room, 349 | SpatialElementBoundaryOptions seb_opt, 350 | View view ) 351 | { 352 | Document doc = view.Document; 353 | 354 | Options opt = new Options 355 | { 356 | View = view 357 | }; 358 | 359 | Clipper c = new Clipper(); 360 | VertexLookup vl = new VertexLookup(); 361 | List curves = new List(); 362 | Polygons union = new Polygons(); 363 | JtLoops loops = null; 364 | 365 | IList> boundary 366 | = room.GetBoundarySegments( 367 | new SpatialElementBoundaryOptions() ); 368 | 369 | if( null != boundary ) // the room may not be bounded 370 | { 371 | Debug.Assert( 1 == boundary.Count, 372 | "this add-in currently supports only rooms " 373 | + "with one single boundary loop" ); 374 | 375 | // Ignore all loops except first, which is 376 | // hopefully outer -- and hopefully the room 377 | // does not have several disjunct parts. 378 | // Ignore holes in the room and 379 | // multiple disjunct parts. 380 | 381 | AddToUnionRoom( union, curves, vl, c, boundary ); 382 | 383 | // Retrieve bounding elements 384 | 385 | List ids = new List(); 386 | 387 | foreach( IList loop in boundary ) 388 | { 389 | foreach( BoundarySegment s in loop ) 390 | { 391 | ids.Add( s.ElementId ); 392 | } 393 | 394 | // Skip out after first segement loop - ignore 395 | // rooms with holes and disjunct parts 396 | 397 | break; 398 | } 399 | 400 | foreach( ElementId id in ids ) 401 | { 402 | // Skip invalid element ids, generated, for 403 | // instance, by a room separator line. 404 | 405 | if( !id.Equals( ElementId.InvalidElementId ) ) 406 | { 407 | Element e = doc.GetElement( id ); 408 | 409 | GeometryElement geo = e.get_Geometry( opt ); 410 | AddToUnion( union, curves, vl, c, geo ); 411 | 412 | bool succeeded = c.Execute( ClipType.ctUnion, union, 413 | PolyFillType.pftPositive, PolyFillType.pftPositive ); 414 | 415 | if( 0 == union.Count ) 416 | { 417 | Debug.Print( string.Format( 418 | "No outline found for {0} <{1}>", 419 | e.Name, e.Id.IntegerValue ) ); 420 | } 421 | } 422 | } 423 | loops = ConvertToLoops( union ); 424 | 425 | loops.NormalizeLoops(); 426 | } 427 | return loops; 428 | } 429 | 430 | 431 | /// 432 | /// Return the outer polygons defined 433 | /// by the given line segments 434 | /// 435 | /// 436 | /// 437 | Polygons CreatePolygons( List curves ) 438 | { 439 | Polygons polys = new Polygons(); 440 | IntPoint p1 = curves.Select( s => s.Item1 ).Min(); 441 | IntPoint p2 = curves.Select( s => s.Item2 ).Min(); 442 | //IntPoint p = Min( p1, p2 ); 443 | return polys; 444 | } 445 | 446 | /// 447 | /// Convert the curves to a polygon 448 | /// and add it to the union 449 | /// 450 | public bool AddToUnion( 451 | Polygons union, 452 | VertexLookup vl, 453 | Clipper c, 454 | List curves ) 455 | { 456 | Polygons polys = CreatePolygons( curves ); 457 | return c.AddPaths( polys, PolyType.ptSubject, true ); 458 | } 459 | 460 | /// 461 | /// Convert Clipper polygons to JtLoops 462 | /// 463 | static JtLoops ConvertToLoops( Polygons union ) 464 | { 465 | JtLoops loops = new JtLoops( union.Count ); 466 | JtLoop loop = new JtLoop( union.First().Count ); 467 | foreach( Polygon poly in union ) 468 | { 469 | loop.Clear(); 470 | foreach( IntPoint p in poly ) 471 | { 472 | loop.Add( new Point2dInt( 473 | (int) p.X, (int) p.Y ) ); 474 | } 475 | loops.Add( loop ); 476 | } 477 | return loops; 478 | } 479 | 480 | /// 481 | /// Create JtLoops representing the given element 482 | /// 2D outlines by retrieving the element solid 483 | /// faces and meshes in the given view, projecting 484 | /// them onto the XY plane and executing 2d Boolean 485 | /// unions on them. 486 | /// 487 | public static Dictionary 488 | GetElementLoops( 489 | View view, 490 | ICollection ids ) 491 | { 492 | Document doc = view.Document; 493 | 494 | Options opt = new Options 495 | { 496 | View = view 497 | }; 498 | 499 | Clipper c = new Clipper(); 500 | VertexLookup vl = new VertexLookup(); 501 | List curves = new List(); 502 | Polygons union = new Polygons(); 503 | Dictionary booleanLoops 504 | = new Dictionary( ids.Count ); 505 | 506 | foreach( ElementId id in ids ) 507 | { 508 | c.Clear(); 509 | vl.Clear(); 510 | union.Clear(); 511 | 512 | Element e = doc.GetElement( id ); 513 | 514 | if( e is Room ) 515 | { 516 | IList> boundary 517 | = (e as Room).GetBoundarySegments( 518 | new SpatialElementBoundaryOptions() ); 519 | 520 | // Ignore all loops except first, which is 521 | // hopefully outer -- and hopefully the room 522 | // does not have several disjunct parts. 523 | 524 | AddToUnionRoom( union, curves, vl, c, boundary ); 525 | } 526 | else 527 | { 528 | GeometryElement geo = e.get_Geometry( opt ); 529 | AddToUnion( union, curves, vl, c, geo ); 530 | } 531 | 532 | //AddToUnion( union, vl, c, curves ); 533 | 534 | //c.AddPaths( subjects, PolyType.ptSubject, true ); 535 | //c.AddPaths( clips, PolyType.ptClip, true ); 536 | 537 | bool succeeded = c.Execute( ClipType.ctUnion, union, 538 | PolyFillType.pftPositive, PolyFillType.pftPositive ); 539 | 540 | if( 0 == union.Count ) 541 | { 542 | Debug.Print( string.Format( 543 | "No outline found for {0} <{1}>", 544 | e.Name, e.Id.IntegerValue ) ); 545 | } 546 | else 547 | { 548 | JtLoops loops = ConvertToLoops( union ); 549 | 550 | loops.NormalizeLoops(); 551 | 552 | booleanLoops.Add( id.IntegerValue, loops ); 553 | } 554 | } 555 | return booleanLoops; 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /ElementOutline/Util.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Windows.Forms; 9 | using Autodesk.Revit.DB; 10 | using Autodesk.Revit.DB.Architecture; 11 | using Autodesk.Revit.UI; 12 | using Autodesk.Revit.UI.Selection; 13 | #endregion // Namespaces 14 | 15 | namespace ElementOutline 16 | { 17 | class Util 18 | { 19 | #region Output folder and export 20 | /// 21 | /// Output folder path; 22 | /// GetTempPath returns a weird GUID-named subdirectory 23 | /// created by Revit, so we will not use that, e.g., 24 | /// C:\Users\tammikj\AppData\Local\Temp\bfd59506-2dff-4b0f-bbe4-31587fcaf508 25 | /// string path = Path.GetTempPath(); 26 | /// @"C:\Users\jta\AppData\Local\Temp" 27 | /// 28 | public const string OutputFolderPath = "C:/tmp"; 29 | 30 | public static void ExportLoops( 31 | string filepath, 32 | IWin32Window owner_window, 33 | string caption, 34 | Document doc, 35 | Dictionary loops ) 36 | { 37 | Bitmap bmp = GeoSnoop.DisplayLoops( loops.Values ); 38 | 39 | GeoSnoop.DisplayImageInForm( owner_window, 40 | caption, false, bmp ); 41 | 42 | using( StreamWriter s = new StreamWriter( filepath ) ) 43 | { 44 | s.WriteLine( caption ); 45 | 46 | List keys = new List( loops.Keys ); 47 | keys.Sort(); 48 | foreach( int key in keys ) 49 | { 50 | ElementId id = new ElementId( key ); 51 | Element e = doc.GetElement( id ); 52 | 53 | s.WriteLine( 54 | "{{\"name\":\"{0}\", \"id\":\"{1}\", " 55 | + "\"uid\":\"{2}\", \"svg_path\":\"{3}\"}}", 56 | e.Name, e.Id, e.UniqueId, 57 | loops[ key ].SvgPath ); 58 | } 59 | s.Close(); 60 | } 61 | } 62 | 63 | public static void CreateOutput( 64 | string file_content, 65 | string description, 66 | Document doc, 67 | JtWindowHandle hwnd, 68 | Dictionary booleanLoops ) 69 | { 70 | string filepath = Path.Combine( Util.OutputFolderPath, 71 | doc.Title + "_" + file_content + ".json" ); 72 | 73 | string caption = doc.Title + " " + description; 74 | 75 | ExportLoops( filepath, hwnd, caption, 76 | doc, booleanLoops ); 77 | } 78 | #endregion // Output folder and export 79 | 80 | #region Element pre- or post-selection 81 | static public ICollection GetSelectedElements( 82 | UIDocument uidoc ) 83 | { 84 | // Do we have any pre-selected elements? 85 | 86 | Selection sel = uidoc.Selection; 87 | 88 | ICollection ids = sel.GetElementIds(); 89 | 90 | // If no elements were pre-selected, 91 | // prompt for post-selection 92 | 93 | if( null == ids || 0 == ids.Count ) 94 | { 95 | IList refs = null; 96 | 97 | try 98 | { 99 | refs = sel.PickObjects( ObjectType.Element, 100 | "Please select elements for 2D outline generation." ); 101 | } 102 | catch( Autodesk.Revit.Exceptions 103 | .OperationCanceledException ) 104 | { 105 | return ids; 106 | } 107 | ids = new List( 108 | refs.Select( 109 | r => r.ElementId ) ); 110 | } 111 | return ids; 112 | } 113 | 114 | /// 115 | /// Allow only room to be selected. 116 | /// 117 | class RoomSelectionFilter : ISelectionFilter 118 | { 119 | public bool AllowElement( Element e ) 120 | { 121 | return e is Room; 122 | } 123 | 124 | public bool AllowReference( Reference r, XYZ p ) 125 | { 126 | return true; 127 | } 128 | } 129 | 130 | static public IEnumerable GetSelectedRooms( 131 | UIDocument uidoc ) 132 | { 133 | Document doc = uidoc.Document; 134 | 135 | // Do we have any pre-selected elements? 136 | 137 | Selection sel = uidoc.Selection; 138 | 139 | IEnumerable ids = sel.GetElementIds() 140 | .Where( id 141 | => (doc.GetElement( id ) is Room) ); 142 | 143 | // If no elements were pre-selected, 144 | // prompt for post-selection 145 | 146 | if( null == ids || 0 == ids.Count() ) 147 | { 148 | IList refs = null; 149 | 150 | try 151 | { 152 | refs = sel.PickObjects( ObjectType.Element, 153 | new RoomSelectionFilter(), 154 | "Please select rooms for 2D outline generation." ); 155 | } 156 | catch( Autodesk.Revit.Exceptions 157 | .OperationCanceledException ) 158 | { 159 | return ids; 160 | } 161 | ids = new List( 162 | refs.Select( 163 | r => r.ElementId ) ); 164 | } 165 | return ids; 166 | } 167 | #endregion // Element pre- or post-selection 168 | 169 | #region Geometrical comparison 170 | const double _eps = 1.0e-9; 171 | 172 | public static bool IsZero( 173 | double a, 174 | double tolerance ) 175 | { 176 | return tolerance > Math.Abs( a ); 177 | } 178 | 179 | public static bool IsZero( double a ) 180 | { 181 | return IsZero( a, _eps ); 182 | } 183 | 184 | public static bool IsEqual( double a, double b ) 185 | { 186 | return IsZero( b - a ); 187 | } 188 | 189 | public static bool IsHorizontal( XYZ v ) 190 | { 191 | return IsZero( v.Z ); 192 | } 193 | #endregion // Geometrical comparison 194 | 195 | #region Unit conversion 196 | const double _feet_to_mm = 25.4 * 12; 197 | 198 | public static int ConvertFeetToMillimetres( 199 | double d ) 200 | { 201 | //return (int) ( _feet_to_mm * d + 0.5 ); 202 | return (int) Math.Round( _feet_to_mm * d, 203 | MidpointRounding.AwayFromZero ); 204 | } 205 | 206 | public static double ConvertMillimetresToFeet( int d ) 207 | { 208 | return d / _feet_to_mm; 209 | } 210 | 211 | const double _radians_to_degrees = 180.0 / Math.PI; 212 | 213 | public static double ConvertDegreesToRadians( int d ) 214 | { 215 | return d * Math.PI / 180.0; 216 | } 217 | 218 | public static int ConvertRadiansToDegrees( 219 | double d ) 220 | { 221 | //return (int) ( _radians_to_degrees * d + 0.5 ); 222 | return (int) Math.Round( _radians_to_degrees * d, 223 | MidpointRounding.AwayFromZero ); 224 | } 225 | 226 | /// 227 | /// Return true if the type b is either a 228 | /// subclass of OR equal to the base class itself. 229 | /// IsSubclassOf returns false if the two types 230 | /// are the same. It only returns true for true 231 | /// non-equal subclasses. 232 | /// 233 | public static bool IsSameOrSubclassOf( 234 | Type a, 235 | Type b ) 236 | { 237 | // http://stackoverflow.com/questions/2742276/in-c-how-do-i-check-if-a-type-is-a-subtype-or-the-type-of-an-object 238 | 239 | return a.IsSubclassOf( b ) || a == b; 240 | } 241 | #endregion // Unit conversion 242 | 243 | #region Formatting 244 | /// 245 | /// Uncapitalise string, i.e. 246 | /// lowercase its first character. 247 | /// 248 | public static string Uncapitalise( string s ) 249 | { 250 | return Char.ToLowerInvariant( s[0] ) 251 | + s.Substring( 1 ); 252 | } 253 | 254 | /// 255 | /// Return an English plural suffix for the given 256 | /// number of items, i.e. 's' for zero or more 257 | /// than one, and nothing for exactly one. 258 | /// 259 | public static string PluralSuffix( int n ) 260 | { 261 | return 1 == n ? "" : "s"; 262 | } 263 | 264 | /// 265 | /// Return an English plural suffix 'ies' or 266 | /// 'y' for the given number of items. 267 | /// 268 | public static string PluralSuffixY( int n ) 269 | { 270 | return 1 == n ? "y" : "ies"; 271 | } 272 | 273 | /// 274 | /// Return an English pluralised string for the 275 | /// given thing or things. If the thing ends with 276 | /// 'y', the plural is assumes to end with 'ies', 277 | /// e.g. 278 | /// (2, 'chair') -- '2 chairs' 279 | /// (2, 'property') -- '2 properties' 280 | /// (2, 'furniture item') -- '2 furniture items' 281 | /// If in doubt, appending 'item' or 'entry' to 282 | /// the thing description is normally a pretty 283 | /// safe bet. Replaces calls to PluralSuffix 284 | /// and PluralSuffixY. 285 | /// 286 | public static string PluralString( 287 | int n, 288 | string thing ) 289 | { 290 | if( 1 == n ) 291 | { 292 | return "1 " + thing; 293 | } 294 | 295 | int i = thing.Length - 1; 296 | char cy = thing[i]; 297 | 298 | return n.ToString() + " " + ( ( 'y' == cy ) 299 | ? thing.Substring( 0, i ) + "ies" 300 | : thing + "s" ); 301 | } 302 | 303 | /// 304 | /// Return a dot (full stop) for zero 305 | /// or a colon for more than zero. 306 | /// 307 | public static string DotOrColon( int n ) 308 | { 309 | return 0 < n ? ":" : "."; 310 | } 311 | 312 | /// 313 | /// Return a string for a real number 314 | /// formatted to two decimal places. 315 | /// 316 | public static string RealString( double a ) 317 | { 318 | return a.ToString( "0.##" ); 319 | } 320 | 321 | /// 322 | /// Return a string representation in degrees 323 | /// for an angle given in radians. 324 | /// 325 | public static string AngleString( double angle ) 326 | { 327 | return RealString( angle * 180 / Math.PI ) + " degrees"; 328 | } 329 | 330 | /// 331 | /// Return a string for a UV point 332 | /// or vector with its coordinates 333 | /// formatted to two decimal places. 334 | /// 335 | public static string PointString( UV p ) 336 | { 337 | return string.Format( "({0},{1})", 338 | RealString( p.U ), 339 | RealString( p.V ) ); 340 | } 341 | 342 | /// 343 | /// Return a string for an XYZ 344 | /// point or vector with its coordinates 345 | /// formatted to two decimal places. 346 | /// 347 | public static string PointString( XYZ p ) 348 | { 349 | return string.Format( "({0},{1},{2})", 350 | RealString( p.X ), 351 | RealString( p.Y ), 352 | RealString( p.Z ) ); 353 | } 354 | 355 | /// 356 | /// Return a string for the XY values of an XYZ 357 | /// point or vector with its coordinates 358 | /// formatted to two decimal places. 359 | /// 360 | public static string PointString2d( XYZ p ) 361 | { 362 | return string.Format( "({0},{1})", 363 | RealString( p.X ), 364 | RealString( p.Y ) ); 365 | } 366 | 367 | /// 368 | /// Return a string displaying the two XYZ 369 | /// endpoints of a geometry curve element. 370 | /// 371 | public static string CurveEndpointString( Curve c ) 372 | { 373 | return string.Format( "({0},{1})", 374 | PointString2d( c.GetEndPoint( 0 ) ), 375 | PointString2d( c.GetEndPoint( 1 ) ) ); 376 | } 377 | 378 | /// 379 | /// Return a string displaying only the XY values 380 | /// of the two XYZ endpoints of a geometry curve 381 | /// element. 382 | /// 383 | public static string CurveEndpointString2d( Curve c ) 384 | { 385 | return string.Format( "({0},{1})", 386 | PointString( c.GetEndPoint( 0 ) ), 387 | PointString( c.GetEndPoint( 1 ) ) ); 388 | } 389 | 390 | /// 391 | /// Return a string for a 2D bounding box 392 | /// formatted to two decimal places. 393 | /// 394 | public static string BoundingBoxString( 395 | BoundingBoxUV b ) 396 | { 397 | //UV d = b.Max - b.Min; 398 | 399 | return string.Format( "({0},{1})", 400 | PointString( b.Min ), 401 | PointString( b.Max ) ); 402 | } 403 | 404 | /// 405 | /// Return a string for a 3D bounding box 406 | /// formatted to two decimal places. 407 | /// 408 | public static string BoundingBoxString( 409 | BoundingBoxXYZ b ) 410 | { 411 | //XYZ d = b.Max - b.Min; 412 | 413 | return string.Format( "({0},{1})", 414 | PointString( b.Min ), 415 | PointString( b.Max ) ); 416 | } 417 | 418 | /// 419 | /// Return a string for an Outline 420 | /// formatted to two decimal places. 421 | /// 422 | public static string OutlineString( Outline o ) 423 | { 424 | //XYZ d = o.MaximumPoint - o.MinimumPoint; 425 | 426 | return string.Format( "({0},{1})", 427 | PointString( o.MinimumPoint ), 428 | PointString( o.MaximumPoint ) ); 429 | } 430 | #endregion // Formatting 431 | 432 | #region Element properties 433 | /// 434 | /// Return a string describing the given element: 435 | /// .NET type name, 436 | /// category name, 437 | /// family and symbol name for a family instance, 438 | /// element id and element name. 439 | /// 440 | public static string ElementDescription( 441 | Element e ) 442 | { 443 | if( null == e ) 444 | { 445 | return ""; 446 | } 447 | 448 | // For a wall, the element name equals the 449 | // wall type name, which is equivalent to the 450 | // family name ... 451 | 452 | FamilyInstance fi = e as FamilyInstance; 453 | 454 | string typeName = e.GetType().Name; 455 | 456 | string categoryName = ( null == e.Category ) 457 | ? string.Empty 458 | : e.Category.Name + " "; 459 | 460 | string familyName = ( null == fi ) 461 | ? string.Empty 462 | : fi.Symbol.Family.Name + " "; 463 | 464 | string symbolName = ( null == fi 465 | || e.Name.Equals( fi.Symbol.Name ) ) 466 | ? string.Empty 467 | : fi.Symbol.Name + " "; 468 | 469 | return string.Format( "{0} {1}{2}{3}<{4} {5}>", 470 | typeName, categoryName, familyName, 471 | symbolName, e.Id.IntegerValue, e.Name ); 472 | } 473 | 474 | /// 475 | /// Return a string describing the given sheet: 476 | /// sheet number and name. 477 | /// 478 | public static string SheetDescription( 479 | Element e ) 480 | { 481 | string sheet_number = e.get_Parameter( 482 | BuiltInParameter.SHEET_NUMBER ) 483 | .AsString(); 484 | 485 | return string.Format( "{0} - {1}", 486 | sheet_number, e.Name ); 487 | } 488 | 489 | /// 490 | /// Return a dictionary of all the given 491 | /// element parameter names and values. 492 | /// 493 | public static bool IsModifiable( Parameter p ) 494 | { 495 | StorageType st = p.StorageType; 496 | 497 | return !( p.IsReadOnly ) 498 | // && p.UserModifiable // ignore this 499 | && ( ( StorageType.Integer == st ) 500 | || ( StorageType.String == st ) ); 501 | } 502 | 503 | /// 504 | /// Return a dictionary of all the given 505 | /// element parameter names and values. 506 | /// 507 | public static Dictionary 508 | GetElementProperties( 509 | Element e ) 510 | { 511 | IList parameters 512 | = e.GetOrderedParameters(); 513 | 514 | Dictionary a 515 | = new Dictionary( 516 | parameters.Count ); 517 | 518 | StorageType st; 519 | string s; 520 | 521 | foreach( Parameter p in parameters ) 522 | { 523 | st = p.StorageType; 524 | 525 | s = string.Format( "{0} {1}", 526 | ( IsModifiable( p ) ? "w" : "r" ), 527 | ( StorageType.String == st 528 | ? p.AsString() 529 | : p.AsInteger().ToString() ) ); 530 | 531 | a.Add( p.Definition.Name, s ); 532 | } 533 | return a; 534 | } 535 | #endregion // Element properties 536 | 537 | #region Messages 538 | /// 539 | /// Display a short big message. 540 | /// 541 | public static void InfoMsg( string msg ) 542 | { 543 | Debug.Print( msg ); 544 | TaskDialog.Show( App.Caption, msg ); 545 | } 546 | 547 | /// 548 | /// Display a longer message in smaller font. 549 | /// 550 | public static void InfoMsg2( 551 | string instruction, 552 | string msg, 553 | bool prompt = true ) 554 | { 555 | Debug.Print( "{0}: {1}", instruction, msg ); 556 | if( prompt ) 557 | { 558 | TaskDialog dlg = new TaskDialog( App.Caption ); 559 | dlg.MainInstruction = instruction; 560 | dlg.MainContent = msg; 561 | dlg.Show(); 562 | } 563 | } 564 | 565 | /// 566 | /// Display an error message. 567 | /// 568 | public static void ErrorMsg( string msg ) 569 | { 570 | Debug.Print( msg ); 571 | TaskDialog dlg = new TaskDialog( App.Caption ); 572 | dlg.MainIcon = TaskDialogIcon.TaskDialogIconWarning; 573 | dlg.MainInstruction = msg; 574 | dlg.Show(); 575 | } 576 | 577 | /// 578 | /// Print a debug log message with a time stamp 579 | /// to the Visual Studio debug output window. 580 | /// 581 | public static void Log( string msg ) 582 | { 583 | string timestamp = DateTime.Now.ToString( 584 | "HH:mm:ss.fff" ); 585 | 586 | Debug.Print( timestamp + " " + msg ); 587 | } 588 | #endregion // Messages 589 | 590 | #region Browse for directory 591 | public static bool BrowseDirectory( 592 | ref string path, 593 | bool allowCreate ) 594 | { 595 | FolderBrowserDialog browseDlg 596 | = new FolderBrowserDialog(); 597 | 598 | browseDlg.SelectedPath = path; 599 | browseDlg.ShowNewFolderButton = allowCreate; 600 | 601 | bool rc = ( DialogResult.OK 602 | == browseDlg.ShowDialog() ); 603 | 604 | if( rc ) 605 | { 606 | path = browseDlg.SelectedPath; 607 | } 608 | return rc; 609 | } 610 | #endregion // Browse for directory 611 | 612 | #region Flip SVG Y coordinates 613 | public static bool SvgFlip = true; 614 | 615 | /// 616 | /// Flip Y coordinate for SVG export. 617 | /// 618 | public static int SvgFlipY( int y ) 619 | { 620 | return SvgFlip ? -y : y; 621 | } 622 | #endregion // Flip SVG Y coordinates 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /ElementOutline/CmdUploadRooms.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using Autodesk.Revit.ApplicationServices; 7 | using Autodesk.Revit.Attributes; 8 | using Autodesk.Revit.DB; 9 | using Autodesk.Revit.DB.Architecture; 10 | using Autodesk.Revit.DB.IFC; 11 | using Autodesk.Revit.UI; 12 | using Autodesk.Revit.UI.Selection; 13 | using Bitmap = System.Drawing.Bitmap; 14 | using BoundarySegment = Autodesk.Revit.DB.BoundarySegment; 15 | //using ComponentManager = Autodesk.Windows.ComponentManager; pre-2020 16 | using IWin32Window = System.Windows.Forms.IWin32Window; 17 | //using DreamSeat; 18 | #endregion 19 | 20 | namespace ElementOutline 21 | { 22 | [Transaction( TransactionMode.ReadOnly )] 23 | internal class CmdUploadRooms : IExternalCommand 24 | { 25 | #region RoomSelectionFilter 26 | class RoomSelectionFilter : ISelectionFilter 27 | { 28 | public bool AllowElement( Element e ) 29 | { 30 | return e is Room; 31 | } 32 | 33 | public bool AllowReference( Reference r, XYZ p ) 34 | { 35 | return true; 36 | } 37 | } 38 | #endregion // RoomSelectionFilter 39 | 40 | static bool _debug_output = false; 41 | 42 | /// 43 | /// If curve tessellation is disabled, only 44 | /// straight line segments from start to end 45 | /// point are exported. 46 | /// 47 | static bool _tessellate_curves = true; 48 | 49 | /// 50 | /// Never tessellate a curve 51 | /// shorter than this length. 52 | /// 53 | const double _min_tessellation_curve_length_in_feet = 0.2; 54 | 55 | /// 56 | /// Conversion factor from foot to quarter inch. 57 | /// 58 | const double _quarter_inch = 1.0 / (12 * 4); 59 | 60 | #region Get room boundary loops 61 | /// 62 | /// Retrieve the room plan view boundary 63 | /// polygon loops and convert to 2D integer-based. 64 | /// For optimisation and consistency reasons, 65 | /// convert all coordinates to integer values in 66 | /// millimetres. Revit precision is limited to 67 | /// 1/16 of an inch, which is abaut 1.2 mm, anyway. 68 | /// 69 | static JtLoops GetRoomLoops( Room room ) 70 | { 71 | SpatialElementBoundaryOptions opt 72 | = new SpatialElementBoundaryOptions(); 73 | 74 | opt.SpatialElementBoundaryLocation = 75 | SpatialElementBoundaryLocation.Center; // loops closed 76 | //SpatialElementBoundaryLocation.Finish; // loops not closed 77 | 78 | IList> loops = room. 79 | GetBoundarySegments( opt ); 80 | 81 | int nLoops = loops.Count; 82 | 83 | JtLoops jtloops = new JtLoops( nLoops ); 84 | 85 | foreach( IList loop in loops ) 86 | { 87 | int nSegments = loop.Count; 88 | 89 | JtLoop jtloop = new JtLoop( nSegments ); 90 | 91 | XYZ p0 = null; // loop start point 92 | XYZ p; // segment start point 93 | XYZ q = null; // segment end point 94 | 95 | foreach( BoundarySegment seg in loop ) 96 | { 97 | // Todo: handle non-linear curve. 98 | // Especially: if two long lines have a 99 | // short arc in between them, skip the arc 100 | // and extend both lines. 101 | 102 | p = seg.GetCurve().GetEndPoint( 0 ); 103 | 104 | jtloop.Add( new Point2dInt( p ) ); 105 | 106 | Debug.Assert( null == q || q.IsAlmostEqualTo( p ), 107 | "expected last endpoint to equal current start point" ); 108 | 109 | q = seg.GetCurve().GetEndPoint( 1 ); 110 | 111 | if( _debug_output ) 112 | { 113 | Debug.Print( "{0} --> {1}", 114 | Util.PointString( p ), 115 | Util.PointString( q ) ); 116 | } 117 | if( null == p0 ) 118 | { 119 | p0 = p; // save loop start point 120 | } 121 | } 122 | Debug.Assert( q.IsAlmostEqualTo( p0 ), 123 | "expected last endpoint to equal loop start point" ); 124 | 125 | jtloops.Add( jtloop ); 126 | } 127 | return jtloops; 128 | } 129 | 130 | //(9.03,10.13,0) --> (-14.59,10.13,0) 131 | //(-14.59,10.13,0) --> (-14.59,1.93,0) 132 | //(-14.59,1.93,0) --> (-2.45,1.93,0) 133 | //(-2.45,1.93,0) --> (-2.45,-3.98,0) 134 | //(-2.45,-3.98,0) --> (9.03,-3.98,0) 135 | //(9.03,-3.98,0) --> (9.03,10.13,0) 136 | //(0.98,-0.37,0) --> (0.98,1.93,0) 137 | //(0.98,1.93,0) --> (5.57,1.93,0) 138 | //(5.57,1.93,0) --> (5.57,-0.37,0) 139 | //(5.57,-0.37,0) --> (0.98,-0.37,0) 140 | 141 | //(9.03,10.13) --> (-14.59,10.13) 142 | //(-14.59,10.13) --> (-14.59,1.93) 143 | //(-14.59,1.93) --> (-2.45,1.93) 144 | //(-2.45,1.93) --> (-2.45,-3.98) 145 | //(-2.45,-3.98) --> (9.03,-3.98) 146 | //(9.03,-3.98) --> (9.03,10.13) 147 | //(0.98,-0.37) --> (0.98,1.93) 148 | //(0.98,1.93) --> (5.57,1.93) 149 | //(5.57,1.93) --> (5.57,-0.37) 150 | //(5.57,-0.37) --> (0.98,-0.37) 151 | 152 | //Room Rooms <212639 Room 1> has 2 loops: 153 | // 0: (2753,3087), (-4446,3087), (-4446,587), (-746,587), (-746,-1212), (2753,-1212) 154 | // 1: (298,-112), (298,587), (1698,587), (1698,-112) 155 | #endregion // Get room boundary loops 156 | 157 | #region Get furniture contained in given room 158 | /// 159 | /// Return the element ids of all furniture and 160 | /// equipment family instances contained in the 161 | /// given room. 162 | /// 163 | static List GetFurniture( Room room ) 164 | { 165 | BoundingBoxXYZ bb = room.get_BoundingBox( null ); 166 | 167 | Outline outline = new Outline( bb.Min, bb.Max ); 168 | 169 | BoundingBoxIntersectsFilter filter 170 | = new BoundingBoxIntersectsFilter( outline ); 171 | 172 | Document doc = room.Document; 173 | 174 | // Todo: add category filters and other 175 | // properties to narrow down the results 176 | 177 | // what categories of family instances 178 | // are we interested in? 179 | 180 | BuiltInCategory[] bics = new BuiltInCategory[] { 181 | BuiltInCategory.OST_Furniture, 182 | BuiltInCategory.OST_PlumbingFixtures, 183 | BuiltInCategory.OST_SpecialityEquipment 184 | }; 185 | 186 | LogicalOrFilter categoryFilter 187 | = new LogicalOrFilter( bics 188 | .Select( 189 | bic => new ElementCategoryFilter( bic ) ) 190 | .ToList() ); 191 | 192 | FilteredElementCollector familyInstances 193 | = new FilteredElementCollector( doc ) 194 | .WhereElementIsNotElementType() 195 | .WhereElementIsViewIndependent() 196 | .OfClass( typeof( FamilyInstance ) ) 197 | .WherePasses( categoryFilter ) 198 | .WherePasses( filter ); 199 | 200 | int roomid = room.Id.IntegerValue; 201 | 202 | List a = new List(); 203 | 204 | foreach( FamilyInstance fi in familyInstances ) 205 | { 206 | if( null != fi.Room 207 | && fi.Room.Id.IntegerValue.Equals( roomid ) ) 208 | { 209 | Debug.Assert( fi.Location is LocationPoint, 210 | "expected all furniture to have a location point" ); 211 | 212 | a.Add( fi ); 213 | } 214 | } 215 | return a; 216 | } 217 | #endregion // Get furniture contained in given room 218 | 219 | /// 220 | /// Return a closed loop of integer-based points 221 | /// scaled to millimetres from a given Revit model 222 | /// face in feet. 223 | /// 224 | internal static JtLoop GetLoop( 225 | Autodesk.Revit.Creation.Application creapp, 226 | Face face ) 227 | { 228 | JtLoop loop = null; 229 | 230 | foreach( EdgeArray a in face.EdgeLoops ) 231 | { 232 | int nEdges = a.Size; 233 | 234 | List curves 235 | = new List( nEdges ); 236 | 237 | XYZ p0 = null; // loop start point 238 | XYZ p; // edge start point 239 | XYZ q = null; // edge end point 240 | 241 | // Test ValidateCurveLoops 242 | 243 | //CurveLoop loopIfc = new CurveLoop(); 244 | 245 | foreach( Edge e in a ) 246 | { 247 | // This requires post-processing using 248 | // SortCurvesContiguous: 249 | 250 | Curve curve = e.AsCurve(); 251 | 252 | if( _debug_output ) 253 | { 254 | p = curve.GetEndPoint( 0 ); 255 | q = curve.GetEndPoint( 1 ); 256 | Debug.Print( "{0} --> {1}", 257 | Util.PointString( p ), 258 | Util.PointString( q ) ); 259 | } 260 | 261 | // This returns the curves already 262 | // correctly oriented: 263 | 264 | curve = e.AsCurveFollowingFace( 265 | face ); 266 | 267 | if( _debug_output ) 268 | { 269 | p = curve.GetEndPoint( 0 ); 270 | q = curve.GetEndPoint( 1 ); 271 | Debug.Print( "{0} --> {1} following face", 272 | Util.PointString( p ), 273 | Util.PointString( q ) ); 274 | } 275 | 276 | curves.Add( curve ); 277 | 278 | // Throws an exception saying "This curve 279 | // will make the loop not contiguous. 280 | // Parameter name: pCurve" 281 | 282 | //loopIfc.Append( curve ); 283 | } 284 | 285 | // We never reach this point: 286 | 287 | //List loopsIfc 288 | // = new List( 1 ); 289 | 290 | //loopsIfc.Add( loopIfc ); 291 | 292 | //IList loopsIfcOut = ExporterIFCUtils 293 | // .ValidateCurveLoops( loopsIfc, XYZ.BasisZ ); 294 | 295 | // This is no longer needed if we use 296 | // AsCurveFollowingFace instead of AsCurve: 297 | 298 | CurveUtils.SortCurvesContiguous( 299 | creapp, curves, _debug_output ); 300 | 301 | q = null; 302 | 303 | loop = new JtLoop( nEdges ); 304 | 305 | foreach( Curve curve in curves ) 306 | { 307 | // Todo: handle non-linear curve. 308 | // Especially: if two long lines have a 309 | // short arc in between them, skip the arc 310 | // and extend both lines. 311 | 312 | p = curve.GetEndPoint( 0 ); 313 | 314 | Debug.Assert( null == q 315 | || q.IsAlmostEqualTo( p, 1e-04 ), 316 | string.Format( 317 | "expected last endpoint to equal current start point, not distance {0}", 318 | ( null == q ? 0 : p.DistanceTo( q ) ) ) ); 319 | 320 | q = curve.GetEndPoint( 1 ); 321 | 322 | if( _debug_output ) 323 | { 324 | Debug.Print( "{0} --> {1}", 325 | Util.PointString( p ), 326 | Util.PointString( q ) ); 327 | } 328 | 329 | if( null == p0 ) 330 | { 331 | p0 = p; // save loop start point 332 | } 333 | 334 | int n = -1; 335 | 336 | if( _tessellate_curves 337 | && _min_tessellation_curve_length_in_feet 338 | < q.DistanceTo( p ) ) 339 | { 340 | IList pts = curve.Tessellate(); 341 | n = pts.Count; 342 | 343 | Debug.Assert( 1 < n, "expected at least two points" ); 344 | Debug.Assert( p.IsAlmostEqualTo( pts[0] ), "expected tessellation start equal curve start point" ); 345 | Debug.Assert( q.IsAlmostEqualTo( pts[n - 1] ), "expected tessellation end equal curve end point" ); 346 | 347 | if( 2 == n ) 348 | { 349 | n = -1; // this is a straight line 350 | } 351 | else 352 | { 353 | --n; // skip last point 354 | 355 | for( int i = 0; i < n; ++i ) 356 | { 357 | loop.Add( new Point2dInt( pts[i] ) ); 358 | } 359 | } 360 | } 361 | 362 | // If tessellation is disabled, 363 | // or curve is too short to tessellate, 364 | // or has only two tessellation points, 365 | // just add the start point: 366 | 367 | if( -1 == n ) 368 | { 369 | loop.Add( new Point2dInt( p ) ); 370 | } 371 | } 372 | Debug.Assert( q.IsAlmostEqualTo( p0, 1e-05 ), 373 | string.Format( 374 | "expected last endpoint to equal current start point, not distance {0}", 375 | p0.DistanceTo( q ) ) ); 376 | } 377 | return loop; 378 | } 379 | 380 | /// 381 | /// Add all plan view boundary loops from 382 | /// given solid to the list of loops. 383 | /// The creation application argument is used to 384 | /// reverse the extrusion analyser output curves 385 | /// in case they are badly oriented. 386 | /// 387 | /// Number of loops added 388 | static int AddLoops( 389 | Autodesk.Revit.Creation.Application creapp, 390 | JtLoops loops, 391 | GeometryObject obj, 392 | ref int nExtrusionAnalysisFailures ) 393 | { 394 | int nAdded = 0; 395 | 396 | Solid solid = obj as Solid; 397 | 398 | if( null != solid 399 | && 0 < solid.Faces.Size ) 400 | { 401 | //Plane plane = new Plane(XYZ.BasisX, 402 | // XYZ.BasisY, XYZ.Zero); // 2016 403 | 404 | Plane plane = Plane.CreateByOriginAndBasis( 405 | XYZ.Zero, XYZ.BasisX, XYZ.BasisY ); // 2017 406 | 407 | ExtrusionAnalyzer extrusionAnalyzer = null; 408 | 409 | try 410 | { 411 | extrusionAnalyzer = ExtrusionAnalyzer.Create( 412 | solid, plane, XYZ.BasisZ ); 413 | } 414 | catch( Autodesk.Revit.Exceptions 415 | .InvalidOperationException ) 416 | { 417 | ++nExtrusionAnalysisFailures; 418 | return nAdded; 419 | } 420 | 421 | Face face = extrusionAnalyzer 422 | .GetExtrusionBase(); 423 | 424 | loops.Add( GetLoop( creapp, face ) ); 425 | 426 | ++nAdded; 427 | } 428 | return nAdded; 429 | } 430 | 431 | #region Obsolete GetPlanViewBoundaryLoopsMultiple 432 | /// 433 | /// Retrieve all plan view boundary loops from 434 | /// all solids of given element. This initial 435 | /// version passes each solid encountered in the 436 | /// given element to the ExtrusionAnalyzer one 437 | /// at a time, which obviously results in multiple 438 | /// loops, many of which are contained within the 439 | /// others. An updated version unites all the 440 | /// solids first and then uses the ExtrusionAnalyzer 441 | /// once only to obtain the true outside shadow 442 | /// contour. 443 | /// 444 | static JtLoops GetPlanViewBoundaryLoopsMultiple( 445 | Element e, 446 | ref int nFailures ) 447 | { 448 | Autodesk.Revit.Creation.Application creapp 449 | = e.Document.Application.Create; 450 | 451 | JtLoops loops = new JtLoops( 1 ); 452 | 453 | //int nSolids = 0; 454 | 455 | Options opt = new Options(); 456 | 457 | GeometryElement geo = e.get_Geometry( opt ); 458 | 459 | if( null != geo ) 460 | { 461 | Document doc = e.Document; 462 | 463 | if( e is FamilyInstance ) 464 | { 465 | geo = geo.GetTransformed( 466 | Transform.Identity ); 467 | } 468 | 469 | //GeometryInstance inst = null; 470 | 471 | foreach( GeometryObject obj in geo ) 472 | { 473 | AddLoops( creapp, loops, obj, ref nFailures ); 474 | 475 | //inst = obj as GeometryInstance; 476 | } 477 | 478 | //if( 0 == nSolids && null != inst ) 479 | //{ 480 | // geo = inst.GetSymbolGeometry(); 481 | 482 | // foreach( GeometryObject obj in geo ) 483 | // { 484 | // AddLoops( creapp, loops, obj, ref nFailures ); 485 | // } 486 | //} 487 | } 488 | return loops; 489 | } 490 | #endregion // Obsolete GetPlanViewBoundaryLoopsMultiple 491 | 492 | /// 493 | /// Retrieve all plan view boundary loops from 494 | /// all solids of the given element geometry 495 | /// united together. 496 | /// 497 | internal static JtLoops GetPlanViewBoundaryLoopsGeo( 498 | Autodesk.Revit.Creation.Application creapp, 499 | GeometryElement geo, 500 | ref int nFailures ) 501 | { 502 | Solid union = null; 503 | 504 | Plane plane = Plane.CreateByOriginAndBasis( 505 | XYZ.Zero, XYZ.BasisX, XYZ.BasisY ); 506 | 507 | foreach( GeometryObject obj in geo ) 508 | { 509 | Solid solid = obj as Solid; 510 | 511 | if( null != solid 512 | && 0 < solid.Faces.Size ) 513 | { 514 | // Some solids, e.g. in the standard 515 | // content 'Furniture Chair - Office' 516 | // cause an extrusion analyser failure, 517 | // so skip adding those. 518 | 519 | try 520 | { 521 | ExtrusionAnalyzer extrusionAnalyzer 522 | = ExtrusionAnalyzer.Create( 523 | solid, plane, XYZ.BasisZ ); 524 | } 525 | catch( Autodesk.Revit.Exceptions 526 | .InvalidOperationException ) 527 | { 528 | solid = null; 529 | ++nFailures; 530 | } 531 | 532 | if( null != solid ) 533 | { 534 | if( null == union ) 535 | { 536 | union = solid; 537 | } 538 | else 539 | { 540 | try 541 | { 542 | union = BooleanOperationsUtils 543 | .ExecuteBooleanOperation( union, solid, 544 | BooleanOperationsType.Union ); 545 | } 546 | catch( Autodesk.Revit.Exceptions 547 | .InvalidOperationException ) 548 | { 549 | ++nFailures; 550 | } 551 | } 552 | } 553 | } 554 | } 555 | 556 | JtLoops loops = new JtLoops( 1 ); 557 | 558 | AddLoops( creapp, loops, union, ref nFailures ); 559 | 560 | return loops; 561 | } 562 | 563 | /// 564 | /// Retrieve all plan view boundary loops from 565 | /// all solids of given element united together. 566 | /// If the element is a family instance, transform 567 | /// its loops from the instance placement 568 | /// coordinate system back to the symbol 569 | /// definition one. 570 | /// If no geometry can be determined, use the 571 | /// bounding box instead. 572 | /// 573 | internal static JtLoops GetSolidPlanViewBoundaryLoops( 574 | Element e, 575 | bool transformInstanceCoordsToSymbolCoords, 576 | ref int nFailures ) 577 | { 578 | Autodesk.Revit.Creation.Application creapp 579 | = e.Document.Application.Create; 580 | 581 | JtLoops loops = null; 582 | 583 | Options opt = new Options(); 584 | 585 | GeometryElement geo = e.get_Geometry( opt ); 586 | 587 | if( null != geo ) 588 | { 589 | Document doc = e.Document; 590 | 591 | if( e is FamilyInstance ) 592 | { 593 | if( transformInstanceCoordsToSymbolCoords ) 594 | { 595 | // Retrieve family instance geometry 596 | // transformed back to symbol definition 597 | // coordinate space by inverting the 598 | // family instance placement transformation 599 | 600 | LocationPoint lp = e.Location 601 | as LocationPoint; 602 | 603 | Transform t = Transform.CreateTranslation( 604 | -lp.Point ); 605 | 606 | Transform r = Transform.CreateRotationAtPoint( 607 | XYZ.BasisZ, -lp.Rotation, lp.Point ); 608 | 609 | geo = geo.GetTransformed( t * r ); 610 | } 611 | else 612 | { 613 | Debug.Assert( 614 | 1 == geo.Count(), 615 | "expected as single geometry instance" ); 616 | 617 | Debug.Assert( 618 | geo.First() is GeometryInstance, 619 | "expected as single geometry instance" ); 620 | 621 | geo = ( geo.First() 622 | as GeometryInstance ).GetInstanceGeometry(); 623 | } 624 | } 625 | 626 | loops = GetPlanViewBoundaryLoopsGeo( 627 | creapp, geo, ref nFailures ); 628 | } 629 | if( null == loops || 0 == loops.Count ) 630 | { 631 | Debug.Print( 632 | "Unable to determine geometry for " 633 | + Util.ElementDescription( e ) 634 | + "; using bounding box instead." ); 635 | 636 | BoundingBoxXYZ bb; 637 | 638 | if( e is FamilyInstance ) 639 | { 640 | bb = ( e as FamilyInstance ).Symbol 641 | .get_BoundingBox( null ); 642 | } 643 | else 644 | { 645 | bb = e.get_BoundingBox( null ); 646 | } 647 | JtLoop loop = new JtLoop( 4 ); 648 | loop.Add( new Point2dInt( bb.Min ) ); 649 | loop.Add( new Point2dInt( bb.Max.X, bb.Min.Y ) ); 650 | loop.Add( new Point2dInt( bb.Max ) ); 651 | loop.Add( new Point2dInt( bb.Min.X, bb.Max.Y ) ); 652 | loops.Add( loop ); 653 | } 654 | return loops; 655 | } 656 | 657 | /// 658 | /// List all the loops retrieved 659 | /// from the given element. 660 | /// 661 | internal static void ListLoops( Element e, JtLoops loops ) 662 | { 663 | int nLoops = loops.Count; 664 | 665 | Debug.Print( "{0} has {1}{2}", 666 | Util.ElementDescription( e ), 667 | Util.PluralString( nLoops, "loop" ), 668 | Util.DotOrColon( nLoops ) ); 669 | 670 | int i = 0; 671 | 672 | foreach( JtLoop loop in loops ) 673 | { 674 | Debug.Print( " {0}: {1}", i++, 675 | loop.ToString() ); 676 | } 677 | } 678 | 679 | /// 680 | /// Upload the selected rooms and the furniture 681 | /// they contain to the cloud database. 682 | /// 683 | public static void UploadRoom( 684 | IntPtr hwnd, 685 | Document doc, 686 | Room room ) 687 | { 688 | BoundingBoxXYZ bb = room.get_BoundingBox( null ); 689 | 690 | if( null == bb ) 691 | { 692 | Util.ErrorMsg( string.Format( "Skipping room {0} " 693 | + "because it has no bounding box.", 694 | Util.ElementDescription( room ) ) ); 695 | 696 | return; 697 | } 698 | 699 | JtLoops roomLoops = GetRoomLoops( room ); 700 | 701 | ListLoops( room, roomLoops ); 702 | 703 | List furniture 704 | = GetFurniture( room ); 705 | 706 | // Map symbol UniqueId to symbol loop 707 | 708 | Dictionary furnitureLoops 709 | = new Dictionary(); 710 | 711 | // List of instances referring to symbols 712 | 713 | List furnitureInstances 714 | = new List( 715 | furniture.Count ); 716 | 717 | int nFailures; 718 | 719 | foreach( FamilyInstance f in furniture ) 720 | { 721 | FamilySymbol s = f.Symbol; 722 | 723 | string uid = s.UniqueId; 724 | 725 | if( !furnitureLoops.ContainsKey( uid ) ) 726 | { 727 | nFailures = 0; 728 | 729 | JtLoops loops = GetSolidPlanViewBoundaryLoops( 730 | f, true, ref nFailures ); 731 | 732 | if( 0 < nFailures ) 733 | { 734 | Debug.Print( "{0}: {1}", 735 | Util.ElementDescription( f ), 736 | Util.PluralString( nFailures, 737 | "extrusion analyser failure" ) ); 738 | } 739 | ListLoops( f, loops ); 740 | 741 | if( 0 < loops.Count ) 742 | { 743 | // Assume first loop is outer one 744 | 745 | furnitureLoops.Add( uid, loops[0] ); 746 | } 747 | } 748 | furnitureInstances.Add( 749 | new JtPlacement2dInt( f ) ); 750 | } 751 | IWin32Window revit_window 752 | = new JtWindowHandle( hwnd ); 753 | 754 | string caption = doc.Title 755 | + " : " + doc.GetElement( room.LevelId ).Name 756 | + " : " + room.Name; 757 | 758 | Bitmap bmp = GeoSnoop.DisplayRoom( roomLoops, 759 | furnitureLoops, furnitureInstances ); 760 | 761 | GeoSnoop.DisplayImageInForm( revit_window, 762 | caption, false, bmp ); 763 | 764 | //DbUpload.DbUploadRoom( room, furniture, 765 | // roomLoops, furnitureLoops ); 766 | } 767 | 768 | #region External command mainline Execute method 769 | public Result Execute( 770 | ExternalCommandData commandData, 771 | ref string message, 772 | ElementSet elements ) 773 | { 774 | UIApplication uiapp = commandData.Application; 775 | UIDocument uidoc = uiapp.ActiveUIDocument; 776 | Application app = uiapp.Application; 777 | Document doc = uidoc.Document; 778 | 779 | IntPtr hwnd = uiapp.MainWindowHandle; 780 | 781 | if( null == doc ) 782 | { 783 | Util.ErrorMsg( "Please run this command in a valid" 784 | + " Revit project document." ); 785 | return Result.Failed; 786 | } 787 | 788 | // Iterate over all pre-selected rooms 789 | 790 | Selection sel = uidoc.Selection; 791 | 792 | ICollection ids = sel.GetElementIds(); 793 | 794 | if( 0 < ids.Count ) 795 | { 796 | foreach( ElementId id in ids ) 797 | { 798 | if( !( doc.GetElement( id ) is Room ) ) 799 | { 800 | Util.ErrorMsg( "Please pre-select only room" 801 | + " elements before running this command." ); 802 | return Result.Failed; 803 | } 804 | } 805 | } 806 | 807 | // If no rooms were pre-selected, 808 | // prompt for post-selection 809 | 810 | if( null == ids || 0 == ids.Count ) 811 | { 812 | IList refs = null; 813 | 814 | try 815 | { 816 | refs = sel.PickObjects( ObjectType.Element, 817 | new RoomSelectionFilter(), 818 | "Please select rooms." ); 819 | } 820 | catch( Autodesk.Revit.Exceptions 821 | .OperationCanceledException ) 822 | { 823 | return Result.Cancelled; 824 | } 825 | ids = new List( 826 | refs.Select( 827 | r => r.ElementId ) ); 828 | } 829 | 830 | // Upload selected rooms to cloud database 831 | 832 | foreach( ElementId id in ids ) 833 | { 834 | UploadRoom( hwnd, doc, 835 | doc.GetElement( id ) as Room ); 836 | } 837 | 838 | //DbUpdater.SetLastSequence(); 839 | 840 | return Result.Succeeded; 841 | } 842 | #endregion // External command mainline Execute method 843 | } 844 | } 845 | --------------------------------------------------------------------------------