Code viewer for World: Lego
/*

      Name:    Michael Walsh
Student No:    20761051
    Module:    CA318 - Advanced Algorithms and AI Search
      Year:    2022/23

*/

// Cloned by Michael Walsh on 14 Nov 2022 from World "webgl_interactive_voxelpainter" by threejs.org porting project 
// Please leave this clone trail here.

//ported from threejs examples
//https://threejs.org/examples/#webgl_interactive_voxelpainter

//load css
AB.loadCSS('/uploads/threeport/main.css'); 

import * as THREE from '/api/threemodule/libs/three.module.js';

let camera, scene, renderer, plane;
let pointer, raycaster;
let rollOverMesh, rollOverMaterial;
let cubeGeo;

const realColors = [
"#f0f8ff", "#00ffff", "#7fffd4", "#f0ffff", "#f5f5dc", "#ffe4c4", "#ffebcd", "#0000ff", "#8a2be2",
"#a52a2a", "#deb887", "#5f9ea0", "#7fff00", "#d2691e", "#ff7f50", "#6495ed", "#fff8dc", "#dc143c",
"#00ffff", "#00008b", "#008b8b", "#b8860b", "#006400", "#bdb76b", "#8b008b", "#556b2f", "#ff8c00",
"#9932cc", "#8b0000", "#e9967a", "#8fbc8f", "#483d8b", "#00ced1", "#9400d3", "#ff1493", "#00bfff",
"#1e90ff", "#b22222", "#228b22", "#ff00ff", "#dcdcdc", "#ffd700", "#daa520", "#008000", "#adff2f",
"#f0fff0", "#ff69b4", "#cd5c5c", "#4b0082", "#fffff0", "#f0e68c", "#e6e6fa", "#fff0f5", "#7cfc00",
"#fffacd", "#add8e6", "#f08080", "#e0ffff", "#fafad2", "#90ee90", "#ffb6c1", "#ffa07a", "#20b2aa",
"#87cefa", "#b0c4de", "#ffffe0", "#00ff00", "#32cd32", "#faf0e6", "#ff00ff", "#800000", "#66cdaa",
"#0000cd", "#ba55d3", "#9370db", "#3cb371", "#7b68ee", "#00fa9a", "#48d1cc", "#c71585", "#191970",
"#f5fffa", "#ffe4e1", "#ffe4b5", "#000080", "#fdf5e6", "#808000", "#6b8e23", "#ffa500", "#ff4500",
"#da70d6", "#eee8aa", "#98fb98", "#afeeee", "#db7093", "#ffefd5", "#ffdab9", "#cd853f", "#ffc0cb",
"#dda0dd", "#b0e0e6", "#800080", "#663399", "#ff0000", "#bc8f8f", "#4169e1", "#8b4513", "#fa8072",
"#f4a460", "#2e8b57", "#fff5ee", "#a0522d", "#c0c0c0", "#87ceeb", "#6a5acd", "#fffafa", "#00ff7f",
"#4682b4", "#d2b48c", "#008080", "#d8bfd8", "#ff6347", "#40e0d0", "#ee82ee", "#f5deb3", "#ffff00",
"#9acd32"];

const objects = {};
let worldHasStarted = false;
let worldId;

let theta;
let iota;
let distance_to_y_axis;
let distance_to_origin;

function init() {
    AB.runReady = false;
    
    AB.msg (`<ul style="padding-left: 2ch;">`
        + `<li>Choose building colour: <input id="user_colour" type="color" value="`
        + AB.randomElementOfArray(realColors) + `" class="ab-largenormbutton"></li>`
        + `<li>Rotate view using arrow keys</li>`
        + `<li>Zoom in and out using a and z</li>`
        + `<li>Add bricks by clicking</li>`
        + `<li>Remove bricks from click + shift</li></ul>`);

    // start the scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf0f0f0);

    // roll-over helpers
    const rollOverGeo = new THREE.BoxGeometry(50, 50, 50);
    rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, transparent: true });
    rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial);
    rollOverMesh.visible = false;
    scene.add(rollOverMesh);

    // cubes
    cubeGeo = new THREE.BoxGeometry(50, 50, 50);

    // grid
    const gridHelper = new THREE.GridHelper(1000, 20);
    scene.add(gridHelper);

    // these organise the what square we are looking at
    raycaster = new THREE.Raycaster();
    pointer = new THREE.Vector2();
    const geometry = new THREE.PlaneGeometry(1000, 1000);
    geometry.rotateX(-Math.PI / 2);

    plane = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ visible: false }));
    scene.add(plane);
    objects[0] = plane;

    // lights
    const ambientLight = new THREE.AmbientLight(0x606060);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(1, 0.75, 0.5).normalize();
    scene.add(directionalLight);

    // renderer
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(document.body.clientWidth, document.body.clientHeight);
    document.body.appendChild(renderer.domElement);

    // start camera, set default angles and distance to origin
    camera = new THREE.PerspectiveCamera(45, document.body.clientWidth / document.body.clientHeight, 1, 10000);
    distance_to_y_axis = 1392;
    distance_to_origin = 1526;
    theta = Math.atan(500 / 1300);
    iota = Math.atan(800 / 1300);
    
    // this sets the camera position and runs render()
    roll(0);

    document.addEventListener('pointermove', onPointerMove);
    document.addEventListener('pointerdown', onPointerDown);
    document.addEventListener('keydown', onDocumentKeyDown);
    window.addEventListener('resize', onWindowResize);

    AB.runReady = true;

    // https://stackoverflow.com/questions/7307983/while-variable-is-not-defined-wait

    // This nifty piece of code waits for the socket to be available
    // I couldn't find a event handler for this
    Object.defineProperty(AB, "socket", {
        configurable: true,
        set(v) {
            Object.defineProperty(AB, "socket", { value: v });
            AB.socketOut ({  
                action: 'request_world',
            });
        }
    });
}

