After introducing this series in the last post, today we’re going to address the first 3 items on our TODO list:
- The initial blue text
- The theme music
- The star field
- The disappearing Star Wars logo
- The crawling text
The following two items are fairly significant, in their own right, so they’ll each take a post of their own to complete. Oh, and I’ve thrown in a surprise item 6, which I’ll unveil when we implement the crawling text.
Before we dive in, it’s important to make some points about the code: because this is mainly just a bit of fun, this code hasn’t been generalised to work with any drawing and any view, etc. I’ve hardcoded a lot of values that just work on a specific drawing and on my system. The timing works well for me, but may be off when working on systems with different performance profiles. Mileage may vary, as they say.
That’s one reason I’m providing a drawing with the appropriate views set up. I originally planned on putting the layers in there, too, but then decided to create those at runtime (as it was simple to do so). I could have done that with the views, too, but it didn’t seem worth the extra effort.
Another thing I should mention: in my first run at this I used transient graphics to display the intro and crawl text. I then decided to switch to db-resident objects, as I thought I could place them on layers that I turn off when they’re no longer needed. I ended up finding that didn’t work – as I have a single transaction making all the drawing modifications in the command, and couldn’t find a way to have the graphics system reflect the pending database changes – so I went ahead and erased them instead. I decided to stick with db-resident rather than transient graphics, nonetheless, but using transient graphics remains a viable approach for this: the reason I’m not doing so isn’t especially significant.
With that, here’s a look at the code in this post running inside AutoCAD:
A few task-specific comments:
1. The initial blue text
This was straightforward to implement. I ended up creating MText with the font information embedded in the contents, rather than a separate style. This is mainly because it’s the approach I used for the crawl text, later on, which uses multiple fonts. Overkill for this text, of course, but it saves me creating the style.
We’re using the intro text to do a bunch of things, behind the scenes. It’s essentially our splash screen for doing things like downloading the MP3 file for the theme music, etc.
2. The theme music
I came across an online version of the crawl music accessed in this great HTML implementation of the opening crawl. Seems like an ideal candidate for SWAPI integration, extending it beyond the single episode. :-) Here’s the code associated with it, if you want to take a look.
I found out that the System.Windows.Media namespace contains a MediaPlayer object that allows you to access/play an MP3 file via a URL. All that remained was to work out some of the timings related to the music – it starts at 8.5 second in, for instance – and apply these to the code. In this version of the code, the player stops either when the music finishes or when AutoCAD is closed, whichever happens first.
3. The star field
This was a fairly simple matter of generating a bunch of random numbers and using them to define stars. I didn’t want to pass around large numbers of AutoCAD objects, though – whether DBPoints or even Point3ds – so I used a list of F# tuples (with x and y values, each of which holds a float between 0 and 1) that later gets transformed into DBPoints somewhere in the screen space. Although for the surprise item 6 I extended that space to be twice as high as currently needed.
Here’s the F# code implementing our first pass at the EPISODE command:
module StarWars.Crawler
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.ApplicationServices.Core
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.Geometry
open Autodesk.AutoCAD.Runtime
open System
open System.Windows.Media
// The intro music MP3 file
let mp3 =
"http://s.cdpn.io/1202/Star_Wars_original_opening_crawl_1977.mp3"
// The layers we want to create as a list of (name, (r, g, b))
let layers =
[
("Stars", (255, 255, 255));
("Intro", (75, 213, 238))
]
// Create layers based on the provided names and colour values
// (only creates layers if they don't already exist... could be
// updated to make sure the layers are on/thawed and have the
// right colour values)
let createLayers (tr:Transaction) (db:Database) =
let lt =
tr.GetObject(db.LayerTableId, OpenMode.ForWrite) :?> LayerTable
layers |>
List.iter (fun (name, (r, g, b)) ->
if not(lt.Has(name)) then
let lay = new LayerTableRecord()
lay.Color <-
Autodesk.AutoCAD.Colors.Color.FromRgb(byte r, byte g, byte b)
lay.Name <- name
lt.Add(lay) |> ignore
tr.AddNewlyCreatedDBObject(lay, true)
)
// Get a view by name
let getView (tr:Transaction) (db:Database) (name:string) =
let vt =
tr.GetObject(db.ViewTableId, OpenMode.ForRead) :?> ViewTable
if vt.Has(name) then
tr.GetObject(vt.[name], OpenMode.ForRead) :?> ViewTableRecord
else
null
// Add an entity to a block and a transaction
let addToDatabase (tr:Transaction) (btr:BlockTableRecord) o =
btr.AppendEntity(o) |> ignore
tr.AddNewlyCreatedDBObject(o, true)
// Flush the graphics for a particular document
let refresh (doc:Document) =
doc.TransactionManager.QueueForGraphicsFlush()
doc.TransactionManager.FlushGraphics()
// Transform between the Display and World Coordinate Systems
let dcs2wcs (vtr:AbstractViewTableRecord) =
Matrix3d.Rotation(-vtr.ViewTwist, vtr.ViewDirection, vtr.Target) *
Matrix3d.Displacement(vtr.Target - Point3d.Origin) *
Matrix3d.PlaneToWorld(vtr.ViewDirection)
// Poll until a music file has downloaded fully
// (could sleep or use a callback to avoid this being too
// CPU-intensive, but hey)
let rec waitForComplete (mp:MediaPlayer) =
if mp.DownloadProgress < 1. then
System.Windows.Forms.Application.DoEvents()
waitForComplete mp
// Poll until a specified delay has elapsed since start
// (could sleep or use a callback to avoid this being too
// CPU-intensive, but hey)
let rec waitForElapsed (start:DateTime) delay =
let elapsed = DateTime.Now - start
if elapsed.Seconds < delay then
System.Windows.Forms.Application.DoEvents()
waitForElapsed start delay
// Create the intro text as an MText object relative to the view
// (has a parameter to the function doesn't execute when loaded...
// also has hardcoded values that make it view-specific)
let createIntro _ =
let mt = new MText()
mt.Contents <-
"{\\fFranklin Gothic Book|b0|i0|c0|p34;" +
"A long time ago, in a galaxy far,\\Pfar away...}"
mt.Layer <- "Intro"
mt.TextHeight <- 0.5
mt.Width <- 10.
mt.Normal <- Vector3d.ZAxis
mt.TransformBy(Matrix3d.Displacement(new Vector3d(1., 6., 0.)))
mt
// Generate a quantity of randomly located stars... a list of (x,y)
// tuples where x and y are between 0 and 1. These will later
// get transformed into the relevant space (on the screen, etc.)
let locateStars quantity =
// Create our random number generator
let ran = new System.Random()
// Note: _ is only used to make sure this function gets
// executed when it is called... if we have no argument
// it's a value that doesn't require repeated execution
let randomPoint _ =
// Get random values between 0 and 1 for our x and y coordinates
(ran.NextDouble(), ran.NextDouble())
// Local recursive function to create n stars at random
// locations (in the plane of the screen)
let rec randomStars n =
match n with
| 0 -> []
| _ -> (randomPoint 0.) :: randomStars (n-1)
// Create the specified number of stars at random locations
randomStars quantity
// Take locations from 0-1 in X and Y and place them
// relative to the screen
let putOnScreen wid hgt dcs (x, y) =
// We want to populate a space that's 2 screens high (so we
// can pan/rotate downwards at the end of the crawl), hence
// the additional multiplier on y
let pt = new Point3d(wid * (x - 0.5), hgt * ((y * -1.5) + 0.5), 0.)
pt.TransformBy(dcs)
// Commands to recreate the open crawl experience for a selected
// Star Wars episode
[<CommandMethod("EPISODE")>]
let episode() =
// Make sure the active document is valid before continuing
let doc = Application.DocumentManager.MdiActiveDocument
if doc <> null then
let db = doc.Database
let ed = doc.Editor
// Start our transaction and create the required layers
use tr = doc.TransactionManager.StartTransaction()
createLayers tr db
// Get our special Initial and Crawl views
let ivtr = getView tr db "Initial"
let cvtr = getView tr db "Crawl"
if ivtr = null || cvtr = null then
ed.WriteMessage(
"\nPlease load StarWarsCrawl.dwg before running command.")
doc.TransactionManager.EnableGraphicsFlush(true)
let btr =
tr.GetObject(doc.Database.CurrentSpaceId, OpenMode.ForWrite)
:?> BlockTableRecord
// Set the initial view: this gives us higher quality text
ed.SetCurrentView(ivtr)
// First we create the intro text
let intro = createIntro ()
intro |> addToDatabase tr btr
// Make sure the intro text is visible
doc |> refresh
ed.UpdateScreen()
// We'll now perform a number of start-up tasks, while our
// initial intro text is visible... we'll start vy recording
// our start time, so we can synchronise our delay
let start = DateTime.Now
// Get our view's DCS matrix
let dcs = dcs2wcs(cvtr)
// Create a host of stars at random screen positions
locateStars 1000 |>
List.iter
(fun xy ->
let p = putOnScreen cvtr.Width cvtr.Height dcs xy
let dbp = new DBPoint(p)
dbp.Layer <- "Stars"
dbp |> addToDatabase tr btr)
// Open the intro music over the web
let mp = new MediaPlayer()
mp.Open(new Uri(mp3))
// Wait for the download to complete before playing it
waitForComplete mp
// Have a minimum delay of 5 seconds showing the intro text
waitForElapsed start 5
// Start the audio at 8.5 seconds in
mp.Position <- new TimeSpan(0, 0, 0, 8, 500)
mp.Play()
// Switch to the crawl view: this will also change the
// visual style from 2D Wireframe to Realistic
ed.SetCurrentView(cvtr)
// Remove the intro text
intro.Erase()
tr.Commit() // Commit the transaction
That’s it for this part in the series. In the next part we’ll take a look at the code to show the disappearing Star Wars logo inside AutoCAD.