Code viewer for World: Load Cubes

// Cloned by Vanya Cadogan on 5 Feb 2023 from World "One Cube World (Three.js)" by Starter user 
// Please leave this clone trail here.

// function replaceScript(){
//     let scripts = document.getElementsByTagName( "script" );
//     for ( let i = 0; i < scripts.length; i++){
//       if ( scripts[i].src == "https://run.ancientbrain.com/api/threeab30/libs/three.min.js" )
//       {
//           scripts[i].src = "https://ancientbrain.com/uploads/cadogav2/three.r149.min.js";
//       }
//     }
// }

// replaceScript();
// $.getScript ("/uploads/cadogav2/three.r149.min.js", function(){
//         console.log('script loaded THREE');
//         console.log(THREE)
// });


// Load CSS
AB.loadCSS("/uploads/cadogav2/app-styles.css");

AB.maxSteps = Infinity;

console.log("-------------")
console.log(THREE)
console.log(AB)
console.log(ABWorld)
console.log("-------------")

class UI {
    
    constructor(){
        this.selectedMaterial = 1;
    }
    
    init(){
        window.addEventListener("contextmenu", e => e.preventDefault());
        document.getElementById('ab-runheaderbox').remove()
        
        document.write(`<header class="menu-header">
            <nav class="menu-nav">
                <ul>
                    <li class="no-select">File</li>
                    <li class="no-select">Views</li>
                    <li class="no-select">Help</li>
                </ul>

                <ul>
                    <i
                        class="fa-solid fa-arrow-rotate-left disabled"
                        title="Undo"
                    ></i>
                    <i
                        class="fa-solid fa-arrow-rotate-right disabled"
                        title="Redo"
                    ></i>
                </ul>

                <ul>
                    <i class="fa-regular fa-floppy-disk" title="Save"></i>
                    <i class="fa-solid fa-download" title="Download Model"></i>
                </ul>

                <ul>
                    <i class="fa-solid fa-pen" title="Draw Block"></i>
                    <i class="fa-solid fa-brush" title="Paint Blocks"></i>
                    <i class="fa-solid fa-eraser" title="Delete Blocks"></i>
                </ul>

                <ul>
                    <i class="fa-solid fa-palette" title="Materials"></i>
                </ul>

                <ul>
                    <i class="fa-solid fa-camera" title="Camera Views"></i>
                    <i
                        class="fa-solid fa-house"
                        title="Center Camera to Origin"
                    ></i>
                </ul>
            </nav>
        </header>`);
    }
}

class Utils {
    
    static updateMousePosition(mouse, event){
        mouse.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1);
    }
    
    static getMouseGridPosition(mouse, raycaster, floor){
        raycaster.setFromCamera(mouse, ABWorld.camera);
        let intersects = raycaster.intersectObjects([floor]);
        if (intersects.length > 0) {
            const point = intersects[0].point;
            const sqPosition = new THREE.Vector3(Math.floor(point.x), 0, Math.floor(point.z));
            
            return sqPosition;
        }
    }
    
    static getPreviewCubePosition(mouse, raycaster, floor){
        raycaster.setFromCamera(mouse, ABWorld.camera);
        let intersects = raycaster.intersectObjects([floor, ...cubes]);
        
        if (intersects.length > 0) {
            let intersect = intersects[0];
            
            if (intersect.object.userData.type === 'cube'){
                console.log('i cube')
                // Get face normal
                const norm = intersect.face.normal;
                //console.log('norm');
                //console.log(norm);
                const cubePosition = intersect.object.position;
                const rayPosition = new THREE.Vector3(Math.floor(cubePosition.x), Math.floor(cubePosition.y), Math.floor(cubePosition.z));
                //console.log('rayPosition');
                //console.log(rayPosition);
                return rayPosition.add(norm);
            } else if (intersect.object.userData.type === 'floor') {
                console.log('i floor')
                const point = intersect.point;
                const sqPosition = new THREE.Vector3(Math.floor(point.x), 0, Math.floor(point.z));
                
                return sqPosition;
            } else {
                console.log('i undefined')
                return undefined;
            }
        }
    }
    
    static getMouseIntersect(mouse, raycaster, floor){
        raycaster.setFromCamera(mouse, ABWorld.camera);
        let intersects = raycaster.intersectObjects([floor,...cubes]);
        if (intersects.length > 0) {
            return intersects[0];
        }
    }
}

