Code viewer for World: Paint By Blocks v2 (Three....

// Cloned by Vanya Cadogan on 24 Feb 2023 from World "Paint By Blocks v1 (Three.js)" by Vanya Cadogan 
// Please leave this clone trail here.
 

//AB.socketStart();

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

$.getScript("/uploads/cadogav2/three.r149.min.js", function(){
    startApp();
});

let rubikFont;

const jsonPath = "uploads/stewalsh/big-ben.json";

class UI {
    
    constructor(){
        this.selectedMaterialID = 0;
        this.selectedItem = null;
        this.isExporting = false;
        this.selectedToolBtn = null;
        this.glbExporter = new GLTFExporter();
        this.objExporter = new OBJExporter();
    }
    
    init(){
        window.addEventListener("contextmenu", e => e.preventDefault());
        document.getElementById('ab-runheaderbox').remove()
        
        document.body.innerHTML += `<header class="menu-header">
            <nav class="menu-nav">
                <ul>
                    <li class="no-select">
                        <span>File</span>
                        <ul class="submenu">
                            <div class="submenu-content">
                                <li id="load_csv">Load Blocks from CSV</li>
                                <li id="save_csv">Save Blocks as CSV</li>
                                <li id="export_glb">Export Model as GLB</li>
                                <li id="export_obj">Export Model as OBJ</li>
                            </div>
                        </ul>
                    </li>
                    <li class="no-select"><span>Views</span></li>
                    <li class="no-select"><span>Help</span></li>
                </ul>

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

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

                <ul>
                    <i id="draw_i_btn" class="fa-solid fa-pen btn-active" title="Draw Block"></i>
                    <i id="paint_i_btn" class="fa-solid fa-brush" title="Paint Blocks"></i>
                    <i id="delete_i_btn" class="fa-solid fa-eraser" title="Delete Blocks"></i>
                </ul>

                <ul class="i-menu">
                    <i class="fa-solid fa-palette" title="Materials"></i>
                    <ul class="submenu">
                        <div class="submenu-content">
                            <div class="grid-container">
                                <div class="item" title="Grass">
                                    <img src="/uploads/cadogav2/mc-grass-side.png" />
                                </div>
                                <div class="item" title="Snow Grass">
                                    <img src="/uploads/cadogav2/mc_grass_block_snow.png" />
                                </div>
                                <div class="item" title="Snow">
                                    <img src="/uploads/cadogav2/mc_snow.png" />
                                </div>
                                <div class="item" title="Barrel">
                                    <img src="/uploads/cadogav2/mc_barrel_side.png" />
                                </div>
                                <div class="item" title="Cobblestone">
                                    <img src="/uploads/cadogav2/mc_cobblestone.png" />
                                </div>
                                <div class="item" title="Dark Diamond">
                                    <img src="/uploads/cadogav2/mc_diamond_block.png" />
                                </div>
                                <div class="item" title="Sandstone Brick">
                                    <img src="/uploads/cadogav2/mc_sandstone_bricks_side.png" />
                                </div>
                                <div class="item" title="Chiseled Sandstone 1">
                                    <img
                                        src="/uploads/cadogav2/mc_sandstone_smooth.png"
                                    />
                                </div>
                                <div class="item" title="Chiseled Sandstone 2">
                                    <img src="/uploads/cadogav2/mc_sandstone_carved.png" />
                                </div>
                                <div class="item" title="Sand">
                                    <img src="/uploads/cadogav2/mc_sand.png" />
                                </div>
                                <div class="item" title="Deepslate Bricks">
                                    <img src="/uploads/cadogav2/mc_deepslate_bricks.png" />
                                </div>
                                <div class="item" title="Oak Log">
                                    <img src="/uploads/cadogav2/mc_oak_log.png" />
                                </div>
                                <div class="item" title="Dark Oak Log">
                                    <img src="/uploads/cadogav2/mc_dark_oak_log.png" />
                                </div>
                                <div class="item" title="Spruce Log">
                                    <img src="/uploads/cadogav2/mc_spruce_log.png" />
                                </div>
                                <div class="item" title="Stripped Oak Log">
                                    <img src="/uploads/cadogav2/mc_stripped_oak_log.png" />
                                </div>
                                <div class="item" title="Acacia Log">
                                    <img src="/uploads/cadogav2/mc_acacia_log.png" />
                                </div>
                                <div class="item" title="Stripped Acacia Log">
                                    <img src="/uploads/cadogav2/mc_stripped_acacia_log.png" />
                                </div>
                                <div class="item" title="Quartz">
                                    <img src="/uploads/cadogav2/mc_quartz_block_side.png" />
                                </div>
                                <div class="item" title="Quartz Bricks">
                                    <img src="/uploads/cadogav2/mc_quartz_bricks.png" />
                                </div>
                                <div class="item" title="Quartz Pillar">
                                    <img src="/uploads/cadogav2/mc_quartz_pillar.png" />
                                </div>
                                <div class="item" title="Bricks">
                                    <img src="/uploads/cadogav2/mc_bricks.png" />
                                </div>
                                <div class="item" title="Cracked Stone Bricks">
                                    <img src="/uploads/cadogav2/mc_cracked_stone_bricks.png" />
                                </div>
                                <div class="item" title="Mossy Stone Bricks">
                                    <img src="/uploads/cadogav2/mc_mossy_stone_bricks.png" />
                                </div>
                                <div class="item" title="Nether Bricks">
                                    <img src="/uploads/cadogav2/mc_nether_bricks.png" />
                                </div>
                                <div class="item" title="Mud Bricks">
                                    <img src="/uploads/cadogav2/mc_mud_bricks.png" />
                                </div>
                                <div class="item" title="Gold">
                                    <img src="/uploads/cadogav2/mc_gold_block.png" />
                                </div>
                                <div class="item" title="Copper">
                                    <img src="/uploads/cadogav2/mc_copper_block.png" />
                                </div>
                                <div class="item" title="Cut Copper">
                                    <img src="/uploads/cadogav2/mc_cut_copper.png" />
                                </div>
                                <div class="item" title="Oxidized Copper">
                                    <img src="/uploads/cadogav2/mc_oxidized_copper.png" />
                                </div>
                                <div class="item" title="Cut Oxidized Copper">
                                    <img src="/uploads/cadogav2/mc_oxidized_cut_copper.png" />
                                </div>
                                <div class="item" title="Bamboo Planks">
                                    <img src="/uploads/cadogav2/mc_bamboo_planks.png" />
                                </div>
                                <div class="item" title="Crimson Planks">
                                    <img src="/uploads/cadogav2/mc_crimson_planks.png" />
                                </div>
                                <div class="item" title="Jungle Planks">
                                    <img src="/uploads/cadogav2/mc_jungle_planks.png" />
                                </div>
                                <div class="item" title="Mangrove Planks">
                                    <img src="/uploads/cadogav2/mc_mangrove_planks.png" />
                                </div>
                                <div class="item" title="Oak Planks">
                                    <img src="/uploads/cadogav2/mc_oak_planks.png" />
                                </div>
                                <div class="item" title="Spruce Planks">
                                    <img src="/uploads/cadogav2/mc_spruce_planks.png" />
                                </div>
                                <div class="item" title="Granite">
                                    <img src="/uploads/cadogav2/mc_granite.png" />
                                </div>
                                <div class="item" title="Polished Granite">
                                    <img src="/uploads/cadogav2/mc_polished_granite.png" />
                                </div>
                            </div>
                        </div>
                    </ul>
                </ul>

                <ul>
                    <i id="camera_views_i_btn" class="fa-solid fa-camera" title="Camera Views"></i>
                    <i  id="camera_reset_btn"
                        class="fa-solid fa-house"
                        title="Center Camera to Origin"
                    ></i>
                </ul>
                <ul class="i-menu">
                    <i
                        class="fa-solid fa-circle-half-stroke"
                        title="Reference Opacity"
                    >
                    </i>
                    <ul class="submenu">
                        <div class="submenu-content">
                            <input
                                type="range"
                                min="0"
                                max="1"
                                value="0.05"
                                step="0.05"
                                class="slider"
                                id="opacity-slider"
                            />
                        </div>
                    </ul>
                </ul>
                <ul>
                    <i class="fa-solid fa-computer-mouse"></i>
                </ul>
            </nav>
        </header>`;
        
        document.body.innerHTML += `<div id="load-display" class="dialog-box">
            <p>Loading Model...</p>
            <div id="progress-bar">
                <div id="progress"></div>
            </div>
            <div class="btn">Close</div>
        </div>`
        
        this.setupListeners();
    }
    
    changeSelectedTool(event){
        if (event.target !== this.selectedToolBtn){
            this.selectedToolBtn.classList.remove("btn-active");
            this.selectedToolBtn = event.target;
            this.selectedToolBtn.classList.add("btn-active");
        }
    }
    
    setupListeners(){
        console.log(document.getElementById("draw_i_btn"));
        this.selectedToolBtn = document.getElementById("draw_i_btn");
        console.log(this.selectedToolBtn);
        
        document.getElementById("draw_i_btn").addEventListener('click',(event) => {
            this.changeSelectedTool(event);
            game.currentTool = Tools.DRAW;
        });
        
        document.getElementById("paint_i_btn").addEventListener('click',(event) => {
            this.changeSelectedTool(event);
            game.currentTool = Tools.PAINT;
        });
        
        document.getElementById("delete_i_btn").addEventListener('click',(event) => {
            this.changeSelectedTool(event);
            game.currentTool = Tools.ERASE;
        });
        
        // Reset Camera View
        document.getElementById("camera_reset_btn").addEventListener('click',function(event){
            if (controls){
                controls.enableDamping = false;
                controls.reset();
                controls.enableDamping = true;
            }
        });
        
        // Export as OBJ
        document.getElementById("export_obj").addEventListener('click',(event) =>{
            
            if (blocksMap.size === 0) {
                console.log('There are no blocks to export');
                return;
            }
            
            // Check if export is already in progress
            if (this.isExporting) {
                console.log('Export is already in progress');
                return;
            }

            // Set the export flag
            this.isExporting = true;
            const group = new THREE.Group();

            // Clone and add the meshes to the group
            for (const mesh of blocksMap.values()) {
                const clone = mesh.clone();
                group.add(clone);
            }

            // Parse group to OBJ
            const result = this.objExporter.parse(group);
            
            // Download OBJ
            const blob = new Blob([result], { type: 'text/plain' });
            saveAs(blob, 'objects.obj');
            
            // Remove the clones from the group
            group.remove(...group.children);
            
            this.isExporting = false;
            
        });
        
        // Export as GLB
        document.getElementById("export_glb").addEventListener('click',(event) =>{
            console.log('this.glbExporter')
            console.log(this.glbExporter)
            if (blocksMap.size === 0) {
                console.log('There are no blocks to export');
                return;
            }
            
            
            // Check if export is already in progress
            if (this.isExporting) {
                console.log('Export is already in progress');
                return;
            }

            // Set the export flag
            this.isExporting = true;
            const objectsToExport = [...blocksMap.values()]
            console.log('objectsToExport')
            console.log(objectsToExport)

            // Export the objects as GLB
            this.glbExporter.parse(objectsToExport, (result) => {
                // Save the GLB file
                console.log('parse')
                const blob = new Blob([result], { type: 'application/json' });
                console.log('saveAs')
                saveAs(blob, 'export.glb');

                // Reset the export flag
                this.isExporting = false;
            }, (error) => {
                console.error(error);
            });
            
        });
        
        document.getElementById("save_csv").addEventListener('click',function(event){
            if (blocksMap.size === 0) {
                console.log('There are no blocks to export');
                return;
            }
            
            fs.saveToCSV();
        });
        
        // load_csv
        document.getElementById("load_csv").addEventListener('click',function(event){
            // Check if the File System Access API is available
            if ('showOpenFilePicker' in window) {
                const options = {
                    types: [{
                        description: 'CSV Files',
                        accept: { 'text/csv': ['.csv'] },
                    }],
                };
                window.showOpenFilePicker(options).then((fileHandles) => {
                    const fileHandle = fileHandles[0];
                
                    // Use the file handle to read the file
                    fileHandle.getFile().then((file) => {
                        
                        fs.loadFromCSV(file);
                    });
              }).catch((error) => {
                    console.error(error);
              });
            } else {
                // No File System Access API available
                console.error('No File System Access API available');
            }
        });
        
        document.getElementById("opacity-slider").addEventListener('input', function() {
            fs.refMaterial.opacity = parseFloat(this.value);
        });
        

        document.querySelectorAll(".item").forEach((item, index) => {
            item.addEventListener("click", () => {
                // unselect previously selected item
                if (this.selectedItem !== null) {
                    this.selectedItem.classList.remove("selected");
                }
                // select clicked item
                item.classList.add("selected");
                this.selectedItem = item;
                this.selectedMaterialID = index;
                // log selected item's number
                console.log(`Selected item number: ${index + 1}`);
            });
        });
    }
}

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, 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, camera);
        let intersects = raycaster.intersectObjects([floor, ...blocksMap.values()]);
        
        if (intersects.length > 0) {
            let intersect = intersects[0];
            
            if (intersect.object.userData.type === 'block'){
                // Get face normal
                const norm = intersect.face.normal;
 
                const blockPosition = intersect.object.position;
                const rayPosition = new THREE.Vector3(Math.floor(blockPosition.x), Math.floor(blockPosition.y), Math.floor(blockPosition.z));

                return rayPosition.add(norm);
            } else if (intersect.object.userData.type === 'floor') {
                const point = intersect.point;
                const sqPosition = new THREE.Vector3(Math.floor(point.x), 0, Math.floor(point.z));
                
                return sqPosition;
            } else {
                return undefined;
            }
        }
    }
    
    static getMouseIntersect(mouse, raycaster, floor){
        raycaster.setFromCamera(mouse, camera);
        let intersects = raycaster.intersectObjects([floor,...blocksMap.values()]);
        if (intersects.length > 0) {
            return intersects[0];
        }
    }
    
    static fitCameraToModel(model, camera){
        const distance = 2;
        const box = new THREE.Box3().setFromObject(model);
        const size = box.getSize(new THREE.Vector3()).length();
        const center = box.getCenter(new THREE.Vector3());
        const direction = camera.position.clone().sub(center).normalize();
        camera.position.copy(direction.multiplyScalar(-distance * size).add(center));
        camera.lookAt(center);
    }
}

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

