Author Topic: Iterating through block definitions by name  (Read 16133 times)

0 Members and 1 Guest are viewing this topic.

Kerry

  • Mesozoic relic
  • Seagull
  • Posts: 11654
  • class keyThumper<T>:ILazy<T>
Re: Iterating through block definitions by name
« Reply #15 on: March 27, 2012, 06:07:38 AM »
< .. >Now we're really nit-picking   :lmao:

 :-D
kdub, kdub_nz in other timelines.
Perfection is not optional.
Everything will work just as you expect it to, unless your expectations are incorrect.
Discipline: None at all.

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #16 on: March 28, 2012, 12:27:08 PM »

Code - C#: [Select]
  1.    
  2.     else     // caller wants a subset, so filter by runtime class:
  3.     {
  4.       foreach( ObjectId id in source )
  5.       {
  6.         if( id.ObjectClass.IsSubclassOf( rxclass ) )
  7.         {
  8.           yield return (T) id.GetObject( mode, openErased, openLocked );
  9.         }
  10.       }
  11.     }
  12.   }
  13.  
  14.  


oops... sorry, the IsSubclassOf() method is another
one of my extension methods, which I forgot to include.

I edited the post and added it.

kaefer

  • Guest
Re: Iterating through block definitions by name
« Reply #17 on: March 29, 2012, 02:44:49 PM »
If there's only a limited set of specific types that my method can use (all of which implement IEnumerable), then I'd write an overload for each.

This condition isn't met when we consider that my method can use any instance of IEnumerable<ObjectId> too. I have to concede that contrary to gut feeling the number of types which implement IEnumerable (ICollection, IList) only non-generically is indeed limited. Over the complete API of a typical AutoCAD vertical there may be a handful of candidates, AecArchMgd for example seems to have none.

From acdbmgd:
Autodesk.AutoCAD.DatabaseServices.AttributeCollection
Autodesk.AutoCAD.DatabaseServices.BlockTableRecord
Autodesk.AutoCAD.DatabaseServices.ObjectIdCollection
Autodesk.AutoCAD.DatabaseServices.SymbolTable
Autodesk.AutoCAD.DatabaseServices.SymbolTableRecord
Autodesk.AutoCAD.LayerManager.LayerCollection

From aecbasemgd:
Autodesk.Aec.DatabaseServices.ClassificationCollection
Autodesk.Aec.DatabaseServices.DisplayRepresentationIdCollection
Autodesk.Aec.DatabaseServices.ObjectIdCollection


The underlying purpose of the various extension methods I showed is to simplify as much as possible, application-level code.

In my opinion, you optimize and therefore complicate as much as possible. The optimizations can be deemed premature; in the sense of premature in an application's life-cycle. I'm pretty confident that every single of them will result in a measurable if not noticeable performance increase. There's nothing wrong with having a well-tuned and well-tested toolbox at one's disposal when aiming for a commercial release. But the majority of uses are either to get the job done no questions asked, or to prototype, or to explain and explore specific concepts.

Back to the original theme, here's is my variation on iterating the BlockTable lazily. And here are the generic functions again, whose full signatures are inferred as:

