awacke1 commited on
Commit
795d8d2
·
verified ·
1 Parent(s): d22ed26

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +569 -201
index.html CHANGED
@@ -1,9 +1,9 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Shared 3D World Builder</title>
7
  <style>
8
  body { margin: 0; overflow: hidden; }
9
  canvas { display: block; }
@@ -12,247 +12,615 @@
12
  </head>
13
  <body>
14
  <div id="info">Click to place selected object, right-click to delete.</div>
15
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
16
- <script>
17
- let scene, camera, renderer, websocket, selectedObjectType = 'None', worldObjects = {};
18
- const username = window.USERNAME || 'Anonymous';
19
- const websocketUrl = window.WEBSOCKET_URL || 'ws://localhost:8765';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  const plotWidth = window.PLOT_WIDTH || 50.0;
21
  const plotDepth = window.PLOT_DEPTH || 50.0;
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  function init() {
24
  scene = new THREE.Scene();
25
- // Orthographic camera for top-down view
 
26
  const aspect = window.innerWidth / window.innerHeight;
27
- const viewSize = plotWidth / 2; // Half of plotWidth for centered view
28
- camera = new THREE.OrthographicCamera(
29
- -viewSize * aspect, viewSize * aspect,
30
- viewSize, -viewSize,
31
- 0.1, 1000
32
- );
33
- camera.position.set(0, 20, 0);
34
- camera.lookAt(0, 0, 0);
35
-
36
- renderer = new THREE.WebGLRenderer();
 
 
37
  renderer.setSize(window.innerWidth, window.innerHeight);
 
 
38
  document.body.appendChild(renderer.domElement);
39
 
40
- // Add lighting
41
- const ambientLight = new THREE.AmbientLight(0x404040);
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  scene.add(ambientLight);
43
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
44
- directionalLight.position.set(0, 10, 0);
 
 
 
 
 
 
 
 
 
 
45
  scene.add(directionalLight);
 
 
 
46
 
 
 
 
47
  const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
48
- const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 });
49
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
50
- ground.rotation.x = -Math.PI / 2;
51
- ground.name = 'ground';
52
- scene.add(ground);
53
-
54
- const gridHelper = new THREE.GridHelper(plotWidth, plotWidth / 2);
55
- scene.add(gridHelper);
56
-
57
- document.addEventListener('mousedown', onMouseDown);
58
- window.addEventListener('resize', () => {
59
- const aspect = window.innerWidth / window.innerHeight;
60
- camera.left = -viewSize * aspect;
61
- camera.right = viewSize * aspect;
62
- camera.top = viewSize;
63
- camera.bottom = -viewSize;
64
- camera.updateProjectionMatrix();
65
- renderer.setSize(window.innerWidth, window.innerHeight);
66
- });
67
-
68
- initWebSocket();
69
- animate();
70
  }
71
 
