awacke1 commited on
Commit
3a030e4
·
verified ·
1 Parent(s): c910728

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +428 -128
index.html CHANGED
@@ -1,12 +1,12 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>Three.js Infinite World</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
8
  </style>
9
- </head>
10
  <body>
11
  <script type="importmap">
12
  {
@@ -19,25 +19,36 @@
19
 
20
  <script type="module">
21
  import * as THREE from 'three';
 
22
 
23
  let scene, camera, renderer, playerMesh;
24
  let raycaster, mouse;
25
  const keysPressed = {};
26
  const playerSpeed = 0.15;
27
- let newlyPlacedObjects = []; // Track objects added THIS session for saving
28
 
29
- // --- Access State from Streamlit ---
30
- const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
31
- const plotsMetadata = window.PLOTS_METADATA || [];
32
- const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
 
 
 
 
 
 
33
  const plotWidth = window.PLOT_WIDTH || 50.0;
34
  const plotDepth = window.PLOT_DEPTH || 50.0;
35
- const globalState = window.GLOBAL_STATE || { objects: [] };
 
36
 
37
- // --- Setup materials and scene
38
  const groundMaterial = new THREE.MeshStandardMaterial({
39
- color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide
40
  });
 
 
 
 
 
41
 
42
  function init() {
43
  scene = new THREE.Scene();
@@ -45,12 +56,12 @@
45
 
46
  const aspect = window.innerWidth / window.innerHeight;
47
  camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
48
- camera.position.set(0, 15, 20);
49
- camera.lookAt(0, 0, 0);
50
  scene.add(camera);
51
 
52
  setupLighting();
53
- setupInitialGround();
54
  setupPlayer();
55
 
56
  raycaster = new THREE.Raycaster();
@@ -59,225 +70,514 @@
59
  renderer = new THREE.WebGLRenderer({ antialias: true });
60
  renderer.setSize(window.innerWidth, window.innerHeight);
61
  renderer.shadowMap.enabled = true;
62
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
63
  document.body.appendChild(renderer.domElement);
64
 
65
- loadInitialObjects();
66
- restoreUnsavedState();
 
 
 
67
 
68
  // Event Listeners
69
  document.addEventListener('mousemove', onMouseMove, false);
70
- document.addEventListener('click', onDocumentClick, false);
71
  window.addEventListener('resize', onWindowResize, false);
72
  document.addEventListener('keydown', onKeyDown);
73
  document.addEventListener('keyup', onKeyUp);
74
 
75
- // Define global functions for Streamlit callbacks.
76
  window.teleportPlayer = teleportPlayer;
77
- window.getSaveDataAndPosition = getSaveDataAndPosition;
78
- window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;
 
79
 
80
- console.log("Three.js Initialized. World ready.");
 
 
 
81
  animate();
82
  }
83
 
84
  function setupLighting() {
85
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
86
  scene.add(ambientLight);
87
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
88
- directionalLight.position.set(50, 150, 100);
89
  directionalLight.castShadow = true;
90
- directionalLight.shadow.mapSize.width = 4096;
91
- directionalLight.shadow.mapSize.height = 4096;
92
- directionalLight.shadow.camera.near = 0.5;
93
- directionalLight.shadow.camera.far = 500;
94
- directionalLight.shadow.camera.left = -150;
95
  directionalLight.shadow.camera.right = 150;
96
  directionalLight.shadow.camera.top = 150;
97
  directionalLight.shadow.camera.bottom = -150;
98
- directionalLight.shadow.bias = -0.001;
99
  scene.add(directionalLight);
 
 
 
100
  }
101
 
102
  function setupInitialGround() {
103
- console.log(`Setting up initial ground for ${plotsMetadata.length} plots.`);
104
  plotsMetadata.forEach(plot => {
105
- createGroundPlane(plot.grid_x, plot.grid_z, false);
106
  });
107
- if (plotsMetadata.length === 0) {
108
- createGroundPlane(0, 0, false);
 
109
  }
110
  }
111
 
112
  function createGroundPlane(gridX, gridZ, isPlaceholder) {
113
  const gridKey = `${gridX}_${gridZ}`;
 
 
 
114
  const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
115
- const material = groundMaterial;
116
  const groundMesh = new THREE.Mesh(groundGeometry, material);
 
117
  groundMesh.rotation.x = -Math.PI / 2;
118
- groundMesh.position.x = gridX * plotWidth + plotWidth / 2;
119
- groundMesh.position.z = gridZ * plotDepth + plotDepth / 2;
 
 
120
  groundMesh.receiveShadow = true;
 
 
121
  scene.add(groundMesh);
 
 
122
  }
123
 
124
  function setupPlayer() {
125
  const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
126
- const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
127
  playerMesh = new THREE.Mesh(playerGeo, playerMat);
128
- playerMesh.position.set(plotWidth/2, 1, plotDepth/2);
129
  playerMesh.castShadow = true;
 
130
  scene.add(playerMesh);
131
  }
132
 
133
- function loadInitialObjects() {
134
- console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
135
- allInitialObjects.forEach(objData => { createAndPlaceObject(objData, false); });
136
- // Also load objects from the global state (if any).
137
- console.log(`Loading ${globalState.objects ? globalState.objects.length : 0} objects from global state.`);
138
- if(globalState.objects) {
139
- globalState.objects.forEach(objData => { createAndPlaceObject(objData, false); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
- console.log("Finished loading objects.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  }
143
 
144
- function createAndPlaceObject(objData, isNewObject) {
 
145
  let loadedObject = null;
146
- switch (objData.type) {
 
 
 
147
  case "Simple House": loadedObject = createSimpleHouse(); break;
148
  case "Tree": loadedObject = createTree(); break;
149
  case "Rock": loadedObject = createRock(); break;
150
  case "Fence Post": loadedObject = createFencePost(); break;
151
- default: console.warn("Unknown object type:", objData.type); break;
152
  }
 
153
  if (loadedObject) {
154
- // Prefer properties from objData using either new or old key names.
155
- if(objData.position && objData.position.x !== undefined) {
 
 
 
 
 
156
  loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
157
- } else if (objData.pos_x !== undefined) {
158
  loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
159
  }
160
- if (objData.rotation) {
161
- loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
162
- } else if (objData.rot_x !== undefined) {
163
- loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
164
- }
165
- loadedObject.userData.obj_id = objData.obj_id || loadedObject.userData.obj_id;
 
 
 
166
  scene.add(loadedObject);
167
- if (isNewObject) { newlyPlacedObjects.push(loadedObject); }
 
 
 
 
 
 
168
  return loadedObject;
169
  }
170
  return null;
171
  }
172
 
173
- // Sample object creation functions.
 
 
 
 
174
  function createSimpleHouse() {
175
- const group = new THREE.Group();
176
- const mat1 = new THREE.MeshStandardMaterial({color:0xffccaa});
177
- const m1 = new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5), mat1);
178
- m1.position.y = 0.75;
179
- group.add(m1);
180
- return group;
181
  }
182
  function createTree() {
183
- const group = new THREE.Group();
184
- const mat1 = new THREE.MeshStandardMaterial({color:0x8B4513});
185
- const m1 = new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8), mat1);
186
- m1.position.y = 1;
187
- group.add(m1);
188
- const mat2 = new THREE.MeshStandardMaterial({color:0x228B22});
189
- const m2 = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0), mat2);
190
- m2.position.y = 2.8;
191
- group.add(m2);
192
- return group;
193
  }
