As threatened last time, we’re now going to make some efficiency improvements in the original command implementation.

In our previous implementation we were blindly asking for files, one after the other, and using failure to indicate when we’d reached the end. Which was fine, but it limited us in a few ways: we could not reliably parallelize this otherwise highly parallelizable operation, and we couldn’t report accurate progress back to the user (as we didn’t know when it was all going to end).

The best way I know of managing this kind of activity is by integrating F# into your project, and this is – in my opinion – one of the absolutely compelling benefits of the F# language: it’s just so easy to capture the logic of “Asynchronous Workflows”, such as this, and to leave the F# subsystem to execute them as efficiently as it can. And, as we’ll see, for a task where we’re downloading and processing multiple files there are huge performance benefits versus performing this sequentially.

Before we look at the code, a few notes on connecting to the Photosynth web service. F# projects – at least with the April 2010 CTP I’m using with VS 2008 – do not have IDE support for adding web service references, so I decided to keep this “discovery” activity in C#.

Here we see the GetCollectionData() method, which is the one we’re going to use in this application.

The latest source project is available here. I’ve included the two files which have either been introduced (F#) or heavily updated (C#) below, but there have been a few other miscellaneous changes to other files in the project.

Let’s start by looking at the C# code in our updated import-photosynth.cs file:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Colors;

using System.Windows.Threading;

using System.Threading;

using System.Text.RegularExpressions;

using System.ServiceModel;

using System.Reflection;

using System.Net;

using System.IO;

using System.Diagnostics;

using System;

using DemandLoading;

using ImportPhotosynth.PhotosynthService;

using Newtonsoft.Json;

using Newtonsoft.Json.Linq;

namespace ImportPhotosynth

{

publicclassAppl : IExtensionApplication

{

publicvoid Initialize()

{

try

{

RegistryUpdate.RegisterForDemandLoading();

Commands.CleanupOnStartup();

}

catch

{ }

}

publicvoid Terminate()

{

Commands.Cleanup();

}

}

publicclassCommands

{

conststring exeName = "ADNPlugin-BrowsePhotosynth2";

staticProcess _p = null;

staticpublicvoid Cleanup()

{

if (_p != null)

{

if (!_p.HasExited)

_p.Kill();

_p.Dispose();

_p = null;

}

}

staticpublicvoid CleanupOnStartup()

{

bool first = true;

foreach (Process proc inProcess.GetProcesses())

{

if (proc.ProcessName.Contains(exeName))

{

if (first)

{

if (System.Windows.Forms.MessageBox.Show(

"Instances of browser executable found running. " +

"Would you like them closed?",

"Import Photosynth",

System.Windows.Forms.MessageBoxButtons.YesNo

) != System.Windows.Forms.DialogResult.Yes)

{

break;

}

first = false;

}

proc.Kill();

}

}

}

[CommandMethod("BP", CommandFlags.Session)]

publicvoid BrowsePhotosynth()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

Cleanup();

string exePath =

Path.GetDirectoryName(

Assembly.GetExecutingAssembly().Location

) + "\\";

if (!File.Exists(exePath + exeName + ".exe"))

{

ed.WriteMessage(

"\nCould not find the {0} tool: please make sure " +

"it is in the same folder as the application DLL.",

exeName

);

return;

}

// Launch our browser window with the AutoCAD's handle

// so that we can receive back command strings

ProcessStartInfo psi =

newProcessStartInfo(

exePath + exeName,

" " + Application.MainWindow.Handle

);

_p = Process.Start(psi);

}

[CommandMethod("IMPORTPHOTOSYNTH", CommandFlags.NoHistory)]

publicvoid ImportPhotosynth()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

HostApplicationServices ha =

HostApplicationServices.Current;

PromptResult pr =

ed.GetString(

"Enter URL of first Photosynth point cloud: "

);

if (pr.Status != PromptStatus.OK)

return;

string path = pr.StringResult;

pr =

ed.GetString(

"Enter name of Photosynth point cloud: "

);

if (pr.Status != PromptStatus.OK)

return;

string name = pr.StringResult;

// The root path has "points_0_0.bin" on the end.

// Strip off the last 5 characters ("0_0.bin"), so

// that we can compose the sequence of URLs needed

// for each of the point cloud files (usually

// going up to about "points_0_23.bin")

if (path.Length > 5)

path = path.Substring(0, path.Length - 7);

// We'll store most local files in the temp folder.

// We get a temp filename, delete the file and

// use the name for our folder

string localPath = Path.GetTempFileName();

File.Delete(localPath);

Directory.CreateDirectory(localPath);

localPath += "\\";

// Paths for our temporary files

string txtPath = localPath + "points.txt";

string lasPath = localPath + "points.las";

// Our PCG file will be stored under My Documents

string outputPath =

Environment.GetFolderPath(

Environment.SpecialFolder.MyDocuments

) + "\\Photosynth Point Clouds\\";

if (!Directory.Exists(outputPath))

Directory.CreateDirectory(outputPath);

string colId = ExtractCollectionId(path);

// We'll use the title as a base filename for the PCG,

// but will use an incremented integer to get an unused

// filename

int cnt = 0;

string pcgPath;

do

{

pcgPath =

outputPath + MakeValidFileName(name) +

(cnt == 0 ? "" : cnt.ToString()) + ".pcg";

cnt++;

}

while (File.Exists(pcgPath));

// The path to the txt2las tool will be the same as the

// executing assembly (our DLL)

string exePath =

Path.GetDirectoryName(

Assembly.GetExecutingAssembly().Location

) + "\\";

if (!File.Exists(exePath + "txt2las.exe"))

{

ed.WriteMessage(

"\nCould not find the txt2las tool: please make sure it " +

"is in the same folder as the application DLL."

);

return;

}

// We now access the Photosynth web service to get the size of

// the cloud(s) we want to download and process

ed.WriteMessage(

"\nAccessing Photosynth web service to get information on "+

"\"{0}\" point cloud(s)...\n", name

);

// We're interested in two URLs

string dzcUrl, jsonUrl;

// Perform manual binding, to avoid having to add binding info

// into acad.exe.config

BasicHttpBinding binding = newBasicHttpBinding();

EndpointAddress address =

newEndpointAddress(

"http://photosynth.net/photosynthws/PhotosynthService.asmx"

);

// Create our SOAP client

PhotosynthServiceSoapClient soapClient =

newPhotosynthServiceSoapClient(binding, address);

using (soapClient)

{

try

{

// Get the data associated with our Photosynth cloud(s)

CollectionResult colRes =

soapClient.GetCollectionData(newGuid(colId), false);

dzcUrl = colRes.DzcUrl;

jsonUrl = colRes.JsonUrl;

}

catch (FormatException fex)

{

ed.WriteMessage("\nInvalid URL: {0}", fex.Message);

return;

}

catch (EndpointNotFoundException ex)

{

ed.WriteMessage(

"\nCould not connect to Photosynth web service: {0}",

ex.Message

);

return;

}

}

if (jsonUrl == null || dzcUrl == null)

{

ed.WriteMessage(

"\nUnable to find information about this point cloud " +

"via the Photosynth web service."

);

return;

}

string jsonData;

// All being well we should now be able to download and process

// the data about our cloud(s)

using (WebClient webClient = newWebClient())

jsonData = webClient.DownloadString(jsonUrl);

// Extract our point cloud dimension information, as per:

// http://pspcexporter.codeplex.com

JObject jObject = JObject.Parse(jsonData);

JToken cols = jObject["l"] ?? jObject["collections"];

JToken col = cols[colId] ?? cols[string.Empty];

JToken numCoordSystems = col["_num_coord_systems"];

JToken coordSystems = col["x"] ?? col["coord_systems"];

// Create the array of integers representing these

// dimensions

int totalClouds = (int)numCoordSystems;

int[] dims = newint[totalClouds];

// Variables to count the number of files and points

int totalFiles = 0;

long totalPoints = 0;

// Populate our dimensions list and count the files

for (int i = 0; i < totalClouds; i++)

{

JToken cs = coordSystems[Convert.ToString(i)];

JToken pc = cs["k"] ?? cs["pointcloud"];

if (pc != null)

{

string s = (string)pc[0];

if (!string.IsNullOrEmpty(s))

{

JToken binFileCount = pc[1];

int fileCount = (int)binFileCount;

dims[i] = fileCount;

totalFiles += fileCount;

}

}

}

// Report back what we've found, thus far

ed.WriteMessage(

"\n{0} point cloud{1} found across {2} file{3}.\n",

totalClouds, totalClouds == 1 ? "" : "s",

totalFiles, totalFiles == 1 ? "" : "s"

);

// Start the progress meter for our processing

// operation

ProgressMeter pm = newProgressMeter();

using (pm)

{

pm.SetLimit(totalFiles);

pm.Start("Downloading/processing Photosynth points");

try

{

// If the current SynchronizationContext is null

// (which appears to be the case when not called

// from the debugger) then create one and set it

// We will need this to coordinate UI update events

// back with this thread

if (SynchronizationContext.Current == null)

{

DispatcherSynchronizationContext context =

newDispatcherSynchronizationContext(

Dispatcher.CurrentDispatcher

);

SynchronizationContext.SetSynchronizationContext(

context

);

}

// Create our processor object

ProcessPhotosynth.PointCloudProcessor pcp =

newProcessPhotosynth.PointCloudProcessor();

// Capture the start time

DateTime start = DateTime.Now;

// When each file is processed, write a message

// to the command-line and update the progress

pcp.JobCompleted +=

delegate(object sender, Tuple<string, int> args)

{

ed.WriteMessage(

"\nProcessed {0} containing {1} points.",

args.Item1, args.Item2

);

pm.MeterProgress();

};

// Process our point cloud(s)

pcp.ProcessPointCloud(path, dims, txtPath);

// The above function launches a set of asynchronous

// tasks and returns. We need to loop while

// processing UI events until the tasks are complete

while (!pcp.IsComplete)

{

System.Windows.Forms.Application.DoEvents();

}

// Now we can find out the results

totalPoints = pcp.TotalPoints;

// And calculate/report the elapsed time

TimeSpan elapsed = DateTime.Now - start;

ed.WriteMessage(

"\nImported {0} points from {1} file{2} in {3}.\n",

totalPoints, totalFiles, totalFiles == 1 ? "" : "s",

elapsed

);

}

catch (System.Exception ex)

{

ed.WriteMessage(

"\nException occurred: {0}", ex.Message

);

}

// Stop the progress meter

pm.Stop();

}

if (totalPoints > 0)

{

// Use the txt2las utility to create a .LAS

// file from our text file

ProcessStartInfo psi =

newProcessStartInfo(

exePath + "txt2las",

"-i \"" + txtPath +

"\" -o \"" + lasPath +

"\" -parse xyzRGB"

);

psi.CreateNoWindow = false;

psi.WindowStyle = ProcessWindowStyle.Hidden;

// Wait up to 20 seconds for the process to exit

try

{

using (Process p = Process.Start(psi))

{

p.WaitForExit(20000);

}

}

catch

{ }

// If there's a problem, we return

if (!File.Exists(lasPath))

{

ed.WriteMessage(

"\nError creating LAS file."

);

return;

}

File.Delete(txtPath);

ed.WriteMessage(

"Indexing the LAS and attaching the PCG.\n"

);

// Index the .LAS file, creating a .PCG

string lasLisp = lasPath.Replace('\\', '/'),

pcgLisp = pcgPath.Replace('\\', '/');

doc.SendStringToExecute(

"(command \"_.POINTCLOUDINDEX\" \"" +

lasLisp + "\" \"" +

pcgLisp + "\")(princ) ",

false, false, false

);

// Attach the .PCG file

doc.SendStringToExecute(

"_.WAITFORFILE \"" +

pcgLisp + "\" \"" +

lasLisp + "\" " +

"(command \"_.-POINTCLOUDATTACH\" \"" +

pcgLisp +

"\" \"0,0\" \"1\" \"0\")(princ) ",

false, false, false

);

doc.SendStringToExecute(

"_.-VISUALSTYLES _C _Conceptual _.ZOOM _E ",

false, false, false

);

}

}

privatestring ExtractCollectionId(string path)

{

conststring synthTag = ".synth_files";

string colId = "";

if (path.Contains(synthTag))

{

string start =

path.Substring(0, path.IndexOf(synthTag));

if (start.Length > 36)

colId = start.Substring(start.Length - 36);

}

return colId;

}

// A command which waits for a particular PCG file to exist

[CommandMethod("WAITFORFILE", CommandFlags.NoHistory)]

publicvoid WaitForFileToExist()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

HostApplicationServices ha =

HostApplicationServices.Current;

PromptResult pr = ed.GetString("Enter path to PCG: ");

if (pr.Status != PromptStatus.OK)

return;

string pcgPath = pr.StringResult.Replace('/', '\\');

pr = ed.GetString("Enter path to LAS: ");

if (pr.Status != PromptStatus.OK)

return;

string lasPath = pr.StringResult.Replace('/', '\\');

// Check the write time for the PCG file...

// if it hasn't been written to for at least four seconds,

// we can continue

constint numSecs = 4;

TimeSpan span = newTimeSpan(0,0,numSecs);

TimeSpan diff;

while (true)

{

if (File.Exists(pcgPath))

{

DateTime dt = File.GetLastWriteTime(pcgPath);

diff = DateTime.Now - dt;

if (diff.Ticks > span.Ticks)

break;

}

System.Windows.Forms.Application.DoEvents();

}

try

{

CleanupTmpFiles(lasPath);

}

catch

{ }

}