72
- function initWebSocket() {
73
- websocket = new WebSocket(websocketUrl);
74
- websocket.onopen = () => console.log('WebSocket connected');
75
- websocket.onmessage = (event) => {
76
- const data = JSON.parse(event.data);
77
- console.log('Received message:', data);
78
- if (data.type === 'initial_state' || data.type === 'object_placed') {
79
- const objects = data.type === 'initial_state' ? data.payload : { [data.payload.object_data.obj_id]: data.payload.object_data };
80
- for (let obj_id in objects) {
81
- if (!worldObjects[obj_id]) {
82
- worldObjects[obj_id] = objects[obj_id];
83
- addObjectToScene(objects[obj_id]);
84
- }
85
- }
86
- } else if (data.type === 'object_deleted') {
87
- if (worldObjects[data.payload.obj_id]) {
88
- removeObjectFromScene(data.payload.obj_id);
89
- delete worldObjects[data.payload.obj_id];
90
- }
91
- } else if (data.type === 'user_join' || data.type === 'user_leave' || data.type === 'user_rename') {
92
- console.log(`${data.payload.username} ${data.type === 'user_join' ? 'joined' : data.type === 'user_leave' ? 'left' : 'renamed to ' + data.payload.new_username}`);
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
  };
95
- websocket.onclose = () => console.log('WebSocket closed');
96
- websocket.onerror = (error) => console.error('WebSocket error:', error);
97
  }
98
 
99
- function updateSelectedObjectType(newType) {
100
- selectedObjectType = newType;
101
- console.log(`Tool changed to: ${newType}`);
 
 
 
 
102
  }
103
 
104
- function addObjectToScene(objData) {
105
- console.log('Adding object:', objData);
106
- let geometry, material, mesh;
107
- switch (objData.type) {
108
- case 'Cube':
109
- geometry = new THREE.BoxGeometry(1, 1, 1);
110
- material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
111
- break;
112
- case 'Sphere':
113
- geometry = new THREE.SphereGeometry(0.5, 32, 32);
114
- material = new THREE.MeshLambertMaterial({ color: 0xff0000 });
115
- break;
116
- case 'Cylinder':
117
- geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
118
- material = new THREE.MeshLambertMaterial({ color: 0x0000ff });
119
- break;
120
- case 'Cone':
121
- geometry = new THREE.ConeGeometry(0.5, 1, 32);
122
- material = new THREE.MeshLambertMaterial({ color: 0xffff00 });
123
- break;
124
- case 'Torus':
125
- geometry = new THREE.TorusGeometry(0.4, 0.1, 16, 100);
126
- material = new THREE.MeshLambertMaterial({ color: 0xff00ff });
127
- break;
128
- case 'Tree':
129
- geometry = new THREE.ConeGeometry(0.5, 2, 8);
130
- material = new THREE.MeshLambertMaterial({ color: 0x228B22 });
131
- break;
132
- case 'Rock':
133
- geometry = new THREE.DodecahedronGeometry(0.5);
134
- material = new THREE.MeshLambertMaterial({ color: 0x808080 });
135
- break;
136
- case 'Simple House':
137
- geometry = new THREE.BoxGeometry(2, 1.5, 2);
138
- material = new THREE.MeshLambertMaterial({ color: 0xA52A2A });
139
- break;
140
- case 'Pine Tree':
141
- geometry = new THREE.ConeGeometry(0.3, 1.5, 8);
142
- material = new THREE.MeshLambertMaterial({ color: 0x006400 });
143
- break;
144
- case 'Brick Wall':
145
- geometry = new THREE.BoxGeometry(3, 1, 0.2);
146
- material = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
147
- break;
148
- case 'Mushroom':
149
- geometry = new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI);
150
- material = new THREE.MeshLambertMaterial({ color: 0xF5F5DC });
151
- break;
152
- case 'Cactus':
153
- geometry = new THREE.CylinderGeometry(0.2, 0.2, 1.5, 8);
154
- material = new THREE.MeshLambertMaterial({ color: 0x2E8B57 });
155
- break;
156
- case 'Campfire':
157
- geometry = new THREE.DodecahedronGeometry(0.3);
158
- material = new THREE.MeshLambertMaterial({ color: 0xFF4500 });
159
  break;
160
- case 'Star':
161
- geometry = new THREE.SphereGeometry(0.3, 4, 2);
162
- material = new THREE.MeshLambertMaterial({ color: 0xFFFF00 });
163
  break;
164
- case 'Gem':
165
- geometry = new THREE.OctahedronGeometry(0.4);
166
- material = new THREE.MeshLambertMaterial({ color: 0x00CED1 });
167
  break;
168
- case 'Tower':
169
- geometry = new THREE.CylinderGeometry(0.3, 0.5, 2, 8);
170
- material = new THREE.MeshLambertMaterial({ color: 0x708090 });
171
  break;
172
- case 'Barrier':
173
- geometry = new THREE.BoxGeometry(2, 0.5, 0.2);
174
- material = new THREE.MeshLambertMaterial({ color: 0x696969 });
175
  break;
176
- case 'Fountain':
177
- geometry = new THREE.CylinderGeometry(0.5, 0.3, 1, 32);
178
- material = new THREE.MeshLambertMaterial({ color: 0x4682B4 });
179
  break;
180
- case 'Lantern':
181
- geometry = new THREE.BoxGeometry(0.3, 0.5, 0.3);
182
- material = new THREE.MeshLambertMaterial({ color: 0xFFD700 });
183
  break;
184
- case 'Sign Post':
185
- geometry = new THREE.BoxGeometry(0.2, 1, 0.2);
186
- material = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
187
  break;
188
  default:
189
- console.warn('Unknown object type:', objData.type);
190
- return;
191
  }
192
- mesh = new THREE.Mesh(geometry, material);
193
- mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
194
- mesh.rotation.set(objData.rotation.x, objData.rotation.y, objData.rotation.z);
195
- mesh.name = objData.obj_id;
196
- scene.add(mesh);
197
- console.log('Object added to scene:', objData.obj_id);
198
  }
199
 
200
- function removeObjectFromScene(obj_id) {
201
- const object = scene.getObjectByName(obj_id);
202
- if (object) {
203
- scene.remove(object);
204
- console.log('Object removed:', obj_id);
205
  }
 
206
  }
207
 