194
  function createRock() {
195
- const mat = new THREE.MeshStandardMaterial({color:0xaaaaaa});
196
- const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0), mat);
197
- rock.position.y = 0.35;
198
- rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
199
- return rock;
 
 
 
 
 
 
 
200
  }
201
  function createFencePost() {
202
- const mat = new THREE.MeshStandardMaterial({color:0xdeb887});
203
- const post = new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2), mat);
204
- post.position.y = 0.75;
205
- return post;
206
  }
207
 
 
 
208
  function onMouseMove(event) {
 
209
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
210
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
211
  }
212
 
213
  function onDocumentClick(event) {
214
  if (selectedObjectType === "None") return;
 
 
 
 
 
215
  raycaster.setFromCamera(mouse, camera);
216
- const intersects = raycaster.intersectObjects(scene.children);
 
217
  if (intersects.length > 0) {
218
- const intersectPoint = intersects[0].point;
219
- let newObject = null;
220
- switch (selectedObjectType) {
221
- case "Simple House": newObject = createSimpleHouse(); break;
222
- case "Tree": newObject = createTree(); break;
223
- case "Rock": newObject = createRock(); break;
224
- case "Fence Post": newObject = createFencePost(); break;
225
- default: return;
226
- }
227
- if (newObject) {
228
- newObject.position.copy(intersectPoint);
229
- scene.add(newObject);
230
- newlyPlacedObjects.push(newObject);
231
- console.log(`Placed new ${selectedObjectType}. Total new objects: ${newlyPlacedObjects.length}`);
232
- }
 
 
 
 
 
 
 
233
  }
234
  }
235
 
236
  function onKeyDown(event) { keysPressed[event.code] = true; }
237
  function onKeyUp(event) { keysPressed[event.code] = false; }
 
 
 
 
 
238
 
 
239
  function teleportPlayer(targetX, targetZ) {
 
240
  if (playerMesh) {
241
  playerMesh.position.x = targetX;
242
  playerMesh.position.z = targetZ;
243
- camera.position.x = targetX;
244
- camera.position.z = targetZ + 20;
245
- console.log("Player teleported.");
246
- }
 
 
 
247
  }