privatevoid CleanupTmpFiles(string txtPath)

{

if (File.Exists(txtPath))

File.Delete(txtPath);

Directory.Delete(

Path.GetDirectoryName(txtPath)

);

}

privatestaticstring MakeValidFileName(string name)

{

string invChars =

Regex.Escape(newstring(Path.GetInvalidFileNameChars()));

string invRegEx = string.Format(@"[{0}]", invChars + ".");

returnRegex.Replace(name, invRegEx, "-");

}

}

}

A few comments on the code:

When running this code a lot in the debugger, I found we were getting lots of hidden instances of our browser executable (as we’re killing it rather than allowing it to exit naturally, due to a bug in the csExWb2 component), so I decided to provide some automatic clean-up functionality on startup of the application

We’ve removed a lot of code dealing with the downloading/processing of point data – this is all now taken care of by the F#-implemented PointCloudProcessor object

This C# code now depends on an additional library called Json.NET (as did the exporter from which I borrowed the code) to parse JSON-formatted information we download from the Photosynth web service

We have a situation where we want to perform asynchronous operations on arbitrary threads, but these operations need to report back to the UI thread in order for us to write text to the command-line and to update AutoCAD’s progress meter. For this we have to make sure we have a valid SynchronizationContext set-up, which will be used from our F# code

Here’s the F# code which takes care of defining and running these asynchronous operations:

We also expose an event which is subscribed to in our C# calling code. This means we don’t need any dependency on AutoCAD libraries in the F# project: to update the command-line and progress meter we simply need to fire that event from the UI thread and let the code execute from the project with the appropriate assembly references

We’re using another asynchronous concept to manage our writing to the local text file for our point data: we’re using an agent to manage this

That’s really about it in terms of the changes. Let’s take it all for a spin.

Downloading and processing these files sequentially takes around 5 minutes (I just measure it at 4:48, but have also see it taking around five and a half).

When we perform the same operation using our new, improved application, it now takes a hair over 35 seconds! That’s less than 1/8th of the time. Here’s this magnificent Photosynth’s point cloud inside AutoCAD:

Well, that’s it for today. I have a few other places I want to invest time working with (or should that be “playing with”? :-) this technology. I want to go through the process of using Photosynth to capture a real-world model and then work on it inside AutoCAD, modelling the captured geometry. I also want to compare the results of this approach with that of working with point clouds generated by a 3D laser scanner (our friends at FARO are hopefully providing one sometime in the next few weeks, which I’m very excited about). This is a really exciting area, and you can expect me to spend more time on it over the coming months (although I’ll continue to address other areas, too, for those that find this stuff boring :-).

[You’ll notice the recording of the Inventor 2011 New APIs is also live – the Revit 2011 version will be posted soon, as will the one for AutoCAD Civil 3D 2011 (once the session has been held, of course :-)].

