├── .gitignore ├── LICENSE ├── README.md ├── TrackChanges.sln └── TrackChanges ├── Command.cs ├── Properties └── AssemblyInfo.cs ├── TrackChanges.addin └── TrackChanges.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Microsoft Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Microsoft Azure Emulator 159 | ecf/ 160 | rcf/ 161 | 162 | # Microsoft Azure ApplicationInsights config file 163 | ApplicationInsights.config 164 | 165 | # Windows Store app package directory 166 | AppPackages/ 167 | BundleArtifacts/ 168 | 169 | # Visual Studio cache files 170 | # files ending in .cache can be ignored 171 | *.[Cc]ache 172 | # but keep track of directories ending in .cache 173 | !*.[Cc]ache/ 174 | 175 | # Others 176 | ClientBin/ 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.pfx 182 | *.publishsettings 183 | node_modules/ 184 | orleans.codegen.cs 185 | 186 | # RIA/Silverlight projects 187 | Generated_Code/ 188 | 189 | # Backup & report files from converting an old project file 190 | # to a newer Visual Studio version. Backup files are not needed, 191 | # because we have git ;-) 192 | _UpgradeReport_Files/ 193 | Backup*/ 194 | UpgradeLog*.XML 195 | UpgradeLog*.htm 196 | 197 | # SQL Server files 198 | *.mdf 199 | *.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | 209 | # GhostDoc plugin setting file 210 | *.GhostDoc.xml 211 | 212 | # Node.js Tools for Visual Studio 213 | .ntvs_analysis.dat 214 | 215 | # Visual Studio 6 build log 216 | *.plg 217 | 218 | # Visual Studio 6 workspace options file 219 | *.opt 220 | 221 | # Visual Studio LightSwitch build output 222 | **/*.HTMLClient/GeneratedArtifacts 223 | **/*.DesktopClient/GeneratedArtifacts 224 | **/*.DesktopClient/ModelManifest.xml 225 | **/*.Server/GeneratedArtifacts 226 | **/*.Server/ModelManifest.xml 227 | _Pvt_Extensions 228 | 229 | # Paket dependency manager 230 | .paket/paket.exe 231 | 232 | # FAKE - F# Make 233 | .fake/ 234 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | # TrackChanges 2 | 3 | TrackChanges is a C# .NET Revit add-in that tracks Revit BIM database modification by creating and comparing snapshots of element properties. 4 | 5 | For more information, please refer 6 | to [The Building Coder](http://thebuildingcoder.typepad.com) and 7 | the detailed article 8 | on [Tracking Element Modification](http://thebuildingcoder.typepad.com/blog/2016/01/tracking-element-modification.html). 9 | 10 | This repository is now stagnant, because the [enhancement](#enhancement) described below is implemented in a separate 11 | new [TrackChangesCloud repo](https://github.com/jeremytammik/TrackChangesCloud) to 12 | retain the impressive simplicity of this project unsullied. 13 | 14 | 15 | ## Enhancement 16 | 17 | I am pondering an enhancement of this external command based add-in that I suggested to Tim Corneliussen in 18 | the [Revit API discussion forum](http://forums.autodesk.com/t5/revit-api/bd-p/160) thread 19 | on [dynamic model update after loading family](http://forums.autodesk.com/t5/revit-api/dynamic-model-update-after-loading-family/m-p/6052310): 20 | 21 | Tim says he needs to track changes. 22 | 23 | I suggested that he take a look at this. 24 | 25 | **Response:** Your solution looks really impressive. I haven't had the chance to implement the main fundamentals in my project yet. As a starting programmer, the concept of hash code is still new to me but it looks like the right way to go. My main concern is how it will affect the performance of the routine. 26 | 27 | The main purpose of my tool is that each addition or modification will be registered by modifying some parameters including a "time-parameter" and "date-parameter". To do so, but correct me if I'm wrong, I need to trigger an event or use a DMU to determine when an element is added or modified. 28 | 29 | Maybe I can use your snapshot technique combining it with a DMU. But doing so the DMU also collects a lot of data next to the snapshot routine. Is this necessary? Are there alternatives to avoid this sort of useless multiple data collecting? 30 | 31 | Do elements themselves contain relevant information about their own creation or modification (perhaps a certain property that most people aren't aware of)? If so, I can use a single event (sort of like your suggestion on your blog), for example the DocumentSavingAs event. Last possible solution I can think of at the moment is a way to look even deeper in to the updaterdata/-information hoping it contains more general information about the addition or modification of the relevant elements. 32 | 33 | Hoping that you'll understand the scenario I'm describing. For now I will try to use the snapshot routine combining it with a DMU and a viewactivating event. Last mentioned will be used to determine whether another document becomes active (when a user has opened multiple projects). I will place an update when I have successfully created a working solution to discuss the results with you fellow readers. If someone can tell me if this solution probably won't really work please do so. 34 | 35 | **Answer:** Thank you very much for your appreciation. 36 | 37 | I think the main characteristic of the modification tracker is simplicity, rather than impressiveness. 38 | 39 | Of course, simplicity is much more impressive than impressiveness :-) 40 | 41 | If you want to be notified on every single modification of an element and store that information immediately, then indeed you can and have to use either DMU or the DocumentChanged event. 42 | 43 | The latter does not allow you to modify anything in the same transaction, though, whereas the former does. 44 | 45 | If you want to guarantee that your date and time markers stored in Revit parameters are always up to date, immediately, then you need to use DMU. 46 | 47 | But do you really need that? 48 | 49 | You need to understand that DMU is complex and adds a significant burden to Revit, depending on how many elements trigger it, which in your case would be many. 50 | 51 | Do you really need to keep track of the element modification on a split second-by-second basis? 52 | 53 | Would it not be enough to track changes every minute, or every ten minutes? 54 | 55 | If so, then you can vastly simplify your approach and vastly reduce the burden on Revit by using the modification tracker and completely avoiding DMU and the DocumentChanged event. 56 | 57 | Just track changes based on snapshots taken every X minutes, for instance. 58 | 59 | Regarding the issue of the hash code: that is a minor detail, and pretty irrelevant. 60 | 61 | You can just store the full data. Depending on what criteria you use to define when an element has changed, you might need to store a lot of information for each element. 62 | 63 | I suggested the hash code as a way to reduce and unify that data storage, but that has absolutely nothing to do with the fundamental concept. 64 | 65 | To quote the original post: "We use the hash code to determine whether the state has been modified compared to a new element state snapshot made at a later time. We could obviously also store the entire original string representation instead of using a hash code. The hash code is small and handy, whereas the entire string contains all the original data. It is up to you to choose which you would like to use." 66 | 67 | The hash code will not affect performance much, just reduce the memory used to cache the starting snapshot. 68 | 69 | I would recommend thinking this through in depth and peace and quiet. 70 | 71 | If you do not require split-second time slice data, I would avoid the DMU and DocumentChanged events, both, completely. 72 | 73 | I am very much looking forward to hearing and discussing your further thoughts on this. 74 | 75 | 76 | 77 | ## Author 78 | 79 | Jeremy Tammik, 80 | [The Building Coder](http://thebuildingcoder.typepad.com) and 81 | [The 3D Web Coder](http://the3dwebcoder.typepad.com), 82 | [ADN](http://www.autodesk.com/adn) 83 | [Open](http://www.autodesk.com/adnopen), 84 | [Autodesk Inc.](http://www.autodesk.com) 85 | 86 | ## Contributors 87 | 88 | - Jason Schaeffer [@joespiff](https://github.com/joespiff) 89 | 90 | 91 | ## License 92 | 93 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). 94 | Please see the [LICENSE](LICENSE) file for full details. 95 | -------------------------------------------------------------------------------- /TrackChanges.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrackChanges", "TrackChanges\TrackChanges.csproj", "{71D7AA3C-646B-490F-A349-F877070F16CD}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {71D7AA3C-646B-490F-A349-F877070F16CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {71D7AA3C-646B-490F-A349-F877070F16CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {71D7AA3C-646B-490F-A349-F877070F16CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {71D7AA3C-646B-490F-A349-F877070F16CD}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /TrackChanges/Command.cs: -------------------------------------------------------------------------------- 1 | #region Namespaces 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Security.Cryptography; 7 | using Autodesk.Revit.ApplicationServices; 8 | using Autodesk.Revit.Attributes; 9 | using Autodesk.Revit.DB; 10 | using Autodesk.Revit.UI; 11 | #endregion 12 | 13 | namespace TrackChanges 14 | { 15 | [Transaction( TransactionMode.ReadOnly )] 16 | public class Command : IExternalCommand 17 | { 18 | #region Geometrical Comparison 19 | const double _eps = 1.0e-9; 20 | 21 | public static double Eps 22 | { 23 | get 24 | { 25 | return _eps; 26 | } 27 | } 28 | 29 | public static double MinLineLength 30 | { 31 | get 32 | { 33 | return _eps; 34 | } 35 | } 36 | 37 | public static double TolPointOnPlane 38 | { 39 | get 40 | { 41 | return _eps; 42 | } 43 | } 44 | 45 | public static bool IsZero( 46 | double a, 47 | double tolerance ) 48 | { 49 | return tolerance > Math.Abs( a ); 50 | } 51 | 52 | public static bool IsZero( double a ) 53 | { 54 | return IsZero( a, _eps ); 55 | } 56 | 57 | public static bool IsEqual( double a, double b ) 58 | { 59 | return IsZero( b - a ); 60 | } 61 | 62 | public static int Compare( double a, double b ) 63 | { 64 | return IsEqual( a, b ) ? 0 : ( a < b ? -1 : 1 ); 65 | } 66 | 67 | public static int Compare( XYZ p, XYZ q ) 68 | { 69 | int d = Compare( p.X, q.X ); 70 | 71 | if( 0 == d ) 72 | { 73 | d = Compare( p.Y, q.Y ); 74 | 75 | if( 0 == d ) 76 | { 77 | d = Compare( p.Z, q.Z ); 78 | } 79 | } 80 | return d; 81 | } 82 | #endregion // Geometrical Comparison 83 | 84 | #region String formatting 85 | /// 86 | /// Convert a string to a byte array. 87 | /// 88 | static byte[] GetBytes( string str ) 89 | { 90 | byte[] bytes = new byte[str.Length 91 | * sizeof( char )]; 92 | 93 | System.Buffer.BlockCopy( str.ToCharArray(), 94 | 0, bytes, 0, bytes.Length ); 95 | 96 | return bytes; 97 | } 98 | 99 | #region OBSOLETE 100 | /// 101 | /// Define a project identifier for the 102 | /// given Revit document. 103 | /// 104 | public static string GetProjectIdentifier( 105 | Document doc ) 106 | { 107 | SHA256 hasher = SHA256Managed.Create(); 108 | 109 | string key = System.Environment.MachineName 110 | + ":" + doc.PathName; 111 | 112 | byte[] hashValue = hasher.ComputeHash( GetBytes( 113 | key ) ); 114 | 115 | string hashb64 = Convert.ToBase64String( 116 | hashValue ); 117 | 118 | return hashb64.Replace( '/', '_' ); 119 | } 120 | #endregion // OBSOLETE 121 | 122 | /// 123 | /// Return a string for a real number 124 | /// formatted to two decimal places. 125 | /// 126 | public static string RealString( double a ) 127 | { 128 | return a.ToString( "0.##" ); 129 | } 130 | 131 | /// 132 | /// Return a string for an XYZ point 133 | /// or vector with its coordinates 134 | /// formatted to two decimal places. 135 | /// 136 | public static string PointString( XYZ p ) 137 | { 138 | return string.Format( "({0},{1},{2})", 139 | RealString( p.X ), 140 | RealString( p.Y ), 141 | RealString( p.Z ) ); 142 | } 143 | 144 | /// 145 | /// Return a string for this bounding box 146 | /// with its coordinates formatted to two 147 | /// decimal places. 148 | /// 149 | public static string BoundingBoxString( 150 | BoundingBoxXYZ bb ) 151 | { 152 | return string.Format( "({0},{1})", 153 | PointString( bb.Min ), 154 | PointString( bb.Max ) ); 155 | } 156 | 157 | /// 158 | /// Return a string for this point array 159 | /// with its coordinates formatted to two 160 | /// decimal places. 161 | /// 162 | public static string PointArrayString( IList pts ) 163 | { 164 | return string.Join( ", ", 165 | pts.Select( 166 | p => PointString( p ) ) ); 167 | } 168 | 169 | /// 170 | /// Return a string for this curve with its 171 | /// tessellated point coordinates formatted 172 | /// to two decimal places. 173 | /// 174 | public static string CurveTessellateString( 175 | Curve curve ) 176 | { 177 | return PointArrayString( curve.Tessellate() ); 178 | } 179 | 180 | /// 181 | /// Return a string for this curve with its 182 | /// tessellated point coordinates formatted 183 | /// to two decimal places. 184 | /// 185 | public static string LocationString( 186 | Location location ) 187 | { 188 | LocationPoint lp = location as LocationPoint; 189 | LocationCurve lc = ( null == lp ) 190 | ? location as LocationCurve 191 | : null; 192 | 193 | return null == lp 194 | ? ( null == lc 195 | ? null 196 | : CurveTessellateString( lc.Curve ) ) 197 | : PointString( lp.Point ); 198 | } 199 | 200 | /// 201 | /// Return a JSON string representing a dictionary 202 | /// of the given parameter names and values. 203 | /// 204 | public static string GetPropertiesJson( 205 | IList parameters ) 206 | { 207 | int n = parameters.Count; 208 | List a = new List( n ); 209 | foreach( Parameter p in parameters ) 210 | { 211 | a.Add( string.Format( "\"{0}\":\"{1}\"", 212 | p.Definition.Name, p.AsValueString() ) ); 213 | } 214 | a.Sort(); 215 | string s = string.Join( ",", a ); 216 | return "{" + s + "}"; 217 | } 218 | 219 | /// 220 | /// Return a string describing the given element: 221 | /// .NET type name, 222 | /// category name, 223 | /// family and symbol name for a family instance, 224 | /// element id and element name. 225 | /// 226 | public static string ElementDescription( 227 | Element e ) 228 | { 229 | if( null == e ) 230 | { 231 | return ""; 232 | } 233 | 234 | // For a wall, the element name equals the 235 | // wall type name, which is equivalent to the 236 | // family name ... 237 | 238 | FamilyInstance fi = e as FamilyInstance; 239 | 240 | string typeName = e.GetType().Name; 241 | 242 | string categoryName = ( null == e.Category ) 243 | ? string.Empty 244 | : e.Category.Name + " "; 245 | 246 | string familyName = ( null == fi ) 247 | ? string.Empty 248 | : fi.Symbol.Family.Name + " "; 249 | 250 | string symbolName = ( null == fi 251 | || e.Name.Equals( fi.Symbol.Name ) ) 252 | ? string.Empty 253 | : fi.Symbol.Name + " "; 254 | 255 | return string.Format( "{0} {1}{2}{3}<{4} {5}>", 256 | typeName, categoryName, familyName, 257 | symbolName, e.Id.IntegerValue, e.Name ); 258 | } 259 | 260 | public static string ElementDescription( 261 | Document doc, 262 | int element_id ) 263 | { 264 | return ElementDescription( doc.GetElement( 265 | new ElementId( element_id ) ) ); 266 | } 267 | #endregion // String formatting 268 | 269 | #region Retrieve solid vertices 270 | /// 271 | /// Define equality between XYZ objects, ensuring 272 | /// that almost equal points compare equal. 273 | /// 274 | class XyzEqualityComparer : IEqualityComparer 275 | { 276 | public bool Equals( XYZ p, XYZ q ) 277 | { 278 | return p.IsAlmostEqualTo( q ); 279 | } 280 | 281 | public int GetHashCode( XYZ p ) 282 | { 283 | return PointString( p ).GetHashCode(); 284 | } 285 | } 286 | 287 | /// 288 | /// Add the vertices of the given solid to 289 | /// the vertex lookup dictionary. 290 | /// 291 | static void AddVertices( 292 | Dictionary vertexLookup, 293 | Transform t, 294 | Solid s ) 295 | { 296 | Debug.Assert( 0 < s.Edges.Size, 297 | "expected a non-empty solid" ); 298 | 299 | foreach( Face f in s.Faces ) 300 | { 301 | Mesh m = f.Triangulate(); 302 | 303 | if (m != null) 304 | { 305 | foreach( XYZ p in m.Vertices ) 306 | { 307 | XYZ q = t.OfPoint( p ); 308 | if( !vertexLookup.ContainsKey( q ) ) 309 | { 310 | vertexLookup.Add( q, 1 ); 311 | } 312 | else 313 | { 314 | ++vertexLookup[q]; 315 | } 316 | } 317 | } 318 | } 319 | } 320 | 321 | /// 322 | /// Recursively add vertices of all solids found 323 | /// in the given geometry to the vertex lookup. 324 | /// Untested! 325 | /// 326 | static void AddVertices( 327 | Dictionary vertexLookup, 328 | Transform t, 329 | GeometryElement geo ) 330 | { 331 | if( null == geo ) 332 | { 333 | Debug.Assert( null != geo, "null GeometryElement" ); 334 | throw new System.ArgumentException( "null GeometryElement" ); 335 | } 336 | 337 | foreach( GeometryObject obj in geo ) 338 | { 339 | Solid solid = obj as Solid; 340 | 341 | if( null != solid ) 342 | { 343 | if( 0 < solid.Faces.Size ) 344 | { 345 | AddVertices( vertexLookup, t, solid ); 346 | } 347 | } 348 | else 349 | { 350 | GeometryInstance inst = obj as GeometryInstance; 351 | 352 | if( null != inst ) 353 | { 354 | //GeometryElement geoi = inst.GetInstanceGeometry(); 355 | GeometryElement geos = inst.GetSymbolGeometry(); 356 | 357 | //Debug.Assert( null == geoi || null == geos, 358 | // "expected either symbol or instance geometry, not both" ); 359 | 360 | Debug.Assert( null != inst.Transform, 361 | "null inst.Transform" ); 362 | 363 | //Debug.Assert( null != inst.GetSymbolGeometry(), 364 | // "null inst.GetSymbolGeometry" ); 365 | 366 | if( null != geos ) 367 | { 368 | AddVertices( vertexLookup, 369 | inst.Transform.Multiply( t ), 370 | geos ); 371 | } 372 | } 373 | } 374 | } 375 | } 376 | 377 | #region OBSOLETE 378 | /// 379 | /// Retrieve the first non-empty solid found for 380 | /// the given element. In case the element is a 381 | /// family instance, it may have its own non-empty 382 | /// solid, in which case we use that. Otherwise we 383 | /// search the symbol geometry. If we use the 384 | /// symbol geometry, we have to keep track of the 385 | /// instance transform to map it to the actual 386 | /// instance project location. 387 | /// 388 | static Solid GetSolid2( Element e, Options opt ) 389 | { 390 | GeometryElement geo = e.get_Geometry( opt ); 391 | 392 | Dictionary a 393 | = new Dictionary( 394 | new XyzEqualityComparer() ); 395 | 396 | Solid solid = null; 397 | GeometryInstance inst = null; 398 | Transform t = Transform.Identity; 399 | 400 | // Some family elements have no own solids, so we 401 | // retrieve the geometry from the symbol instead; 402 | // others do have own solids on the instance itself 403 | // and no contents in the instance geometry 404 | // (e.g. in rst_basic_sample_project.rvt). 405 | 406 | foreach( GeometryObject obj in geo ) 407 | { 408 | solid = obj as Solid; 409 | 410 | if( null != solid 411 | && 0 < solid.Faces.Size ) 412 | { 413 | break; 414 | } 415 | 416 | inst = obj as GeometryInstance; 417 | } 418 | 419 | if( null == solid && null != inst ) 420 | { 421 | geo = inst.GetSymbolGeometry(); 422 | t = inst.Transform; 423 | 424 | foreach( GeometryObject obj in geo ) 425 | { 426 | solid = obj as Solid; 427 | 428 | if( null != solid 429 | && 0 < solid.Faces.Size ) 430 | { 431 | break; 432 | } 433 | } 434 | } 435 | return solid; 436 | } 437 | #endregion // OBSOLETE 438 | 439 | /// 440 | /// Return a sorted list of all unique vertices 441 | /// of all solids in the given element's geometry 442 | /// in lexicographical order. 443 | /// 444 | static List GetCanonicVertices( Element e ) 445 | { 446 | GeometryElement geo = e.get_Geometry( new Options() ); 447 | Transform t = Transform.Identity; 448 | 449 | Dictionary vertexLookup 450 | = new Dictionary( 451 | new XyzEqualityComparer() ); 452 | 453 | AddVertices( vertexLookup, t, geo ); 454 | 455 | List keys = new List( vertexLookup.Keys ); 456 | 457 | keys.Sort( Compare ); 458 | 459 | return keys; 460 | } 461 | #endregion // Retrieve solid vertices 462 | 463 | #region Retrieve elements of interest 464 | /// 465 | /// Retrieve all elements to track. 466 | /// It is up to you to decide which elements 467 | /// are of interest to you. 468 | /// 469 | static IEnumerable GetTrackedElements( 470 | Document doc ) 471 | { 472 | Categories cats = doc.Settings.Categories; 473 | 474 | List a = new List(); 475 | 476 | foreach( Category c in cats ) 477 | { 478 | if( CategoryType.Model == c.CategoryType ) 479 | { 480 | a.Add( new ElementCategoryFilter( c.Id ) ); 481 | } 482 | } 483 | 484 | ElementFilter isModelCategory 485 | = new LogicalOrFilter( a ); 486 | 487 | Options opt = new Options(); 488 | 489 | return new FilteredElementCollector( doc ) 490 | .WhereElementIsNotElementType() 491 | .WhereElementIsViewIndependent() 492 | .WherePasses( isModelCategory ) 493 | .Where( e => 494 | ( null != e.get_BoundingBox( null ) ) 495 | && ( null != e.get_Geometry( opt ) ) ); 496 | } 497 | #endregion // Retrieve elements of interest 498 | 499 | #region Store element state 500 | /// 501 | /// Return a string representing the given element 502 | /// state. This is the information you wish to track. 503 | /// It is up to you to ensure that all data you are 504 | /// interested in really is included in this snapshot. 505 | /// In this case, we ignore all elements that do not 506 | /// have a valid bounding box. 507 | /// 508 | static string GetElementState( Element e ) 509 | { 510 | string s = null; 511 | 512 | BoundingBoxXYZ bb = e.get_BoundingBox( null ); 513 | 514 | if( null != bb ) 515 | { 516 | List properties = new List(); 517 | 518 | properties.Add( ElementDescription( e ) 519 | + " at " + LocationString( e.Location ) ); 520 | 521 | if( !( e is FamilyInstance ) ) 522 | { 523 | properties.Add( "Box=" 524 | + BoundingBoxString( bb ) ); 525 | 526 | properties.Add( "Vertices=" 527 | + PointArrayString( GetCanonicVertices( e ) ) ); 528 | } 529 | 530 | properties.Add( "Parameters=" 531 | + GetPropertiesJson( e.GetOrderedParameters() ) ); 532 | 533 | s = string.Join( ", ", properties ); 534 | 535 | //Debug.Print( s ); 536 | } 537 | return s; 538 | } 539 | #endregion // Store element state 540 | 541 | #region Creating a Database State Snapshot 542 | /// 543 | /// Return a dictionary mapping element id values 544 | /// to hash codes of the element state strings. 545 | /// This represents a snapshot of the current 546 | /// database state. 547 | /// 548 | static Dictionary GetSnapshot( 549 | IEnumerable a ) 550 | { 551 | Dictionary d 552 | = new Dictionary(); 553 | 554 | SHA256 hasher = SHA256Managed.Create(); 555 | 556 | foreach( Element e in a ) 557 | { 558 | //Debug.Print( e.Id.IntegerValue.ToString() 559 | // + " " + e.GetType().Name ); 560 | 561 | string s = GetElementState( e ); 562 | 563 | if( null != s ) 564 | { 565 | string hashb64 = Convert.ToBase64String( 566 | hasher.ComputeHash( GetBytes( s ) ) ); 567 | 568 | d.Add( e.Id.IntegerValue, hashb64 ); 569 | } 570 | } 571 | return d; 572 | } 573 | #endregion // Creating a Database State Snapshot 574 | 575 | #region Report differences 576 | /// 577 | /// Compare the start and end states and report the 578 | /// differences found. In this implementation, we 579 | /// just store a hash code of the element state. 580 | /// If you choose to store the full string 581 | /// representation, you can use that for comparison, 582 | /// and then report exactly what changed and the 583 | /// original values as well. 584 | /// 585 | static void ReportDifferences( 586 | Document doc, 587 | Dictionary start_state, 588 | Dictionary end_state ) 589 | { 590 | int n1 = start_state.Keys.Count; 591 | int n2 = end_state.Keys.Count; 592 | 593 | List keys = new List( start_state.Keys ); 594 | 595 | foreach( int id in end_state.Keys ) 596 | { 597 | if( !keys.Contains( id ) ) 598 | { 599 | keys.Add( id ); 600 | } 601 | } 602 | 603 | keys.Sort(); 604 | 605 | int n = keys.Count; 606 | 607 | Debug.Print( 608 | "{0} elements before, {1} elements after, {2} total", 609 | n1, n2, n ); 610 | 611 | int nAdded = 0; 612 | int nDeleted = 0; 613 | int nModified = 0; 614 | int nIdentical = 0; 615 | List report = new List(); 616 | 617 | foreach( int id in keys ) 618 | { 619 | if( !start_state.ContainsKey( id ) ) 620 | { 621 | ++nAdded; 622 | report.Add( id.ToString() + " added " 623 | + ElementDescription( doc, id ) ); 624 | } 625 | else if( !end_state.ContainsKey( id ) ) 626 | { 627 | ++nDeleted; 628 | report.Add( id.ToString() + " deleted" ); 629 | } 630 | else if( start_state[id] != end_state[id] ) 631 | { 632 | ++nModified; 633 | report.Add( id.ToString() + " modified " 634 | + ElementDescription( doc, id ) ); 635 | } 636 | else 637 | { 638 | ++nIdentical; 639 | } 640 | } 641 | 642 | string msg = string.Format( 643 | "Stopped tracking changes now.\r\n" 644 | + "{0} deleted, {1} added, {2} modified, " 645 | + "{3} identical elements:", 646 | nDeleted, nAdded, nModified, nIdentical ); 647 | 648 | string s = string.Join( "\r\n", report ); 649 | 650 | Debug.Print( msg + "\r\n" + s ); 651 | TaskDialog dlg = new TaskDialog( "Track Changes" ); 652 | dlg.MainInstruction = msg; 653 | dlg.MainContent = s; 654 | dlg.Show(); 655 | } 656 | #endregion // Report differences 657 | 658 | /// 659 | /// Current snapshot of database state. 660 | /// You could also store the entire element state 661 | /// strings here, not just their hash code, to 662 | /// report their complete original and modified 663 | /// values. 664 | /// 665 | static Dictionary _start_state = null; 666 | 667 | #region External Command Mainline Execute Method 668 | public Result Execute( 669 | ExternalCommandData commandData, 670 | ref string message, 671 | ElementSet elements ) 672 | { 673 | UIApplication uiapp = commandData.Application; 674 | UIDocument uidoc = uiapp.ActiveUIDocument; 675 | Application app = uiapp.Application; 676 | Document doc = uidoc.Document; 677 | 678 | IEnumerable a = GetTrackedElements( doc ); 679 | 680 | if( null == _start_state ) 681 | { 682 | _start_state = GetSnapshot( a ); 683 | TaskDialog.Show( "Track Changes", 684 | "Started tracking changes now." ); 685 | } 686 | else 687 | { 688 | Dictionary end_state = GetSnapshot( a ); 689 | ReportDifferences( doc, _start_state, end_state ); 690 | _start_state = null; 691 | } 692 | return Result.Succeeded; 693 | } 694 | #endregion // External Command Mainline Execute Method 695 | } 696 | } 697 | 698 | // Z:\a\rvt\little_house_2016.rvt 699 | // C:\Program Files\Autodesk\Revit 2016\Samples\rac_advanced_sample_project.rvt 700 | // Z:\a\rvt\rme_2016_empty.rvt -------------------------------------------------------------------------------- /TrackChanges/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremytammik/TrackChanges/05eb705da753940089108e051197ce8d9010e348/TrackChanges/Properties/AssemblyInfo.cs -------------------------------------------------------------------------------- /TrackChanges/TrackChanges.addin: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Command TrackChanges 5 | Some description for TrackChanges 6 | TrackChanges.dll 7 | TrackChanges.Command 8 | e3210cad-a710-4b6f-9470-999642bf8df4 9 | com.typepad.thebuildingcoder 10 | The Building Coder, http://thebuildingcoder.typepad.com 11 | 12 | 13 | -------------------------------------------------------------------------------- /TrackChanges/TrackChanges.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | None 6 | 7 | 8 | 9 | 10 | Debug 11 | AnyCPU 12 | 13 | 14 | 15 | 16 | {71D7AA3C-646B-490F-A349-F877070F16CD} 17 | Library 18 | Properties 19 | TrackChanges 20 | v4.5 21 | 512 22 | 23 | 24 | true 25 | full 26 | false 27 | bin\Debug\ 28 | DEBUG;TRACE 29 | prompt 30 | 4 31 | Program 32 | $(ProgramW6432)\Autodesk\Revit 2016\Revit.exe 33 | false 34 | 35 | 36 | pdbonly 37 | true 38 | bin\Release\ 39 | TRACE 40 | prompt 41 | 4 42 | Program 43 | $(ProgramW6432)\Autodesk\Revit 2016\Revit.exe 44 | false 45 | 46 | 47 | 48 | $(ProgramW6432)\Autodesk\Revit 2016\RevitAPI.dll 49 | False 50 | 51 | 52 | $(ProgramW6432)\Autodesk\Revit 2016\RevitAPIUI.dll 53 | False 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | copy "$(ProjectDir)TrackChanges.addin" "$(AppData)\Autodesk\REVIT\Addins\2016" 71 | copy "$(ProjectDir)bin\debug\TrackChanges.dll" "$(AppData)\Autodesk\REVIT\Addins\2016" 72 | 73 | --------------------------------------------------------------------------------