248
 
249
- // This function packages new object data and player position for Streamlit.
250
- function getSaveDataAndPosition() {
251
- const objectsToSave = newlyPlacedObjects.map(obj => {
252
- return {
253
- obj_id: obj.userData.obj_id,
254
- type: selectedObjectType,
255
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
256
- rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
257
- };
258
- });
259
- // Use the player mesh's position as the player position.
260
- const playerPos = playerMesh ? { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z } : {x:0, y:0, z:0};
261
- const payload = {
262
- playerPosition: playerPos,
263
- objectsToSave: objectsToSave
264
- };
265
- console.log("Prepared payload:", payload);
266
- // Clear newlyPlacedObjects after packaging.
267
- newlyPlacedObjects = [];
268
- return JSON.stringify(payload);
269
  }
270
 
 
271
  function resetNewlyPlacedObjects() {
272
- newlyPlacedObjects = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
 
275
  function animate() {
276
  requestAnimationFrame(animate);
 
 
277
  renderer.render(scene, camera);
278
  }
279
 
 
280
  init();
 
281
  </script>
282
  </body>
283
- </html>
 
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
+ <title>Three.js Shared World</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
8
  </style>
9
+ </head>
10
  <body>
11
  <script type="importmap">
12
  {
 
19
 
20
  <script type="module">
21
  import * as THREE from 'three';
22
+ // No OrbitControls needed if using custom player movement
23
 
24
  let scene, camera, renderer, playerMesh;
25
  let raycaster, mouse;
26
  const keysPressed = {};
27
  const playerSpeed = 0.15;
 
28
 
29
+ // --- State Management ---
30
+ let newlyPlacedObjects = []; // Track objects added THIS client, THIS session, NOT YET SAVED
31
+ const serverObjectsMap = new Map(); // Map<obj_id, THREE.Object3D> - Objects loaded from server
32
+ let pollIntervalId = null; // To store the interval timer
33
+
34
+
35
+ // --- Access State from Streamlit (Injected) ---
36
+ const initialWorldState = window.INITIAL_WORLD_STATE || []; // List of authoritative objects on load
37
+ const plotsMetadata = window.PLOTS_METADATA || []; // List of saved plot info
38
+ let selectedObjectType = window.SELECTED_OBJECT_TYPE || "None"; // Can be updated by Python
39
  const plotWidth = window.PLOT_WIDTH || 50.0;
40
  const plotDepth = window.PLOT_DEPTH || 50.0;
41
+ const pollIntervalMs = window.STATE_POLL_INTERVAL_MS || 5000;
42
+
43
 
 
44
  const groundMaterial = new THREE.MeshStandardMaterial({
45
+ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide, name: "ground_saved"
46
  });
47
+ const placeholderGroundMaterial = new THREE.MeshStandardMaterial({
48
+ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide, name: "ground_placeholder"
49
+ });
50
+ const groundMeshes = {}; // Store references to ground meshes: 'x_z' string key -> mesh
51
+
52
 
53
  function init() {
54
  scene = new THREE.Scene();
 
56
 
57
  const aspect = window.innerWidth / window.innerHeight;
58
  camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
59
+ camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20); // Start looking at center of 0,0
60
+ camera.lookAt(plotWidth/2, 0, plotDepth/2);
61
  scene.add(camera);
62
 
63
  setupLighting();
64
+ setupInitialGround(); // Setup ground based on PLOTS_METADATA
65
  setupPlayer();
66
 
67
  raycaster = new THREE.Raycaster();
 
70
  renderer = new THREE.WebGLRenderer({ antialias: true });
71
  renderer.setSize(window.innerWidth, window.innerHeight);
72
  renderer.shadowMap.enabled = true;
73
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
74
  document.body.appendChild(renderer.domElement);
75
 
76
+ // --- Load Initial Objects provided by Server ---
77
+ console.log(`Loading ${initialWorldState.length} initial objects from Python.`);
78
+ synchronizeWorldState(initialWorldState); // Use the sync function for initial load too
79
+
80
+ // restoreUnsavedState(); // Maybe not needed if polling handles everything? Or keep for page reloads? Let's disable for now.
81
 
82
  // Event Listeners
83
  document.addEventListener('mousemove', onMouseMove, false);
84
+ document.addEventListener('click', onDocumentClick, false); // Place object
85
  window.addEventListener('resize', onWindowResize, false);
86
  document.addEventListener('keydown', onKeyDown);
87
  document.addEventListener('keyup', onKeyUp);
88
 
89
+ // --- Define global functions needed by Python ---
90
  window.teleportPlayer = teleportPlayer;
91
+ window.getNewlyPlacedObjectsForSave = getNewlyPlacedObjectsForSave; // Function for save button
92
+ window.resetNewlyPlacedObjects = resetNewlyPlacedObjects; // Called by Python after successful save
93
+ window.updateSelectedObjectType = updateSelectedObjectType; // Called by Python when selectbox changes
94
 
95
+ // --- Start Polling for State Updates ---
96
+ startPolling();
97
+
98
+ console.log("Three.js Initialized. World ready. Starting state polling.");
99
  animate();
100
  }
101
 
102
  function setupLighting() {
103
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Slightly brighter ambient
104
  scene.add(ambientLight);
105
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); // Stronger directional
106
+ directionalLight.position.set(75, 150, 100); // Angled light
107
  directionalLight.castShadow = true;
108
+ directionalLight.shadow.mapSize.width = 2048; // Balance performance and quality
109
+ directionalLight.shadow.mapSize.height = 2048;
110
+ directionalLight.shadow.camera.near = 10; // Adjust frustum based on typical scene scale
111
+ directionalLight.shadow.camera.far = 400;
112
+ directionalLight.shadow.camera.left = -150; // Wider shadow area
113
  directionalLight.shadow.camera.right = 150;
114
  directionalLight.shadow.camera.top = 150;
115
  directionalLight.shadow.camera.bottom = -150;
116
+ directionalLight.shadow.bias = -0.002; // Adjust shadow bias carefully
117
  scene.add(directionalLight);
118
+ // Add a hemisphere light for softer fill
119
+ const hemiLight = new THREE.HemisphereLight( 0xabcdef, 0x55aa55, 0.5 );
120
+ scene.add( hemiLight );
121
  }
