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();