awacke1 commited on
Commit
84a18e0
·
verified ·
1 Parent(s): c4d7d14

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +216 -186
index.html CHANGED
@@ -1,11 +1,10 @@
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
- /* No Save button needed here anymore */
9
  </style>
10
  </head>
11
  <body>
@@ -21,20 +20,30 @@
21
  <script type="module">
22
  import * as THREE from 'three';
23
 
24
- let scene, camera, renderer, groundMesh = null, playerMesh;
25
  let raycaster, mouse;
26
  const keysPressed = {};
27
  const playerSpeed = 0.15;
28
  let newlyPlacedObjects = []; // Track objects added THIS session for saving
 
 
29
 
30
  // --- Session Storage Key ---
31
- const SESSION_STORAGE_KEY = 'unsavedWorldBuilderState';
32
 
33
  // --- Access State from Streamlit ---
34
  const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
 
35
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
36
  const plotWidth = window.PLOT_WIDTH || 50.0;
37
- const nextPlotXOffset = window.NEXT_PLOT_X_OFFSET || 0.0; // Where the next plot starts
 
 
 
 
 
 
 
38
 
39
 
40
  function init() {
@@ -42,13 +51,13 @@
42
  scene.background = new THREE.Color(0xabcdef);
43
 
44
  const aspect = window.innerWidth / window.innerHeight;
45
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 2000); // Increase far plane
46
  camera.position.set(0, 15, 20);
47
  camera.lookAt(0, 0, 0);
48
  scene.add(camera);
49
 
50
  setupLighting();
51
- setupGround(); // Ground needs to be potentially very wide now
52
  setupPlayer();
53
 
54
  raycaster = new THREE.Raycaster();
@@ -60,10 +69,8 @@
60
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
61
  document.body.appendChild(renderer.domElement);
62
 
63
- loadInitialObjects(); // Load ALL objects passed from Python
64
-
65
- // *** ADDED: Restore unsaved state from sessionStorage ***
66
- restoreUnsavedState();
67
 
68
  // Event Listeners
69
  document.addEventListener('mousemove', onMouseMove, false);
@@ -71,14 +78,13 @@
71
  window.addEventListener('resize', onWindowResize, false);
72
  document.addEventListener('keydown', onKeyDown);
73
  document.addEventListener('keyup', onKeyUp);
74
- // No save button listener needed here
75
 
76
- // Define global functions needed by Python/streamlit-js-eval
77
  window.teleportPlayer = teleportPlayer;
78
- window.getSaveData = getSaveData;
79
  window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;
80
 
81
- console.log("Three.js Initialized. Ready for commands.");
82
  animate();
83
  }
84
 
@@ -86,57 +92,74 @@
86
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
87
  scene.add(ambientLight);
88
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
89
- directionalLight.position.set(50, 100, 75); // Adjust light position for wider world
90
  directionalLight.castShadow = true;
91
- directionalLight.shadow.mapSize.width = 2048*2; // Larger shadow map maybe needed
92
- directionalLight.shadow.mapSize.height = 2048*2;
93
  directionalLight.shadow.camera.near = 0.5;
94
- directionalLight.shadow.camera.far = 500; // Increase shadow distance
95
- // Adjust shadow camera frustum dynamically later if needed, make it wide for now
96
- directionalLight.shadow.camera.left = -100;
97
- directionalLight.shadow.camera.right = 100;
98
- directionalLight.shadow.camera.top = 100;
99
- directionalLight.shadow.camera.bottom = -100;
100
  directionalLight.shadow.bias = -0.001;
101
  scene.add(directionalLight);
102
  }
103
 
104
- function setupGround() { /* ... unchanged ... */
105
- const groundWidth = Math.max(plotWidth, nextPlotXOffset + plotWidth);
106
- const groundDepth = 50;
107
- const groundGeometry = new THREE.PlaneGeometry(groundWidth, groundDepth);
108
- const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1 });
109
- groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  groundMesh.rotation.x = -Math.PI / 2;
111
  groundMesh.position.y = -0.05;
112
- groundMesh.position.x = (groundWidth / 2.0) - (plotWidth / 2.0) ;
 
 
 