122
 
123
  function setupInitialGround() {
124
+ console.log(`Setting up initial ground for ${plotsMetadata.length} saved plots.`);
125
  plotsMetadata.forEach(plot => {
126
+ createGroundPlane(plot.grid_x, plot.grid_z, false); // false = not a placeholder
127
  });
128
+ // Ensure ground at 0,0 exists if no plots are saved yet
129
+ if (!groundMeshes['0_0']) {
130
+ createGroundPlane(0, 0, false); // Create the starting ground if needed
131
  }
132
  }
133
 
134
  function createGroundPlane(gridX, gridZ, isPlaceholder) {
135
  const gridKey = `${gridX}_${gridZ}`;
136
+ if (groundMeshes[gridKey]) return groundMeshes[gridKey]; // Don't recreate
137
+
138
+ // console.log(`Creating ${isPlaceholder ? 'placeholder' : 'initial'} ground at ${gridX}, ${gridZ}`);
139
  const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
140
+ const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
141
  const groundMesh = new THREE.Mesh(groundGeometry, material);
142
+
143
  groundMesh.rotation.x = -Math.PI / 2;
144
+ groundMesh.position.y = -0.05; // Slightly below 0
145
+ groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
146
+ groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
147
+
148
  groundMesh.receiveShadow = true;
149
+ groundMesh.userData.gridKey = gridKey;
150
+ groundMesh.userData.isPlaceholder = isPlaceholder;
151
  scene.add(groundMesh);
152
+ groundMeshes[gridKey] = groundMesh; // Store reference
153
+ return groundMesh;
154
  }
155
 
156
  function setupPlayer() {
157
  const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
158
+ const playerMat = new THREE.MeshStandardMaterial({ color: 0x0055ff, roughness: 0.6 });
159
  playerMesh = new THREE.Mesh(playerGeo, playerMat);
160
+ playerMesh.position.set(plotWidth / 2, 0.8, plotDepth / 2); // Start Y pos based on capsule height/2 + radius maybe?
161
  playerMesh.castShadow = true;
162
+ playerMesh.receiveShadow = false; // Player shouldn't receive shadows on itself much
163
  scene.add(playerMesh);
164
  }
165
 
