ThatOpen / web-ifc-three

The official IFC Loader for Three.js.
https://ifcjs.github.io/info/
MIT License
512 stars 132 forks source link

react-three-fiber: manually have to set `manager.state.models[modelID]` before `createSubset` + `removeSubset` not working #83

Closed Amar-Gill closed 2 years ago

Amar-Gill commented 2 years ago

Description

In my react-three-fiber project, I have to manually set ifcManager.state.models[modelId] before making a call to ifcManager.createSubset(). I thought the createSubset method would handle this on it's own:

const [highlightedModel, setHighlightedModel] = useState({ id: -1 });

function highlight(intersection: Intersection<IFCModel<Event>>, material: Material) {
    const { faceIndex } = intersection;
    const { modelID, geometry } = intersection.object;
    const id = manager.getExpressId(geometry, faceIndex);

    setHighlightedModel({ id: modelID });

    manager.state.models[modelID] = intersection.object;

    manager.createSubset({
      modelID,
      ids: [id],
      material,
      scene,
      removePrevious: true,
    });
  }

Without the call to manager.state.models[modelID] = intersection.object; I get the following error:

Uncaught TypeError: this.state.models[modelID] is undefined
    getGeometry ItemsMap.ts:117
    generateGeometryIndexMap ItemsMap.ts:94
    createSubset SubsetCreator.ts:79
    createSubset BasePropertyManager.ts:9
    createSubset web-ifc-three_IFCLoader.js:54214
    highlight IFCContainer.tsx:44
    handleDblClick IFCContainer.tsx:32
    handlePointer react-three-fiber.esm.js:414
    handleIntersects react-three-fiber.esm.js:289
    handlePointer react-three-fiber.esm.js:376
    connect react-three-fiber.esm.js:1465
    connect react-three-fiber.esm.js:1463
    Provider react-three-fiber.esm.js:1807
    invokePassiveEffectCreate react-reconciler.development.js:16054
    callCallback2 react-reconciler.development.js:12184
    invokeGuardedCallbackDev react-reconciler.development.js:12233
    invokeGuardedCallback react-reconciler.development.js:12292
    flushPassiveEffectsImpl react-reconciler.development.js:16141
    unstable_runWithPriority scheduler.development.js:468
    runWithPriority react-reconciler.development.js:2495
    flushPassiveEffects react-reconciler.development.js:16014
    commitBeforeMutationEffects react-reconciler.development.js:15891
    workLoop scheduler.development.js:417
    flushWork scheduler.development.js:390
    performWorkUntilDeadline scheduler.development.js:157
    js scheduler.development.js:180
    js scheduler.development.js:644
    __require2 chunk-UC7LELEO.js:48
    js index.js:6
    __require2 chunk-UC7LELEO.js:48
    React 2
    __require2 chunk-UC7LELEO.js:48
    js React
    __require2 chunk-UC7LELEO.js:48
    <anonymous> react-dom:1
ItemsMap.ts:117:9

Additionally, when I call removeSubset(modelID, highlightMaterial) visually it appears the highlight remains on the model:

  function handleDblClick(event: Intersection<IFCModel<Event>>) {
    if (Object.keys(manager.state.models).length) {
      manager.removeSubset(highlightedModel.id, highlightMaterial);
    }
    highlight(event, highlightMaterial);
  }

Steps to Reproduce

I have a repo where I reproduce this issue here.

git clone git@github.com:Amar-Gill/r3f-model-highlight-repro.git
cd r3f-model-highlight-repro/
npm i && npm run dev

Double click any mesh, it will be highlighted. Double clicking a new mesh will highlight it also, but the highlight on the previously clicked mesh will remain:

Screen Shot 2022-01-29 at 1 39 11 PM

Next, comment out the line for manager.state.models[modelID] = intersection.object; and double click a mesh. The mesh will not highlight and error will appear on console:

Uncaught TypeError: this.state.models[modelID] is undefined
iserranoe commented 2 years ago

I'having the same issue, which was not present in previous versions.

agviegas commented 2 years ago

