Source: App.jsx

/**
 * @file
 *
 * Summary.
 * <p>Two rotating cubes using {@link https://react.dev React}
 * with {@link https://www.digitalocean.com/community/tutorials/how-to-set-up-a-react-project-with-vite Vite}.</p>
 * When the mouse is hovered onto a cube, its color changes from orange to hotpink.<br>
 * When a cube is clicked, its scale is toggled from 1 to 1.5 and its color changes.
 * The process of selecting colors is more complicated than it seems, because
 * {@link external:react.useEffect React useState} is asynchronous!
 *
 * <p>We label the cubes and their colors by calling Text,
 * with Hi-quality rendering w/ signed distance fields (SDF) and antialiasing,
 * using {@link https://protectwise.github.io/troika/troika-three-text/ troika-3d-text}.
 * It is also possible to use Text3D, with {@link https://hyper2.com.br/js/fonts/ type face} fonts.</p>
 *
 * <figure>
 *  <img src="../Text3D.png" width="256">
 *  <figcaption style="font-size: 100%">Text3D with bevel</figcaption>
 * </figure>
 *
 * <p>Finally, {@link https://codesandbox.io/p/sandbox/np6s28 decals}
 * are applied to each face of the cubes.
 *
 * Decals are objects that interfere with the
 * "{@link https://antongerdelan.net/opengl/raycasting.html mouse picking}"
 * and to avoid mistakes,
 * we call {@link https://r3f.docs.pmnd.rs/api/events event.stopPropagation()}
 * to get only the first intersection when a pick ray is cast. Furthermore, if
 * {@link http://drei.docs.pmnd.rs/abstractions/decal#decal no material
 * is specified}, a transparent meshBasicMaterial
 * with a polygonOffsetFactor of -10 will be created,
 * producing an awkward effect when the cubes overlap.</p>
 *
 * <figure>
 *  <img src="../decals.png" width="256">
 *  <figcaption style="font-size: 100%">Overlap with transparent meshBasicMaterial</figcaption>
 * </figure>
 *
 * <figure>
 *  <img src="../cubes2.png" width="256">
 *  <figcaption style="font-size: 100%">meshBasicMaterial specified</figcaption>
 * </figure>
 *
 <p>Usage: </p>
 * <ul>
 *  <li>To install {@link https://www.npmjs.com/package/jsdoc jsdoc},
 * {@link https://www.npmjs.com/package/vite Vite},
 * yarn and {@link https://pnpm.io pnpm}:</li>
 *  <ul>
 *    <li>sudo npm install --global vite</li>
 *    <li>sudo npm install --global yarn</li>
 *    <li>sudo npm install -g jsdoc</li>
 *    <li>sudo npm install -g pnpm</li>
 *  </ul>
 *  <li>To run the version with modules and Node.js version
 *     {@link https://nodejs.org/en/blog/release/v18.19.0 18} or
 *     {@link https://nodejs.org/en/blog/release/v20.10.0 20}:</li>
 *  <ul>
 *    <li>cd cubes-app</li>
 *    <li>{@link https://www.npmjs.com npm} or {@link https://yarnpkg.com yarn} install</li>
 *    <li>{@link https://www.npmjs.com npm} run dev -- --host (for using vite) <br> or
 *        {@link https://www.npmjs.com npm} start <br> or
 *        {@link https://yarnpkg.com/package/react yarn} start</li>
 *  </ul>
 *  <li>To use vercel {@link https://vercel.com/docs/cli cli} to run
 *  vercel {@link https://vercel.com/docs/cli/dev dev} before deploying:</li>
 *  <ul>
 *    <li>cd cubes-app</li>
 *    <li>{@link https://pnpm.io pnpm} i -g vercel or
 *        {@link https://pnpm.io pnpm} i -g vercel@latest (to update to the latest version)</li>
 *    <li>{@link https://www.npmjs.com npm} or {@link https://yarnpkg.com yarn} install</li>
 *    <li>{@link https://vercel.com vercel} dev</li>
 *  </ul>
 * </ul>
 *
 * @author Paulo Roma
 * @since 10/10/2024
 * @see <a href="../src/App.jsx">source</a>
 * @see {@link https://krotalias.github.io/cubes-app-git/doc-cubes-vite/index.html source2}
 * @see <a href="https://cubes-app.vercel.app/">link</a>
 * @see {@link https://krotalias.github.io/cubes-app-git/ link2}
 * @see <a href="../package.json">package.json</a>
 * @see {@link https://codesandbox.io/p/sandbox/sfypdx original code}
 * @see <iframe title="Cubes" src="https://cubes-app.vercel.app/" style="position: relative; right: 40px; margin-bottom: 0px; transform: scale(0.85); width: 380px; height: 380px"></iframe>
 */

