├── .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 |
--------------------------------------------------------------------------------