awacke1 commited on
Commit
5b776e8
·
verified ·
1 Parent(s): e6bd8ca

Update backup1.index.html

Browse files
Files changed (1) hide show
  1. backup1.index.html +247 -192
backup1.index.html CHANGED
@@ -1,32 +1,14 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>Three.js World Builder</title>
5
  <style>
6
  body { margin: 0; overflow: hidden; }
7
  canvas { display: block; }
8
- /* Style for the save button */
9
- #saveButton {
10
- position: absolute;
11
- top: 10px;
12
- left: 10px;
13
- padding: 10px 15px;
14
- background-color: #4CAF50;
15
- color: white;
16
- border: none;
17
- border-radius: 5px;
18
- cursor: pointer;
19
- font-size: 16px;
20
- z-index: 10; /* Ensure it's above the canvas */
21
- }
22
- #saveButton:hover {
23
- background-color: #45a049;
24
- }
25
  </style>
26
  </head>
27
  <body>
28
- <button id="saveButton">💾 Save Work</button>
29
-
30
  <script type="importmap">
31
  {
32
  "imports": {
@@ -39,33 +21,35 @@
39
  <script type="module">
40
  import * as THREE from 'three';
41
 
42
- let scene, camera, renderer, groundMesh, playerMesh;
43
  let raycaster, mouse;
44
- const placedObjects = [];
45
- const keysPressed = {}; // Track key presses for smooth movement
46
  const playerSpeed = 0.15;
 
47
 
48
- // --- Access State from Streamlit (set via injected script) ---
 
 
 
 
49
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
50
- const initialObjects = window.INITIAL_OBJECTS || [];
51
- const currentSpaceId = window.CURRENT_SPACE_ID || null;
52
- const currentSpaceName = window.CURRENT_SPACE_NAME || ""; // Get name for save redirect
53
 
54
  function init() {
55
  scene = new THREE.Scene();
56
  scene.background = new THREE.Color(0xabcdef);
57
 
58
  const aspect = window.innerWidth / window.innerHeight;
59
- const viewSize = 30;
60
- // Using PerspectiveCamera now for better depth perception with movement
61
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
62
- camera.position.set(0, 15, 20); // Position behind and above origin
63
  camera.lookAt(0, 0, 0);
64
  scene.add(camera);
65
 
66
  setupLighting();
67
- setupGround();
68
- setupPlayer(); // Create player representation
69
 
70
  raycaster = new THREE.Raycaster();
71
  mouse = new THREE.Vector2();
@@ -76,163 +60,253 @@
76
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
77
  document.body.appendChild(renderer.domElement);
78
 
79
- // --- Load Initial Objects ---
80
- loadInitialObjects();
 
 
81
 
82
- // --- Event Listeners ---
83
  document.addEventListener('mousemove', onMouseMove, false);
84
  document.addEventListener('click', onDocumentClick, false);
85
  window.addEventListener('resize', onWindowResize, false);
86
  document.addEventListener('keydown', onKeyDown);
87
  document.addEventListener('keyup', onKeyUp);
88
- document.getElementById('saveButton').addEventListener('click', onSaveClick);
89
 
 
 
 
 
90
 
 
91
  animate();
92
  }
93
 
94
- function setupLighting() {
95
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
96
  scene.add(ambientLight);
97
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
98
- directionalLight.position.set(15, 30, 20);
99
  directionalLight.castShadow = true;
100
- directionalLight.shadow.mapSize.width = 2048;
101
- directionalLight.shadow.mapSize.height = 2048;
102
  directionalLight.shadow.camera.near = 0.5;
103
- directionalLight.shadow.camera.far = 100;
104
- directionalLight.shadow.camera.left = -30;
105
- directionalLight.shadow.camera.right = 30;
106
- directionalLight.shadow.camera.top = 30;
107
- directionalLight.shadow.camera.bottom = -30;
 
108
  directionalLight.shadow.bias = -0.001;
109
  scene.add(directionalLight);
110
  }
111
 
112
- function setupGround() {
113
- const groundGeometry = new THREE.PlaneGeometry(50, 50); // Size matches constant in Python
 
 
114
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1 });
115
  groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
116
  groundMesh.rotation.x = -Math.PI / 2;
117
  groundMesh.position.y = -0.05;
 
118
  groundMesh.receiveShadow = true;
119
  scene.add(groundMesh);
 
120
  }
121
 
122
- function setupPlayer() {
123
- // Simple Capsule or Box primitive for player
124
- const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8); // Radius, Height
125
  const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
126
  playerMesh = new THREE.Mesh(playerGeo, playerMat);
127
- playerMesh.position.set(0, 0.4 + 0.8/2, 5); // Start position (Y adjusted for capsule base)
128
  playerMesh.castShadow = true;
129
  playerMesh.receiveShadow = true;