import { useRef, useState, useEffect, Suspense } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import {
  Bounds,
  OrbitControls,
  Text,
  Text3D,
  useMatcapTexture,
  Center,
  useTexture,
  Decal,
} from "@react-three/drei";
import "./index.css";

/**
 * Three.js module.
 * @external THREE
 * @see {@link https://threejs.org/docs/#manual/en/introduction/Installation Installation}
 * @see {@link https://discoverthreejs.com DISCOVER three.js}
 * @see {@link https://riptutorial.com/ebook/three-js Learning three.js}
 */

/**
 * <p>React</p>
 * The library for web and native user interfaces.
 * @external react
 * @see {@link https://react.dev/ React Top-Level API}
 * @see {@link https://react.dev/reference/react/Suspense Suspense}
 * @see {@link https://react.dev/reference/react/useState useState}
 * @see {@link https://react.dev/reference/react/useRef useRef}
 * @see {@link https://react.dev/reference/react/useEffect useEffect}
 */

/**
 * <p>React DOM.</p>
 * The react-dom package contains methods that are only supported
 * for the web applications (which run in the browser DOM environment).
 * <p>They are not supported for React Native.</p>
 * @external react-dom
 * @see {@link https://react.dev/reference/react-dom React DOM APIs}
 */

/**
 * <p>A React renderer for three.js.</p>
 * Build your scene declaratively with re-usable,
 * self-contained components that react to state,
 * are readily interactive and can participate in React's ecosystem.
 * @external react-three/fiber
 * @see {@link https://r3f.docs.pmnd.rs/api/canvas Canvas}
 * @see {@link https://r3f.docs.pmnd.rs/api/events Events}
 * @see {@link https://r3f.docs.pmnd.rs/api/hooks Hooks}
 * @see {@link https://r3f.docs.pmnd.rs/getting-started/introduction R3F introduction}
 * @see {@link https://byteofdev.com/posts/how-to-use-esm/ How to use ESM}
 * @see {@link https://www.youtube.com/watch?v=DPl34H2ISsk I wish I knew this before using React Three Fiber}
 * @see {@link https://r3f.docs.pmnd.rs/tutorials/how-it-works How does it work?}
 * @see {@link https://dev.to/studio_hungry/notes-on-react-three-fiber-4f8g Notes on react-three-fiber}
 * @see {@link https://codyb.co/articles/a-technical-breakdown-of-react-three-fiber A technical breakdown of react-three-fiber}
 */

/**
 * A growing collection of useful helpers and fully functional,
 * ready-made abstractions for @react-three/fiber.
 * @external react-three/drei
 * @see {@link https://github.com/pmndrs/drei drei}
 * @see {@link https://drei.docs.pmnd.rs/cameras/perspective-camera PerspectiveCamera}
 * @see {@link https://drei.docs.pmnd.rs/controls/introduction Controls}
 * @see {@link https://sbcode.net/react-three-fiber/orbit-controls/ OrbitControls}
 * @see {@link http://drei.docs.pmnd.rs/misc/select Select}
 * @see {@link https://drei.docs.pmnd.rs/staging/bounds Bounds}
 * @see {@link http://drei.docs.pmnd.rs/staging/environment Environment}
 * @see {@link http://drei.docs.pmnd.rs/staging/lightformer Lightformer}
 * @see {@link http://drei.docs.pmnd.rs/abstractions/text Text}
 * @see {@link https://drei.docs.pmnd.rs/abstractions/text3d#text3d Text3D}
 * @see {@link http://drei.docs.pmnd.rs/abstractions/decal#decal Decal}
 * @see {@link http://drei.docs.pmnd.rs/staging/matcap-texture-use-matcap-texture#matcaptexture-/-usematcaptexture useMatcapTexture}
 */