113
  groundMesh.receiveShadow = true;
 
114
  scene.add(groundMesh);
115
- console.log(`Ground setup with width: ${groundWidth} centered near x=0`);
 
 
 
116
  }
117
 
118
  function setupPlayer() { /* ... unchanged ... */
119
  const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
120
  const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
121
  playerMesh = new THREE.Mesh(playerGeo, playerMat);
122
- playerMesh.position.set(2, 0.4 + 0.8/2, 5); // Start near origin (x=2 to be slightly in first plot)
123
- playerMesh.castShadow = true;
124
- playerMesh.receiveShadow = true;
125
  scene.add(playerMesh);
126
  }
127
 
128
- function loadInitialObjects() {
129
  console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
130
- allInitialObjects.forEach(objData => {
131
- createAndPlaceObject(objData, false); // Use helper, false=don't add to newlyPlaced
132
- });
133
- console.log("Finished loading initial objects.");
134
  }
135
-
136
- // --- NEW: Helper to create/place objects from data ---
137
- function createAndPlaceObject(objData, isNewObject) {
138
  let loadedObject = null;
139
- // Need to deserialize object based on 'type' field from CSV/Storage
140
  switch (objData.type) {
141
  case "Simple House": loadedObject = createSimpleHouse(); break;
142
  case "Tree": loadedObject = createTree(); break;
@@ -144,182 +167,133 @@
144
  case "Fence Post": loadedObject = createFencePost(); break;
145
  default: console.warn("Unknown object type in data:", objData.type); break;
146
  }
147
-
148
  if (loadedObject) {
149
- // Position depends on whether it's from initial load (world) or storage (world)
150
- // or potentially relative if storage format changes (keep world for now)
151
  if (objData.position && objData.position.x !== undefined) {
152
  loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
153
- } else if (objData.pos_x !== undefined) { // Handle CSV loaded format too
154
  loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
155
  }
156
-
157
- // Apply rotation
158
- if (objData.rotation) { // Format from storage/newlyPlaced
159
  loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
160
- } else if (objData.rot_x !== undefined) { // Format from CSV
161
  loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
162
  }
163
-
164
- // Assign unique ID if it has one, else use the one generated by createObjectBase
165
  loadedObject.userData.obj_id = objData.obj_id || loadedObject.userData.obj_id;
166
-
167
  scene.add(loadedObject);
168
-
169
- if (isNewObject) {
170
- // Only add to this array if it's being placed now or restored from session
171
- newlyPlacedObjects.push(loadedObject);
172
- }
173
- return loadedObject; // Return the mesh/group
174
  }
175
  return null;
176
  }
177
-
178
-
179
- // --- NEW: Save/Load/Clear Unsaved State using sessionStorage ---
180
- function saveUnsavedState() {
181
  try {
182
- const stateToSave = newlyPlacedObjects.map(obj => ({
183
- obj_id: obj.userData.obj_id,
184
- type: obj.userData.type,
185
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
186
- rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
187
- }));
188
  sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(stateToSave));
189
  console.log(`Saved ${stateToSave.length} unsaved objects to sessionStorage.`);
190
- } catch (e) {
191
- console.error("Error saving state to sessionStorage:", e);
192
- }
193
  }
194
-
195
- function restoreUnsavedState() {
196
  try {
197
  const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
198
  if (savedState) {
199
  console.log("Found unsaved state in sessionStorage. Restoring...");
200
  const objectsToRestore = JSON.parse(savedState);
201
  if (Array.isArray(objectsToRestore)) {
202
- // Clear existing newlyPlaced array before restoring
203
- newlyPlacedObjects = [];
204
- let count = 0;
205
- objectsToRestore.forEach(objData => {
206
- // Create object, add to scene, AND add to newlyPlacedObjects array
207
- if(createAndPlaceObject(objData, true)) {
208
- count++;
209
- }
210
- });
211
  console.log(`Restored ${count} objects.`);
212
  }
213
- } else {
214
- console.log("No unsaved state found in sessionStorage.");
215
- }
216
- } catch (e) {
217
- console.error("Error restoring state from sessionStorage:", e);
218
- // Clear potentially corrupted storage
219
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
220
- }
221
  }
