I would like to be able to drag an object across a plane (think of a piece on a chess board) in a canvas using React-three-fiber and an orthographic camera.

Here is an example (not mine) of that working with a fixed camera position: https://codepen.io/kaolay/pen/bqKjVz

But I would like to be able to move the camera too - so I have added Orbitcontrols which are disabled when the object is being dragged.

I have a code sandbox here with my attempt based many other's examples: https://codesandbox.io/s/inspiring-franklin-2r3ri?file=/src/Obj.jsx

The main code is in two files, App.jsx with the canvas, camera, and orbitcontrols. And Obj.jsx with the mesh that gets dragged as well as the dragging logic inside of a use-gesture useDrag function.

App.jsx

import React, { useState } from "react";
import { Canvas } from "@react-three/fiber";
import Obj from "./Obj.jsx";
import { OrthographicCamera, OrbitControls } from "@react-three/drei";
import * as THREE from "three";

export default function App() {
  const [isDragging, setIsDragging] = useState(false);

  return (
    <Canvas style={{ background: "white" }} shadows dpr={[1, 2]}>
      <ambientLight intensity={0.5} />
      <directionalLight
        intensity={0.5}
        castShadow
        shadow-mapSize-height={512}
        shadow-mapSize-width={512}
      />

      <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
        <planeBufferGeometry attach="geometry" args={[10, 10]} receiveShadow />
        <meshPhongMaterial
          attach="material"
          color="#ccc"
          side={THREE.DoubleSide}
          receiveShadow
        />
      </mesh>

      <Obj setIsDragging={setIsDragging} />

      <OrthographicCamera makeDefault zoom={50} position={[0, 40, 200]} />

      <OrbitControls minZoom={10} maxZoom={50} enabled={!isDragging} />
    </Canvas>
  );
}

Obj.jsx (with the offending code in the Use Drag function)

import React, { useState } from "react";
import { useDrag } from "@use-gesture/react";
import { animated, useSpring } from "@react-spring/three";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";

function Obj({ setIsDragging }) {
  const { camera } = useThree();
  const [pos, setPos] = useState([0, 1, 0]);
  const { size, viewport } = useThree();
  const aspect = size.width / viewport.width;
  const [spring, api] = useSpring(() => ({
    // position: [0, 0, 0],
    position: pos,
    scale: 1,
    rotation: [0, 0, 0],
    config: { friction: 10 }
  }));
  const bind = useDrag(
    ({ active, delta, movement: [x, y], velocity, timeStamp, memo = 0 }) => {
      if (active) {

 //// THIS IS THE CODE THAT I KNOW IS NOT WORKING /////

        let vDir = new THREE.Vector3();
        let vPos = new THREE.Vector3(
          (x / window.innerWidth) * 2 - 1,
          -(y / window.innerHeight) * 2 + 1,
          0.5
        ).unproject(camera);

        vDir.copy(vPos).sub(camera.position).normalize();
        let flDistance = -camera.position.z / vDir.z;
        vPos = vPos.copy(camera.position).add(vDir.multiplyScalar(flDistance));
        const arbitraryFactor = 1; // I suspect this has to reflect the distance from camera in all dims...
        setPos([vPos.x * arbitraryFactor, 1.5, -vPos.y * arbitraryFactor]);

 //// END /////
      }

      setIsDragging(active);

      api.start({
        // position: active ? [x / aspect, -y / aspect, 0] : [0, 0, 0],
        position: pos,
        scale: active ? 1.2 : 1,
        rotation: [y / aspect, x / aspect, 0]
      });
      return timeStamp;
    }
  );

  return (
    <animated.mesh {...spring} {...bind()} castShadow>
      <dodecahedronBufferGeometry
        castShadow
        attach="geometry"
        args={[1.4, 0]}
      />
      <meshNormalMaterial attach="material" />
    </animated.mesh>
  );
}

export default Obj;

A few references that have been helpful but have not got me there yet! Mouse / Canvas X, Y to Three.js World X, Y, Z

https://codesandbox.io/s/react-three-fiber-gestures-forked-lpfv3?file=/src/App.js:1160-1247

https://codesandbox.io/embed/react-three-fiber-gestures-08d22?codemirror=1

https://codesandbox.io/s/r3f-lines-capture-1gkvp

https://github.com/pmndrs/react-three-fiber/discussions/641

And finally relinking to my code sandbox example again: https://codesandbox.io/s/inspiring-franklin-2r3ri?file=/src/Obj.jsx:0-1848

2

There are 2 best solutions below

1
Korimako On BEST ANSWER

I took a different approach after I realized that React-Three-Fiber passes event info into useDrag, which contains the coordinate and Ray information I needed.

https://codesandbox.io/s/musing-night-wso9v?file=/src/App.jsx

App.jsx

import React, { useState } from "react";
import { Canvas } from "@react-three/fiber";
import Obj from "./Obj.jsx";
import { OrthographicCamera, OrbitControls } from "@react-three/drei";
import * as THREE from "three";

