awacke1 commited on
Commit
1f9eb7e
·
verified ·
1 Parent(s): d1944fb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +41 -254
index.html CHANGED
@@ -20,288 +20,75 @@
20
  <script type="module">
21
  import * as THREE from 'three';
22
 
23
- // --- Variables ---
24
  let scene, camera, renderer, playerMesh;
25
  let raycaster, mouse;
26
  const keysPressed = {};
27
  const playerSpeed = 0.15;
28
- let newlyPlacedObjects = []; // For sessionStorage
29
  const placeholderPlots = new Set();
30
  const groundMeshes = {};
31
- const allRenderedObjects = {}; // Tracks all current objects by ID
32
 
33
  const SESSION_STORAGE_KEY = 'unsavedDbWorldState_v2';
34
 
35
- // --- State from Python ---
36
- const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
37
- const plotsMetadata = window.PLOTS_METADATA || [];
38
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
39
  const plotWidth = window.PLOT_WIDTH || 50.0;
40
  const plotDepth = window.PLOT_DEPTH || 50.0;
41
 
42
- // --- Materials ---
43
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
44
  const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
45
 
46
- // --- Initialization ---
47
- function init() {
48
- scene = new THREE.Scene();
49
- scene.background = new THREE.Color(0xabcdef);
50
-
51
- const aspect = window.innerWidth / window.innerHeight;
52
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
53
- camera.position.set(plotWidth / 2, 15, plotDepth / 2 + 20); // Start looking at first plot
54
- camera.lookAt(plotWidth / 2, 0, plotDepth/2);
55
- scene.add(camera);
56
-
57
- setupLighting();
58
- setupInitialGround(); // Creates ground based on plotsMetadata
59
- setupPlayer();
60
 
61
- raycaster = new THREE.Raycaster();
62
- mouse = new THREE.Vector2();
63
-
64
- renderer = new THREE.WebGLRenderer({ antialias: true });
65
- renderer.setSize(window.innerWidth, window.innerHeight);
66
- renderer.shadowMap.enabled = true;
67
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
68
- document.body.appendChild(renderer.domElement);
69
 
70
- loadInitialObjects(); // Loads objects from DB data
71
- restoreUnsavedState(); // Loads unsaved from sessionStorage
72
 
73
  // Event Listeners
74
- document.addEventListener('mousemove', onMouseMove, false);
75
- document.addEventListener('click', onDocumentClick, false);
76
- window.addEventListener('resize', onWindowResize, false);
77
- document.addEventListener('keydown', onKeyDown);
78
- document.addEventListener('keyup', onKeyUp);
79
 
80
- // Define global functions for Python
81
  window.teleportPlayer = teleportPlayer;
82
- window.getSaveDataAndPosition = getSaveDataAndPosition;
 
83
 
84
  console.log("Three.js Initialized (DB Backend v2). World ready.");
85
  animate();
86
  }
87
 