Hi! What version of IFC.js are you using? Are you accessing / changing ifcManager.state.models somewhere else? Are you using the version of Three.js specified by the peer dependency of IFC.js? 🤔

iserranoe commented 2 years ago

In my case, I have these dependencies:

"node_modules/web-ifc-three": {
      "version": "0.0.102",
      "resolved": "https://registry.npmjs.org/web-ifc-three/-/web-ifc-three-0.0.102.tgz",
      "integrity": "sha512-Mxx4tN89Lb4dZd/....",
      "dependencies": {
        "three-mesh-bvh": "^0.5.2",
        "web-ifc": "^0.0.32"
      },
      "peerDependencies": {
        "three": "^0.135.0"
      }
    },
 "node_modules/web-ifc-viewer": {
      "version": "1.0.127",
      "resolved": "https://registry.npmjs.org/web-ifc-viewer/-/web-ifc-viewer-1.0.127.tgz",
      "integrity": "sha512-0K4ivXDoGXiP3Q8/...",
      "dependencies": {
        "camera-controls": "^1.33.1",
        "dat.gui": "^0.7.7",
        "gsap": "^3.7.1",
        "postprocessing": "^6.23.1",
        "three-mesh-bvh": "^0.5.2",
        "web-ifc": "^0.0.32",
        "web-ifc-three": "^0.0.102"
      },
      "peerDependencies": {
        "three": "^0.135.0"
      }
    },

And in package.json

    "three": "^0.135.0",
    "three-mesh-bvh": "^0.5.2",
    "web-ifc": "^0.0.32",
    "web-ifc-three": "0.0.102",
    "web-ifc-viewer": "^1.0.127",

And I think I'm not using ifcManager.state.models anywhere else. And ifc is IFC2x3

Amar-Gill commented 2 years ago

package.json

"dependencies": {
    "@react-three/drei": "^8.6.3",
    "@react-three/fiber": "^7.0.25",
    "@types/three": "^0.136.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "three": "^0.136.0",
    "web-ifc-three": "0.0.102"
  },

In node_modules, web-ifc-three shows

  "peerDependencies": {
    "three": "^0.135.0"
  }

I'll try downgrading to 0.135

Amar-Gill commented 2 years ago

I'm also not changing ifcManager.state.models anywhere else

Amar-Gill commented 2 years ago

@agviegas still same error after downgrading to three@0.135.0 which matches peer dependancy of web-ifc-three version I installed.

Amar-Gill commented 2 years ago

Do you think it's because I'm using useBVH hook from @react-three/drei and not three-mesh-bvh used in examples?

import { useBVH } from '@react-three/drei';

Edit: tried without the hook, doesn't look like it's the issue.

agviegas commented 2 years ago

Hum. Two questions:

  1. You are using the IFCLoader from web-ifc-three and not the one from Three / react-three-fiber, right?
  2. Are you using web-workers?

If you have an example I can debug (either a small repo or a live example) I will happily look into it.

Amar-Gill commented 2 years ago

Correct I'm using IFCLoader from web-ifc-three. So I was talking with r3f team, and was able to use the useLoader hook from r3f package to load the load the ifc model. It seems to fix my first issue, where I need to manually set manager.state.models[modelID]. However, the removeSubset functionality is still not working. I will close this issue and make a separate issue for that.

I've updated my git repo to show this working. I'll share the code here as well.

Amar-Gill commented 2 years ago
// App.tsx
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import React, { Suspense } from 'react';

import IFCContainer from './IFCContainer';

function App() {
  return (
    <>
      <Canvas
        camera={{
          far: 500,
          near: 1,
          zoom: 1,
          position: [1, 8, 20],
          rotation: [-0.4, 0.55, 0.2],
        }}>
        <ambientLight intensity={0.1} color="white" />
        <directionalLight color="white" position={[40, 40, 100]} />
        <OrbitControls enablePan={true} enableZoom={true} enableRotate={true} />
        <Suspense fallback={null}>
          <IFCContainer />
        </Suspense>
        <gridHelper args={[100, 100]} />
        <axesHelper args={[25]} />
      </Canvas>
    </>
  );
}