Adam Nagy, from our DevTech team in Europe, has created this excellent DevNote on the ADN website covering the various options for dealing with this problem. I’ve used this as the basis for the details in today’s post. (Thanks, Adam! :-)

The problem is not at all specific to Autodesk products, although – if the above threads are an indicator – it does seem that as commonly used .NET plugin hosts our products are among the more common environments in which people are hitting this issue.

The cause of the problem boils down to the fact that VS 2010 does not choose the right version of the debugger for Class Library projects targeting prior versions of the .NET Framework: it always uses the default version, the debugger targeting v4 of the .NET Framework. This debugger doesn’t see breakpoints in projects targeting older versions of .NET.

The three workarounds Adam has highlighted show different ways to make VS 2010 use the correct debugger (one which actually hits breakpoints) on these projects. Which solution works best for you will depend on your specific scenario (I would tend to use option 2 or 3, myself).

Solution 1

Start the exe that loads your AddIn (acad.exe, revit.exe, etc) and then attach to it from Visual Studio (Debug -> Attach to Process...)

Solution 2

Modify the config file of the exe that loads your AddIn (acad.exe.config, revit.exe.config, etc) so that it contains the following just before the </configuration> part:

<startup>

<supportedRuntimeversion="v2.0.50727" />

</startup>

Solution 3

Add the exe that loads your AddIn as an existing project to your solution and set the debugger version for it to v2.0.

Right-click the Solution in the Solution Explorer and select Add -> Existing Project... and locate the exe that loads your AddIn

Right-click on the Project that has just been added and select Set as StartUp Project

April 23, 2010

As alluded to in the last post in this series (ignoring a related post that dealt with user interface integration) I wasn’t really happy with some of the tricks I needed in the WinForms version to try and make a coherent user interface for tracking accessed point clouds in a hosted Photosynth browsing session. This post replaces the WinForms UI with one implemented using WPF, and in fact might also have been titled “Using data-binding in WPF to track a list of objects with associated thumbnails” or something to that effect. :-)

What I’ve done in the new version of the solution used to build this tool is to add a separate WPF application project (called BrowsePhotosynth) while leaving the WinForms project there as a historical reference (that one is still called Browser). The new WPF project builds an executable called “ADNPlugins-BrowsePhotosynth2.exe”, which has made it easy to update the DLL project to work with this new version – all we have to do is add a “2” in a few places. Otherwise the code in the main DLL module remains unchanged (and won’t be listed in this post). Please refer back to the previous post for instructions on getting the application to work: if you have already got the last version working, you should be able to drop the new DLL and EXE into the same folder and they should just work.

Firstly we have the main App.xaml file, which only has one interesting change, and that’s to call our Application_Startup() event before the UI is shown:

<Application x:Class="BrowsePhotosynth.App"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

StartupUri="BrowsePhotosynth.xaml"

Startup="Application_Startup">

<Application.Resources>

</Application.Resources>

</Application>

And in the associated App.xaml.cs “code-behind” we have that function’s implementation:

using System.Windows;

namespace BrowsePhotosynth

{

publicpartialclassApp : Application

{

internalstaticint _hwnd;

privatevoid Application_Startup(

object sender, StartupEventArgs e

)

{

// Extract the handle passed as an argument.

// This is AutoCAD's main window, and we'll use it

// to pump messages. If there's no handle, set it

// to 0, which means "standalone mode"

_hwnd =

(e.Args.Length > 0 ? int.Parse(e.Args[0]) : 0);

}

}

}

The only notable difference to the equivalent code from the WinForms version is that we need to look at item 0 in the argument array rather than item 1. Otherwise we store the HWnd as a static integer which we can then access from our main browser implementation.

Here’s the BrowsePhotosynth.xaml file (the indentation is a little off, in terms of where attributes are relative to their elements, but anyway):

The comments should go some way to explaining the logic behind the design.

The “code-behind” file is actually in many ways less complicated than its predecessor, as we aren’t having to deal with custom draw code (that’s taken care of in the XAML, now).

Here’s a summary of the main enhancements/optimizations:

We now use data-binding to display the information we have about our synths

In the previous version we had to maintain various lists (the synth data, the images and the visible list), so this has simplified things greatly

We had a little more plumbing to do to make sure our properties implement INotifyPropertyChanged properly, so that the UI gets updated automatically when the properties change

We also have to use an ObservableCollection<> rather than a List<> for our SynthInfo objects, which allow the notifications to propagate properly. Luckily this doesn’t result in a change in the XML data stored for our synths, which is cool

WPF had issues with dependency properties such as this being updated from other threads, so we no longer use a timer to update our images

Dispatcher.Invoke() is actually a better way for queuing up this task, in any case, and makes life much simpler

Our Window is constructed via App.xaml, and so can no longer take arguments

We now set the URL in our form constructor and get AutoCAD’s HWnd from the app object, where it gets set as a static variable on startup

Our events are now hooked up via XAML, which streamlines our form’s constructor

Because the exception is more visible from WPF, we don’t ever exit the application, even when the “X” is used by the user

We now just hide the form and let AutoCAD kill the process when the BP command is next called

This version of the application doesn’t have a right-click menu for importing the point clouds

This is should be a trivial thing to add back, in case

In terms of the UI changes, we have a nice little slider that allows the user to change the size of the images in the list, which is quite handy

It allows you to zoom into the image to help decide whether to import it

Right, that’s about it for the changes. Here’s the updated C# code file, BrowsePhotosynth.xaml.cs:

using System;

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.ComponentModel;

using System.IO;

using System.Net;

using System.Runtime.InteropServices;

using System.Text;

using System.Text.RegularExpressions;

using System.Threading;

using System.Windows.Threading;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Input;

using System.Xml;

using System.Xml.Serialization;

namespace BrowsePhotosynth