130
  scene.add(playerMesh);
131
  }
132
 
133
- function loadInitialObjects() {
134
- console.log("Loading initial objects:", initialObjects);
135
- initialObjects.forEach(objData => {
136
- let loadedObject = null;
137
- switch (objData.type) {
138
- case "Simple House": loadedObject = createSimpleHouse(); break;
139
- case "Tree": loadedObject = createTree(); break;
140
- case "Rock": loadedObject = createRock(); break;
141
- case "Fence Post": loadedObject = createFencePost(); break;
142
- // Add cases for any other types you save
143
- }
144
- if (loadedObject && objData.position) {
145
- // Apply saved position and rotation
146
- loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
147
- if (objData.rotation) {
148
- loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order);
149
- } else if (objData.quaternion) { // Handle if saved as quaternion
150
- loadedObject.quaternion.set(objData.quaternion._x, objData.quaternion._y, objData.quaternion._z, objData.quaternion._w);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
- scene.add(loadedObject);
153
- placedObjects.push(loadedObject); // Track loaded objects
154
  }
155
- });
 
 
 
 
 
 
 
 
 
 
156
  }
157
 
158
 
159
- // --- Object Creation Functions (Keep these from previous step) ---
160
- function createSimpleHouse() { /* ... copy from previous ... */
161
- const group = new THREE.Group();
 
 
 
 
 
162
  const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
163
  const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
164
- const base = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
165
- base.position.y = 1.5 / 2; base.castShadow = true; base.receiveShadow = true; group.add(base);
166
  const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial);
167
  roof.position.y = 1.5 + 1 / 2; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = true; group.add(roof);
168
- group.userData.type = "Simple House"; // Store type for saving
169
  return group;
170
  }
171
- function createTree() { /* ... copy from previous ... */
172
- const group = new THREE.Group();
173
  const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 });
174
  const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 });
175
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial);
176
  trunk.position.y = 2 / 2; trunk.castShadow = true; trunk.receiveShadow = true; group.add(trunk);
177
  const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial);
178
  leaves.position.y = 2 + 0.8; leaves.castShadow = true; leaves.receiveShadow = true; group.add(leaves);
179
- group.userData.type = "Tree";
180
  return group;
181
  }
182
- function createRock() { /* ... copy from previous ... */
183
- const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
184
- const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial);
185
- rock.position.y = 0.7 / 2; rock.rotation.x = Math.random() * Math.PI; rock.rotation.y = Math.random() * Math.PI;
186
- rock.castShadow = true; rock.receiveShadow = true;
187
- rock.userData.type = "Rock";
188
- return rock;
189
  }
190
- function createFencePost() { /* ... copy from previous ... */
191
- const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 });
192
- const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial);
193
- post.position.y = 1.5 / 2; post.castShadow = true; post.receiveShadow = true;
194
- post.userData.type = "Fence Post";
195
- return post;
196
  }
197
 
 
198
  // --- Event Handlers ---
199
- function onMouseMove(event) {
200
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
201
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
202
  }
203
 
204
  function onDocumentClick(event) {
205
- if (selectedObjectType === "None") return; // Don't place
206
-
207
- // Prevent placing if clicking the save button
208
- if (event.target.id === 'saveButton') return;
209
-
210
 
211
  raycaster.setFromCamera(mouse, camera);
212
- const intersects = raycaster.intersectObject(groundMesh);
213
 
214
  if (intersects.length > 0) {
215
  const intersectPoint = intersects[0].point;
216
- let newObject = null;
217
-
218
- switch (selectedObjectType) { // Use create functions
219
- case "Simple House": newObject = createSimpleHouse(); break;
220
- case "Tree": newObject = createTree(); break;
221
- case "Rock": newObject = createRock(); break;
222
- case "Fence Post": newObject = createFencePost(); break;
223
- default: console.warn("Unknown object type:", selectedObjectType); return;
224
  }
225
 
226
- if (newObject) {
227
- newObject.position.copy(intersectPoint);
228
- // Adjust Y based on object's geometry center if needed (already done in create funcs mostly)
229
- if (newObject.geometry && newObject.geometry.boundingBox) {
230
- // Optional fine-tuning if origin isn't at base
231
- } else if (newObject.type === "Group") {
232
- // Assume group origin is logical base
233
- }
234
- scene.add(newObject);
235
- placedObjects.push(newObject);
 
 
 
236
  }
237
  }
238
  }
@@ -240,86 +314,69 @@
240
  function onKeyDown(event) { keysPressed[event.code] = true; }
241
  function onKeyUp(event) { keysPressed[event.code] = false; }
242
 