222
-
223
- function clearUnsavedState() {
224
  sessionStorage.removeItem(SESSION_STORAGE_KEY);
225
- newlyPlacedObjects = []; // Clear the array in memory too
226
  console.log("Cleared unsaved state from memory and sessionStorage.");
227
  }
228
-
229
-
230
- // --- Object Creation Functions (MUST add userData.type and obj_id) ---
231
- function createObjectBase(type) { // Helper to assign common data
232
- const obj = { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
233
- return obj;
234
  }
235
- function createSimpleHouse() { /* ... unchanged, uses createObjectBase ... */
236
- const base = createObjectBase("Simple House");
237
- const group = new THREE.Group(); Object.assign(group, base);
238
- const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
239
- const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
240
- const baseMesh = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
241
- baseMesh.position.y = 1.5 / 2; baseMesh.castShadow = true; baseMesh.receiveShadow = true; group.add(baseMesh);
242
- const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial);
243
- roof.position.y = 1.5 + 1 / 2; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = true; group.add(roof);
244
- return group;
245
  }
246
- function createTree() { /* ... unchanged, uses createObjectBase ... */
247
- const base = createObjectBase("Tree"); const group = new THREE.Group(); Object.assign(group, base);
248
- const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 });
249
- const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 });
250
- const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial);
251
- trunk.position.y = 2 / 2; trunk.castShadow = true; trunk.receiveShadow = true; group.add(trunk);
252
- const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial);
253
- leaves.position.y = 2 + 0.8; leaves.castShadow = true; leaves.receiveShadow = true; group.add(leaves);
254
- return group;
255
  }
256
- function createRock() { /* ... unchanged, uses createObjectBase ... */
257
- const base = createObjectBase("Rock");
258
- const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
259
- const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial); Object.assign(rock, base);
260
- rock.position.y = 0.7 / 2; rock.rotation.x = Math.random() * Math.PI; rock.rotation.y = Math.random() * Math.PI;
261
- rock.castShadow = true; rock.receiveShadow = true;
262
- return rock;
263
  }
264
- function createFencePost() { /* ... unchanged, uses createObjectBase ... */
265
- const base = createObjectBase("Fence Post");
266
- const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 });
267
- const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial); Object.assign(post, base);
268
- post.position.y = 1.5 / 2; post.castShadow = true; post.receiveShadow = true;
269
- return post;
270
  }
271
 
272
-
273
  // --- Event Handlers ---
274
  function onMouseMove(event) { /* ... unchanged ... */
275
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
276
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
277
  }
278
 
