vasturiano / 3d-force-graph

3D force-directed graph component using ThreeJS/WebGL
https://vasturiano.github.io/3d-force-graph/example/large-graph/
MIT License
4.64k stars 818 forks source link

Select multiple nodes #406

Open danielpgp1012 opened 3 years ago

danielpgp1012 commented 3 years ago

As a user I find it difficult to select multiple nodes individually. I would like it to be possible to drag the mouse and select volumes with nodes or edges

manunamz commented 1 year ago

The following is a sample implementation of box selection for 3d-force-graph. The code snippet emulates the 2d implementation from this example but adds the necessary components to work in 3d -- which is mostly converting between 2d/3d coordinate spaces.

Thanks much to @Alexithemia for the prior example.

import { SelectionBox } from 'three/addons/interactive/SelectionBox.js';

let graph,         // your 'ForceGraph3D' instance
    selectionBox,  // three.js's 'SelectionBox' (see links below)
    selectedNodes, // your array of selected nodes
    cameraPos;     // camera position

// forceGraph element is the element provided to the Force Graph Library
document.getElementById('forceGraph').addEventListener('pointerdown', (e) => {
  if (e.shiftKey) {
    e.preventDefault();
    boxSelect = document.createElement('div');
    boxSelect.id = 'boxSelect';
    boxSelect.style.left = e.offsetX.toString() + 'px';
    boxSelect.style.top = e.offsetY.toString() + 'px';
    boxSelectStart = {
      x: e.offsetX,
      y: e.offsetY
    };
    // app element is the element just above the forceGraph element.
    document.getElementById('app').appendChild(boxSelect);
    // utility to convert between 2d select box coordinates and 3d graph coordinates
    selectionBox = new SelectionBox(graph.camera(), graph.scene());
    // window <-> graph-coords translation
    selectionBox.startPoint.set(
      (evt.clientX / window.innerWidth) * 2 - 1,
      - (evt.clientY / window.innerHeight) * 2 + 1,
      0.5,
    );
    // save camera position
    cameraPos = graph.cameraPosition();
  }
});

document.getElementById('forceGraph').addEventListener('pointermove', (e) => {
  if (e.shiftKey && boxSelect) {
    e.preventDefault();
    if (e.offsetX < boxSelectStart.x) {
      boxSelect.style.left = e.offsetX.toString() + 'px';
      boxSelect.style.width = (boxSelectStart.x - e.offsetX).toString() + 'px';
    } else {
      boxSelect.style.left = boxSelectStart.x.toString() + 'px';
      boxSelect.style.width = (e.offsetX - boxSelectStart.x).toString() + 'px';
    }
    if (e.offsetY < boxSelectStart.y) {
      boxSelect.style.top = e.offsetY.toString() + 'px';
      boxSelect.style.height = (boxSelectStart.y - e.offsetY).toString() + 'px';
    } else {
      boxSelect.style.top = boxSelectStart.y.toString() + 'px';
      boxSelect.style.height = (e.offsetY - boxSelectStart.y).toString() + 'px';
    }
    // window <-> graph-coords translation
    selectionBox.endPoint.set(
      (evt.clientX / window.innerWidth) * 2 - 1,
      - (evt.clientY / window.innerHeight) * 2 + 1,
      0.5,
    );
    graph.cameraPosition(cameraPos);
  } else if (boxSelect) {
    boxSelect.remove();
  }
});

document.getElementById('forceGraph').addEventListener('pointerup', (e) => {
  if (e.shiftKey && boxSelect) {
    e.preventDefault();
    boxSelect.remove();
    // window <-> graph-coords translation
    selectionBox.endPoint.set(
      (evt.clientX / window.innerWidth) * 2 - 1,
      - (evt.clientY / window.innerHeight) * 2 + 1,
      0.5,
    );
    // set selected nodes
                                        // (these accessors may be specific to how I put nodes together...)
    selectedNodes = new Set(selectionBox.select()
                                        .filter((item) => item.__graphObjType === 'node')
                                        .map((item) => item.__data));
    // reset camera position var
    cameraPos = undefined;
  } else if (boxSelect) {
    boxSelect.remove();
  }
});

// CSS for box to show up on top - 
#boxSelect {
    position: absolute;
    z-index: 300000;
    border-style: dotted;
    border-color: #3e74cc;
    background-color: rgba(255, 255, 255, 0.5);
    pointer-events: none;
}

These code snippets were adapted from the following three.js examples:

Edit: Fix camera rather than disable controls.