208
- function onMouseDown(event) {
209
- event.preventDefault();
210
- const mouse = new THREE.Vector2(
211
- (event.clientX / window.innerWidth) * 2 - 1,
212
- -(event.clientY / window.innerHeight) * 2 + 1
213
- );
214
- const raycaster = new THREE.Raycaster();
215
- raycaster.setFromCamera(mouse, camera);
216
- const intersects = raycaster.intersectObjects(scene.children);
217
-
218
- if (event.button === 0 && selectedObjectType !== 'None') {
219
- for (let intersect of intersects) {
220
- if (intersect.object.name === 'ground') {
221
- const position = intersect.point;
222
- position.y = 0.5; // Place above ground
223
- console.log('Placing object at:', position);
224
- placeObject(position);
225
- break;
226
- }
 
 
 
 
227
  }
228
- } else if (event.button === 2) {
229
- for (let intersect of intersects) {
230
- if (intersect.object.name && intersect.object.name !== 'ground') {
231
- console.log('Deleting object:', intersect.object.name);
232
- websocket.send(JSON.stringify({
233
- type: 'delete_object',
234
- payload: { obj_id: intersect.object.name, username }
235
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  break;
237
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  }
 
 
 
239
  }
 
 
 
 
 
 
 
 
 
240
  }
241
 
242
- function placeObject(position) {
243
- if (selectedObjectType === 'None') return;
244
- const obj_id = `obj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
245
- const objectData = {
246
- obj_id: obj_id,
247
- type: selectedObjectType,
248
- position: { x: position.x, y: position.y, z: position.z },
249
- rotation: { x: 0, y: 0, z: 0 }
250
- };
251
- console.log('Sending place_object:', objectData);
252
- websocket.send(JSON.stringify({
253
- type: 'place_object',
254
- payload: { username, object_data: objectData }
255
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
 
258
  function animate() {
 
1
  <!DOCTYPE html>
2
+ <html>
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Shared World Builder</title>
7
  <style>
8
  body { margin: 0; overflow: hidden; }
9
  canvas { display: block; }
 
12
  </head>
13
  <body>
14
  <div id="info">Click to place selected object, right-click to delete.</div>
15
+ <script type="importmap">
16
+ {
17
+ "imports": {
18
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
19
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <script type="module">
25
+ import * as THREE from 'three';
26
+
27
+ let scene, camera, renderer;
28
+ let raycaster, mouse;
29
+ let socket = null;
30
+ let connectionRetries = 0;
31
+ const MAX_RETRIES = 5;
32
+
33
+ // Access State from Streamlit
34
+ const myUsername = window.USERNAME || `User_${Math.random().toString(36).substring(2, 6)}`;
35
+ const websocketUrl = window.WEBSOCKET_URL || "ws://localhost:8765";
36
+ let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
37
  const plotWidth = window.PLOT_WIDTH || 50.0;
38
  const plotDepth = window.PLOT_DEPTH || 50.0;
39
 
40
+ // World State
41
+ const worldObjects = new Map();
42
+ const groundMeshes = {};
43
+
44
+ // Materials
45
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
46
+ const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
47
+ const objectMaterials = {
48
+ 'wood': new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }),
49
+ 'leaf': new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }),
50
+ 'stone': new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 }),
51
+ 'house_wall': new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 }),
52
+ 'house_roof': new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 }),
53
+ 'brick': new THREE.MeshStandardMaterial({ color: 0x9B4C43, roughness: 0.85 }),
54
+ 'metal': new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.4, metalness: 0.8 }),
55
+ 'gem': new THREE.MeshStandardMaterial({ color: 0x4FFFFF, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.8 }),
56
+ 'light': new THREE.MeshBasicMaterial({ color: 0xFFFF88 })
57
+ };
58
+
59
  function init() {
60
  scene = new THREE.Scene();
61
+ scene.background = new THREE.Color(0xabcdef);
62
+
63
  const aspect = window.innerWidth / window.innerHeight;
64
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 5000);
65
+ camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20);
66
+ camera.lookAt(plotWidth/2, 0, plotDepth/2);
67
+ scene.add(camera);
68
+
69
+ setupLighting();
70
+ createGroundPlane(0, 0, false);
71
+
72
+ raycaster = new THREE.Raycaster();
73
+ mouse = new THREE.Vector2();
74
+
75
+ renderer = new THREE.WebGLRenderer({ antialias: true });
76
  renderer.setSize(window.innerWidth, window.innerHeight);
77
+ renderer.shadowMap.enabled = true;
78
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
79
  document.body.appendChild(renderer.domElement);
80
 
81
+ connectWebSocket();
82
+
83
+ document.addEventListener('mousemove', onMouseMove, false);
84
+ document.addEventListener('click', onDocumentClick, false);
85
+ document.addEventListener('contextmenu', onRightClick, false);
86
+ window.addEventListener('resize', onWindowResize, false);
87
+
88
+ window.updateSelectedObjectType = updateSelectedObjectType;
89
+
90
+ console.log(`Three.js Initialized for user: ${myUsername}. Connecting to ${websocketUrl}...`);
91
+ animate();
92
+ }
93
+
94
+ function setupLighting() {
95
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
96
  scene.add(ambientLight);
97
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
98
+ directionalLight.position.set(75, 150, 100);
99
+ directionalLight.castShadow = true;
100
+ directionalLight.shadow.mapSize.width = 2048;
101
+ directionalLight.shadow.mapSize.height = 2048;
102
+ directionalLight.shadow.camera.near = 10;
103
+ directionalLight.shadow.camera.far = 400;
104
+ directionalLight.shadow.camera.left = -150;
105
+ directionalLight.shadow.camera.right = 150;
106
+ directionalLight.shadow.camera.top = 150;
107
+ directionalLight.shadow.camera.bottom = -150;
108
+ directionalLight.shadow.bias = -0.002;
109
  scene.add(directionalLight);
110
+ const hemiLight = new THREE.HemisphereLight(0xabcdef, 0x55aa55, 0.5);
111
+ scene.add(hemiLight);
112
+ }
113
 
114
+ function createGroundPlane(gridX, gridZ, isPlaceholder) {
115
+ const gridKey = `${gridX}_${gridZ}`;
116
+ if (groundMeshes[gridKey]) return groundMeshes[gridKey];
117
  const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
118
+ const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
119
+ const groundMesh = new THREE.Mesh(groundGeometry, material);
120
+ groundMesh.rotation.x = -Math.PI / 2;
121
+ groundMesh.position.y = -0.05;
122
+ groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
123
+ groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
124
+ groundMesh.receiveShadow = true;
125
+ groundMesh.userData.gridKey = gridKey;
126
+ groundMesh.userData.isPlaceholder = isPlaceholder;
127
+ groundMesh.name = 'ground';
128
+ scene.add(groundMesh);
129
+ groundMeshes[gridKey] = groundMesh;
130
+ return groundMesh;
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
+ function connectWebSocket() {
134
+ console.log("Attempting WebSocket connection...");
135
+ socket = new WebSocket(websocketUrl);
136
+
137
+ socket.onopen = () => {
138
+ console.log("WebSocket connection established.");
139
+ connectionRetries = 0;
140
+ };
141
+
142
+ socket.onmessage = (event) => {
143
+ try {
144
+ const data = JSON.parse(event.data);
145
+ console.log("WebSocket message received:", data);
146
+ handleWebSocketMessage(data);
147
+ } catch (e) {
148
+ console.error("Failed to parse WebSocket message:", event.data, e);
149
+ }
150
+ };
151
+
152
+ socket.onerror = (error) => {
153
+ console.error("WebSocket error:", error);
154
+ };
155
+
156
+ socket.onclose = (event) => {
157
+ console.warn(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}. Clean: ${event.wasClean}`);
158
+ socket = null;
159
+ if (connectionRetries < MAX_RETRIES) {
160
+ connectionRetries++;
161
+ const delay = Math.pow(2, connectionRetries) * 1000;
162
+ console.log(`Attempting reconnection in ${delay / 1000}s...`);
163
+ setTimeout(connectWebSocket, delay);
164
+ } else {
165
+ console.error("WebSocket reconnection failed after max retries.");
166
  }
167
  };
 
 
168
  }
169
 
170
+ function sendWebSocketMessage(type, payload) {
171
+ if (socket && socket.readyState === WebSocket.OPEN) {
172
+ const message = JSON.stringify({ type, payload });
173
+ socket.send(message);
174
+ } else {
175
+ console.warn("WebSocket not open. Message not sent:", type, payload);
176
+ }
177
  }
178
 
179
+ function handleWebSocketMessage(data) {
180
+ const { type, payload } = data;
181
+
182
+ switch (type) {
183
+ case "initial_state":
184
+ console.log(`Received initial world state with ${Object.keys(payload).length} objects.`);
185
+ clearWorldObjects();
186
+ for (const obj_id in payload) {
187
+ createAndPlaceObject(payload[obj_id], false);
188
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  break;
190
+ case "object_placed":
191
+ console.log(`Object placed by ${payload.username}:`, payload.object_data);
192
+ createAndPlaceObject(payload.object_data, false);
193
  break;
194
+ case "object_deleted":
195
+ console.log(`Object deleted by ${payload.username}:`, payload.obj_id);
196
+ removeObjectById(payload.obj_id);
197
  break;
198
+ case "user_join":
199
+ console.log(`User joined: ${payload.username} (${payload.id})`);
 
200
  break;
201
+ case "user_leave":
202
+ console.log(`User left: ${payload.username} (${payload.id})`);
 
203
  break;
204
+ case "user_rename":
205
+ console.log(`User ${payload.old_username} is now ${payload.new_username}`);
 
206
  break;
207
+ case "chat_message":
208
+ console.log(`Chat from ${payload.username}: ${payload.message}`);
 
209
  break;
210
+ case "tool_selected":
211
+ console.log(`Tool selected by ${payload.username}: ${payload.tool_type}`);
212
+ selectedObjectType = payload.tool_type;
213
  break;
214
  default:
215
+ console.warn("Received unknown WebSocket message type:", type);
 
216
  }
 
 
 
 
 
 
217
  }
218
 
219
+ function clearWorldObjects() {
220
+ console.log("Clearing existing world objects...");
221
+ for (const [obj_id, mesh] of worldObjects.entries()) {
222
+ scene.remove(mesh);
 
223
  }
224
+ worldObjects.clear();
225
  }
226
 
227
+ function removeObjectById(obj_id) {
228
+ if (worldObjects.has(obj_id)) {
229
+ const mesh = worldObjects.get(obj_id);
230
+ scene.remove(mesh);
231
+ worldObjects.delete(obj_id);
232
+ console.log(`Removed object ${obj_id} from scene.`);
233
+ } else {
234
+ console.warn(`Attempted to remove non-existent object ID: ${obj_id}`);
235
+ }
236
+ }
237
+
238
+ function createAndPlaceObject(objData, isNewlyPlacedLocally) {
239
+ if (!objData || !objData.obj_id || !objData.type) {
240
+ console.warn("Invalid object data:", objData);
241
+ return null;
242
+ }
243
+
244
+ let mesh = worldObjects.get(objData.obj_id);
245
+ let isNew = false;
246
+
247
+ if (mesh) {
248
+ if (mesh.position.distanceToSquared(new THREE.Vector3(objData.position.x, objData.position.y, objData.position.z)) > 0.001) {
249
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
250
  }
251
+ if (objData.rotation && (
252
+ Math.abs(mesh.rotation.x - objData.rotation._x) > 0.01 ||
253
+ Math.abs(mesh.rotation.y - objData.rotation._y) > 0.01 ||
254
+ Math.abs(mesh.rotation.z - objData.rotation._z) > 0.01 )) {
255
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
256
+ }
257
+ } else {
258
+ mesh = createPrimitiveMesh(objData.type);
259
+ if (!mesh) return null;
260
+ isNew = true;
261
+ mesh.userData.obj_id = objData.obj_id;
262
+ mesh.userData.type = objData.type;
263
+ mesh.position.set(objData.position.x, objData.position.y, objData.position.z);
264
+ if (objData.rotation) {
265
+ mesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
266
+ }
267
+ scene.add(mesh);
268
+ worldObjects.set(objData.obj_id, mesh);
269
+ console.log(`Created new object ${objData.obj_id} (${objData.type})`);
270
+ }
271
+ return mesh;
272
+ }
273
+
274
+ function createPrimitiveMesh(type) {
275
+ let mesh = null;
276
+ let geometry, material, material2;
277
+
278
+ const wood = objectMaterials.wood;
279
+ const leaf = objectMaterials.leaf;
280
+ const stone = objectMaterials.stone;
281
+ const house_wall = objectMaterials.house_wall;
282
+ const house_roof = objectMaterials.house_roof;
283
+ const brick = objectMaterials.brick;
284
+ const metal = objectMaterials.metal;
285
+ const gem = objectMaterials.gem;
286
+ const lightMat = objectMaterials.light;
287
+
288
+ try {
289
+ switch(type) {
290
+ case "Tree":
291
+ mesh = new THREE.Group();
292
+ geometry = new THREE.CylinderGeometry(0.3, 0.4, 2, 8);
293
+ material = wood;
294
+ const trunk = new THREE.Mesh(geometry, material);
295
+ trunk.position.y = 1;
296
+ trunk.castShadow = true;
297
+ trunk.receiveShadow = true;
298
+ mesh.add(trunk);
299
+ geometry = new THREE.IcosahedronGeometry(1.2, 0);
300
+ material = leaf;
301
+ const canopy = new THREE.Mesh(geometry, material);
302
+ canopy.position.y = 2.8;
303
+ canopy.castShadow = true;
304
+ canopy.receiveShadow = false;
305
+ mesh.add(canopy);
306
  break;
307
+ case "Rock":
308
+ geometry = new THREE.IcosahedronGeometry(0.7, 1);
309
+ material = stone;
310
+ mesh = new THREE.Mesh(geometry, material);
311
+ mesh.castShadow = true;
312
+ mesh.receiveShadow = true;
313
+ mesh.scale.set(1, Math.random()*0.4 + 0.8, 1);
314
+ break;
315
+ case "Simple House":
316
+ mesh = new THREE.Group();
317
+ geometry = new THREE.BoxGeometry(2, 1.5, 2.5);
318
+ material = house_wall;
319
+ const body = new THREE.Mesh(geometry, material);
320
+ body.position.y = 0.75;
321
+ body.castShadow = true;
322
+ body.receiveShadow = true;
323
+ mesh.add(body);
324
+ geometry = new THREE.ConeGeometry(1.8, 1, 4);
325
+ material = house_roof;
326
+ const roof = new THREE.Mesh(geometry, material);
327
+ roof.position.y = 1.5 + 0.5;
328
+ roof.rotation.y = Math.PI / 4;
329
+ roof.castShadow = true;
330
+ roof.receiveShadow = false;
331
+ mesh.add(roof);
332
+ break;
333
+ case "Pine Tree":
334
+ mesh = new THREE.Group();
335
+ geometry = new THREE.CylinderGeometry(0.2, 0.3, 2.5, 8);
336
+ material = wood;
337
+ const pineTrunk = new THREE.Mesh(geometry, material);
338
+ pineTrunk.position.y = 1.25;
339
+ pineTrunk.castShadow = true;
340
+ pineTrunk.receiveShadow = true;
341
+ mesh.add(pineTrunk);
342
+ geometry = new THREE.ConeGeometry(1, 2.5, 8);
343
+ material = leaf;
344
+ const pineCanopy = new THREE.Mesh(geometry, material);
345
+ pineCanopy.position.y = 2.5 + (2.5/2) - 0.5;
346
+ pineCanopy.castShadow = true;
347
+ pineCanopy.receiveShadow = false;
348
+ mesh.add(pineCanopy);
349
+ break;
350
+ case "Brick Wall":
351
+ geometry = new THREE.BoxGeometry(3, 2, 0.3);
352
+ material = brick;
353
+ mesh = new THREE.Mesh(geometry, material);
354
+ mesh.position.y = 1;
355
+ mesh.castShadow = true;
356
+ mesh.receiveShadow = true;
357
+ break;
358
+ case "Sphere":
359
+ geometry = new THREE.SphereGeometry(0.8, 16, 12);
360
+ material = metal;
361
+ mesh = new THREE.Mesh(geometry, material);
362
+ mesh.position.y = 0.8;
363
+ mesh.castShadow = true;
364
+ mesh.receiveShadow = true;
365
+ break;
366
+ case "Cube":
367
+ geometry = new THREE.BoxGeometry(1, 1, 1);
368
+ material = stone;
369
+ mesh = new THREE.Mesh(geometry, material);
370
+ mesh.position.y = 0.5;
371
+ mesh.castShadow = true;
372
+ mesh.receiveShadow = true;
373
+ break;
374
+ case "Cylinder":
375
+ geometry = new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16);
376
+ material = metal;
377
+ mesh = new THREE.Mesh(geometry, material);
378
+ mesh.position.y = 0.75;
379
+ mesh.castShadow = true;
380
+ mesh.receiveShadow = true;
381
+ break;
382
+ case "Cone":
383
+ geometry = new THREE.ConeGeometry(0.6, 1.2, 16);
384
+ material = house_roof;
385
+ mesh = new THREE.Mesh(geometry, material);
386
+ mesh.position.y = 0.6;
387
+ mesh.castShadow = true;
388
+ mesh.receiveShadow = true;
389
+ break;
390
+ case "Torus":
391
+ geometry = new THREE.TorusGeometry(0.6, 0.2, 8, 24);
392
+ material = gem;
393
+ mesh = new THREE.Mesh(geometry, material);
394
+ mesh.position.y = 0.7;
395
+ mesh.castShadow = true;
396
+ mesh.receiveShadow = true;
397
+ mesh.rotation.x = Math.PI / 2;
398
+ break;
399
+ case "Mushroom":
400
+ mesh = new THREE.Group();
401
+ geometry = new THREE.CylinderGeometry(0.15, 0.1, 0.6, 8);
402
+ material = house_wall;
403
+ const stem = new THREE.Mesh(geometry, material);
404
+ stem.position.y = 0.3;
405
+ stem.castShadow = true;
406
+ stem.receiveShadow = true;
407
+ mesh.add(stem);
408
+ geometry = new THREE.SphereGeometry(0.4, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2);
409
+ material = house_roof;
410
+ const cap = new THREE.Mesh(geometry, material);
411
+ cap.position.y = 0.6;
412
+ cap.castShadow = true;
413
+ cap.receiveShadow = false;
414
+ mesh.add(cap);
415
+ break;
416
+ case "Cactus":
417
+ mesh = new THREE.Group();
418
+ material = leaf;
419
+ geometry = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
420
+ const main = new THREE.Mesh(geometry, material);
421
+ main.position.y = 0.75;
422
+ main.castShadow = true;
423
+ main.receiveShadow = true;
424
+ mesh.add(main);
425
+ geometry = new THREE.CylinderGeometry(0.2, 0.2, 0.8, 8);
426
+ const arm1 = new THREE.Mesh(geometry, material);
427
+ arm1.position.set(0.3, 1, 0);
428
+ arm1.rotation.z = Math.PI / 4;
429
+ arm1.castShadow = true;
430
+ arm1.receiveShadow = true;
431
+ mesh.add(arm1);
432
+ const arm2 = new THREE.Mesh(geometry, material);
433
+ arm2.position.set(-0.3, 0.8, 0);
434
+ arm2.rotation.z = -Math.PI / 4;
435
+ arm2.castShadow = true;
436
+ arm2.receiveShadow = true;
437
+ mesh.add(arm2);
438
+ break;
439
+ case "Campfire":
440
+ mesh = new THREE.Group();
441
+ material = wood;
442
+ geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 5);
443
+ const log1 = new THREE.Mesh(geometry, material);
444
+ log1.rotation.x = Math.PI/2;
445
+ log1.position.set(0, 0.1, 0.2);
446
+ mesh.add(log1);
447
+ const log2 = new THREE.Mesh(geometry, material);
448
+ log2.rotation.set(Math.PI/2, 0, Math.PI/3);
449
+ log2.position.set(0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6));
450
+ mesh.add(log2);
451
+ const log3 = new THREE.Mesh(geometry, material);
452
+ log3.rotation.set(Math.PI/2, 0, -Math.PI/3);
453
+ log3.position.set(-0.2*Math.cos(Math.PI/6), 0.1, -0.2*Math.sin(Math.PI/6));
454
+ mesh.add(log3);
455
+ material2 = lightMat;
456
+ geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
457
+ const flame = new THREE.Mesh(geometry, material2);
458
+ flame.position.y = 0.35;
459
+ mesh.add(flame);
460
+ break;
461
+ case "Star":
462
+ geometry = new THREE.SphereGeometry(0.5, 4, 2);
463
+ material = lightMat;
464
+ mesh = new THREE.Mesh(geometry, material);
465
+ mesh.position.y = 1;
466
+ break;
467
+ case "Gem":
468
+ geometry = new THREE.OctahedronGeometry(0.6, 0);
469
+ material = gem;
470
+ mesh = new THREE.Mesh(geometry, material);
471
+ mesh.position.y = 0.6;
472
+ mesh.castShadow = true;
473
+ mesh.receiveShadow = true;
474
+ break;
475
+ case "Tower":
476
+ geometry = new THREE.CylinderGeometry(1, 1.2, 5, 8);
477
+ material = stone;
478
+ mesh = new THREE.Mesh(geometry, material);
479
+ mesh.position.y = 2.5;
480
+ mesh.castShadow = true;
481
+ mesh.receiveShadow = true;
482
+ break;
483
+ case "Barrier":
484
+ geometry = new THREE.BoxGeometry(2, 0.5, 0.5);
485
+ material = metal;
486
+ mesh = new THREE.Mesh(geometry, material);
487
+ mesh.position.y = 0.25;
488
+ mesh.castShadow = true;
489
+ mesh.receiveShadow = true;
490
+ break;
491
+ case "Fountain":
492
+ mesh = new THREE.Group();
493
+ material = stone;
494
+ geometry = new THREE.CylinderGeometry(1.5, 1.5, 0.3, 16);
495
+ const baseF = new THREE.Mesh(geometry, material);
496
+ baseF.position.y = 0.15;
497
+ mesh.add(baseF);
498
+ geometry = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 16);
499
+ const midF = new THREE.Mesh(geometry, material);
500
+ midF.position.y = 0.3 + 0.25;
501
+ mesh.add(midF);
502
+ geometry = new THREE.CylinderGeometry(0.4, 0.4, 0.7, 16);
503
+ const topF = new THREE.Mesh(geometry, material);
504
+ topF.position.y = 0.8 + 0.35;
505
+ mesh.add(topF);
506
+ mesh.castShadow = true;
507
+ mesh.receiveShadow = true;
508
+ break;
509
+ case "Lantern":
510
+ mesh = new THREE.Group();
511
+ material = metal;
512
+ geometry = new THREE.BoxGeometry(0.4, 0.6, 0.4);
513
+ const bodyL = new THREE.Mesh(geometry, material);
514
+ bodyL.position.y = 0.3;
515
+ mesh.add(bodyL);
516
+ geometry = new THREE.SphereGeometry(0.15);
517
+ material2 = lightMat;
518
+ const lightL = new THREE.Mesh(geometry, material2);
519
+ lightL.position.y = 0.3;
520
+ mesh.add(lightL);
521
+ mesh.castShadow = true;
522
+ break;
523
+ case "Sign Post":
524
+ mesh = new THREE.Group();
525
+ material = wood;
526
+ geometry = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 8);
527
+ const postS = new THREE.Mesh(geometry, material);
528
+ postS.position.y = 0.9;
529
+ mesh.add(postS);
530
+ geometry = new THREE.BoxGeometry(0.8, 0.4, 0.05);
531
+ const signS = new THREE.Mesh(geometry, material);
532
+ signS.position.y = 1.5;
533
+ mesh.add(signS);
534
+ mesh.castShadow = true;
535
+ mesh.receiveShadow = true;
536
+ break;
537
+ default:
538
+ console.warn("Unknown primitive type for mesh creation:", type);
539
+ return null;
540
  }