Code - F#: [Select]
  1. val getObject :
  2.   Autodesk.AutoCAD.DatabaseServices.ObjectId ->
  3.     #Autodesk.AutoCAD.DatabaseServices.DBObject
  4. val getObjects<'T when 'T :> Autodesk.AutoCAD.DatabaseServices.DBObject> :
  5.   (System.Collections.IEnumerable ->
  6.      seq<#Autodesk.AutoCAD.DatabaseServices.DBObject>)
  7. val userBlocks :
  8.   (Autodesk.AutoCAD.DatabaseServices.ObjectId ->
  9.      seq<Autodesk.AutoCAD.DatabaseServices.BlockTableRecord>)
  10. val take : int -> 'a option -> (seq<'a> -> seq<'a>)

Top-level let-bound functions are compiled as static methods. getObject takes an ObjectId and downcasts to any subtype of DBObject, if one could be inferred. It doesn't depend on a type parameter, that is just a convenient way to supply a type at compile time. getObjects is our bone of contention, and take is a fully generic function.

Code - F#: [Select]
  1. open Autodesk.AutoCAD.DatabaseServices
  2. open Autodesk.AutoCAD.EditorInput
  3. open Autodesk.AutoCAD.Runtime
  4. type acadApp = Autodesk.AutoCAD.ApplicationServices.Application
  5.  
  6. // get DBObject for read and cast to 'T, if type parameter present
  7. let getObject<'T when 'T :> DBObject> (oid: ObjectId) =
  8.    oid.GetObject OpenMode.ForRead :?> 'T
  9.  
  10. // get those objects from an untyped IEnumerable that derive
  11. // from runtime class of type parameter
  12. let getObjects<'T when 'T :> DBObject> : System.Collections.IEnumerable -> _ =
  13.   let rx = RXClass.GetClass typeof<'T>
  14.    Seq.cast<ObjectId>
  15.    >> Seq.filter (fun oid -> oid.ObjectClass.IsDerivedFrom rx)
  16.    >> Seq.map getObject<'T>
  17.  
  18. let userBlocks =
  19.    getObject<BlockTable>
  20.    >> getObjects<BlockTableRecord>
  21.    >> Seq.filter (fun (btr : BlockTableRecord) ->
  22.        not(
  23.            btr.IsAnonymous ||
  24.            btr.IsFromExternalReference ||
  25.            btr.IsFromOverlayReference ||
  26.            btr.IsLayout ||
  27.            btr.IsAProxy ) )
  28.  
  29. let take n whenEndReached =
  30.    Seq.mapi (fun i v ->
  31.        if i < n then Some v
  32.        elif i = n then whenEndReached
  33.        else None )
  34.    >> Seq.takeWhile Option.isSome
  35.    >> Seq.map Option.get
  36.  
  37. [<CommandMethod "PrintTenBlockNames">]
  38. let printTenBlockNamesCommand() =
  39.    let db = HostApplicationServices.WorkingDatabase
  40.    let ed = acadApp.DocumentManager.MdiActiveDocument.Editor
  41.    use tr = db.TransactionManager.StartTransaction()
  42.  
  43.    let userBlockNamesToPrint =
  44.        db.BlockTableId
  45.        |> userBlocks
  46.        |> Seq.map (fun btr -> btr.Name)
  47.        |> take 10 (Some "...")
  48.  
  49.    for s in userBlockNamesToPrint do
  50.        ed.WriteMessage("\n{0} ", s)
  51.        
  52.    tr.Commit()

Edit: Seq.choose isn't lazy, Seq.takeWhile is
« Last Edit: March 29, 2012, 03:42:02 PM by kaefer »

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Iterating through block definitions by name
« Reply #18 on: March 29, 2012, 05:15:00 PM »
Maybe this question is quite different with F# where these functions aren't extension methods (or type extension).
Another thing with F# is that, as far I know, we can't overload functions, so if the getObject<> function uses the OpenMode.ForRead argument, Tony's Upgrade() method should be usefull and be something like this when converted to F#:

Code - F#: [Select]
  1. type AcEx = Autodesk.AutoCAD.Runtime.Exception
  2.  
  3. let upgradeOpen<'T when 'T :> DBObject> : 'T seq -> _ =
  4.    Seq.choose(fun dbo ->
  5.        if dbo.IsWriteEnabled then Some dbo
  6.        else try dbo.UpgradeOpen(); Some dbo
  7.             with | :? AcEx as ex when ex.ErrorStatus = ErrorStatus.OnLockedLayer -> None
  8.                  | _ -> raise (System.ArgumentException()))

With the getObject<> and getObjects<> function as defined  by kaefer, the WEEDLINES command may look like this:

Code - F#: [Select]
  1. [<CommandMethod("WEEDLINES")>]
  2. let weedLines() =
  3.     let db = HostApplicationServices.WorkingDatabase
  4.     use tr = db.TransactionManager.StartTransaction()
  5.     db.CurrentSpaceId
  6.     |> getObject<BlockTableRecord>
  7.     |> getObjects<Line>
  8.     |> Seq.filter(fun l -> l.Length < 0.1)
  9.     |> upgradeOpen
  10.     |> Seq.iter(fun l -> l.Erase())
  11.     tr.Commit()
Speaking English as a French Frog

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #19 on: March 29, 2012, 07:05:57 PM »
Maybe this question is quite different with F# where these functions aren't extension methods (or type extension).
Another thing with F# is that, as far I know, we can't overload functions, so if the getObject<> function uses the OpenMode.ForRead argument, Tony's Upgrade() method should be usefull and be something like this when converted to F#:

Code - F#: [Select]
  1. type AcEx = Autodesk.AutoCAD.Runtime.Exception
  2.  
  3. let upgradeOpen<'T when 'T :> DBObject> : 'T seq -> _ =
  4.    Seq.choose(fun dbo ->
  5.        if dbo.IsWriteEnabled then Some dbo
  6.        else try dbo.UpgradeOpen(); Some dbo
  7.             with | :? AcEx as ex when ex.ErrorStatus = ErrorStatus.OnLockedLayer -> None
  8.                  | _ -> raise (System.ArgumentException()))

With the getObject<> and getObjects<> function as defined  by kaefer, the WEEDLINES command may look like this:

Code - F#: [Select]
  1. [<CommandMethod("WEEDLINES")>]
  2. let weedLines() =
  3.     let db = HostApplicationServices.WorkingDatabase
  4.     use tr = db.TransactionManager.StartTransaction()
  5.     db.CurrentSpaceId
  6.     |> getObject<BlockTableRecord>
  7.     |> getObjects<Line>
  8.     |> Seq.filter(fun l -> l.Length < 0.1)
  9.     |> upgradeOpen
  10.     |> Seq.iter(fun l -> l.Erase())
  11.     tr.Commit()

In this case, overloading was used as a means of
implementing optional parameters, which are supported
directly in C# 4.0, so If i were limiting my code to C# 4.0,
there would be no need for overloading in this context:

Code - C#: [Select]
  1.  
  2. // Uses C# 4.0 optional arguments, no overloads needed:
  3.  
  4. public static class AcDbExtensions
  5. {
  6.  
  7.   public static T GetObject<T>( this ObjectId id,
  8.          OpenMode mode = OpenMode.ForRead,
  9.          bool openErased = false,
  10.          bool openObjectOnLockedLayer = false )
  11.     where T: DBObject
  12.   {
  13.        return (T) id.GetObject( mode, openErased, openObjectOnLockedLayer );
  14.   }
  15.  
  16.   public static IEnumerable<T> GetObjects<T>(
  17.     this BlockTableRecord btr,
  18.     OpenMode mode = OpenMode.ForRead,
  19.     bool openErased = false,
  20.     bool includeObjectsOnLockedLayers = false )  where T: Entity
  21.   {
  22.     BlockTableRecord source = openErased ? btr.IncludingErased : btr;
  23.     bool unfiltered = typeof( T ) == typeof( Entity );
  24.     bool openLocked = mode == OpenMode.ForWrite && includeObjectsOnLockedLayers;
  25.     RXClass rxclass = RXClass.GetClass( typeof( T ) );
  26.     if( unfiltered )
  27.     {
  28.       foreach( ObjectId id in source )
  29.       {
  30.         yield return (T) id.GetObject( mode, openErased, openLocked );
  31.       }
  32.     }
  33.     else     // caller wants a subset, so filter by runtime class:
  34.     {
  35.       foreach( ObjectId id in source )
  36.       {
  37.         if( id.ObjectClass.IsSubclassOf( rxclass ) )
  38.         {
  39.           yield return (T) id.GetObject( mode, openErased, openLocked );
  40.         }
  41.       }
  42.     }
  43.   }
  44.  
  45. }
  46.  
  47.  

The only issue with optional parameters in C# 4.0 that you need to be careful with, is that they are not really optional arguments at all  :lmao:, they're just a compiler trick, and there is a pitfall to their use. When you declare a method that takes an optional argument and call  the method without supplying a value for the optional argument, the generated code passes the default value specified in the method declaration, as if the optional argument were explicitly specified in the call. The issue with that is that the default value you specify in the method declaration is copied to the call site. If the call site is in another assembly, and you then change the default value for the optional argument and recompile the assembly containing the method, the 'old' default value will continue to be passed when the call comes from another assembly that was not also recompiled against the revised assembly containing the method with the optional argument.

Jeff H

  • Needs a day job
  • Posts: 6150
Re: Iterating through block definitions by name
« Reply #20 on: May 02, 2012, 11:56:08 AM »
Just wanted to add that been real busy and not much time to play around but the little code that tested and refactored that is a little different but based on Tony's  extension examples  the more I realize how useful implementing those extensions classes are.
 
It sometimes get tough figuring out what abstractions you need to make and the full consequences of them due to lack of knowledge in areas of the API, but being able to filter, group, create an anonymous type with the properties needed etc.... just helps a ton.
 
Sometimes forget that to AutoCAD is a database and to look at it as if you were query database with all the ins, out, different ways depending on this or that, and gotchas to acessing data, but these help with a nice clean way to get, edit, display, etc....
 
Thanks again Tony
 
If you have not read the examples or messed with them I definitely would,
or of course you could wait a year so until Autodesk 'borrows' it >:D
 
 

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #21 on: May 09, 2012, 05:58:03 AM »
I must have missed this reply, and saw it just now when I went to get the link to this thread.

See below:


If there's only a limited set of specific types that my method can use (all of which implement IEnumerable), then I'd write an overload for each.


This condition isn't met when we consider that my method can use any instance of IEnumerable<ObjectId> too.


Your method isn't using IEnumerable<ObjectId>, it's using IEnumerable and IEnumerator. IEnumerator.Current returns the value boxed in a System.Object.  While it's true that foreach() also uses IEnumerator on types that implement only IEnumerable, the only overload of GetObjects<T>() that I have here that uses foreach() is the one targeting BlockTableRecord, SymbolTable, and IEnumerable<ObjectId>, and the latter will test its argument to see if it is an array and in that case, will delegate to the overload that takes an array of ObjectId[], which uses the array indexer to iterate over the items (because it's faster).