{

///<summary>

/// Interaction logic for BrowsePhotosynth.xaml

///</summary>

publicpartialclassPhotosynthBrowser : Window

{

// A Win32 function we'll use to send messages to AutoCAD

[DllImport("user32.dll")]

privatestaticexternIntPtr SendMessageW(

IntPtr hWnd, int Msg, IntPtr wParam,

refCOPYDATASTRUCT lParam

);

// And the structure we'll require to do so

privatestructCOPYDATASTRUCT

{

publicIntPtr dwData;

publicint cbData;

publicIntPtr lpData;

}

publicabstractclassNotifyPropertyChangedBase :

INotifyPropertyChanged

{

#region INotifyPropertyChanged Members

publiceventPropertyChangedEventHandler PropertyChanged;

#endregion

#region Methods

protectedbool CheckPropertyChanged<T>(

string propertyName, ref T oldValue, ref T newValue

)

{

if (oldValue == null && newValue == null)

returnfalse;

if ((oldValue == null && newValue != null) ||

!oldValue.Equals((T)newValue))

{

oldValue = newValue;

FirePropertyChanged(propertyName);

returntrue;

}

returnfalse;

}

protectedvoid FirePropertyChanged(string propertyName)

{

if (PropertyChanged != null)

{

PropertyChanged(

this, newPropertyChangedEventArgs(propertyName)

);

}

}

#endregion

}

// A class containing the browsing information about each

// point-cloud. This is made serializable to XML, to

// allow easy persistence

[XmlRoot("Synth")]

publicclassSynthInfo : NotifyPropertyChangedBase

{

// The name of our Photosynth

privatestring _name;

[XmlAttribute("Name")]

publicstring Name

{

get { return _name; }

set

{

if (CheckPropertyChanged<string>(

"Name", ref _name, refvalue))

FirePropertyChanged("Name");

}

}

// Its URL

privatestring _url;

[XmlElement("Url")]

publicstring Url

{

get { return _url; }

set

{

if (CheckPropertyChanged<string>(

"Url", ref _url, refvalue))

FirePropertyChanged("Url");

}

}

// The location of its image file

privatestring _image;

[XmlElement("Image")]

publicstring Image

{

get { return _image; }

set

{

if (CheckPropertyChanged<string>(

"Image", ref _image, refvalue))

FirePropertyChanged("ImagePath");

}

}

// A more complete location for the image

// (not persisted to XML)

publicstring ImagePath

{

get { return _imagePath + _image; }

}

}

// The location of our JPGs, PCGs and the history XML

privatestaticstring _imagePath = "";

// We store a central list of these SynthInfo objects

privateObservableCollection<SynthInfo> _synths =

newObservableCollection<SynthInfo>();

// Public property for the URL loaded into the browser

// (this is the URL of the browser, which is typically

// "http://photosynth.net")

privatestring _url = null;

publicstring Url

{

set { _url = value; }

get { return _url; }

}

// Public property for the handle of the AutoCAD application

// we're connected to

privateint _hwnd = 0;

publicint HWnd

{

set { _hwnd = value; }

get { return _hwnd; }

}

// Internal constants

conststring pointsName = "points_0_0.bin";

conststring suffix = " - Photosynth";

conststring historyXml = "BrowsingHistory.xml";

constint imgWidth = 128;

constint imgHeight = imgWidth;

// Form constructor

public PhotosynthBrowser()

{

InitializeComponent();

_imagePath =

Environment.GetFolderPath(

Environment.SpecialFolder.MyDocuments

) + "\\Photosynth Point Clouds\\";

_url = "http://photosynth.net";

_hwnd = App._hwnd;

}

privatevoid WindowClosing(

object sender, System.ComponentModel.CancelEventArgs e

)

{

// Store our browsing history to XML

SerializeHistory(_synths);

// If running from AutoCAD, just hide the application,

// as we need to kill it rather then let it exit

// (the browser control causes a COM Exception in WPF)

if (_hwnd > 0)

{

e.Cancel = true;

Hide();

}

}

privatevoid WindowLoaded(object sender, RoutedEventArgs e)

{

// Navigate to the provided URL, if it's non-null and

// not the one we're already pointed at

if (!String.IsNullOrEmpty(_url) &&

browser.LocationUrl.ToString() != _url)

browser.Navigate(_url);

// Load our browsing history from the XML file.

_synths = DeserializeHistory();

clouds.ItemsSource = _synths;

UpdateImages();

}

// Select an item when the mouse hovers over it

privatevoid ListBoxItem_MouseEnter(

object sender, MouseEventArgs e

)

{

clouds.SelectedItem = (sender asViewbox).DataContext;

if (!clouds.IsFocused)

clouds.Focus();

}

// When an item in our list is clicked on

privatevoid Viewbox_MouseDown(

object sender, MouseButtonEventArgs e

)

{

if (clouds.SelectedItems.Count == 1)

{

if (e.LeftButton == MouseButtonState.Pressed)

ImportPointCloud();

}

}

// Clear our history list when the clear button is clicked

privatevoid ClearButton_Click(object sender, RoutedEventArgs e)

{

_synths.Clear();

string histFile = _imagePath + historyXml;

if (File.Exists(histFile))

File.Delete(histFile);

}

// Find out when HTTP traffic is in progress

privatevoid HttpTransaction(

object sender,

csExWB.ProtocolHandlerBeginTransactionEventArgs e

)

{

// If we detect the first point cloud file in a series...

if (e.URL.Contains(pointsName))

{

csExWB.cEXWB wb = (csExWB.cEXWB)sender;

// Get the page's title and extract the URL

string title = wb.GetTitle(true);

// If the point cloud was embedded in the main page,

// let's extract its actual title from the HTML content

// (this will change as the Photosynth page structure

// changes, but if it doesn't find the relevant entries

// then we just use the overall title)

if (title.StartsWith("Photosynth"))

{

string src = wb.DocumentSource;

if (src.Contains("title-block"))

{

src = src.Substring(src.IndexOf("title-block"));

if (src.Contains("A href="))

{

src = src.Substring(src.IndexOf("A href="));

if (src.Contains(">"))

{

src = src.Substring(src.IndexOf(">"));

if (src.Contains("<"))

{

int endPos = src.IndexOf("<");

if (endPos > 1)

{

title = src.Substring(1, endPos - 1);

}

}

}

}

}

}

elseif (title.EndsWith(suffix))

{

// Strip off the common suffix, if it's there

title =

title.Substring(0, title.Length - suffix.Length);

}

// Extract the base URL, without the initial point-cloud

// name

string baseUrl =

e.URL.Substring(0, e.URL.Length - pointsName.Length);

// Use this info to create a new entry in our list

AddToPointCloudList(title, baseUrl);

// We cannot just start downloading images directly, but

// we can if we queue up the function call

Dispatcher.Invoke(

DispatcherPriority.Normal,

(ThreadStart)delegate() { UpdateImages(); }

);

}

}

// Update the images in our synth list, if needed

privatevoid UpdateImages()

{

// Add images for any items which don't yet have them

// (realistically this will usually be just one image,

// as we check every second and browsing takes time)

for (int i=0; i < _synths.Count; i++)

{

SynthInfo sinf = _synths[i];

if (String.IsNullOrEmpty(sinf.Image))

{

string baseUrl = sinf.Url;

// Transform our base URL to get the URL

// to an appropriate image on the server

if (baseUrl.Contains(".synth_files"))

{

string imageUrl =

baseUrl.Substring(

0, baseUrl.LastIndexOf(".synth_files")

)

+ "_files/6/0_0.jpg";

// Create a web client to download the image

WebClient wc = newWebClient();

using (wc)

{

string locFile =

MakeValidFileName(sinf.Name) + ".jpg";

string locImage = _imagePath + locFile;

// Try to download our image file

try

{

wc.DownloadFile(imageUrl, locImage);

}

catch

{ }

// If we were successful, load and add it

if (File.Exists(locImage))

{

// Make sure our browsing history reflects

// the existence of the downloaded image

sinf.Image = locFile;

_synths[i] = sinf;

}

}

}

}

}

}

// Add a point cloud with a certain title and URL to our list

privatevoid AddToPointCloudList(string title, string baseUrl)

{

bool found = false;

// First we check that it's not already in the list

foreach (SynthInfo sinf in _synths)

{

if (sinf.Name == title && sinf.Url == baseUrl)

{

found = true;

break;

}

}

// If it isn't add it to the list and to our browsing history

if (!found)

{

SynthInfo sinf = newSynthInfo();

sinf.Url = baseUrl;

sinf.Name = title;

_synths.Add(sinf);

}

}

// Import a point cloud into AutoCAD by firing a command

privatevoid ImportPointCloud()

{

// If we're not connected to an AutoCAD session (via

// the handle we received as a command-line argument),

// then we show a message and continue

if (_hwnd == 0)

{

MessageBox.Show(

"This browser is not connected to an instance of " +

"AutoCAD. Relaunch from AutoCAD to import Point " +

"Clouds from your browsing history.",

"Browse Photosynth",

MessageBoxButton.OK,

MessageBoxImage.Information

);

}

else

{

// Assume the item in the _synths list is at the same

// location as the selected item

SynthInfo sinf = _synths[clouds.SelectedIndex];

string title = sinf.Name;

string firstUrl = sinf.Url + pointsName;

// Hide the form and stop the browsing operation

Hide();

browser.NavToBlank();

browser.Stop();

// Fire off our command to AutoCAD

SendCommandToAutoCAD(

"_.IMPORTPHOTOSYNTH \"" + firstUrl + "\" \"" +

title + "\" "

);

// We no longer exit the application, as a COM exception

// causes problems with WPF

}

}

// Save the current state of our browsing history

// to an XML file

privatevoid SerializeHistory(

ObservableCollection<SynthInfo> synths

)

{

if (!Directory.Exists(_imagePath))

Directory.CreateDirectory(_imagePath);

if (_synths.Count > 0)

{

XmlSerializer xs =

newXmlSerializer(

typeof(ObservableCollection<SynthInfo>)

);

XmlTextWriter xw =

newXmlTextWriter(_imagePath + historyXml, Encoding.UTF8);

xs.Serialize(xw, synths);

xw.Close();

}

}

// Read and return the previous browsing history from

// our stored XML file

privateObservableCollection<SynthInfo> DeserializeHistory()

{

string histFile = _imagePath + historyXml;

if (File.Exists(histFile))

{

XmlSerializer xs =

newXmlSerializer(

typeof(ObservableCollection<SynthInfo>)

);

XmlTextReader xr = newXmlTextReader(histFile);

if (xs.CanDeserialize(xr))

{

ObservableCollection<SynthInfo> synths =

(ObservableCollection<SynthInfo>)xs.Deserialize(xr);

xr.Close();

return synths;

}

}

returnnewObservableCollection<SynthInfo>();

}

// Just use the Win32 API to communicate with AutoCAD.

// We simply need to send a command string, so this

// approach avoids a dependency on AutoCAD's COM

// interface

privatevoid SendCommandToAutoCAD(string toSend)

{

constint WM_COPYDATA = 0x4A;

COPYDATASTRUCT cds = newCOPYDATASTRUCT();

cds.dwData = newIntPtr(1);

string data = toSend + "\0";

cds.cbData = data.Length * Marshal.SystemDefaultCharSize;

cds.lpData = Marshal.StringToCoTaskMemAuto(data);

SendMessageW(

newIntPtr(_hwnd),

WM_COPYDATA,

newIntPtr(this.HWnd),

ref cds

);

Marshal.FreeCoTaskMem(cds.lpData);

}

// Function to create a valid filename from a string.

// This has been duplicated from the plugin project

privatestaticstring MakeValidFileName(string name)

{

string invChars =

Regex.Escape(newstring(Path.GetInvalidFileNameChars()));

string invRegEx = string.Format(@"[{0}]", invChars + ".");

returnRegex.Replace(name, invRegEx, "-");

}

}

}