166
+ // --- Core State Synchronization Logic ---
167
+ function synchronizeWorldState(serverStateList) {
168
+ console.log(`Synchronizing state. Received ${serverStateList.length} objects from server.`);
169
+ const receivedIds = new Set();
170
+ let updatedCount = 0;
171
+ let addedCount = 0;
172
+ let removedCount = 0;
173
+
174
+ // 1. Add new objects and Update existing ones
175
+ serverStateList.forEach(objData => {
176
+ if (!objData || !objData.obj_id) {
177
+ console.warn("Received invalid object data during sync:", objData);
178
+ return;
179
+ }
180
+ receivedIds.add(objData.obj_id);
181
+ const existingMesh = serverObjectsMap.get(objData.obj_id);
182
+
183
+ if (existingMesh) {
184
+ // Object exists, check if update needed (simple position/rotation check)
185
+ let needsUpdate = false;
186
+ if (existingMesh.position.distanceToSquared(objData.position) > 0.01) { // Use squared distance, epsilon check
187
+ existingMesh.position.set(objData.position.x, objData.position.y, objData.position.z);
188
+ needsUpdate = true;
189
+ }
190
+ // TODO: Add more robust rotation check if needed (e.g., quaternion comparison)
191
+ if (objData.rotation &&
192
+ (Math.abs(existingMesh.rotation.x - objData.rotation._x) > 0.01 ||
193
+ Math.abs(existingMesh.rotation.y - objData.rotation._y) > 0.01 ||
194
+ Math.abs(existingMesh.rotation.z - objData.rotation._z) > 0.01 )) {
195
+ existingMesh.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
196
+ needsUpdate = true;
197
+ }
198
+ if(needsUpdate) updatedCount++;
199
+
200
+ } else {
201
+ // Object is new, create and add it
202
+ const newMesh = createAndPlaceObject(objData, false); // false = not a "newly placed" local object
203
+ if (newMesh) {
204
+ serverObjectsMap.set(objData.obj_id, newMesh); // Add to our server map
205
+ addedCount++;
206
+ } else {
207
+ console.warn("Failed to create object mesh for:", objData);
208
+ }
209
+ }
210
+ });
211
+
212
+ // 2. Remove objects that are in our scene but NOT in the server list
213
+ const idsToRemove = [];
214
+ for (const [obj_id, mesh] of serverObjectsMap.entries()) {
215
+ if (!receivedIds.has(obj_id)) {
216
+ // This object was removed on the server
217
+ scene.remove(mesh);
218
+ // Dispose geometry/material? Important for larger scenes
219
+ if (mesh.geometry) mesh.geometry.dispose();
220
+ if (mesh.material) {
221
+ if (Array.isArray(mesh.material)) {
222
+ mesh.material.forEach(m => m.dispose());
223
+ } else {
224
+ mesh.material.dispose();
225
+ }
226
+ }
227
+ idsToRemove.push(obj_id);
228
+ removedCount++;
229
+ }
230
+ }
231
+
232
+ // Remove from the map after iteration
233
+ idsToRemove.forEach(id => serverObjectsMap.delete(id));
234
+
235
+ if(addedCount > 0 || removedCount > 0 || updatedCount > 0) {
236
+ console.log(`Sync complete: ${addedCount} added, ${updatedCount} updated, ${removedCount} removed.`);
237
  }
238
+ // else { console.log("Sync complete: No changes detected."); }
239
+
240
+ // 3. Update ground planes based on latest plot metadata (if it changes - less frequent)
241
+ // This could be done less often than object sync if metadata is stable.
242
+ updateGroundFromMetadata();
243
+ }
244
+
245
+ function updateGroundFromMetadata() {
246
+ const currentMetadata = window.PLOTS_METADATA || []; // Get potentially updated metadata
247
+ const metadataKeys = new Set();
248
+
249
+ // Ensure ground exists for all saved plots and is not a placeholder
250
+ currentMetadata.forEach(plot => {
251
+ const gridKey = `${plot.grid_x}_${plot.grid_z}`;
252
+ metadataKeys.add(gridKey);
253
+ const existingGround = groundMeshes[gridKey];
254
+ if (existingGround) {
255
+ // If it exists but was a placeholder, upgrade it
256
+ if (existingGround.userData.isPlaceholder) {
257
+ console.log(`Upgrading placeholder ground at ${gridKey} to saved.`);
258
+ existingGround.material = groundMaterial;
259
+ existingGround.userData.isPlaceholder = false;
260
+ }
261
+ } else {
262
+ // If it doesn't exist, create it as a saved plot
263
+ console.log(`Creating missing saved ground at ${gridKey}.`);
264
+ createGroundPlane(plot.grid_x, plot.grid_z, false);
265
+ }
266
+ });
267
+
268
+ // Optional: Remove placeholder ground if a plot was deleted externally?
269
+ // Or just let them persist visually until player moves away.
270
  }
271
 
