How to add mouse events to each mesh part loaded from Rhino3dm file in React-Three-Fiber?

I want to realize a function that changes color when the mouse is over each mesh part loaded from an Rhino3dm file with react-three-fiber .But in the code below mouse events are not working properly for some reason.

For GLTF, using Object.values(gltf.nodes).map worked fine.
movie

For Rhino3dm, I use object.children.map and I think this is bad.

Do you know the cause and solution?

This is the sandbox URL. test_Rhino3dmLoader - CodeSandbox

import "./styles.css";
import * as THREE from "three";
import { useEffect, useMemo, useRef, useState } from "react";
import { Canvas } from "@react-three/fiber";
import { useLoader } from "@react-three/fiber";
import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader";
import { Environment, OrbitControls} from "@react-three/drei";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Suspense } from "react";

const Part = (child) => {
  const objRef = useRef();
  const [hovered, setHover] = useState(false);
  const material = new THREE.MeshStandardMaterial({
    color: hovered ? "hotpink" : "orange"
  });
  return (
    <mesh
      key={child.id}
      ref={objRef}
      castShadow
      receiveShadow
      onPointerOver={(e) => {
        e.stopPropagation();
        setHover(true);
      }}
      onPointerOut={(e) => {
        e.stopPropagation();
        setHover(false);
      }}
    >
      <primitive object={child} material={material} />
    </mesh>
  );
};

const Model = () => {
  // For some reason mouse events don't work well in Rhino3dm.
  const object = useLoader(Rhino3dmLoader, "./rhino_logo.3dm", (loader) => {
    loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@0.15.0-beta/");
  });
  
  const model = object.children.map((child) => Part(child));

  // For gltf the below code worked fine.
  /* 
  const gltf = useLoader(GLTFLoader, "./rhino_logo.glb");
  const model = Object.values(gltf.nodes).map((child) => {
    if(child.isMesh){
     return Part(child)
    }
  });
  */
  return model;
};

export default function App() {
  return (
    <div className="App">
      <Canvas>
        <Suspense fallback={null}>
          <Model />
          <OrbitControls />
          <Environment preset="sunset" background />
        </Suspense>
      </Canvas>
    </div>
  );
}

it’s a little bit confused. object.children could be deeply nested.

you can do something like this

const hovered = new THREE.MeshStandardMaterial({ color: "hotpink" })
const unhovered = new THREE.MeshStandardMaterial({ color: "orange" })

const Model = (props) => {
  const object = useLoader(Rhino3dmLoader, "./rhino_logo.3dm", (loader) => {
    loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@0.15.0-beta/")
  })
  useLayoutEffect(() => {
    object.traverse(o => o.material = unhovered)
  }, [])
  return (
    <primitive
      onPointerOver={(e) => {
        e.stopPropagation()
        e.object.material = hovered
      }}
      onPointerOut={(e) => {
        e.stopPropagation()
        e.object.material = unhovered
      }}
      object={object}
      {...props}
    />
  )
}

<Canvas>
  <Model scale={0.15} rotation={[-Math.PI / 2, 0, 0]} />

but keep in mind that if that model were declarative everything would be so easy. this is the difference between imperative and declarative in a nutshell, in vanilla threejs you get served a blob, a black box, and it’s hard to control that with traverse and it gets way too complex. all this evaporates with the declarative approach: save it as gltf and type npx gltfjsx yourmodel.gltf --transform

now the full graph would be under your control.

1 Like

Thank you so much for your help! Your solution was exactly what I was looking for and it works perfectly. I had no idea there was such a smart way to approach this problem.Thanks again!