A blog for developers programming with Autodesk platforms, particularly AutoCAD and Forge. With a special focus on AR/VR and IoT.

April 23, 2010

Importing Photosynth point clouds into AutoCAD 2011 - Part 3

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. :-)