272
+
273
+ function createAndPlaceObject(objData, isNewlyPlaced) {
274
  let loadedObject = null;
275
+ const objType = objData.type;
276
+
277
+ // Use a factory pattern for cleaner object creation
278
+ switch (objType) {
279
  case "Simple House": loadedObject = createSimpleHouse(); break;
280
  case "Tree": loadedObject = createTree(); break;
281
  case "Rock": loadedObject = createRock(); break;
282
  case "Fence Post": loadedObject = createFencePost(); break;
283
+ default: console.warn("Unknown object type:", objType); return null;
284
  }
285
+
286
  if (loadedObject) {
287
+ // Set common properties
288
+ loadedObject.userData.obj_id = objData.obj_id || THREE.MathUtils.generateUUID(); // Ensure ID exists
289
+ loadedObject.userData.type = objType;
290
+ loadedObject.userData.isNewlyPlaced = isNewlyPlaced; // Flag if local & unsaved
291
+
292
+ // Set position (handle both incoming formats)
293
+ if (objData.position && objData.position.x !== undefined) {
294
  loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
295
+ } else if (objData.pos_x !== undefined) { // Fallback for older format if needed
296
  loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
297
  }
298
+
299
+ // Set rotation (handle both incoming formats)
300
+ if (objData.rotation && objData.rotation._x !== undefined) {
301
+ loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
302
+ } else if (objData.rot_x !== undefined) { // Fallback
303
+ loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
304
+ }
305
+
306
+ // Add to scene
307
  scene.add(loadedObject);
308
+
309
+ // Track if it's a new local object needing save
310
+ if (isNewlyPlaced) {
311
+ newlyPlacedObjects.push(loadedObject);
312
+ console.log(`Tracked new local object: ${objType} (${loadedObject.userData.obj_id}). Total unsaved: ${newlyPlacedObjects.length}`);
313
+ // No session storage saving needed now, rely on polling/save button
314
+ }
315
  return loadedObject;
316
  }
317
  return null;
318
  }
319
 
320
+ // --- Object Creation Functions (Factories) ---
321
+ // Add castShadow = true to all relevant parts
322
+ function createObjectBase(type) {
323
+ return { userData: { type: type } }; // obj_id added later
324
+ }
325
  function createSimpleHouse() {
326
+ const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base);
327
+ const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7});
328
+ const m1=new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5),mat1); m1.position.y=1.5/2; m1.castShadow=true; m1.receiveShadow=true; group.add(m1);
329
+ const m2=new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4),mat2); m2.position.y=1.5+1/2; m2.rotation.y=Math.PI/4; m2.castShadow=true; m2.receiveShadow=false; group.add(m2); return group;
 
 
330
  }
331
  function createTree() {
332
+ const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base);
333
+ const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8});
334
+ const m1=new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8),mat1); m1.position.y=1; m1.castShadow=true; m1.receiveShadow=true; group.add(m1);
335
+ const m2=new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0),mat2); m2.position.y=2.8; m2.castShadow=true; m2.receiveShadow=false; group.add(m2); return group;
 
 
 
 
 
 
336
  }
337
  function createRock() {
338
+ const base=createObjectBase("Rock"); const mat=new THREE.MeshStandardMaterial({color:0xaaaaaa,roughness:0.8,metalness:0.1});
339
+ const geo = new THREE.IcosahedronGeometry(0.7, 1); // Slightly more detail
340
+ geo.positionData = geo.attributes.position.array; // Deform geometry slightly
341
+ for (let i = 0; i < geo.positionData.length; i += 3) {
342
+ const noise = Math.random() * 0.15 - 0.075; // Small random displacement
343
+ geo.positionData[i] *= (1 + noise);
344
+ geo.positionData[i+1] *= (1 + noise);
345
+ geo.positionData[i+2] *= (1 + noise);
346
+ }
347
+ geo.computeVertexNormals(); // Recalculate normals after deformation
348
+ const rock=new THREE.Mesh(geo,mat); Object.assign(rock,base);
349
+ rock.position.y=0.35; rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI); rock.castShadow=true; rock.receiveShadow=true; return rock;
350
  }
351
  function createFencePost() {
352
+ const base=createObjectBase("Fence Post"); const mat=new THREE.MeshStandardMaterial({color:0xdeb887,roughness:0.9});
353
+ const post=new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2),mat); Object.assign(post,base);
354
+ post.position.y=0.75; post.castShadow=true; post.receiveShadow=true; return post;
 
355
  }
356
 
357
+
358
+ // --- Event Handlers ---
359
  function onMouseMove(event) {
360
+ // Calculate mouse position in normalized device coordinates (-1 to +1)
361
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
362
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
363
  }
364
 