class Game {
    
    constructor(ui, textureManager){
        this.currentBlockMaterial = 1;
        this.currentTool = Tools.DRAW;
        this.mouse = new THREE.Vector2();
        this.raycaster = new THREE.Raycaster();
        this.previewBlock;
        this.floor;
        this.gridSize = 10;
        this.gridHelper;
        this.ui = ui;
        this.tm = textureManager;
    }
    
    init(){
        
        // Create grid
        this.gridHelper = new THREE.GridHelper(this.gridSize, this.gridSize, 0x7d8085, 0x4f5154);
        scene.add(this.gridHelper);
    
        this.createFloor();
        this.createPreviewCube();
        this.tm.init();
    }
    
    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';
        this.floor.receiveShadow = true;
        scene.add(this.floor);
    }
    
    createPreviewCube(){
        const geometry = new THREE.BoxGeometry(1,1,1);
        
        const material = new THREE.MeshPhysicalMaterial({
            color: 0x39e0fa, //0x40b8ff,
            emissive: 0x134359,//0x134359,
            roughness: 0.5,
            transmission: 0.75,
            thickness: 1,
            reflectivity: 0.5,
            specularIntensity: 0,
            transparent: true,
            //encoding: THREE.sRGBEncoding
            
        });
        
        this.previewBlock = new THREE.Mesh(geometry, material);
        
        this.previewBlock.position.set(0.5, 0.5, 0.5);
        
        scene.add(this.previewBlock);
    }
    
    movePreviewBlock(){
        const sqPosition = Utils.getPreviewCubePosition(this.mouse, this.raycaster, this.floor);
    
        if (sqPosition !== undefined){
            this.previewBlock.visible = true;
            this.previewBlock.position.copy(sqPosition.clone().add(new THREE.Vector3(0.5, 0.5, 0.5)));
        } else {
            this.previewBlock.visible = false;
        }
    }
    
    addBlock(){
        const previewBlockPosition = this.previewBlock.position;
        const key = `${previewBlockPosition.x},${previewBlockPosition.y},${previewBlockPosition.z}`;
        const isBlockOverlapping = blocksMap.has(key);
        
        if (!isBlockOverlapping && this.previewBlock.visible === true){
            const geometry = new THREE.BoxGeometry(1, 1, 1);

            let block;
            if (this.tm.materials.has(this.ui.selectedMaterialID)){
                block = new THREE.Mesh(geometry, this.tm.materials.get(this.ui.selectedMaterialID));
                block.userData.materialID = this.ui.selectedMaterialID;
            } else {
                block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: "#FF9933" }));
                block.userData.materialID = -1;
            }
            block.position.copy(this.previewBlock.position);
            block.userData.type = 'block';
            block.castShadow = true;
            block.receiveShadow = true;
            block.updateMatrixWorld(); // updates for next intersect
            blocksMap.set(key, block);
            scene.add(block);
            
            // Update preview cube position
            this.movePreviewBlock();

        } else {
            console.log('A cube already exists at this position or is out of range!')
        }
    }
    
    removeBlock(){
        if (blocksMap.size === 0) return;

        // Get intersected object
        const intersect = Utils.getMouseIntersect(this.mouse, this.raycaster, this.floor);
        
        if (intersect !== undefined) {
            if (intersect.object.userData.type === 'block'){
                const block = intersect.object;
                const key = `${block.position.x},${block.position.y},${block.position.z}`;
                scene.remove(block);
                blocksMap.delete(key);
            }
        }
    }
    
    updateGridHelper(newSize){
        scene.remove(this.gridHelper);
        this.gridSize = newSize;
        this.gridHelper = new THREE.GridHelper(this.gridSize, this.gridSize, 0x7d8085, 0x4f5154);
        scene.add(this.gridHelper);
        // Adjust the floor scale
        const scaleMultiplier = newSize / 10;
        this.floor.scale.set(scaleMultiplier, scaleMultiplier);
    }
    
    paintSingleBlock(){
        console.log('paint one');
        if (blocksMap.size === 0) return;
        
        // Get intersected object
        const intersect = Utils.getMouseIntersect(this.mouse, this.raycaster, this.floor);
        
        if (intersect !== undefined) {
            if (intersect.object.userData.type === 'block'){
                const block = intersect.object;
                const key = `${block.position.x},${block.position.y},${block.position.z}`;
                
                if (block.userData.materialID !== this.ui.selectedMaterialID){
                    block.material = this.tm.materials.get(this.ui.selectedMaterialID);
                    block.userData.materialID = this.ui.selectedMaterialID;
                }
            }
        }
    }
    
    paintAllSimilarBlocks(){
        console.log('paint all');
        if (blocksMap.size === 0) return;
        
        // Get intersected object
        const intersect = Utils.getMouseIntersect(this.mouse, this.raycaster, this.floor);
        
        if (intersect !== undefined) {
            if (intersect.object.userData.type === 'block'){
                const selectedBlock = intersect.object;
                const key = `${selectedBlock.position.x},${selectedBlock.position.y},${selectedBlock.position.z}`;
                
                if (selectedBlock.userData.materialID !== this.ui.selectedMaterialID){
                    console.log('paint all - 1');
                    const selectedMaterialID = selectedBlock.userData.materialID;
                    
                    for (const block of blocksMap.values()){
                        console.log('paint all', block.userData.materialID, selectedMaterialID);
                        // if block has the same material as block
                        if (block.userData.materialID == selectedMaterialID){
                            console.log('paint all - same');
                            block.material = this.tm.materials.get(this.ui.selectedMaterialID);
                            block.userData.materialID = this.ui.selectedMaterialID;
                        }   
                    }
                }
            }
        }
    }
}