Let’s see what happens when we run our updated BP command. Right off we should see the same history gets displayed in the new browser:

You’ll notice black isn’t mapped as the transparent colour in these images, which can be done with WPF but with quite a bit more work than it took with WinForms, unfortunately.

If we hover over the button at the top, we see it highlighted with our green background colour:

We can use the slider at the bottom to adjust the size of the image to get them all visible in the list:

And we can slide it the other way to zoom right into the thumbnail images, too:

Other than that the application should behave as before with respect to AutoCAD.

My next planned enhancement is on the AutoCAD side of things as opposed to the browsing interface. I’d like to to use the Photosynth web service to query information about a particular synth to help optimize the download and processing of the points. Which may mean using F# or perhaps the Task Parallel Library from C# to parallelize much of these operations (taking advantage of multiple cores and the effect of network latency). But that’s for another day. :-)

April 21, 2010

Once again I’m a little late to the party: Shaan, Volker, Harlan, Brian and Jeremy have all beaten me to it, but perhaps this somewhat later post will be a useful reminder for people. Hopefully, anyway.

Our first DevCamps were held in September 2007. We followed them less than a year later with another set of DevCamps in June 2008. After holding these two sets of events so closely together we learned – and heard from attendees – that a better frequency for these events would be every other year. And that’s certainly our plan moving forward.

Which means we’re now due some more… :-)

These are the two DevCamps we’ll be holding in about 6 weeks from now:

Autodesk Manufacturing DevCamp – June 2-4, 2010 Portland, Oregon, USA

Autodesk AEC DevCamp – June 7-9, 2010 Boston, Massachusetts, USA

DevCamps are great opportunities to learn about the latest Autodesk technology – from a development perspective – in the AEC and Manufacturing industries, as well as being very valuable from a networking perspective: you get to spend quality time with the people who design and implement our products as well as with members of my team, who are – among other things – responsible for making sure external input helps drive these products’ direction.

April 19, 2010

A big thanks to Stephen Preston for passing on this very interesting information. Stephen is one of the “volcanically challenged” members of my team, so fingers crossed he’ll be able to get a flight across to the UK in the coming days. Aside from Stephen’s delayed trip back to the motherland, the recent travel disruptions have also impacted an Inventor API training class being held in Moscow (which will go ahead with a back-up trainer) and extended another team member’s vacation in the Philippines.

The lack of a native 64-bit driver for ODBC, ADO, OLE DB, etc. access to Microsoft Access databases has been a big problem for developers porting their code to support 64-bit, especially as when working within an AutoCAD plugin you’re not able to thunk down to 32-bit using WoW64.

The application was developed by Glenn Ryan, and there are a number of notable things about this tool. Firstly, it implements something that’s very useful – the ability to save and restore the “loaded/unloaded” state of the various external references in a drawing – and, secondly, it does so in a very elegant way. It’s rare that I come across such a well-structure codebase, and I, for one, learned a number of new tricks when looking into it. Glenn has done a great job with this one, and I fully recommend taking the time to check it out.

There’s quite a lot to the code, so I’m not going to duplicate it here. I will show the main application dialog – in this case with a number of XrefStates created – to give you a feel for its capabilities:

Once you’ve set up the XrefStates for your project they get saved inside the master drawing, which allows them to be used in future editing sessions. To do this Glenn chose to use Xrecords, which store the data in a relatively open manner (as opposed to it being locked up in a custom object requiring a enabler).

A big thanks to Glenn for providing this very useful – and well-written – application.

April 13, 2010

I am developing a plugin in C#, which will add a link in Quick Access Toolbar in AutoCAD. […] My problem is that I don’t know how to add a link into existing Quick Access Toolbar and Menu Bar in AutoCAD using ObjectARX SDK and C#. Also I want that on click of that link it should open a new window.

Here’s the C# code. To make it work you will need to place a coupleof .ico files in your DLL’s folder (these could very easily be stored as resources in your application’s project, which is left as an exercise for the reader).

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.Windows;

using System.Windows.Media.Imaging;

using System.Reflection;

using System.IO;

using System.Collections.Generic;

using System;

namespace AppMenus

{

publicclassExtApp : IExtensionApplication

{

// String constants

conststring appText = "Browse Photosynth";

conststring appDesc =

"Browse the Photosynth site and import point " +

"clouds into AutoCAD.";

conststring smallFile = "Browser-16x16.ico";

conststring largeFile = "Browser-32x32.ico";

conststring bpCmd = "_.BP";

publicvoid Initialize()

{

// We defer the creation of our Application Menu to when

// the menu is next accessed

ComponentManager.ApplicationMenu.Opening +=

newEventHandler<EventArgs>(ApplicationMenu_Opening);

// We defer the creation of our Quick Access Toolbar item

// to when the application is next idle

Application.Idle += newEventHandler(Application_OnIdle);

}

publicvoid Terminate()

{

// Assuming these events have fired, they have already

// been removed

ComponentManager.ApplicationMenu.Opening -=

newEventHandler<EventArgs>(ApplicationMenu_Opening);

Application.Idle -= newEventHandler(Application_OnIdle);

}

void Application_OnIdle(object sender, EventArgs e)

{

// Remove the event when it is fired

Application.Idle -= newEventHandler(Application_OnIdle);

// Add our Quick Access Toolbar item

AddQuickAccessToolbarItem();

}

void ApplicationMenu_Opening(object sender, EventArgs e)

{

// Remove the event when it is fired

ComponentManager.ApplicationMenu.Opening -=

newEventHandler<EventArgs>(ApplicationMenu_Opening);

// Add our Application Menu

AddApplicationMenu();

}

privatevoid AddApplicationMenu()

{

ApplicationMenu menu = ComponentManager.ApplicationMenu;

if (menu != null && menu.MenuContent != null)

{

// Create our Application Menu Item

ApplicationMenuItem mi = newApplicationMenuItem();

mi.Text = appText;

mi.Description = appDesc;

mi.LargeImage = GetIcon(largeFile);

// Attach the handler to fire out command

mi.CommandHandler = newAutoCADCommandHandler(bpCmd);

// Add it to the menu content

menu.MenuContent.Items.Add(mi);

}

}

privatevoid AddQuickAccessToolbarItem()

{

Autodesk.Windows.ToolBars.QuickAccessToolBarSource qat =

ComponentManager.QuickAccessToolBar;

if (qat != null)

{

// Create our Ribbon Button

RibbonButton rb = newRibbonButton();

rb.Text = appText;

rb.Description = appDesc;

rb.Image = GetIcon(smallFile);

// Attach the handler to fire out command

rb.CommandHandler = newAutoCADCommandHandler(bpCmd);

// Add it to the Quick Access Toolbar

qat.AddStandardItem(rb);

}

}

private System.Windows.Media.ImageSource GetIcon(string ico)

{

// We'll look for our icons in the folder of the assembly

// (we could also use a resources, of course)

string path =

Path.GetDirectoryName(

Assembly.GetExecutingAssembly().Location

);

// Check our .ico file exists

string fileName = path + "\\" + ico;

if (File.Exists(fileName))

{

// Get access to it via a stream

Stream fs =

newFileStream(

fileName,

FileMode.Open,

FileAccess.Read,

FileShare.Read

);

using (fs)

{

// Decode the contents and return them

IconBitmapDecoder dec =

newIconBitmapDecoder(

fs,

BitmapCreateOptions.PreservePixelFormat,

BitmapCacheOption.Default

);

return dec.Frames[0];

}

}

returnnull;

}

}

// A class to fire commands to AutoCAD

publicclassAutoCADCommandHandler

: System.Windows.Input.ICommand

{

privatestring _command = "";

public AutoCADCommandHandler(string cmd)

{

_command = cmd;

}

#pragma warning disable 67

publiceventEventHandler CanExecuteChanged;

#pragma warning restore 67

publicbool CanExecute(object parameter)

{

returntrue;

}

publicvoid Execute(object parameter)

{

if (!String.IsNullOrEmpty(_command))

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

doc.SendStringToExecute(

_command + " ", false, false, false

);

}

}

}

}

