Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Shared 3D World Builder</title> | |
<style> | |
body { margin: 0; overflow: hidden; } | |
canvas { display: block; } | |
#info { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.7); padding: 10px; } | |
</style> | |
</head> | |
<body> | |
<div id="info">Click to place selected object, right-click to delete.</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> | |
<script> | |
let scene, camera, renderer, websocket, selectedObjectType = 'None', worldObjects = {}; | |
const username = window.USERNAME || 'Anonymous'; | |
const websocketUrl = window.WEBSOCKET_URL || 'ws://localhost:8765'; | |
const plotWidth = window.PLOT_WIDTH || 50.0; | |
const plotDepth = window.PLOT_DEPTH || 50.0; | |
function init() { | |
scene = new THREE.Scene(); | |
// Orthographic camera for top-down view | |
const aspect = window.innerWidth / window.innerHeight; | |
const viewSize = plotWidth / 2; // Half of plotWidth for centered view | |
camera = new THREE.OrthographicCamera( | |
-viewSize * aspect, viewSize * aspect, | |
viewSize, -viewSize, | |
0.1, 1000 | |
); | |
camera.position.set(0, 20, 0); | |
camera.lookAt(0, 0, 0); | |
renderer = new THREE.WebGLRenderer(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
// Add lighting | |
const ambientLight = new THREE.AmbientLight(0x404040); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); | |
directionalLight.position.set(0, 10, 0); | |
scene.add(directionalLight); | |
const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth); | |
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.name = 'ground'; | |
scene.add(ground); | |
const gridHelper = new THREE.GridHelper(plotWidth, plotWidth / 2); | |
scene.add(gridHelper); | |
document.addEventListener('mousedown', onMouseDown); | |
window.addEventListener('resize', () => { | |
const aspect = window.innerWidth / window.innerHeight; | |
camera.left = -viewSize * aspect; | |
camera.right = viewSize * aspect; | |
camera.top = viewSize; | |
camera.bottom = -viewSize; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
initWebSocket(); | |
animate(); | |
} | |
function initWebSocket() { | |
websocket = new WebSocket(websocketUrl); | |
websocket.onopen = () => console.log('WebSocket connected'); | |
websocket.onmessage = (event) => { | |
const data = JSON.parse(event.data); | |
console.log('Received message:', data); | |
if (data.type === 'initial_state' || data.type === 'object_placed') { | |
const objects = data.type === 'initial_state' ? data.payload : { [data.payload.object_data.obj_id]: data.payload.object_data }; | |
for (let obj_id in objects) { | |
if (!worldObjects[obj_id]) { | |
worldObjects[obj_id] = objects[obj_id]; | |
addObjectToScene(objects[obj_id]); | |
} | |
} | |
} else if (data.type === 'object_deleted') { | |
if (worldObjects[data.payload.obj_id]) { | |
removeObjectFromScene(data.payload.obj_id); | |
delete worldObjects[data.payload.obj_id]; | |
} | |
} else if (data.type === 'user_join' || data.type === 'user_leave' || data.type === 'user_rename') { | |
console.log(`${data.payload.username} ${data.type === 'user_join' ? 'joined' : data.type === 'user_leave' ? 'left' : 'renamed to ' + data.payload.new_username}`); | |
} | |
}; | |
websocket.onclose = () => console.log('WebSocket closed'); | |
websocket.onerror = (error) => console.error('WebSocket error:', error); | |
} | |
function updateSelectedObjectType(newType) { | |
selectedObjectType = newType; | |
console.log(`Tool changed to: ${newType}`); | |
} | |
function addObjectToScene(objData) { | |
console.log('Adding object:', objData); | |
let geometry, material, mesh; | |
switch (objData.type) { | |
case 'Cube': | |
geometry = new THREE.BoxGeometry(1, 1, 1); | |
material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); | |
break; | |
case 'Sphere': | |
geometry = new THREE.SphereGeometry(0.5, 32, 32); | |
material = new THREE.MeshLambertMaterial({ color: 0xff0000 }); | |
break; | |
case 'Cylinder': | |
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); | |
material = new THREE.MeshLambertMaterial({ color: 0x0000ff }); | |
break; | |
case 'Cone': | |
geometry = new THREE.ConeGeometry(0.5, 1, 32); | |
material = new THREE.MeshLambertMaterial({ color: 0xffff00 }); | |
break; | |
case 'Torus': | |
geometry = new THREE.TorusGeometry(0.4, 0.1, 16, 100); | |
material = new THREE.MeshLambertMaterial({ color: 0xff00ff }); | |
break; | |
case 'Tree': | |
geometry = new THREE.ConeGeometry(0.5, 2, 8); | |
material = new THREE.MeshLambertMaterial({ color: 0x228B22 }); | |
break; | |
case 'Rock': | |
geometry = new THREE.DodecahedronGeometry(0.5); | |
material = new THREE.MeshLambertMaterial({ color: 0x808080 }); | |
break; | |
case 'Simple House': | |
geometry = new THREE.BoxGeometry(2, 1.5, 2); | |
material = new THREE.MeshLambertMaterial({ color: 0xA52A2A }); | |
break; | |
case 'Pine Tree': | |
geometry = new THREE.ConeGeometry(0.3, 1.5, 8); | |
material = new THREE.MeshLambertMaterial({ color: 0x006400 }); | |
break; | |
case 'Brick Wall': | |
geometry = new THREE.BoxGeometry(3, 1, 0.2); | |
material = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); | |
break; | |
case 'Mushroom': | |
geometry = new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI); | |
material = new THREE.MeshLambertMaterial({ color: 0xF5F5DC }); | |
break; | |
case 'Cactus': | |
geometry = new THREE.CylinderGeometry(0.2, 0.2, 1.5, 8); | |
material = new THREE.MeshLambertMaterial({ color: 0x2E8B57 }); | |
break; | |
case 'Campfire': | |
geometry = new THREE.DodecahedronGeometry(0.3); | |
material = new THREE.MeshLambertMaterial({ color: 0xFF4500 }); | |
break; | |
case 'Star': | |
geometry = new THREE.SphereGeometry(0.3, 4, 2); | |
material = new THREE.MeshLambertMaterial({ color: 0xFFFF00 }); | |
break; | |
case 'Gem': | |
geometry = new THREE.OctahedronGeometry(0.4); | |
material = new THREE.MeshLambertMaterial({ color: 0x00CED1 }); | |
break; | |
case 'Tower': | |
geometry = new THREE.CylinderGeometry(0.3, 0.5, 2, 8); | |
material = new THREE.MeshLambertMaterial({ color: 0x708090 }); | |
break; | |
case 'Barrier': | |
geometry = new THREE.BoxGeometry(2, 0.5, 0.2); | |
material = new THREE.MeshLambertMaterial({ color: 0x696969 }); | |
break; | |
case 'Fountain': | |
geometry = new THREE.CylinderGeometry(0.5, 0.3, 1, 32); | |
material = new THREE.MeshLambertMaterial({ color: 0x4682B4 }); | |
break; | |
case 'Lantern': | |
geometry = new THREE.BoxGeometry(0.3, 0.5, 0.3); | |
material = new THREE.MeshLambertMaterial({ color: 0xFFD700 }); | |
break; | |
case 'Sign Post': | |
geometry = new THREE.BoxGeometry(0.2, 1, 0.2); | |
material = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); | |
break; | |
default: | |
console.warn('Unknown object type:', objData.type); | |
return; | |
} | |
mesh = new THREE.Mesh(geometry, material); | |
mesh.position.set(objData.position.x, objData.position.y, objData.position.z); | |
mesh.rotation.set(objData.rotation.x, objData.rotation.y, objData.rotation.z); | |
mesh.name = objData.obj_id; | |
scene.add(mesh); | |
console.log('Object added to scene:', objData.obj_id); | |
} | |
function removeObjectFromScene(obj_id) { | |
const object = scene.getObjectByName(obj_id); | |
if (object) { | |
scene.remove(object); | |
console.log('Object removed:', obj_id); | |
} | |
} | |
function onMouseDown(event) { | |
event.preventDefault(); | |
const mouse = new THREE.Vector2( | |
(event.clientX / window.innerWidth) * 2 - 1, | |
-(event.clientY / window.innerHeight) * 2 + 1 | |
); | |
const raycaster = new THREE.Raycaster(); | |
raycaster.setFromCamera(mouse, camera); | |
const intersects = raycaster.intersectObjects(scene.children); | |
if (event.button === 0 && selectedObjectType !== 'None') { | |
for (let intersect of intersects) { | |
if (intersect.object.name === 'ground') { | |
const position = intersect.point; | |
position.y = 0.5; // Place above ground | |
console.log('Placing object at:', position); | |
placeObject(position); | |
break; | |
} | |
} | |
} else if (event.button === 2) { | |
for (let intersect of intersects) { | |
if (intersect.object.name && intersect.object.name !== 'ground') { | |
console.log('Deleting object:', intersect.object.name); | |
websocket.send(JSON.stringify({ | |
type: 'delete_object', | |
payload: { obj_id: intersect.object.name, username } | |
})); | |
break; | |
} | |
} | |
} | |
} | |
function placeObject(position) { | |
if (selectedObjectType === 'None') return; | |
const obj_id = `obj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; | |
const objectData = { | |
obj_id: obj_id, | |
type: selectedObjectType, | |
position: { x: position.x, y: position.y, z: position.z }, | |
rotation: { x: 0, y: 0, z: 0 } | |
}; | |
console.log('Sending place_object:', objectData); | |
websocket.send(JSON.stringify({ | |
type: 'place_object', | |
payload: { username, object_data: objectData } | |
})); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
renderer.render(scene, camera); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |