« December 2007 | Main | February 2008 »
Adding aliases for custom AutoCAD commands
I had an interesting question come in by email and thought I'd share it via this post. To summarise the request, the developer needed to allow command aliasing for their custom commands inside AutoCAD. The good news is that there's a standard mechanism that works for both built-in and custom commands: acad.pgp.
acad.pgp is now found in this location on my system:
C:\Documents and Settings\walmslk\Application Data\Autodesk\AutoCAD 2008\R17.1\enu\Support\acad.pgp
We can edit this text file to add our own command aliases at the end:
NL, *NETLOAD
MCC, *MYCUSTOMCOMMAND
Here we've simply created an alias for the standard NETLOAD command (a simple enough change, but one that saves me lots of time when developing .NET modules) and for a custom command called MYCUSTOMCOMMAND. If I type NL and then MCC at the AutoCAD command-line, after saving the file and re-starting AutoCAD, I see:
Command: NL
NETLOAD
Command: MCC
Unknown command "MYCUSTOMCOMMAND". Press F1 for help.
Now the fact that MCC displays an error is fine - I don't actually have a module loaded that implements the MYCUSTOMCOMMAND command - but we can see that it found the alias and used it (and the MYCUSTOMCOMMAND name could also be used to demand-load an application, for instance).
January 31, 2008 in AutoCAD, Commands | Permalink | Comments (7) | TrackBack
Using F# to simulate hardware behaviour
This post has nothing whatsoever to do with Autodesk software: I just thought some of you might be interested in an old project I worked on during my University studies. I've already mentioned the project briefly in a couple of previous posts.
So, after dusting off the 3.5 floppies I found in the attic, and working out how to extract the code from the gzipped tarballs they contained (thankfully WinZIP took care of that), I started the work to port the code from Miranda to F#. Miranda is still available for many OS platforms, although it has apparently largely been succeeded by the open, committee-defined (originally, at least) functional language, Haskell. But the main point of this exercise was not as much to get the code working as it was for me to become familiar with the F# syntax, and what adjustments might be needed to my thinking for me to code with it.
Before I summarise the lessons learned from the porting exercise, a few words on the original project: I worked on this during 1994-5, with my project partner, Barry Kiernan, supervised by Dr. Steve Hill, from the University of Kent at Canterbury (UKC) Computing Laboratory. I've unfortunately lost contact with both Barry and Steve, so if either of you are reading this, please get in touch!
We adopted Miranda, as this was the functional programming language being taught at UKC at the time. I'm fairly sure that the original code would work with very little modification in Haskell, though, as Miranda is a simpler language and the two appear to have a similar syntax.
The project was to model the behaviour of a Motorola 6800 processor: a simple yet popular, 8-bit processor from the 1970s. The intent behind the project was to validate the use of purely functional programming languages when modelling hardware systems such as micro-processors. What was very interesting was our ability to adjust the level of abstraction: our first implementation used integers to hold op-codes, memory values, register contents, etc., but we later refined it to deal with individual bits of data, moving them around using buses. We also implemented an assembler using Miranda, which was both fun and helpful for testing. That's another strength of functional programming, generally: it is well-suited to language-oriented programming.
I have to admit many specifics of the project are now somewhat vague to me, but I was still able to migrate the code with relatively little effort: despite the fact we're talking about nearly 2,800 lines of source (including comments), it took me several hours, rather than days. I should also point out that I'm certain I haven't used F#'s capabilities optimally - I still consider myself to be a learner when it comes to F# - but I expect I'll come back to the code a tweak it, once in a while.
Here are some notes regarding the migration process:
- F#'s type inference was great: rather than having to define algebraic types for the various functions, these were inferred 100% correctly. The few times I added type information to force the system to understand what I'd done, it turned out to be a logic error I needed to fix.
- F# Interactive was very helpful, although when I first started out with the migration I didn't really use it (I've only since realised how useful a feature it is). I've now come to love the ease with which you can load and test F# code fragments within Visual Studio using F# Interactive.
- For now I've created one monolithic source file. In time I'll probably split this into separate files, but for now this was the simplest way to proceed.
The only big change needed to the code was to remove the use of multiple signatures to define a function's behaviour. With Miranda and Haskell it's standard practice to pattern match at the function signature level. For instance, here's the implementation of a function that performs a "two's complement negate" operation on a list of binary digits:
neg1 :: [num] -> (bool, [num])
neg1 [] = (False, [])
neg1 (1:t)
= (True, (0:comt)) , if inv
= (True, (1:comt)) , otherwise
where
(inv, comt) = neg1 t
neg1 (0:t)
= (True, (1:comt)) , if inv
= (False, (0:comt)) , otherwise
where
(inv, comt) = neg1 t
In F# the pattern matching is performed within the function:
let rec neg1 lst =
match lst with
| [] -> (false, [])
| 1 :: t ->
let (inv, comt) = neg1 t
if inv then
(true, 0::comt)
else
(true, 1::comt)
| 0 :: t ->
let (inv, comt) = neg1 t
if inv then
(true, 1::comt)
else
(false, 0::comt)
| _ -> failwith "neg1 problem!"
These changes were not especially hard to implement, but it did take some time for me to get used to the difference in approach. Note also the final wildcard match ('_') needed to prevent F# from warning me of an incomplete pattern match: this is presumably because the type included in the list was not officially constrained to be binary (0 or 1).
Alright - thanks for bearing with me... here's the F# source file, in case you're still interested. The simplest way to see it in action is to open the file inside Visual Studio (with F# installed, of course), select its entire contents and hit Alt-Enter. This will load it into F# Interactive, at which point you should see some automated test results displayed and be able to run the test assembly language program by typing the following line into the F# Interactive window:
run mult;;
January 29, 2008 in F# | Permalink | Comments (0) | TrackBack
Using F# Asynchronous Workflows to simplify concurrent programming in AutoCAD
In the last post we saw some code that downloaded data - serially - from a number of websites via RSS and created AutoCAD entities linking to the various posts.
As promised, in today's post we take that code and enable it to query the same data in parallel by using Asynchronous Workflows in F#. Asynchronous Workflows are an easy-to-use yet powerful mechanism for enabling concurrent programming in F#.
Firstly, a little background as to why this type of technique is important. As many - if not all - of you are aware, the days of raw processor speed doubling every couple of years are over. The technical innovations that enabled Moore's Law to hold true for half a century are - at least in the area of silicon-based microprocessor design - hitting a wall (it's apparently called the Laws of Physics :-). Barring some disruptive technological development, the future gains in computing performance are to be found in the use of parallel processing, whether via multiple cores, processors or distributed clouds of computing resources.
Additionally, with an increasing focus on distributed computing and information resources, managing tasks asynchronously becomes more important, as information requests across a network inevitably introduce a latency that can be mitigated by the tasks being run in parallel.
The big problem is that concurrent programming is - for the most-part - extremely difficult to do, and even harder to retro-fit into existing applications. Traditional lock-based parallelism (where locks are used to control access to shared computing resources) is both unwieldy and prone to blocking. New technologies, such as Asynchronous Workflows and Software Transactional Memory, provide considerable hope (and this is a topic I have on my list to cover at some future point).
Today's post looks at a relatively simple scenario, in the sense that we want to perform a set of discrete tasks in parallel, harnessing those fancy multi-core systems for those of you lucky enough to have them (I'm hoping to get one when I next replace my notebook, sometime in March), but that these tasks are indeed independent: we want to wait until they are all complete, but we do not have the additional burden of them communicating amongst themselves or using shared resources (e.g. accessing shared memory) during their execution.
We are also going to be very careful only to run parallel tasks unrelated to AutoCAD. Any access made into AutoCAD's database, for instance, needs to be performed in series: AutoCAD is not thread-safe when it comes to the vast majority of its programmatically-accessible functionality. So we're going to run a set of asynchronous, parallel tasks to query our various RSS feeds, and combine the results before creating the corresponding geometry in AutoCAD. This all sounds very complex, but the good (actually great) news is that Asynchronous Workflows does all the heavy lifting. Phew.
Here's the modified F# code, with the modified/new lines coloured in red:
1 // Use lightweight F# syntax
2
3 #light
4
5 // Declare a specific namespace and module name
6
7 module MyNamespace.MyApplication
8
9 // Import managed assemblies
10
11 #I @"C:\Program Files\Autodesk\AutoCAD 2008"
12
13 #r "acdbmgd.dll"
14 #r "acmgd.dll"
15
16 open Autodesk.AutoCAD.Runtime
17 open Autodesk.AutoCAD.ApplicationServices
18 open Autodesk.AutoCAD.DatabaseServices
19 open Autodesk.AutoCAD.Geometry
20 open System.Xml
21 open System.Collections
22 open System.Collections.Generic
23 open System.IO
24 open System.Net
25 open Microsoft.FSharp.Control.CommonExtensions
26
27 // The RSS feeds we wish to get. The first two values are
28 // only used if our code is not able to parse the feed's XML
29
30 let feeds =
31 [ ("Through the Interface",
32 "http://blogs.autodesk.com/through-the-interface",
33 "http://through-the-interface.typepad.com/through_the_interface/rss.xml");
34
35 ("Don Syme's F# blog",
36 "http://blogs.msdn.com/dsyme/",
37 "http://blogs.msdn.com/dsyme/rss.xml");
38
39 ("Shaan Hurley's Between the Lines",
40 "http://autodesk.blogs.com/between_the_lines",
41 "http://autodesk.blogs.com/between_the_lines/rss.xml");
42
43 ("Scott Sheppard's It's Alive in the Lab",
44 "http://blogs.autodesk.com/labs",
45 "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");
46
47 ("Lynn Allen's Blog",
48 "http://blogs.autodesk.com/lynn",
49 "http://lynn.blogs.com/lynn_allens_blog/index.rdf");
50
51 ("Heidi Hewett's AutoCAD Insider",
52 "http://blogs.autodesk.com/autocadinsider",
53 "http://heidihewett.blogs.com/my_weblog/index.rdf") ]
54
55 // Fetch the contents of a web page, asynchronously
56
57 let httpAsync(url:string) =
58 async { let req = WebRequest.Create(url)
59 use! resp = req.GetResponseAsync()
60 use stream = resp.GetResponseStream()
61 use reader = new StreamReader(stream)
62 return reader.ReadToEnd() }
63
64 // Load an RSS feed's contents into an XML document object
65 // and use it to extract the titles and their links
66 // Hopefully these always match (this could be coded more
67 // defensively)
68
69 let titlesAndLinks (name, url, xml) =
70 let xdoc = new XmlDocument()
71 xdoc.LoadXml(xml)
72
73 let titles =
74 [ for n in xdoc.SelectNodes("//*[name()='title']")
75 -> n.InnerText ]
76 let links =
77 [ for n in xdoc.SelectNodes("//*[name()='link']") ->
78 let inn = n.InnerText
79 if inn.Length > 0 then
80 inn
81 else
82 let href = n.Attributes.GetNamedItem("href").Value
83 let rel = n.Attributes.GetNamedItem("rel").Value
84 if href.Contains("feedburner") then
85 ""
86 else
87 href ]
88
89 let descs =
90 [ for n in xdoc.SelectNodes
91 ("//*[name()='description' or name()='content' or name()='subtitle']")
92 -> n.InnerText ]
93
94 // A local function to filter out duplicate entries in
95 // a list, maintaining their current order.
96 // Another way would be to use:
97 // Set.of_list lst |> Set.to_list
98 // but that results in a sorted (probably reordered) list.
99
100 let rec nub lst =
101 match lst with
102 | a::[] -> [a]
103 | a::b ->
104 if a = List.hd b then
105 nub b
106 else
107 a::nub b
108 | [] -> []
109
110 // Filter the links to get (hopefully) the same number
111 // and order as the titles and descriptions
112
113 let real = List.filter (fun (x:string) -> x.Length > 0)
114 let lnks = real links |> nub
115
116 // Return a link to the overall blog, if we don't have
117 // the same numbers of titles, links and descriptions
118
119 let lnum = List.length lnks
120 let tnum = List.length titles
121 let dnum = List.length descs
122
123 if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then
124 [(name,url,url)]
125 else
126 List.zip3 titles lnks descs
127
128 // For a particular (name,url) pair,
129 // create an AutoCAD HyperLink object
130
131 let hyperlink (name,url,desc) =
132 let hl = new HyperLink()
133 hl.Name <- url
134 hl.Description <- desc
135 (name, hl)
136
137 // Use asynchronous workflows in F# to download
138 // an RSS feed and return AutoCAD HyperLinks
139 // corresponding to its posts
140
141 let hyperlinksAsync (name, url, feed) =
142 async { let! xml = httpAsync feed
143 let tl = titlesAndLinks (name, url, xml)
144 return List.map hyperlink tl }
145
146 // Now we declare our command
147
148 [<CommandMethod("rss")>]
149 let createHyperlinksFromRss() =
150
151 // Let's get the usual helpful AutoCAD objects
152
153 let doc =
154 Application.DocumentManager.MdiActiveDocument
155 let db = doc.Database
156
157 // "use" has the same effect as "using" in C#
158
159 use tr =
160 db.TransactionManager.StartTransaction()
161
162 // Get appropriately-typed BlockTable and BTRs
163
164 let bt =
165 tr.GetObject
166 (db.BlockTableId,OpenMode.ForRead)
167 :?> BlockTable
168 let ms =
169 tr.GetObject
170 (bt.[BlockTableRecord.ModelSpace],
171 OpenMode.ForWrite)
172 :?> BlockTableRecord
173
174 // Add text objects linking to the provided list of
175 // HyperLinks, starting at the specified location
176
177 // Note the valid use of tr and ms, as they are in scope
178
179 let addTextObjects pt lst =
180 // Use a for loop, as we care about the index to
181 // position the various text items
182
183 let len = List.length lst
184 for index = 0 to len - 1 do
185 let txt = new DBText()
186 let (name:string,hl:HyperLink) = List.nth lst index
187 txt.TextString <- name
188 let offset =
189 if index = 0 then
190 0.0
191 else
192 1.0
193
194 // This is where you can adjust:
195 // the initial outdent (x value)
196 // and the line spacing (y value)
197
198 let vec =
199 new Vector3d
200 (1.0 * offset,
201 -0.5 * (Int32.to_float index),
202 0.0)
203 let pt2 = pt + vec
204 txt.Position <- pt2
205 ms.AppendEntity(txt) |> ignore
206 tr.AddNewlyCreatedDBObject(txt,true)
207 txt.Hyperlinks.Add(hl) |> ignore
208
209 // Here's where we do the real work, by firing
210 // off - and coordinating - asynchronous tasks
211 // to create HyperLink objects for all our posts
212
213 let links =
214 Async.Run
215 (Async.Parallel
216 [ for (name,url,feed) in feeds ->
217 hyperlinksAsync (name,url,feed) ])
218
219 // Add the resulting objects to the model-space
220
221 let len = Array.length links
222 for index = 0 to len - 1 do
223
224 // This is where you can adjust:
225 // the column spacing (x value)
226 // the vertical offset from origin (y axis)
227
228 let pt =
229 new Point3d
230 (15.0 * (Int32.to_float index),
231 30.0,
232 0.0)
233 addTextObjects pt (Array.get links index)
234
235 tr.Commit()
You can download the new F# source file from here.
A few comments on the changes:
Lines 57-62 define our new httpAsync() function, which uses GetResponseAsync() - a function exposed in F# 1.9.3.7 - to download the contents of a web-page asynchronously [and which I stole shamelessly from Don Syme, who presented the code last summer at Microsoft's TechEd].
Lines 141-144 define another asynchronous function, hyperlinksAsync(), which calls httpAsync() and then - as before - extracts the feed information and creates a corresponding list of HyperLinks. This is significant: creation of AutoCAD HyperLink objects will be done on parallel; it is the addition of these objects to the drawing database that needs to be performed serially.
Lines 214-217 replace our very simple "map" with something slightly more complex: this code runs a list of tasks in parallel and waits for them all to complete before continuing. What is especially cool about this implementation is the fact that exceptions in individual tasks result in the overall task failing (a good thing, believe it or not :-), and the remaining tasks being terminated gracefully.
Lines 221 and 233 change our code to handle an array, rather than a list (while "map" previously returned a list, Async.Run returns an array).
When run, the code creates exactly the same thing as last time (although there are a few more posts in some of the blogs ;-)
A quick word on timing: I used "F# Interactive" to do a little benchmarking on my system, and even though it's single-core, single-processor, there was a considerable difference between the two implementations. I'll talk more about F# Interactive at some point, but think of it to F# in Visual Studio as the command-line is to LISP in AutoCAD: you can very easily test out fragments of F#, either by entering them directly into the F# Interactive window or highlighting them in Visual Studio's text editor and hitting Alt-Enter.
To enable function timing I entered "#time;;" (without the quotations marks) in the F# Interactive window. I then selected and loaded the supporting functions needed for each test - not including the code that adds the DBText objects with their HyperLinks to the database, as we're only in Visual Studio, not inside AutoCAD - and executed the "let links = ..." assignment in our two implementations of the createHyperlinksFromRss() function (i.e. the RSS command). These functions do create lists of AutoCAD HyperLinks, but that's OK: this is something works even outside AutoCAD, although we wouldn't be able to do anything much with them. Also, the fact we're not including the addition of the entities to the AutoCAD database is not relevant: by then we should have identical data in both versions, which would be added in exactly the same way.
Here are the results:
I executed the code for serial querying and parallel querying twice (to make sure there were no effects from page caching on the measurement):
val links : (string * HyperLink) list list
Real: 00:00:14.636, CPU: 00:00:00.15, GC gen0: 5, gen1: 1, gen2: 0
val links : (string * HyperLink) list array
Real: 00:00:06.245, CPU: 00:00:00.31, GC gen0: 3, gen1: 0, gen2: 0
val links : (string * HyperLink) list list
Real: 00:00:15.45, CPU: 00:00:00.46, GC gen0: 5, gen1: 1, gen2: 0
val links : (string * HyperLink) list array
Real: 00:00:03.832, CPU: 00:00:00.62, GC gen0: 2, gen1: 1, gen2: 0
So the serial execution took 14.5 to 15.5 seconds, while the parallel execution took 3.8 to 6.3 seconds.
January 25, 2008 in AutoCAD, AutoCAD .NET, Concurrent programming, F#, Weblogs | Permalink | Comments (1) | TrackBack
Turning AutoCAD into an RSS reader with F#
OK, OK, you are probably thinking "why would anyone ever want to use AutoCAD as an RSS reader?". The answer is, of course, "they wouldn't". The point of the next few posts is not actually to enable AutoCAD to be used to read RSS, but to show how it is possible to use F# and .NET to extract information from RSS feeds and create corresponding AutoCAD entities.
The reason I came onto this subject will also become more clear when you see my next post: I have been researching Asynchronous Workflows in F# - an uber-cool mechanism for managing concurrent, asynchronous tasks - and this seemed like a valid place to start. The problem I was looking for was one where I could simultaneously query and manipulate data from multiple sources, and then use that data to create AutoCAD entities. So, ultimately, the choice of RSS was both logical and completely irrelevant. :-)
Today I'm going to present code that works synchronously: in a single thread we are going to query website after website to download individual RSS feeds and to process them, extracting information on the various posts listed in the RSS, and create HyperLink objects in AutoCAD attached to DBText entities. These will be laid out such that - if you really, really wanted to - you could use these entities to open the various posts in your internet browser.
The reason I chose F# was really the ability to succinctly launch and coordinate asynchronous tasks - something you'll see in the next post, of course. While I could have used C# or VB.NET, F# is also well suited to dealing with lists of data - such as we'll be extracting from the various RSS feeds.
I used F# 1.9.3.7 to run this code: you will certainly need this version to run the code in the following post, as the asynchronous HTTP request functionality is new to the 1.9.3.7 release.
A few additional notes on the implementation... The below code somehow manages to support various RSS standards: Atom, RSS 1.0, RSS 2.0. But some of it feels like a bit of a "hack". The code queries for the titles, links and descriptions contained in each feed, and does some programmatic manipulation to end up - in the cases I've tested - with equal numbers of each. Feeds that use Feedburner, for instance, contain various types of link, which made this very tricky, but the below code appears to work for most cases. The point of this exercise is not to implement an "all singing, all dancing" implementation for RSS consumption: I simply did what was needed to get a number of different blogs working. If a particular feed you add doesn't work, you will just get a single entry created inside AutoCAD. Please don't expect me to debug why it doesn't work for that feed, as that was never the point of this exercise (and I wasted far too long getting to this point, believe me :-).
Here's the F# code:
// Use lightweight F# syntax
#light
// Declare a specific namespace and module name
module MyNamespace.MyApplication
// Import managed assemblies
#I @"C:\Program Files\Autodesk\AutoCAD 2008"
#r "acdbmgd.dll"
#r "acmgd.dll"
open Autodesk.AutoCAD.Runtime
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.Geometry
open System.Xml
open System.Collections
open System.Collections.Generic
open System.IO
open System.Net
open Microsoft.FSharp.Control.CommonExtensions
// The RSS feeds we wish to get. The first two values are
// only used if our code is not able to parse the feed's XML
let feeds =
[ ("Through the Interface",
"http://blogs.autodesk.com/through-the-interface",
"http://through-the-interface.typepad.com/through_the_interface/rss.xml");
("Don Syme's F# blog",
"http://blogs.msdn.com/dsyme/",
"http://blogs.msdn.com/dsyme/rss.xml");
("Shaan Hurley's Between the Lines",
"http://autodesk.blogs.com/between_the_lines",
"http://autodesk.blogs.com/between_the_lines/rss.xml");
("Scott Sheppard's It's Alive in the Lab",
"http://blogs.autodesk.com/labs",
"http://labs.blogs.com/its_alive_in_the_lab/rss.xml");
("Lynn Allen's Blog",
"http://blogs.autodesk.com/lynn",
"http://lynn.blogs.com/lynn_allens_blog/index.rdf");
("Heidi Hewett's AutoCAD Insider",
"http://blogs.autodesk.com/autocadinsider",
"http://heidihewett.blogs.com/my_weblog/index.rdf") ]
// Fetch the contents of a web page, synchronously
let httpSync (url:string) =
let req = WebRequest.Create(url)
use resp = req.GetResponse()
use stream = resp.GetResponseStream()
use reader = new StreamReader(stream)
reader.ReadToEnd()
// Load an RSS feed's contents into an XML document object
// and use it to extract the titles and their links
// Hopefully these always match (this could be coded more
// defensively)
let titlesAndLinks (name, url, xml) =
let xdoc = new XmlDocument()
xdoc.LoadXml(xml)
let titles =
[ for n in xdoc.SelectNodes("//*[name()='title']")
-> n.InnerText ]
let links =
[ for n in xdoc.SelectNodes("//*[name()='link']") ->
let inn = n.InnerText
if inn.Length > 0 then
inn
else
let href = n.Attributes.GetNamedItem("href").Value
let rel = n.Attributes.GetNamedItem("rel").Value
if href.Contains("feedburner") then
""
else
href ]
let descs =
[ for n in xdoc.SelectNodes
("//*[name()='description' or name()='content' or name()='subtitle']")
-> n.InnerText ]
// A local function to filter out duplicate entries in
// a list, maintaining their current order.
// Another way would be to use:
// Set.of_list lst |> Set.to_list
// but that results in a sorted (probably reordered) list.
let rec nub lst =
match lst with
| a::[] -> [a]
| a::b ->
if a = List.hd b then
nub b
else
a::nub b
| [] -> []
// Filter the links to get (hopefully) the same number
// and order as the titles and descriptions
let real = List.filter (fun (x:string) -> x.Length > 0)
let lnks = real links |> nub
// Return a link to the overall blog, if we don't have
// the same numbers of titles, links and descriptions
let lnum = List.length lnks
let tnum = List.length titles
let dnum = List.length descs
if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then
[(name,url,url)]
else
List.zip3 titles lnks descs
// For a particular (name,url) pair,
// create an AutoCAD HyperLink object
let hyperlink (name,url,desc) =
let hl = new HyperLink()
hl.Name <- url
hl.Description <- desc
(name, hl)
// Download an RSS feed and return AutoCAD HyperLinks for its posts
let hyperlinksSync (name, url, feed) =
let xml = httpSync feed
let tl = titlesAndLinks (name, url, xml)
List.map hyperlink tl
// Now we declare our command
[<CommandMethod("rss")>]
let createHyperlinksFromRss() =
// Let's get the usual helpful AutoCAD objects
let doc =
Application.DocumentManager.MdiActiveDocument
let db = doc.Database
// "use" has the same effect as "using" in C#
use tr =
db.TransactionManager.StartTransaction()
// Get appropriately-typed BlockTable and BTRs
let bt =
tr.GetObject
(db.BlockTableId,OpenMode.ForRead)
:?> BlockTable
let ms =
tr.GetObject
(bt.[BlockTableRecord.ModelSpace],
OpenMode.ForWrite)
:?> BlockTableRecord
// Add text objects linking to the provided list of
// HyperLinks, starting at the specified location
// Note the valid use of tr and ms, as they are in scope
let addTextObjects pt lst =
// Use a for loop, as we care about the index to
// position the various text items
let len = List.length lst
for index = 0 to len - 1 do
let txt = new DBText()
let (name:string,hl:HyperLink) = List.nth lst index
txt.TextString <- name
let offset =
if index = 0 then
0.0
else
1.0
// This is where you can adjust:
// the initial outdent (x value)
// and the line spacing (y value)
let vec =
new Vector3d
(1.0 * offset,
-0.5 * (Int32.to_float index),
0.0)
let pt2 = pt + vec
txt.Position <- pt2
ms.AppendEntity(txt) |> ignore
tr.AddNewlyCreatedDBObject(txt,true)
txt.Hyperlinks.Add(hl) |> ignore
// Here's where we use the varous functions
// we've defined
let links =
List.map hyperlinksSync feeds
// Add the resulting objects to the model-space
let len = List.length links
for index = 0 to len - 1 do
// This is where you can adjust:
// the column spacing (x value)
// the vertical offset from origin (y axis)
let pt =
new Point3d
(15.0 * (Int32.to_float index),
30.0,
0.0)
addTextObjects pt (List.nth links index)
tr.Commit()
Here's a portion of what gets created when you run the "rss" command:
That's it for today - in the next post we'll look at how to use asynchronous workflows to run RSS extraction tasks in parallel.
January 23, 2008 in AutoCAD, AutoCAD .NET, F#, Weblogs | Permalink | Comments (3) | TrackBack
Source now available for the .NET Framework 3.5
A quick post, for now, just to point you to this blog:
This will only work with Visual Studio 2008, it seems, so I haven't yet tested this out myself (I tend to be a laggard when it comes to Visual Studio, for some reason).
A quick note on what I've been up to in my spare time: I'm currently diving deeply into F#, to prepare for some internal presentations I'll be giving in February. As part of the exercise I went up to the attic and dusted off the 3.5" floppies containing my old final year project from my Computer Science studies, which uses a functional programming language called Miranda to model the behaviour of a Motorola 6800 processor. I've managed to convert this to F#, and am now seeing if I can get it to show some results.
I'll be back posting more regularly next week, I hope.
January 17, 2008 in F#, Visual Studio | Permalink | Comments (0) | TrackBack
Understanding the properties of textual linetype segments in AutoCAD
In the last post we looked at using .NET to define complex linetypes containing text segments. In the post I admitted to not knowing specifics about the properties used to create the text segment in the linetype, and, in the meantime, an old friend took pity on me and came to the rescue. :-)
Mike Kehoe, who I've known for many years since we worked together in the Guildford office of Autodesk UK, sent me some information that I've reproduced below. Mike now works for Micro Concepts Ltd., an Autodesk reseller, developer and training centre. He originally wrote the below description in the R12/12 timeframe, but apparently most of it remains valid; and while it refers to the text string used to define a linetype in a .lin file, these are also mostly properties that are exposed via the .NET interface.
Example: Using Text within a Linetype.
A,.5,-.2,["MK",STANDARD,S=.2,R=0.0,X=-0.1,Y=-.1],-.2The key elements for defining the TEXT are as follows:
"MK" - These are the letters that will be printed along the line.
STANDARD -This tells AutoCAD what text style to apply to the text. NB: This is optional. When no style is defined AutoCAD will use the current text style – TextStyle holds the setting for the current text style.
[Note from Kean: I found the text style to be mandatory when using the .NET interface.]
S=.2 - This is the text scaling factor. However, there are 2 options: (1) when the text style's height is 0, then S defines the height; in this case, 0.2 units; or (2) when the text style's height parameter is non-zero, the height is found by multiplying the text style's height by this number; in this case, the linetype would place the text at 20% of the height defined in the text style.
R=0.0 - This rotates the text relative to the direction of the line; e.g.: 0.0 means there is no rotation. NB: This is optional. When no rotation is defined AutoCAD will assume zero degrees. The default measurement is degrees; NB: you can use r to specify radians, g for grads, or d for degrees, such as R=150g.
[Note from Kean: just like ObjectARX, the .NET interface accepts radians for this value, in SetShapeRotationAt(). A quick reminder: 360 degrees = 2 x PI radians. So you can pass 90 degrees using "System.Math.PI / 2".]
A=0.0 - This rotates the text relative to the x-axis ("A" is short for Absolute); this ensures the text is always oriented in the same direction, no matter the direction of the line. The rotation is always performed within the text baseline and capital height. That's so that you don't get text rotated way off near the orbit of Pluto.
[Note from Kean: to use this style of rotation using .NET, you need to use SetShapeIsUcsOrientedAt() to make sure the rotation is calculated relative to the current UCS rather than the direction of the line.]
X=-0.1 - This setting moves the text just in the x-direction from the linetype definition vertex.
Y=-0.1 – This setting moves the text in the y-direction from the linetype definition vertex.
These 2 settings can be used to center the text in the line. The units are defined from the linetype scale factor, which is stored in system variable LtScale.
Thanks for the information, Mike!
January 11, 2008 in AutoCAD, AutoCAD .NET, Drawing structure, Object properties | Permalink | Comments (0) | TrackBack
Creating a complex AutoCAD linetype containing text using .NET
In my last post we saw some code to create a simple linetype using .NET. As a comment on that post, Mark said:
Kean, i tried you code and it works great and it also got me thinking... is it possible to programmitically add text in as well? I've tried using ltr.SetTextAt(1, "TEST") but so far i've had no luck, any suggestions???
It turned out to be quite a bit more complicated to make a linetype containing text than merely calling SetTextAt() on one of the segments. In order to understand what properties needed setting, I first loaded the HOT_WATER_SUPPLY linetype from acad.lin (using the LINETYPE command):
I then looked at the contents of the linetype table using ArxDbg (the ObjectARX SDK sample that is very helpful for understanding drawing structure). Here's what the SNOOPDB command - defined by the ArxDbg application - showed for the loaded linetype:
From there it was fairly straightforward to determine the code needed to create our own complex linetype containing text segments. I decided to call the new linetype "COLD_WATER_SUPPLY", and have it resemble the original in every way but placing "CW" in the middle segment, rather than "HW" (with the descriptions updated to match, of course). As I've simply copied the properties of an existing linetype, please don't ask me to explain what they all mean. :-)
Here's the C# code:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
namespace Linetype
{
public class Commands
{
[CommandMethod("CCL")]
public void CreateComplexLinetype()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// We'll use the textstyle table to access
// the "Standard" textstyle for our text
// segment
TextStyleTable tt =
(TextStyleTable)tr.GetObject(
db.TextStyleTableId,
OpenMode.ForRead
);
// Get the linetype table from the drawing
LinetypeTable lt =
(LinetypeTable)tr.GetObject(
db.LinetypeTableId,
OpenMode.ForWrite
);
// Create our new linetype table record...
LinetypeTableRecord ltr =
new LinetypeTableRecord();
// ... and set its properties
ltr.Name = "COLD_WATER_SUPPLY";
ltr.AsciiDescription =
"Cold water supply ---- CW ---- CW ---- CW ----";
ltr.PatternLength = 0.9;
ltr.NumDashes = 3;
// Dash #1
ltr.SetDashLengthAt(0, 0.5);
// Dash #2
ltr.SetDashLengthAt(1, -0.2);
ltr.SetShapeStyleAt(1,

Atom


