Source: scene.js

import { OrbitControl } from "./OrbitControl.js";
import {
    Scene, Vector4, MeshBasicMaterial, ShapeGeometry, ArrayCamera, MeshLambertMaterial, DirectionalLight, PerspectiveCamera, AmbientLight, PointLightHelper, WebGLRenderer, PointLight, BoxGeometry, DodecahedronGeometry, CylinderGeometry,
    SphereGeometry, MeshPhongMaterial, Mesh, PlaneGeometry, Color, PCFSoftShadowMap, Raycaster, Vector2, Vector3, RectAreaLight, AxesHelper
} from "./three.js";
import { shapeStore } from "./Shapes3D.js";

/**
 * Represents a scene in the application.
 * @class
 */
const scene = new Scene();
/**
 * Represents the camera used in the scene.
 * @type {PerspectiveCamera}
 */
const camera = new PerspectiveCamera();
scene.background = new Color("rgb(188,244,250)");
/**
 * Represents the global light in the scene.
 * @type {AmbientLight}
 */
const globalLight = new AmbientLight(0xeeeeee);
scene.add(globalLight);

/**
 * Represents a light source in the scene.
 * @type {PointLight}
 */
const light = new PointLight(0xBCF4FA, 15, 0);
light.castShadow = true;
/**
 * Represents a helper for a point light.
 * @type {PointLightHelper}
 */
const helper = new PointLightHelper(light, 2);
scene.add(light);
scene.add(helper);
light.intensity = 0.5;
light.position.set(0, 0, 1).normalize();

/**
 * The WebGL renderer used for rendering the scene.
 * @type {WebGLRenderer}
 */
const renderer = new WebGLRenderer({ antialias: true });

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFSoftShadowMap;
renderer.setClearColor(0x999999);

let resizeObeserver;
let firstPlacementCoord = null;
let currentShapePlacements = [];

/**
 * Represents a collection of input shapes.
 * @typedef {Object} inputShapes
 * @property {Function} get - Retrieves the input shapes.
 * @property {Function} add - Adds a shape to the collection.
 * @property {Function} clear - Clears the collection of input shapes.
 * @property {Array} store - The array that stores the input shapes.
 */
export let inputShapes = {
    get() {
        return this.store;
    },
    add(shape_name) {
        this.store.push(shape_name);
    },
    clear() {
        this.store = [];
    },
    store: []
};

export let inputCoords = {
    get() {
        return this.store;
    },
    add(coord) {
        this.store.push(coord);
    },
    clear() {
        this.store = [];
    },
    store: []
};

/**
 * Object representing the colours used in the scene.
 * @typedef {Object} Colours
 * @property {number} A - The hexadecimal value of colour A.
 * @property {number} B - The hexadecimal value of colour B.
 * @property {number} C - The hexadecimal value of colour C.
 * @property {number} D - The hexadecimal value of colour D.
 * @property {number} E - The hexadecimal value of colour E.
 * @property {number} F - The hexadecimal value of colour F.
 * @property {number} G - The hexadecimal value of colour G.
 * @property {number} H - The hexadecimal value of colour H.
 * @property {number} I - The hexadecimal value of colour I.
 * @property {number} J - The hexadecimal value of colour J.
 * @property {number} K - The hexadecimal value of colour K.
 * @property {number} L - The hexadecimal value of colour L.
 */
const Colours = {
    A: 0x228B1E,
    B: 0x6D359A,
    C: 0x1E9195,
    D: 0x931515,
    E: 0xA2A42C,
    F: 0x9F1B92,
    G: 0x904512,
    H: 0x0E2B0C,
    I: 0x272899,
    J: 0x966E9A,
    K: 0x205F90,
    L: 0x9DA15E,
};

/**
 * Represents a number of spheres in a shape object.
 * @typedef {Object} ShapeObject
 * @property {number} A - The number of spheres in A.
 * @property {number} B - The number of spheres in B.
 * @property {number} C - The number of spheres in C.
 * @property {number} D - The number of spheres in D.
 * @property {number} E - The number of spheres in E.
 * @property {number} F - The number of spheres in F.
 * @property {number} G - The number of spheres in G.
 * @property {number} H - The number of spheres in H.
 * @property {number} I - The number of spheres in I.
 * @property {number} J - The number of spheres in J.
 * @property {number} K - The number of spheres in K.
 * @property {number} L - The number of spheres in L.
 */

/**
 * The shape object.
 * @type {ShapeObject}
 */
const shape_obj = {
    "A": 5,
    "B": 5,
    "C": 5,
    "D": 4,
    "E": 5,
    "F": 5,
    "G": 4,
    "H": 4,
    "I": 5,
    "J": 5,
    "K": 3,
    "L": 5,
};

/**
 * Object representing the placement of shapes.
 * @type {Object.<string, number>}
 */