88
- // --- Setup Functions ---
89
- function setupLighting() { const a=new THREE.AmbientLight(0xffffff,0.5); scene.add(a); const d=new THREE.DirectionalLight(0xffffff,1.0); d.position.set(50,150,100); d.castShadow=true; d.shadow.mapSize.width=4096; d.shadow.mapSize.height=4096; d.shadow.camera.near=0.5; d.shadow.camera.far=500; d.shadow.camera.left=-150; d.shadow.camera.right=150; d.shadow.camera.top=150; d.shadow.camera.bottom=-150; d.shadow.bias=-0.001; scene.add(d); }
90
- function setupInitialGround() { plotsMetadata.forEach(p => {createGroundPlane(p.grid_x,p.grid_z,false);}); if(plotsMetadata.length===0) {createGroundPlane(0,0,false);} }
91
- function createGroundPlane(gx,gz,isPlaceholder) { const k=`${gx}_${gz}`; if(groundMeshes[k]) return; const geo=new THREE.PlaneGeometry(plotWidth,plotDepth); const mat=isPlaceholder?placeholderGroundMaterial:groundMaterial; const mesh=new THREE.Mesh(geo,mat); mesh.rotation.x=-Math.PI/2; mesh.position.y=-0.05; mesh.position.x=gx*plotWidth+plotWidth/2; mesh.position.z=gz*plotDepth+plotDepth/2; mesh.receiveShadow=true; mesh.userData.gridKey=k; scene.add(mesh); groundMeshes[k]=mesh; if(isPlaceholder){placeholderPlots.add(k);} }
92
- function setupPlayer() { const g=new THREE.CapsuleGeometry(0.4,0.8,4,8); const m=new THREE.MeshStandardMaterial({color:0x0000ff,roughness:0.6}); playerMesh=new THREE.Mesh(g,m); playerMesh.position.set(plotWidth/2, 0.8, plotDepth/2); playerMesh.castShadow=true; playerMesh.receiveShadow=true; scene.add(playerMesh); }
93
-
94
- // --- Object Loading / State Management ---
95
- function loadInitialObjects() { console.log(`Loading ${allInitialObjects.length} initial objects.`); clearAllRenderedObjects(); allInitialObjects.forEach(d => { createAndPlaceObject(d, false); }); console.log("Finished initial load."); }
96
- function clearAllRenderedObjects() { Object.values(allRenderedObjects).forEach(o => { if(o.parent) o.parent.remove(o); /* Remove safely */ }); for (const k in allRenderedObjects) delete allRenderedObjects[k]; newlyPlacedObjects = []; }
97
- function createAndPlaceObject(objData, isNewObjectForSession) { let obj=null; switch(objData.type){case "Simple House":obj=createSimpleHouse();break; case "Tree":obj=createTree();break; case "Rock":obj=createRock();break; case "Fence Post":obj=createFencePost();break; default: return null;} if(obj){ obj.userData.obj_id = objData.obj_id || obj.userData.obj_id; if(allRenderedObjects[obj.userData.obj_id]){console.warn(`Duplicate obj ID load skipped: ${obj.userData.obj_id}`); return null;} if(objData.position&&objData.position.x!==undefined){obj.position.set(objData.position.x,objData.position.y,objData.position.z);} else if(objData.pos_x!==undefined){obj.position.set(objData.pos_x,objData.pos_y,objData.pos_z);} else {obj.position.set(0,0.5,0);} if(objData.rotation){obj.rotation.set(objData.rotation._x,objData.rotation._y,objData.rotation._z,objData.rotation._order||'XYZ');} else if(objData.rot_x!==undefined){obj.rotation.set(objData.rot_x,objData.rot_y,objData.rot_z,objData.rot_order||'XYZ');} scene.add(obj); allRenderedObjects[obj.userData.obj_id]=obj; if(isNewObjectForSession){newlyPlacedObjects.push(obj);} return obj; } return null; }
98
- function saveUnsavedState() { try { const d = newlyPlacedObjects.map(o => ({obj_id:o.userData.obj_id, type:o.userData.type, position:{x:o.position.x,y:o.position.y,z:o.position.z}, rotation:{_x:o.rotation.x,_y:o.rotation.y,_z:o.rotation.z,_order:o.rotation.order}})); sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(d)); } catch(e) { console.error("Session save error:", e); } }
99
- function restoreUnsavedState() { try { const s=sessionStorage.getItem(SESSION_STORAGE_KEY); if(s) { const d=JSON.parse(s); if(Array.isArray(d)) { let c=0; d.forEach(o => { if(createAndPlaceObject(o, true)) c++;}); console.log(`Restored ${c} unsaved objects.`); } } } catch(e) { console.error("Session restore error:", e); sessionStorage.removeItem(SESSION_STORAGE_KEY); } }
100
- // --- Object Creation Primitives ---
101
- function createObjectBase(type) { return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } }; }
102
- function createSimpleHouse() { const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base); const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7}); const m1=new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5),mat1); m1.position.y=0.75;m1.castShadow=true;m1.receiveShadow=true;group.add(m1); const m2=new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4),mat2); m2.position.y=1.5+0.5;m2.rotation.y=Math.PI/4;m2.castShadow=true;m2.receiveShadow=true;group.add(m2); return group; }
103
- function createTree() { const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base); const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8}); 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); 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; }
104
- function createRock() { const base=createObjectBase("Rock"); const mat=new THREE.MeshStandardMaterial({color:0xaaaaaa,roughness:0.8,metalness:0.1}); const rock=new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0),mat); Object.assign(rock,base); 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; }
105
- function createFencePost() { // Continuing from where it cut off
106
- const base=createObjectBase("Fence Post");
107
- const mat=new THREE.MeshStandardMaterial({color:0xdeb887, roughness:0.9}); // BurlyWood color
108
- const post=new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), mat);
109
- Object.assign(post, base); // Add userData to the mesh itself
110
- post.position.y= 1.5 / 2; // Position base at y=0
111
- post.castShadow=true;
112
- post.receiveShadow=true;
113
- return post;
114
- }
115
-
116
- // --- Event Handlers ---
117
- function onMouseMove(event) {
118
- // Update mouse vector for raycasting
119
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
120
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
121
- }
122
-
123
- function onDocumentClick(event) {
124
- if (selectedObjectType === "None") return; // Don't place if 'None' selected
125
- // Determine which ground mesh(es) to check for intersection
126
- const groundCandidates = Object.values(groundMeshes);
127
- if (groundCandidates.length === 0) {
128
- console.warn("No ground exists to place objects on.");
129
- return;
130
- }
131
-
132
- raycaster.setFromCamera(mouse, camera);
133
- const intersects = raycaster.intersectObjects(groundCandidates); // Check all ground planes
134
 
135
- if (intersects.length > 0) {
136
- // Found an intersection point on a ground plane
137
- const intersectPoint = intersects[0].point;
138
- let newObjectToPlace = null;
139
 
140
- // Create the selected object type
141
- switch (selectedObjectType) {
142
- case "Simple House": newObjectToPlace = createSimpleHouse(); break;
143
- case "Tree": newObjectToPlace = createTree(); break;
144
- case "Rock": newObjectToPlace = createRock(); break;
145
- case "Fence Post": newObjectToPlace = createFencePost(); break;
146
- default: console.warn("Attempted to place unknown object type:", selectedObjectType); return;
147
- }
148
 
149
- if (newObjectToPlace) {
150
- // Position the new object at the click point
151
- newObjectToPlace.position.copy(intersectPoint);
152
- // Adjust Y position slightly so it's definitely above the ground plane
153
- newObjectToPlace.position.y = Math.max(0.01, newObjectToPlace.position.y);
154
 
155
- scene.add(newObjectToPlace);
156
- // Add to tracked lists
157
- allRenderedObjects[newObjectToPlace.userData.obj_id] = newObjectToPlace;
158
- newlyPlacedObjects.push(newObjectToPlace);
159
- // Save the updated unsaved state to sessionStorage
160
- saveUnsavedState();
161
- console.log(`Placed new ${selectedObjectType}. Total rendered: ${Object.keys(allRenderedObjects).length}, Unsaved in session: ${newlyPlacedObjects.length}`);
162
- }
163
- }
164
- }
165
-
166
- function onKeyDown(event) { keysPressed[event.code] = true; }
167
- function onKeyUp(event) { keysPressed[event.code] = false; }
168
-
169
- // --- Functions called by Python ---
170
- function teleportPlayer(targetX, targetZ) {
171
- console.log(`JS teleportPlayer called: Target X=${targetX}, Z=${targetZ}`);
172
- if (playerMesh) {
173
- playerMesh.position.set(targetX, playerMesh.position.y, targetZ); // Set X and Z
174
- // Instantly snap camera to new player position
175
- const offset = new THREE.Vector3(0, 15, 20); // Camera offset
176
- const cameraTargetPosition = playerMesh.position.clone().add(offset);
177
- camera.position.copy(cameraTargetPosition);
178
- camera.lookAt(playerMesh.position); // Look at player
179
- console.log("Player teleported to:", playerMesh.position);
180
- } else {
181
- console.error("Player mesh not found for teleport.");
182
- }
183
- }
184
-
185
- function getSaveDataAndPosition() {
186
- if (!playerMesh) {
187
- console.error("Player mesh missing, cannot determine save plot.");
188
- return JSON.stringify({ playerPosition: {x:0,y:0,z:0}, objectsToSave: [] });
189
- }
190
-
191
- const playerPos = { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z };
192
- const currentGridX = Math.floor(playerPos.x / plotWidth);
193
- const currentGridZ = Math.floor(playerPos.z / plotDepth);
194
- const minX = currentGridX * plotWidth, maxX = minX + plotWidth;
195
- const minZ = currentGridZ * plotDepth, maxZ = minZ + plotDepth;
196
-
197
- console.log(`getSaveData: Player in grid [${currentGridX}, ${currentGridZ}]. Filtering objects within X:[${minX.toFixed(1)},${maxX.toFixed(1)}), Z:[${minZ.toFixed(1)},${maxZ.toFixed(1)})`);
198
-
199
- // Filter ALL currently rendered objects to find those within these boundaries
200
- const objectsInPlot = Object.values(allRenderedObjects).filter(obj =>
201
- obj.position.x >= minX && obj.position.x < maxX &&
202
- obj.position.z >= minZ && obj.position.z < maxZ
203
- ).map(obj => { // Serialize the filtered objects
204
- if (!obj.userData || !obj.userData.type || !obj.userData.obj_id) {
205
- console.warn("Skipping object with missing user data during save serialization:", obj);
206
- return null;
207
- }
208
- const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
209
- return { // Send WORLD coordinates
210
- obj_id: obj.userData.obj_id, type: obj.userData.type,
211
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
212
- rotation: rotation
213
- };
214
- }).filter(obj => obj !== null); // Filter out any nulls from warnings
215
-
216
- const payload = {
217
- playerPosition: playerPos,
218
- objectsToSave: objectsInPlot // Contains all relevant objects for the current plot
219
- };
220
- console.log(`Prepared payload with ${objectsInPlot.length} objects for saving plot (${currentGridX},${currentGridZ}).`);
221
- return JSON.stringify(payload);
222
- }
223
-
224
- // --- Animation Loop ---
225
- function updatePlayerMovement() {
226
- if (!playerMesh) return;
227
- const moveDirection = new THREE.Vector3(0, 0, 0);
228
- if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
229
- if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
230
- if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
231
- if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
232
-
233
- if (moveDirection.lengthSq() > 0) {
234
- const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize();
235
- const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
236
- const worldMove = new THREE.Vector3();
237
- worldMove.add(forward.multiplyScalar(-moveDirection.z)); // W/S moves along camera forward/backward
238
- worldMove.add(right.multiplyScalar(-moveDirection.x)); // A/D moves along camera right/left
239
- worldMove.normalize().multiplyScalar(playerSpeed);
240
- playerMesh.position.add(worldMove);
241
- playerMesh.position.y = Math.max(0.8, playerMesh.position.y); // Keep player base y near 0.8
242
-
243
- checkAndExpandGround(); // Check if new ground needs to be created visually
244
- }
245
- }
246
-
247
- function checkAndExpandGround() {
248
- if (!playerMesh) return;
249
- const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
250
- const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
251
-
252
- // Check immediate neighbors and one step further maybe? Check radius 1 for now.
253
- for (let dx = -1; dx <= 1; dx++) {
254
- for (let dz = -1; dz <= 1; dz++) {
255
- // if (dx === 0 && dz === 0) continue; // Also check current cell just in case
256
-
257
- const checkX = currentGridX + dx;
258
- const checkZ = currentGridZ + dz;
259
- const gridKey = `${checkX}_${checkZ}`;
260
-
261
- // If no ground mesh exists for this grid cell yet...
262
- if (!groundMeshes[gridKey]) {
263
- // Check if it corresponds to a SAVED plot (metadata from Python)
264
- const isSavedPlot = plotsMetadata.some(plot => plot.grid_x === checkX && plot.grid_z === checkZ);
265
- // If it's NOT a saved plot, create a visual placeholder
266
- if (!isSavedPlot) {
267
- createGroundPlane(checkX, checkZ, true); // true = is placeholder
268
- }
269
- // If it IS a saved plot but the mesh is missing (e.g. after clear), recreate it
270
- // This shouldn't happen often with current logic but adds robustness
271
- else {
272
- createGroundPlane(checkX, checkZ, false);
273
- }
274
- }
275
- }
276
- }
277
- }
278
-
279
- function updateCamera() {
280
- if (!playerMesh) return;
281
- const offset = new THREE.Vector3(0, 15, 20); // Fixed third-person offset
282
- const targetPosition = playerMesh.position.clone().add(offset);
283
- // Smoothly interpolate camera position
284
- camera.position.lerp(targetPosition, 0.08);
285
- // Always look at the player's current position
286
- camera.lookAt(playerMesh.position);
287
- }
288
-
289
- function onWindowResize() {
290
- camera.aspect = window.innerWidth / window.innerHeight;
291
- camera.updateProjectionMatrix();
292
- renderer.setSize(window.innerWidth, window.innerHeight);
293
- }
294
-
295
- function animate() {
296
- requestAnimationFrame(animate);
297
- updatePlayerMovement(); // Includes ground expansion check
298
- updateCamera();
299
- renderer.render(scene, camera);
300
- }
301
-
302
- // --- Start the application ---
303
- init();
304
-
305
- </script>
306
- </body>
307
- </html>
 