A few comments on the code:

We delay the creation of both the Application Menu and Quick Access Toolbar items, but for different reasons:

The Application Menu only gets created when it’s first accessed, so we need to wait for that to happen before adding our item

The Quick Access Toolbar item cannot be created on Initialize(), as our module may have been loaded on AutoCAD startup and the QAT may not yet be ready

We have temporarily disabled a warning (CS0067) which tells us that an event handler – which we need to implement to complete the ICommand interface – is not used in our code

Now let’s see it in action. As you can probably tell from the code, it’s basically adding a “launch” UI to the application I showed in the last post.

When we select either item, our BP command – as implemented previously – gets launched. Vikas had requested a dialog be shown, but I strongly recommend that this is implemented via a command rather than being displayed directly in the code. This just helps AutoCAD synchronise its user interface appropriately and will avoid lots of subtle issues you might otherwise hit.

April 09, 2010

In the last post we looked at a command to allow importing of Photosynth point clouds into AutoCAD. In this post we’ll put a GUI on the front end, to avoid people having to sniff network traffic to determine the location of the appropriate files on the Photosynth servers.

The application is actually relative simple: it hosts a browser control that gets pointed at the Photosynth web-site, allowing the user to browse through Photosynths. As point clouds are detected (as the browser has some handy events notifying of the HTTP traffic generated by the embedded Photosynth application, and we know that the first point cloud file is always named “points_0_0.bin”), they get added to a list on the right-hand side of the form. They initially get added with just the title (the URL is stored elsewhere) and we then start a timer which will fire each second and – for any items that don’t yet have one – download an image from the server at an appropriate level of detail which we use for a thumbnail. We don’t download the image directly from the HTTP event, as that causes re-entrancy issues (the event will get fired again, which causes the prior event to get cancelled).

Over time the list builds up. When the user feels like it, they can click (or right-click, as they prefer) an item from the list to import it into AutoCAD. This will then cause the command we saw previously to get launched with the URL and the title.

When I started this application I created an in-process form for the browser. I found a really cool control, called csExWb2, which provided the HTTP events for which I was looking. While pretty extensive, there are two main problems with this control: firstly, it’s 32-bit only, so it can’t be hosted inside an AutoCAD plugin on a 64-bit system. Secondly, under certain circumstances it seems to have trouble exiting (and I’m not the only person to hit this, by all accounts).

Thankfully the same approach appears to address both problems: hosting the browser in a separate executable should allow it to run as a 32-bit process on 64-bit systems, and it will allow us to kill the process from our command-implementation should it choose not to exit cleanly. There are ancillary benefits to this approach related to per-process memory consumption (the browser can quickly consume 100+ Mb of memory) and the ability to rebuild the browser without restarting AutoCAD, but those really are of secondary importance.

As we’re using a separate executable, there are clearly some Inter-Process Communication (IPC) issues to deal with. One option would have been to use COM for this, but I decided to go old school and just launch a process for the browser (passing in the handle of the AutoCAD instance as a command-line (string) parameter) and then use the SendMessage() Win32 API to communicate back to AutoCAD.

A couple of extra points to note… I ended up using WinForms for this UI (along with a fun OwnerDraw implementation to make the custom UI look consistent with Photosynth’s) but I could very easily imagine using WPF for this (and I’m sure it would look much better, too). That may be for version 2. I also found the WinForms ListView control to behave quite strangely: I had to jump through some hoops to get the items to centre, for instance, and I’m still not happy that hovering/selecting on the left-hand side of an item doesn’t cause it to be selected. Again, something a WPF version would resolve, I expect.

Most of the heavy lifting is done by the main implementation file behind the BrowserForm class:

using System.Collections.Generic;

using System.ComponentModel;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.IO;

using System.Net;

using System.Runtime.InteropServices;

using System.Text;

using System.Text.RegularExpressions;

using System.Windows.Forms;

using System.Xml;

using System.Xml.Serialization;

using System;

namespace BrowsePhotosynth