I also have an overload of GetObjects<T> that takes an IEnumerable<ObjectId>, and it is by-far them most complicated of all the overloads, because it tests the argument to see if it is an array of ObjectId[] and uses another overload that is optimized for arrays.  So, I think I have all the bases covered with my overloads.

Ditto for ObjectIdCollection. Here is a reduced version of one of my implementations of GetObjects<T> for ObjectIdCollection:

Code - C#: [Select]
  1.  
  2.   public static IEnumerable<T> GetObjects<T>( this ObjectIdCollection ids )
  3.   {
  4.        RXClass rxclass = RXClass.GetClass( typeof( T ) );
  5.        int cnt = ids.Count;
  6.        for( int i = 0; i < cnt; i++ )
  7.        {
  8.            ObjectId id = ids[i];
  9.            if( id.ObjectClass.IsDerivedFrom( rxclass ) )
  10.               yield return (T) id.GetObject( OpenMode.ForRead, false, false );
  11.        }
  12.    }
  13.  
  14.  

And, I'm not sure what this has do with a method called "GetObjects()" showing up in the Intellisense dropdown list for an instance of anything that implements IEnumerable, and I have a major issue with that.

There really is no excuse for not providing overloads targeting the handful of types that expose a sequence of ObjectIds, especially considering that each also has other distinct variations (like supporting erased objects) that don't apply all IEnumerables. For example, SymbolTable and BlockTableRecord both support the IncludingErased property, so you can't write a single one-size-fits-all version of GetObjects<T> that accommodates that option, or other parameters that do not apply to any IEnumerable, without introducing even more complexity than multiple overloads entails.

