Loading coming back on Model interaction

Hello, I have a small problem and I can’t seem to find any solution.

I made a small interactive game where a pawn is moved in a maze by selecting cards given randomly to the user.

I have multiple models imported in my scene, they are all .glb files.

At the start, I display a progress bar indicating the percentage of the total loading, when the loading is at 100%, the scene is shown.

However, whenever I click and choose the first card to play (It only does this on the first chosen card, after this, it never pops up again), the loading screen comes back quickly and then goes away again.

I tried to find what was the problem and I can’t understand what is causing this. I am still inexperienced with three js and less experienced with react three fiber.

Here are the codes of my 3 components for the cards management :

"use client";

import { useRouter } from "next/navigation";

import React, { Suspense, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";

import { Mutex } from "async-mutex";

// Used to check if the element are placed correctly
import { Html, OrbitControls, useProgress } from "@react-three/drei";

// Custom types
import { CardType } from "@/config/cardconfig";
import { BoardAnimationHandle, Cell, Maze } from "@/types/board";

// Custom component - 3D Meshes
import Board from "@/components/game/3d/Board";
import Hand from "@/components/game/3d/Hand";
import { Progress } from "@/components/ui/progress";

function LoadingIndicator() {
  const { progress } = useProgress();
  return (
    <Html
      className="flex flex-col items-center justify-center px-40 w-[100vw] h-[100vh] bg-secondary gap-y-5"
      center
    >
      <div className="font-portfolioMedieval text-2xl">Loading</div>
      <div className="font-portfolioSubtitle text-lg">
        {progress.toFixed(2)} %
      </div>
      <Progress value={progress} />
    </Html>
  );
}

// This component is used as the Game Controller of the app => all the game logic will depart from here
const Scene = () => {
  // Router
  const router = useRouter();

  // Mutex
  const [mutex, setMutex] = useState(new Mutex());

  // Get the viewport
  const { viewport } = useThree();

  // Scale function for the board and the hand of cards
  const boardScale = viewport.width * 0.0801 + 0.2214;
  const handScale = viewport.width * 0.0162381597 + 0.8421650877;
  // const handScale = 0.88;

  // Maze configs
  // Generate the maze of the board
  const generateMaze = (
    rows: number,
    cols: number,
    treasures: number,
    ennemies: number
  ): Maze => {
    // Generate array for the maze full of walls
    const maze: Maze = {
      paths: Array.from({ length: rows }, (_, y) =>
        Array.from({ length: cols }, (_, x) => ({
          x,
          y,
          top: true,
          right: true,
          bottom: true,
          left: true,
          visited: false,
        }))
      ),
    };

    // Create an array of cells to be visited (to form the maze)
    const stack: Cell[] = [];
    let currentCell: Cell = maze.paths[0][0];
    currentCell.visited = true;

    stack.push(currentCell);

    // Visit "all" cells of the maze, but with creating the walls between them
    while (stack.length > 0) {
      currentCell = stack.pop()!;
      const { x, y } = currentCell;

      // Find the neighbors of the current cell
      const neighbors = [
        { cell: maze.paths[y - 1]?.[x], direction: "top" },
        { cell: maze.paths[y]?.[x + 1], direction: "right" },
        { cell: maze.paths[y + 1]?.[x], direction: "bottom" },
        { cell: maze.paths[y]?.[x - 1], direction: "left" },
      ];

      neighbors.sort(() => Math.random() - 0.5);

      // If the neighbors are alreay "visited", put on a wall
      for (const { cell, direction } of neighbors) {
        if (cell && !cell.visited) {
          // Wall creation within the maze
          switch (direction) {
            case "top": {
              currentCell.top = false;
              cell.bottom = false;
            }
            case "right": {
              currentCell.right = false;
              cell.left = false;
            }
            case "bottom": {
              currentCell.bottom = false;
              cell.top = false;
            }
            case "left": {
              currentCell.left = false;
              cell.right = false;
            }
          }
          cell.visited = true;
          stack.push(cell);
        }
      }
    }

    // Wall creation on the sides of the maze
    for (const row of maze.paths) {
      for (const cell of row) {
        if (cell.x === 0) {
          cell.left = true;
        }
        if (cell.y === 0) {
          cell.top = true;
        }
        if (cell.x === maze.paths[cell.y].length - 1) {
          cell.right = true;
        }
        if (cell.y === maze.paths.length - 1) {
          cell.bottom = true;
        }
      }
    }

    // Player creation
    const playerX = maze.paths[0].length - 1;
    const playerY = maze.paths.length - 1;
    maze.paths[playerX][playerY].player = true;

    //Exit creation
    if (
      !maze.paths[0][0].exit &&
      !maze.paths[0][0].treasure &&
      !maze.paths[0][0].ennemy &&
      !maze.paths[0][0].player
    ) {
      maze.paths[0][0].exit = true;
    }

    // Treasure creation
    for (let i = 0; i < treasures; i++) {
      const treasureX = Math.floor(Math.random() * cols);
      const treasureY = Math.floor(Math.random() * rows);

      if (
        !maze.paths[treasureX][treasureY].exit &&
        !maze.paths[treasureX][treasureY].treasure &&
        !maze.paths[treasureX][treasureY].ennemy &&
        !maze.paths[treasureX][treasureY].player
      ) {
        maze.paths[treasureX][treasureY].treasure = true;
      }
    }

    // Ennemy creation
    for (let i = 0; i < ennemies; i++) {
      const ennemyX = Math.floor(Math.random() * cols);
      const ennemyY = Math.floor(Math.random() * rows);
      if (
        !maze.paths[ennemyX][ennemyY].exit &&
        !maze.paths[ennemyX][ennemyY].treasure &&
        !maze.paths[ennemyX][ennemyY].ennemy &&
        !maze.paths[ennemyX][ennemyY].player
      ) {
        maze.paths[ennemyX][ennemyY].ennemy = true;
      }
    }

    return maze;
  };

  // Find the player in the maze
  const findPlayer = (): number[] => {
    let playerPos = [-1, -1];
    maze.current.paths.map((row) => {
      row.map((cell) => {
        if (cell.player) {
          playerPos = [cell.y, cell.x];
        }
      });
    });
    return playerPos;
  };

  // Refs
  // Board ref
  const boardRef = useRef<BoardAnimationHandle>(null);

  // Maze
  const maze = useRef(generateMaze(5, 5, 3, 3));

  // Animation
  // Movement of the board on the mouse movements
  useFrame((state, delta) => {
    boardRef.current!.boardMeshRef().position.z = THREE.MathUtils.lerp(
      boardRef.current!.boardMeshRef().position.z,
      (state.pointer.y * Math.PI) / 100,
      0.05
    );
    boardRef.current!.boardMeshRef().position.x = THREE.MathUtils.lerp(
      boardRef.current!.boardMeshRef().position.x,
      (state.pointer.x * Math.PI) / 100,
      0.05
    );
  });

  // Game functions
  // Play a card
  const onCardUsed = (actionType: CardType, index: number) => {
    // Position of the player (y is 1st and x is 2nd)
    const playerPosition = findPlayer();

    // Dice thrown to get a card number
    const diceThrow = Math.floor(Math.random() * 4 + 1);

    // The real possible steps
    let steps = 0;
    // The positions of the encountered treasures
    let treasureFlags = [];
    // If the pawn encountered the exit
    let exitFlag = false;

    switch (actionType) {
      case CardType.Forward: {
        for (let i = 0; i < diceThrow; i++) {
          // Exit of the maze
          if (playerPosition[0] - i - 1 == 0 && playerPosition[1] == 0) {
            exitFlag = true;
          }
          // Border of the maze
          if (playerPosition[0] - i <= 0) {
            break;
          }
          // Wall
          if (
            maze.current.paths[playerPosition[0] - i][playerPosition[1]].top ||
            maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
              .bottom
          ) {
            break;
          }
          // Ennemy
          if (
            maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
              .ennemy
          ) {
            break;
          }
          // Treasure
          if (playerPosition[0] - i - 1 >= 0) {
            if (
              maze.current.paths[playerPosition[0] - i - 1][playerPosition[1]]
                .treasure
            ) {
              treasureFlags.push([
                playerPosition[0] - i - 1,
                playerPosition[1],
              ]);
              steps++;
              break;
            }
          }
          // Otherwise, count possible steps
          steps++;
        }

        boardRef.current?.moveForward(
          steps,
          playerPosition,
          treasureFlags,
          exitFlag
        );
        break;
      }
      case CardType.Backward: {
        for (let i = 0; i < diceThrow; i++) {
          // Exit of the maze
          if (playerPosition[0] + i + 1 == 0 && playerPosition[1] == 0) {
            exitFlag = true;
          }
          // Border of the maze
          if (playerPosition[0] + i >= maze.current.paths.length - 1) {
            break;
          }
          // Wall
          if (
            maze.current.paths[playerPosition[0] + i][playerPosition[1]]
              .bottom ||
            maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]].top
          ) {
            break;
          }
          // Ennemy
          if (
            maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]]
              .ennemy
          ) {
            break;
          }
          // Treasure
          if (playerPosition[0] + i + 1 < maze.current.paths.length) {
            if (
              maze.current.paths[playerPosition[0] + i + 1][playerPosition[1]]
                .treasure
            ) {
              treasureFlags.push([
                playerPosition[0] + i + 1,
                playerPosition[1],
              ]);
              steps++;
              break;
            }
          }
          // Otherwise, count possible steps
          steps++;
        }
        boardRef.current?.moveBackward(
          steps,
          playerPosition,
          treasureFlags,
          exitFlag
        );
        break;
      }
      case CardType.Left: {
        for (let i = 0; i < diceThrow; i++) {
          // Exit of the maze
          if (playerPosition[0] == 0 && playerPosition[1] - i - 1 == 0) {
            exitFlag = true;
          }
          // Border of the maze
          if (playerPosition[1] - i <= 0) {
            break;
          }
          // Wall
          if (
            maze.current.paths[playerPosition[0]][playerPosition[1] - i].left ||
            maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
              .right
          ) {
            break;
          }
          // Ennemy
          if (
            maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
              .ennemy
          ) {
            break;
          }
          // Treasure
          if (playerPosition[1] - i - 1 >= 0) {
            if (
              maze.current.paths[playerPosition[0]][playerPosition[1] - i - 1]
                .treasure
            ) {
              treasureFlags.push([
                playerPosition[0],
                playerPosition[1] - i - 1,
              ]);
              steps++;
              break;
            }
          }
          // Otherwise, count possible steps
          steps++;
        }
        boardRef.current?.moveLeft(
          steps,
          playerPosition,
          treasureFlags,
          exitFlag
        );
        break;
      }
      case CardType.Right: {
        for (let i = 0; i < diceThrow; i++) {
          // Exit of the maze
          if (playerPosition[0] == 0 && playerPosition[1] + i + 1 == 0) {
            exitFlag = true;
          }
          // Border of the maze
          if (playerPosition[1] + i >= maze.current.paths[0].length - 1) {
            break;
          }
          // Wall
          if (
            maze.current.paths[playerPosition[0]][playerPosition[1] + i]
              .right ||
            maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
              .left
          ) {
            break;
          }
          // Ennemy
          if (
            maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
              .ennemy
          ) {
            break;
          }
          // Treasure
          if (playerPosition[1] + i + 1 < maze.current.paths[0].length) {
            if (
              maze.current.paths[playerPosition[0]][playerPosition[1] + i + 1]
                .treasure
            ) {
              treasureFlags.push([
                playerPosition[0],
                playerPosition[1] + i + 1,
              ]);
              steps++;
              break;
            }
          }
          // Otherwise, count possible steps
          steps++;
        }

        boardRef.current?.moveRight(
          steps,
          playerPosition,
          treasureFlags,
          exitFlag
        );
        break;
      }
      case CardType.Attack: {
        // Side value
        let side = -1;

        // Check if there is an enemy around the player - clockwise, an enemy at a time
        if (
          playerPosition[0] - 1 >= 0 &&
          !maze.current.paths[playerPosition[0]][playerPosition[1]].top &&
          !maze.current.paths[playerPosition[0] - 1][playerPosition[1]]
            .bottom &&
          maze.current.paths[playerPosition[0] - 1][playerPosition[1]].ennemy
        ) {
          // Top
          side = 0;
        }
        if (
          playerPosition[1] + 1 < maze.current.paths[0].length &&
          !maze.current.paths[playerPosition[0]][playerPosition[1]].right &&
          !maze.current.paths[playerPosition[0]][playerPosition[1] + 1].left &&
          maze.current.paths[playerPosition[0]][playerPosition[1] + 1].ennemy
        ) {
          // Right
          side = 1;
        }
        if (
          playerPosition[0] + 1 < maze.current.paths.length &&
          !maze.current.paths[playerPosition[0]][playerPosition[1]].bottom &&
          !maze.current.paths[playerPosition[0] + 1][playerPosition[1]].top &&
          maze.current.paths[playerPosition[0] + 1][playerPosition[1]].ennemy
        ) {
          // Bottom
          side = 2;
        }
        if (
          playerPosition[1] - 1 >= 0 &&
          !maze.current.paths[playerPosition[0]][playerPosition[1]].left &&
          !maze.current.paths[playerPosition[0]][playerPosition[1] - 1].right &&
          maze.current.paths[playerPosition[0]][playerPosition[1] - 1].ennemy
        ) {
          // Left
          side = 3;
        }

        // Attack the nearest enemy
        boardRef.current?.attack(side, playerPosition);
        break;
      }
    }
  };

  // Change the position of the player in the maze and update the maze
  const playerMovement = (
    mazeB: Maze,
    newPos: number[],
    treasure: boolean
  ): void => {
    mazeB.paths.map((row) => {
      row.map((cell) => {
        if (cell.player) {
          cell.player = false;
        }
      });
    });

    // y is the row, y is the col
    mazeB.paths[newPos[0]][newPos[1]].player = true;

    if (treasure) {
      mazeB.paths[newPos[0]][newPos[1]].treasure = false;
    }

    maze.current = mazeB;
  };

  // Delete the attacked ene
  const playerAttack = (mazeB: Maze, enemyPos: number[]): void => {
    mazeB.paths[enemyPos[0]][enemyPos[1]].ennemy = false;

    maze.current = mazeB;
  };

  const playerExit = () => {
    router.push("/portfolio#myself");
  };

  return (
    <>
      <Board
        ref={boardRef}
        mutex={mutex}
        maze={maze.current}
        scale={boardScale}
        playerMovement={playerMovement}
        playerAttack={playerAttack}
        playerExit={playerExit}
      />
      <Hand mutex={mutex} scale={handScale} onCardUsed={onCardUsed} />
    </>
  );
};

const Page = () => {
  return (
    <div className="h-screen">
      {/* Camera options */}
      <Canvas
        camera={{ fov: 45, near: 0.1, far: 1000, position: [0, 4.8, 3.3] }}
        shadows={true}
      >
        <Suspense fallback={<LoadingIndicator />}>
          {/* Lights */}
          <ambientLight intensity={0.3} color={0xa3a3a3}></ambientLight>
          <directionalLight
            intensity={0.8}
            color={0xffffff}
            position={[0, 10, 0]}
            castShadow
            shadow-mapSize={[1024, 1024]}
          />
          <pointLight
            position={[-4.5, 2, 0]}
            intensity={9}
            color={"#c56f28"}
          ></pointLight>
          <pointLight
            position={[4.5, 2, 0]}
            intensity={9}
            color={"#c56f28"}
          ></pointLight>
          <pointLight
            position={[0, 2, -3]}
            intensity={9}
            color={"#c56f28"}
          ></pointLight>
          <pointLight
            position={[0, 2, 4]}
            intensity={9}
            color={"#c56f28"}
          ></pointLight>

          <directionalLight
            intensity={1.5}
            color={"#c56f28"}
            position={[0, 10, 10]}
            castShadow
            shadow-mapSize={[1024, 1024]}
          />

          {/* Scene and the game */}
          <Scene />
        </Suspense>
      </Canvas>
    </div>
  );
};