243
- function onSaveClick() {
244
- console.log("Save button clicked.");
245
- const objectsToSave = placedObjects.map(obj => {
246
- // IMPORTANT: Need to know the TYPE of the object placed
247
- // We added `userData.type` in the create functions
248
- const type = obj.userData.type || "Unknown"; // Get stored type
249
-
250
- // Simplify rotation: Use Euler angles which are often easier to serialize/deserialize than Quaternions
251
- const rotation = {
252
- _x: obj.rotation.x,
253
- _y: obj.rotation.y,
254
- _z: obj.rotation.z,
255
- _order: obj.rotation.order // Important!
256
- };
257
-
258
- return {
259
- type: type,
260
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
261
- // Store Euler rotation instead of quaternion for simplicity here
262
- rotation: rotation
263
- // quaternion: { _x: obj.quaternion.x, _y: obj.quaternion.y, _z: obj.quaternion.z, _w: obj.quaternion.w } // Alternative
264
- };
265
- }).filter(obj => obj.type !== "Unknown"); // Don't save unknowns
266
-
267
- const saveDataJson = JSON.stringify(objectsToSave);
268
- const saveDataEncoded = encodeURIComponent(saveDataJson);
269
-
270
- // Construct URL with save data, space ID, and name
271
- const params = new URLSearchParams();
272
- params.set('save_data', saveDataEncoded);
273
- if (currentSpaceId) {
274
- params.set('space_id', currentSpaceId);
275
- }
276
- if (currentSpaceName) { // Send current name from sidebar input
277
- params.set('space_name', encodeURIComponent(currentSpaceName));
278
- }
279
-
280
- console.log("Redirecting to save...");
281
- // Trigger reload with query parameters
282
- window.location.search = params.toString();
283
-
284
  }
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- function updatePlayerMovement() {
288
- if (!playerMesh) return;
289
-
290
- const moveDirection = new THREE.Vector3(0, 0, 0);
291
- if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
292
- if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
293
- if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
294
- if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
295
-
296
- if (moveDirection.lengthSq() > 0) {
297
- moveDirection.normalize().multiplyScalar(playerSpeed);
298
 
299
- // Apply movement relative to player's current orientation (if needed)
300
- // For simple world-axis movement:
301
- playerMesh.position.add(moveDirection);
302
 
303
- // Collision detection would go here!
 
 
 
 
 
 
 
304
 
305
- // Basic ground clamping (prevent falling through)
306
- playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2); // Adjust based on capsule size
307
- }
 
 
308
  }
309
 
310
- function updateCamera() {
311
  if (!playerMesh) return;
312
- // Simple third-person follow cam
313
- const offset = new THREE.Vector3(0, 5, 10); // Camera distance from player
314
- // Can be enhanced to use player rotation later
315
  const targetPosition = playerMesh.position.clone().add(offset);
316
- // Smooth camera movement (Lerp)
317
- camera.position.lerp(targetPosition, 0.1); // Adjust lerp factor for smoothness
318
- camera.lookAt(playerMesh.position); // Always look at the player
319
  }
320
 
321
-
322
- function onWindowResize() {
323
  camera.aspect = window.innerWidth / window.innerHeight;
324
  camera.updateProjectionMatrix();
325
  renderer.setSize(window.innerWidth, window.innerHeight);
@@ -327,14 +384,12 @@
327
 
328
  function animate() {
329
  requestAnimationFrame(animate);
330
-
331
  updatePlayerMovement();
332
- updateCamera(); // Make camera follow player
333
-
334
  renderer.render(scene, camera);
335
  }
336
 
337
- // --- Start the app ---
338
  init();
339
 
340
  </script>
 
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>
 
 
12
  <script type="importmap">
13
  {
14
  "imports": {
 
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() {
41
  scene = new THREE.Scene();
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();
55
  mouse = new THREE.Vector2();
 
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);
70
  document.addEventListener('click', onDocumentClick, false);
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
 
85
+ function setupLighting() { /* ... unchanged ... */
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;
143
+ case "Rock": loadedObject = createRock(); break;
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
  }
 
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);
326
+ camera.lookAt(playerMesh.position);
327
+ console.log("Player teleported to:", playerMesh.position);
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
350
+ console.log(`JS resetNewlyPlacedObjects called.`);
351
+ clearUnsavedState(); // Clear memory array AND sessionStorage
352
+ }
 
 
 
 
 
 
 
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);
 
377
  }
378
 
379
+ function onWindowResize() { /* ... unchanged ... */
 
380
  camera.aspect = window.innerWidth / window.innerHeight;
381
  camera.updateProjectionMatrix();
382
  renderer.setSize(window.innerWidth, window.innerHeight);
 
384
 
385
  function animate() {
386
  requestAnimationFrame(animate);
 
387
  updatePlayerMovement();
388
+ updateCamera();
 
389
  renderer.render(scene, camera);
390
  }
391
 
392
+ // --- Start ---
393
  init();
394
 
395
  </script>