const shape_placed_obj = {
    "A": 0,
    "B": 0,
    "C": 0,
    "D": 0,
    "E": 0,
    "F": 0,
    "G": 0,
    "H": 0,
    "I": 0,
    "J": 0,
    "K": 0,
    "L": 0,
};


/**
 * Initializes the scene with the given canvas.
 * 
 * @param {HTMLCanvasElement} canvas - The canvas element to render the scene on.
 */
export function initialiseScene(canvas) {
    camera.fov = 75;
    camera.near = 0.2;
    camera.far = 300;
    camera.position.z = 18;
    camera.position.x = -0;
    camera.position.y = 0;

    renderer.setSize(canvas.clientWidth, canvas.clientWidth);
    resizeObeserver = new ResizeObserver(entries => {
        entries.forEach(entry => {
            camera.aspect = canvas.clientWidth / canvas.clientHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(canvas.clientWidth, canvas.clientWidth);
        })
    });
    resizeObeserver.observe(canvas);

    const controls = new OrbitControl(camera, renderer.domElement);
    controls.enablePan = false;
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.screenSpacePanning = false;
    controls.maxDistance = 300;

    controls.target = new Vector3(5, 3.8, 5);
    controls.maxPolarAngle = Math.PI / 2;

    function arrayCoordsFromWorldCoords(x, y, height) {
        let layer = Math.round((height - 1) / Math.sqrt(2));
        let x_index;
        let y_index;
        if (layer % 2 === 1) {
            x_index = (x - 1 - 1 * layer) / 2;
            y_index = (y - 1 - 1 * layer) / 2;
        } else {
            x_index = (x - 1 - 1 * layer) / 2;
            y_index = (y - 1 - 1 * layer) / 2;
        }
        return [x_index, y_index, layer];
    }

    /**
     * Sets the input shape and coordinate.
     * If the shape is not already in the inputShapes set, it adds the shape and creates a new array with the given coordinate.
     * If the shape is already in the inputShapes set, it appends the coordinate to the existing array.
     * 
     * @param {string} shape - The shape to set as input.
     * @param {number} coord - The coordinate to set for the shape.
     */
    function setInput(shape, coord) {
        if (!(inputShapes.get().includes(shape))) {
            inputShapes.add(shape);
            inputCoords.add(new Array(coord));
        } else {
            inputCoords.get()[inputShapes.get().indexOf(shape)].push(coord);
        }
    }

    const raycaster = new Raycaster();
    const pointer = new Vector2();


        /**
     * Handles the click event on the canvas.
     * @param {MouseEvent} event - The click event object.
     */
    function onClick(event) {
            const canvasBounds = canvas.getBoundingClientRect();
            pointer.x = ((event.clientX - canvasBounds.left) / canvas.clientWidth) * 2 - 1;
            pointer.y = -((event.clientY - canvasBounds.top) / canvas.clientHeight) * 2 + 1;
            raycaster.setFromCamera(pointer, camera);

            let currentShapeElement = document.getElementById("currentImage");
            let shape = currentShapeElement.className;
            let currentShape = shapeStore[shape]
//            console.log("This is onclick", shape, currentShape.layout.length);

            const intersects = raycaster.intersectObjects(scene.children);

            for (let i = 0; i < intersects.length; i++) {
                if (intersects[i].object.visible === true && intersects[i].object.name[0] === "s" &&
                    intersects[i].object.material.color.equals(new Color(0x999999))) {

                    let coord = arrayCoordsFromWorldCoords(intersects[i].object.position.x, intersects[i].object.position.z, intersects[i].object.position.y);
                    let shapeIndex = inputShapes.get().indexOf(shape);
                    let lastCoord = shapeIndex >= 0 && inputCoords.get()[shapeIndex].length > 0 ? inputCoords.get()[shapeIndex][inputCoords.get()[shapeIndex].length - 1] : null;

                        if (isPlacementValid(coord, currentShape, lastCoord)) {
                            if(updateCounts(shape)) {
                                intersects[i].object.material.color.set(Colours[shape]);
                                setInput(shape, coord);
                                console.log("Placed sphere for shape:", shape, "at coordinates:", coord);
                                firstPlacementCoord = coord;
                                break;
                            }
                        } else {
                            alert("Invalid placement: Sphere is not correctly adjacent.");
                        }
                }
            }
        }

        /**
         * Updates the counts of a given shape.
         * 
         * @param {string} shape - The shape to update the counts for.
         * @returns {boolean} - Returns true if the counts were successfully updated, false otherwise.
         */
        function updateCounts(shape) {
            // Check if the shape exists in shape_obj
            if (shape_obj.hasOwnProperty(shape)) {
                // Check if there are available shapes to place
                if (shape_obj[shape] > 0) {
                    // Subtract count from shape_obj and add to shape_placed_obj
                    shape_obj[shape] -= 1;
                    shape_placed_obj[shape] += 1;
                    return true;
                } else {
                    // Display an alert if shape count is exhausted
                    alert("Shape spheres out of bounds. Please input the same no. of spheres as defined for the piece");
                    return false;
                }
            } else {
                console.error(`Shape ${shape} not found in shape_obj.`);
                return false;
            }
             // Log the updated counts (you can remove this in your actual implementation)
//            console.log("Updated shape_obj:", shape_obj);
//            console.log("Updated shape_placed_obj:", shape_placed_obj);
//            return true;
        }




    /**
     * Checks if a placement coordinate is valid for a given shape.
     * @param {number[]} coord - The coordinate to check.
     * @param {string} shape - The shape being placed.
     * @param {number[]} lastCoord - The last placed coordinate.
     * @returns {boolean} - True if the placement is valid, false otherwise.
     */
    function isPlacementValid(coord, shape, lastCoord) {
        return (
            !lastCoord ||
            (lastCoord[2] === coord[2] && (Math.abs(lastCoord[0] - coord[0]) + Math.abs(lastCoord[1] - coord[1]) === 1)) ||
            (Math.abs(lastCoord[2] - coord[2]) === 1 && lastCoord[0] === coord[0] && lastCoord[1] === coord[1]) ||
            (Math.abs(lastCoord[0] - coord[0]) === 1 && Math.abs(lastCoord[1] - coord[1]) === 1 && lastCoord[2] === coord[2]) ||
            // Diagonal condition
            (Math.abs(lastCoord[0] - coord[0]) === 1 && Math.abs(lastCoord[1] - coord[1]) === 1 && Math.abs(lastCoord[2] - coord[2]) === 1)
        );
}



    window.addEventListener('click', onClick);

    /**
     * Animates the scene by rendering it, updating the controls, and requesting the next animation frame.
     */
    function animate() {
        renderer.render(scene, camera);
        controls.update();
        requestAnimationFrame(animate);
    }

    canvas.appendChild(renderer.domElement);

    const meshfloor = new Mesh(
        new PlaneGeometry(130, 130, 10, 10),
        new MeshPhongMaterial({
            color: 0xBCF4FA,
            wireframe: false
        })
    )
    meshfloor.rotation.x -= Math.PI / 2;
    meshfloor.receiveShadow = true;

    light.position.set(4, 20, 4);

    animate();
}