/**
 * <p>Color table.</p>
 * RGB primary colors and their
 * {@link https://en.wikipedia.org/wiki/Complementary_colors complementary}
 * colors, CYM, used for printing.
 * @type {Object<Number, String>}
 */
const colors = {
  0: "red",
  2: "green",
  4: "blue",
  1: "cyan",
  3: "magenta",
  5: "yellow",
  6: "orange",
  7: "hotpink",
};

/**
 * Number of colors used for shading cubes.
 * @type {Number}
 */
const ncolors = Object.keys(colors).length - 2;

/**
 * Box component.
 * @param {Object} props information that you pass to a JSX tag.
 * @param {React.MutableRefObject} props.color.State color state.
 * @param {Array<Number>} props.position box position.
 * @param {String} props.name box name.
 * @returns {ThreeElements} view as regular three.js elements expressed in JSX.
 */
function Box({ colorState, position, name } = props) {
  /**
   * This reference will give us direct access to a Box mesh.
   * @type {React.MutableRefObjec}
   * @global
   */
  const meshRef = useRef();

  const [color, setColor] = colorState;

  /**
   * Set up the clicked and active states.
   * States are pairs with an stateful value,
   * and a function to update it.
   */
  const [clicked, setClick] = useState(false);
  const [active, setActive] = useState(false);
  const root = document.querySelector(":root");
  /**
   * Element identified by "#output".
   * @type {Element}
   * @global
   */
  const output = document.querySelector("#output");
  const [pmndrsImg, reactImg, threeImg] = useTexture([
    "./pmndrs.png",
    "./react.png",
    "./three.png",
  ]);

  /**
   * Returns the next color index (key) in the range [0, {@link ncolors}] from the {@link colors color Object}.
   * @global
   * @param {Number} c color index.
   * @returns {Number} next color index.
   */
  const nextColor = (c) => (c >= ncolors ? 0 : (1 + c) % ncolors);

  /**
   * This hook gives you access to the state model which contains
   * the default renderer, the scene, your camera, and so on.
   * It also gives you the current size of the canvas in screen and viewport coordinates.
   * @function useThree
   * @memberof external:react-three/fiber
   */

  /**
   * <p>Subscribe this component to the render-loop, to rotate the mesh in each frame.</p>
   * This hook allows you to execute code on every rendered frame,
   * like running effects, updating controls, and so on.
   * You receive the state (same as useThree) and a clock delta.
   * Your callback function will be invoked just before a frame is rendered.
   * When the component unmounts it is unsubscribed automatically from the render-loop.
   * @function useFrame
   * @memberof external:react-three/fiber
   */
  useFrame((state, delta) => (meshRef.current.rotation.x += delta));

  /**
   * Sets the {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML innerHTML}
   * and {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style style.color}
   * properties of the Element {@link output}.
   * @global
   * @param {String} txt output setter id.
   * @param {Number} cor color index.
   */
  const setOutput = (txt, cor) => {
    if (output) {
      output.style.color = colors[cor];
      output.innerHTML = `${txt}<br /> name: ${meshRef.current.name}, color: ${cor} → ${colors[cor]}`;
    }
  };

  /**
   * <p>React {@link https://react.dev/reference/react/useState useState}
   * hook is asynchronous!</p>
   * <p>Basically, you don't get update value right after updating state.</p>
   *
   * The {@link https://react.dev/reference/react/useEffect useEffect}
   * hook executes after the function returns
   * the generated component instance within it,
   * which means that any ref or state will be assigned before
   * the useEffect hook gets called.
   *
   * <p>This code will always use the latest value of clicked,
   * which will be used in the next draw.</p>
   *
   * @function useEffect
   * @memberof external:react
   *
   * @see {@link https://making.close.com/posts/state-management-with-async-functions The Pitfalls of useState with Asynchronous Functions in React}
   * @see {@link https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia React useState hook is asynchronous!}
   */
  useEffect(() => {
    const cor = color === false ? ncolors : nextColor(color);
    setColor(cor);
    setOutput("useEffect", cor);
    console.log(
      `useEffect: clicked ${clicked}, name: ${meshRef.current.name}, color: ${cor} → ${colors[cor]}`,
    );
  }, [clicked]);

  return (
    <mesh
      position={position}
      name={name}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      /**
       * Gets the picked (clicked) object (Box) and sets its color.
       * <p>Fires after both the mousedown and mouseup events have fired in that order.</p>
       * The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML innerHTML}
       * property of the Element {@link output} is also updated.
       * @param {PointerEvent} event pointer ThreeEvent.
       * @event click
       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event Element: click event}
       */
      onClick={(event) => {
        const cubeName = event.eventObject.name;
        event.stopPropagation();
        setActive(!active);
        // either way does work
        if (cubeName.includes("1")) {
          setClick(!clicked);
        } else {
          // functional update
          setColor((prevColor) => nextColor(prevColor));
          const cor = nextColor(color);
          setOutput("functional update", cor);
        }
      }}
      /**
       * Gets the hovered object (Box) and sets its color.
       * <p>Fired when a pointing device is moved into an element's hit test boundaries.</p>
       * The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML innerHTML}
       * property of the Element {@link output} is also updated.
       * @param {PointerEvent} event pointer ThreeEvent.
       * @event pointover
       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerover_event Element: pointerover event}
       */
      onPointerOver={(event) => {
        setColor(ncolors + 1);
        setOutput("Hovered", ncolors + 1);
      }}
      /**
       * Gets the unhovered object (Box) and sets its color.
       * <p>Fired when a pointing device is moved out of the hit test boundaries of an element.</p>
       * The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML innerHTML}
       * property of the Element {@link output} is also updated.
       * @param {PointerEvent} event pointer ThreeEvent.
       * @event pointout
       * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerout_event Element: pointerout event}
       */
      onPointerOut={(event) => {
        setColor(ncolors);
        setOutput("Unhovered", ncolors);
      }}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={colors[color]} />
      <Decal position={[0.5, 0, 0]} scale={0.75} map={reactImg} />
      <Decal position={[-0.5, 0, 0]} scale={0.75} map={reactImg} />
      <Decal
        position={[0, 0.5, 0]}
        rotation={[Math.PI / 3, 0, 0]}
        scale={0.75}
        map={threeImg}
      />
      <Decal
        position={[0, -0.5, 0]}
        rotation={[Math.PI / 3, 0, 0]}
        scale={0.75}
        map={threeImg}
      />
      <Decal
        position={[0, 0, 0.5]}
        rotation={[0, 0, 0]}
        scale={0.85}
        map={pmndrsImg}
      />
      <Decal
        position={[0, 0, -0.5]}
        rotation={[0, 0, -Math.PI / 2]}
        scale={0.85}
        map={pmndrsImg}
      />
    </mesh>
  );
}