279
- function onDocumentClick(event) {
280
- if (selectedObjectType === "None" || !groundMesh) return;
 
 
 
281
 
282
- raycaster.setFromCamera(mouse, camera);
283
- const intersects = raycaster.intersectObject(groundMesh); // Intersect ground ONLY
 
284
 
285
- if (intersects.length > 0) {
286
- const intersectPoint = intersects[0].point;
287
- let newObjectToPlace = null; // Use a different name to avoid confusion
 
288
 
289
- switch (selectedObjectType) {
290
  case "Simple House": newObjectToPlace = createSimpleHouse(); break;
291
  case "Tree": newObjectToPlace = createTree(); break;
292
  case "Rock": newObjectToPlace = createRock(); break;
293
  case "Fence Post": newObjectToPlace = createFencePost(); break;
294
  default: return;
295
- }
296
-
297
- if (newObjectToPlace) {
298
- // Position in WORLD coordinates where clicked
299
- newObjectToPlace.position.copy(intersectPoint);
300
- // Adjust Y based on object's geometry center if needed
301
- // (Create functions mostly handle placing base at y=0)
302
-
303
- scene.add(newObjectToPlace);
304
- newlyPlacedObjects.push(newObjectToPlace); // Add to list for saving
305
-
306
- // *** ADDED: Save state to sessionStorage immediately after placing ***
307
- saveUnsavedState();
308
 
309
- console.log(`Placed new ${selectedObjectType}. Total unsaved: ${newlyPlacedObjects.length}`);
310
- }
311
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
 
314
  function onKeyDown(event) { keysPressed[event.code] = true; }
315
  function onKeyUp(event) { keysPressed[event.code] = false; }
316
 
317
  // --- Functions called by Python via streamlit-js-eval ---
318
- function teleportPlayer(targetX) { /* ... unchanged ... */
319
- console.log("JS teleportPlayer called with targetX:", targetX);
320
  if (playerMesh) {
321
- playerMesh.position.x = targetX + 2.0;
322
- playerMesh.position.z = 5.0;
 
323
  const offset = new THREE.Vector3(0, 15, 20);
324
  const targetPosition = playerMesh.position.clone().add(offset);
325
  camera.position.copy(targetPosition);
@@ -328,22 +302,27 @@
328
  } else { console.error("Player mesh not found for teleport."); }
329
  }
330
 
331
- function getSaveData() { // Called by Python when save button is clicked
332
- console.log(`JS getSaveData called. Returning ${newlyPlacedObjects.length} new objects.`);
333
- // Prepare data in the format Python expects (world positions)
334
  const objectsToSave = newlyPlacedObjects.map(obj => {
335
  if (!obj.userData || !obj.userData.type) { return null; }
336
  const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
337
- return {
338
- obj_id: obj.userData.obj_id,
339
- type: obj.userData.type,
340
  position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
341
  rotation: rotation
342
  };
343
  }).filter(obj => obj !== null);
344
 
345
- console.log("Prepared data for saving:", objectsToSave);
346
- return JSON.stringify(objectsToSave); // Return as JSON string
 
 
 
 
 
 
347
  }
348
 
349
  function resetNewlyPlacedObjects() { // Called by Python AFTER successful save
@@ -353,24 +332,75 @@
353
 
354
 
355
  // --- Animation Loop ---
356
- function updatePlayerMovement() { /* ... unchanged ... */
357
- if (!playerMesh) return;
358
- const moveDirection = new THREE.Vector3(0, 0, 0);
359
- if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
360
- if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
361
- if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
362
- if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
363
-
364
- if (moveDirection.lengthSq() > 0) {
365
- moveDirection.normalize().multiplyScalar(playerSpeed);
366
- playerMesh.position.add(moveDirection);
367
- playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
368
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  }
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  function updateCamera() { /* ... unchanged ... */
372
  if (!playerMesh) return;
373
- const offset = new THREE.Vector3(0, 15, 20);
374
  const targetPosition = playerMesh.position.clone().add(offset);
375
  camera.position.lerp(targetPosition, 0.08);
376
  camera.lookAt(playerMesh.position);
@@ -384,7 +414,7 @@
384
 
385
  function animate() {
386
  requestAnimationFrame(animate);
387
- updatePlayerMovement();
388
  updateCamera();
389
  renderer.render(scene, camera);
390
  }
 
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>
 
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
+ const placeholderPlots = new Set(); // Track visually created placeholder grounds: 'x_z' string key
29
+ const groundMeshes = {}; // Store references to ground meshes: 'x_z' string key -> mesh
30
 
31
  // --- Session Storage Key ---
32
+ const SESSION_STORAGE_KEY = 'unsavedInfiniteWorldState';
33
 
34
  // --- Access State from Streamlit ---
35
  const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
36
+ const plotsMetadata = window.PLOTS_METADATA || []; // List of saved plot info
37
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
38
  const plotWidth = window.PLOT_WIDTH || 50.0;
39
+ const plotDepth = window.PLOT_DEPTH || 50.0; // Use plot depth
40
+
41
+ const groundMaterial = new THREE.MeshStandardMaterial({ // Reusable material
42
+ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide
43
+ });
44
+ const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ // Dimmer for placeholders
45
+ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide
46
+ });
47
 
48
 
49
  function init() {
 
51
  scene.background = new THREE.Color(0xabcdef);
52
 
53
  const aspect = window.innerWidth / window.innerHeight;
54
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000); // Increase far plane more
55
  camera.position.set(0, 15, 20);
56
  camera.lookAt(0, 0, 0);
57
  scene.add(camera);
58
 
59
  setupLighting();
60
+ setupInitialGround(); // Setup ground for existing plots ONLY initially
61
  setupPlayer();
62
 
63
  raycaster = new THREE.Raycaster();
 
69
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
70
  document.body.appendChild(renderer.domElement);
71
 
72
+ loadInitialObjects();
73
+ restoreUnsavedState(); // Restore unsaved objects after initial load
 
 
74
 
75
  // Event Listeners
76
  document.addEventListener('mousemove', onMouseMove, false);
 
78
  window.addEventListener('resize', onWindowResize, false);
79
  document.addEventListener('keydown', onKeyDown);
80
  document.addEventListener('keyup', onKeyUp);
 
81
 
82
+ // Define global functions needed by Python
83
  window.teleportPlayer = teleportPlayer;
84
+ window.getSaveDataAndPosition = getSaveDataAndPosition; // Renamed JS function
85
  window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;
86
 
87
+ console.log("Three.js Initialized. World ready.");
88
  animate();
89
  }
90
 
 
92
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
93
  scene.add(ambientLight);
94
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
95
+ directionalLight.position.set(50, 150, 100); // Higher and angled light
96
  directionalLight.castShadow = true;
97
+ directionalLight.shadow.mapSize.width = 4096; // Increase shadow map size
98
+ directionalLight.shadow.mapSize.height = 4096;
99
  directionalLight.shadow.camera.near = 0.5;
100
+ directionalLight.shadow.camera.far = 500;
101
+ // Dynamic frustum needed for large worlds, but keep wide for now
102
+ directionalLight.shadow.camera.left = -150;
103
+ directionalLight.shadow.camera.right = 150;
104
+ directionalLight.shadow.camera.top = 150;
105
+ directionalLight.shadow.camera.bottom = -150;
106
  directionalLight.shadow.bias = -0.001;
107
  scene.add(directionalLight);
108
  }
109
 
110
+ function setupInitialGround() {
111
+ // Create ground ONLY for plots defined in plotsMetadata
112
+ console.log(`Setting up initial ground for ${plotsMetadata.length} plots.`);
113
+ plotsMetadata.forEach(plot => {
114
+ createGroundPlane(plot.grid_x, plot.grid_z, false); // false = not a placeholder
115
+ });
116
+ // Create a small initial ground at 0,0 if no plots exist yet
117
+ if (plotsMetadata.length === 0) {
118
+ createGroundPlane(0, 0, false);
119
+ }
120
+ }
121
+
122
+ // *** NEW: Function to create a ground plane for a specific grid cell ***
123
+ function createGroundPlane(gridX, gridZ, isPlaceholder) {
124
+ const gridKey = `${gridX}_${gridZ}`;
125
+ if (groundMeshes[gridKey]) return; // Don't recreate if it exists
126
+
127
+ console.log(`Creating ${isPlaceholder ? 'placeholder' : 'initial'} ground at ${gridX}, ${gridZ}`);
128
+ const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
129
+ const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
130
+ const groundMesh = new THREE.Mesh(groundGeometry, material);
131
+
132
  groundMesh.rotation.x = -Math.PI / 2;
133
  groundMesh.position.y = -0.05;
134
+ // Position the center of the plane correctly
135
+ groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
136
+ groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
137
+
138
  groundMesh.receiveShadow = true;
139
+ groundMesh.userData.gridKey = gridKey; // Store key for potential removal/update
140
  scene.add(groundMesh);
141
+ groundMeshes[gridKey] = groundMesh; // Store reference
142
+ if (isPlaceholder) {
143
+ placeholderPlots.add(gridKey); // Track placeholders
144
+ }
145
  }
146
 
147
  function setupPlayer() { /* ... unchanged ... */
148
  const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
149
  const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
150
  playerMesh = new THREE.Mesh(playerGeo, playerMat);
151
+ playerMesh.position.set(plotWidth / 2, 0.4 + 0.8/2, plotDepth/2); // Start in center of 0,0 plot
152
+ playerMesh.castShadow = true; playerMesh.receiveShadow = true;
 
153
  scene.add(playerMesh);
154
  }
155
 
156
+ function loadInitialObjects() { /* ... unchanged, uses createAndPlaceObject ... */
157
  console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
158
+ allInitialObjects.forEach(objData => { createAndPlaceObject(objData, false); });
159
+ console.log("Finished loading initial objects.");
 
 
160
  }
161
+ function createAndPlaceObject(objData, isNewObject) { /* ... unchanged ... */
 
 
162
  let loadedObject = null;
 
163
  switch (objData.type) {
164
  case "Simple House": loadedObject = createSimpleHouse(); break;
165
  case "Tree": loadedObject = createTree(); break;
 
167
  case "Fence Post": loadedObject = createFencePost(); break;
168
  default: console.warn("Unknown object type in data:", objData.type); break;
169
  }
 
170
  if (loadedObject) {
 
 
171
  if (objData.position && objData.position.x !== undefined) {
172
  loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
173
+ } else if (objData.pos_x !== undefined) {
174
  loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
175
  }
176
+ if (objData.rotation) {
 
 
177
  loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
178
+ } else if (objData.rot_x !== undefined) {
179
  loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
180
  }
 
 
181
  loadedObject.userData.obj_id = objData.obj_id || loadedObject.userData.obj_id;
 
182
  scene.add(loadedObject);
183
+ if (isNewObject) { newlyPlacedObjects.push(loadedObject); }
184
+ return loadedObject;
 
 
 
 
185
  }
186
  return null;
187
  }
188
+ function saveUnsavedState() { /* ... unchanged ... */
 
 
 
189
  try {
190
+ const stateToSave = newlyPlacedObjects.map(obj => ({ obj_id: obj.userData.obj_id, type: obj.userData.type, position: { x: obj.position.x, y: obj.position.y, z: obj.position.z }, rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order } }));
 
 
 
 
 
191
  sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(stateToSave));
192
  console.log(`Saved ${stateToSave.length} unsaved objects to sessionStorage.`);
193
+ } catch (e) { console.error("Error saving state to sessionStorage:", e); }
 
 
194
  }