class TextureManager {
    
    constructor(){
        this.loader = new THREE.TextureLoader();
        this.materials = new Map();
        this.materialList = [];
    }
    
    init(){
        
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc-grass-side.png",
            "/uploads/cadogav2/mc-grass-top.png",
            "/uploads/cadogav2/mc-grass-bottom.png"], 0);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_grass_block_snow.png",
            "/uploads/cadogav2/mc_snow.png",
            "/uploads/cadogav2/mc-grass-bottom.png"], 1);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_snow.png"], 2);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_barrel_side.png",
            "/uploads/cadogav2/mc_barrel_top_closed.png",
            "/uploads/cadogav2/mc_barrel_bottom.png"], 3);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_cobblestone.png"], 4);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_diamond_block.png"], 5);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_sandstone_bricks_side.png",
            "/uploads/cadogav2/mc_sandstone_top.png"], 6);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_sandstone_smooth.png",
            "/uploads/cadogav2/mc_sandstone_top.png"], 7);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_sandstone_carved.png",
            "/uploads/cadogav2/mc_sandstone_top.png"], 8);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_sand.png"], 9);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_deepslate_bricks.png"], 10);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_oak_log.png",
            "/uploads/cadogav2/mc_oak_log_top.png"], 11);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_dark_oak_log.png",
            "/uploads/cadogav2/mc_dark_oak_log_top.png"], 12);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_spruce_log.png",
            "/uploads/cadogav2/mc_spruce_log_top.png"], 13);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_stripped_oak_log.png",
            "/uploads/cadogav2/mc_stripped_oak_log_top.png"], 14);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_acacia_log.png",
            "/uploads/cadogav2/mc_acacia_log_top.png"], 15);
        // mc_stripped_acacia_log.png
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_stripped_acacia_log.png",
            "/uploads/cadogav2/mc_stripped_acacia_log_top.png"], 16);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_quartz_block_side.png"], 17);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_quartz_bricks.png"], 18);
        this.createMaterialFromTextures([
            "/uploads/cadogav2/mc_quartz_pillar.png",
            "/uploads/cadogav2/mc_quartz_pillar_top.png"], 19);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_bricks.png"], 20);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_cracked_stone_bricks.png"], 21);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_mossy_stone_bricks.png"], 22);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_nether_bricks.png"], 23);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_mud_bricks.png"], 24);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_gold_block.png"], 25);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_copper_block.png"], 26);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_cut_copper.png"], 27);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_oxidized_copper.png"], 28);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_oxidized_cut_copper.png"], 29);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_bamboo_planks.png"], 30);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_crimson_planks.png"], 31);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_jungle_planks.png"], 32);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_mangrove_planks.png"], 33);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_oak_planks.png"], 34);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_spruce_planks.png"], 35);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_granite.png"], 36);
        this.createMaterialFromTextures(["/uploads/cadogav2/mc_polished_granite.png"], 37);
    }
    
    createMaterialFromTextures(textureFilesArray, index){
        const materialsArray = [];
        
        let sideMaterial;
        
        if (textureFilesArray.length === 1){
            const texture = this.loader.load(textureFilesArray[0], (t) => {
                t.minFilter = THREE.NearestFilter;
                t.magFilter = THREE.NearestFilter;
            });
            const material = new THREE.MeshStandardMaterial({ map: texture });
            
            materialsArray.push(material);
            materialsArray.push(material);
            materialsArray.push(material);
            materialsArray.push(material);
            materialsArray.push(material);
            materialsArray.push(material);
        } else if (textureFilesArray.length === 2){
            const texture1 = this.loader.load(textureFilesArray[0], (t) => {
                t.minFilter = THREE.NearestFilter;
                t.magFilter = THREE.NearestFilter;
            });
            const texture2 = this.loader.load(textureFilesArray[1], (t) => {
                t.minFilter = THREE.NearestFilter;
                t.magFilter = THREE.NearestFilter;
            });
            const material1 = new THREE.MeshStandardMaterial({ map: texture1 });
            const material2 = new THREE.MeshStandardMaterial({ map: texture2 });
            
            materialsArray.push(material1);
            materialsArray.push(material1);
            materialsArray.push(material2);
            materialsArray.push(material2);
            materialsArray.push(material1);
            materialsArray.push(material1);
        } else if (textureFilesArray.length === 3){
            textureFilesArray.forEach((path, i) => {
                const texture = this.loader.load(path, (t) => {
                    t.minFilter = THREE.NearestFilter;
                    t.magFilter = THREE.NearestFilter;
                });
                const material = new THREE.MeshStandardMaterial({ map: texture });
                materialsArray.push(material);
                
                if (i === 0) {
                    sideMaterial = material;
                    materialsArray.push(material); // push additional side material
                } else if (i === 2) {
                    // push 2 more additional side materials for the last faces
                    materialsArray.push(sideMaterial);
                    materialsArray.push(sideMaterial);
                }
            });
        }
        
        if (materialsArray.length > 0){
            this.materials.set(index, materialsArray);
        }
    }
}

