This post was almost called "Generating Koch fractals in AutoCAD using .NET - Part 3", following on from Parts 1 & 2 of the series. But by the time I'd completed the code, I realised it to be of more general appeal and decided to provide it with a more representative title.
I started off by adding a progress meter and an escape key handler to the code in the last post. Then, while refactoring the code, I decided to encapsulate the functionality in a standalone class that could be dropped into pretty much any AutoCAD .NET project (although I've implemented it in C#, as usual).
So what we have is a new class called LongOperationManager, which does the following:
- Displays and updates a progress meter (at the bottom left of AutoCAD's window)
- Allowing you to set an arbitrary message and total number of operations
- Listens for "escape" in case the user wants to interrupt the current operation
Here's the class implementation:
public class LongOperationManager :
IDisposable, System.Windows.Forms.IMessageFilter
{
// The message code corresponding to a keypress
const int WM_KEYDOWN = 0x0100;
// The number of times to update the progress meter
// (for some reason you need 600 to tick through
// for each percent)
const int progressMeterIncrements = 600;
// Internal members for metering progress
private ProgressMeter pm;
private long updateIncrement;
private long currentInc;
// External flag for checking cancelled status
public bool cancelled = false;
// Constructor
public LongOperationManager(string message)
{
System.Windows.Forms.Application.
AddMessageFilter(this);
pm = new ProgressMeter();
pm.Start(message);
pm.SetLimit(progressMeterIncrements);
currentInc = 0;
}
// System.IDisposable.Dispose
public void Dispose()
{
pm.Stop();
pm.Dispose();
System.Windows.Forms.Application.
RemoveMessageFilter(this);
}
// Set the total number of operations
public void SetTotalOperations(long totalOps)
{
// We really just care about when we need
// to update the timer
updateIncrement =
(totalOps > progressMeterIncrements ?
totalOps / progressMeterIncrements :
totalOps
);
}
// This function is called whenever an operation
// is performed
public bool Tick()
{
if (++currentInc == updateIncrement)
{
pm.MeterProgress();
currentInc = 0;
System.Windows.Forms.Application.DoEvents();
}
// Check whether the filter has set the flag
if (cancelled)
pm.Stop();
return !cancelled;
}
// The message filter callback
public bool PreFilterMessage(
ref System.Windows.Forms.Message m
)
{
if (m.Msg == WM_KEYDOWN)
{
// Check for the Escape keypress
System.Windows.Forms.Keys kc =
(System.Windows.Forms.Keys)(int)m.WParam &
System.Windows.Forms.Keys.KeyCode;
if (m.Msg == WM_KEYDOWN &&
kc == System.Windows.Forms.Keys.Escape)
{
cancelled = true;
}
// Return true to filter all keypresses
return true;
}
// Return false to let other messages through
return false;
}
}
In terms of how to use the class... first of all you create an instance of it, setting the string to be shown on the progress meter (just like AutoCAD's ProgressMeter class). As the LongOperationManager implements IDisposable, then at the end you should either call Dispose or manage it's scope with the using() statement.
I chose to separate the setting of the total number of operations to be completed from the object's construction, as in our example we need to an initial pass before we know how many objects we're working with (and we want to at least put the label on the progress meter while we perform that initial pass).
Then we just call the Tick() method whenever we perform an operation - this updates the progress meter and checks for use of the escape key. The idea is that you set the total number of operations and then call Tick() for each one of those individual operations - the class takes care of how often it needs to update the progress meter. If it finds escape has been used, the Tick() method will return false.
That's about it, aside from the fact you can also query the "cancelled" property to see whether escape has been used.
Here's the basic approach:
LongOperationManager lom =
new LongOperationManager("Fractalizing entities");
using (lom)
{
...
lom.SetTotalOperations(totalOps);
...
while (true)
{
...
if (!lom.Tick())
{
ed.WriteMessage("\nFractalization cancelled.\n");
break;
}
}
}
Here's the code integrated into the previous example, with the significant lines in red:
1 using Autodesk.AutoCAD.ApplicationServices;
2 using Autodesk.AutoCAD.DatabaseServices;
3 using Autodesk.AutoCAD.EditorInput;
4 using Autodesk.AutoCAD.Runtime;
5 using Autodesk.AutoCAD.Geometry;
6 using System.Collections.Generic;
7 using System;
8
9 namespace Kochizer
10 {
11 public class Commands
12 {
13 // We generate 4 new entities for every old entity
14 // (unless a complex entity such as a polyline)
15
16 const int newEntsPerOldEnt = 4;
17
18 [CommandMethod("KA")]
19 public void KochizeAll()
20 {
21 Document doc =
22 Application.DocumentManager.MdiActiveDocument;
23 Database db = doc.Database;
24 Editor ed = doc.Editor;
25
26 // Acquire user input - whether to create the
27 // new geometry to the left or the right...
28
29 PromptKeywordOptions pko =
30 new PromptKeywordOptions(
31 "\nCreate fractal to side (Left/<Right>): "
32 );
33 pko.Keywords.Add("Left");
34 pko.Keywords.Add("Right");
35
36 PromptResult pr =
37 ed.GetKeywords(pko);
38 bool bLeft = false;
39
40 if (pr.Status != PromptStatus.None &&
41 pr.Status != PromptStatus.OK)
42 return;
43
44 if ((string)pr.StringResult == "Left")
45 bLeft = true;
46
47 // ... and the recursion depth for the command.
48
49 PromptIntegerOptions pio =
50 new PromptIntegerOptions(
51 "\nEnter recursion level <1>: "
52 );
53 pio.AllowZero = false;
54 pio.AllowNegative = false;
55 pio.AllowNone = true;
56
57 PromptIntegerResult pir =
58 ed.GetInteger(pio);
59 int recursionLevel = 1;
60
61 if (pir.Status != PromptStatus.None &&
62 pir.Status != PromptStatus.OK)
63 return;
64
65 if (pir.Status == PromptStatus.OK)
66 recursionLevel = pir.Value;
67
68 // Create and add our long operation handler
69 LongOperationManager lom =
70 new LongOperationManager("Fractalizing entities");
71
72 using (lom)
73 {
74 // Note: strictly speaking we're not recursing,
75 // we're iterating, but the effect to the user
76 // is the same.
77
78 Transaction tr =
79 doc.TransactionManager.StartTransaction();
80 using (tr)
81 {
82 BlockTable bt =
83 (BlockTable)tr.GetObject(
84 db.BlockTableId,
85 OpenMode.ForRead
86 );
87 using (bt)
88 {
89 // No need to open the block table record
90 // for write, as we're just reading data
91 // for now
92
93 BlockTableRecord btr =
94 (BlockTableRecord)tr.GetObject(
95 bt[BlockTableRecord.ModelSpace],
96 OpenMode.ForRead
97 );
98 using (btr)
99 {
100 // List of changed entities
101 // (will contain complex entities, such as
102 // polylines"
103
104 ObjectIdCollection modified =
105 new ObjectIdCollection();
106
107 // List of entities to erase
108 // (will contain replaced entities)
109
110 ObjectIdCollection toErase =
111 new ObjectIdCollection();
112
113 // List of new entitites to add
114 // (will be processed recursively or
115 // assed to the open block table record)
116
117 List<Entity> newEntities =
118 new List<Entity>(
119 db.ApproxNumObjects * newEntsPerOldEnt
120 );
121
122 // Kochize each entity in the open block
123 // table record
124
125 foreach (ObjectId objId in btr)
126 {
127 Entity ent =
128 (Entity)tr.GetObject(
129 objId,
130 OpenMode.ForRead
131 );
132 Kochize(
133 ent,
134 modified,
135 toErase,
136 newEntities,
137 bLeft
138 );
139 }
140
141 // The number of operations is...
142 // The number of complex entities multiplied
143 // by the recursion level (they each get
144 // "kochized" once per level,
145 // even if that's a long operation)
146 // plus
147 // (4^0 + 4^1 + 4^2 + 4^3... + 4^n) multiplied
148 // by the number of db-resident ents
149 // where n is the recursion level. Phew!
150
151 long totalOps =
152 modified.Count * recursionLevel +
153 operationCount(recursionLevel) *
154 toErase.Count;
155 lom.SetTotalOperations(totalOps);
156
157 // If we need to loop,
158 // work on the returned entities
159
160 while (--recursionLevel > 0)
161 {
162 // Create an output array
163
164 List<Entity> newerEntities =
165 new List<Entity>(
166 newEntities.Count * newEntsPerOldEnt
167 );
168
169 // Kochize all the modified (complex) ents
170
171 foreach (ObjectId objId in modified)
172 {
173 if (!lom.Tick())
174 {
175 ed.WriteMessage(
176 "\nFractalization cancelled.\n"
177 );
178 break;
179 }
180
181 Entity ent =
182 (Entity)tr.GetObject(
183 objId,
184 OpenMode.ForRead
185 );
186 Kochize(
187 ent,
188 modified,
189 toErase,
190 newerEntities,
191 bLeft
192 );
193 }
194
195 // Kochize all the non-db resident entities
196 if (!lom.cancelled)
197 {
198 foreach (Entity ent in newEntities)
199 {
200 if (!lom.Tick())
201 {
202 ed.WriteMessage(
203 "\nFractalization cancelled.\n"
204 );
205 break;
206 }
207 Kochize(
208 ent,
209 modified,
210 toErase,
211 newerEntities,
212 bLeft
213 );
214 }
215 }
216
217 // We now longer need the intermediate ents
218 // previously output for the level above,
219 // we replace them with the latest output
220
221 newEntities.Clear();
222 newEntities = newerEntities;
223 }
224
225 lom.Tick();
226
227 if (!lom.cancelled)
228 {
229 // Erase each replaced db-resident ent
230
231 foreach (ObjectId objId in toErase)
232 {
233 Entity ent =
234 (Entity)tr.GetObject(
235 objId,
236 OpenMode.ForWrite
237 );
238 ent.Erase();
239 }
240
241 // Add the new entities
242
243 btr.UpgradeOpen();
244 foreach (Entity ent in newEntities)
245 {
246 btr.AppendEntity(ent);
247 tr.AddNewlyCreatedDBObject(ent, true);
248 }
249 }
250 tr.Commit();
251 }
252 }
253 }
254 }
255 }
256
257 static long
258 operationCount(int nRecurse)
259 {
260 if (1 >= nRecurse)
261 return 1;
262 return
263 (long)Math.Pow(
264 newEntsPerOldEnt,
265 nRecurse - 1
266 )
267 + operationCount(nRecurse - 1);
268 }
269
270 // Dispatch function to call through to various per-type
271 // functions
272
273 private void Kochize(
274 Entity ent,
275 ObjectIdCollection modified,
276 ObjectIdCollection toErase,
277 List<Entity> toAdd,
278 bool bLeft
279 )
280 {
281 Line ln = ent as Line;
282 if (ln != null)
283 {
284 Kochize(ln, modified, toErase, toAdd, bLeft);
285 return;
286 }
287 Arc arc = ent as Arc;
288 if (arc != null)
289 {
290 Kochize(arc, modified, toErase, toAdd, bLeft);
291 return;
292 }
293 Polyline pl = ent as Polyline;
294 if (pl != null)
295 {
296 Kochize(pl, modified, toErase, toAdd, bLeft);
297 return;
298 }
299 }
300
301 // Create 4 new lines from a line passed in
302
303 private void Kochize(
304 Line ln,
305 ObjectIdCollection modified,
306 ObjectIdCollection toErase,
307 List<Entity> toAdd,
308 bool bLeft
309 )
310 {
311 // Get general info about the line
312 // and calculate the main 5 points
313
314 Point3d pt1 = ln.StartPoint,
315 pt5 = ln.EndPoint;
316 Vector3d vec1 = pt5 - pt1,
317 norm1 = vec1.GetNormal();
318 double d_3 = vec1.Length / 3;
319 Point3d pt2 = pt1 + (norm1 * d_3),
320 pt4 = pt1 + (2 * norm1 * d_3);
321 Vector3d vec2 = pt4 - pt2;
322
323 if (bLeft)
324 vec2 =
325 vec2.RotateBy(
326 Math.PI / 3, new Vector3d(0, 0, 1)
327 );
328 else
329 vec2 =
330 vec2.RotateBy(
331 5 * Math.PI / 3, new Vector3d(0, 0, 1)
332 );
333 Point3d pt3 = pt2 + vec2;
334
335 // Mark the original to be erased
336
337 if (ln.ObjectId != ObjectId.Null)
338 toErase.Add(ln.ObjectId);
339
340 // Create the first line
341
342 Line ln1 = new Line(pt1, pt2);
343 ln1.SetPropertiesFrom(ln);
344 ln1.Thickness = ln.Thickness;
345 toAdd.Add(ln1);
346
347 // Create the second line
348
349 Line ln2 = new Line(pt2, pt3);
350 ln2.SetPropertiesFrom(ln);
351 ln2.Thickness = ln.Thickness;
352 toAdd.Add(ln2);
353
354 // Create the third line
355
356 Line ln3 = new Line(pt3, pt4);
357 ln3.SetPropertiesFrom(ln);
358 ln3.Thickness = ln.Thickness;
359 toAdd.Add(ln3);
360
361 // Create the fourth line
362
363 Line ln4 = new Line(pt4, pt5);
364 ln4.SetPropertiesFrom(ln);
365 ln4.Thickness = ln.Thickness;
366 toAdd.Add(ln4);
367 }
368
369 // Create 4 new arcs from an arc passed in
370
371 private void Kochize(
372 Arc arc,
373 ObjectIdCollection modified,
374 ObjectIdCollection toErase,
375 List<Entity> toAdd,
376 bool bLeft
377 )
378 {
379 // Get general info about the arc
380 // and calculate the main 5 points
381
382 Point3d pt1 = arc.StartPoint,
383 pt5 = arc.EndPoint;
384 double length = arc.GetDistAtPoint(pt5),
385 angle = arc.StartAngle;
386 Vector3d full = pt5 - pt1;
387
388 Point3d pt2 = arc.GetPointAtDist(length / 3),
389 pt4 = arc.GetPointAtDist(2 * length / 3);
390
391 // Mark the original to be erased
392
393 if (arc.ObjectId != ObjectId.Null)
394 toErase.Add(arc.ObjectId);
395
396 // Create the first arc
397
398 Point3d mid = arc.GetPointAtDist(length / 6);
399 CircularArc3d tmpArc =
400 new CircularArc3d(pt1, mid, pt2);
401 Arc arc1 = circArc2Arc(tmpArc);
402 arc1.SetPropertiesFrom(arc);
403 arc1.Thickness = arc.Thickness;
404 toAdd.Add(arc1);
405
406 // Create the second arc
407
408 mid = arc.GetPointAtDist(length / 2);
409 tmpArc.Set(pt2, mid, pt4);
410 if (bLeft)
411 tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, pt2);
412 else
413 tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt2);
414 Arc arc2 = circArc2Arc(tmpArc);
415 arc2.SetPropertiesFrom(arc);
416 arc2.Thickness = arc.Thickness;
417 toAdd.Add(arc2);
418
419 // Create the third arc
420
421 tmpArc.Set(pt2, mid, pt4);
422 if (bLeft)
423 tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt4);
424 else
425 tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, pt4);
426 Arc arc3 = circArc2Arc(tmpArc);
427 arc3.SetPropertiesFrom(arc);
428 arc3.Thickness = arc.Thickness;
429 toAdd.Add(arc3);
430
431 // Create the fourth arc
432
433 mid = arc.GetPointAtDist(5 * length / 6);
434 Arc arc4 =
435 circArc2Arc(new CircularArc3d(pt4, mid, pt5));
436 arc4.SetPropertiesFrom(arc);
437 arc4.Thickness = arc.Thickness;
438 toAdd.Add(arc4);
439 }
440
441 Arc circArc2Arc(CircularArc3d circArc)
442 {
443 Point3d center = circArc.Center;
444 Vector3d normal = circArc.Normal;
445 Vector3d refVec = circArc.ReferenceVector;
446 Plane plane = new Plane(center, normal);
447 double ang = refVec.AngleOnPlane(plane);
448 return new Arc(
449 center,
450 normal,
451 circArc.Radius,
452 circArc.StartAngle + ang,
453 circArc.EndAngle + ang
454 );
455 }
456
457 private void Kochize(
458 Polyline pl,
459 ObjectIdCollection modified,
460 ObjectIdCollection toErase,
461 List<Entity> toAdd,
462 bool bLeft
463 )
464 {
465 pl.UpgradeOpen();
466
467 if (pl.ObjectId != ObjectId.Null &&
468 !modified.Contains(pl.ObjectId))
469 {
470 modified.Add(pl.ObjectId);
471 }
472
473 for(int vn = 0; vn < pl.NumberOfVertices; vn++)
474 {
475 SegmentType st = pl.GetSegmentType(vn);
476 if (st != SegmentType.Line && st != SegmentType.Arc)
477 continue;
478
479 double sw = pl.GetStartWidthAt(vn),
480 ew = pl.GetEndWidthAt(vn);
481
482 if (st == SegmentType.Line)
483 {
484 if (vn + 1 == pl.NumberOfVertices)
485 continue;
486
487 LineSegment2d ls = pl.GetLineSegment2dAt(vn);
488 Point2d pt1 = ls.StartPoint,
489 pt5 = ls.EndPoint;
490 Vector2d vec = pt5 - pt1;
491 double d_3 = vec.Length / 3;
492 Point2d pt2 = pt1 + (vec.GetNormal() * d_3),
493 pt4 = pt1 + (vec.GetNormal() * 2 * d_3);
494 Vector2d vec2 = pt4 - pt2;
495
496 if (bLeft)
497 vec2 = vec2.RotateBy(Math.PI / 3);
498 else
499 vec2 = vec2.RotateBy(5 * Math.PI / 3);
500
501 Point2d pt3 = pt2 + vec2;
502
503 pl.AddVertexAt(++vn, pt2, 0, sw, ew);
504 pl.AddVertexAt(++vn, pt3, 0, sw, ew);
505 pl.AddVertexAt(++vn, pt4, 0, sw, ew);
506 }
507 else if (st == SegmentType.Arc)
508 {
509 CircularArc3d ca = pl.GetArcSegmentAt(vn);
510 double oldBulge = pl.GetBulgeAt(vn);
511
512 // Build a standard arc and use that for the calcs
513
514 Arc arc = circArc2Arc(ca);
515
516 // Get the main 5 points
517
518 Point3d pt1 = arc.StartPoint,
519 pt5 = arc.EndPoint;
520
521 double ln = arc.GetDistAtPoint(pt5);
522 Point3d pt2 = arc.GetPointAtDist(ln / 3),
523 pt4 = arc.GetPointAtDist(2 * ln / 3);
524
525 Point3d mid = arc.GetPointAtDist(ln / 2);
526
527 CircularArc3d tmpArc =
528 new CircularArc3d(pt2, mid, pt4);
529 if (bLeft)
530 tmpArc.RotateBy(5*Math.PI/3, tmpArc.Normal, pt4);
531 else
532 tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, pt4);
533
534 Point3d pt3 = tmpArc.StartPoint;
535
536 // Now add the new segments, setting the bulge
537 // for the existing one and the new ones to a third
538 // (as the segs are a third as big as the old one)
539
540 CoordinateSystem3d ecs = pl.Ecs.CoordinateSystem3d;
541 Plane pn = new Plane(ecs.Origin, pl.Normal);
542 double bu = oldBulge / 3;
543
544 pl.SetBulgeAt(vn, bu);
545 pl.AddVertexAt(
546 ++vn, pt2.Convert2d(pn), bu, sw, ew);
547 pl.AddVertexAt(
548 ++vn, pt3.Convert2d(pn), bu, sw, ew);
549 pl.AddVertexAt(
550 ++vn, pt4.Convert2d(pn), bu, sw, ew);
551 }
552 }
553 pl.DowngradeOpen();
554 }
555
556 public class LongOperationManager :
557 IDisposable, System.Windows.Forms.IMessageFilter
558 {
559 // The message code corresponding to a keypress
560 const int WM_KEYDOWN = 0x0100;
561
562 // The number of times to update the progress meter
563 // (for some reason you need 600 to tick through
564 // for each percent)
565 const int progressMeterIncrements = 600;
566
567 // Internal members for metering progress
568 private ProgressMeter pm;
569 private long updateIncrement;
570 private long currentInc;
571
572 // External flag for checking cancelled status
573 public bool cancelled = false;
574
575 // Constructor
576
577 public LongOperationManager(string message)
578 {
579 System.Windows.Forms.Application.
580 AddMessageFilter(this);
581 pm = new ProgressMeter();
582 pm.Start(message);
583 pm.SetLimit(progressMeterIncrements);
584 currentInc = 0;
585 }
586
587 // System.IDisposable.Dispose
588
589 public void Dispose()
590 {
591 pm.Stop();
592 pm.Dispose();
593 System.Windows.Forms.Application.
594 RemoveMessageFilter(this);
595 }
596
597 // Set the total number of operations
598
599 public void SetTotalOperations(long totalOps)
600 {
601 // We really just care about when we need
602 // to update the timer
603 updateIncrement =
604 (totalOps > progressMeterIncrements ?
605 totalOps / progressMeterIncrements :
606 totalOps
607 );
608 }
609
610 // This function is called whenever an operation
611 // is performed
612
613 public bool Tick()
614 {
615 if (++currentInc == updateIncrement)
616 {
617 pm.MeterProgress();
618 currentInc = 0;
619 System.Windows.Forms.Application.DoEvents();
620 }
621 // Check whether the filter has set the flag
622 if (cancelled)
623 pm.Stop();
624
625 return !cancelled;
626 }
627
628 // The message filter callback
629
630 public bool PreFilterMessage(
631 ref System.Windows.Forms.Message m
632 )
633 {
634 if (m.Msg == WM_KEYDOWN)
635 {
636 // Check for the Escape keypress
637 System.Windows.Forms.Keys kc =
638 (System.Windows.Forms.Keys)(int)m.WParam &
639 System.Windows.Forms.Keys.KeyCode;
640
641 if (m.Msg == WM_KEYDOWN &&
642 kc == System.Windows.Forms.Keys.Escape)
643 {
644 cancelled = true;
645 }
646
647 // Return true to filter all keypresses
648 return true;
649 }
650 // Return false to let other messages through
651 return false;
652 }
653 }
654 }
655 }
656
Here's the source the source file for download.