{

publicpartialclassBrowserForm : Form

{

// A Win32 function we'll use to send messages to AutoCAD

[DllImport("user32.dll")]

privatestaticexternIntPtr SendMessageW(

IntPtr hWnd, int Msg, IntPtr wParam,

refCOPYDATASTRUCT lParam

);

// And the structure we'll require to do so

privatestructCOPYDATASTRUCT

{

publicIntPtr dwData;

publicint cbData;

publicIntPtr lpData;

}

// A class containing the browsing information about each

// point-cloud. This is made serializable to XML, to

// allow easy persistence

[XmlRoot("Synth")]

publicclassSynthInfo

{

// The name of our Photosynth

privatestring _name;

[XmlAttribute("Name")]

publicstring Name

{

get { return _name; }

set { _name = value; }

}

// Its URL

privatestring _url;

[XmlElement( "Url" )]

publicstring Url

{

get { return _url; }

set { _url = value; }

}

// The location of its image file

privatestring _image;

[XmlElement( "Image" )]

publicstring Image

{

get { return _image; }

set { _image = value; }

}

}

// We store a central list of these SynthInfo objects.

privateList<SynthInfo> _synths = newList<SynthInfo>();

// A timer to call back into the code to download images

// (to avoid rentrancy)

privateTimer _timer = newTimer();

// Public property for the URL loaded into the browser

// (this is the URL of the browser, which is typically

// "http://photosynth.net")

privatestring _url = null;

publicstring Url

{

set { _url = value; }

get { return _url; }

}

// Public property for the handle of the AutoCAD application

// we're connected to

privateint _hwnd = 0;

publicint HWnd

{

set { _hwnd = value; }

get { return _hwnd; }

}

// Internal constants

conststring pointsName = "points_0_0.bin";

conststring suffix = " - Photosynth";

conststring historyXml = "BrowsingHistory.xml";

constint imgWidth = 128;

constint imgHeight = imgWidth;

// Form constructor

public BrowserForm(string url, int hwnd)

{

InitializeComponent();

_url = url;

_hwnd = hwnd;

// Handlers for our various events

// Form events for loading/closing

Load += newEventHandler(BrowserForm_Load);

FormClosing +=

newFormClosingEventHandler(BrowserForm_FormClosing);

// Our main browser event, telling us when a URL is being

// accessed (this allows us to detect when point clouds

// are being accessed by the Photosynth application)

_browser.ProtocolHandlerBeginTransaction +=

new csExWB.ProtocolHandlerBeginTransactionEventHandler(

cEXWB1_ProtocolHandlerBeginTransaction

);

// Events for selection of items from the list (and

// our owner-draw implementation)

_cloudList.MouseClick +=

newMouseEventHandler(cloudList_MouseClick);

_cloudList.DrawItem +=

newDrawListViewItemEventHandler(cloudList_DrawItem);

// Event for selection from the the right-click menu

_cloudsMenu.ItemClicked +=

newToolStripItemClickedEventHandler(

ContextMenuStrip_ItemClicked

);

// Set the appropriate list display properties

_cloudImages.ImageSize = newSize(imgWidth, imgHeight);

_cloudList.OwnerDraw = true;

}

privatevoid BrowserForm_Load(object sender, EventArgs e)

{

// Navigate to the provided URL, if it's non-null and

// not the one we're already pointed at

if (!String.IsNullOrEmpty(_url) &&

_browser.LocationUrl != _url)

_browser.Navigate(_url);

// Load our browsing history from the XML file.

_synths = DeserializeHistory();

// For each member in the history, re-create

// entries in the list view

foreach (SynthInfo sinf in _synths)

{

// Create a list view item with appropriate indentation

ListViewItem lvi = newListViewItem(sinf.Name);

lvi.IndentCount = 10;

_cloudList.Items.Add(lvi);

// If we have a valid image file, load it into the list

// (if one doesn't exist then it should be downloaded

// from the server when the timer fires)

if (!String.IsNullOrEmpty(sinf.Image))

{

string imgFile = GetOutputLocation() + sinf.Image;

if (File.Exists(imgFile))

lvi.ImageIndex =

_cloudImages.Images.Add(

System.Drawing.Image.FromFile(imgFile),

Color.Black

);

}

}

}

void BrowserForm_FormClosing(

object sender, FormClosingEventArgs e

)

{

// Store our browsing history to XML

SerializeHistory(_synths);

}

privatevoid StartTimer()

{

// Create a timer which will fire every second

// (we use this to check for images to download and

// add to our dialog, as we get re-entrancy problems

// if we do so from our http monitoring callback)

_timer.Interval = 100;

_timer.Tick += newEventHandler(OnTick);

_timer.Start();

}

privatevoid StopTimer()

{

// Stop our timer

_timer.Stop();

_timer.Tick -= newEventHandler(OnTick);

}

privatevoid cEXWB1_ProtocolHandlerBeginTransaction(

object sender,

csExWB.ProtocolHandlerBeginTransactionEventArgs e

)

{

// If we detect the first point cloud file in a series...

if (e.URL.Contains(pointsName))

{

csExWB.cEXWB wb = (csExWB.cEXWB)sender;

// Get the page's title and extract the URL

string title = wb.GetTitle(true);

// If the point cloud was embedded in the main page,

// let's extract its actual title from the HTML content

// (this will change as the Photosynth page structure

// changes, but if it doesn't find the relevant entries

// then we just use the overall title)

if (title.StartsWith("Photosynth"))

{

string src = wb.DocumentSource;

if (src.Contains("title-block"))

{

src = src.Substring(src.IndexOf("title-block"));

if (src.Contains("A href="))

{

src = src.Substring(src.IndexOf("A href="));

if (src.Contains(">"))

{

src = src.Substring(src.IndexOf(">"));

if (src.Contains("<"))

{

int endPos = src.IndexOf("<");

if (endPos > 1)

{

title = src.Substring(1, endPos - 1);

}

}

}

}

}

}

elseif (title.EndsWith(suffix))

{

// Strip off the common suffix, if it's there

title =

title.Substring(0, title.Length - suffix.Length);

}

// Extract the base URL, without the initial point-cloud

// name

string baseUrl =

e.URL.Substring(0, e.URL.Length - pointsName.Length);

// Use this info to create a new entry in our list

// and start the timer to get the related image

AddToPointCloudList(title, baseUrl);

StartTimer();

}

}

privatevoid OnTick(object sender, EventArgs e)

{

// When the timer fires, check if there are images to add...

if (_cloudImages.Images.Count < _synths.Count)

{

Cursor old = this.Cursor;

this.Cursor = Cursors.WaitCursor;

// Stop the timer, just during processing

StopTimer();

// Add images for any items which don't yet have them

// (realistically this will usually be just one image,

// as we check every second and browsing takes time)

for (

int i = _cloudImages.Images.Count; i < _synths.Count; i++

)

{

// This should be redundant, as _synths should have the

// same number of items as _cloudList.Items, but anyway

if (i < _cloudList.Items.Count)

{

// Get the information on the synth for which we need

// to download the image

SynthInfo sinf = _synths[i];

string baseUrl = sinf.Url;

// Get the list view item

ListViewItem lvi = _cloudList.Items[i];

// Transform our base URL to get the URL

// to an appropriate image on the server

if (baseUrl.Contains(".synth_files"))

{

string imageUrl =

baseUrl.Substring(

0, baseUrl.LastIndexOf(".synth_files")

)

+ "_files/6/0_0.jpg";

// Create a web client to download the image

WebClient wc = newWebClient();

using (wc)

{

string locFile =

MakeValidFileName(lvi.Text) + ".jpg";

string locImage = GetOutputLocation() + locFile;

// Try to download our image file

try

{

wc.DownloadFile(imageUrl, locImage);

}

catch

{ }

// If we were successful, load and add it

if (File.Exists(locImage))

{

lvi.ImageIndex =

_cloudImages.Images.Add(

System.Drawing.Image.FromFile(locImage),

Color.Black

);

// Make sure our browsing history reflects

// the existence of the downloaded image

sinf.Image = locFile;

_synths[i] = sinf;

}

}

}

}

}

this.Cursor = old;

}

}

privatevoid cloudList_DrawItem(

object sender,

DrawListViewItemEventArgs e

)

{

// Restrict the bounds to no greater than the

// visible width

Rectangle bounds = e.Bounds;

if (bounds.Width > _cloudList.ClientSize.Width)

bounds.Width = _cloudList.ClientSize.Width;

if ((e.State &

(ListViewItemStates.Hot | ListViewItemStates.Selected)

) != 0)

{

// Draw the background for a selected or hovered item

if ((e.State & ListViewItemStates.Hot) != 0)

e.Item.Selected = true;

// Create a linear gradient brush going between

// "Photosynth green" and black, then draw the background

LinearGradientBrush brush =

newLinearGradientBrush(

bounds,

Color.FromArgb(255, 166, 203, 2),

Color.Black,

LinearGradientMode.Vertical

);

using (brush)

{

e.Graphics.FillRectangle(brush, e.Bounds);

}

}

else

{

// Draw a black background for an unselected item

e.Graphics.FillRectangle(Brushes.Black, e.Bounds);

}

// Draw the item text for views other than "Details"

if (_cloudList.View != View.Details)

{

// Restrict the item bounds to no greater than the

// visible width

Rectangle itemSize = e.Item.Bounds;

if (itemSize.Width > _cloudList.ClientSize.Width)

itemSize.Width = _cloudList.ClientSize.Width;

// Reduce the bounds further to the image size we want

itemSize.Inflate(

(itemSize.Width - imgWidth) / -2,

(itemSize.Height - imgHeight) / -2

);

// Get the image from the list, if it's there, otherwise

// create a blank image

System.Drawing.Image img =

(_cloudImages.Images.Count > e.ItemIndex ?

_cloudImages.Images[e.ItemIndex] :

new System.Drawing.Bitmap(imgWidth, imgHeight)

);

// Draw the image and the text

e.Graphics.DrawImage(img, itemSize);

e.DrawText(

TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter

);

}

}

privatevoid cloudList_MouseClick(

object sender,

MouseEventArgs e

)

{

// If an item in the list is clicked, then either import

// the associated point cloud directly or show the menu

// (if the right mouse button was used)

if (_cloudList.SelectedItems.Count == 1)

{

if (e.Button == MouseButtons.Left)

ImportPointCloud();

elseif (e.Button == MouseButtons.Right)

_cloudsMenu.Show(MousePosition);

}

}

privatevoid ContextMenuStrip_ItemClicked(

object sender,

ToolStripItemClickedEventArgs e

)

{

// If the right-click menu item's "import" item

// was used, import the selected point cloud

if (e.ClickedItem.Name == "_importMenuItem")

ImportPointCloud();

}

privatevoid AddToPointCloudList(string title, string baseUrl)

{

// Add a point cloud with a certain title and URL to our list

ListViewItem lvi;

bool found = false;

// First we check that it's not already in the list

for (int idx = 0; idx < _cloudList.Items.Count; idx++)

{

if (_synths.Count > idx)

{

lvi = _cloudList.Items[idx];

if (lvi.Text == title && _synths[idx].Url == baseUrl)

{

found = true;

break;

}

}

}

// If it isn't add it to the list and to our browsing history

if (!found)

{

lvi = newListViewItem(title);

lvi.IndentCount = 10;

_cloudList.Items.Add(lvi);

SynthInfo sinf = newSynthInfo();

sinf.Url = baseUrl;

sinf.Name = title;

_synths.Add(sinf);

}

}

privatevoid ImportPointCloud()

{

// If we're not connected to an AutoCAD session (via

// the handle we received as a command-line argument),

// then we show a message and continue.

if (_hwnd == 0)

{

MessageBox.Show(

"This browser is not connected to an instance of " +

"AutoCAD. Relaunch from AutoCAD to import Point " +

"Clouds from your browsing history.",

"Browse Photosynth",

MessageBoxButtons.OK,

MessageBoxIcon.Information

);

}

else

{

// Get the selected items from the list

ListView.SelectedListViewItemCollection sel =

_cloudList.SelectedItems;

// Get the index of the first (and only) selected item

int idx = sel[0].Index;

// Assume the item in the _synths list is at the same

// location

SynthInfo sinf = _synths[idx];

string title = sinf.Name;

string firstUrl = sinf.Url + pointsName;

// Hide the form and stop the browsing operation

Visible = false;

_browser.NavToBlank();

_browser.Stop();

// Fire off our command to AutoCAD

SendCommandToAutoCAD(

"_.IMPORTPHOTOSYNTH \"" + firstUrl + "\" \"" +

title + "\" "

);

// Exit the application

Application.Exit();

}

}

// Clear the point cloud history from the browser and

// delete the XMl history file.

privatevoid ClearHistory_Click(object sender, EventArgs e)

{

_synths.Clear();

_cloudImages.Images.Clear();

_cloudList.Items.Clear();

string histFile = GetOutputLocation() + historyXml;

if (File.Exists(histFile))

File.Delete(histFile);

}

// The location of our various output files (the PCGs, JPGs

// and the browsing history)

privatestring GetOutputLocation()

{

return

Environment.GetFolderPath(

Environment.SpecialFolder.MyDocuments

) + "\\Photosynth Point Clouds\\";

}

// Save the current state of our browsing history

// to an XML file.

privatevoid SerializeHistory(List<SynthInfo> synths)

{

string outputPath = GetOutputLocation();

if (!Directory.Exists(outputPath))

Directory.CreateDirectory(outputPath);

if (_synths.Count > 0)

{

XmlSerializer xs =

newXmlSerializer(typeof(List<SynthInfo>));

XmlTextWriter xw =

newXmlTextWriter(outputPath + historyXml, Encoding.UTF8);

xs.Serialize(xw, synths);

xw.Close();

}

}

// Read and return the previous browsing history from

// our stored XML file.

privateList<SynthInfo> DeserializeHistory()

{

string histFile = GetOutputLocation() + historyXml;

if (File.Exists(histFile))

{

XmlSerializer xs =

newXmlSerializer(typeof(List<SynthInfo>));

XmlTextReader xr = newXmlTextReader(histFile);

if (xs.CanDeserialize(xr))

{

List<SynthInfo> synths =

(List<SynthInfo>)xs.Deserialize(xr);

xr.Close();

return synths;

}

}

returnnewList<SynthInfo>();

}

// Just use the Win32 API to communicate with AutoCAD.

// We simply need to send a command string, so this

// approach avoids a dependency on AutoCAD's COM

// interface.

privatevoid SendCommandToAutoCAD(string toSend)

{

constint WM_COPYDATA = 0x4A;

COPYDATASTRUCT cds = newCOPYDATASTRUCT();

cds.dwData = newIntPtr(1);

string data = toSend + "\0";

cds.cbData = data.Length * Marshal.SystemDefaultCharSize;

cds.lpData = Marshal.StringToCoTaskMemAuto(data);

SendMessageW(

newIntPtr(_hwnd), WM_COPYDATA, this.Handle, ref cds

);

Marshal.FreeCoTaskMem(cds.lpData);

}

// Function to create a valid filename from a string.

// This has been duplicated from the plugin project.

privatestaticstring MakeValidFileName(string name)

{

string invChars =

Regex.Escape(newstring(Path.GetInvalidFileNameChars()));

string invRegEx = string.Format(@"[{0}]", invChars + ".");

returnRegex.Replace(name, invRegEx, "-");

}

}

}