20
  <script type="module">
21
  import * as THREE from 'three';
22
 
23
+ // ... (Keep all variables: scene, camera, renderer, playerMesh, etc.) ...
24
  let scene, camera, renderer, playerMesh;
25
  let raycaster, mouse;
26
  const keysPressed = {};
27
  const playerSpeed = 0.15;
28
+ let newlyPlacedObjects = []; // For sessionStorage persistence between non-save reruns
29
  const placeholderPlots = new Set();
30
  const groundMeshes = {};
31
+ const allRenderedObjects = {}; // Track all scene objects: id -> mesh/group
32
 
33
  const SESSION_STORAGE_KEY = 'unsavedDbWorldState_v2';
34
 
35
+ // --- Access State from Streamlit ---
36
+ const allInitialObjects = window.ALL_INITIAL_OBJECTS || []; // From DB
37
+ const plotsMetadata = window.PLOTS_METADATA || []; // From DB
38
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
39
  const plotWidth = window.PLOT_WIDTH || 50.0;
40
  const plotDepth = window.PLOT_DEPTH || 50.0;
41
 
 
42
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide });
43
  const placeholderGroundMaterial = new THREE.MeshStandardMaterial({ color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide });
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ // --- init() function ---
47
+ function init() {
48
+ // ... (scene, camera, renderer setup as before) ...
49
+ scene = new THREE.Scene(); scene.background = new THREE.Color(0xabcdef);
50
+ const aspect = window.innerWidth / window.innerHeight; camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000); camera.position.set(0, 15, 20); camera.lookAt(0, 0, 0); scene.add(camera);
51
+ setupLighting(); setupInitialGround(); setupPlayer();
52
+ raycaster = new THREE.Raycaster(); mouse = new THREE.Vector2();
53
+ renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement);
54
 