195
+ function restoreUnsavedState() { /* ... unchanged ... */
 
196
  try {
197
  const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
198
  if (savedState) {
199
  console.log("Found unsaved state in sessionStorage. Restoring...");
200
  const objectsToRestore = JSON.parse(savedState);
201
  if (Array.isArray(objectsToRestore)) {
202
+ newlyPlacedObjects = []; let count = 0;
203
+ objectsToRestore.forEach(objData => { if(createAndPlaceObject(objData, true)) { count++; } });
 
 
 
 
 
 
 
204
  console.log(`Restored ${count} objects.`);
205
  }
206
+ } else { console.log("No unsaved state found in sessionStorage."); }
207
+ } catch (e) { console.error("Error restoring state from sessionStorage:", e); sessionStorage.removeItem(SESSION_STORAGE_KEY); }
 
 
 
 
 
 
208
  }
209
+ function clearUnsavedState() { /* ... unchanged ... */
 
210
  sessionStorage.removeItem(SESSION_STORAGE_KEY);
211
+ newlyPlacedObjects = [];
212
  console.log("Cleared unsaved state from memory and sessionStorage.");
213
  }
214
+ function createObjectBase(type) { /* ... unchanged ... */
215
+ return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
 
 
 
 
216
  }
217
+ function createSimpleHouse() { /* ... unchanged ... */
218
+ const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base);
219
+ const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7});
220
+ 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);
221
+ 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=true;group.add(m2); return group;
 
 
 
 
 