export default function App() {
  const [isDragging, setIsDragging] = useState(false);
  const floorPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);

  return (
    <Canvas style={{ background: "white" }} shadows dpr={[1, 2]}>
      <ambientLight intensity={0.5} />
      <directionalLight
        intensity={0.5}
        castShadow
        shadow-mapSize-height={512}
        shadow-mapSize-width={512}
      />

      <mesh
        rotation={[-Math.PI / 2, 0, 0]}
        position={[0, -0.1, 0]}
        receiveShadow
      >
        <planeBufferGeometry attach="geometry" args={[10, 10]} receiveShadow />
        <meshPhongMaterial
          attach="material"
          color="#ccc"
          side={THREE.DoubleSide}
          receiveShadow
        />
      </mesh>

      <planeHelper args={[floorPlane, 5, "red"]} />

      <gridHelper args={[100, 100]} />

      <Obj setIsDragging={setIsDragging} floorPlane={floorPlane} />

      <OrthographicCamera makeDefault zoom={50} position={[0, 40, 200]} />

      <OrbitControls minZoom={10} maxZoom={50} enabled={!isDragging} />
    </Canvas>
  );
}

Obj.jsx

import React, { useState, useRef } from "react";
import { useDrag } from "@use-gesture/react";
import { animated, useSpring } from "@react-spring/three";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";

function Obj({ setIsDragging, floorPlane }) {
  const [pos, setPos] = useState([0, 1, 0]);
  const { size, viewport } = useThree();
  const aspect = size.width / viewport.width;

  let planeIntersectPoint = new THREE.Vector3();

  const dragObjectRef = useRef();

  const [spring, api] = useSpring(() => ({
    // position: [0, 0, 0],
    position: pos,
    scale: 1,
    rotation: [0, 0, 0],
    config: { friction: 10 }
  }));

  const bind = useDrag(
    ({ active, movement: [x, y], timeStamp, event }) => {
      if (active) {
        event.ray.intersectPlane(floorPlane, planeIntersectPoint);
        setPos([planeIntersectPoint.x, 1.5, planeIntersectPoint.z]);
      }

      setIsDragging(active);

      api.start({
        // position: active ? [x / aspect, -y / aspect, 0] : [0, 0, 0],
        position: pos,
        scale: active ? 1.2 : 1,
        rotation: [y / aspect, x / aspect, 0]
      });
      return timeStamp;
    },
    { delay: true }
  );

  return (
    <animated.mesh {...spring} {...bind()} castShadow>
      <dodecahedronBufferGeometry
        ref={dragObjectRef}
        attach="geometry"
        args={[1.4, 0]}
      />
      <meshNormalMaterial attach="material" />
    </animated.mesh>
  );
}

export default Obj;

That should not have taken me as long as it did, I hope that this is helpful for someone else!

0
Muhammad Bilal On

To achieve dragging an object in the X and Z coordinates constrained in Y in React-three/fiber with an orthographic camera while allowing the camera to be moved via OrbitControls, you can use the following modified code. I've made some adjustments to the logic inside the useDrag function in Obj.jsx:

import React, { useState } from "react";
import { useDrag } from "@use-gesture/react";
import { animated, useSpring } from "@react-spring/three";
import { useThree } from "@react-three/fiber";
import * as THREE from "three";

function Obj({ setIsDragging }) {
    const { camera } = useThree();
    const [pos, setPos] = useState([0, 1, 0]);
    const { size, viewport } = useThree();
    const aspect = size.width / viewport.width;
    const [spring, api] = useSpring(() => ({
        position: pos,
        scale: 1,
        rotation: [0, 0, 0],
        config: { friction: 10 }
    }));

    const bind = useDrag(
        ({ active, delta, movement: [x, y], velocity, timeStamp }) => {
            if (active) {
                const raycaster = new THREE.Raycaster();
                const mouse = new THREE.Vector2();
                mouse.x = (x / size.width) * 2 - 1;
                mouse.y = -(y / size.height) * 2 + 1;

                raycaster.setFromCamera(mouse, camera);

                // Define the plane in Y-axis at the current object position
                const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), pos[1]);

                // Intersect the ray with the plane
                const intersection = new THREE.Vector3();
                raycaster.ray.intersectPlane(plane, intersection);

                // Update the object position
                setPos([intersection.x, pos[1], intersection.z]);
            }

            setIsDragging(active);

            api.start({
                position: pos,
                scale: active ? 1.2 : 1,
                rotation: [y / aspect, x / aspect, 0]
            });

            return timeStamp;
        }
    );

    return (
        <animated.mesh {...spring} {...bind()} castShadow>
            <dodecahedronBufferGeometry
                castShadow
                attach="geometry"
                args={[1.4, 0]}
            />
            <meshNormalMaterial attach="material" />
        </animated.mesh>
    );
}

export default Obj;

This modification uses the Raycaster to cast a ray from the mouse position and intersects it with a plane in the Y-axis at the current object position. This ensures that the object will be dragged along the X and Z coordinates while staying in the Y coordinate.