541
+ } catch (e) {
542
+ console.error(`Error creating geometry/mesh for type ${type}:`, e);
543
+ return null;
544
  }
545
+
546
+ if (mesh) {
547
+ mesh.userData = { type: type };
548
+ if (!mesh.position.y && mesh.geometry) {
549
+ mesh.geometry.computeBoundingBox();
550
+ mesh.position.y = (mesh.geometry.boundingBox.max.y - mesh.geometry.boundingBox.min.y) / 2;
551
+ }
552
+ }
553
+ return mesh;
554
  }
555
 
556
+ function onMouseMove(event) {
557
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
558
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
559
+ }
560
+
561
+ function onDocumentClick(event) {
562
+ if (selectedObjectType === "None" || !selectedObjectType) return;
563
+
564
+ const groundCandidates = Object.values(groundMeshes);
565
+ if (groundCandidates.length === 0) return;
566
+
567
+ raycaster.setFromCamera(mouse, camera);
568
+ const intersects = raycaster.intersectObjects(groundCandidates);
569
+
570
+ if (intersects.length > 0) {
571
+ const intersectPoint = intersects[0].point;
572
+
573
+ const newObjData = {
574
+ obj_id: THREE.MathUtils.generateUUID(),
575
+ type: selectedObjectType,
576
+ position: { x: intersectPoint.x, y: 0, z: intersectPoint.z },
577
+ rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' }
578
+ };
579
+
580
+ const tempMesh = createPrimitiveMesh(selectedObjectType);
581
+ if (tempMesh && tempMesh.geometry) {
582
+ tempMesh.geometry.computeBoundingBox();
583
+ const height = tempMesh.geometry.boundingBox.max.y - tempMesh.geometry.boundingBox.min.y;
584
+ newObjData.position.y = (height / 2) + intersectPoint.y + 0.01;
585
+ } else {
586
+ newObjData.position.y = 0.5 + intersectPoint.y;
587
+ }
588
+
589
+ console.log(`Placing ${selectedObjectType} (${newObjData.obj_id}) at`, newObjData.position);
590
+
591
+ createAndPlaceObject(newObjData, true);
592
+
593
+ sendWebSocketMessage("place_object", {
594
+ username: myUsername,
595
+ object_data: newObjData
596
+ });
597
+ }
598
+ }
599
+
600
+ function onRightClick(event) {
601
+ event.preventDefault();
602
+ raycaster.setFromCamera(mouse, camera);
603
+ const intersects = raycaster.intersectObjects(Array.from(worldObjects.values()));
604
+
605
+ if (intersects.length > 0) {
606
+ const obj_id = intersects[0].object.userData.obj_id;
607
+ console.log(`Deleting object ${obj_id}`);
608
+ sendWebSocketMessage("delete_object", {
609
+ username: myUsername,
610
+ obj_id: obj_id
611
+ });
612
+ }
613
+ }
614
+
615
+ function onWindowResize() {
616
+ camera.aspect = window.innerWidth / window.innerHeight;
617
+ camera.updateProjectionMatrix();
618
+ renderer.setSize(window.innerWidth, window.innerHeight);
619
+ }
620
+
621
+ function updateSelectedObjectType(newType) {
622
+ console.log("JS updateSelectedObjectType received:", newType);
623
+ selectedObjectType = newType;
624
  }
625
 
626
  function animate() {