class FileSystem {
    
    constructor(){
        this.fileLoader = new THREE.FileLoader();
        this.fileReader = new FileReader();
        this.glbLoader = new GLTFLoader();
        this.xhr = new XMLHttpRequest();
        this.refMaterial = new THREE.MeshStandardMaterial({
            color: 0xffffff,
            opacity: 0.05,
            transparent: true,
            flatShading: true,
        });
        this.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"
        };
        this.colorsLength = Object.keys(this.colors).length;
        this.importGLBModel = this.importGLBModel.bind(this);
    }
    
    importMineCraftJson(){
        let jsonData;
        this.fileLoader.load(jsonPath, (data) => {
            jsonData = JSON.parse(data);
            // Create blocks from json
            this.createAllBlocks(jsonData, 0.5);
        });
    }
    
    createBlockFromBlockData(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 % (this.colorsLength - 1) : Math.floor(Math.random() * (this.colorsLength - 1));
        const material = new THREE.MeshLambertMaterial({ color: this.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;
    }
    
    createAllBlocks(data, scaleFactor) {
        const group = new THREE.Group();
        
        data.elements.forEach(element => {
            const cube = this.createBlockFromBlockData(element);
            group.add(cube);
        });
        
        // 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
        scene.add(...group.children);
        
        // Clear the group
        group.children = [];
    }
    
    // IMPORT GLB
    importGLBModel(path, byteSize=0){
        $('#load-display').css('display', 'flex');
        
        // "uploads/stewalsh/mc-wh.glb"
        this.glbLoader.load("uploads/stewalsh/mc-sol.glb", (gltf) => {
                $('#load-display').css('display', 'none');
                // Get model
                const model = gltf.scene;
                this.centerToGrid(model)
        
        	    model.traverse((child) => {
                    if (child instanceof THREE.Mesh) {
                        // Use the child's own texture map for the material
                        if (child.material.map) {
                            this.refMaterial.map = child.material.map;
                            //this.refMaterial.needsUpdate = true;
                            //this.refMaterial.normalScale = child.material.normalScale;
                        }
                
                        // Set the material on the mesh
                        child.material = this.refMaterial;
                    }
                });

                Utils.fitCameraToModel(model, camera)
                
                // const box = new THREE.BoxHelper( model, 0xffff00 );
                // scene.add( box );
                // Add the model to the scene
                scene.add(model);
            },
    
            // Loading progress
            function (xhr) {
                let progress;
                
                if (byteSize === 0){
                    if (xhr.total > 0) {
                        progress = (xhr.loaded / xhr.total) * 100;
                    }
                } else {
                    progress = (xhr.loaded / byteSize) * 100;
                }
    
                console.log("progress", progress)
                $('#progress').width(progress + '%');
            },
    
            // Load failed
            function (error) {
                console.error("Error loading GLB file:", error);
            }
        );
    }
    
    saveToCSV() {
        // CSV header
        const header = "x_pos,y_pos,z_pos,x_rot,y_rot,z_rot,material_id\n";
        let csvData = header;
    
        for (const block of blocksMap.values()) {
            // Get the position and rotation values
            const pos = block.position;
            const rot = block.rotation;
    
            // Create a new row
            const row = `${pos.x},${pos.y},${pos.z},${rot.x},${rot.y},${rot.z},${block.userData.materialID}\n`;
    
            // Add row
            csvData += row;
      }
    
      const blob = new Blob([csvData], { type: "text/csv" });
    
      // Save the Blob as CSV file
      saveAs(blob, "block_data.csv");
    }
    
    loadFromCSV(file) {
        this.fileReader.readAsText(file);
        
        this.fileReader.onload = () => {
          const csvData = this.fileReader.result;
          const lines = csvData.split(/\r?\n/);
          // Skip Header, i = 1
          for (let i = 1; i < lines.length; i++) {
            // Get all values of the csv line
            const values = lines[i].split(',');
            if (values.length < 7) continue;
            
            const position = new THREE.Vector3(parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]));
            const rotation = new THREE.Vector3(parseFloat(values[3]), parseFloat(values[4]), parseFloat(values[5]));
            const materialID = parseInt(values[6]);
    
            const key = `${position.x},${position.y},${position.z}`;
            const geometry = new THREE.BoxGeometry(1, 1, 1);
            let block;
            if (materialID >= 0){
                block = new THREE.Mesh(geometry, game.tm.materials.get(materialID));
            } else {
                block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: "#FF9933" }));
            }
            block.position.copy(position);
            block.rotation.set(rotation.x, rotation.y, rotation.z);
            block.userData.materialID = materialID;
            block.userData.type = 'block';
            block.castShadow = true;
            block.receiveShadow = true;
            block.updateMatrixWorld(); // updates for next intersect
            if (!blocksMap.has(key)){
                console.log("add",key)
                blocksMap.set(key, block);
                scene.add(block);
            } else {
                console.log("skip",key)
            }
          }
        };
    }
    
    centerToGrid(model) {
        // Get the bounding box of the model
        const box = new THREE.Box3().setFromObject(model);
        // Get the size of the bounding box
        const size = box.getSize(new THREE.Vector3());
        // Get center of the bounding box
        const center = box.getCenter(new THREE.Vector3());
        
        // Get the maximum dimension of the bounding box
        const maxDimension = Math.max(size.x, size.z);
        const gridSize = Math.ceil(maxDimension / 2) * 2;
        game.updateGridHelper(gridSize);
  
        // Center model
        model.position.x -= center.x;
        model.position.y -= box.min.y;
        model.position.z -= center.z;
        
        console.log("box",box)
        console.log("size",size)
        
        // Calculate the top-left corner of the rectangle
        const topLeftX = center.x - size.x / 2;
        const topLeftZ = center.z - size.z / 2;
        
        // Calculate the nearest rounded point and update the new rounded center coordinates
        const newCenterX = Math.round(topLeftX) + size.x / 2;
        const newCenterZ = Math.round(topLeftZ) + size.z / 2;
        
        const offsetX = gridSize % 2 === 0 && size.x % 2 === 0? 0 : 0.5;
        const offsetZ = gridSize % 2 === 0 && size.z % 2 === 0? 0 : 0.5;
        
        model.position.x += offsetX;
        model.position.z += offsetZ;
    }
}