222
  }
223
+ function createTree() { /* ... unchanged ... */
224
+ const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base);
225
+ const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8});
226
+ 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);
227
+ const m2=new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0),mat2); m2.position.y=2.8; m2.castShadow=true;m2.receiveShadow=true;group.add(m2); return group;
 
 
 
 
228
  }
229
+ function createRock() { /* ... unchanged ... */
230
+ const base=createObjectBase("Rock"); const mat=new THREE.MeshStandardMaterial({color:0xaaaaaa,roughness:0.8,metalness:0.1});
231
+ const rock=new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0),mat); Object.assign(rock,base);
232
+ rock.position.y=0.35; rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0); rock.castShadow=true;rock.receiveShadow=true; return rock;
 
 
 
233
  }
234
+ function createFencePost() { /* ... unchanged ... */
235
+ const base=createObjectBase("Fence Post"); const mat=new THREE.MeshStandardMaterial({color:0xdeb887,roughness:0.9});
236
+ const post=new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2),mat); Object.assign(post,base);
237
+ post.position.y=0.75; post.castShadow=true;post.receiveShadow=true; return post;
 
 
238
  }
239
 
 
240
  // --- Event Handlers ---
241
  function onMouseMove(event) { /* ... unchanged ... */
242
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
243
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
244
  }
