I mentioned this device, late last year, and have been meaning to spend time integrating it into AutoCAD ever since. The 3dRudder is an interesting perpheral: while currently targeted at helping seated VR users navigate intuitively in 3D – effectively keeping their hands free – it was originally intended for CAD users. So it’s nice that it’s going back to its roots, as it were. Here’s an excerpt of a video of Christian Slater demoing the 3dRudder integration with Rhino (I’m kidding about it being Christian Slater – he just looks a bit like him :-).
Over the last few days, I’ve finally managed to find time to create a skeleton .NET application that integrates the 3dRudder into AutoCAD. Unfortunately I haven’t managed to spend the time to make use of the navigation APIs to pan/zoom/orbit around AutoCAD’s 2D or 3D environments, but that shouldn’t be a huge task for anyone wanting to take this code further.
Here’s the C# code that makes use of the 3dRudder .NET SDK to bring data into AutoCAD:
using System;
using System.Runtime.InteropServices;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;
using ns3DRudder;
namespace MyApplication
{
public partial class Commands
{
[CommandMethod("3DRUDDER", CommandFlags.Modal)]
public void Rudder()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
// Create and run our jig
var rj = new RudderJig();
var pr = ed.Drag(rj);
}
}
public class RudderJig : DrawJig
{
int _numRuddersConnected;
CSdk _rudderSdk;
Axis axis;
ModeAxis mode;
CurveArray curves;
int _selectedIndex = -1;
ns3DRudder.Status _prevStatus;
private class Item
{
public string Name;
public int Value;
public Item(string name, int value)
{
Name = name; Value = value;
}
public override string ToString()
{
// Generates the text shown in the combo box
return Name;
}
}
// To stop the running jig by sending a cancel request
[DllImport("accore.dll", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "?acedPostCommand@@YAHPEB_W@Z"
)]
extern static private int acedPostCommand(string strExpr);
public RudderJig()
{
_rudderSdk = new CSdk();
_rudderSdk.Init();
axis = new Axis();
mode = ModeAxis.NormalizedValue;
curves = new CurveArray();
}
// Flag and property for when we want to exit
protected bool _finished;
public bool Finished
{
get { return _finished; }
set { _finished = value; }
}
private void UpdateStatus()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
if (_numRuddersConnected != _rudderSdk.GetNumberOfConnectedDevice())
{
_numRuddersConnected = _rudderSdk.GetNumberOfConnectedDevice();
for (uint i = 0; i < i3DR._3DRUDDER_SDK_MAX_DEVICE; i++)
{
if (_rudderSdk.IsDeviceConnected(i))
{
ed.WriteMessage("3DRudder #{0}\n", i);
_selectedIndex = (int)i;
}
}
}
if (_selectedIndex != -1)
{
var selected = (uint)_selectedIndex;
if (_rudderSdk.IsDeviceConnected(selected))
{
// ushort nVersion = _rudderSdk.GetVersion(selected);
string Status = "";
if (_rudderSdk.GetAxis(selected, mode, axis, curves) == ErrorCode.Success)
{
var stat = _rudderSdk.GetStatus(selected);
switch (stat)
{
case ns3DRudder.Status.NoFootStayStill:
Status = "Don't put your feet!!! Stay still for 5s";
break;
case ns3DRudder.Status.Initialisation:
Status = "Initialisation";
break;
case ns3DRudder.Status.PutYourFeet:
Status = "Please put your feet";
break;
case ns3DRudder.Status.PutSecondFoot:
Status = "Put your second foot";
break;
case ns3DRudder.Status.StayStill:
Status = "Stay still";
break;
case ns3DRudder.Status.InUse:
Status = "3DRudder in use";
break;
case ns3DRudder.Status.ExtendedMode:
Status = "Extended functions activated";
break;
}
if (stat != _prevStatus)
{
doc.Editor.WriteMessage("{0}\n", Status);
_prevStatus = stat;
}
if (stat == ns3DRudder.Status.InUse || stat == ns3DRudder.Status.ExtendedMode)
{
var XAxis = axis.GetXAxis();
var YAxis = axis.GetYAxis();
var ZAxis = axis.GetZAxis();
var ZRotation = axis.GetZRotation();
ed.WriteMessage(
"X: {0:0.00}, Y: {1:0.00}, Z: {2:0.00}, Rotation: {3:0.00} ({4})\n",
XAxis, YAxis, ZAxis, ZRotation, Status
);
}
}
}
}
}
protected virtual SamplerStatus SamplerData()
{
UpdateStatus();
ForceMessage();
return SamplerStatus.NoChange; // Cancel;
}
protected virtual bool WorldDrawData(WorldDraw draw)
{
return false;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
var opts = new JigPromptPointOptions("\nClick to finish: ");
opts.UserInputControls = UserInputControls.AnyBlankTerminatesInput;
opts.Cursor = CursorType.Invisible;
var ppr = prompts.AcquirePoint(opts);
if (ppr.Status == PromptStatus.OK || ppr.Status == PromptStatus.Cancel)
{
if (_finished)
{
CancelJig();
return SamplerStatus.Cancel;
}
return SamplerData();
}
return SamplerStatus.Cancel;
}
protected override bool WorldDraw(WorldDraw draw)
{
return WorldDrawData(draw);
}
// Cancel the running jig
internal static void CancelJig()
{
acedPostCommand("CANCELCMD");
}
public void ForceMessage()
{
System.Windows.Forms.Cursor.Position =
System.Windows.Forms.Cursor.Position;
System.Windows.Forms.Application.DoEvents();
}
}
}
To get this code to work, you just need to create a standard AutoCAD .NET project with the usual assembly references (which can be added via NuGet) as well as an additional reference to 3DRudderSDK.net (which can found in the .NET sample on the 3dRudder website).
The above code was very much inspired by the work I did way back when to integrate point cloud input from Kinect into AutoCAD: in many ways it’s a similiar scenario, in that there’s an external device that sends data to AutoCAD and influences the active session in some way. It’s quite possible that there are other approaches that might not need a separate command to be called or the cursor to be on the drawing canvas, such as – for instance – using the DocumentCollection.ExecuteInCommandContextAsync() method. But I’ve stuck to what I know best for this initial integration.
When we run the 3DRUDDER command – with a 3dRudder connected and initialized – we should see a stream of data coming from the device that indicates the way the person is using it: X, Y and Z axes as well as a Z rotation (I’m not fully sure what the Z axis measurement means, admittedly: for me X is roll, Y is pitch and the Z rotation is yaw… the Z axis data seems redundant, although it’s altogether possible that I’m just not understanding it properly).
Here’s another video of the Rhino integration, this time showing in more detail how 3dRudder gestures can be interpreted for 3D model navigation:
I wish I had more time to spend taking this code further – I won’t have space during our round-the-world trip (which starts in just over a week!) to slip a 3dRudder into my backpack – but hopefully someone reading this blog will take up the baton.