55
+ loadInitialObjects(); // Load from DB data
56
+ restoreUnsavedState(); // Restore from SessionStorage
57
 
58
  // Event Listeners
59
+ document.addEventListener('mousemove', onMouseMove, false); document.addEventListener('click', onDocumentClick, false); window.addEventListener('resize', onWindowResize, false); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp);
 
 
 
 
60
 
61
+ // Define global functions needed by Python
62
  window.teleportPlayer = teleportPlayer;
63
+ window.getSaveDataAndPosition = getSaveDataAndPosition; // Ensure this is defined correctly below
64
+ // window.resetNewlyPlacedObjects = resetNewlyPlacedObjects; // No longer needed
65
 
66
  console.log("Three.js Initialized (DB Backend v2). World ready.");
67
  animate();
68
  }
69
 
70
+ // --- setupLighting(), setupInitialGround(), createGroundPlane(), setupPlayer() ---
71
+ // ... (Keep these functions exactly as in the previous version) ...
72
+ function setupLighting() { const a=new THREE.AmbientLight(0xffffff,0.5); scene.add(a); const d=new THREE.DirectionalLight(0xffffff,1.0); d.position.set(50,150,100); d.castShadow=true; d.shadow.mapSize.width=4096; d.shadow.mapSize.height=4096; d.shadow.camera.near=0.5; d.shadow.camera.far=500; d.shadow.camera.left=-150; d.shadow.camera.right=150; d.shadow.camera.top=150; d.shadow.camera.bottom=-150; d.shadow.bias=-0.001; scene.add(d); }
73
+ function setupInitialGround() { plotsMetadata.forEach(p => {createGroundPlane(p.grid_x,p.grid_z,false);}); if(plotsMetadata.length===0) {createGroundPlane(0,0,false);} }
74
+ function createGroundPlane(gx,gz,isPlaceholder) { const k=`<span class="math-inline">\{gx\}\_</span>{gz}`; if(groundMeshes[k]) return; const geo=new THREE.PlaneGeometry(plotWidth,plotDepth); const mat=isPlaceholder?placeholderGroundMaterial:groundMaterial; const mesh=new THREE.Mesh(geo,mat); mesh.rotation.x=-Math.PI/2; mesh.position.y=-0.05; mesh.position.x=gx*plotWidth+plotWidth/2; mesh.position.z=gz*plotDepth+plotDepth/2; mesh.receiveShadow=true; mesh.userData.gridKey=k; scene.add(mesh); groundMeshes[k]=mesh; if(isPlaceholder){placeholderPlots.add(k);} }
75
+ function setupPlayer() { const g=new THREE.CapsuleGeometry(0.4,0.8,4,8); const m=new THREE.MeshStandardMaterial({color:0x0000ff,roughness:0.6}); playerMesh=new THREE.Mesh(g,m); playerMesh.position.set(plotWidth/2,0.8,plotDepth/2); playerMesh.castShadow=true; playerMesh.receiveShadow=true; scene.add(playerMesh); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
 
 
 
 
77
 
78
+ // --- loadInitialObjects(), clearAllRenderedObjects(), createAndPlaceObject() ---
79
+ // ... (Keep these functions exactly as in the previous version - ensuring createAndPlaceObject adds to allRenderedObjects) ...
80
+ function loadInitialObjects() { console.log(`Loading ${allInitialObjects.length} initial objects.`); clearAllRenderedObjects(); allInitialObjects.forEach(d => { createAndPlaceObject(d, false); }); console.log("Finished initial load."); }
81
+ function clearAllRenderedObjects() { Object.values(allRenderedObjects).forEach(o => scene.remove(o)); for (const k in allRenderedObjects) delete allRenderedObjects[k]; newlyPlacedObjects = []; /* Important: clear this too when reloading all */ }
82
+ function createAndPlaceObject(objData, isNewObjectForSession) { let obj=null; switch (objData.type) { case "Simple House": obj=createSimpleHouse(); break; case "Tree": obj=createTree(); break; case "Rock": obj=createRock(); break; case "Fence Post": obj=createFencePost(); break; default: return null; } if (obj) { obj.userData.obj_id = objData.obj_id || obj.userData.obj_id; if (allRenderedObjects[obj.userData.obj_id]) return null; if(objData.position&&objData.position.x!==undefined){obj.position.set(objData.position.x,objData.position.y,objData.position.z);}else if(objData.pos_x!==undefined){obj.position.set(objData.pos_x,objData.pos_y,objData.pos_z);} if(objData.rotation){obj.rotation.set(objData.rotation._x,objData.rotation._y,objData.rotation._z,objData.rotation._order||'XYZ');}else if(objData.rot_x!==undefined){obj.rotation.set(objData.rot_x,objData.rot_y,objData.rot_z,objData.rot_order||'XYZ');} scene.add(obj); allRenderedObjects[obj.userData.obj_id] = obj; if(isNewObjectForSession){newlyPlacedObjects.push(obj);} return obj; } return null; }
 
 
 
83
 
84
+ // --- saveUnsavedState(), restoreUnsavedState(), clearUnsavedSessionState() ---
85
+ // ... (Keep these sessionStorage functions exactly as in the previous version) ...
86
+ function saveUnsavedState() { try { const d = newlyPlacedObjects.map(o => ({obj_id:o.userData.obj_id, type:o.userData.type, position:{x:o.position.x,y:o.position.y,z:o.position.z}, rotation:{_x:o.rotation.x,_y:o.rotation.y,_z:o.rotation.z,_order:o.rotation.order}})); sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(d)); } catch(e) { console.error("Session save error:", e); } }
87
+ function restoreUnsavedState() { try { const s=sessionStorage.getItem(SESSION_STORAGE_KEY); if(s) { const d=JSON.parse(s); if(Array.isArray(d)) { let c=0; d.forEach(o => { if(createAndPlaceObject(o, true)) c++;}); console.log(`Restored ${c} unsaved objects.`); } } } catch(e) { console.error("Session restore error:", e); sessionStorage.removeItem(SESSION_STORAGE_KEY); } }
88
+ // function clearUnsavedSessionState() { sessionStorage.removeItem(SESSION_STORAGE_KEY); newlyPlacedObjects = []; console.log("Cleared unsaved session state."); } // Might be useful later
89
 
90
+ // --- Object Creation Functions ---
91
+ // ... (Keep these exactly as before, ensuring they assign userData.type and userData.obj_id) ...
92
+ function createObjectBase(type) { return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } }; }
93
+ function createSimpleHouse() { const base = createObjectBase("Simple House"); const group = new THREE.Group(); Object.assign(group, base); const mat1=new THREE.MeshStandardMaterial({color:0xffccaa,roughness:0.8}), mat2=new THREE.MeshStandardMaterial({color:0xaa5533,roughness:0.7}); const m1=new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5),mat1); m1.position.y=0.75;m1.castShadow=true;m1.receiveShadow=true;group.add(m1); const m2=new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4),mat2); m2.position.y=1.5+0.5;m2.rotation.y=Math.PI/4;m2.castShadow=true;m2.receiveShadow=true;group.add(m2); return group; }
94
+ function createTree() { const base=createObjectBase("Tree"); const group=new THREE.Group(); Object.assign(group,base); const mat1=new THREE.MeshStandardMaterial({color:0x8B4513,roughness:0.9}), mat2=new THREE.MeshStandardMaterial({color:0x228B22,roughness:0.8}); 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); const m2=