const Tools = {
  DRAW: "DRAW",
  PAINT: "PAINT",
  ERASE: "ERASE",
}

class Game {
    
    constructor(ui){
        this.currentBlockMaterial = 1;
        this.currentTool = Tools.DRAW;
        this.mouse = new THREE.Vector2();
        this.raycaster = new THREE.Raycaster();
        this.previewCube;
        this.floor;
        this.gridSize = 10;
        this.gridHelper;
        this.ui = ui;
    }
    
    init(){
        
        // Create grid
        this.gridHelper = new THREE.GridHelper(this.gridSize, this.gridSize, 0x7d8085, 0x4f5154);
        ABWorld.scene.add(this.gridHelper);
    
        this.createFloor();
        this.createPreviewCube();
    }
    
    createFloor(){
        let geometry = new THREE.PlaneGeometry(this.gridSize, this.gridSize, this.gridSize, this.gridSize);
        let material = new THREE.ShadowMaterial();
        material.opacity = 0.2;
        this.floor = new THREE.Mesh(geometry, material);
        this.floor.rotation.x = -Math.PI / 2;
        this.floor.userData.type = 'floor';
        ABWorld.scene.add(this.floor);
    }
    
    createPreviewCube(){
        const geometry = new THREE.BoxGeometry(1,1,1);
        
        const material = new THREE.MeshStandardMaterial({
            color: 0x40b8ff,
            emissive: 0x3489f7,
            transparent: true,
            opacity: 0.6,
            roughness: 0.5,
        });
        
        this.previewCube = new THREE.Mesh(geometry, material);
        
        this.previewCube.position.set(0.5, 0.5, 0.5);
        
        ABWorld.scene.add(this.previewCube);
    }
    
    movePreviewBlock(){
        const sqPosition = Utils.getPreviewCubePosition(this.mouse, this.raycaster, this.floor);
    
        if (sqPosition !== undefined){
            this.previewCube.visible = true;
            this.previewCube.position.copy(sqPosition.clone().add(new THREE.Vector3(0.5, 0.5, 0.5)));
        } else {
            this.previewCube.visible = false;
        }
    }
    
    addBlock(){
        const previewCubePosiition = this.previewCube.position;
        const isCubesOverlapping = cubes.some(cube => cube.position.equals(previewCubePosiition));
        
        if (!isCubesOverlapping && this.previewCube.visible === true){
            const geometry = new THREE.BoxGeometry(1, 1, 1);
            const material = new THREE.MeshLambertMaterial({ color: colors[this.ui.selectedMaterial] });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.copy(this.previewCube.position);
            cube.userData.type = 'cube';
            cube.updateMatrixWorld(); // updates for next intersect
            ABWorld.scene.add(cube);
            cubes.push(cube);
            
            // Update preview cube position
            this.movePreviewBlock();

        } else {
            console.log('A cube already exists at this position or is out of range!')
        }
    }
    
    removeBlock(){
        if (!Array.isArray(cubes) || !cubes.length) return;

        // Get intersected object
        const intersect = Utils.getMouseIntersect(this.mouse, this.raycaster, this.floor);
        
        if (intersect !== undefined) {
            if (intersect.object.userData.type === 'cube'){
                const cube = intersect.object;
                ABWorld.scene.remove(cube);
                const index = cubes.indexOf(cube);
                if (index !== -1) {
                    cubes.splice(index, 1);
                }
            }
        }
    }
}



const skycolor = '0xffffff';
const startRadius = 100;               // distance from centre we start the camera at
const maxRadius = startRadius * 10;     // maximum distance from camera we render things 


// const jsonPath = "uploads/cadogav2/test_data.json";
// const jsonPath = "uploads/cadogav2/torus_37x11x37.json";
const jsonPath = "uploads/stewalsh/mc-test-export.json";