245
 
246
+ function onDocumentClick(event) { // Place object and save state
247
+ if (selectedObjectType === "None") return;
248
+ // Determine ground mesh to intersect (could be multiple now)
249
+ const groundCandidates = Object.values(groundMeshes);
250
+ if (groundCandidates.length === 0) return; // No ground to place on
251
 
252
+ raycaster.setFromCamera(mouse, camera);
253
+ // Intersect ALL ground meshes
254
+ const intersects = raycaster.intersectObjects(groundCandidates);
255
 
256
+ if (intersects.length > 0) {
257
+ // Intersect point is on the specific ground mesh clicked
258
+ const intersectPoint = intersects[0].point;
259
+ let newObjectToPlace = null;
260
 
261
+ switch (selectedObjectType) { /* ... create object ... */
262
  case "Simple House": newObjectToPlace = createSimpleHouse(); break;
263
  case "Tree": newObjectToPlace = createTree(); break;
264
  case "Rock": newObjectToPlace = createRock(); break;
265
  case "Fence Post": newObjectToPlace = createFencePost(); break;
266
  default: return;
267
+ }
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ if (newObjectToPlace) {
270
+ newObjectToPlace.position.copy(intersectPoint);
271
+ // Adjust Y position if needed based on object geometry origin
272
+ // Base objects on y=0 in create functions for simplicity
273
+ if(newObjectToPlace.geometry?.type.includes("Geometry")){ // Adjust Y so base is on ground
274
+ newObjectToPlace.position.y += 0.01; // Slight offset above ground
275
+ } else if (newObjectToPlace.type === "Group") {
276
+ newObjectToPlace.position.y += 0.01; // Assume group base is near 0
277
+ }
278
+
279
+ scene.add(newObjectToPlace);
280
+ newlyPlacedObjects.push(newObjectToPlace);
281
+ saveUnsavedState(); // Save to sessionStorage immediately
282
+ console.log(`Placed new ${selectedObjectType}. Total unsaved: ${newlyPlacedObjects.length}`);
283
+ }
284
+ }
285
  }
286
 
287
  function onKeyDown(event) { keysPressed[event.code] = true; }
288
  function onKeyUp(event) { keysPressed[event.code] = false; }
289
 
290
  // --- Functions called by Python via streamlit-js-eval ---
291
+ function teleportPlayer(targetX, targetZ) { // Now accepts Z coordinate
292
+ console.log(`JS teleportPlayer called with targetX: ${targetX}, targetZ: ${targetZ}`);
293
  if (playerMesh) {
294
+ // Teleport near center of the target plot
295
+ playerMesh.position.x = targetX;
296
+ playerMesh.position.z = targetZ;
297
  const offset = new THREE.Vector3(0, 15, 20);
298
  const targetPosition = playerMesh.position.clone().add(offset);
299
  camera.position.copy(targetPosition);
 
302
  } else { console.error("Player mesh not found for teleport."); }
303
  }
304
 