function addEventListeners(){
    
    canvas.addEventListener('mousedown', onMouseDown);
    canvas.addEventListener('mousemove', onMouseMove);
    canvas.addEventListener('mouseup', onMouseUp);
    
    window.addEventListener("resize", () => {
        // Update sizes
        sizes.width = window.innerWidth;
        sizes.height = window.innerHeight;
    
        // Update camera
        camera.aspect = sizes.width / sizes.height;
        camera.updateProjectionMatrix();
    
        // Update renderer
        renderer.setSize(sizes.width, sizes.height);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    });
    
}

function onMouseDown(event){
    console.log('DOWN');
    
    mouseDownPos = {
        x: event.clientX,
        y: event.clientY
    };
    
    controls.enableRotate = false;
    
    cameraStartPosition.copy(camera.position);
    startDragging = true;
    isOrbiting = false;
    lastButton = event.button;
}

function onMouseMove(event){
    console.log('MOVE')
    Utils.updateMousePosition(game.mouse, event);
    
    game.movePreviewBlock();
    
    if (startDragging){
        const distanceX = Math.abs(event.clientX - mouseDownPos.x);
        const distanceY = Math.abs(event.clientY - mouseDownPos.y);
        
        if (lastButton === 0 && !isOrbiting && (distanceX > mouseMoveThreshold || distanceY > mouseMoveThreshold)) {
            console.log('TRUE')
            isOrbiting = true;
            
            mouseDownPos = {
                x: event.clientX,
                y: event.clientY
            };
            
            controls.enableRotate = true;
        
            // force pointer event for orbit controls
            const pointerEvent = new PointerEvent('pointerdown', {
              bubbles: true,
              cancelable: true,
              pointerType: 'mouse',
              button: event.button,
              clientX: event.clientX,
              clientY: event.clientY,
            });
            controls.domElement.dispatchEvent(pointerEvent);
        }
    }
}

