├── .gitignore ├── LICENSE ├── README.md ├── RoomVolumeDirectShape.sln ├── RoomVolumeDirectShape ├── Command.cs ├── GltfNodeData.cs ├── IntPoint3d.cs ├── Properties │ └── AssemblyInfo.cs ├── RoomVolumeDirectShape.addin ├── RoomVolumeDirectShape.csproj ├── TriangleIndices.cs └── Util.cs └── img ├── rac_basic_sample_project.png └── rac_basic_sample_project_room_volumes.png /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RoomVolumeDirectShape 2 | 3 | Revit C# .NET add-in creating DirectShape elements representing room volume. 4 | 5 | The RoomVolumeDirectShape add-in performs the following simple steps: 6 | 7 | - Retrieve all rooms in the BIM using a filtered element collector, then, for each room: 8 | - Query each room for its closed shell using 9 | the [ClosedShell API call](https://www.revitapidocs.com/2020/1a510aef-63f6-4d32-c0ff-a8071f5e23b8.htm) 10 | - Generate a [DirectShape element](https://www.revitapidocs.com/2020/bfbd137b-c2c2-71bb-6f4a-992d0dcf6ea8.htm) representing the geometry 11 | - Query each room for all its properties, mostly stored in parameters 12 | (cf., [getting all parameter values](https://thebuildingcoder.typepad.com/blog/2018/05/getting-all-parameter-values.html) 13 | and [retrieving parameter values from an element](https://thebuildingcoder.typepad.com/blog/2018/05/getting-all-parameter-values.html#5)) 14 | - Generate a JSON string representing a dictionary of the room properties 15 | - Store the room property JSON string in the DirectShape Comment property 16 | 17 | ## Motivation 18 | 19 | This add-in was inspired by the following request: 20 | 21 | The context: We are building digital twins out of BIM data. To do so, we use Revit, Dynamo, and Forge. 22 | 23 | The issue: We rely on the rooms in Revit to perform a bunch of tasks (reassign equipment localization, rebuild a navigation tree, and so on). 24 | 25 | Unfortunately, theses rooms are not displayed in the Revit 3D view. 26 | 27 | Therefore, they are nowhere to be found in the Forge SVF file. 28 | 29 | Our (so-so) solution: The original solution was developed with Autodesk consulting. 30 | 31 | We use Dynamo to extract the room geometry and build Revit volumes. 32 | 33 | It works, but it is: 34 | 35 | - Not very robust: Some rooms has to be recreated manually, Dynamo crashes, geometry with invalid faces is produced, etc. 36 | - Not very fast: The actual script exports SAT files and reimports them. 37 | - Manual: Obviously, and also tedious and error-prone. 38 | 39 | The whole process amounts to several hours of manual work. 40 | 41 | We want to fix this. 42 | 43 | Our goal: A robust implementation that will get rid of Dynamo, automate the process in Revit, and in the end, run that in a Forge Design Automation process. 44 | 45 | The ideal way forward is exactly what you describe: A native C# Revit API that find the rooms, creates a direct shape volume for them, and copy their properties to that. 46 | 47 | No intermediate formats, no UI, just straight automation work. 48 | 49 | 50 | ## Solution 51 | 52 | The solution is explained in detail 53 | in [The Building Coder](https://thebuildingcoder.typepad.com) discussion 54 | on [`DirectShape` element to represent room volume](https://thebuildingcoder.typepad.com/blog/2019/05/generate-directshape-element-to-represent-room-volume.html). 55 | 56 | 57 | ## Sample Run 58 | 59 | I tested this in the well-known standard Revit *rac_basic_sample_project.rvt* sample model: 60 | 61 |
62 | Revit Architecture rac_basic_sample_project.rvt 63 |
64 | 65 | Isolated, the resulting direct shapes look like this: 66 | 67 |
68 | DirectShape elements representing room volumes 69 |
70 | 71 | 72 | ## Cleaning up the Solid for the Forge Viewer 73 | 74 | The solid returned by `Room.GetClosedShell` does not display properly in the Forge viewer; in fact, the generic model direct shape elements are completely ignored and do not even appear in the Forge viewer model browser. 75 | 76 | Implemented `CopyGeometry` to fix that, and tried various approaches to recreate the solid myself, iterating over the solid faces and building new faces with a `TessellatedShapeBuilder`. 77 | 78 | Three approaches attemtped, using: 79 | 80 | - `Face.Triangulate` 81 | - `Face.EdgeLoops` 82 | - `Face.GetEdgesAsCurveLoops` 83 | 84 | Using the unoriented edge loops does not work. It would require reorienting the edges properly to define a valid new solid. 85 | 86 | Using `GetEdgesAsCurveLoops` creates a good-looking solid in Revit, but it has some weird normals in the Forge viewer. 87 | 88 | Currently, the triangulation approach seems be the only one that deliver reliable results in the Forge viewer. 89 | 90 | 91 | ## Author 92 | 93 | 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) 94 | 95 | 96 | ## License 97 | 98 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). 99 | Please see the [LICENSE](LICENSE) file for full details. 100 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoomVolumeDirectShape", "RoomVolumeDirectShape\RoomVolumeDirectShape.csproj", "{338EE02A-603F-468D-B3EB-CF2AD9E8F245}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {338EE02A-603F-468D-B3EB-CF2AD9E8F245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {338EE02A-603F-468D-B3EB-CF2AD9E8F245}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {338EE02A-603F-468D-B3EB-CF2AD9E8F245}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {338EE02A-603F-468D-B3EB-CF2AD9E8F245}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/Command.cs: -------------------------------------------------------------------------------- 1 | //#define USE_FACE_TRIANGULATION 2 | #define CREATE_NEW_SOLID_USING_TESSELATION 3 | 4 | #region Namespaces 5 | using System; 6 | using System.Linq; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using Autodesk.Revit.ApplicationServices; 10 | using Autodesk.Revit.Attributes; 11 | using Autodesk.Revit.DB; 12 | using Autodesk.Revit.DB.Architecture; 13 | using Autodesk.Revit.UI; 14 | using System.IO; 15 | #endregion 16 | 17 | namespace RoomVolumeDirectShape 18 | { 19 | [Transaction( TransactionMode.Manual )] 20 | public class Command : IExternalCommand 21 | { 22 | // Cannot use OST_Rooms; DirectShape.CreateElement 23 | // throws ArgumentExceptionL: Element id categoryId 24 | // may not be used as a DirectShape category. 25 | 26 | /// 27 | /// Category assigned to the room volume direct shape 28 | /// 29 | ElementId _id_category_for_direct_shape 30 | = new ElementId( BuiltInCategory.OST_GenericModel ); 31 | 32 | /// 33 | /// DirectShape parameter to populate with JSON 34 | /// dictionary containing all room properies 35 | /// 36 | BuiltInParameter _bip_properties 37 | = BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS; 38 | 39 | /// 40 | /// Return a JSON string representing a dictionary 41 | /// mapping string key to string value. 42 | /// 43 | static string FormatDictAsJson( 44 | Dictionary d ) 45 | { 46 | List keys = new List( d.Keys ); 47 | keys.Sort(); 48 | 49 | List key_vals = new List( 50 | keys.Count ); 51 | 52 | foreach( string key in keys ) 53 | { 54 | key_vals.Add( 55 | string.Format( "\"{0}\" : \"{1}\"", 56 | key, d[key] ) ); 57 | } 58 | return "{" + string.Join( ", ", key_vals ) + "}"; 59 | } 60 | 61 | /// 62 | /// Return parameter storage type abbreviation 63 | /// 64 | static char ParameterStorageTypeChar( 65 | Parameter p ) 66 | { 67 | if( null == p ) 68 | { 69 | throw new ArgumentNullException( 70 | "p", "expected non-null parameter" ); 71 | } 72 | 73 | char abbreviation = '?'; 74 | 75 | switch( p.StorageType ) 76 | { 77 | case StorageType.Double: 78 | abbreviation = 'r'; // real number 79 | break; 80 | case StorageType.Integer: 81 | abbreviation = 'n'; // integer number 82 | break; 83 | case StorageType.String: 84 | abbreviation = 's'; // string 85 | break; 86 | case StorageType.ElementId: 87 | abbreviation = 'e'; // element id 88 | break; 89 | case StorageType.None: 90 | throw new ArgumentOutOfRangeException( 91 | "p", "expected valid parameter " 92 | + "storage type, not 'None'" ); 93 | } 94 | return abbreviation; 95 | } 96 | 97 | /// 98 | /// Return parameter value formatted as string 99 | /// 100 | static string ParameterToString( Parameter p ) 101 | { 102 | string s = "null"; 103 | 104 | if( p != null ) 105 | { 106 | switch( p.StorageType ) 107 | { 108 | case StorageType.Double: 109 | s = p.AsDouble().ToString( "0.##" ); 110 | break; 111 | case StorageType.Integer: 112 | s = p.AsInteger().ToString(); 113 | break; 114 | case StorageType.String: 115 | s = p.AsString(); 116 | break; 117 | case StorageType.ElementId: 118 | s = p.AsElementId().IntegerValue.ToString(); 119 | break; 120 | case StorageType.None: 121 | s = "none"; 122 | break; 123 | } 124 | } 125 | return s; 126 | } 127 | 128 | /// 129 | /// Return all the element parameter values in a 130 | /// dictionary mapping parameter names to values 131 | /// 132 | static Dictionary GetParamValues( 133 | Element e ) 134 | { 135 | // Two choices: 136 | // Element.Parameters property -- Retrieves 137 | // a set containing all the parameters. 138 | // GetOrderedParameters method -- Gets the 139 | // visible parameters in order. 140 | 141 | //IList ps = e.GetOrderedParameters(); 142 | 143 | ParameterSet pset = e.Parameters; 144 | 145 | Dictionary d 146 | = new Dictionary( pset.Size ); 147 | 148 | foreach( Parameter p in pset ) 149 | { 150 | // AsValueString displays the value as the 151 | // user sees it. In some cases, the underlying 152 | // database value returned by AsInteger, AsDouble, 153 | // etc., may be more relevant, as done by 154 | // ParameterToString 155 | 156 | string key = string.Format( "{0}({1})", 157 | p.Definition.Name, 158 | ParameterStorageTypeChar( p ) ); 159 | 160 | string val = ParameterToString( p ); 161 | 162 | if( d.ContainsKey( key ) ) 163 | { 164 | if( d[key] != val ) 165 | { 166 | d[key] += " | " + val; 167 | } 168 | } 169 | else 170 | { 171 | d.Add( key, val ); 172 | } 173 | } 174 | return d; 175 | } 176 | 177 | static string GetRoomPropertiesJson( Room r ) 178 | { 179 | Dictionary param_values 180 | = GetParamValues( r ); 181 | 182 | // These room properties are all stored in 183 | // parameters and therefore already captured 184 | 185 | //double baseOffset = r.BaseOffset; 186 | //double limitOffset = r.LimitOffset; 187 | //double unboundedHeight = r.UnboundedHeight; 188 | //string upperLimit = r.UpperLimit.Name; 189 | //double volume = r.Volume; 190 | 191 | return FormatDictAsJson( param_values ); 192 | } 193 | 194 | /// 195 | /// XYZ equality comparer to eliminate 196 | /// slightly differing vertices 197 | /// 198 | class XyzEqualityComparer : IEqualityComparer 199 | { 200 | /// 201 | /// Tolerance. 202 | /// 0.003 imperial feet is ca. 0.9 mm 203 | /// 204 | double _tol = 0.003; 205 | 206 | public bool Equals( XYZ a, XYZ b ) 207 | { 208 | return _tol > a.DistanceTo( b ); 209 | } 210 | 211 | public int GetHashCode( XYZ a ) 212 | { 213 | string format = "0.####"; 214 | string s = a.X.ToString( format ) 215 | + "," + a.Y.ToString( format ) 216 | + "," + a.Z.ToString( format ); 217 | return s.GetHashCode(); 218 | } 219 | } 220 | 221 | /// 222 | /// Create a new list of geometry objects from the 223 | /// given input. As input, we supply the result of 224 | /// Room.GetClosedShell. The output is the exact 225 | /// same solid lacking whatever flaws are present 226 | /// in the input solid. 227 | /// 228 | static IList CopyGeometry( 229 | GeometryElement geo, 230 | ElementId materialId, 231 | List coords, 232 | List indices ) 233 | { 234 | TessellatedShapeBuilderResult result = null; 235 | 236 | TessellatedShapeBuilder builder 237 | = new TessellatedShapeBuilder(); 238 | 239 | // Need to include the key in the value, otherwise 240 | // no way to access it later, cf. 241 | // https://stackoverflow.com/questions/1619090/getting-a-keyvaluepair-directly-from-a-dictionary 242 | 243 | Dictionary> pts 244 | = new Dictionary>( 245 | new XyzEqualityComparer() ); 246 | 247 | int nSolids = 0; 248 | //int nFaces = 0; 249 | int nTriangles = 0; 250 | //int nVertices = 0; 251 | List vertices = new List( 3 ); 252 | 253 | foreach( GeometryObject obj in geo ) 254 | { 255 | Solid solid = obj as Solid; 256 | 257 | if( null != solid ) 258 | { 259 | if( 0 < solid.Volume ) 260 | { 261 | ++nSolids; 262 | 263 | builder.OpenConnectedFaceSet( false ); 264 | 265 | #region Create a new solid based on tessellation of the invalid room closed shell solid 266 | #if CREATE_NEW_SOLID_USING_TESSELATION 267 | 268 | Debug.Assert( 269 | SolidUtils.IsValidForTessellation( solid ), 270 | "expected a valid solid for room closed shell" ); 271 | 272 | SolidOrShellTessellationControls controls 273 | = new SolidOrShellTessellationControls() 274 | { 275 | // 276 | // Summary: 277 | // A positive real number specifying how accurately a triangulation should approximate 278 | // a solid or shell. 279 | // 280 | // Exceptions: 281 | // T:Autodesk.Revit.Exceptions.ArgumentOutOfRangeException: 282 | // When setting this property: The given value for accuracy must be greater than 283 | // 0 and no more than 30000 feet. 284 | // This statement is not true. I set Accuracy = 0.003 and an exception was thrown. 285 | // Setting it to 0.006 was acceptable. 0.03 is a bit over 9 mm. 286 | // 287 | // Remarks: 288 | // The maximum distance from a point on the triangulation to the nearest point on 289 | // the solid or shell should be no greater than the specified accuracy. This constraint 290 | // may be approximately enforced. 291 | Accuracy = 0.03, 292 | // 293 | // Summary: 294 | // An number between 0 and 1 (inclusive) specifying the level of detail for the 295 | // triangulation of a solid or shell. 296 | // 297 | // Exceptions: 298 | // T:Autodesk.Revit.Exceptions.ArgumentOutOfRangeException: 299 | // When setting this property: The given value for levelOfDetail must lie between 300 | // 0 and 1 (inclusive). 301 | // 302 | // Remarks: 303 | // Smaller values yield coarser triangulations (fewer triangles), while larger values 304 | // yield finer triangulations (more triangles). 305 | LevelOfDetail = 0.1, 306 | // 307 | // Summary: 308 | // A non-negative real number specifying the minimum allowed angle for any triangle 309 | // in the triangulation, in radians. 310 | // 311 | // Exceptions: 312 | // T:Autodesk.Revit.Exceptions.ArgumentOutOfRangeException: 313 | // When setting this property: The given value for minAngleInTriangle must be at 314 | // least 0 and less than 60 degrees, expressed in radians. The value 0 means to 315 | // ignore the minimum angle constraint. 316 | // 317 | // Remarks: 318 | // A small value can be useful when triangulating long, thin objects, in order to 319 | // keep the number of triangles small, but it can result in long, thin triangles, 320 | // which are not acceptable for all applications. If the value is too large, this 321 | // constraint may not be satisfiable, causing the triangulation to fail. This constraint 322 | // may be approximately enforced. A value of 0 means to ignore the minimum angle 323 | // constraint. 324 | MinAngleInTriangle = 3 * Math.PI / 180.0, 325 | // 326 | // Summary: 327 | // A positive real number specifying the minimum allowed value for the external 328 | // angle between two adjacent triangles, in radians. 329 | // 330 | // Exceptions: 331 | // T:Autodesk.Revit.Exceptions.ArgumentOutOfRangeException: 332 | // When setting this property: The given value for minExternalAngleBetweenTriangles 333 | // must be greater than 0 and no more than 30000 feet. 334 | // 335 | // Remarks: 336 | // A small value yields more smoothly curved triangulated surfaces, usually at the 337 | // expense of an increase in the number of triangles. Note that this setting has 338 | // no effect for planar surfaces. This constraint may be approximately enforced. 339 | MinExternalAngleBetweenTriangles = 0.2 * Math.PI 340 | }; 341 | 342 | TriangulatedSolidOrShell shell 343 | = SolidUtils.TessellateSolidOrShell( solid, controls ); 344 | 345 | int n = shell.ShellComponentCount; 346 | 347 | Debug.Assert( 1 == n, 348 | "expected just one shell component in room closed shell" ); 349 | 350 | TriangulatedShellComponent component 351 | = shell.GetShellComponent( 0 ); 352 | 353 | int coordsBase = coords.Count; 354 | int indicesBase = indices.Count; 355 | 356 | n = component.VertexCount; 357 | 358 | for( int i = 0; i < n; ++i ) 359 | { 360 | XYZ v = component.GetVertex( i ); 361 | coords.Add( new IntPoint3d( v ) ); 362 | } 363 | 364 | n = component.TriangleCount; 365 | 366 | for( int i = 0; i < n; ++i ) 367 | { 368 | TriangleInShellComponent t 369 | = component.GetTriangle( i ); 370 | 371 | vertices.Clear(); 372 | 373 | vertices.Add( component.GetVertex( t.VertexIndex0 ) ); 374 | vertices.Add( component.GetVertex( t.VertexIndex1 ) ); 375 | vertices.Add( component.GetVertex( t.VertexIndex2 ) ); 376 | 377 | indices.Add( new TriangleIndices( 378 | coordsBase + t.VertexIndex0, 379 | coordsBase + t.VertexIndex1, 380 | coordsBase + t.VertexIndex2 ) ); 381 | 382 | TessellatedFace tf = new TessellatedFace( 383 | vertices, materialId ); 384 | 385 | if( builder.DoesFaceHaveEnoughLoopsAndVertices( tf ) ) 386 | { 387 | builder.AddFace( tf ); 388 | ++nTriangles; 389 | } 390 | } 391 | #else 392 | // Iterate over the individual solid faces 393 | 394 | foreach( Face f in solid.Faces ) 395 | { 396 | vertices.Clear(); 397 | 398 | #region Use face triangulation 399 | #if USE_FACE_TRIANGULATION 400 | 401 | Mesh mesh = f.Triangulate(); 402 | int n = mesh.NumTriangles; 403 | 404 | for( int i = 0; i < n; ++i ) 405 | { 406 | MeshTriangle triangle = mesh.get_Triangle( i ); 407 | 408 | XYZ p1 = triangle.get_Vertex( 0 ); 409 | XYZ p2 = triangle.get_Vertex( 1 ); 410 | XYZ p3 = triangle.get_Vertex( 2 ); 411 | 412 | vertices.Clear(); 413 | vertices.Add( p1 ); 414 | vertices.Add( p2 ); 415 | vertices.Add( p3 ); 416 | 417 | TessellatedFace tf 418 | = new TessellatedFace( 419 | vertices, materialId ); 420 | 421 | if( builder.DoesFaceHaveEnoughLoopsAndVertices( tf ) ) 422 | { 423 | builder.AddFace( tf ); 424 | ++nTriangles; 425 | } 426 | } 427 | #endif // USE_FACE_TRIANGULATION 428 | #endregion // Use face triangulation 429 | 430 | #region Use original solid and its EdgeLoops 431 | #if USE_EDGELOOPS 432 | // This returns arbitrarily ordered and 433 | // oriented edges, so no solid can be 434 | // generated. 435 | 436 | foreach( EdgeArray loop in f.EdgeLoops ) 437 | { 438 | foreach( Edge e in loop ) 439 | { 440 | XYZ p = e.AsCurve().GetEndPoint( 0 ); 441 | XYZ q = p; 442 | 443 | if( pts.ContainsKey( p ) ) 444 | { 445 | KeyValuePair kv = pts[p]; 446 | q = kv.Key; 447 | int n = kv.Value; 448 | pts[p] = new KeyValuePair( 449 | q, ++n ); 450 | 451 | Debug.Print( "Ignoring vertex at {0} " 452 | + "with distance {1} to existing " 453 | + "vertex {2}", 454 | p, p.DistanceTo( q ), q ); 455 | } 456 | else 457 | { 458 | pts[p] = new KeyValuePair( 459 | p, 1 ); 460 | } 461 | 462 | vertices.Add( q ); 463 | ++nVertices; 464 | } 465 | } 466 | #endif // USE_EDGELOOPS 467 | #endregion // Use original solid and its EdgeLoops 468 | 469 | #region Use original solid and GetEdgesAsCurveLoops 470 | #if USE_AS_CURVE_LOOPS 471 | 472 | // The solids generated by this have some weird 473 | // normals, so they do not render correctly in 474 | // the Forge viewer. Revert to triangles again. 475 | 476 | IList loops 477 | = f.GetEdgesAsCurveLoops(); 478 | 479 | foreach( CurveLoop loop in loops ) 480 | { 481 | foreach( Curve c in loop ) 482 | { 483 | XYZ p = c.GetEndPoint( 0 ); 484 | XYZ q = p; 485 | 486 | if( pts.ContainsKey( p ) ) 487 | { 488 | KeyValuePair kv = pts[p]; 489 | q = kv.Key; 490 | int n = kv.Value; 491 | pts[p] = new KeyValuePair( 492 | q, ++n ); 493 | 494 | Debug.Print( "Ignoring vertex at {0} " 495 | + "with distance {1} to existing " 496 | + "vertex {2}", 497 | p, p.DistanceTo( q ), q ); 498 | } 499 | else 500 | { 501 | pts[p] = new KeyValuePair( 502 | p, 1 ); 503 | } 504 | 505 | vertices.Add( q ); 506 | ++nVertices; 507 | } 508 | } 509 | #endif // USE_AS_CURVE_LOOPS 510 | #endregion // Use original solid and GetEdgesAsCurveLoops 511 | 512 | builder.AddFace( new TessellatedFace( 513 | vertices, materialId ) ); 514 | 515 | ++nFaces; 516 | } 517 | 518 | #endif // CREATE_NEW_SOLID_USING_TESSELATION 519 | #endregion // Create a new solid based on tessellation of the invalid room closed shell solid 520 | 521 | builder.CloseConnectedFaceSet(); 522 | builder.Target = TessellatedShapeBuilderTarget.AnyGeometry; // Solid failed 523 | builder.Fallback = TessellatedShapeBuilderFallback.Mesh; // use Abort if target is Solid 524 | builder.Build(); 525 | result = builder.GetBuildResult(); 526 | 527 | // Debug printout log of current solid's glTF facet data 528 | 529 | n = coords.Count - coordsBase; 530 | 531 | Debug.Print( "{0} glTF vertex coordinates " 532 | + "in millimetres:", n ); 533 | 534 | Debug.Print( string.Join( " ", coords 535 | .TakeWhile( ( p, i ) => coordsBase <= i ) 536 | .Select( p => p.ToString() ) ) ); 537 | 538 | n = indices.Count - indicesBase; 539 | 540 | Debug.Print( "{0} glTF triangles:", n ); 541 | 542 | Debug.Print( string.Join( " ", indices 543 | .TakeWhile( ( ti, i ) => indicesBase <= i ) 544 | .Select( ti => ti.ToString() ) ) ); 545 | } 546 | } 547 | } 548 | return result.GetGeometricalObjects(); 549 | } 550 | 551 | public Result Execute( 552 | ExternalCommandData commandData, 553 | ref string message, 554 | ElementSet elements ) 555 | { 556 | UIApplication uiapp = commandData.Application; 557 | UIDocument uidoc = uiapp.ActiveUIDocument; 558 | Application app = uiapp.Application; 559 | Document doc = uidoc.Document; 560 | 561 | string id_addin = uiapp.ActiveAddInId.GetGUID() 562 | .ToString(); 563 | 564 | IEnumerable rooms 565 | = new FilteredElementCollector( doc ) 566 | .WhereElementIsNotElementType() 567 | .OfClass( typeof( SpatialElement ) ) 568 | .Where( e => e.GetType() == typeof( Room ) ) 569 | .Cast(); 570 | 571 | // Collect room data for glTF export 572 | 573 | List room_data = new List( 574 | rooms.Count() ); 575 | 576 | // Collect geometry data for glTF: a list of 577 | // vertex coordinates in millimetres, and a list 578 | // of triangle vertex indices into the coord list. 579 | 580 | List gltf_coords = new List(); 581 | List gltf_indices = new List(); 582 | 583 | using( Transaction tx = new Transaction( doc ) ) 584 | { 585 | tx.Start( "Generate Direct Shape Elements " 586 | + "Representing Room Volumes" ); 587 | 588 | foreach( Room r in rooms ) 589 | { 590 | Debug.Print( "Processing " 591 | + r.Name + "..." ); 592 | 593 | // Collect data for current room 594 | 595 | GltfNodeData rd = new GltfNodeData( r ); 596 | 597 | GeometryElement geo = r.ClosedShell; 598 | 599 | Debug.Assert( 600 | geo.First() is Solid, 601 | "expected a solid for room closed shell" ); 602 | 603 | Solid solid = geo.First() as Solid; 604 | 605 | #region Fix the shape 606 | #if FIX_THE_SHAPE_SOMEHOW 607 | // Create IList step by step 608 | 609 | Solid solid = geo.First() 610 | as Solid; 611 | 612 | // The room closed shell solid faces have a 613 | // non-null graphics style id: 614 | // Interior Fill 106074 615 | // The sphere faces' graphics style id is null. 616 | // Maybe this graphics style does something 617 | // weird in the Forge viewer? 618 | // Let's create a copy of the room solid and 619 | // see whether that resets the graphics style. 620 | 621 | solid = SolidUtils.CreateTransformed( 622 | solid, Transform.Identity ); 623 | 624 | shape = new GeometryObject[] { solid }; 625 | 626 | // Create a sphere 627 | 628 | var center = XYZ.Zero; 629 | double radius = 2.0; 630 | 631 | var p = center + radius * XYZ.BasisY; 632 | var q = center - radius * XYZ.BasisY; 633 | 634 | var profile = new List(); 635 | profile.Add( Line.CreateBound( p, q ) ); 636 | profile.Add( Arc.Create( q, p, 637 | center + radius * XYZ.BasisX ) ); 638 | 639 | var curveLoop = CurveLoop.Create( profile ); 640 | 641 | var options = new SolidOptions( 642 | ElementId.InvalidElementId, // material 643 | ElementId.InvalidElementId ); // graphics style 644 | 645 | var frame = new Frame( center, 646 | XYZ.BasisX, -XYZ.BasisZ, XYZ.BasisY ); 647 | 648 | var sphere = GeometryCreationUtilities 649 | .CreateRevolvedGeometry( frame, 650 | new CurveLoop[] { curveLoop }, 651 | 0, 2 * Math.PI, options ); 652 | 653 | shape = new GeometryObject[] { solid, sphere }; 654 | #endif // #if FIX_THE_SHAPE_SOMEHOW 655 | #endregion // Fix the shape 656 | 657 | IList shape 658 | = geo.ToList(); 659 | 660 | // Previous counts define offsets 661 | // to new binary data 662 | 663 | rd.CoordinatesBegin = gltf_coords.Count; 664 | rd.TriangleVertexIndicesBegin 665 | = gltf_indices.Count; 666 | 667 | // Create a new solid to use for the direct 668 | // shape from the flawed solid returned by 669 | // GetClosedShell and gather glTF facet data 670 | 671 | shape = CopyGeometry( geo, 672 | ElementId.InvalidElementId, 673 | gltf_coords, gltf_indices ); 674 | 675 | rd.CoordinatesCount = gltf_coords.Count 676 | - rd.CoordinatesBegin; 677 | 678 | rd.TriangleVertexIndexCount 679 | = gltf_indices.Count 680 | - rd.TriangleVertexIndicesBegin; 681 | 682 | IEnumerable pts 683 | = gltf_coords.Skip( 684 | rd.CoordinatesBegin ); 685 | 686 | rd.Min = new IntPoint3d( 687 | pts.Min( p => p.X ), 688 | pts.Min( p => p.Y ), 689 | pts.Min( p => p.Z ) ); 690 | rd.Max = new IntPoint3d( 691 | pts.Max( p => p.X ), 692 | pts.Max( p => p.Y ), 693 | pts.Max( p => p.Z ) ); 694 | 695 | Dictionary param_values 696 | = GetParamValues( r ); 697 | 698 | string json = FormatDictAsJson( param_values ); 699 | 700 | DirectShape ds = DirectShape.CreateElement( 701 | doc, _id_category_for_direct_shape ); 702 | 703 | ds.ApplicationId = id_addin; 704 | ds.ApplicationDataId = r.UniqueId; 705 | ds.SetShape( shape ); 706 | ds.get_Parameter( _bip_properties ).Set( json ); 707 | ds.Name = "Room volume for " + r.Name; 708 | room_data.Add( rd ); 709 | } 710 | tx.Commit(); 711 | } 712 | 713 | // Save glTF text and binary data to two files; 714 | // metadata, min, max, buffer information; 715 | // vertex coordinates and triangle indices 716 | 717 | // Path.GetTempPath() returns a weird subdirectory 718 | // created by Revit, so we will not use that here, e.g., 719 | // C:\Users\tammikj\AppData\Local\Temp\bfd59506-2dff-4b0f-bbe4-31587fcaf508 720 | 721 | //string path = Path.GetTempPath(); 722 | 723 | string path = "C:/tmp"; 724 | 725 | path = Path.Combine( path, doc.Title + "_gltf" ); 726 | 727 | using( StreamWriter s = new StreamWriter( 728 | path + ".txt" ) ) 729 | { 730 | int n = room_data.Count; 731 | 732 | s.WriteLine( "{0} room{1}", n, ( ( 1 == n ) ? "" : "s" ) ); 733 | s.WriteLine( "{0},{1},{2},{3},{4},{5},{6},{7},{8}", 734 | "id", // "ElementId", 735 | "uid", // "UniqueId", 736 | "name", // "RoomName", 737 | "min", 738 | "max", 739 | "coord begin", // "CoordinatesBegin", 740 | "count", // "CoordinatesCount", 741 | "indices begin", // "TriangleVertexIndicesBegin", 742 | "count" ); // "TriangleVertexIndexCount" 743 | 744 | foreach( GltfNodeData rd in room_data ) 745 | { 746 | s.WriteLine( rd.ToString() ); 747 | } 748 | } 749 | 750 | using( FileStream f = File.Create( path + ".bin" ) ) 751 | { 752 | using( BinaryWriter writer = new BinaryWriter( f ) ) 753 | { 754 | foreach( IntPoint3d p in gltf_coords ) 755 | { 756 | writer.Write( (float) p.X ); 757 | writer.Write( (float) p.Y ); 758 | writer.Write( (float) p.Z ); 759 | } 760 | foreach( TriangleIndices ti in gltf_indices ) 761 | { 762 | foreach( int i in ti.Indices ) 763 | { 764 | Debug.Assert( ushort.MaxValue > i, 765 | "expected vertex index to fit into unsigned short" ); 766 | 767 | writer.Write( (ushort) i ); 768 | } 769 | } 770 | } 771 | } 772 | return Result.Succeeded; 773 | } 774 | } 775 | } 776 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/GltfNodeData.cs: -------------------------------------------------------------------------------- 1 | using Autodesk.Revit.DB.Architecture; 2 | 3 | namespace RoomVolumeDirectShape 4 | { 5 | /// 6 | /// Room data for glTF export: 7 | /// element id and guid, room name, 8 | /// coordinateoffset, coordinatecount, 9 | /// vertexindexoffset, vertexcount 10 | /// (byte or object coount?), 11 | /// min and max x, y, z coord values 12 | /// 13 | class GltfNodeData 14 | { 15 | public int ElementId { get; set; } 16 | public string RoomName { get; set; } 17 | public string UniqueId { get; set; } 18 | public IntPoint3d Min { get; set; } 19 | public IntPoint3d Max { get; set; } 20 | public int CoordinatesBegin { get; set; } 21 | public int CoordinatesCount { get; set; } 22 | public int TriangleVertexIndicesBegin { get; set; } 23 | public int TriangleVertexIndexCount { get; set; } 24 | 25 | public GltfNodeData( Room r ) 26 | { 27 | ElementId = r.Id.IntegerValue; 28 | UniqueId = r.UniqueId; 29 | RoomName = r.Name; 30 | } 31 | 32 | /// 33 | /// Display as a string. 34 | /// 35 | public override string ToString() 36 | { 37 | return string.Format( 38 | "{0},{1},{2},{3},{4},{5},{6},{7},{8}", 39 | ElementId, 40 | UniqueId, 41 | RoomName, 42 | Min, 43 | Max, 44 | CoordinatesBegin, 45 | CoordinatesCount, 46 | TriangleVertexIndicesBegin, 47 | TriangleVertexIndexCount ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/IntPoint3d.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Autodesk.Revit.DB; 3 | 4 | namespace RoomVolumeDirectShape 5 | { 6 | /// 7 | /// An integer-based 3D point class. 8 | /// 9 | class IntPoint3d : IComparable 10 | { 11 | public int X { get; set; } 12 | public int Y { get; set; } 13 | public int Z { get; set; } 14 | 15 | /// 16 | /// Initialise a 2D millimetre integer 17 | /// point to the given values. 18 | /// 19 | public IntPoint3d( int x, int y, int z ) 20 | { 21 | X = x; 22 | Y = y; 23 | Z = z; 24 | } 25 | 26 | /// 27 | /// Convert a 2D Revit UV to a 3D millimetre 28 | /// integer point by scaling from feet to mm. 29 | /// 30 | public IntPoint3d( UV p ) 31 | { 32 | X = Util.FootToMmInt( p.U ); 33 | Y = Util.FootToMmInt( p.V ); 34 | Z = 0; 35 | } 36 | 37 | /// 38 | /// Convert a 3D Revit XYZ to a 3D millimetre 39 | /// integer point, scaling from feet to mm. 40 | /// 41 | public IntPoint3d( XYZ p ) 42 | { 43 | X = Util.FootToMmInt( p.X ); 44 | Y = Util.FootToMmInt( p.Y ); 45 | Z = Util.FootToMmInt( p.Z ); 46 | } 47 | 48 | /// 49 | /// Convert Revit coordinates XYZ to a 2D 50 | /// millimetre integer point by scaling 51 | /// from feet to mm. 52 | /// 53 | public IntPoint3d( double x, double y, double z ) 54 | { 55 | X = Util.FootToMmInt( x ); 56 | Y = Util.FootToMmInt( y ); 57 | Z = Util.FootToMmInt( z ); 58 | } 59 | 60 | /// 61 | /// Comparison with another point, important 62 | /// for dictionary lookup support. 63 | /// 64 | public int CompareTo( IntPoint3d a ) 65 | { 66 | int d = X - a.X; 67 | 68 | if( 0 == d ) 69 | { 70 | d = Y - a.Y; 71 | 72 | if( 0 == d ) 73 | { 74 | d = Z - a.Z; 75 | } 76 | } 77 | return d; 78 | } 79 | 80 | /// 81 | /// Display as a string. 82 | /// 83 | public override string ToString() 84 | { 85 | return string.Format( "({0},{1},{2})", X, Y, Z ); 86 | } 87 | 88 | /// 89 | /// Display as a string. 90 | /// 91 | public string ToString( 92 | bool onlySpaceSeparator ) 93 | { 94 | string format_string = onlySpaceSeparator 95 | ? "{0} {1} {2}" 96 | : "({0},{1},{2})"; 97 | 98 | return string.Format( format_string, X, Y, Z ); 99 | } 100 | 101 | /// 102 | /// Add two points, i.e. treat one of 103 | /// them as a translation vector. 104 | /// 105 | public static IntPoint3d operator +( 106 | IntPoint3d a, 107 | IntPoint3d b ) 108 | { 109 | return new IntPoint3d( 110 | a.X + b.X, a.Y + b.Y, a.Z + b.Z ); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/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( "RoomVolumeDirectShape" )] 9 | [assembly: AssemblyDescription( "Revit C# .NET Add-In generating DirectShape elements to represent room volumes" )] 10 | [assembly: AssemblyConfiguration( "" )] 11 | [assembly: AssemblyCompany( "Autodesk Inc." )] 12 | [assembly: AssemblyProduct( "RoomVolumeDirectShape Revit C# .NET Add-In" )] 13 | [assembly: AssemblyCopyright( "Copyright 2019 (C) Jeremy Tammik, Autodesk Inc." )] 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( "321044f7-b0b2-4b1c-af18-e71a19252be0" )] 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 | // 36 | // History: 37 | // 38 | // 2019-05-27 2020.0.0.0 initial rough draft ready for testing and refining 39 | // 2019-05-27 2020.0.0.1 implemented first working version 40 | // 2019-05-27 2020.0.0.2 properly set direct shape `Name` property 41 | // 2019-06-26 2020.0.0.3 fixing for Forge viewer, updated for eason 42 | // 2019-06-26 2020.0.0.4 the solid returned by Room.GetClosedShell does not display in the Forge viewer; implemented CopyGeometry to create a new solid to replace it; triangles work; EdgeLoops does not; GetEdgesAsCurveLoops appeared to work 43 | // 2019-06-27 2020.0.0.5 the solid copied from the room closed shell generated using GetEdgesAsCurveLoops does not display properly in the Forge viewer; reverted to triangulation again 44 | // 2019-06-27 2020.0.0.6 added assertions 45 | // 2019-06-27 2020.0.0.6 created a new solid from the room closed shell using SolidUtils.TessellateSolidOrShell 46 | // 2019-06-27 2020.0.0.7 added code to generate glTF facet data 47 | // 2019-06-27 2020.0.0.7 store glTF facet data to binary file 48 | // 2019-06-29 2020.0.0.8 implemented gltf data export for multiple rooms 49 | // 2019-06-29 2020.0.0.9 corrected min max calculation 50 | // 2019-06-29 2020.0.0.10 corrected min max calculation 51 | // 52 | [assembly: AssemblyVersion( "2020.0.0.10" )] 53 | [assembly: AssemblyFileVersion( "2020.0.0.10" )] 54 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/RoomVolumeDirectShape.addin: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Command RoomVolumeDirectShape 5 | Some description for RoomVolumeDirectShape 6 | RoomVolumeDirectShape.dll 7 | RoomVolumeDirectShape.Command 8 | fb982ab3-1c51-419a-a790-94058590438c 9 | com.typepad.thebuildingcoder 10 | The Building Coder, http://thebuildingcoder.typepad.com 11 | 12 | 13 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/RoomVolumeDirectShape.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | None 7 | 8 | 9 | 10 | 11 | Debug 12 | AnyCPU 13 | {338EE02A-603F-468D-B3EB-CF2AD9E8F245} 14 | Library 15 | Properties 16 | RoomVolumeDirectShape 17 | RoomVolumeDirectShape 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 | $(ProgramW6432)\Autodesk\Revit 2020\RevitAPI.dll 45 | False 46 | 47 | 48 | $(ProgramW6432)\Autodesk\Revit 2020\RevitAPIUI.dll 49 | False 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | if exist "$(AppData)\Autodesk\REVIT\Addins\2020" copy "$(ProjectDir)*.addin" "$(AppData)\Autodesk\REVIT\Addins\2020" 67 | if exist "$(AppData)\Autodesk\REVIT\Addins\2020" copy "$(ProjectDir)$(OutputPath)*.dll" "$(AppData)\Autodesk\REVIT\Addins\2020" 68 | 69 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/TriangleIndices.cs: -------------------------------------------------------------------------------- 1 | namespace RoomVolumeDirectShape 2 | { 3 | class TriangleIndices 4 | { 5 | public int [] Indices; 6 | 7 | public TriangleIndices( int i, int j, int k ) 8 | { 9 | Indices = new int[3] { i, j, k }; 10 | } 11 | 12 | /// 13 | /// Display as a string. 14 | /// 15 | public override string ToString() 16 | { 17 | return string.Format( "({0},{1},{2})", 18 | Indices[0], Indices[1], Indices[2] ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RoomVolumeDirectShape/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Autodesk.Revit.DB; 5 | using System.Diagnostics; 6 | 7 | namespace RoomVolumeDirectShape 8 | { 9 | class Util 10 | { 11 | #region Geometrical Comparison 12 | public const double _eps = 1.0e-9; 13 | 14 | public static double Eps 15 | { 16 | get 17 | { 18 | return _eps; 19 | } 20 | } 21 | 22 | public static double MinLineLength 23 | { 24 | get 25 | { 26 | return _eps; 27 | } 28 | } 29 | 30 | public static double TolPointOnPlane 31 | { 32 | get 33 | { 34 | return _eps; 35 | } 36 | } 37 | 38 | public static bool IsZero( 39 | double a, 40 | double tolerance = _eps ) 41 | { 42 | return tolerance > Math.Abs( a ); 43 | } 44 | 45 | public static bool IsEqual( 46 | double a, 47 | double b, 48 | double tolerance = _eps ) 49 | { 50 | return IsZero( b - a, tolerance ); 51 | } 52 | 53 | public static int Compare( 54 | double a, 55 | double b, 56 | double tolerance = _eps ) 57 | { 58 | return IsEqual( a, b, tolerance ) 59 | ? 0 60 | : ( a < b ? -1 : 1 ); 61 | } 62 | 63 | public static int Compare( 64 | XYZ p, 65 | XYZ q, 66 | double tolerance = _eps ) 67 | { 68 | int d = Compare( p.X, q.X, tolerance ); 69 | 70 | if( 0 == d ) 71 | { 72 | d = Compare( p.Y, q.Y, tolerance ); 73 | 74 | if( 0 == d ) 75 | { 76 | d = Compare( p.Z, q.Z, tolerance ); 77 | } 78 | } 79 | return d; 80 | } 81 | 82 | /// 83 | /// Implement a comparison operator for lines 84 | /// in the XY plane useful for sorting into 85 | /// groups of parallel lines. 86 | /// 87 | public static int Compare( Line a, Line b ) 88 | { 89 | XYZ pa = a.GetEndPoint( 0 ); 90 | XYZ qa = a.GetEndPoint( 1 ); 91 | XYZ pb = b.GetEndPoint( 0 ); 92 | XYZ qb = b.GetEndPoint( 1 ); 93 | XYZ va = qa - pa; 94 | XYZ vb = qb - pb; 95 | 96 | // Compare angle in the XY plane 97 | 98 | double ang_a = Math.Atan2( va.Y, va.X ); 99 | double ang_b = Math.Atan2( vb.Y, vb.X ); 100 | 101 | int d = Compare( ang_a, ang_b ); 102 | 103 | if( 0 == d ) 104 | { 105 | // Compare distance of unbounded line to origin 106 | 107 | double da = ( qa.X * pa.Y - qa.Y * pa.Y ) 108 | / va.GetLength(); 109 | 110 | double db = ( qb.X * pb.Y - qb.Y * pb.Y ) 111 | / vb.GetLength(); 112 | 113 | d = Compare( da, db ); 114 | 115 | if( 0 == d ) 116 | { 117 | // Compare distance of start point to origin 118 | 119 | d = Compare( pa.GetLength(), pb.GetLength() ); 120 | 121 | if( 0 == d ) 122 | { 123 | // Compare distance of end point to origin 124 | 125 | d = Compare( qa.GetLength(), qb.GetLength() ); 126 | } 127 | } 128 | } 129 | return d; 130 | } 131 | 132 | /// 133 | /// Predicate to test whewther two points or 134 | /// vectors can be considered equal with the 135 | /// given tolerance. 136 | /// 137 | public static bool IsEqual( 138 | XYZ p, 139 | XYZ q, 140 | double tolerance = _eps ) 141 | { 142 | return 0 == Compare( p, q, tolerance ); 143 | } 144 | 145 | /// 146 | /// Return true if the given bounding box bb 147 | /// contains the given point p in its interior. 148 | /// 149 | public bool BoundingBoxXyzContains( 150 | BoundingBoxXYZ bb, 151 | XYZ p ) 152 | { 153 | return 0 < Compare( bb.Min, p ) 154 | && 0 < Compare( p, bb.Max ); 155 | } 156 | 157 | /// 158 | /// Return true if the vectors v and w 159 | /// are non-zero and perpendicular. 160 | /// 161 | bool IsPerpendicular( XYZ v, XYZ w ) 162 | { 163 | double a = v.GetLength(); 164 | double b = v.GetLength(); 165 | double c = Math.Abs( v.DotProduct( w ) ); 166 | return _eps < a 167 | && _eps < b 168 | && _eps > c; 169 | // c * c < _eps * a * b 170 | } 171 | 172 | public static bool IsParallel( XYZ p, XYZ q ) 173 | { 174 | return p.CrossProduct( q ).IsZeroLength(); 175 | } 176 | 177 | public static bool IsCollinear( Line a, Line b ) 178 | { 179 | XYZ v = a.Direction; 180 | XYZ w = b.Origin - a.Origin; 181 | return IsParallel( v, b.Direction ) 182 | && IsParallel( v, w ); 183 | } 184 | 185 | public static bool IsHorizontal( XYZ v ) 186 | { 187 | return IsZero( v.Z ); 188 | } 189 | 190 | public static bool IsVertical( XYZ v ) 191 | { 192 | return IsZero( v.X ) && IsZero( v.Y ); 193 | } 194 | 195 | public static bool IsVertical( XYZ v, double tolerance ) 196 | { 197 | return IsZero( v.X, tolerance ) 198 | && IsZero( v.Y, tolerance ); 199 | } 200 | 201 | public static bool IsHorizontal( Edge e ) 202 | { 203 | XYZ p = e.Evaluate( 0 ); 204 | XYZ q = e.Evaluate( 1 ); 205 | return IsHorizontal( q - p ); 206 | } 207 | 208 | public static bool IsHorizontal( PlanarFace f ) 209 | { 210 | return IsVertical( f.FaceNormal ); 211 | } 212 | 213 | public static bool IsVertical( PlanarFace f ) 214 | { 215 | return IsHorizontal( f.FaceNormal ); 216 | } 217 | 218 | public static bool IsVertical( CylindricalFace f ) 219 | { 220 | return IsVertical( f.Axis ); 221 | } 222 | 223 | /// 224 | /// Minimum slope for a vector to be considered 225 | /// to be pointing upwards. Slope is simply the 226 | /// relationship between the vertical and 227 | /// horizontal components. 228 | /// 229 | const double _minimumSlope = 0.3; 230 | 231 | /// 232 | /// Return true if the Z coordinate of the 233 | /// given vector is positive and the slope 234 | /// is larger than the minimum limit. 235 | /// 236 | public static bool PointsUpwards( XYZ v ) 237 | { 238 | double horizontalLength = v.X * v.X + v.Y * v.Y; 239 | double verticalLength = v.Z * v.Z; 240 | 241 | return 0 < v.Z 242 | && _minimumSlope 243 | < verticalLength / horizontalLength; 244 | 245 | //return _eps < v.Normalize().Z; 246 | //return _eps < v.Normalize().Z && IsVertical( v.Normalize(), tolerance ); 247 | } 248 | 249 | /// 250 | /// Return the maximum value from an array of real numbers. 251 | /// 252 | public static double Max( double[] a ) 253 | { 254 | Debug.Assert( 1 == a.Rank, "expected one-dimensional array" ); 255 | Debug.Assert( 0 == a.GetLowerBound( 0 ), "expected zero-based array" ); 256 | Debug.Assert( 0 < a.GetUpperBound( 0 ), "expected non-empty array" ); 257 | double max = a[0]; 258 | for( int i = 1; i <= a.GetUpperBound( 0 ); ++i ) 259 | { 260 | if( max < a[i] ) 261 | { 262 | max = a[i]; 263 | } 264 | } 265 | return max; 266 | } 267 | #endregion // Geometrical Comparison 268 | 269 | #region Unit Handling 270 | 271 | const double _inchToMm = 25.4; 272 | const double _footToMm = 12 * _inchToMm; 273 | const double _footToMeter = 0.001 * _footToMm; 274 | const double _sqfToSqm = _footToMeter * _footToMeter; 275 | 276 | 277 | /// 278 | /// Convert a given length in feet to millimetres, 279 | /// rounded to the closest millimetre. 280 | /// 281 | public static int FootToMmInt( double length ) 282 | { 283 | return (int) Math.Round( _footToMm * length, 284 | MidpointRounding.AwayFromZero ); 285 | } 286 | 287 | /// 288 | /// Convert a given length in square feet 289 | /// to square metres. 290 | /// 291 | public static double SquareFootToSquareMeter( 292 | double area ) 293 | { 294 | return _sqfToSqm * area; 295 | } 296 | #endregion // Unit Handling 297 | 298 | #region Formatting 299 | /// 300 | /// Return an English plural suffix for the given 301 | /// number of items, i.e. 's' for zero or more 302 | /// than one, and nothing for exactly one. 303 | /// 304 | public static string PluralSuffix( int n ) 305 | { 306 | return 1 == n ? "" : "s"; 307 | } 308 | 309 | /// 310 | /// Return a string for a real number 311 | /// formatted to two decimal places. 312 | /// 313 | public static string RealString( double a ) 314 | { 315 | return a.ToString( "0.##" ); 316 | } 317 | 318 | /// 319 | /// Return a string listing the space-delimited X 320 | /// and Y coordinates converted from feet to millimetres 321 | /// from a list of XYZ vertices in imperial feet. 322 | /// 323 | static string XyzListTo3dPointString( 324 | List vertices ) 325 | { 326 | return string.Join( " ", 327 | vertices.Select( p 328 | => new IntPoint3d( p.X, p.Y, p.Z ) 329 | .ToString( true ) ) ); 330 | } 331 | #endregion // Formatting 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /img/rac_basic_sample_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/RoomVolumeDirectShape/57cb677cee5a530f13226faea361b114bda019e0/img/rac_basic_sample_project.png -------------------------------------------------------------------------------- /img/rac_basic_sample_project_room_volumes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/RoomVolumeDirectShape/57cb677cee5a530f13226faea361b114bda019e0/img/rac_basic_sample_project_room_volumes.png --------------------------------------------------------------------------------