export default Page;
import React, { useRef, useState } from "react";

import gsap from "gsap";
import { useGSAP } from "@gsap/react";

import { Mesh } from "three";
import Card from "./Card";
import { CardInfo, HandProp } from "@/types/hand";

import { CardsPositions, UsePos, UseRot } from "@/config/handconst";
import { CARD_CONFIG } from "@/config/cardconfig";
import { ThreeEvent } from "@react-three/fiber";
import { Html } from "@react-three/drei";

import { Button, buttonVariants } from "@/components/ui/button";
import { Shuffle } from "lucide-react";
import { cn } from "@/lib/utils";

const Hand = ({ mutex, scale, onCardUsed }: HandProp) => {
  // GSAP
  const { contextSafe } = useGSAP();

  // Generating a number between 1 and 5 (for the card configuration) (1 and 5 included)
  const generateRandomCardConfig = () => {
    return Math.floor(Math.random() * 5);
  };

  const generateRandomHand = (): CardInfo[] => {
    const initialHand: CardInfo[] = [];

    const indices = new Set<number>();
    while (indices.size < 4) {
      indices.add(generateRandomCardConfig());
    }

    indices.forEach((index) => {
      initialHand.push({ cardConfig: CARD_CONFIG[index] });
    });

    return initialHand;
  };

  // How many shuffle is possible
  const [shuffle, setShuffle] = useState<number>(5);

  // Hand of cards and their refs
  const [hand, setHand] = useState(generateRandomHand());
  // Refs on the cards
  const cardsRefs = useRef<Mesh[]>([]);
  const buttonRef = useRef<HTMLDivElement>(null);

  // How many card left
  const cardQuantityIndex = hand.length - 1;

  // If a card has been clicked
  const isClicked = useRef<boolean>(false);

  // Shuffle a new hand
  const shuffleHand = () => {
    cardsRefs.current = [];
    setHand(generateRandomHand());
  };

  // Animation when the card is hovered in by the mouse
  const HoverIn = contextSafe((index: number, e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation();
    if (!isClicked.current) {
      // Move the hovered card
      gsap.to(cardsRefs.current[index]?.position!, {
        x: CardsPositions[cardQuantityIndex].HoverPos[index][0],
        y: CardsPositions[cardQuantityIndex].HoverPos[index][1],
        z: CardsPositions[cardQuantityIndex].HoverPos[index][2],
      });
      // Rotate the hovered card
      gsap.to(cardsRefs.current[index]?.rotation!, {
        x: CardsPositions[cardQuantityIndex].HoverRot[index][0],
        y: CardsPositions[cardQuantityIndex].HoverRot[index][1],
        z: CardsPositions[cardQuantityIndex].HoverRot[index][2],
      });

      // Move the other cards to move them apart from the hovered one
      for (let i = 0; i <= cardQuantityIndex; i++) {
        // If the card exist and is not the hovered one, move it to the correct position
        if (cardsRefs.current[i] && i != index) {
          gsap.to(cardsRefs.current[i]?.position!, {
            x: CardsPositions[cardQuantityIndex].MoveHoverPos[i][0],
            y: CardsPositions[cardQuantityIndex].MoveHoverPos[i][1],
            z: CardsPositions[cardQuantityIndex].MoveHoverPos[i][2],
          });
        }
      }
    }
  });

  // Animation when the card is hovered out by the mouse
  const HoverOut = contextSafe((index: number, e: ThreeEvent<PointerEvent>) => {
    e.stopPropagation();
    if (!isClicked.current) {
      // Move the hovered card to its base position
      gsap.to(cardsRefs.current[index]?.position!, {
        x: CardsPositions[cardQuantityIndex].BasePos[index][0],
        y: CardsPositions[cardQuantityIndex].BasePos[index][1],
        z: CardsPositions[cardQuantityIndex].BasePos[index][2],
      });
      // Rotate the hovered card to it base rotation
      gsap.to(cardsRefs.current[index]?.rotation!, {
        x: CardsPositions[cardQuantityIndex].BaseRot[index][0],
        y: CardsPositions[cardQuantityIndex].BaseRot[index][1],
        z: CardsPositions[cardQuantityIndex].BaseRot[index][2],
      });

      // Move the other cards to move them apart from the hovered one
      for (let i = 0; i <= cardQuantityIndex; i++) {
        // If the card exist and is not the hovered one, move it to the correct position
        if (cardsRefs.current[i] && i != index) {
          gsap.to(cardsRefs.current[i]?.position!, {
            x: CardsPositions[cardQuantityIndex].BasePos[i][0],
            y: CardsPositions[cardQuantityIndex].BasePos[i][1],
            z: CardsPositions[cardQuantityIndex].BasePos[i][2],
          });
        }
      }
    }
  });

  // Animation and use of the card when the card is clicked
  const ClickOn = contextSafe((index: number, e: ThreeEvent<MouseEvent>) => {
    e.stopPropagation();
    mutex.acquire().then(() => {
      if (!isClicked.current) {
        isClicked.current = true;

        let nextPos = cardQuantityIndex - 1;

        gsap.to(cardsRefs.current[index]?.position!, {
          x: UsePos[0],
          y: UsePos[1],
          z: UsePos[2],
        });

        // Move the other cards to their next base position
        let count = 0;

        for (let i = 0; i <= cardQuantityIndex; i++) {
          if (cardsRefs.current[i] && i != index) {
            gsap.to(cardsRefs.current[i]?.position!, {
              x: CardsPositions[nextPos].BasePos[count][0],
              y: CardsPositions[nextPos].BasePos[count][1],
              z: CardsPositions[nextPos].BasePos[count][2],
            });
            gsap.to(cardsRefs.current[i]?.rotation!, {
              x: CardsPositions[nextPos].BaseRot[count][0],
              y: CardsPositions[nextPos].BaseRot[count][1],
              z: CardsPositions[nextPos].BaseRot[count][2],
            });
            count++;
          }
        }

        gsap.to(cardsRefs.current[index]?.rotation!, {
          x: UseRot[0],
          y: UseRot[1],
          z: UseRot[2],
          onComplete: () => {
            // Play the card
            onCardUsed(hand[index].cardConfig.cardType, index);

            // Update the cards references
            cardsRefs.current = cardsRefs.current?.filter(
              (_, i) => i !== index
            );

            // Use the card and update the hand
            setHand(hand.filter((_, i) => i !== index));

            // Allow the possibility to click again
            isClicked.current = false;

            // When all the cards are used, shuffle new cards
            if (hand.length <= 1) {
              shuffleHand();
            }
          },
        });
      }
    });
  });

  // Click on the button to do a new shuffle
  const manualShuffle = () => {
    shuffleHand();
    setShuffle(shuffle - 1);
  };

  return (
    <mesh scale={scale}>
      {shuffle > 0 && (
        <Html className="w-[100vw] h-[100vh] fixed inset-0 flex justify-center items-center pointer-events-none">
          <div ref={buttonRef} className="absolute -top-[48.5%] -left-[48.5%]">
            <Button
              className={cn(
                "gap-x-3 pointer-events-auto",
                buttonVariants({
                  variant: "secondary",
                  className:
                    "w-24 h-11 font-portfolioMedieval text-primary text-3xl",
                })
              )}
              onClick={manualShuffle}
            >
              {shuffle}
              <Shuffle className="h-8 w-8" />
            </Button>
          </div>
        </Html>
      )}
      {hand.map((card, index) => (
        <Card
          ref={(cardElem) => (cardsRefs.current[index] = cardElem!)}
          key={index}
          index={index}
          cardConfig={card.cardConfig}
          position={CardsPositions[cardQuantityIndex].BasePos[index]}
          rotation={CardsPositions[cardQuantityIndex].BaseRot[index]}
          hoverIn={HoverIn}
          hoverOut={HoverOut}
          clickOn={ClickOn}
        />
      ))}
    </mesh>
  );
};

