//AB.socketStart();
// Load CSS
AB.loadCSS("/uploads/cadogav2/app-styles.css");
$.getScript("/uploads/cadogav2/three.r149.min.js", function(){
console.log(this)
startApp();
});
let rubikFont;
const jsonPath = "uploads/stewalsh/big-ben.json";
class UI {
constructor(){
this.selectedMaterial = null;
this.isExporting = false;
this.glbExporter = new GLTFExporter();
this.objExporter = new OBJExporter();
console.log('this.glbExporter')
console.log(this.glbExporter)
}
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">
<li>Load Model</li>
<li id="export_glb">Export Model as GLB</li>
<li id="export_obj">Export Model as OBJ</li>
<li>Save State</li>
</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" 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="grid-container">
<div class="item">
<img src="/uploads/cadogav2/mc-grass-side.png" />
</div>
<div class="item">
<img src="/uploads/cadogav2/mc-grass-side.png" />
</div>
<div class="item">
<img src="/uploads/cadogav2/mc-grass-side.png" />
</div>
<div class="item">
<img
src="/uploads/cadogav2/mc-grass-side.png"
/>
</div>
<div class="item">
<img src="/uploads/cadogav2/mc-grass-side.png" />
</div>
<div class="item">
<img src="/uploads/cadogav2/mc-grass-side.png" />
</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">
<input
type="range"
min="0"
max="1"
value="0.05"
step="0.05"
class="slider"
id="opacity-slider"
/>
</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();
}
setupListeners(){
// home btn
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) =>{
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 group = new THREE.Group();
group.add(...blocksMap.values());
const result = this.objExporter.parse(group);
const blob = new Blob([result], { type: 'text/plain' });
saveAs(blob, 'objects.obj');
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("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.selectedMaterial !== null) {
this.selectedMaterial.classList.remove("selected");
}
// select clicked item
item.classList.add("selected");
this.selectedMaterial = item;
// 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,
});
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);
const material = new THREE.MeshStandardMaterial({ color: fs.colors[1] });
//console.log(this.tm.materialList[0])
const block = new THREE.Mesh(geometry, material);
block.position.copy(this.previewBlock.position);
block.userData.type = 'block';
block.castShadow = true;
block.receiveShadow = true;
block.updateMatrixWorld(); // updates for next intersect
scene.add(block);
blocksMap.set(key, 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);
}
}
class TextureManager {
constructor(){
this.loader = new THREE.TextureLoader();
this.materialList = [];
}
init(){
const sideGrassTexture = this.loader.load("/uploads/cadogav2/mc-grass-side.png");
console.log('sideGrassTexture')
console.log(sideGrassTexture)
const grassMaterial = [
new THREE.MeshStandardMaterial({map : sideGrassTexture, color: 0xffffff, flatShading: true, vertexColors: true,}),
new THREE.MeshStandardMaterial({map : sideGrassTexture, color: 0xffffff, flatShading: true, vertexColors: true,}),
new THREE.MeshStandardMaterial({map : this.loader.load("/uploads/cadogav2/mc-grass-top.png"), color: 0xffffff, flatShading: true, vertexColors: true,}),
new THREE.MeshStandardMaterial({map : this.loader.load("/uploads/cadogav2/mc-grass-bottom.png"), color: 0xffffff, flatShading: true, vertexColors: true,}),
new THREE.MeshStandardMaterial({map : sideGrassTexture, color: 0xffffff, flatShading: true, vertexColors: true,}),
new THREE.MeshStandardMaterial({map : sideGrassTexture, color: 0xffffff, flatShading: true, vertexColors: true,})
];
console.log('grassMaterial')
console.log(grassMaterial)
this.materialList.push(grassMaterial);
}
}
class FileSystem {
constructor(){
this.fileLoader = new THREE.FileLoader();
this.glbLoader = new GLTFLoader();
this.xhr = new XMLHttpRequest();
this.refMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
opacity: 0.05,
transparent: true,
flatShading: true,
vertexColors: 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;
}
});
console.log('model')
console.log(model)
Utils.fitCameraToModel(model, camera)
const box = new THREE.BoxHelper( model, 0xffff00 );
scene.add( box );
// Add the model to the scene
scene.add(model);
console.log("game.floor")
console.log(game.floor)
},
// 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);
}
);
}
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('click down', startDragging);
mouseDownPos = {
x: event.clientX,
y: event.clientY
};
const intersect = Utils.getMouseIntersect(game.mouse, game.raycaster, game.floor);
if (intersect === undefined){
controls.enableRotate = true;
startDragging = false;
// 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);
return;
}
controls.enableRotate = false;
cameraStartPosition.copy(camera.position);
startDragging = true;
}
function onMouseMove(event){
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 (distanceX > mouseMoveThreshold || distanceY > mouseMoveThreshold) {
mouseDownPos = {
x: event.clientX,
y: event.clientY
};
controls.enableRotate = true;
}
}
}
function onMouseUp(event){
startDragging = false;
console.log('click up============================================');
// 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 (event.button === 0){
game.addBlock();
} else if (event.button === 2){
game.removeBlock();
}
}
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 = 10;
let cameraStartPosition = new THREE.Vector3();
let startDragging = false;
function startApp(){
console.log('AB')
console.log(AB)
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.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, 1)//(0x3258a8);
scene.add(ambientLight);
game.init();
addEventListeners();
fs = new FileSystem();
controls = new OrbitControls(camera, canvas);
controls.mouseButtons = {LEFT: 0, MIDDLE: 2};
controls.dampingFactor = 0.15;
controls.enableDamping = true;
// 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();
}