loops )
199 | {
200 | JtBoundingBox2dInt bbFrom = new JtBoundingBox2dInt();
201 | foreach( JtLoops a in loops )
202 | {
203 | bbFrom.ExpandToContain( a.BoundingBox );
204 | }
205 | //bbFrom.AdjustBy();
206 |
207 | // Adjust target rectangle height to the
208 | // displayee loop height.
209 |
210 | int width = _form_width;
211 | int height = (int) (width * bbFrom.AspectRatio + 0.5);
212 |
213 | // the bounding box fills the rectangle
214 | // perfectly and completely, inverted and
215 | // non-uniformly distorted.
216 |
217 | // Reduce target rectangle slightly so line
218 | // segments along the outer edge are visible.
219 | //width -= 6;
220 | //height -= 6;
221 |
222 | // Specify transformation target rectangle
223 | // including a margin.
224 |
225 | int bottom = height - (_margin + _margin);
226 |
227 | Point[] parallelogramPoints = new Point[] {
228 | new Point( _margin, bottom ), // upper left
229 | new Point( width - _margin, bottom ), // upper right
230 | new Point( _margin, _margin ) // lower left
231 | };
232 |
233 | // Transform from native loop coordinate system
234 | // to target display coordinates.
235 |
236 | Matrix transform = new Matrix(
237 | bbFrom.Rectangle, parallelogramPoints );
238 |
239 | //int edge_width = 4;
240 |
241 | Bitmap bmp = new Bitmap( width, height );
242 |
243 | Graphics graphics = Graphics.FromImage( bmp );
244 |
245 | graphics.Clear( System.Drawing.Color.White );
246 |
247 | foreach( JtLoops a in loops )
248 | {
249 | DrawLoopsOnGraphics( graphics,
250 | a.GetGraphicsPathLines(), transform );
251 | }
252 | return bmp;
253 | }
254 | #endregion // DisplayRoom
255 |
256 | #region DisplayImageInForm
257 | ///
258 | /// Generate a form on the fly and display the
259 | /// given bitmap image in it in a picture box.
260 | ///
261 | /// Owner window
262 | /// Form caption
263 | /// Modal versus modeless
264 | /// Bitmap image to display
265 | public static void DisplayImageInForm(
266 | IWin32Window owner,
267 | string caption,
268 | bool modal,
269 | Bitmap bmp )
270 | {
271 | Form form = new Form();
272 | form.Text = caption;
273 |
274 | form.Size = new Size( bmp.Width + 7,
275 | bmp.Height + 13 );
276 |
277 | form.FormBorderStyle = FormBorderStyle
278 | .FixedToolWindow;
279 |
280 | PictureBox pb = new PictureBox();
281 | pb.Location = new System.Drawing.Point( 0, 0 );
282 | pb.Dock = System.Windows.Forms.DockStyle.Fill;
283 | pb.Size = bmp.Size;
284 | pb.Parent = form;
285 | pb.Image = bmp;
286 |
287 | if( modal )
288 | {
289 | form.ShowDialog( owner );
290 | }
291 | else
292 | {
293 | form.Show( owner );
294 | }
295 | }
296 | #endregion // DisplayImageInForm
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ElementOutline
2 |
3 | Revit C# .NET add-in to export 2D outlines of RVT project `Element` instances.
4 |
5 | Table of contents:
6 |
7 | - [Task – 2D polygon representing birds-eye view of an element](#2)
8 | - [CmdExtrusionAnalyzer](#3)
9 | - [Alternative approaches to determine 2D element outline](#4)
10 | - [Cmd2dBoolean](#5)
11 | - [CmdRoomOuterOutline](#6)
12 | - [Author](#7)
13 | - [License](#8)
14 |
15 | The add-in implements three external commands:
16 |
17 | - [CmdExtrusionAnalyzer](#3) – generate element outline using `ExtrusionAnalyzer`
18 | - [Cmd2dBoolean](#5) – generate element outline using 2D Booleans
19 | - [CmdRoomOuterOutline](#6) – outer room outline using 2D Booleans
20 |
21 | All three generate element outlines of various types in various ways.
22 |
23 | The first uses the Revit API and
24 | the [`ExtrusionAnalyzer` class](https://www.revitapidocs.com/2020/ba9e3283-6868-8834-e8bf-2ea9e7358930.htm).
25 |
26 | The other two make use of
27 | the [Clipper integer coordinate based 2D Boolean operations library](http://angusj.com/delphi/clipper.php).
28 |
29 | The add-in also implements a bunch of utilities for converting Revit coordinates to 2D data in millimetre units and displaying the resulting element outlines in a Windows form.
30 |
31 |
32 | ## Task – 2D Polygon Representing Birds-Eye View of an Element
33 |
34 | The goal is to export the 2D outlines of Revit `Element` instances, i.e., for each element, associate its element id or unique id with the list of X,Y coordinates describing a polygon representing the visual birds-eye view look of its outline.
35 |
36 | Additional requirements:
37 |
38 | - Address family instances as well as elements that might be built as part of the construction, including wall, floor, railing, ceiling, mechanical duct, panel, plumbing pipe.
39 | - Generate a separate outline in place for each element, directly in its appropriate location and orientation.
40 | - Output the result in a simple text file.
41 |
42 | There is no need for a rendered view, just coordinates defining a 2D polygon around the element.
43 |
44 | The goal is: given an element id, retrieve a list of X,Y coordinates describing the birds-eye view look of an element.
45 |
46 |
61 |
62 | For instance, here is an apartment layout showing a birdseye view of bathtubs, doors, toilets and other accessories:
63 |
64 |
65 |
66 |
67 |
68 | In end effect, we generate a dictionary mapping an element id or unique id to a list of space delimited pairs of X Y vertex coordinates in millimetres.
69 |
70 |
71 | ## CmdExtrusionAnalyzer
72 |
73 | This code was originally implemented as part of (and later extracted from)
74 | the [RoomEditorApp project](https://github.com/jeremytammik/RoomEditorApp).
75 |
76 | The approach implemented for the room editor is not based on the 2D view, but on the element geometry solids in the 3D view and the result of applying
77 | the [`ExtrusionAnalyzer` class](https://www.revitapidocs.com/2020/ba9e3283-6868-8834-e8bf-2ea9e7358930.htm) to them,
78 | creating a vertical projection of the 3D element shape onto the 2D XY plane.
79 | This approach is described in detail in the discussion on
80 | the [extrusion analyser and plan view boundaries](https://thebuildingcoder.typepad.com/blog/2013/04/extrusion-analyser-and-plan-view-boundaries.html).
81 |
82 | The [GeoSnoop .NET boundary curve loop visualisation](https://thebuildingcoder.typepad.com/blog/2013/04/geosnoop-net-boundary-curve-loop-visualisation.html) provides
83 | some example images of the resulting outlines.
84 |
85 | As you can see there, the outline generated is more precise and detailed than the standard 2D Revit representation.
86 |
87 | The standard plan view of the default desk and chair components look like this in Revit:
88 |
89 |
90 |
91 | The loops exported by the RoomEditorApp add-in for the same desk and chair look like this instead:
92 |
93 |
94 |
95 | E.g., for the desk, you notice the little bulges for the desk drawer handles sticking out a little bit beyond the desktop surface.
96 |
97 | For the chair, the arm rests are missing, because the solids used to model them do not make it through the extruson analyser, or maybe because the code ignores multiple disjunct loops.
98 |
99 | Here is a sample model with four elements highlighted in blue:
100 |
101 |
102 |
103 | For them, the CmdExtrusionAnalyzer command generates the following JSON file defining their outline polygon in SVG format:
104 |
105 |
106 | {"name":"pt2+20+7", "id":"576786", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cd12", "svg_path":"M-56862 -9150 L-56572 -9150 -56572 -14186 -56862 -14186Z"}
107 | {"name":"pt70/210", "id":"576925", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cd9d", "svg_path":"M-55672 -11390 L-55672 -11290 -55656 -11290 -55656 -11278 -55087 -11278 -55087 -11270 -55076 -11270 -55076 -11242 -55182 -11242 -55182 -11214 -55048 -11214 -55048 -11270 -55037 -11270 -55037 -11278 -54988 -11278 -54988 -11290 -54972 -11290 -54972 -11390Z"}
108 | {"name":"pt80/115", "id":"576949", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cdb5", "svg_path":"M-56572 -10580 L-56572 -9430 -55772 -9430 -55772 -10580Z"}
109 | {"name":"מנוע מזגן מפוצל", "id":"576972", "uid":"bc43ed2e-7e23-4f0e-9588-ab3c43f3d388-0008cdcc", "svg_path":"M-56753 -8031 L-56713 -8031 -56713 -8018 -56276 -8018 -56276 -8031 -56265 -8031 -56265 -8109 -56276 -8109 -56276 -8911 -56252 -8911 -56252 -8989 -56276 -8989 -56276 -9020 -56277 -9020 -56278 -9020 -56711 -9020 -56713 -9020 -56713 -8989 -56753 -8989 -56753 -8911 -56713 -8911 -56713 -8109 -56753 -8109Z"}
110 |
111 |
112 | `M`, `L` and `Z` stand for `moveto`, `lineto` and `close`, respectively. Repetitions of `L` can be omitted. Nice and succinct.
113 |
114 | However, the extrusion analyser approach obviously fails for all elements that do not define any solids, e.g., 2D elements represented only by curves and meshes.
115 |
116 | Hence the continued research to find an alternative approach and the implementation of `Cmd2dBoolean` described below making use of the Clipper library and 2D Booleans instead.
117 |
118 | In July 2019, I checked with the development team and asked whether they could suggest a better way to retrieve the 2D outline of an element.
119 |
120 | They responded that my `ExtrusionAnalyzer` approach seems like the best (and maybe only) way to achieve this right now.
121 |
122 | Considering Cmd2dBoolean, I might add the caveat 'using the Revit API' to the last statement.
123 |
124 |
125 | ## Alternative Approaches to Determine 2D Element Outline
126 |
127 | The `ExtrusionAnalyzer` approach based on element solids does not successfully address the task of generating the 2D birds-eye view outline for all Revit elements.
128 |
129 | I therefore explored other avenues.
130 |
131 | Concave hull:
132 |
133 | - http://ubicomp.algoritmi.uminho.pt/local/concavehull.html
134 | - https://towardsdatascience.com/the-concave-hull-c649795c0f0f
135 | - https://github.com/kubkon/powercrust
136 | - https://adared.ch/concaveman-cpp-a-very-fast-2d-concave-hull-maybe-even-faster-with-c-and-python/
137 | - https://www.codeproject.com/Articles/1201438/The-Concave-Hull-of-a-Set-of-Points
138 | - http://www.cs.ubc.ca/research/flann/
139 |
140 | 2D outline:
141 |
142 | - https://github.com/eppz/Unity.Library.eppz.Geometry
143 | - https://github.com/eppz/Clipper
144 | - https://github.com/eppz/Triangle.NET
145 | - https://en.wikipedia.org/wiki/Sweep_line_algorithm
146 | - https://stackoverflow.com/questions/4213117/the-generalization-of-bentley-ottmann-algorithm
147 | - https://ggolikov.github.io/bentley-ottman/
148 | - Joining unordered line segments – https://stackoverflow.com/questions/1436091/joining-unordered-line-segments
149 | - http://www3.cs.stonybrook.edu/~algorith/implement/sweep/implement.shtml
150 | - https://github.com/mikhaildubov/Computational-geometry/blob/master/2)%20Any%20segments%20intersection/src/ru/dubov/anysegmentsintersect/SegmentsIntersect.java
151 | - https://github.com/jeremytammik/wykobi/blob/master/wykobi_naive_group_intersections.inl
152 |
153 | Alpha shape:
154 |
155 | - https://en.wikipedia.org/wiki/Alpha_shape
156 | - https://pypi.org/project/alphashape/
157 | - https://alphashape.readthedocs.io/
158 |
159 | I determined that some elements have no solids, just meshes, hence the extrusion analyser approach cannot be used.
160 |
161 | Looked at the [alpha shape implementation here](https://pypi.org/project/alphashape).
162 |
163 | I worked on a 2D contour outline following algorithm, but it turned out quite complex.
164 |
165 | I had another idea for a much simpler approach using 2D Boolean operations, uniting all the solid faces and mesh faces into one single 2D polygon set.
166 |
167 | - Join all line segments into closed polygons
168 | - Union all the polygons using Clipper
169 |
170 | That seems to return robust results.
171 |
172 |
173 | ## Cmd2dBoolean
174 |
175 | I completed a new poly2d implementation using 2D Booleans instead of the solids and extrusion analyser.
176 | I expect it is significantly faster.
177 |
178 | The ElementOutline release 2020.0.0.10 exports outlines from both solids and 2D Booleans and generates identical results for both, so that is a good sign.
179 |
180 | Maybe meshes and solids cover all requirements.
181 | I am still experimenting and testing.
182 | What is missing besides meshes and solids?
183 |
184 | I tested successfully on an intercom element.
185 | It is not a mesh, just a circle, represented by a full closed arc.
186 | I implemented support to include circles as well as solids and meshes in the Boolean operation.
187 |
188 | I also implemented a utility `GeoSnoop` to display the loops generated in a temporary Windows form.
189 |
190 | Here is an image showing part of a sample Revit model in the middle including a wall, bathtub and intercom element and two GeoSnoop windows:
191 |
192 |
193 |
194 | The left GeoSnoop window shows the outline loops retrieved from the solids using the extrusion analyser.
195 | The right one shows the loops retrieved from the 2D Booleans, including closed arcs.
196 | Note the differences in the intercom and the bathtub drain.
197 |
198 | My target is to continue enhancing the 2D Booleans until they include all the solid loop information, so that we can then get rid of the solid and extrusion analyser code.
199 |
200 | Maybe all I need to do is to use LevelOfDetail = Fine?
201 |
202 | ```
203 | Options opt = new Options
204 | {
205 | IncludeNonVisibleObjects = true,
206 | DetailLevel = ViewDetailLevel.Fine
207 | };
208 | GeometryElement geomElem = element.get_Geometry(opt);
209 | ```
210 |
211 | I might try again with fine detail level.
212 | However, the circle already looks pretty good to me.
213 | In fact, right now, I think all I need is there, in the combination of the two approaches.
214 |
215 | The first image was generated by capturing data from a 2D view.
216 | Capturing the 2D Booleans from a 3D view gives us all we need, I think.
217 |
218 | Tested a few use-cases and it seems to be working fine.
219 |
220 | Currently, the production pipeline uses an implementation in Python based on
221 | the [Shapely library](https://github.com/Toblerity/Shapely) for
222 | manipulation and analysis of geometric objects to `union()` the triangles.
223 |
224 | Since it is slower, it would be better to switch to Clipper.
225 |
226 |
227 | ## CmdRoomOuterOutline
228 |
229 | I implemented the third command `CmdRoomOuterOutline` after an unsuccessful attempt at generating the outer outline of a room including its bounding elements
230 | by [specifying a list of offsets to `CreateViaOffset`](https://thebuildingcoder.typepad.com/blog/2019/12/dashboards-createviaoffset-and-room-outline-algorithms.html#3).
231 |
232 | After that failure, I suggested a number of alternative approaches
233 | to [determine the room outline including surrounding walls](https://thebuildingcoder.typepad.com/blog/2019/12/dashboards-createviaoffset-and-room-outline-algorithms.html#4).
234 |
235 | **Question:** I started to look at the possibility of tracing the outside of the walls several weeks ago, when I was at a loss utilising `CreateViaOffset`.
236 |
237 | I was finding it difficult to create the closed loop necessary, and particularly how I would achieve this were the wall thickness changes across its length.
238 |
239 | Could you point me in the right direction, possibly some sample code that I could examine and see if I could get it to work to my requirements.
240 |
241 | **Answer:** I see several possible alternative approaches avoiding the use of `CreateViaOffset`, based on:
242 |
243 | - Room boundary curves and wall thicknesses
244 | - Room boundary curves and wall bottom face edges
245 | - Projection of 3D union of room and wall solids
246 | - 2D union of room and wall footprints
247 |
248 | The most immediate and pure Revit API approach would be to get the curves representing the room boundaries, determine the wall thicknesses, offset the wall boundary curves outwards by wall thickness plus minimum offset, and ensure that everything is well connected by adding small connecting segments in the gaps where the offset jumps.
249 |
250 | Several slightly more complex pure Revit API approaches could be designed by using the wall solids instead of just offsetting the room boundary curves based on the wall thickness. For instance, we could query the wall bottom face for its edges, determine and patch together all the bits of edge segments required to go around the outside of the wall instead of the inside.
251 |
252 | Slightly more complex still, and still pure Revit API: determine the room closed shell solid, unite it with all the wall solids, and make use of the extrusion analyser to project this union vertically onto the XY plane and grab its outside edge.
253 |
254 | Finally, making use of a minimalistic yet powerful 2D Boolean operation library, perform the projection onto the XY plane first, and unite the room footprint with all its surrounding wall footprints in 2D instead. Note that the 2D Booleans are integer based. To make use of those, I convert the geometry from imperial feet units using real numbers to integer-based millimetres.
255 |
256 | The two latter approaches are both implemented in
257 | my [ElementOutline add-in](https://github.com/jeremytammik/ElementOutline).
258 |
259 | I mentioned it here in two previous threads:
260 |
261 | - [Question regarding SVG data](https://forums.autodesk.com/t5/revit-api-forum/question-regarding-svg-data-from-revit/m-p/9106146)
262 | - [How do I get the outline and stakeout path of a built-in loft family](https://forums.autodesk.com/t5/revit-api-forum/how-do-i-get-the-outline-and-stakeout-path-of-a-built-in-loft/m-p/9148138)
263 |
264 | Probably all the pure Revit API approaches will run into various problematic exceptional cases, whereas the 2D Booleans seem fast, reliable and robust and may well be able to handle all the exceptional cases that can possibly occur, so I would recommend trying that out first.
265 |
266 | I ended up implementing my suggestion in the new external command `CmdRoomOuterOutline`.
267 |
268 | It makes use of the 2D Boolean outline generation functionality implemented for Cmd2dBoolean, adding code to generate a polygon for the room boundary and unite it with all the bounding elements.
269 |
270 | It successfully handles the wall width sample model:
271 |
272 |
273 |
274 | It also gracefully handles the room separator situation:
275 |
276 |
277 |
278 |
279 | ## Author
280 |
281 | Jeremy Tammik, [The Building Coder](http://thebuildingcoder.typepad.com), [ADN](http://www.autodesk.com/adn) [Open](http://www.autodesk.com/adnopen), [Autodesk Inc.](http://www.autodesk.com)
282 |
283 |
284 | ## License
285 |
286 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT).
287 | Please see the [LICENSE](LICENSE) file for full details.
288 |
289 |
--------------------------------------------------------------------------------
/ElementOutline/ClipperRvt.cs:
--------------------------------------------------------------------------------
1 | #region Namespaces
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using Autodesk.Revit.DB;
7 | using Autodesk.Revit.DB.Architecture;
8 | using Autodesk.Revit.DB.IFC;
9 | using ClipperLib;
10 | #endregion
11 |
12 | namespace ElementOutline
13 | {
14 | using Polygon = List;
15 | using Polygons = List>;
16 | using LineSegment = Tuple;
17 |
18 | class ClipperRvt
19 | {
20 | ///
21 | /// Map Point2dInt coordinates to
22 | /// Clipper IntPoint instances.
23 | /// The purpose of this is that depending on the
24 | /// precision used by the comparison operator,
25 | /// different Point2dInt input keys may actually
26 | /// map to the same IntPoint value.
27 | ///
28 | public class VertexLookup : Dictionary
29 | {
30 | public IntPoint GetOrAdd( XYZ p )
31 | {
32 | Point2dInt q = new Point2dInt( p );
33 | if( !ContainsKey( q ) )
34 | {
35 | Add( q, new IntPoint { X = q.X, Y = q.Y } );
36 | }
37 | return this[ q ];
38 | }
39 | }
40 |
41 | ///
42 | /// Add the 2D projection of the given mesh triangles
43 | /// to the current element outline union
44 | ///
45 | static public bool AddToUnion(
46 | Polygons union,
47 | VertexLookup vl,
48 | Clipper c,
49 | Mesh m )
50 | {
51 | int n = m.NumTriangles;
52 |
53 | Polygons triangles = new Polygons( n );
54 | Polygon triangle = new Polygon( 3 );
55 |
56 | for( int i = 0; i < n; ++i )
57 | {
58 | MeshTriangle mt = m.get_Triangle( i );
59 |
60 | triangle.Clear();
61 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 0 ) ) );
62 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 1 ) ) );
63 | triangle.Add( vl.GetOrAdd( mt.get_Vertex( 2 ) ) );
64 | triangles.Add( triangle );
65 | }
66 | return c.AddPaths( triangles, PolyType.ptSubject, true );
67 | }
68 |
69 | ///
70 | /// Add the 2D projection of the given face
71 | /// to the current element outline union
72 | ///
73 | static public bool AddToUnion(
74 | Polygons union,
75 | VertexLookup vl,
76 | Clipper c,
77 | Face f )
78 | {
79 | IList loops = f.GetEdgesAsCurveLoops();
80 |
81 | // ExporterIFCUtils class can also be used for
82 | // non-IFC purposes. The SortCurveLoops method
83 | // sorts curve loops (edge loops) so that the
84 | // outer loops come first.
85 |
86 | IList> sortedLoops
87 | = ExporterIFCUtils.SortCurveLoops( loops );
88 |
89 | int n = loops.Count;
90 |
91 | Debug.Assert( 0 < n,
92 | "expected at least one face loop" );
93 |
94 | Polygons faces = new Polygons( n );
95 | Polygon face2d = new Polygon( loops[ 0 ].NumberOfCurves() );
96 |
97 | //foreach( IList loops2
98 | // in sortedLoops )
99 |
100 | foreach( CurveLoop loop in loops )
101 | {
102 | // Outer curve loops are counter-clockwise
103 |
104 | if( loop.IsCounterclockwise( XYZ.BasisZ ) )
105 | {
106 | face2d.Clear();
107 |
108 | foreach( Curve curve in loop )
109 | {
110 | IList pts = curve.Tessellate();
111 |
112 | IntPoint a = vl.GetOrAdd( pts[ 0 ] );
113 |
114 | face2d.Add( a );
115 |
116 | n = pts.Count;
117 |
118 | for( int i = 1; i < n; ++i )
119 | {
120 | IntPoint b = vl.GetOrAdd( pts[ i ] );
121 |
122 | if( b != a )
123 | {
124 | face2d.Add( b );
125 | a = b;
126 | }
127 | }
128 | }
129 | faces.Add( face2d );
130 | }
131 | }
132 | return c.AddPaths( faces, PolyType.ptSubject, true );
133 | }
134 |
135 | ///
136 | /// Add the 2D projection of the given arc
137 | /// to the current element outline union
138 | ///
139 | static public bool AddToUnion(
140 | Polygons union,
141 | VertexLookup vl,
142 | Clipper c,
143 | Arc arc )
144 | {
145 | IList pts = arc.Tessellate();
146 | int n = pts.Count;
147 |
148 | Polygons faces = new Polygons( 1 );
149 | Polygon face2d = new Polygon( n );
150 |
151 | IntPoint a = vl.GetOrAdd( pts[ 0 ] );
152 |
153 | face2d.Add( a );
154 |
155 | for( int i = 1; i < n; ++i )
156 | {
157 | IntPoint b = vl.GetOrAdd( pts[ i ] );
158 |
159 | if( b != a )
160 | {
161 | face2d.Add( b );
162 | a = b;
163 | }
164 | }
165 | faces.Add( face2d );
166 |
167 | return c.AddPaths( faces, PolyType.ptSubject, true );
168 | }
169 |
170 | ///
171 | /// Return the union of all outlines projected onto
172 | /// the XY plane from the geometry solids and meshes
173 | ///
174 | static public bool AddToUnion(
175 | Polygons union,
176 | List curves,
177 | VertexLookup vl,
178 | Clipper c,
179 | GeometryElement geoElem )
180 | {
181 | foreach( GeometryObject obj in geoElem )
182 | {
183 | // Curve
184 | // Edge
185 | // Face
186 | // GeometryElement
187 | // GeometryInstance
188 | // Mesh
189 | // Point
190 | // PolyLine
191 | // Profile
192 | // Solid
193 |
194 | // Skip objects that contribute no 2D surface
195 |
196 | Curve curve = obj as Curve;
197 | if( null != curve )
198 | {
199 | Arc arc = curve as Arc;
200 |
201 | if( null != arc && arc.IsCyclic )
202 | {
203 | AddToUnion( union, vl, c, arc );
204 | }
205 | else if( curve.IsBound )
206 | {
207 | curves.Add( new LineSegment(
208 | vl.GetOrAdd( curve.GetEndPoint( 0 ) ),
209 | vl.GetOrAdd( curve.GetEndPoint( 1 ) ) ) );
210 | }
211 | continue;
212 | }
213 |
214 | Solid solid = obj as Solid;
215 | if( null != solid )
216 | {
217 | foreach( Face f in solid.Faces )
218 | {
219 | // Skip pretty common case: vertical planar face
220 |
221 | if( f is PlanarFace
222 | && Util.IsHorizontal( ((PlanarFace) f).FaceNormal ) )
223 | {
224 | continue;
225 | }
226 | AddToUnion( union, vl, c, f );
227 | }
228 | continue;
229 | }
230 |
231 | Mesh mesh = obj as Mesh;
232 | if( null != mesh )
233 | {
234 | AddToUnion( union, vl, c, mesh );
235 | continue;
236 | }
237 |
238 | GeometryInstance inst = obj as GeometryInstance;
239 | if( null != inst )
240 | {
241 | GeometryElement txGeoElem
242 | = inst.GetInstanceGeometry(
243 | Transform.Identity ); // inst.Transform
244 |
245 | AddToUnion( union, curves, vl, c, txGeoElem );
246 | continue;
247 | }
248 | Debug.Assert( false,
249 | "expected only solid, mesh or instance" );
250 | }
251 | return true;
252 | }
253 |
254 | ///
255 | /// Return the union of the outermost room boundary
256 | /// loop projected onto the XY plane.
257 | ///
258 | static public bool AddToUnionRoom(
259 | Polygons union,
260 | List curves,
261 | VertexLookup vl,
262 | Clipper c,
263 | IList> boundary )
264 | {
265 | int n = boundary.Count;
266 |
267 | Debug.Assert( 0 < n,
268 | "expected at least one room boundary loop" );
269 |
270 | Polygons faces = new Polygons( n );
271 | Polygon face2d = new Polygon( boundary[ 0 ].Count );
272 |
273 | foreach( IList loop in boundary )
274 | {
275 | // Outer curve loops are counter-clockwise
276 |
277 | face2d.Clear();
278 |
279 | foreach( BoundarySegment s in loop )
280 | {
281 | IList pts = s.GetCurve().Tessellate();
282 |
283 | IntPoint a = vl.GetOrAdd( pts[ 0 ] );
284 |
285 | face2d.Add( a );
286 |
287 | n = pts.Count;
288 |
289 | for( int i = 1; i < n; ++i )
290 | {
291 | IntPoint b = vl.GetOrAdd( pts[ i ] );
292 |
293 | if( b != a )
294 | {
295 | face2d.Add( b );
296 | a = b;
297 | }
298 | }
299 | faces.Add( face2d );
300 | }
301 | }
302 | return c.AddPaths( faces, PolyType.ptSubject, true );
303 | }
304 |
305 | //static List GetRoomBoundaryIds(
306 | // Room room,
307 | // SpatialElementBoundaryOptions seb_opt )
308 | //{
309 | // List ids = null;
310 |
311 | // IList> sloops
312 | // = room.GetBoundarySegments( seb_opt );
313 |
314 | // if( null != sloops ) // the room may not be bounded
315 | // {
316 | // Debug.Assert( 1 == sloops.Count, "this add-in "
317 | // + "currently supports only rooms with one "
318 | // + "single boundary loop" );
319 |
320 | // ids = new List();
321 |
322 | // foreach( IList sloop in sloops )
323 | // {
324 | // foreach( BoundarySegment s in sloop )
325 | // {
326 | // ids.Add( s.ElementId );
327 | // }
328 |
329 | // // Skip out after first segement loop - ignore
330 | // // rooms with holes and disjunct parts
331 |
332 | // break;
333 | // }
334 | // }
335 | // return ids;
336 | //}
337 |
338 | ///
339 | /// Create a JtLoop representing the 2D outline of
340 | /// the given room including all its bounding elements
341 | /// by creating the inner room boundary loop and
342 | /// uniting it with the bounding elements solid faces
343 | /// and meshes in the given view, projecting
344 | /// them onto the XY plane and executing 2D Boolean
345 | /// unions on them.
346 | ///
347 | public static JtLoops GetRoomOuterBoundaryLoops(
348 | Room room,
349 | SpatialElementBoundaryOptions seb_opt,
350 | View view )
351 | {
352 | Document doc = view.Document;
353 |
354 | Options opt = new Options
355 | {
356 | View = view
357 | };
358 |
359 | Clipper c = new Clipper();
360 | VertexLookup vl = new VertexLookup();
361 | List curves = new List();
362 | Polygons union = new Polygons();
363 | JtLoops loops = null;
364 |
365 | IList> boundary
366 | = room.GetBoundarySegments(
367 | new SpatialElementBoundaryOptions() );
368 |
369 | if( null != boundary ) // the room may not be bounded
370 | {
371 | Debug.Assert( 1 == boundary.Count,
372 | "this add-in currently supports only rooms "
373 | + "with one single boundary loop" );
374 |
375 | // Ignore all loops except first, which is
376 | // hopefully outer -- and hopefully the room
377 | // does not have several disjunct parts.
378 | // Ignore holes in the room and
379 | // multiple disjunct parts.
380 |
381 | AddToUnionRoom( union, curves, vl, c, boundary );
382 |
383 | // Retrieve bounding elements
384 |
385 | List ids = new List();
386 |
387 | foreach( IList loop in boundary )
388 | {
389 | foreach( BoundarySegment s in loop )
390 | {
391 | ids.Add( s.ElementId );
392 | }
393 |
394 | // Skip out after first segement loop - ignore
395 | // rooms with holes and disjunct parts
396 |
397 | break;
398 | }
399 |
400 | foreach( ElementId id in ids )
401 | {
402 | // Skip invalid element ids, generated, for
403 | // instance, by a room separator line.
404 |
405 | if( !id.Equals( ElementId.InvalidElementId ) )
406 | {
407 | Element e = doc.GetElement( id );
408 |
409 | GeometryElement geo = e.get_Geometry( opt );
410 | AddToUnion( union, curves, vl, c, geo );
411 |
412 | bool succeeded = c.Execute( ClipType.ctUnion, union,
413 | PolyFillType.pftPositive, PolyFillType.pftPositive );
414 |
415 | if( 0 == union.Count )
416 | {
417 | Debug.Print( string.Format(
418 | "No outline found for {0} <{1}>",
419 | e.Name, e.Id.IntegerValue ) );
420 | }
421 | }
422 | }
423 | loops = ConvertToLoops( union );
424 |
425 | loops.NormalizeLoops();
426 | }
427 | return loops;
428 | }
429 |
430 |
431 | ///
432 | /// Return the outer polygons defined
433 | /// by the given line segments
434 | ///
435 | ///
436 | ///
437 | Polygons CreatePolygons( List curves )
438 | {
439 | Polygons polys = new Polygons();
440 | IntPoint p1 = curves.Select( s => s.Item1 ).Min();
441 | IntPoint p2 = curves.Select( s => s.Item2 ).Min();
442 | //IntPoint p = Min( p1, p2 );
443 | return polys;
444 | }
445 |
446 | ///
447 | /// Convert the curves to a polygon
448 | /// and add it to the union
449 | ///
450 | public bool AddToUnion(
451 | Polygons union,
452 | VertexLookup vl,
453 | Clipper c,
454 | List curves )
455 | {
456 | Polygons polys = CreatePolygons( curves );
457 | return c.AddPaths( polys, PolyType.ptSubject, true );
458 | }
459 |
460 | ///
461 | /// Convert Clipper polygons to JtLoops
462 | ///
463 | static JtLoops ConvertToLoops( Polygons union )
464 | {
465 | JtLoops loops = new JtLoops( union.Count );
466 | JtLoop loop = new JtLoop( union.First().Count );
467 | foreach( Polygon poly in union )
468 | {
469 | loop.Clear();
470 | foreach( IntPoint p in poly )
471 | {
472 | loop.Add( new Point2dInt(
473 | (int) p.X, (int) p.Y ) );
474 | }
475 | loops.Add( loop );
476 | }
477 | return loops;
478 | }
479 |
480 | ///
481 | /// Create JtLoops representing the given element
482 | /// 2D outlines by retrieving the element solid
483 | /// faces and meshes in the given view, projecting
484 | /// them onto the XY plane and executing 2d Boolean
485 | /// unions on them.
486 | ///
487 | public static Dictionary
488 | GetElementLoops(
489 | View view,
490 | ICollection ids )
491 | {
492 | Document doc = view.Document;
493 |
494 | Options opt = new Options
495 | {
496 | View = view
497 | };
498 |
499 | Clipper c = new Clipper();
500 | VertexLookup vl = new VertexLookup();
501 | List curves = new List();
502 | Polygons union = new Polygons();
503 | Dictionary booleanLoops
504 | = new Dictionary( ids.Count );
505 |
506 | foreach( ElementId id in ids )
507 | {
508 | c.Clear();
509 | vl.Clear();
510 | union.Clear();
511 |
512 | Element e = doc.GetElement( id );
513 |
514 | if( e is Room )
515 | {
516 | IList> boundary
517 | = (e as Room).GetBoundarySegments(
518 | new SpatialElementBoundaryOptions() );
519 |
520 | // Ignore all loops except first, which is
521 | // hopefully outer -- and hopefully the room
522 | // does not have several disjunct parts.
523 |
524 | AddToUnionRoom( union, curves, vl, c, boundary );
525 | }
526 | else
527 | {
528 | GeometryElement geo = e.get_Geometry( opt );
529 | AddToUnion( union, curves, vl, c, geo );
530 | }
531 |
532 | //AddToUnion( union, vl, c, curves );
533 |
534 | //c.AddPaths( subjects, PolyType.ptSubject, true );
535 | //c.AddPaths( clips, PolyType.ptClip, true );
536 |
537 | bool succeeded = c.Execute( ClipType.ctUnion, union,
538 | PolyFillType.pftPositive, PolyFillType.pftPositive );
539 |
540 | if( 0 == union.Count )
541 | {
542 | Debug.Print( string.Format(
543 | "No outline found for {0} <{1}>",
544 | e.Name, e.Id.IntegerValue ) );
545 | }
546 | else
547 | {
548 | JtLoops loops = ConvertToLoops( union );
549 |
550 | loops.NormalizeLoops();
551 |
552 | booleanLoops.Add( id.IntegerValue, loops );
553 | }
554 | }
555 | return booleanLoops;
556 | }
557 | }
558 | }
559 |
--------------------------------------------------------------------------------
/ElementOutline/Util.cs:
--------------------------------------------------------------------------------
1 | #region Namespaces
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Drawing;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Windows.Forms;
9 | using Autodesk.Revit.DB;
10 | using Autodesk.Revit.DB.Architecture;
11 | using Autodesk.Revit.UI;
12 | using Autodesk.Revit.UI.Selection;
13 | #endregion // Namespaces
14 |
15 | namespace ElementOutline
16 | {
17 | class Util
18 | {
19 | #region Output folder and export
20 | ///
21 | /// Output folder path;
22 | /// GetTempPath returns a weird GUID-named subdirectory
23 | /// created by Revit, so we will not use that, e.g.,
24 | /// C:\Users\tammikj\AppData\Local\Temp\bfd59506-2dff-4b0f-bbe4-31587fcaf508
25 | /// string path = Path.GetTempPath();
26 | /// @"C:\Users\jta\AppData\Local\Temp"
27 | ///
28 | public const string OutputFolderPath = "C:/tmp";
29 |
30 | public static void ExportLoops(
31 | string filepath,
32 | IWin32Window owner_window,
33 | string caption,
34 | Document doc,
35 | Dictionary loops )
36 | {
37 | Bitmap bmp = GeoSnoop.DisplayLoops( loops.Values );
38 |
39 | GeoSnoop.DisplayImageInForm( owner_window,
40 | caption, false, bmp );
41 |
42 | using( StreamWriter s = new StreamWriter( filepath ) )
43 | {
44 | s.WriteLine( caption );
45 |
46 | List keys = new List( loops.Keys );
47 | keys.Sort();
48 | foreach( int key in keys )
49 | {
50 | ElementId id = new ElementId( key );
51 | Element e = doc.GetElement( id );
52 |
53 | s.WriteLine(
54 | "{{\"name\":\"{0}\", \"id\":\"{1}\", "
55 | + "\"uid\":\"{2}\", \"svg_path\":\"{3}\"}}",
56 | e.Name, e.Id, e.UniqueId,
57 | loops[ key ].SvgPath );
58 | }
59 | s.Close();
60 | }
61 | }
62 |
63 | public static void CreateOutput(
64 | string file_content,
65 | string description,
66 | Document doc,
67 | JtWindowHandle hwnd,
68 | Dictionary booleanLoops )
69 | {
70 | string filepath = Path.Combine( Util.OutputFolderPath,
71 | doc.Title + "_" + file_content + ".json" );
72 |
73 | string caption = doc.Title + " " + description;
74 |
75 | ExportLoops( filepath, hwnd, caption,
76 | doc, booleanLoops );
77 | }
78 | #endregion // Output folder and export
79 |
80 | #region Element pre- or post-selection
81 | static public ICollection GetSelectedElements(
82 | UIDocument uidoc )
83 | {
84 | // Do we have any pre-selected elements?
85 |
86 | Selection sel = uidoc.Selection;
87 |
88 | ICollection ids = sel.GetElementIds();
89 |
90 | // If no elements were pre-selected,
91 | // prompt for post-selection
92 |
93 | if( null == ids || 0 == ids.Count )
94 | {
95 | IList refs = null;
96 |
97 | try
98 | {
99 | refs = sel.PickObjects( ObjectType.Element,
100 | "Please select elements for 2D outline generation." );
101 | }
102 | catch( Autodesk.Revit.Exceptions
103 | .OperationCanceledException )
104 | {
105 | return ids;
106 | }
107 | ids = new List(
108 | refs.Select(
109 | r => r.ElementId ) );
110 | }
111 | return ids;
112 | }
113 |
114 | ///
115 | /// Allow only room to be selected.
116 | ///
117 | class RoomSelectionFilter : ISelectionFilter
118 | {
119 | public bool AllowElement( Element e )
120 | {
121 | return e is Room;
122 | }
123 |
124 | public bool AllowReference( Reference r, XYZ p )
125 | {
126 | return true;
127 | }
128 | }
129 |
130 | static public IEnumerable GetSelectedRooms(
131 | UIDocument uidoc )
132 | {
133 | Document doc = uidoc.Document;
134 |
135 | // Do we have any pre-selected elements?
136 |
137 | Selection sel = uidoc.Selection;
138 |
139 | IEnumerable ids = sel.GetElementIds()
140 | .Where( id
141 | => (doc.GetElement( id ) is Room) );
142 |
143 | // If no elements were pre-selected,
144 | // prompt for post-selection
145 |
146 | if( null == ids || 0 == ids.Count() )
147 | {
148 | IList refs = null;
149 |
150 | try
151 | {
152 | refs = sel.PickObjects( ObjectType.Element,
153 | new RoomSelectionFilter(),
154 | "Please select rooms for 2D outline generation." );
155 | }
156 | catch( Autodesk.Revit.Exceptions
157 | .OperationCanceledException )
158 | {
159 | return ids;
160 | }
161 | ids = new List(
162 | refs.Select(
163 | r => r.ElementId ) );
164 | }
165 | return ids;
166 | }
167 | #endregion // Element pre- or post-selection
168 |
169 | #region Geometrical comparison
170 | const double _eps = 1.0e-9;
171 |
172 | public static bool IsZero(
173 | double a,
174 | double tolerance )
175 | {
176 | return tolerance > Math.Abs( a );
177 | }
178 |
179 | public static bool IsZero( double a )
180 | {
181 | return IsZero( a, _eps );
182 | }
183 |
184 | public static bool IsEqual( double a, double b )
185 | {
186 | return IsZero( b - a );
187 | }
188 |
189 | public static bool IsHorizontal( XYZ v )
190 | {
191 | return IsZero( v.Z );
192 | }
193 | #endregion // Geometrical comparison
194 |
195 | #region Unit conversion
196 | const double _feet_to_mm = 25.4 * 12;
197 |
198 | public static int ConvertFeetToMillimetres(
199 | double d )
200 | {
201 | //return (int) ( _feet_to_mm * d + 0.5 );
202 | return (int) Math.Round( _feet_to_mm * d,
203 | MidpointRounding.AwayFromZero );
204 | }
205 |
206 | public static double ConvertMillimetresToFeet( int d )
207 | {
208 | return d / _feet_to_mm;
209 | }
210 |
211 | const double _radians_to_degrees = 180.0 / Math.PI;
212 |
213 | public static double ConvertDegreesToRadians( int d )
214 | {
215 | return d * Math.PI / 180.0;
216 | }
217 |
218 | public static int ConvertRadiansToDegrees(
219 | double d )
220 | {
221 | //return (int) ( _radians_to_degrees * d + 0.5 );
222 | return (int) Math.Round( _radians_to_degrees * d,
223 | MidpointRounding.AwayFromZero );
224 | }
225 |
226 | ///
227 | /// Return true if the type b is either a
228 | /// subclass of OR equal to the base class itself.
229 | /// IsSubclassOf returns false if the two types
230 | /// are the same. It only returns true for true
231 | /// non-equal subclasses.
232 | ///
233 | public static bool IsSameOrSubclassOf(
234 | Type a,
235 | Type b )
236 | {
237 | // http://stackoverflow.com/questions/2742276/in-c-how-do-i-check-if-a-type-is-a-subtype-or-the-type-of-an-object
238 |
239 | return a.IsSubclassOf( b ) || a == b;
240 | }
241 | #endregion // Unit conversion
242 |
243 | #region Formatting
244 | ///
245 | /// Uncapitalise string, i.e.
246 | /// lowercase its first character.
247 | ///
248 | public static string Uncapitalise( string s )
249 | {
250 | return Char.ToLowerInvariant( s[0] )
251 | + s.Substring( 1 );
252 | }
253 |
254 | ///
255 | /// Return an English plural suffix for the given
256 | /// number of items, i.e. 's' for zero or more
257 | /// than one, and nothing for exactly one.
258 | ///
259 | public static string PluralSuffix( int n )
260 | {
261 | return 1 == n ? "" : "s";
262 | }
263 |
264 | ///
265 | /// Return an English plural suffix 'ies' or
266 | /// 'y' for the given number of items.
267 | ///
268 | public static string PluralSuffixY( int n )
269 | {
270 | return 1 == n ? "y" : "ies";
271 | }
272 |
273 | ///
274 | /// Return an English pluralised string for the
275 | /// given thing or things. If the thing ends with
276 | /// 'y', the plural is assumes to end with 'ies',
277 | /// e.g.
278 | /// (2, 'chair') -- '2 chairs'
279 | /// (2, 'property') -- '2 properties'
280 | /// (2, 'furniture item') -- '2 furniture items'
281 | /// If in doubt, appending 'item' or 'entry' to
282 | /// the thing description is normally a pretty
283 | /// safe bet. Replaces calls to PluralSuffix
284 | /// and PluralSuffixY.
285 | ///
286 | public static string PluralString(
287 | int n,
288 | string thing )
289 | {
290 | if( 1 == n )
291 | {
292 | return "1 " + thing;
293 | }
294 |
295 | int i = thing.Length - 1;
296 | char cy = thing[i];
297 |
298 | return n.ToString() + " " + ( ( 'y' == cy )
299 | ? thing.Substring( 0, i ) + "ies"
300 | : thing + "s" );
301 | }
302 |
303 | ///
304 | /// Return a dot (full stop) for zero
305 | /// or a colon for more than zero.
306 | ///
307 | public static string DotOrColon( int n )
308 | {
309 | return 0 < n ? ":" : ".";
310 | }
311 |
312 | ///
313 | /// Return a string for a real number
314 | /// formatted to two decimal places.
315 | ///
316 | public static string RealString( double a )
317 | {
318 | return a.ToString( "0.##" );
319 | }
320 |
321 | ///
322 | /// Return a string representation in degrees
323 | /// for an angle given in radians.
324 | ///
325 | public static string AngleString( double angle )
326 | {
327 | return RealString( angle * 180 / Math.PI ) + " degrees";
328 | }
329 |
330 | ///
331 | /// Return a string for a UV point
332 | /// or vector with its coordinates
333 | /// formatted to two decimal places.
334 | ///
335 | public static string PointString( UV p )
336 | {
337 | return string.Format( "({0},{1})",
338 | RealString( p.U ),
339 | RealString( p.V ) );
340 | }
341 |
342 | ///
343 | /// Return a string for an XYZ
344 | /// point or vector with its coordinates
345 | /// formatted to two decimal places.
346 | ///
347 | public static string PointString( XYZ p )
348 | {
349 | return string.Format( "({0},{1},{2})",
350 | RealString( p.X ),
351 | RealString( p.Y ),
352 | RealString( p.Z ) );
353 | }
354 |
355 | ///
356 | /// Return a string for the XY values of an XYZ
357 | /// point or vector with its coordinates
358 | /// formatted to two decimal places.
359 | ///
360 | public static string PointString2d( XYZ p )
361 | {
362 | return string.Format( "({0},{1})",
363 | RealString( p.X ),
364 | RealString( p.Y ) );
365 | }
366 |
367 | ///
368 | /// Return a string displaying the two XYZ
369 | /// endpoints of a geometry curve element.
370 | ///
371 | public static string CurveEndpointString( Curve c )
372 | {
373 | return string.Format( "({0},{1})",
374 | PointString2d( c.GetEndPoint( 0 ) ),
375 | PointString2d( c.GetEndPoint( 1 ) ) );
376 | }
377 |
378 | ///
379 | /// Return a string displaying only the XY values
380 | /// of the two XYZ endpoints of a geometry curve
381 | /// element.
382 | ///
383 | public static string CurveEndpointString2d( Curve c )
384 | {
385 | return string.Format( "({0},{1})",
386 | PointString( c.GetEndPoint( 0 ) ),
387 | PointString( c.GetEndPoint( 1 ) ) );
388 | }
389 |
390 | ///
391 | /// Return a string for a 2D bounding box
392 | /// formatted to two decimal places.
393 | ///
394 | public static string BoundingBoxString(
395 | BoundingBoxUV b )
396 | {
397 | //UV d = b.Max - b.Min;
398 |
399 | return string.Format( "({0},{1})",
400 | PointString( b.Min ),
401 | PointString( b.Max ) );
402 | }
403 |
404 | ///
405 | /// Return a string for a 3D bounding box
406 | /// formatted to two decimal places.
407 | ///
408 | public static string BoundingBoxString(
409 | BoundingBoxXYZ b )
410 | {
411 | //XYZ d = b.Max - b.Min;
412 |
413 | return string.Format( "({0},{1})",
414 | PointString( b.Min ),
415 | PointString( b.Max ) );
416 | }
417 |
418 | ///
419 | /// Return a string for an Outline
420 | /// formatted to two decimal places.
421 | ///
422 | public static string OutlineString( Outline o )
423 | {
424 | //XYZ d = o.MaximumPoint - o.MinimumPoint;
425 |
426 | return string.Format( "({0},{1})",
427 | PointString( o.MinimumPoint ),
428 | PointString( o.MaximumPoint ) );
429 | }
430 | #endregion // Formatting
431 |
432 | #region Element properties
433 | ///
434 | /// Return a string describing the given element:
435 | /// .NET type name,
436 | /// category name,
437 | /// family and symbol name for a family instance,
438 | /// element id and element name.
439 | ///
440 | public static string ElementDescription(
441 | Element e )
442 | {
443 | if( null == e )
444 | {
445 | return "";
446 | }
447 |
448 | // For a wall, the element name equals the
449 | // wall type name, which is equivalent to the
450 | // family name ...
451 |
452 | FamilyInstance fi = e as FamilyInstance;
453 |
454 | string typeName = e.GetType().Name;
455 |
456 | string categoryName = ( null == e.Category )
457 | ? string.Empty
458 | : e.Category.Name + " ";
459 |
460 | string familyName = ( null == fi )
461 | ? string.Empty
462 | : fi.Symbol.Family.Name + " ";
463 |
464 | string symbolName = ( null == fi
465 | || e.Name.Equals( fi.Symbol.Name ) )
466 | ? string.Empty
467 | : fi.Symbol.Name + " ";
468 |
469 | return string.Format( "{0} {1}{2}{3}<{4} {5}>",
470 | typeName, categoryName, familyName,
471 | symbolName, e.Id.IntegerValue, e.Name );
472 | }
473 |
474 | ///
475 | /// Return a string describing the given sheet:
476 | /// sheet number and name.
477 | ///
478 | public static string SheetDescription(
479 | Element e )
480 | {
481 | string sheet_number = e.get_Parameter(
482 | BuiltInParameter.SHEET_NUMBER )
483 | .AsString();
484 |
485 | return string.Format( "{0} - {1}",
486 | sheet_number, e.Name );
487 | }
488 |
489 | ///
490 | /// Return a dictionary of all the given
491 | /// element parameter names and values.
492 | ///
493 | public static bool IsModifiable( Parameter p )
494 | {
495 | StorageType st = p.StorageType;
496 |
497 | return !( p.IsReadOnly )
498 | // && p.UserModifiable // ignore this
499 | && ( ( StorageType.Integer == st )
500 | || ( StorageType.String == st ) );
501 | }
502 |
503 | ///
504 | /// Return a dictionary of all the given
505 | /// element parameter names and values.
506 | ///
507 | public static Dictionary
508 | GetElementProperties(
509 | Element e )
510 | {
511 | IList parameters
512 | = e.GetOrderedParameters();
513 |
514 | Dictionary a
515 | = new Dictionary(
516 | parameters.Count );
517 |
518 | StorageType st;
519 | string s;
520 |
521 | foreach( Parameter p in parameters )
522 | {
523 | st = p.StorageType;
524 |
525 | s = string.Format( "{0} {1}",
526 | ( IsModifiable( p ) ? "w" : "r" ),
527 | ( StorageType.String == st
528 | ? p.AsString()
529 | : p.AsInteger().ToString() ) );
530 |
531 | a.Add( p.Definition.Name, s );
532 | }
533 | return a;
534 | }
535 | #endregion // Element properties
536 |
537 | #region Messages
538 | ///
539 | /// Display a short big message.
540 | ///
541 | public static void InfoMsg( string msg )
542 | {
543 | Debug.Print( msg );
544 | TaskDialog.Show( App.Caption, msg );
545 | }
546 |
547 | ///
548 | /// Display a longer message in smaller font.
549 | ///
550 | public static void InfoMsg2(
551 | string instruction,
552 | string msg,
553 | bool prompt = true )
554 | {
555 | Debug.Print( "{0}: {1}", instruction, msg );
556 | if( prompt )
557 | {
558 | TaskDialog dlg = new TaskDialog( App.Caption );
559 | dlg.MainInstruction = instruction;
560 | dlg.MainContent = msg;
561 | dlg.Show();
562 | }
563 | }
564 |
565 | ///
566 | /// Display an error message.
567 | ///
568 | public static void ErrorMsg( string msg )
569 | {
570 | Debug.Print( msg );
571 | TaskDialog dlg = new TaskDialog( App.Caption );
572 | dlg.MainIcon = TaskDialogIcon.TaskDialogIconWarning;
573 | dlg.MainInstruction = msg;
574 | dlg.Show();
575 | }
576 |
577 | ///
578 | /// Print a debug log message with a time stamp
579 | /// to the Visual Studio debug output window.
580 | ///
581 | public static void Log( string msg )
582 | {
583 | string timestamp = DateTime.Now.ToString(
584 | "HH:mm:ss.fff" );
585 |
586 | Debug.Print( timestamp + " " + msg );
587 | }
588 | #endregion // Messages
589 |
590 | #region Browse for directory
591 | public static bool BrowseDirectory(
592 | ref string path,
593 | bool allowCreate )
594 | {
595 | FolderBrowserDialog browseDlg
596 | = new FolderBrowserDialog();
597 |
598 | browseDlg.SelectedPath = path;
599 | browseDlg.ShowNewFolderButton = allowCreate;
600 |
601 | bool rc = ( DialogResult.OK
602 | == browseDlg.ShowDialog() );
603 |
604 | if( rc )
605 | {
606 | path = browseDlg.SelectedPath;
607 | }
608 | return rc;
609 | }
610 | #endregion // Browse for directory
611 |
612 | #region Flip SVG Y coordinates
613 | public static bool SvgFlip = true;
614 |
615 | ///
616 | /// Flip Y coordinate for SVG export.
617 | ///
618 | public static int SvgFlipY( int y )
619 | {
620 | return SvgFlip ? -y : y;
621 | }
622 | #endregion // Flip SVG Y coordinates
623 | }
624 | }
625 |
--------------------------------------------------------------------------------
/ElementOutline/CmdUploadRooms.cs:
--------------------------------------------------------------------------------
1 | #region Namespaces
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using Autodesk.Revit.ApplicationServices;
7 | using Autodesk.Revit.Attributes;
8 | using Autodesk.Revit.DB;
9 | using Autodesk.Revit.DB.Architecture;
10 | using Autodesk.Revit.DB.IFC;
11 | using Autodesk.Revit.UI;
12 | using Autodesk.Revit.UI.Selection;
13 | using Bitmap = System.Drawing.Bitmap;
14 | using BoundarySegment = Autodesk.Revit.DB.BoundarySegment;
15 | //using ComponentManager = Autodesk.Windows.ComponentManager; pre-2020
16 | using IWin32Window = System.Windows.Forms.IWin32Window;
17 | //using DreamSeat;
18 | #endregion
19 |
20 | namespace ElementOutline
21 | {
22 | [Transaction( TransactionMode.ReadOnly )]
23 | internal class CmdUploadRooms : IExternalCommand
24 | {
25 | #region RoomSelectionFilter
26 | class RoomSelectionFilter : ISelectionFilter
27 | {
28 | public bool AllowElement( Element e )
29 | {
30 | return e is Room;
31 | }
32 |
33 | public bool AllowReference( Reference r, XYZ p )
34 | {
35 | return true;
36 | }
37 | }
38 | #endregion // RoomSelectionFilter
39 |
40 | static bool _debug_output = false;
41 |
42 | ///
43 | /// If curve tessellation is disabled, only
44 | /// straight line segments from start to end
45 | /// point are exported.
46 | ///
47 | static bool _tessellate_curves = true;
48 |
49 | ///
50 | /// Never tessellate a curve
51 | /// shorter than this length.
52 | ///
53 | const double _min_tessellation_curve_length_in_feet = 0.2;
54 |
55 | ///
56 | /// Conversion factor from foot to quarter inch.
57 | ///
58 | const double _quarter_inch = 1.0 / (12 * 4);
59 |
60 | #region Get room boundary loops
61 | ///
62 | /// Retrieve the room plan view boundary
63 | /// polygon loops and convert to 2D integer-based.
64 | /// For optimisation and consistency reasons,
65 | /// convert all coordinates to integer values in
66 | /// millimetres. Revit precision is limited to
67 | /// 1/16 of an inch, which is abaut 1.2 mm, anyway.
68 | ///
69 | static JtLoops GetRoomLoops( Room room )
70 | {
71 | SpatialElementBoundaryOptions opt
72 | = new SpatialElementBoundaryOptions();
73 |
74 | opt.SpatialElementBoundaryLocation =
75 | SpatialElementBoundaryLocation.Center; // loops closed
76 | //SpatialElementBoundaryLocation.Finish; // loops not closed
77 |
78 | IList> loops = room.
79 | GetBoundarySegments( opt );
80 |
81 | int nLoops = loops.Count;
82 |
83 | JtLoops jtloops = new JtLoops( nLoops );
84 |
85 | foreach( IList loop in loops )
86 | {
87 | int nSegments = loop.Count;
88 |
89 | JtLoop jtloop = new JtLoop( nSegments );
90 |
91 | XYZ p0 = null; // loop start point
92 | XYZ p; // segment start point
93 | XYZ q = null; // segment end point
94 |
95 | foreach( BoundarySegment seg in loop )
96 | {
97 | // Todo: handle non-linear curve.
98 | // Especially: if two long lines have a
99 | // short arc in between them, skip the arc
100 | // and extend both lines.
101 |
102 | p = seg.GetCurve().GetEndPoint( 0 );
103 |
104 | jtloop.Add( new Point2dInt( p ) );
105 |
106 | Debug.Assert( null == q || q.IsAlmostEqualTo( p ),
107 | "expected last endpoint to equal current start point" );
108 |
109 | q = seg.GetCurve().GetEndPoint( 1 );
110 |
111 | if( _debug_output )
112 | {
113 | Debug.Print( "{0} --> {1}",
114 | Util.PointString( p ),
115 | Util.PointString( q ) );
116 | }
117 | if( null == p0 )
118 | {
119 | p0 = p; // save loop start point
120 | }
121 | }
122 | Debug.Assert( q.IsAlmostEqualTo( p0 ),
123 | "expected last endpoint to equal loop start point" );
124 |
125 | jtloops.Add( jtloop );
126 | }
127 | return jtloops;
128 | }
129 |
130 | //(9.03,10.13,0) --> (-14.59,10.13,0)
131 | //(-14.59,10.13,0) --> (-14.59,1.93,0)
132 | //(-14.59,1.93,0) --> (-2.45,1.93,0)
133 | //(-2.45,1.93,0) --> (-2.45,-3.98,0)
134 | //(-2.45,-3.98,0) --> (9.03,-3.98,0)
135 | //(9.03,-3.98,0) --> (9.03,10.13,0)
136 | //(0.98,-0.37,0) --> (0.98,1.93,0)
137 | //(0.98,1.93,0) --> (5.57,1.93,0)
138 | //(5.57,1.93,0) --> (5.57,-0.37,0)
139 | //(5.57,-0.37,0) --> (0.98,-0.37,0)
140 |
141 | //(9.03,10.13) --> (-14.59,10.13)
142 | //(-14.59,10.13) --> (-14.59,1.93)
143 | //(-14.59,1.93) --> (-2.45,1.93)
144 | //(-2.45,1.93) --> (-2.45,-3.98)
145 | //(-2.45,-3.98) --> (9.03,-3.98)
146 | //(9.03,-3.98) --> (9.03,10.13)
147 | //(0.98,-0.37) --> (0.98,1.93)
148 | //(0.98,1.93) --> (5.57,1.93)
149 | //(5.57,1.93) --> (5.57,-0.37)
150 | //(5.57,-0.37) --> (0.98,-0.37)
151 |
152 | //Room Rooms <212639 Room 1> has 2 loops:
153 | // 0: (2753,3087), (-4446,3087), (-4446,587), (-746,587), (-746,-1212), (2753,-1212)
154 | // 1: (298,-112), (298,587), (1698,587), (1698,-112)
155 | #endregion // Get room boundary loops
156 |
157 | #region Get furniture contained in given room
158 | ///
159 | /// Return the element ids of all furniture and
160 | /// equipment family instances contained in the
161 | /// given room.
162 | ///
163 | static List GetFurniture( Room room )
164 | {
165 | BoundingBoxXYZ bb = room.get_BoundingBox( null );
166 |
167 | Outline outline = new Outline( bb.Min, bb.Max );
168 |
169 | BoundingBoxIntersectsFilter filter
170 | = new BoundingBoxIntersectsFilter( outline );
171 |
172 | Document doc = room.Document;
173 |
174 | // Todo: add category filters and other
175 | // properties to narrow down the results
176 |
177 | // what categories of family instances
178 | // are we interested in?
179 |
180 | BuiltInCategory[] bics = new BuiltInCategory[] {
181 | BuiltInCategory.OST_Furniture,
182 | BuiltInCategory.OST_PlumbingFixtures,
183 | BuiltInCategory.OST_SpecialityEquipment
184 | };
185 |
186 | LogicalOrFilter categoryFilter
187 | = new LogicalOrFilter( bics
188 | .Select(
189 | bic => new ElementCategoryFilter( bic ) )
190 | .ToList() );
191 |
192 | FilteredElementCollector familyInstances
193 | = new FilteredElementCollector( doc )
194 | .WhereElementIsNotElementType()
195 | .WhereElementIsViewIndependent()
196 | .OfClass( typeof( FamilyInstance ) )
197 | .WherePasses( categoryFilter )
198 | .WherePasses( filter );
199 |
200 | int roomid = room.Id.IntegerValue;
201 |
202 | List a = new List();
203 |
204 | foreach( FamilyInstance fi in familyInstances )
205 | {
206 | if( null != fi.Room
207 | && fi.Room.Id.IntegerValue.Equals( roomid ) )
208 | {
209 | Debug.Assert( fi.Location is LocationPoint,
210 | "expected all furniture to have a location point" );
211 |
212 | a.Add( fi );
213 | }
214 | }
215 | return a;
216 | }
217 | #endregion // Get furniture contained in given room
218 |
219 | ///
220 | /// Return a closed loop of integer-based points
221 | /// scaled to millimetres from a given Revit model
222 | /// face in feet.
223 | ///
224 | internal static JtLoop GetLoop(
225 | Autodesk.Revit.Creation.Application creapp,
226 | Face face )
227 | {
228 | JtLoop loop = null;
229 |
230 | foreach( EdgeArray a in face.EdgeLoops )
231 | {
232 | int nEdges = a.Size;
233 |
234 | List curves
235 | = new List( nEdges );
236 |
237 | XYZ p0 = null; // loop start point
238 | XYZ p; // edge start point
239 | XYZ q = null; // edge end point
240 |
241 | // Test ValidateCurveLoops
242 |
243 | //CurveLoop loopIfc = new CurveLoop();
244 |
245 | foreach( Edge e in a )
246 | {
247 | // This requires post-processing using
248 | // SortCurvesContiguous:
249 |
250 | Curve curve = e.AsCurve();
251 |
252 | if( _debug_output )
253 | {
254 | p = curve.GetEndPoint( 0 );
255 | q = curve.GetEndPoint( 1 );
256 | Debug.Print( "{0} --> {1}",
257 | Util.PointString( p ),
258 | Util.PointString( q ) );
259 | }
260 |
261 | // This returns the curves already
262 | // correctly oriented:
263 |
264 | curve = e.AsCurveFollowingFace(
265 | face );
266 |
267 | if( _debug_output )
268 | {
269 | p = curve.GetEndPoint( 0 );
270 | q = curve.GetEndPoint( 1 );
271 | Debug.Print( "{0} --> {1} following face",
272 | Util.PointString( p ),
273 | Util.PointString( q ) );
274 | }
275 |
276 | curves.Add( curve );
277 |
278 | // Throws an exception saying "This curve
279 | // will make the loop not contiguous.
280 | // Parameter name: pCurve"
281 |
282 | //loopIfc.Append( curve );
283 | }
284 |
285 | // We never reach this point:
286 |
287 | //List loopsIfc
288 | // = new List( 1 );
289 |
290 | //loopsIfc.Add( loopIfc );
291 |
292 | //IList loopsIfcOut = ExporterIFCUtils
293 | // .ValidateCurveLoops( loopsIfc, XYZ.BasisZ );
294 |
295 | // This is no longer needed if we use
296 | // AsCurveFollowingFace instead of AsCurve:
297 |
298 | CurveUtils.SortCurvesContiguous(
299 | creapp, curves, _debug_output );
300 |
301 | q = null;
302 |
303 | loop = new JtLoop( nEdges );
304 |
305 | foreach( Curve curve in curves )
306 | {
307 | // Todo: handle non-linear curve.
308 | // Especially: if two long lines have a
309 | // short arc in between them, skip the arc
310 | // and extend both lines.
311 |
312 | p = curve.GetEndPoint( 0 );
313 |
314 | Debug.Assert( null == q
315 | || q.IsAlmostEqualTo( p, 1e-04 ),
316 | string.Format(
317 | "expected last endpoint to equal current start point, not distance {0}",
318 | ( null == q ? 0 : p.DistanceTo( q ) ) ) );
319 |
320 | q = curve.GetEndPoint( 1 );
321 |
322 | if( _debug_output )
323 | {
324 | Debug.Print( "{0} --> {1}",
325 | Util.PointString( p ),
326 | Util.PointString( q ) );
327 | }
328 |
329 | if( null == p0 )
330 | {
331 | p0 = p; // save loop start point
332 | }
333 |
334 | int n = -1;
335 |
336 | if( _tessellate_curves
337 | && _min_tessellation_curve_length_in_feet
338 | < q.DistanceTo( p ) )
339 | {
340 | IList pts = curve.Tessellate();
341 | n = pts.Count;
342 |
343 | Debug.Assert( 1 < n, "expected at least two points" );
344 | Debug.Assert( p.IsAlmostEqualTo( pts[0] ), "expected tessellation start equal curve start point" );
345 | Debug.Assert( q.IsAlmostEqualTo( pts[n - 1] ), "expected tessellation end equal curve end point" );
346 |
347 | if( 2 == n )
348 | {
349 | n = -1; // this is a straight line
350 | }
351 | else
352 | {
353 | --n; // skip last point
354 |
355 | for( int i = 0; i < n; ++i )
356 | {
357 | loop.Add( new Point2dInt( pts[i] ) );
358 | }
359 | }
360 | }
361 |
362 | // If tessellation is disabled,
363 | // or curve is too short to tessellate,
364 | // or has only two tessellation points,
365 | // just add the start point:
366 |
367 | if( -1 == n )
368 | {
369 | loop.Add( new Point2dInt( p ) );
370 | }
371 | }
372 | Debug.Assert( q.IsAlmostEqualTo( p0, 1e-05 ),
373 | string.Format(
374 | "expected last endpoint to equal current start point, not distance {0}",
375 | p0.DistanceTo( q ) ) );
376 | }
377 | return loop;
378 | }
379 |
380 | ///
381 | /// Add all plan view boundary loops from
382 | /// given solid to the list of loops.
383 | /// The creation application argument is used to
384 | /// reverse the extrusion analyser output curves
385 | /// in case they are badly oriented.
386 | ///
387 | /// Number of loops added
388 | static int AddLoops(
389 | Autodesk.Revit.Creation.Application creapp,
390 | JtLoops loops,
391 | GeometryObject obj,
392 | ref int nExtrusionAnalysisFailures )
393 | {
394 | int nAdded = 0;
395 |
396 | Solid solid = obj as Solid;
397 |
398 | if( null != solid
399 | && 0 < solid.Faces.Size )
400 | {
401 | //Plane plane = new Plane(XYZ.BasisX,
402 | // XYZ.BasisY, XYZ.Zero); // 2016
403 |
404 | Plane plane = Plane.CreateByOriginAndBasis(
405 | XYZ.Zero, XYZ.BasisX, XYZ.BasisY ); // 2017
406 |
407 | ExtrusionAnalyzer extrusionAnalyzer = null;
408 |
409 | try
410 | {
411 | extrusionAnalyzer = ExtrusionAnalyzer.Create(
412 | solid, plane, XYZ.BasisZ );
413 | }
414 | catch( Autodesk.Revit.Exceptions
415 | .InvalidOperationException )
416 | {
417 | ++nExtrusionAnalysisFailures;
418 | return nAdded;
419 | }
420 |
421 | Face face = extrusionAnalyzer
422 | .GetExtrusionBase();
423 |
424 | loops.Add( GetLoop( creapp, face ) );
425 |
426 | ++nAdded;
427 | }
428 | return nAdded;
429 | }
430 |
431 | #region Obsolete GetPlanViewBoundaryLoopsMultiple
432 | ///
433 | /// Retrieve all plan view boundary loops from
434 | /// all solids of given element. This initial
435 | /// version passes each solid encountered in the
436 | /// given element to the ExtrusionAnalyzer one
437 | /// at a time, which obviously results in multiple
438 | /// loops, many of which are contained within the
439 | /// others. An updated version unites all the
440 | /// solids first and then uses the ExtrusionAnalyzer
441 | /// once only to obtain the true outside shadow
442 | /// contour.
443 | ///
444 | static JtLoops GetPlanViewBoundaryLoopsMultiple(
445 | Element e,
446 | ref int nFailures )
447 | {
448 | Autodesk.Revit.Creation.Application creapp
449 | = e.Document.Application.Create;
450 |
451 | JtLoops loops = new JtLoops( 1 );
452 |
453 | //int nSolids = 0;
454 |
455 | Options opt = new Options();
456 |
457 | GeometryElement geo = e.get_Geometry( opt );
458 |
459 | if( null != geo )
460 | {
461 | Document doc = e.Document;
462 |
463 | if( e is FamilyInstance )
464 | {
465 | geo = geo.GetTransformed(
466 | Transform.Identity );
467 | }
468 |
469 | //GeometryInstance inst = null;
470 |
471 | foreach( GeometryObject obj in geo )
472 | {
473 | AddLoops( creapp, loops, obj, ref nFailures );
474 |
475 | //inst = obj as GeometryInstance;
476 | }
477 |
478 | //if( 0 == nSolids && null != inst )
479 | //{
480 | // geo = inst.GetSymbolGeometry();
481 |
482 | // foreach( GeometryObject obj in geo )
483 | // {
484 | // AddLoops( creapp, loops, obj, ref nFailures );
485 | // }
486 | //}
487 | }
488 | return loops;
489 | }
490 | #endregion // Obsolete GetPlanViewBoundaryLoopsMultiple
491 |
492 | ///
493 | /// Retrieve all plan view boundary loops from
494 | /// all solids of the given element geometry
495 | /// united together.
496 | ///
497 | internal static JtLoops GetPlanViewBoundaryLoopsGeo(
498 | Autodesk.Revit.Creation.Application creapp,
499 | GeometryElement geo,
500 | ref int nFailures )
501 | {
502 | Solid union = null;
503 |
504 | Plane plane = Plane.CreateByOriginAndBasis(
505 | XYZ.Zero, XYZ.BasisX, XYZ.BasisY );
506 |
507 | foreach( GeometryObject obj in geo )
508 | {
509 | Solid solid = obj as Solid;
510 |
511 | if( null != solid
512 | && 0 < solid.Faces.Size )
513 | {
514 | // Some solids, e.g. in the standard
515 | // content 'Furniture Chair - Office'
516 | // cause an extrusion analyser failure,
517 | // so skip adding those.
518 |
519 | try
520 | {
521 | ExtrusionAnalyzer extrusionAnalyzer
522 | = ExtrusionAnalyzer.Create(
523 | solid, plane, XYZ.BasisZ );
524 | }
525 | catch( Autodesk.Revit.Exceptions
526 | .InvalidOperationException )
527 | {
528 | solid = null;
529 | ++nFailures;
530 | }
531 |
532 | if( null != solid )
533 | {
534 | if( null == union )
535 | {
536 | union = solid;
537 | }
538 | else
539 | {
540 | try
541 | {
542 | union = BooleanOperationsUtils
543 | .ExecuteBooleanOperation( union, solid,
544 | BooleanOperationsType.Union );
545 | }
546 | catch( Autodesk.Revit.Exceptions
547 | .InvalidOperationException )
548 | {
549 | ++nFailures;
550 | }
551 | }
552 | }
553 | }
554 | }
555 |
556 | JtLoops loops = new JtLoops( 1 );
557 |
558 | AddLoops( creapp, loops, union, ref nFailures );
559 |
560 | return loops;
561 | }
562 |
563 | ///
564 | /// Retrieve all plan view boundary loops from
565 | /// all solids of given element united together.
566 | /// If the element is a family instance, transform
567 | /// its loops from the instance placement
568 | /// coordinate system back to the symbol
569 | /// definition one.
570 | /// If no geometry can be determined, use the
571 | /// bounding box instead.
572 | ///
573 | internal static JtLoops GetSolidPlanViewBoundaryLoops(
574 | Element e,
575 | bool transformInstanceCoordsToSymbolCoords,
576 | ref int nFailures )
577 | {
578 | Autodesk.Revit.Creation.Application creapp
579 | = e.Document.Application.Create;
580 |
581 | JtLoops loops = null;
582 |
583 | Options opt = new Options();
584 |
585 | GeometryElement geo = e.get_Geometry( opt );
586 |
587 | if( null != geo )
588 | {
589 | Document doc = e.Document;
590 |
591 | if( e is FamilyInstance )
592 | {
593 | if( transformInstanceCoordsToSymbolCoords )
594 | {
595 | // Retrieve family instance geometry
596 | // transformed back to symbol definition
597 | // coordinate space by inverting the
598 | // family instance placement transformation
599 |
600 | LocationPoint lp = e.Location
601 | as LocationPoint;
602 |
603 | Transform t = Transform.CreateTranslation(
604 | -lp.Point );
605 |
606 | Transform r = Transform.CreateRotationAtPoint(
607 | XYZ.BasisZ, -lp.Rotation, lp.Point );
608 |
609 | geo = geo.GetTransformed( t * r );
610 | }
611 | else
612 | {
613 | Debug.Assert(
614 | 1 == geo.Count(),
615 | "expected as single geometry instance" );
616 |
617 | Debug.Assert(
618 | geo.First() is GeometryInstance,
619 | "expected as single geometry instance" );
620 |
621 | geo = ( geo.First()
622 | as GeometryInstance ).GetInstanceGeometry();
623 | }
624 | }
625 |
626 | loops = GetPlanViewBoundaryLoopsGeo(
627 | creapp, geo, ref nFailures );
628 | }
629 | if( null == loops || 0 == loops.Count )
630 | {
631 | Debug.Print(
632 | "Unable to determine geometry for "
633 | + Util.ElementDescription( e )
634 | + "; using bounding box instead." );
635 |
636 | BoundingBoxXYZ bb;
637 |
638 | if( e is FamilyInstance )
639 | {
640 | bb = ( e as FamilyInstance ).Symbol
641 | .get_BoundingBox( null );
642 | }
643 | else
644 | {
645 | bb = e.get_BoundingBox( null );
646 | }
647 | JtLoop loop = new JtLoop( 4 );
648 | loop.Add( new Point2dInt( bb.Min ) );
649 | loop.Add( new Point2dInt( bb.Max.X, bb.Min.Y ) );
650 | loop.Add( new Point2dInt( bb.Max ) );
651 | loop.Add( new Point2dInt( bb.Min.X, bb.Max.Y ) );
652 | loops.Add( loop );
653 | }
654 | return loops;
655 | }
656 |
657 | ///
658 | /// List all the loops retrieved
659 | /// from the given element.
660 | ///
661 | internal static void ListLoops( Element e, JtLoops loops )
662 | {
663 | int nLoops = loops.Count;
664 |
665 | Debug.Print( "{0} has {1}{2}",
666 | Util.ElementDescription( e ),
667 | Util.PluralString( nLoops, "loop" ),
668 | Util.DotOrColon( nLoops ) );
669 |
670 | int i = 0;
671 |
672 | foreach( JtLoop loop in loops )
673 | {
674 | Debug.Print( " {0}: {1}", i++,
675 | loop.ToString() );
676 | }
677 | }
678 |
679 | ///
680 | /// Upload the selected rooms and the furniture
681 | /// they contain to the cloud database.
682 | ///
683 | public static void UploadRoom(
684 | IntPtr hwnd,
685 | Document doc,
686 | Room room )
687 | {
688 | BoundingBoxXYZ bb = room.get_BoundingBox( null );
689 |
690 | if( null == bb )
691 | {
692 | Util.ErrorMsg( string.Format( "Skipping room {0} "
693 | + "because it has no bounding box.",
694 | Util.ElementDescription( room ) ) );
695 |
696 | return;
697 | }
698 |
699 | JtLoops roomLoops = GetRoomLoops( room );
700 |
701 | ListLoops( room, roomLoops );
702 |
703 | List furniture
704 | = GetFurniture( room );
705 |
706 | // Map symbol UniqueId to symbol loop
707 |
708 | Dictionary furnitureLoops
709 | = new Dictionary();
710 |
711 | // List of instances referring to symbols
712 |
713 | List furnitureInstances
714 | = new List(
715 | furniture.Count );
716 |
717 | int nFailures;
718 |
719 | foreach( FamilyInstance f in furniture )
720 | {
721 | FamilySymbol s = f.Symbol;
722 |
723 | string uid = s.UniqueId;
724 |
725 | if( !furnitureLoops.ContainsKey( uid ) )
726 | {
727 | nFailures = 0;
728 |
729 | JtLoops loops = GetSolidPlanViewBoundaryLoops(
730 | f, true, ref nFailures );
731 |
732 | if( 0 < nFailures )
733 | {
734 | Debug.Print( "{0}: {1}",
735 | Util.ElementDescription( f ),
736 | Util.PluralString( nFailures,
737 | "extrusion analyser failure" ) );
738 | }
739 | ListLoops( f, loops );
740 |
741 | if( 0 < loops.Count )
742 | {
743 | // Assume first loop is outer one
744 |
745 | furnitureLoops.Add( uid, loops[0] );
746 | }
747 | }
748 | furnitureInstances.Add(
749 | new JtPlacement2dInt( f ) );
750 | }
751 | IWin32Window revit_window
752 | = new JtWindowHandle( hwnd );
753 |
754 | string caption = doc.Title
755 | + " : " + doc.GetElement( room.LevelId ).Name
756 | + " : " + room.Name;
757 |
758 | Bitmap bmp = GeoSnoop.DisplayRoom( roomLoops,
759 | furnitureLoops, furnitureInstances );
760 |
761 | GeoSnoop.DisplayImageInForm( revit_window,
762 | caption, false, bmp );
763 |
764 | //DbUpload.DbUploadRoom( room, furniture,
765 | // roomLoops, furnitureLoops );
766 | }
767 |
768 | #region External command mainline Execute method
769 | public Result Execute(
770 | ExternalCommandData commandData,
771 | ref string message,
772 | ElementSet elements )
773 | {
774 | UIApplication uiapp = commandData.Application;
775 | UIDocument uidoc = uiapp.ActiveUIDocument;
776 | Application app = uiapp.Application;
777 | Document doc = uidoc.Document;
778 |
779 | IntPtr hwnd = uiapp.MainWindowHandle;
780 |
781 | if( null == doc )
782 | {
783 | Util.ErrorMsg( "Please run this command in a valid"
784 | + " Revit project document." );
785 | return Result.Failed;
786 | }
787 |
788 | // Iterate over all pre-selected rooms
789 |
790 | Selection sel = uidoc.Selection;
791 |
792 | ICollection ids = sel.GetElementIds();
793 |
794 | if( 0 < ids.Count )
795 | {
796 | foreach( ElementId id in ids )
797 | {
798 | if( !( doc.GetElement( id ) is Room ) )
799 | {
800 | Util.ErrorMsg( "Please pre-select only room"
801 | + " elements before running this command." );
802 | return Result.Failed;
803 | }
804 | }
805 | }
806 |
807 | // If no rooms were pre-selected,
808 | // prompt for post-selection
809 |
810 | if( null == ids || 0 == ids.Count )
811 | {
812 | IList refs = null;
813 |
814 | try
815 | {
816 | refs = sel.PickObjects( ObjectType.Element,
817 | new RoomSelectionFilter(),
818 | "Please select rooms." );
819 | }
820 | catch( Autodesk.Revit.Exceptions
821 | .OperationCanceledException )
822 | {
823 | return Result.Cancelled;
824 | }
825 | ids = new List(
826 | refs.Select(
827 | r => r.ElementId ) );
828 | }
829 |
830 | // Upload selected rooms to cloud database
831 |
832 | foreach( ElementId id in ids )
833 | {
834 | UploadRoom( hwnd, doc,
835 | doc.GetElement( id ) as Room );
836 | }
837 |
838 | //DbUpdater.SetLastSequence();
839 |
840 | return Result.Succeeded;
841 | }
842 | #endregion // External command mainline Execute method
843 | }
844 | }
845 |
--------------------------------------------------------------------------------