/**
 * Creates a text with the Box identifier and color used.
 * <pre>
 *    Box 1 (Text)
 *  color: 5 → yellow
 * </pre>
 * @param {String} txt text.
 * @param {Number} color text color index.
 * @returns {String} composed text.
 */
function createText(txt, color) {
  const arrow = txt.includes("3D") ? "-->" : "→";
  const cor = colors[color];
  const str = `${color} ${arrow} ${cor}`;
  const len = Math.abs(7 + str.length - txt.length);

  return `${" ".repeat(len) + txt}\ncolor: ${str}`;
}

/**
 * Display a {@link external:react-three/drei 3D text}.
 * @param {Object} props information that you pass to a JSX tag.
 * @param {Array<Number>} props.position text position.
 * @param {String} props.txt text.
 * @param {String} props.color text color.
 * @returns {ThreeElements} view as regular three.js elements expressed in JSX.
 * @see {@link https://codesandbox.io/p/sandbox/r3f-drei-3d-text-de86ih?file=%2Fsrc%2FApp.js%3A35%2C15-35%2C27 3f-drei-3d-text}
 */
function DisplayText3D({ position, txt, color } = props) {
  const { viewport } = useThree();

  const w = viewport.width;
  const h = viewport.height;
  const d = Math.min(w, h);
  const fsize = Math.max(d / 30, 0.08);

  // const [matcapTexture] = useMatcapTexture("CB4E88_F99AD6_F384C3_ED75B9");
  // <meshMatcapMaterial color={cor} matcap={matcapTexture} />
  return (
    <Text3D
      position={position}
      scale={[1, 1, 0.1]}
      size={fsize}
      maxWidth={[w / 5, h * 2, 1]}
      font={"./helvetiker_regular.typeface.json"}
      curveSegments={24}
      bevelEnabled
      bevelSegments={1}
      bevelSize={0.005}
      bevelThickness={0.03}
      height={0.5}
      lineHeight={1.9}
      letterSpacing={0.02}
    >
      {txt}
      <meshStandardMaterial color={color} />
    </Text3D>
  );
}

