The good news with AutoCAD 2013 is its direct support for .TXT and .XYZ files – which means we can remove step 3. The bad news – as RS noticed – is that AutoCAD 2013’s POINTCLOUDINDEX command can’t easily be called programmatically: it respects neither the fact it’s being called from a script (which I typically force by making sure the string I send to the command-line contains an AutoLISP (command) call rather than just the direct command) nor the value of FILEDIA. It always displays a dialog box prompting the user to select the input file.
There is a silver lining to this particular cloud, though: as Christer pointed out, the POINTCLOUDINDEX command’s implementation in AutoCAD 2013 calls through to a standalone indexing executable: AdPointCloudIndexer.exe. This tool resides in AutoCAD’s program folder and uses the codecs contained in the IndexCodecs sub-folder.
Here’s the usage information you get when calling AdPointCloudIndexer inside a command-prompt with no arguments:
There’s actually a nice side benefit of calling the executable directly in step 4 rather than the POINTCLOUDINDEX command: we can now do away with step 5 (which added some complexity to the implementation).
To implement step 3, we need to fire off a process running a command such as this one:
It’s worth noting that to get the RGB data to be recognised and added to the .PCG, I had to save the points to a file of type .XYZ rather than just a plain old .TXT.
Let’s see what this means to the BrowsePhotosynth code (which Viru Aithal is currently reworking to update the Plugin on the Month on Autodesk Labs, so this will hopefully save him some effort :-). The only file I ended up having to update was import-photosynth.cs:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Threading;
using System.Windows.Threading;
using System.Text.RegularExpressions;
using System.Reflection;
using System.IO;
using System.Diagnostics;
using System;
namespace PhotosynthImporter
{
public class Commands
{
const string exeName = "ADNPlugin-PhotosynthBrowser";
readonly string[] syncModes = new string[]
{ "C# synchronous", "F# synchronous", "F# asynchronous" };
static Process _p = null;
Logging.EventLog _el = null;
static public void Cleanup()
{
if (_p != null)
{
if (!_p.HasExited)
_p.Kill();
_p.Dispose();
_p = null;
}
}
static public void CleanupOnStartup()
{
bool first = true;
foreach (Process proc in Process.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("ADNPLUGINS", "EXITPS", CommandFlags.NoHistory)]
public void CleanupBrowser()
{
Cleanup();
}
[CommandMethod("ADNPLUGINS", "REMOVEPS", CommandFlags.Modal)]
static public void RemoveBrowsePhotosynth()
{
DemandLoading.RegistryUpdate.UnregisterForDemandLoading();
Editor ed =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nThe BrowsePhotosynth plugin will not be loaded" +
" automatically in future editing sessions.");
}
[CommandMethod("ADNPLUGINS", "BROWSEPS", CommandFlags.Session)]
static public void 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 =
new ProcessStartInfo(
exePath + exeName,
" " + Application.MainWindow.Handle
);
_p = Process.Start(psi);
}
[CommandMethod("ADNPLUGINS", "IMPORTPS", CommandFlags.NoHistory)]
public void ImportPhotosynth()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptResult pr =
ed.GetString(
"Enter Photosynth collection ID: "
);
if (pr.Status != PromptStatus.OK)
return;
string colId = pr.StringResult;
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.xyz";
// 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);
_el = new Logging.EventLog(outputPath + "log.txt");
// 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 for the AdPointCloudIndexer tool is the same as
// for AutoCAD
string exePath =
Path.GetDirectoryName(
System.Windows.Forms.Application.ExecutablePath
) + "\\";
if (!File.Exists(exePath + "AdPointCloudIndexer.exe"))
{
ed.WriteMessage(
"\nCould not find the AdPointCloudIndexer tool."
);
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
);
int totalClouds = 0, totalFiles = 0;
long totalPts = 0;
int[] dims = null;
try
{
DataFromService.PointAndFileCount(
ed, colId, ref totalClouds, ref totalFiles,
ref totalPts, ref dims
);
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nUnable to get data on this Photosynth: ",
ex.Message
);
return;
}
// Report back what we've found, thus far
ed.WriteMessage(
"\n{0} point cloud{1} found. " +
"{2} point cloud (containing {3} file{4}) " +
"will now be imported.\n",
totalClouds, totalClouds == 1 ? "" : "s",
totalClouds > 1 ? "The first" : "This",
totalFiles, totalFiles == 1 ? "" : "s"
);
// Start the progress meter for our processing
// operation
ProgressMeter pm = new ProgressMeter();
using (pm)
{
pm.SetLimit(totalFiles);
pm.Start("Downloading/processing Photosynth points");
short sync, log;
bool foundVars = true;
try
{
sync =
(short)Application.GetSystemVariable("BROWSEPSSYNC");
log =
(short)Application.GetSystemVariable("BROWSEPSLOG");
}
catch
{
sync = 2;
log = 0;
foundVars = false;
}
if (foundVars)
ed.WriteMessage(
"\nDownloading/processing via {0} (BROWSEPSSYNC = {1}).",
syncModes[sync], sync
);
// Capture the start time
DateTime start = DateTime.Now;
try
{
switch (sync)
{
case 2:
{
// Fully asynchronous using F#
// We will need this to coordinate UI update events
// back with this thread
SynchronizationContext sc =
SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext()
);
bool cancelled = false;
PhotosynthProcAsyncFs.PointCloudProcessor pcp =
new PhotosynthProcAsyncFs.PointCloudProcessor();
// When each file is processed, write a message
// to the command-line and update the progress
pcp.JobCompleted +=
(s, a) =>
{
ed.WriteMessage(
"\nProcessed {0} containing {1} points.",
a.Item1, a.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();
// If the user has cancelled...
cancelled = CheckEscape(ed);
if (cancelled)
{
pcp.Cancel();
break;
}
}
if (pcp.IsComplete && !cancelled)
{
// Now we can find out the results
totalPts = pcp.TotalPoints;
if (pcp.Failures > 0)
ed.WriteMessage(
"\nFailed on {0} files.", pcp.Failures
);
}
else
totalPts = 0;
// Set the sychronization context back
SynchronizationContext.SetSynchronizationContext(sc);
break;
}
case 1:
{
// Synchronous using F#
PhotosynthProcSyncFs.PointCloudProcessor pcp =
new PhotosynthProcSyncFs.PointCloudProcessor();
pcp.JobCompleted +=
(s, a) =>
{
ed.WriteMessage(
"\nProcessed {0} containing {1} points.",
a.Item1, a.Item2
);
pm.MeterProgress();
};
pcp.CheckForCancel +=
(s, a) =>
{
a.Value = CheckEscape(ed);
};
// Process our point cloud
totalPts =
pcp.ProcessPointCloud(path, dims, txtPath);
break;
}
default:
{
// Synchronous using C#
PhotosynthProcSyncCs.PointCloudProcessor pcp =
new PhotosynthProcSyncCs.PointCloudProcessor(
ed, pm, localPath
);
totalPts =
pcp.ProcessPointCloud(path, dims, txtPath);
break;
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nError processing point cloud: {0}.",
ex.Message
);
}
if (totalPts > 0)
{
// Calculate/report the elapsed time
TimeSpan elapsed = DateTime.Now - start;
ed.WriteMessage(
"\nImported {0} points from {1} file{2} in {3}.\n",
totalPts, totalFiles, totalFiles == 1 ? "" : "s",
elapsed
);
if (log > 0)
{
_el.Log(
String.Format(
"\n{0} using {1}: " +
"{2} points from {3} file{4} in {5}\n",
name, syncModes[sync],
totalPts, totalFiles,
totalFiles == 1 ? "" : "s", elapsed
)
);
}
}
// Stop the progress meter
pm.Stop();
}
if (totalPts <= 0)
{
return;
}
// Use the AdPointCloudIndexer tool to create a .PCG from
// our .XYZ file
ed.WriteMessage(
"\nIndexing the downloaded points.\n"
);
ProcessStartInfo psi =
new ProcessStartInfo(
exePath + "AdPointCloudIndexer",
"-i \"" + txtPath + "\" " +
"-o \"" + pcgPath + "\" -RGB"
);
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();
}
}
catch
{ }
// If there's a problem, we return
if (!File.Exists(pcgPath))
{
ed.WriteMessage(
"\nError indexing points."
);
return;
}
CleanupTmpFiles(txtPath);
string pcgLisp = pcgPath.Replace('\\', '/');
// Attach the .PCG file
doc.SendStringToExecute(
"_.-VISUALSTYLES _C _Conceptual " +
"_.UCSICON _OF " +
"(command \"_.-POINTCLOUDATTACH\" \"" +
pcgLisp + "\" \"0,0\" \"1\" \"0\")(princ) ",
false, false, false
);
Cleanup();
}
static internal bool CheckEscape(Editor ed)
{
System.Windows.Forms.Application.DoEvents();
if (HostApplicationServices.Current.UserBreak())
{
ed.WriteMessage(
"\nOperation canceled."
);
return true;
}
return false;
}
internal static void CleanupTmpFiles(string txtPath)
{
if (File.Exists(txtPath))
File.Delete(txtPath);
Directory.Delete(
Path.GetDirectoryName(txtPath)
);
}
private static string MakeValidFileName(string name)
{
string invChars =
Regex.Escape(
new string(Path.GetInvalidFileNameChars()) + ","
);
string invRegEx = string.Format(@"[{0}]", invChars + ".");
return Regex.Replace(name, invRegEx, "-");
}
}
}
Aside from the use of AdPointCloudIndexer, I also made some other miscellaneous changes to the code:
This results in a cloud of 703K points being brought into AutoCAD (which is still not as big as this 1.1m point synth, but is nonetheless pretty amazing):
In the next post, we’ll apply the same technique to the Kinect integration for AutoCAD.
Recent Comments
Archives
More...