export default App;
Amar-Gill commented 2 years ago
// IFCContainer.tsx
import { useBVH } from '@react-three/drei';
import { useLoader, useThree } from '@react-three/fiber';
import React, { useRef, useState } from 'react';
import { Intersection, Material, MeshLambertMaterial } from 'three';
import { IFCManager } from 'web-ifc-three/IFC/components/IFCManager';
import { IFCModel } from 'web-ifc-three/IFC/components/IFCModel';
import { IFCLoader } from 'web-ifc-three/IFCLoader';

let manager: IFCManager;

export default function IFCContainer() {
  const ifc = useLoader(IFCLoader, 'test-file.ifc', (loader: any) => {
    manager = loader.ifcManager;
    manager.setWasmPath('resources/');
  });
  const [highlightedModel, setHighlightedModel] = useState({ id: -1 });

  const mesh = useRef(null!);
  useBVH(mesh);

  const scene = useThree((state) => state.scene);

  const highlightMaterial = new MeshLambertMaterial({
    transparent: true,
    opacity: 0.6,
    color: 0xff00ff,
    depthTest: false,
  });

  function handleDblClick(event: Intersection<IFCModel<Event>>) {
    manager.removeSubset(highlightedModel.id, highlightMaterial);
    highlight(event, highlightMaterial);
  }

  function highlight(intersection: Intersection<IFCModel<Event>>, material: Material) {
    const { faceIndex } = intersection;
    const { modelID, geometry } = intersection.object;
    const id = manager.getExpressId(geometry, faceIndex);

    setHighlightedModel({ id: modelID });

    manager.createSubset({
      modelID,
      ids: [id],
      material,
      scene,
      removePrevious: true,
    });
  }

  return <primitive ref={mesh} object={ifc} onDoubleClick={handleDblClick} />;
}
agviegas commented 2 years ago

Thanks a lot for sharing it! 💛

iserranoe commented 2 years ago

I couldn't solve the problem. I'm basically following this example: https://ifcjs.github.io/info/docs/Guide/web-ifc-three/Tutorials/Highlighting/#how-to-do-it, inside a React.useEffect hook. It's curious that I get the error the second time I go the page, but the first time it works fine; and, also, I didn't have this problem with version 0.0.67. I'm also having this warning when I leave the page: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. Maybe, it has to do?

agviegas commented 2 years ago

Hi @iserranoe! Just FYI, right now IFC.js is not supposed to be destroyed / reinitialized multiple times during the execution of the application. This might change in the near future, but right now this is how it is. Are you destroying / reinitializing it?

iserranoe commented 2 years ago

Hi @agviegas! I don't think I am destroying or reinitializing it

agviegas commented 2 years ago

Are you sure @iserranoe? I mention this because I am aware that libraries/frameworks like react or angular do this automatically. For instance, if you are initializing IFC.js in a component, it is destroyed whenever that component is removed from the DOM (e.g. the user navigates away from the viewer).

agviegas commented 2 years ago

The plan is to support this destruction / reinitialization very soon (hopefully before the end of next week). Until then, you can try locating IFC.js somewhere that doesn't get automatically destroyed. If that's not your case, let us know and we'll investigate further!

iserranoe commented 2 years ago

Well, I'm destroying the scene in the useEffect cleanup function, but if I remove it I get the same error

return () => {        
        try {
          setRenderer()
          mount_current.removeChild(renderer.domElement);
          scene.clear();

        } catch (error) {
          console.error(error);
        }
      }; 