Here is the code we’ve added to the original plugin code to define our new BP command:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Colors;

using System.Diagnostics;

using System.Text.RegularExpressions;

using System.Reflection;

using System.IO;

using System.Net;

using System;

using DemandLoading;

namespace ImportPhotosynth

{

publicclassAppl : IExtensionApplication

{

publicvoid Initialize()

{

try

{

RegistryUpdate.RegisterForDemandLoading();

}

catch

{ }

}

publicvoid Terminate()

{

Commands.Cleanup();

}

}

publicclassCommands

{

staticProcess _p = null;

staticpublicvoid Cleanup()

{

if (_p != null)

{

if (!_p.HasExited)

_p.Kill();

_p.Dispose();

_p = null;

}

}

[CommandMethod("BP", CommandFlags.Session)]

publicvoid BrowsePhotosynth()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

Cleanup();

string exePath =

Path.GetDirectoryName(

Assembly.GetExecutingAssembly().Location

) + "\\";

if (!File.Exists(exePath + "ADNPlugin-BrowsePhotosynth.exe"))

{

ed.WriteMessage(

"\nCould not find the ADNPlugin-BrowsePhotosynth " +

"tool: please make sure it is in the same folder " +

"as the application DLL."

);

return;

}

// Launch our browser window with the AutoCAD's handle

// so that we can receive back command strings

ProcessStartInfo psi =

newProcessStartInfo(

exePath + "ADNPlugin-BrowsePhotosynth",

" " + Application.MainWindow.Handle

);

_p = Process.Start(psi);

}

[CommandMethod("IMPORTPHOTOSYNTH", CommandFlags.NoHistory)]

publicvoid ImportPhotosynth()

{

…

The rest of the code in this source file is identical to that from the previous post.

A few comments on getting the application to work…

Pre-built versions of the files you need can be found in the project’s bin folder. txt2las.exe is the original one provided on the lastools website, csExWB.dll is built exactly from the application source, and ComUtilities.dll is the pre-built module provided along with it.

You will need to extract the various files into a single folder on your hard drive (I recommend a folder under your main AutoCAD 2011 program files folder – I would normally put them straight into the program files folder, but as some of them have generic names that aren’t prefixed by an RDS, this is a little risky).

You will need to use regsvr32 to register the ComUtilities.dll file: you can either open a command-prompt window and browse to the folder, entering “regsvr32 ComUtilities.dll” or – and this is the approach I tend to use – create a desktop shortcut to the regsvr32.exe file in your Windows\System32 folder and drag & drop the ComUtilities.dll file from Windows Explorer onto that shortcut.

I haven’t tried this out on a 64-bit system, but I believe it will work (if it doesn’t please let me know).

The best way to run the application is directly from inside AutoCAD: you NETLOAD the ADNPlugins-ImportPhotosynth.dll into AutoCAD 2011, which should create demand-loading entries for future, automatic loading. You can then use the BP command to launch the browser dialog. [It’s also possible to launch the executable application directly, but this will only allow you to populate the browsing history for later use inside an AutoCAD session – without being “connected” to AutoCAD, it won’t do anything more).

When the application loads, if there’s an embedded Photosynth in the main page, this should get added to the history (bear in mind that Photosynth now hosts panoramas, so not all items have a point cloud behind):

And as you browse (there’s not yet a “back” button on the form, so you will need to right-click on a non-Silverlight part of the page to get access to this via a context menu) you will see additional Photosynths get added to our history:

And then when you find a Photosynth that interests you, hovering over the item in the right-hand side of the dialog should select it:

And, if you’re inside AutoCAD, you can then left- or right-click it to import it:

If you’re interested in getting at the data generated by this application, open up the “Photosynth Point Clouds” folder under your “My Documents”. In here you’ll find JPGs and PCGs for the various point clouds, as well as an XML file containing your browsing history:

There are some really cool Photosynths out there – knock yourselves out! :-)

In a future post I’m going to show a little more on the “front-end”: to describe an attempt at capturing a 3D point cloud from a set of 2D images. If you’re interested in seeing a first try at this, search Photosynth for “kean notebook”: the first item in the list should be a relatively simple Photosynth of a notebook PC my manager (Jim Quanci) captured using the camera in his Blackberry while we were in the Tokyo office a couple of weeks ago. Nothing very exciting, just the early results of a very basic test.