305
+ // *** UPDATED: Send player position along with save data ***
306
+ function getSaveDataAndPosition() {
307
+ console.log(`JS getSaveDataAndPosition called. Found ${newlyPlacedObjects.length} new objects.`);
308
  const objectsToSave = newlyPlacedObjects.map(obj => {
309
  if (!obj.userData || !obj.userData.type) { return null; }
310
  const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
311
+ return { // Send WORLD positions
312
+ obj_id: obj.userData.obj_id, type: obj.userData.type,
 
313
  position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
314
  rotation: rotation
315
  };
316
  }).filter(obj => obj !== null);
317
 
318
+ const playerPos = playerMesh ? { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z } : {x:0, y:0, z:0};
319
+
320
+ const payload = {
321
+ playerPosition: playerPos,
322
+ objectsToSave: objectsToSave
323
+ };
324
+ console.log("Prepared payload for saving:", payload);
325
+ return JSON.stringify(payload); // Return as JSON string
326
  }
327
 
328
  function resetNewlyPlacedObjects() { // Called by Python AFTER successful save
 
332
 
333
 
334
  // --- Animation Loop ---
335
+ function updatePlayerMovement() {
336
+ if (!playerMesh) return;
337
+ const moveDirection = new THREE.Vector3(0, 0, 0);
338
+ // Basic WASD movement
339
+ if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
340
+ if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
341
+ if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
342
+ if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
343
+
344
+ if (moveDirection.lengthSq() > 0) {
345
+ moveDirection.normalize().multiplyScalar(playerSpeed);
346
+ // Apply movement relative to camera direction for better control
347
+ const forward = new THREE.Vector3();
348
+ camera.getWorldDirection(forward);
349
+ forward.y = 0; // Project onto XZ plane
350
+ forward.normalize();
351
+ const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize(); // Get right vector
352
+
353
+ const worldMove = new THREE.Vector3();
354
+ worldMove.add(forward.multiplyScalar(-moveDirection.z)); // W/S -> Forward/Backward
355
+ worldMove.add(right.multiplyScalar(-moveDirection.x)); // A/D -> Left/Right
356
+ worldMove.normalize().multiplyScalar(playerSpeed);
357
+
358
+ playerMesh.position.add(worldMove);
359
+
360
+ // Basic ground clamping
361
+ playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2); // Adjust based on capsule base
362
+
363
+ // *** ADDED: Check for ground expansion ***
364
+ checkAndExpandGround();
365
+ }
366
  }
367
 
368
+ // *** NEW: Check if player is near edge and create placeholder ground ***
369
+ function checkAndExpandGround() {
370
+ if (!playerMesh) return;
371
+
372
+ const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
373
+ const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
374
+
375
+ // Check surrounding cells (Manhattan distance 1)
376
+ for (let dx = -1; dx <= 1; dx++) {
377
+ for (let dz = -1; dz <= 1; dz++) {
378
+ if (dx === 0 && dz === 0) continue; // Skip current cell
379
+
380
+ const checkX = currentGridX + dx;
381
+ const checkZ = currentGridZ + dz;
382
+ const gridKey = `${checkX}_${checkZ}`;
383
+
384
+ // Check if this grid cell already has ground (initial or placeholder)
385
+ if (!groundMeshes[gridKey]) {
386
+ // Check if this grid cell corresponds to a SAVED plot (from metadata)
387
+ const isSavedPlot = plotsMetadata.some(plot => plot.grid_x === checkX && plot.grid_z === checkZ);
388
+
389
+ // If it's NOT a saved plot, create a placeholder
390
+ if (!isSavedPlot) {
391
+ createGroundPlane(checkX, checkZ, true); // true = is placeholder
392
+ }
393
+ // If it IS a saved plot but somehow missing ground (error?), recreate it?
394
+ // else { createGroundPlane(checkX, checkZ, false); } // Optional robustness
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+
401
  function updateCamera() { /* ... unchanged ... */
402
  if (!playerMesh) return;
403
+ const offset = new THREE.Vector3(0, 15, 20); // Fixed offset for now
404
  const targetPosition = playerMesh.position.clone().add(offset);
405
  camera.position.lerp(targetPosition, 0.08);
406
  camera.lookAt(playerMesh.position);
 
414
 
415
  function animate() {
416
  requestAnimationFrame(animate);
417
+ updatePlayerMovement(); // Includes ground check now
418
  updateCamera();
419
  renderer.render(scene, camera);
420
  }