Quote
I have to concede that contrary to gut feeling the number of types which implement IEnumerable (ICollection, IList) only non-generically is indeed limited. Over the complete API of a typical AutoCAD vertical there may be a handful of candidates, AecArchMgd for example seems to have none.

From acdbmgd:
Autodesk.AutoCAD.DatabaseServices.AttributeCollection
Autodesk.AutoCAD.DatabaseServices.BlockTableRecord
Autodesk.AutoCAD.DatabaseServices.ObjectIdCollection
Autodesk.AutoCAD.DatabaseServices.SymbolTable
Autodesk.AutoCAD.DatabaseServices.SymbolTableRecord
Autodesk.AutoCAD.LayerManager.LayerCollection

From aecbasemgd:
Autodesk.Aec.DatabaseServices.ClassificationCollection
Autodesk.Aec.DatabaseServices.DisplayRepresentationIdCollection
Autodesk.Aec.DatabaseServices.ObjectIdCollection


The underlying purpose of the various extension methods I showed is to simplify as much as possible, application-level code.

In my opinion, you optimize and therefore complicate as much as possible. The optimizations can be deemed premature; in the sense of premature in an application's life-cycle.


Apparently you didn't get the meaning of 'application-level' code. The complicated library code is what makes it possible for me to write application-level code that is far less complicated. By application-level code, I mean implementation of simple commands and so forth, that one might otherwise write in LISP. But, in fact, the API's I showed a small glimpse of here, makes writing simple commands easier in .NET than they would be if written in LISP.