365
  function onDocumentClick(event) {
366
  if (selectedObjectType === "None") return;
367
+
368
+ // Find all ground meshes (saved and placeholder)
369
+ const groundCandidates = Object.values(groundMeshes);
370
+ if (groundCandidates.length === 0) return; // No ground to place on
371
+
372
  raycaster.setFromCamera(mouse, camera);
373
+ const intersects = raycaster.intersectObjects(groundCandidates); // Intersect with all ground planes
374
+
375
  if (intersects.length > 0) {
376
+ const intersectPoint = intersects[0].point; // Use the closest intersection point
377
+ const clickedGroundMesh = intersects[0].object; // The specific ground mesh clicked
378
+
379
+ // Create the object data structure first
380
+ const newObjData = {
381
+ obj_id: THREE.MathUtils.generateUUID(), // Generate ID immediately
382
+ type: selectedObjectType,
383
+ position: { x: intersectPoint.x, y: intersectPoint.y, z: intersectPoint.z }, // Y will be adjusted
384
+ rotation: { _x: 0, _y: Math.random() * Math.PI * 2, _z: 0, _order: 'XYZ' } // Random Y rotation
385
+ };
386
+
387
+ // Adjust Y position based on object type after creation, before adding to scene
388
+ // (Example: ensures base is near y=0)
389
+ // This logic might need refinement based on your object origins
390
+ if(selectedObjectType === "Simple House") newObjData.position.y = 0;
391
+ else if(selectedObjectType === "Tree") newObjData.position.y = 0;
392
+ else if(selectedObjectType === "Rock") newObjData.position.y = 0; // Rock origin is center, adjust later maybe?
393
+ else if(selectedObjectType === "Fence Post") newObjData.position.y = 0;
394
+ // else default y = intersectPoint.y (which is ~0 on flat ground)
395
+
396
+ // Now create the mesh and add it to the scene, marking as newly placed
397
+ createAndPlaceObject(newObjData, true); // true = is newly placed
398
  }
399
  }
400
 
401
  function onKeyDown(event) { keysPressed[event.code] = true; }
402
  function onKeyUp(event) { keysPressed[event.code] = false; }
403
+ function onWindowResize() {
404
+ camera.aspect = window.innerWidth / window.innerHeight;
405
+ camera.updateProjectionMatrix();
406
+ renderer.setSize(window.innerWidth, window.innerHeight);
407
+ }
408
 
409
+ // --- Functions called by Python ---
410
  function teleportPlayer(targetX, targetZ) {
411
+ console.log(`JS teleportPlayer called: x=${targetX}, z=${targetZ}`);
412
  if (playerMesh) {
413
  playerMesh.position.x = targetX;
414
  playerMesh.position.z = targetZ;
415
+ // Force camera update immediately after teleport
416
+ const offset = new THREE.Vector3(0, 15, 20); // Keep consistent offset
417
+ const targetPosition = playerMesh.position.clone().add(offset);
418
+ camera.position.copy(targetPosition);
419
+ camera.lookAt(playerMesh.position);
420
+ console.log("Player teleported to:", playerMesh.position);
421
+ } else { console.error("Player mesh not found for teleport."); }
422
  }
423
 
424
+ // Called by Python's save button
425
+ function getNewlyPlacedObjectsForSave() {
426
+ console.log(`JS getNewlyPlacedObjectsForSave called. Found ${newlyPlacedObjects.length} objects.`);
427
+ // IMPORTANT: Send a *copy* of the data, not the live Three.js objects.
428
+ // Use WORLD coordinates as they are in the scene.
429
+ const dataToSend = newlyPlacedObjects.map(obj => {
430
+ if (!obj.userData || !obj.userData.type || !obj.userData.obj_id) return null;
431
+ return {
432
+ obj_id: obj.userData.obj_id,
433
+ type: obj.userData.type,
434
+ position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
435
+ rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
436
+ };
437
+ }).filter(obj => obj !== null); // Filter out any potential nulls
438
+
439
+ console.log("Prepared data for saving:", dataToSend);
440
+ // Return as JSON string for streamlit_js_eval
441
+ return JSON.stringify(dataToSend);
 
 
442
  }
443
 
