Source: src/project.js

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GroundProjectedSkybox } from 'three/addons/objects/GroundProjectedSkybox.js';
import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
import { Context } from '/js/src/context.js';
import { Handlers } from './handlers';

/**
 * The Project Module.
 * @module Project
 */
const Project = (() => {

  /**
   * The Context constant provides a neutral scope to use as part of the state handling tasks.
   * @name module:Project.context
   * @private
   */
  const context = Context;

  /**
   * The Stage set function prepares the three js environment that will run the scenes.
   * @alias module:Project.setStage
   * @returns {THREE.Scene} scene
   */
  const stage_set = () => {
    console.group("stage_set");
    // Scene
    const scene = new THREE.Scene();

    // Name the main scene
    scene.name = "Main scene";

    scene.background = new THREE.Color(0x443333);
    scene.fog = new THREE.Fog(0xcdcdcd, 0.3, 22);

    stage_set_skydome();

    // Add props
    // const stage_add_props;
    // Ground
    // const plane = new THREE.Mesh(
    //   new THREE.PlaneGeometry(8, 8),
    //   new THREE.MeshPhongMaterial({ color: 0xcbcbcb, specular: 0x101010 })
    // );
    // plane.rotation.x = - Math.PI / 2;
    // plane.position.y = -0.0015;
    // plane.receiveShadow = true;
    //scene.add( plane );

    // Spotlight
    const spotLight = new THREE.SpotLight();
    spotLight.intensity = .7;
    spotLight.angle = Math.PI * 2;
    spotLight.penumbra = 0.5;
    spotLight.castShadow = true;
    spotLight.position.set(1, 2, 2);
    spotLight.lookAt(0, 0, 0);
    scene.add(spotLight);
    const spotLight2 = new THREE.SpotLight();
    spotLight2.intensity = .7;
    spotLight2.angle = Math.PI * 2;
    spotLight2.penumbra = 0.5;
    spotLight2.castShadow = true;
    spotLight2.position.set(-1, 2, -2);
    spotLight2.lookAt(0, 0, 0);
    scene.add(spotLight2);
    const spotLight3 = new THREE.SpotLight();
    spotLight3.intensity = .7;
    spotLight3.angle = Math.PI * 2;
    spotLight3.penumbra = 0.5;
    spotLight3.castShadow = true;
    spotLight3.position.set(2, 2, 1);
    spotLight3.lookAt(0, 0, 0);
    scene.add(spotLight3);
    const spotLight4 = new THREE.SpotLight();
    spotLight4.intensity = .7;
    spotLight4.angle = Math.PI * 2;
    spotLight4.penumbra = 0.5;
    spotLight4.castShadow = true;
    spotLight4.position.set(-1, 2, 2);
    spotLight4.lookAt(0, 0, 0);
    scene.add(spotLight4);

    // // Ambient light
    const AmgLight = new THREE.AmbientLight(0xffffff, 1);
    scene.add(AmgLight);

    // Rect Area Light
    const width = 10;
    const height = 10;
    const intensity = 1.5;
    const rectLight = new THREE.RectAreaLight(0xffffff, intensity, width, height);
    rectLight.position.set(5, 5, 0);
    rectLight.lookAt(0, 1.5, 0);
    scene.add(rectLight)
    // const rectLightHelper = new RectAreaLightHelper(rectLight);
    // rectLight.add(rectLightHelper);

    const EMLight = new THREE.HemisphereLight(0xffacac, 0xcdcdcd, 1);
    EMLight.position.set(2, 5, 2);
    EMLight.lookAt(0, 0, 0);
    scene.add(EMLight);

    // Directional Light(s)
    const dirLight1 = new THREE.DirectionalLight(0xffacac, 3);
    dirLight1.position.set(4, 4, 4);
    dirLight1.lookAt(0, 0.5, 0);
    dirLight1.castShadow = true;
    //Set up shadow properties for the light
    dirLight1.shadow.mapSize.width = 4096; // default
    dirLight1.shadow.mapSize.height = 4096; // default
    dirLight1.shadow.camera.near = 0.001; // default
    dirLight1.shadow.camera.far = 1000; // default
    dirLight1.shadow.radius = 15;
    dirLight1.shadow.blurSamples = 25;
    scene.add(dirLight1);
    // const helper = new THREE.DirectionalLightHelper(dirLight1,3);
    // scene.add(helper);

    // const dirLight2 = new THREE.DirectionalLight(0xcdcdcd, .7);
    // dirLight2.position.set(-1, 3, 1);
    // dirLight2.position.set(-1, 3, 1);
    // dirLight2.lookAt(0,0,0);
    // scene.add(dirLight2);

    // Add to context: scene
    context.add({ 'name': "scene", 'obj': scene });

    console.groupEnd();
    return scene;
  };

  /**
   * In the early setup the change sky Handler is still not capable of adapting scene elements, we must use direct calls.
   * @private
   * @function
   */
  const stage_set_skydome = () => {
    console.group("stage_set_skydome");
    const tex_src = context.get.config.skydomeTexturesDefaultDir + context.get.config.skydome.textures[
      context.get.config.sceneDefaultStage
    ];

    console.log("stage_set_skydome started", tex_src);
    // EnvMap and skybox
    try {
      // Load an equirect and set
      new THREE.TextureLoader().load(tex_src,
        //'/hdri/kloofendal_48d_partly_cloudy_puresky_4k_web.jpg',
        texture => {
          texture.name = "Original skydome";
          console.log(`start ${texture.name}`);
          texture.wrapS = THREE.RepeatWrapping;
          texture.repeat.x = -1;
          texture.mapping = THREE.EquirectangularReflectionMapping;
          texture.colorSpace = THREE.SRGBColorSpace;
          texture.flipY = true;
          context.get.scene.background = texture;
          context.get.scene.environment = texture;

          // Use skybox addon
          const skybox = new GroundProjectedSkybox(texture);
          skybox.name = "Original Skybox";
          skybox.scale.setScalar(100);
          skybox.height = 20;
          skybox.radius = 200;
          skybox.visible = false;
          context.get.scene.add(skybox);

          // Add to context: envMap
          context.add({ "name": "envMap", "obj": texture });

          // Add to context: skybox
          context.add({ "name": "skybox", "obj": skybox });
        }
      );
    } catch (e) {
      console.log(`Environment Map: ${e.message}`);
    }
    console.groupEnd();
  };

  /**
   * The camera is created from standarized defaults, See {@link module:Context}.
   * @alias module:Project.setCamera
   * @returns {THREE.Camera} camera
   */
  const camera_set = () => {
    console.group("camera_set");
    // Import from context: config
    const config = context.get.config;

    // Import from context: coords
    const coords = context.coords;

    // Camera
    const camera = new THREE.PerspectiveCamera(
      config.cameraFov,
      config.cameraRatio,
      config.cameraNearFrustum,
      config.cameraFarFrustum,
    );

    // Camera offset
    const camera_pos = new THREE.Vector3().addVectors(coords.cam_offset, coords.target);
    console.log("Camera POS:", camera_pos);
    
    // Camera Position
    const {x,y,z} = camera_pos;
    camera.position.set(x, y, z);

    // Export camera
    window.THREE_camera = camera;

    // Add to context: camera
    context.add({ 'name': "camera", 'obj': camera });

    console.groupEnd();
    return camera;
  };
/**
   * The Orbit Control system is used and instantiated from default values, See {@link module:Context}.
   * @alias module:Project.setControls
   * @returns {OrbitControls} controls
   * @async
   */
  const controls_set = async () => {
    console.group("controls_set");
    const controls_promise = new Promise((resolve, reject) => {
      console.log("controls start");
      const controls = new OrbitControls(
        context.get.camera,
        context.get.renderer.domElement
      );
      controls.listenToKeyEvents(window); // Optional.
      controls.enableDamping = true;
      controls.enablePan = false;
      controls.dampingFactor = 0.05;
      controls.screenSpacePanning = false;
      controls.minDistance = context.get.config.controlsMinDistance;
      controls.maxDistance = context.get.config.controlsMaxDistance;
      controls.maxPolarAngle = context.get.config.controlsMaxPolarAngle;
      controls.target = context.coords.target;
      controls.autoRotateSpeed = 3;
      controls.autoRotate = false;

      console.log("controls promise");
      if (controls) {
        resolve(controls);
      } else {
        reject((error) => {
          console.log(error.message);
        });
      }
    });

    controls_promise.then(
      (controls) => {
        console.log(["Controls Set", controls]);

        // Add event listeners
        controls.addEventListener('change', (e) => { Handlers.onControlsChange(e) });
        controls.addEventListener('start', (e) => { Handlers.onControlsStart(e) });
        controls.addEventListener('end', (e) => { Handlers.onControlsEnd(e) });

        // Export controls
        window.gltf_controls = controls;

        // Add to context
        context.add({ 'name': "controls", 'obj': controls });

      }
    ).catch(e => console.log(e.message));

    console.groupEnd();
    // Return controls promise
    return controls_promise;
  };

  /**
   * The Rederer is built from default values, See {@link module:Context}.
   * @alias module:Project.setRenderer
   * @returns {THREE.Renderer} renderer
   * @async
   */
  const renderer_set = async () => {
    console.group("renderer_set");
    const renderer_promise = new Promise((resolve, reject) => {
      const renderer = new THREE.WebGLRenderer();
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      renderer.toneMappingExposure = 1;
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;


      const r_target = document.querySelector("#canvasroot");
      r_target.appendChild(renderer.domElement);
      console.log(`renderer domElement: ${renderer.domElement}`);


      if (renderer) {
        resolve(renderer);
      } else {
        reject((error) => {
          console.log(error.message);
        });
      }
    });

    renderer_promise
      .then(
        (renderer) => {
          // Export renderer
          window.gltf_renderer = renderer;
          context.add({ 'name': 'renderer', 'obj': renderer });

          // On window resize.
          console.log(`Adding onWindowResize to window listeners`);
          window.addEventListener('resize', Handlers.onWindowResize);

          // reference self class
          const self = Project;
          // Recurse into self promise for Controls.
          self.setControls();
        })
      .catch((e) => {
        console.log(e.message);
      });

    console.groupEnd();
    return renderer_promise;
  };

  /**
   * @typedef {Object} ret 
   * @property {THREE.gltf} gltf - The loaded instance of a gltf file.
   * @property {THREE.AnimationMixer} mixer - The animation clip mixer.
   * @property {THREE.GLTFLoader} loader - Instance that can load additional gltf files.
   */
  /**
   * The GLTF file holds props and animations for one or more scenes.  We wait for a promise of the gltf to apply the mixer.
   * @alias module:Project.loadGltfScene
   * @returns {ret} Items created in the promise response.
   * @async
   */
  const gltf_scene_load = async () => {
    console.group("gltf_scene_load");
    const gltf_load_promise = new Promise((resolve, reject) => {
      try {
        // GLTF Loader
        console.log("Loader Starts");
        const loader = new GLTFLoader();
        // // Optional: Provide a DRACOLoader instance to decode compressed mesh data
        // const dracoLoader = new DRACOLoader();
        // dracoLoader.setDecoderPath( '/examples/jsm/libs/draco/' );
        // loader.setDRACOLoader( dracoLoader );


        loader.load(
          '/gltf/animated/animated.gltf',
          gltf => {

            // GLTF Scene
            const model = gltf.scene;

            // Name the Model
            model.name = "Product Scene";

            // Mixer
            const mixer = new THREE.AnimationMixer(model);

            //Add shadow receive/cast to all meshes
            model.traverse(
              node => {
                if (node.isMesh) {
                  if (node.name == 'Sky') {
                    node.material.side = THREE.BackSide;
                  } else {
                    //Culling aware material
                    node.material.side = THREE.FrontSide;
                    node.doubleSided = true;
                  }
                  // Apply Shadows
                  node.castShadow = true;
                  node.receiveShadow = true;
                }
              }
            );

            // Make the model content
            context.get.scene.add(model);

            console.log(`Loader gltf ready for ${model.name}`);

            resolve({ loader, gltf, mixer });
          },
          // called while loading is progressing
          xhr => {

            console.log((xhr.loaded / xhr.total * 100) + '% loaded');

          },
          // called when loading has errors
          error => {
            console.log(`There was an Error Loading the GLTF: ${error.message}`, error);
          }

        );
      } catch (e) {
        console.log(e.message);
        reject(e);
      }
    });

    gltf_load_promise.then((ret) => {

      // Destruct promise resolve
      const { loader, gltf, mixer } = ret;

      // Add to context: mixer
      context.add({ 'name': "mixer", 'obj': mixer });
      // Add to context: gltf
      context.add({ 'name': "gltf", 'obj': gltf });
      // Add to context: loader
      context.add({ 'name': "loader", 'obj': loader });

      // Add Event Listteners for mixer.
      mixer.addEventListener('finished', (e) => { Handlers.onMixerFinished(e) });
      mixer.addEventListener('loop', (e) => { Handlers.onMixerLoop(e) });

      // Init props and stage to default scene
      Handlers.sceneStageAnim('icosphere');

    }).catch((e) => console.log(e.message));
    console.groupEnd();
  };

  // Project return
  return {
    setStage: () => stage_set(),
    setCamera: () => camera_set(),
    setRenderer: () => renderer_set(),
    setControls: () => controls_set(),
    loadGltfScene: () => gltf_scene_load(),
  };
})();

export { Project };