Close

[A] Introducing 3DxPoint: The 3D Trackpoint

A project log for AirBerries and SpaceExplorer

I should start using my £80 split keyboard and £40 spacemouse more than my £32 keyboard and £16 mouse.

kelvinakelvinA 11/03/2023 at 23:582 Comments

I'll be honest. I knew this was going to take a while to do, and so I avoided doing it until my £16 mouse finally actually stopped working. Like, it technically works, but the cable had to be within a specific angle or else it'll disconnect. Well that angle has been getting smaller and smaller over the passing months and, a week ago, this angle was too small to do anything meaningful. I can't model #Coaxial Hotend [gd0144] with the Windows 10 touchscreen trackpad, so I really had to do something and do it now.

Since then, I've spent about 36 combined hours to develop the 3DxPoint.dll that implements all the features I mentioned in the previous log, and other than some refinements that I'll mention at the end, I've got a working minimum viable product.

Getting a new Visual Studio project set up

All the way back in April, i got an email from jwick (mod on the 3Dconnexion fourms) which included the source code for a modified 3DxSample.dll that was mainly to get mouse button presses on an Axis instead of a Button. He mentioned that useful information about the true capabilities of the 3DxWare driver is in a beta forum, but it seems pedestrian users like myself don't have access to that.

So, the first order of business was to rename the project to a more fitting name, and I thought of 3DxPoint, short for "3DxWare X Trackpoint", since the control should be very similar to a massively enlarged trackpoint.

Now, it seems that just renaming a few things in Visual Studio to "3DxPoint" wasn't going to work, so I created a brand new DLL project and deleted the automatically created files to bring in the new ones. And then I got this complaint:

Well, as I soon found out, those files I deleted were more or less the same thing as the 3DxSample files but with a new name (I guess thing's have changed since 2015, which was the datestamp of those files), so I backtracked.

Obviously, stuff was still broken, mainly to do with pch.cpp. This answer solved it:

I got things to compile, but I had warnings:

I was looking at the code and it didn't look like that was possible for fp == 0, but I didn't want to assume things that could be the cause of bugs. Well then I found this, showing that it is indeed possible:

I just tacked "&& fp != NULL" inside all the if statements.

Getting 3DxWare 10.6.4 to run a DLL

A hurdle I hit very quickly was that, like 3DxSample.dll, 3DxPoint.dll didn't seem to be doing anything. There's an init function that writes to a fill every time it's called, and nothing was being written. 

After doing some trial and error, I decided to reread the instructions slowly and carefully:

1) Save it into %ProgramFiles%/3Dconnexion/3DxWare/3DxWinCore/Win64/DLLs/.  

I kind-of just assumed that the DLL went in the same directory as the XML that used it. I only have a "3DxWinCore64" folder, so I used that instead.

2) Add these AxisActions to an app-specific cfg, or Global.xml

Now, I thought that Desktop.xml counted, but it actually doesn't. This is what happened.

I added a ButtonAction in Global.xml.

I restarted the driver and looked at 3DxPoint.txt:

I audiably ghasped:

The AxisAction that I defined in Desktop.xml but not Global.xml magically started working:

Initial testing and understanding

One of the frist things I found out was how the deadband affected the readings. This is a deadband of 40 compared to a deadband of 0:

This also tells me that the absolute max value produced is most likely 350. 

I also found out that the ButtonAction set in Global.xml doesn't actually need to be used for anything in the same xml for the 3DxPoint.dll to still work for Desktop.xml.

Code cleanup and consolidation

All these block-if's to deal with logging were making things a bit cluttered and it was all boilerplate anyway, so I turned some parts into macros:

I did some research and changed the first if to a block if (see below), though I feel like the above solution was more elegant now that I'm looking back on things.

Then I found out this nice way to do enums, which I used in the goal to consolidate functions for left/middle/right button presses (from 3DxSample) into a single, unified function:

I also found out about the "#if 0" statement to quicly "comment out" parts of code. I could also use "#if 1" to be able to minimise entire sections of code. 

This SendMouseButton eventually grew to include more or less everything done by a typical mouse, as seen by the MouseEvent enum.

I had also read the docs in more detail, and it turns out jwick had already implemented the smooth scroll feature in the original 3DxSample.cpp. Ultra-smooth scroll looks and feels so much better, and it even works in Fusion 360!

Getting the ButtonRing magnitude and angle

The ButtonRing is what I'm calling the virtual ring of buttons activated by tilting the spacemouse in that direction. I also understand that, from looking in 3Dconnexion forums about using the spacemouse as a 2D mouse, some prefer to tilt the spacemouse to move the cursor, thus the ButtonRing would be activated by translations instead of tilting.

What I started off with was a struct that had all the stuff I'd think I'd need (for 3DxPoint in general) and then some logging:

It feels great to finally use something I learned in maths class (well, "Further Maths") in the real world. Complex numbers allow for an easy avenue to convert an X and Y value into a magnitude and direction. 

As you may also see, I have "bool Button[8][2]" instead of "Button[8]". As I'm working on a force-sensitive project myself that has its roots in double-actuation switches (see #Tetrinsic [gd0041]) I wanted to try to get dual actuation virtual buttons on the ButtonRing.

Everything seemed to work ok, but I wanted a nicer looking log message that actually put the magnitude / angle under the spotlight. I had to find out how to save unicode characters to a file, but I got this solution:

I also added padding onto the files so that the log data was all nice and aligned.

Licensing

So now I had spent a couple days on this and the percentage of 3DxSample / jwick code I was using had become quite low, so I finally decided to look into the wide world of open source licensing.

I first found out that this is the header, which is a format I've seen from the QMK Taipo implementation:

Then the differences between 2-clause BSD (and how it's very similar to MIT) and 3-clause BSD.

And then I found this reddit comment:

Since I played Burnout 3 when I was a kid, "Shake That Bush Again" (see video below) was already playing in my mind upon reading "boost", since that's one of the main elements of the game. It was all too easy to believe that they were singing "Shake That Boost Again", and I'd be tapping the boost button in time for the beat when it played.

There's actually a website page for it, and it mentions a nice bit of history and even a convenient FAQ section.

So yeah, it's MIT compatible and I like the name of it so I went with it.

Speed Multiplier

The next thing to work on was being able to lift and lower the spacemouse to speed or slow down the rate of the cursor and scrollwheel. Thus I started plotting graphs on Desmos / Excel to see what would give me the feeling I desired in the range 0 - 2 (of which, -350 to 350 was mapped to)

I tried the above one and it didn't feel that fast when lifting the mouse, and this is probably why:

I just used a classic exponent, since being able to slow down for more precision is more important than speeding up to quickly jump from one side of the screen to the other.

I also noticed that the SpaceExplorer liked to lock towards cardinal directions:

I'm not actually sure I can see this as a bug or a feature. There's many times when I am just trying to move the cursor up/down/left/right and other pointing devices make such a task very hard. Unfortunately, this extends into the 3rd dimension, making the speed multiplier axis less responsive than I'd like at times.

I also found out this nice shortcut when doing all these tweaks and changes:

I also implemented a mirroring feature so that a button could be pressed to swap from right to left handed use.

Selecting a button from the ButtonRing

Now, I don't fully understand the code because Me In The Zone wrote it, and trying to juggle what was happening where was a strenuous mental exercise. If I could code closer to thinking speed (cough #Tetent [gd0090]  cough) then it wouldn't be so bad; instead I have to put some considerable energy trying to keep implementation ideas in my short term memory long enough to either implement it in the code, or write it somewhere. For example, here's my coding equivalent to "I sketched this idea in Paint3D":

I've read online that there are coders, programmers and software developers that claim that their typing speed is never their bottleneck, but their thinking speed. I'm not sure if it's a benefit or a drawback, but I'm 100% on the other side of the coin. Perhaps I'm just a "Documentation Driven Development" kind of guy; I think "How would I explain the implementation of this feature on Hackaday" instead of "How would I write a test for this feature" (Test Driven Development) or "How would I implement this feature". Maybe I should look into Behavour Driven Development?

Returnint to topic. All I want to mention is that, even though there's like 4 nested if statements, actually adding button commands has been made straightforward by reading the LogMessage commands:

I feel like it's spaghetti code because there's all sorts of booleans doing things around the function. It makes sense, but I do wonder if there's a more elegant way.

Anyway, I think it'll be much better to explain the behaviour.

The first thing to know is that, for a physical button, the minimum force to press the button from up --> down is higher than from down --> up. I've implemented this like a Schmitt Trigger:

Next, there's both an inner and outer button, so it's not like I can just turn on a button the moment I pass the threshold. When I start to tilt the spacemouse on the SpaceExplorer, all the values are consistently larger than the previous one up until the point where I actually want to stop tilting. Observe the following diagram I made in Paint3D, where the X axis is time and the Y axis is the magnitude of tilt:

Notice all the perfectly straightly drawn lines. That's thanks to 3DxPoint.

The dark red line is one such tilt movement and first stops increasing above the outer threshold, so the outer virtual button should be activated. So that users don't have to strain their hands, they can now reduce the amount of force needed to keep it enabled. It only turns off when the magnitude falls below the exit threshold.

The orange line stopped increasing above the inner threshold, so the inner virtual button should be activated.

Now lets say a 3rd movement comes along, shown in bright red:

As you can see, even though it starts off noisy, all the noise is under the inner threshold so it doesn't count to anything. This could also be from a previous movement, where the user never fully stopped tilting the spacemouse (say, for a double click) . Since the first time the bright red line stopped increasing was in the inner threshold, the inner button will be enabled until it falls under the exit threshold. It won't spontaneously decide to select the outer button just because the user happened to cross the outer threshold.

You should also remember that I don't get a continuous curve, but a list of values:

I've set it so that the virtual button isn't pressed down when the very next value completely skips over both thresholds, as the alternative would be a very quick click.

That was a 1D representation. To make it work in 2D (so accidentally going into a different octant didn't do anything), the complex number is saved. I called it triggerEvent, which is "static", meaning that it is saved between function calls:

The reason onExit exists is because I need to be able to execute a virtual button_up event. 

Now looking at it, I feel like this code would work too, and less susceptible to any resolution issues:
I removed that "//do nothing" part and made it so that currentMagnitude has to be less than prevMagnitude. I think it's now a bit easier to understand what's happening.

I was considering coding up some way to have an easily adjustable way to have an arbritrary amount of virtual buttons, but I decided that such a feature was just feature-creep and stuck with hard coding 8 x 2 buttons.

One of the things I needed to do was check if an angle was inbetween 2 other angles, so that the octant could be determined. I searched it up, expecting there to actually be a standard function for this in C++, but it turns out that doesn't exist and Bing stepped up to generate something:

So I pasted that character soup in the codebase and, since I'd been coding straight for hours, I decided to take a break (reading about open source community's general funding issue) for like 10 minutes, and I came back with the idea that there was a more readable way to write this function:

I guess, if I squint now, I can understand that it doesn't discriminate between minimum and maximum values

Trying things out

My idea, with 4 of these 16 virtual buttons at my disposal, was to do the following:

This is for my right hand, which I usually use with a mouse. The first time I actually set it up and tried it put a massive smile on my face because it actually worked. The analogy I thought of was "3D Touch" from Apple:

😂I like how the woman in the video is says "Think of it like right clicking a mouse..." about 3D Touch and now I'm saying, 8 years later, "Think of it like using 3D Touch on an iPhone..." about right clicking.

I was thinking that I'd have to do some kind of nifty timer based logic or something, but just waiting for the moment when I stop increasing force is quite reliable. What isn't reliable is knowing when I've actually crossed a threshold, and it validates my reasoning behind haptic events for #Tetrinsic [gd0041].

Limitations and future improvements

The first minor limitation is that 3DxWare needs to be running as Admin, or else 3DxPoint isn't going to work if you open up admin-level programs like Task Manager. The next one is that moving the spacemouse doesn't remove the screensaver. Last limitation that I've found so far is that 3DxPoint wont work if the program in focus is not responding.

The immediate future improvement I want to look into is perhaps having a dynamic threshold based on how far down the spacemouse is pushed. It seems that I get quite a few accidental button presses when I'm also trying to move my cursor with the speed multiplier not equal to 1 (a.k.a I've lifted or lowered the spacemouse). Perhaps this is just a skill issue, and will help me have better control when using Fusion 360 in the traditional 6 axis mode. 

