In the last post we talked about a recent optimization to Dasher 360, where we implemented a point cloud rather than individual SVG-based markers for our various sensors. As mentioned, last time, this was pretty straightforward to get working, but did add some complexity: rather than having seperate DOM-resident markers – which can easily have separate tooltips assigned – we now have a single object and need to be able to display tooltips when individual points in the cloud are hovered over.
Here’s the basic algorithm we used to determine when an individual sensor was being hovered over:
- Implement a ‘mousemove’ event listener on the viewer’s container. The rest of this algorithm is executed on mouse move.
- Check whether the cursor is on a UI element (see below for more details on this).
- Fire a ray along the camera direction (there’s code for this you can find in the Forge viewer .js file).
- Check for intersections with our point cloud.
- If there are hits, filter out any that aren’t visible.
- Sort the remaining hits by distance from the camera.
- Get the closest point to the camera. Change its colour to your hover colour.
- Get the screen point of its world coordinates and then use this as the location for the tooltip.
We use tooltipster to display tooltips, but there are various other JavaScript tooltip libraries out there.
For the 2nd step: when I first looked at this, I realised I could iterate through the UI panels and check their various client coordinates. The below is in TypeScript, but that really only means the function signature – the rest is pure JavaScript.
cursorOnPanel(x: number, y: number): Boolean {
let panels = this.viewer.dockingPanels;
// Panels may not be present when dealing with an instance of Viewer3D.js
// (as opposed to an instance of GuiViewer3D.js)
if (!panels) {
return false;
}
for (let i = 0; i < panels.length; ++i) {
let panel = panels[i];
let cont = panel.container;
if (cont.clientWidth > 0 && cont.clientHeight > 0) {
if (
x >= cont.offsetLeft && x <= cont.offsetLeft + cont.offsetWidth &&
y >= cont.offsetTop && y <= cont.offsetTop + cont.offsetHeight
) {
return true;
}
}
}
return false;
}
When I first ran this code, it didn’t include our custom dialogs. It was then that I realised we hadn’t been calling viewer.addPanel() for these dialogs. This is well worth doing and not only for the above code to include them: if they’ve been added to the viewer then as the viewer gets resized the dialogs will get moved to stay inside the viewer’s extents. Very handy.
In an internal discussion resident JavaScript guru, Philippe Leefsma, suggested an alternative approach. He mentioned document.elementFromPoint(), which allowed me to simplify the code somewhat. Now we only return false if the cursor isn’t over the canvas.
cursorOnPanel(x: number, y: number): Boolean {
let elem = document.elementFromPoint(x, y);
if (elem && elem.localName === 'canvas') {
return false;
}
return true;
}
I suppose this could even be reduced to this, albeit at the expense of readability.
cursorOnPanel(x: number, y: number): Boolean {
let elem = document.elementFromPoint(x, y);
return !(elem && elem.localName === 'canvas');
}
The code seems to work well. There are lots of situations where people will want to use THREE.js point clouds to display large numbers of markers, such as this, so at some point we’ll try to package this into a more reusable extension for the Forge viewer.