function onMouseUp(event){
    console.log('UP');
    startDragging = false;

    // event.button, 0 = left, 1 = middle, 2 = right
    // console.log(event.button)

    Utils.updateMousePosition(game.mouse, event);
    
    // if camera position has been moved since mousedown, then exit function
    // to ignore add or remove block functions
    let startRounded = cameraStartPosition.clone().round();
    let cameraRounded = camera.position.clone().round();
    
    if (!startRounded.equals(cameraRounded)){
        return;
    }
    
    if (game.currentTool === Tools.DRAW){
        if (event.button === 0){
            game.addBlock();
        } else if (event.button === 2){
            game.removeBlock();
        }
    } else if (game.currentTool === Tools.PAINT){
        if (event.button === 0){
            game.paintSingleBlock();
        } else if (event.button === 2){
            game.paintAllSimilarBlocks();
        }
    }

}

const sizes = {
    width: window.innerWidth,
    height: window.innerHeight,
};

let canvas;
let camera;
let scene;
let renderer;
let controls;

let ui;
// TextureManager
let tm;
let game;
// FileSystem
let fs;
const blocksMap = new Map();
let glbLoader;


let mouseDownPos = null;
const mouseMoveThreshold = 25;
let cameraStartPosition = new THREE.Vector3();
let startDragging = false;
let isOrbiting = false;
let lastButton = 0;