This is the complete function:

  const [renderersnap, setRenderer] = React.useState()  
  const mount = React.useRef(null);
  const [properties, setProp] = React.useState([])
  const [message, setMessage] = React.useState(false)

  React.useEffect(() => {     
      // let height = mount.current.clientHeight;

      // Se crean los nuevos constructores
      const scene = new Scene();
      const width = mount.current.clientWidth;
      const ratio = 16/9;
      const size = {
        width: width,
        height: width/ratio
      }

      // Se crea la cámara, punto de vista del usuario
      const camera = new PerspectiveCamera(75, ratio);  

      const lightColor = 0xffffff;
      const ambientLight = new AmbientLight(lightColor, 0.5);
      scene.add(ambientLight);
      const directionalLight = new DirectionalLight(lightColor, 1);
      directionalLight.position.set(0, 10, 0);
      directionalLight.target.position.set(-5, 0, 0);
      scene.add(directionalLight);
      scene.add(directionalLight.target);
      scene.background = new Color('white');

      // Se crea el renderer
      const renderer = new WebGLRenderer({ 
        antialias: true, 
        alpha: true,
        preserveDrawingBuffer: true 
      });    
      renderer.setSize(size.width, size.height);
      //renderer.setClearColor('#000000');
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));      
      const threeCanvas = renderer.domElement 

      // Se crea el grid y los ejes de la escena:
      //const grid = new GridHelper(50, 30);
      //scene.add(grid);
      const axes = new AxesHelper();
      axes.material.depthTest = false;
      axes.renderOrder = 1;
      scene.add(axes);

      // Se crean los controladores de las órbitas, para navegar por la escena
      const controls = new OrbitControls(camera, threeCanvas); //renderer.domElement coge la ref={mount}, en los tutoriales, se pone en lugar the three canvas
      controls.enableDamping = true;
      controls.target.set(-2, 0, 0);
      //controls.enableZoom = true;

      // Cargar el archivo
      let loader;
      loader = new IFCLoader();
      //Este método indica dónde se sitúa el archivo wasm, que he copiado y pegado de node_modules/web-ifc
      //Este es el path de public
      loader.ifcManager.setWasmPath('../../'); 
      //Este método permite seleccionar objetos más rápidamente, especialmente aquellos con geometrías grandes. 
      loader.ifcManager.setupThreeMeshBVH (
        computeBoundsTree,
        disposeBoundsTree,
        acceleratedRaycast)

      // Creamos un array para almacenar las referencias de a los modelos IFC en la escena para seleccionarlos
      let ifcModels = []
      //Método para obtener todas las propiedades del archivo

      //* **** Propiedades del objeto *****/
      // Se carga el objeto desde una url y se modifican las propiedades de la cámara según el objeto

      let center = {}
      const url = installation.model3d.storage.url_ifc
      loader.load(
      // '/media/modelos/RadioTower.fbx',
        url,
        (object) => {
          scene.add(object);
          ifcModels.push(object)

          // Comento esto porque si muevo el objeto los elementos destacados aparecen en la posición original. 
          // Tengo que solucionarlo
          // Se crea un cubo alrededor del objeto
          const box = new Box3().setFromObject(object);
          // Se calcula el centro del cubo
          center = box.getCenter(new Vector3());
          // Se calcula el lado del cubo (creo que es lado)
          const size = box.getSize(new Vector3()).length();

          // ***** Propiedades de la cámara *****
          // Movemos la cámara algo más lejos del objeto. Cubo dentro de esfera.
          const r = size//Math.sqrt(3)*size/2;

          const theta = 0;
          // Con phi=0, la cámara se queda en el plano x-z
          const phi = 0;

          // Muevo la cámara en todo caso para que se vea bien el objeto
          const camara_x = r * Math.cos(phi) * Math.sin(theta)+center.x;
          const camara_y = r * Math.sin(phi) * Math.sin(theta)+center.y;
          const camara_z = r * Math.cos(theta)+center.z;
          camera.position.x = camara_x;
          camera.position.y = camara_y;
          camera.position.z = camara_z;

          scene.add(object);
        }
      );

      // scene.add(cube);

      // Raycaster, sólo coge información del primer objeto que se encuentra
      // Funciones para extraer el id del objecto que se selecciona
      const raycaster = new Raycaster()
      raycaster.firstHitOnly = true;
      const mouse = new Vector2()

      // Esta función lanza rayos para calcular la posición actual del ratón en la pantalla
      // Especifica con qué objetos choca el rayo, que sólo puede chocar con los modelos IFC cargados. Si hay más objetos en la escena, los ignorará. 
      const cast = (event) =>{
          // Computes the position of the mouse on the screen
          const bounds = threeCanvas.getBoundingClientRect();

          const x1 = event.clientX - bounds.left;
          const x2 = bounds.right - bounds.left;
          mouse.x = (x1 / x2) * 2 - 1;

          const y1 = event.clientY - bounds.top;
          const y2 = bounds.bottom - bounds.top;
          mouse.y = -(y1 / y2) * 2 + 1;

          // Places it on the camera pointing to the mouse
          raycaster.setFromCamera(mouse, camera);

          // Casts a ray
          return raycaster.intersectObjects(ifcModels);
      }

      //https://ifcjs.github.io/info/docs/Guide/web-ifc-three/Tutorials/Highlighting/#how-to-do-it
      // Crear material para seleccionar el elemento
      const preselectMat = new MeshLambertMaterial({
        transparent: true,
        opacity: 0.6,
        color: '#0063c1',
        depthTest: false
      })

      // Reference to the previous selection: 
      let preselectModel = { id: - 1};
      const ifc = loader.ifcManager;
      //const output = document.getElementById("id-output");

      const highlight = async (event, material, model) => {
        //console.log(ifc)       
        const found = cast(event)[0];
        if (found){
            // Gets model ID
            model.id = found.object.modelID;

            const index = found.faceIndex;
            const geometry = found.object.geometry;
            const id = ifc.getExpressId(geometry,index);   

            // Pongo esto a ver si se soluciona un error, pero no me va: https://github.com/IFCjs/web-ifc-three/issues/83
            //ifc.state.models[model.id] = found.object

            // Creates subset
            ifc.createSubset({
                modelID: model.id,
                ids: [id],
                material: material,
                scene: scene,
                removePrevious: true,
            })

            const properties_new = await ifc.getItemProperties(model.id, id, true);
            setProp(properties_new)
            setMessage(true)
          }              
        else {          
            // Removes previous highlight. Para que funcione hay que quitar scene
            ifc.removeSubset(model.id, material);
            setMessage(false)
        }
      }

      threeCanvas.onclick = (event) => highlight(
                                        event,
                                        preselectMat,
                                        preselectModel);

      // Monta el renderizador, no entiendo
      const mount_current = mount.current
      mount_current.appendChild(renderer.domElement);

      //* **** Función para la animación, necesaria para mover el objeto */
      const animate = () => {
        window.requestAnimationFrame(animate);
        controls.update();
        renderer.render(scene, camera);
        setRenderer(renderer) //Aquí no está cogiendo bien el renderer
      };
      requestAnimationFrame(animate);

      return () => {        
        // He puesto este try para los casos en los que no hay modelo.
        // Aunque haya un condicional para comprobar que el modelo no sea nulo,
        // cuando se va a la instalación sin modelo sin refrescar,
        // daba error porque hace el contenido de useEffect una vez antes de cargar la página.
        try {
          setRenderer()
          mount_current.removeChild(renderer.domElement);
          scene.clear();

        } catch (error) {
          console.error(error);
        }
      };  
  },[mount,installation.model3d]);
iserranoe commented 2 years ago

I finally made it work using @Amar-Gill sugestion and making model.id=0: When I set 'ifc.state.models[model.id] = found.object' I had problems with 'ifc.getItemProperties'. As before, this was happening when I changed pages. I found that model.id was 0 in the first case and an int number in the second case, so I just set model.id to 0, but I really don't know what is going on, just that it works so far!

const highlight = async (event, material, model) => {  
      const found = cast(event)[0];
      if (found){
          // Gets model ID
          model.id = found.object.modelID;

          const index = found.faceIndex;
          const geometry = found.object.geometry;
          let id = await ifc.getExpressId(geometry,index);   

          ifc.state.models[model.id] = found.object

          // Creates subset
          ifc.createSubset({
              modelID: model.id,
              ids: [id],
              material: material,
              scene: scene,
              removePrevious: true,
          })

          model.id=0
          const properties_new = await ifc.getItemProperties(model.id, id, true);
          setProp(properties_new)
          setMessage(true)
        }              
      else {          
          // Removes previous highlight. Para que funcione hay que quitar scene
          ifc.removeSubset(model.id, material);
          setMessage(false)
      }
    }
agviegas commented 2 years ago

This new tutorial might be relevant. 🙂