const colors = {
  0: "#B8255F",
  1: "#FF9933",
  2: "#7ECC49",
  3: "#6ACCBC",
  4: "#4073FF",
  5: "#884DFF",
  6: "#EB96EB",
  7: "#FF8D85",
  8: "#FF5733",
  9: "#DAF7A6",
  10: "#FFEC59",
  11: "#C05780",
  12: "#6C88C4",
  13: "#74737A",
  14: "#CCCCFF",
  15: "#9FE2BF",
  16: "#8A3186",
  17: "#4057A7",
  18: "#042838",
  19: "#2DCED9",
  20: "#CBD6E2",
  21: "#4FB06D",
  22: "#BE398D",
  23: "#FF3B30",
  24: "#FFFFFF"
};

const colorsLength = Object.keys(colors).length;

let cubes = [];

function createAllCubes(data, scaleFactor) {
    const group = new THREE.Group();
    
    data.elements.forEach(element => {
        const cube = createCube(element);
        
        //cubes.push(cube)

        group.add(cube);
        
    });
    
    //group.add(...cubes)
    
    // Scale group
    group.scale.set(scaleFactor, scaleFactor, scaleFactor);
    
    // Transfer new scale directly to the children
    let position = new THREE.Vector3()
    let scale = new THREE.Vector3()

    group.children.forEach(child => {
        child.getWorldPosition(position)
        child.position.copy(position)
    
        child.getWorldScale(scale)
        child.scale.copy(scale);
    });

    // Add children to the scene
    ABWorld.scene.add(...group.children);
    
    // Clear the group
    group.children = [];
}

function createCube(blockData) {
    const from = blockData.from.map(x => Math.round(x));
    const to = blockData.to.map(x => Math.round(x));
    const width = to[0] - from[0];
    const height = to[1] - from[1];
    const depth = to[2] - from[2];
    const geometry = new THREE.BoxGeometry(width, height, depth);
    const colorIndex = blockData.color !== undefined? blockData.color % (colorsLength - 1) : Math.floor(Math.random() * (colorsLength - 1));
    const material = new THREE.MeshLambertMaterial({ color: colors[colorIndex] });
    const cube = new THREE.Mesh(geometry, material);
    // x, y, z
    cube.position.set(from[0] + width / 2, from[1] + height / 2, from[2] + depth / 2);
    return cube;
}



//===============================================================================================================
//          EVENTS
//===============================================================================================================

function addEventListeners(){
    const canvas = document.getElementById('ab-threepage');
    canvas.addEventListener('mousedown', onMouseDown);
    canvas.addEventListener('mousemove', onMouseMove);
}

function onMouseDown(event){
    
    console.log('click down');
    // event.button, 0 = left, 1 = middle, 2 = right
    // console.log(event.button)

    Utils.updateMousePosition(game.mouse, event);
    
    if (event.button === 0){
        game.addBlock();
    } else if (event.button === 2){
        game.removeBlock();
    }

}

function onMouseMove(event){
    Utils.updateMousePosition(game.mouse, event);
    
    game.movePreviewBlock();
}

const ui = new UI();
const game = new Game(ui);

// Define what the World does at the start of a run: 

AB.world.newRun = function() 
{
    ui.init();
    
    ABWorld.renderer = new THREE.WebGLRenderer({ antialias: true, alpha:true });
    ABWorld.renderer.shadowMap.enabled = true;
	ABWorld.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    
    // start a 3D scene: 
    ABWorld.init3d(startRadius, maxRadius, skycolor);
    // Transparent background
    ABWorld.renderer.setClearColor(0xffffff,0);
    
    // Must come after scene has been initiated
    game.init();
    
    console.log('cam')
    ABWorld.camera.fov = 45;
    ABWorld.camera.updateProjectionMatrix();
    
    
    // Lights
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(0, 10, 10);
    directionalLight.castShadow = true;
    ABWorld.scene.add(directionalLight);
    
    const ambientLight = new THREE.AmbientLight(0xd3def5, 1)//(0x3258a8);
    ABWorld.scene.add(ambientLight);
    
    
    // Events
    addEventListeners();

    const loader = new THREE.FileLoader();
    
    loader.load(jsonPath, function ( data ) {
	    jsonData = JSON.parse(data)
        createAllCubes(jsonData, 0.5);
	});
};