// horizontal rotation
function yaw(rad) {
    theta += rad;
    
    theta = theta % (Math.PI * 2);

    camera.position.setX(distance_to_y_axis * Math.sin(theta));
    camera.position.setZ(distance_to_y_axis * Math.cos(theta));

    camera.lookAt(0, 0, 0);
    render();
}

// vertical rotation
function roll(rad) {
    iota += rad;
    iota = iota % (Math.PI * 2);

    // In Australia everything is upside down
    let flipped = Math.abs(iota) > (Math.PI / 2) && Math.abs(iota) < ( 3 * Math.PI / 2);
    camera.up.setY(flipped ? -1 : 1);
    
    distance_to_y_axis = distance_to_origin * Math.cos(iota);
    camera.position.setY(distance_to_origin * Math.sin(iota));

    yaw(0);
}

// adjust distance from origin
function zoom(change) {
    distance_to_origin += change;
    roll(0);
}


function onWindowResize() {
    renderer.setSize(document.body.clientWidth, document.body.clientHeight);
    camera.aspect = document.body.clientWidth / document.body.clientHeight;
    camera.updateProjectionMatrix();
}

function getFirstIntersectedObject() {
    let intersects = raycaster.intersectObjects(Object.values(objects), false);
    if(intersects.length > 0) return intersects[0];
    else return null;
}

function onPointerMove(event) {
    pointer.set((event.clientX / document.body.clientWidth) * 2 - 1, - (event.clientY / document.body.clientHeight) * 2 + 1);

    raycaster.setFromCamera(pointer, camera);

    const intersect = getFirstIntersectedObject();

    if(intersect) {
        rollOverMesh.position.copy(intersect.point).add(intersect.face.normal);
        rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);
        rollOverMesh.visible = true;
    } else {
        rollOverMesh.visible = false;
    }
    render();
}

function onPointerDown(event) {

    pointer.set((event.clientX / document.body.clientWidth) * 2 - 1, - (event.clientY / document.body.clientHeight) * 2 + 1);

    raycaster.setFromCamera(pointer, camera);

    const intersect = getFirstIntersectedObject();

    if(!intersect) return;

    // delete cube
    if(event.shiftKey) {
        if (intersect.object !== plane) {
            scene.remove(intersect.object);
            delete objects[intersect.object.position.toArray().join('|')];

            AB.socketOut ({  
                action: 'remove',
                worldId: worldId,
                position: intersect.object.position,
            });
        }

    // create cube
    } else {
        let color = document.getElementById('user_colour').value;
        let position = new THREE.Vector3();
        position.copy(intersect.point).add(intersect.face.normal);
		position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);

        if(!worldHasStarted) {
            worldId = Math.floor(Math.random() * 9999999999999999);
            worldHasStarted = true;
        }

        addBrick(position, color);

         AB.socketOut ({  
            action: 'add',
            worldId: worldId,
            position: position,
            color: color,
        }); 
    }

    render();
}

function onDocumentKeyDown(event) {
    switch (event.code) {
        case "ArrowLeft":
            yaw(-0.1);
            break;  
        case "ArrowRight":
            yaw(0.1);
            break;
        case "ArrowUp":
            roll(0.1);
            break;
        case "ArrowDown":
            roll(-0.1);
            break;
        case "KeyA":
            zoom(-20);
            break;
        case "KeyZ":
            zoom(20);
            break;
    }
}

function render() {
    renderer.render(scene, camera);
}


function addBrick(vector, color) {
    // don't allow vectors underneath the grid
    if(vector.y < 0) return;

    let cubeMaterial = new THREE.MeshLambertMaterial({
        color: color,
        opacity: 0,
        transparent: false
    });
    
    const brick = new THREE.Mesh(cubeGeo, cubeMaterial);
    brick.position.copy(vector);
    scene.add(brick);

    objects[brick.position.toArray().join('|')] = brick;
}

function removeBrick(pos) {
    let key = pos.x + "|" + pos.y + "|" + pos.z;
    let todel = objects[key];
    if(todel) {
        scene.remove(todel);
        delete objects[key];
    }
}

AB.socketIn = function(data)
{
    if (! AB.runReady) return;

    switch(data.action) {
    case 'add':
        if(worldHasStarted && worldId == data.worldId) {
            addBrick(data.position, data.color);
            render();
        }
        break;
    case 'remove':
        if(worldHasStarted && worldId == data.worldId) {
            removeBrick(data.position);
            render();
        }
        break;
    case 'request_world':
        if (worldHasStarted) {
            console.log("Received a request for a copy of this world");
            
            let o = Object.values(objects);
            let bricks = [];
            for(let i = 0; i < o.length; i++) {
                if(o[i] !== plane)
                    bricks.push([o[i].position, o[i].material.color]);
            }

            AB.socketOut ({  
                action: 'sending_world',
                id: worldId,
                bricks: bricks,
            });
        }

        break;
    case 'sending_world':
        console.log("Received a copy of a world from another user");
        
        if(!worldHasStarted) {
            worldId = data.id;
            worldHasStarted = true;

            for(let i = 0; i < data.bricks.length; i++) {
                addBrick(data.bricks[i][0], data.bricks[i][1]);
            }
        }
        render();
        break;
    }
};

AB.socketStart();
init();