I usually optimize (and complicate) library code that is called often but rarely if ever modified, making its complexity irrelevant in the everyday use of it. The whole point of making the library code complicated is to un-complicate and simplify the application-level code that uses the library code.

Quote
I'm pretty confident that every single of them will result in a measurable if not noticeable performance increase. There's nothing wrong with having a well-tuned and well-tested toolbox at one's disposal when aiming for a commercial release. But the majority of uses are either to get the job done no questions asked, or to prototype, or to explain and explore specific concepts.

In a post in another thread that I just wrote, I was too lazy to write a code example that uses only native APIs to do something relatively simple in an example command implementation. So, I used the same APIs that I use routinely in my real coding to get the job done more quickly, no questions asked.

It is that complicated API with all of its complicated overloads of GetObjects<T>() that allows me to write much simpler application-level code, much faster and with far fewer errors. As far as a single one-size-fits-all version of GetObjects() goes, that doesn't work because it can't easily provide for options for some types, like including erased objects, etc. 
« Last Edit: May 18, 2012, 02:46:41 PM by TheMaster »

fixo

  • Guest
Re: Iterating through block definitions by name
« Reply #22 on: May 10, 2012, 09:57:06 AM »
Sorry for off-topic
Can you read your PM

CADbloke

  • Bull Frog
  • Posts: 342
  • Crash Test Dummy
Re: Iterating through block definitions by name
« Reply #23 on: May 07, 2013, 10:48:48 PM »
... Here is a reduced version of one of my implementations of GetObjects<T> for ObjectIdCollection:

Code - C#: [Select]
  1.  
  2.   public static IEnumerable<T> GetObjects<T>( this ObjectIdCollection ids )
  3.   {
  4.        RXClass rxclass = RXClass.GetClass( typeof( T ) );
  5.        int cnt = ids.Count;
  6.        for( int i = 0; i < cnt; i++ )
  7.        {
  8.            ObjectId id = ids[i];
  9.            if( id.ObjectClass.IsDerivedFrom( rxclass ) )
  10.               yield return (T) id.GetObject( OpenMode.ForRead, false, false );
  11.        }
  12.    }
  13.  
  14.  

This wouldn't compile for me - Cannot convert type 'Autodesk.AutoCAD.DatabaseServices.DBObject' to 'T' on line 10. Updating Line 2 fixed it ....
     
Code - C#: [Select]
  1. public static IEnumerable<T> GetObjects<T>( this ObjectIdCollection ids )  where T: Entity

Now we're REALLY nitpicking. Great discussion to everyone. Thanks. :)
« Last Edit: May 08, 2013, 05:39:26 AM by CADbloke »

Micaletti

  • Guest
Re: Iterating through block definitions by name
« Reply #24 on: May 07, 2013, 11:33:43 PM »
Wow. Just when I start to feel smug and nerdy, 6 real nerds come along and remind me that I'm just a geek.

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #25 on: May 09, 2013, 12:33:13 AM »
... Here is a reduced version of one of my implementations of GetObjects<T> for ObjectIdCollection:

Code - C#: [Select]
  1.  
  2.   public static IEnumerable<T> GetObjects<T>( this ObjectIdCollection ids )
  3.   {
  4.        RXClass rxclass = RXClass.GetClass( typeof( T ) );
  5.        int cnt = ids.Count;
  6.        for( int i = 0; i < cnt; i++ )
  7.        {
  8.            ObjectId id = ids[i];
  9.            if( id.ObjectClass.IsDerivedFrom( rxclass ) )
  10.               yield return (T) id.GetObject( OpenMode.ForRead, false, false );
  11.        }
  12.    }
  13.  
  14.  

This wouldn't compile for me - Cannot convert type 'Autodesk.AutoCAD.DatabaseServices.DBObject' to 'T' on line 10. Updating Line 2 fixed it ....
     
Code - C#: [Select]
  1. public static IEnumerable<T> GetObjects<T>( this ObjectIdCollection ids )  where T: Entity

Now we're REALLY nitpicking. Great discussion to everyone. Thanks. :)


Yes, sorry. There is a missing constraint, but it should be where T: DBObject, verses Entity, since ObjectIdCollection can contain ids of any DBObject.