function startApp(){
    console.log('AB')
	console.log(AB)
	fs = new FileSystem();
	ui = new UI();
    ui.init();
    tm = new TextureManager();
    game = new Game(ui, tm);
    
    const container = document.getElementById("ab-runcanvas");

    // Create a canvas element
    canvas = document.createElement("canvas");
    container.appendChild(canvas);
    
    scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 1000 );
	camera.position.x = -10;
    camera.position.y = 7.5;
    camera.position.z = 10;
	scene.add(camera);

    renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        antialias: true,
        alpha: true,
        preserveDrawingBuffer : true 
    });
    renderer.setSize(sizes.width, sizes.height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setClearColor(0xff0000, 0);
    //renderer.outputEncoding = THREE.sRGBEncoding;
    //renderer.shadowMap.enabled = true;
    //renderer.sortObjects = false;
    //renderer.shadowMap.type = THREE.BasicShadowMap;
    
    // Lights
    let directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(-5, 10, 10);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 2048;
    directionalLight.shadow.mapSize.height = 2048;
    directionalLight.shadow.radius = 1;
    scene.add(directionalLight);
    
    let ambientLight = new THREE.AmbientLight(0xd3def5, 0.55)//(0x3258a8);
    scene.add(ambientLight);
    
    
    game.init();
    
    addEventListeners();
	
	controls = new OrbitControls(camera, canvas);
	controls.mouseButtons = {LEFT: 0, MIDDLE: 2};
	controls.dampingFactor = 0.15;
    controls.enableDamping = true;
    // controls.addEventListener('start', function(e) {
    //     console.log('c start')
    // });
    
    // controls.addEventListener('change', function(e) {
    //     console.log('c change')
    // });
    
    // controls.addEventListener('end', function(e) {
    //     console.log('c end')
    // });
    
    
    // AxesHelper
    const axesHelper = new THREE.AxesHelper();
    scene.add(axesHelper)
    
    // Import Model
    // fs.importMineCraftJson()
    fs.importGLBModel();
    
    

	
	const animate = () => {
        // const elapsedTime = clock.getElapsedTime();
    
        // Update controls
        controls.update();
    
        // Render
        renderer.render(scene, camera);
        //composer.render()
    
        // Call tick again on the next frame
        requestAnimationFrame(animate);
    };
    
    animate();
}