444
+ // Called by Python AFTER successful save and cache clear
445
  function resetNewlyPlacedObjects() {
446
+ console.log(`JS resetNewlyPlacedObjects called.`);
447
+ // Objects just saved are now part of the authoritative state.
448
+ // They should NOT be tracked as 'newly placed' anymore.
449
+ // They will be handled by the regular sync process.
450
+ // We don't remove them from the scene here, sync will keep them.
451
+ newlyPlacedObjects.forEach(obj => {
452
+ if(obj.userData) obj.userData.isNewlyPlaced = false; // Mark them as not-new locally
453
+ });
454
+ newlyPlacedObjects = []; // Clear the local tracking array
455
+ // No need to clear session storage as we aren't using it now.
456
+ console.log("Local 'newly placed' object list cleared.");
457
+ }
458
+
459
+ // Called by Python when the selectbox changes
460
+ function updateSelectedObjectType(newType) {
461
+ console.log("JS updateSelectedObjectType received:", newType);
462
+ selectedObjectType = newType;
463
+ // Optionally provide visual feedback (e.g., change cursor)
464
+ }
465
+
466
+
467
+ // --- Polling Logic ---
468
+ function startPolling() {
469
+ if (pollIntervalId) {
470
+ clearInterval(pollIntervalId); // Clear existing interval if any
471
+ }
472
+ console.log(`Starting state polling every ${pollIntervalMs}ms`);
473
+
474
+ // Initial immediate poll request? Or wait for first interval? Wait seems fine.
475
+ pollIntervalId = setInterval(async () => {
476
+ // Check if the request function exists (it's set up by Python)
477
+ if (typeof window.requestStateUpdate === 'function') {
478
+ try {
479
+ console.log("Polling server for state update...");
480
+ const latestState = await window.requestStateUpdate(); // Calls the Python function
481
+
482
+ if (latestState && Array.isArray(latestState)) {
483
+ // We got the state, synchronize the scene
484
+ synchronizeWorldState(latestState);
485
+ } else {
486
+ console.warn("Polling received invalid state:", latestState);
487
+ }
488
+ } catch (error) {
489
+ console.error("Error during state polling:", error);
490
+ // Optional: Implement backoff or stop polling after too many errors
491
+ }
492
+ } else {
493
+ console.warn("window.requestStateUpdate function not available for polling yet.");
494
+ // It might take a moment for streamlit_js_eval to set up the function
495
+ }
496
+ }, pollIntervalMs);
497
+ }
498
+
499
+
500
+ // --- Animation Loop ---
501
+ function updatePlayerMovement() {
502
+ if (!playerMesh) return;
503
+ const moveDirection = new THREE.Vector3(0, 0, 0);
504
+ if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
505
+ if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
506
+ if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
507
+ if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
508
+
509
+ if (moveDirection.lengthSq() > 0) {
510
+ // Camera-relative movement
511
+ const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize();
512
+ const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
513
+ const worldMove = new THREE.Vector3();
514
+ worldMove.add(forward.multiplyScalar(-moveDirection.z)); // W/S maps to camera forward/backward
515
+ worldMove.add(right.multiplyScalar(-moveDirection.x)); // A/D maps to camera left/right
516
+ worldMove.normalize().multiplyScalar(playerSpeed);
517
+
518
+ playerMesh.position.add(worldMove);
519
+ // Basic ground clamping (replace with better physics later if needed)
520
+ playerMesh.position.y = Math.max(playerMesh.position.y, 0.8); // Adjust based on player geometry origin
521
+
522
+ // Check if we need to create *placeholder* ground nearby
523
+ checkAndExpandGroundVisuals();
524
+ }
525
+ }
526
+
527
+ // Create visual placeholder ground dynamically
528
+ function checkAndExpandGroundVisuals() {
529
+ if (!playerMesh) return;
530
+ const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
531
+ const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
532
+ const viewDistanceGrids = 2; // How many grids away to check/create placeholders
533
+
534
+ for (let dx = -viewDistanceGrids; dx <= viewDistanceGrids; dx++) {
535
+ for (let dz = -viewDistanceGrids; dz <= viewDistanceGrids; dz++) {
536
+ // if (dx === 0 && dz === 0) continue; // Check current cell too
537
+
538
+ const checkX = currentGridX + dx;
539
+ const checkZ = currentGridZ + dz;
540
+ const gridKey = `${checkX}_${checkZ}`;
541
+
542
+ // If ground doesn't exist AT ALL for this grid key, create a placeholder
543
+ if (!groundMeshes[gridKey]) {
544
+ // Check if it's actually a *saved* plot according to metadata
545
+ const isSaved = plotsMetadata.some(p => p.grid_x === checkX && p.grid_z === checkZ);
546
+ if (!isSaved) {
547
+ // Only create placeholder if it's NOT a known saved plot
548
+ // console.log(`Creating placeholder ground at ${gridKey}`); // Can be noisy
549
+ createGroundPlane(checkX, checkZ, true); // true = is placeholder
550
+ }
551
+ // If it IS saved but missing, the synchronizeWorldState/updateGround should handle it
552
+ }
553
+ }
554
+ }
555
+ // Optional: Add logic here to *remove* distant placeholder ground meshes for performance
556
+ }
557
+
558
+
559
+ function updateCamera() {
560
+ if (!playerMesh) return;
561
+ // Smooth follow camera
562
+ const offset = new THREE.Vector3(0, 12, 18); // Adjust offset for desired view
563
+ const targetPosition = playerMesh.position.clone().add(offset);
564
+ // Use lerp for smooth camera movement
565
+ camera.position.lerp(targetPosition, 0.08); // Adjust lerp factor for smoothness (lower is smoother)
566
+ // Always look at the player's approximate head position
567
+ const lookAtTarget = playerMesh.position.clone().add(new THREE.Vector3(0, 0.5, 0));
568
+ camera.lookAt(lookAtTarget);
569
  }
570
 
571
  function animate() {
572
  requestAnimationFrame(animate);
573
+ updatePlayerMovement();
574
+ updateCamera();
575
  renderer.render(scene, camera);
576
  }
577
 
578
+ // --- Start ---
579
  init();
580
+
581
  </script>
582
  </body>
583
+ </html>