/**
 * Creates a sphere object with the specified position, color, radius, and number of segments.
 * @param {number} x - The x-coordinate of the sphere's position.
 * @param {number} y - The y-coordinate of the sphere's position.
 * @param {number} z - The z-coordinate of the sphere's position.
 * @param {string} color - The color of the sphere.
 * @param {number} radius - The radius of the sphere.
 * @param {number} segs - The number of segments used to create the sphere.
 * @returns {Mesh} The created sphere object.
 */
function createSphere(x, y, z, color, radius, segs) {
    let mat = new MeshPhongMaterial({
        color: color,
        specular: color,
        shininess: 30
    });
    mat.castShadow = true;
    mat.receiveShadow = true;
    let sphere = new Mesh(new SphereGeometry(radius, segs, segs), mat);
    sphere.position.set(x, z, y);
    sphere.castShadow = true;
    sphere.receiveShadow = true;
    sphere.name = ["s", x, y, z].join(",");
    return sphere;
}

/**
 * Removes a sphere instance from the scene and disposes its material and geometry.
 * @param {Object3D} instance - The sphere instance to dispose.
 */
function disposeSphere(instance) {
    scene.remove(instance);
    instance.material.dispose();
    instance.dispose();
}

/**
 * Represents a Scene object.
 * @class
 */
export default class {
    /**
     * Creates a sphere and adds it to the scene.
     * @param {number} x - The x-coordinate of the sphere.
     * @param {number} y - The y-coordinate of the sphere.
     * @param {number} z - The z-coordinate of the sphere.
     * @param {string} color - The color of the sphere.
     * @param {number} [radius=1] - The radius of the sphere.
     * @param {number} [segs=15] - The number of segments of the sphere.
     * @returns {Object} The created sphere object.
     */
    createSphere(x, y, z, color, radius = 1, segs = 15) {
        return createSphere(x, y, z, color, radius, segs);
    }

    /**
     * Disposes a sphere from the scene.
     * @param {Object} sphere - The sphere object to be disposed.
     */
    disposeSphere(sphere) {
        disposeSphere(sphere);
    }

    /**
     * Adds an object to the scene.
     * @param {Object} obj - The object to be added to the scene.
     */
    add(obj) {
        scene.add(obj);
    }

    /**
     * Initializes the scene with the provided DOM element.
     * @param {HTMLElement} dom - The DOM element to initialize the scene with.
     */
    init(dom) {
        initialiseScene(dom);
    }

    /**
     * Disposes the scene and cleans up resources.
     */
    dispose() {
        resizeObeserver.disconnect();
        cancelAnimationFrame();
    }
};

export { Colours, shape_obj, shape_placed_obj };