export default Hand;
import React from "react";
import { Mesh } from "three";
import { ThreeEvent, useLoader } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { CardProp } from "@/types/hand";
import { useGLTF } from "@react-three/drei";

// A card to be used in the game
const Card = React.forwardRef<Mesh, CardProp>((props, ref) => {
  const gltf = useLoader(GLTFLoader, props.cardConfig.frontTexture);

  return (
    <group>
      <primitive
        ref={ref}
        object={gltf.scene.clone()}
        position={props.position}
        rotation={props.rotation}
        scale={0.0315}
        onPointerEnter={(e: ThreeEvent<PointerEvent>) =>
          props.hoverIn(props.index, e)
        }
        onPointerLeave={(e: ThreeEvent<PointerEvent>) =>
          props.hoverOut(props.index, e)
        }
        onClick={(e: ThreeEvent<MouseEvent>) => props.clickOn(props.index, e)}
      ></primitive>
    </group>
  );
});

Card.displayName = "Card";

export default Card;

Thank you in advance, I stuck right now

the answer is suspense.

<ThisWontBeAffectedWhenSomeAsyncModelLoads />
<Suspense fallback={null}>
  <SomeAsyncModel />
</Suspense>

btw

const Card = React.forwardRef<Mesh, CardProp>((props, ref) => {
  const gltf = useLoader(GLTFLoader, props.cardConfig.frontTexture);
  return (
    <group>
      <primitive
        ref={ref}
        object={gltf.scene.clone()}

you are cloning the full scene on every render :smiling_face_with_tear:

im guessing you have figured that you can’t mount one and the same object multiple times in threejs and hence you clone. but memoize it! also no reason to pick out specific props (rotation, position, etc) when you can …spread.

import { useGLTF } from '@react-three/drei'

const Card = forwardRef((props, fref) => {
  const { scene } = useGLTF(props.cardConfig.frontTexture)
  const clone = useMemo(() => scene.clone(), [scene])
  return <primitive ref={fref} object={clone} {...props} />

last but not least you don’t have to clone, this is a unwashed/unclean threejs practice, in react you have this GitHub - pmndrs/gltfjsx: 🎮 Turns GLTFs into JSX components

npx gltfjsx card.glb --transform

it compresses the model, but also gives you a freely re-usable component.

1 Like

Thank you so much, I am learning a lot thanks to you !

I can’t test right now, but I’ll tell you whenever I can.

For the solution with , if I understood correctly, I have to get the cards out of the Suspense element to get rid of the loading screen poping up whenever ?

But then, it won’t be counted in the first loading right ?

Thanks a lot

anything inside a suspense block unmounts/remounts if something in it suspends. if you don’t have any suspense block declared then your canvas acts as one. so consider this

<Canvas>
  <mesh>
    <boxGeometry />
  </mesh>
  <AsyncGLTF />
</Canvas>

you will see a white canvas, and eventually the box will show together with the model. now consider this

  <mesh>
    <boxGeometry />
  </mesh>
  <Suspense fallback={null}>
    <AsyncGLTF />
  </Suspense>

first the box will show, then the model will show. you could also have a fallback

  <mesh>
    <boxGeometry />
  </mesh>
  <Suspense fallback={<mesh><sphereGeometry /></mesh>}>
    <AsyncGLTF />
  </Suspense>

first the box will show, a sphere will show up, when the model is loaded the sphere will vanish and the model shows.

it’s good learning about suspense, it’s a very powerful feature that solves all the async/loading problems in vanilla threejs. also enables really useful patterns https://twitter.com/0xca0a/status/1653168029755219970