/**
 * Display a {@link external:react-three/drei text}.
 * @param {Object} props information that you pass to a JSX tag.
 * @param {Array<Number>} props.position text position.
 * @param {String} props.txt text.
 * @param {String} props.color text color.
 * @returns {ThreeElements} view as regular three.js elements expressed in JSX.
 */
function DisplayText({ position, txt, color } = props) {
  const { viewport } = useThree();
  const d = Math.min(viewport.width, viewport.height);
  const fsize = Math.max(d / 15, 0.18);

  return (
    <Text
      position={position}
      fontSize={fsize}
      color={color}
      anchorX="center"
      anchorY="middle"
    >
      {txt}
    </Text>
  );
}

/**
 * <p>Returns a {@link https://legacy.reactjs.org/docs/introducing-jsx.html JSX}
 * element with a R3F canvas.</p>
 * When you want to aggregate data from multiple children or to have two child components communicate with each other,
 * move the state upwards so that it lives in the parent component.
 * The parent can then pass the state back down to the children via props,
 * so that the child components are always in sync with each other and with the parent.
 * All of this is possible because of {@link https://levelup.gitconnected.com/unlocking-the-power-of-closures-in-react-components-ba5903f4710a closures}.
 * @module
 * @function App
 * @returns {HTMLCanvasElement} R3F {@link external:react-three/fiber Canvas}.
 */
export default function App() {
  const cs1 = useState(false);
  const cs2 = useState(false);
  return (
    <>
      <div id="output"></div>
      <Canvas camera={{ fov: 35, position: [0, 0, 4] }}>
        <OrbitControls />
        <ambientLight intensity={Math.PI / 2} />
        <spotLight
          position={[10, 10, 10]}
          angle={0.15}
          penumbra={1}
          decay={0}
          intensity={Math.PI}
        />
        <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
        <Suspense>
          <Bounds fit clip margin={1.2} damping={0}>
            <Box position={[-1.2, 0, 0]} name={"Box1"} colorState={cs1} />
            <Box position={[1.2, 0, 0]} name={"Box2"} colorState={cs2} />
            <DisplayText
              position={[-1.2, 1.5, 0]}
              txt={createText("Box 1 (Text)", cs1[0])}
              color={colors[cs1[0]]}
            />
            <DisplayText
              position={[1.2, 1.5, 0]}
              txt={createText("Box 2 (Text)", cs2[0])}
              color={colors[cs2[0]]}
            />
            <Center top center>
              <DisplayText3D
                position={[0, 0, 0]}
                txt={"R3F (Text3D)"}
                color={"#C0C0C0"}
              />
            </Center>
          </Bounds>
        </Suspense>
      </Canvas>
    </>
  );
}