(Tip for Fusion360.xml - change ResponseCurve to 1.0. Now the SpaceExplorer feels a lot more connected to the movements I want to perform.)

Another improvement is to add an enum for the PC I want to compile 3DxPoint.dll for, as the keyboard shortcuts I need vary between #Teti [gd0022] and Q3ti (my 8", Intel N100 laptop). This is also to allow me to quickly put out a general-purpose 3DxPoint.dll that only has the mouse-related virtual buttons I mentioned earlier.

Obviously, changing my sync client from OneDrive to a Github repo is also on the list. It was just that I didn't want to have yet another thing to keep track of, compared to OneDrive where I could save 3DxPoint.cpp on Teti and pick it up again on Q3ti without having to remember to commit and sync. I also need to clean up code I thought I was going to need (like the Octant enum) but didn't.

Looking at the logs, I don't usually go above 180 when activating the outer button, so it might even be possible to have three sets of button rings. Who would even need a SpaceMouse Pro when you've got that many commands at most a "3DxPoint Toggle Button -> Tilt -> 3DxPoint Toggle Button" away?

I might also want to consider removing buttonAlreadyPressed and instead implement an onEntry boolean instead. It might fix this rare but known bug that I don't get a down event (for example, after line 10134):

As mentioned earlier, it's much more of an issue if I get button-down events without a button-up to restore balance. Actually, I can see if I can implement it now...

Implementing onEntry


This code above has already become more understandable to read. 

And it still seems to totally works! 

I actually got the onEntry idea when I was looking at the Schmitt trigger diagram I mentioned earlier. These logs may take hours to write (I think it's been over 5hrs since I started writing this one) but now I understand the code that Me In The Zone wrote at 2am this morning.

Conclusions

I've been testing out the SpaceExplorer for most of today, and even though my precision with it isn't quite where I'd like, I feel like an actual professional when using it; well, like a person in stock-footage acting like a professional:

It might be because the cursor moves smoothly, like in motion graphics / infomercials.

Honestly, considering 3Dconnexion and everyone has been calling this line of devices "SpaceMice" for over 10 years, I'm suprised this functionality isn't baked into the driver itself. Reason 1 out of 2 why I never got much use out of my SpaceExplorer was because I'd have to be hotswapping a hand from the keyboard/mouse to it, and 2 of 2 was that I didn't get much experience using the device outside of Fusion360.

A SpaceMouse Wireless + a vibration motor + driver support (+ round display) would make for an outstanding professional input device if 24 virtual buttons are reliably usasble. Perhaps #OS3M Mouse could take it all the way one day.

Discussions

colton.baldridge wrote 11/20/2023 at 07:51 point

Thanks for the shoutout to OS3M Mouse! I just posted an update with the latest progress I've made, I'd love to get your feedback on functionality you'd like to see!

  Are you sure? yes | no

Mary Afolabi wrote 11/04/2023 at 19:20 point

You are so talented. Well done for programming this. I like your Desmos application.  The name 3DxPoint sounds so creative.

  Are you sure? yes | no