awacke1 commited on
Commit
006833b
·
verified ·
1 Parent(s): d046ba4

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +243 -168
index.html CHANGED
@@ -1,13 +1,32 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>Three.js Pick and Place</title>
5
  <style>
6
- body { margin: 0; overflow: hidden; } /* Hide scrollbars */
7
  canvas { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </style>
9
  </head>
10
  <body>
 
 
11
  <script type="importmap">
12
  {
13
  "imports": {
@@ -20,242 +39,298 @@
20
  <script type="module">
21
  import * as THREE from 'three';
22
 
23
- let scene, camera, renderer, groundMesh;
24
  let raycaster, mouse;
25
- const placedObjects = []; // Keep track of placed objects
 
 
26
 
27
- // --- Access State from Streamlit (set in global scope via injection) ---
28
- // Default value if injection fails for some reason
29
  const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
 
 
 
30
 
31
  function init() {
32
- // --- Basic Setup ---
33
  scene = new THREE.Scene();
34
- scene.background = new THREE.Color(0xabcdef); // Light blue sky
35
 
36
- // --- Camera (Orthographic adjusted for better view) ---
37
  const aspect = window.innerWidth / window.innerHeight;
38
- const viewSize = 30; // Increase view size
39
- camera = new THREE.OrthographicCamera(
40
- -viewSize * aspect / 2, viewSize * aspect / 2,
41
- viewSize / 2, -viewSize / 2,
42
- 0.1, 100 // Adjust near/far planes
43
- );
44
- // Position camera higher and angled slightly for better 3D view
45
- camera.position.set(10, 20, 10);
46
- camera.lookAt(scene.position); // Look at the center
47
- camera.zoom = 1.2; // Zoom in a bit
48
- camera.updateProjectionMatrix();
49
  scene.add(camera);
50
 
51
- // --- Lighting (Improved) ---
52
- // Ambient light for overall illumination
53
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Soft white light
54
- scene.add(ambientLight);
55
 
56
- // Directional light for sunlight and shadows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
58
- directionalLight.position.set(15, 30, 20); // Adjust position for angle
59
  directionalLight.castShadow = true;
60
- // Configure shadow properties (important!)
61
- directionalLight.shadow.mapSize.width = 2048; // Higher res shadows
62
  directionalLight.shadow.mapSize.height = 2048;
63
  directionalLight.shadow.camera.near = 0.5;
64
  directionalLight.shadow.camera.far = 100;
65
- // Adjust shadow camera frustum to cover the playable area
66
  directionalLight.shadow.camera.left = -30;
67
  directionalLight.shadow.camera.right = 30;
68
  directionalLight.shadow.camera.top = 30;
69
  directionalLight.shadow.camera.bottom = -30;
70
- directionalLight.shadow.bias = -0.001; // Helps prevent shadow acne
71
-
72
  scene.add(directionalLight);
73
- // Optional: Visualize the shadow camera
74
- // const shadowHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
75
- // scene.add(shadowHelper);
76
-
77
-
78
- // --- Ground ---
79
- const groundGeometry = new THREE.PlaneGeometry(50, 50); // Larger ground
80
- // Use MeshStandardMaterial for lighting/shadows
81
- const groundMaterial = new THREE.MeshStandardMaterial({
82
- color: 0x55aa55, // Grassy green
83
- roughness: 0.9,
84
- metalness: 0.1
85
- });
86
  groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
87
- groundMesh.rotation.x = -Math.PI / 2; // Rotate flat
88
- groundMesh.position.y = -0.05; // Position slightly below origin
89
- groundMesh.receiveShadow = true; // Allow ground to receive shadows
90
  scene.add(groundMesh);
 
91
 
92
- // --- Raycaster and Mouse Vector ---
93
- raycaster = new THREE.Raycaster();
94
- mouse = new THREE.Vector2();
95
-
96
- // --- Renderer (Shadows Enabled) ---
97
- renderer = new THREE.WebGLRenderer({ antialias: true });
98
- renderer.setSize(window.innerWidth, window.innerHeight);
99
- renderer.shadowMap.enabled = true; // Enable shadow map rendering
100
- renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
101
- document.body.appendChild(renderer.domElement);
102
-
103
- // --- Event Listeners ---
104
- document.addEventListener('mousemove', onMouseMove, false);
105
- document.addEventListener('click', onDocumentClick, false);
106
- window.addEventListener('resize', onWindowResize, false);
107
 
108
- // --- Start Animation ---
109
- animate();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
 
112
- // --- Object Creation Functions (Using Primitives) ---
113
 
114
- function createSimpleHouse() {
 
115
  const group = new THREE.Group();
116
  const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
117
  const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
118
-
119
- // Base (BoxGeometry)
120
  const base = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
121
- base.position.y = 1.5 / 2; // Sit on ground
122
- base.castShadow = true;
123
- base.receiveShadow = true;
124
- group.add(base);
125
-
126
- // Roof (ConeGeometry or angled planes) - using Cone here
127
- const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial); // Radius, Height, Segments (4 for pyramid)
128
- roof.position.y = 1.5 + 1 / 2; // Place on top of base
129
- roof.rotation.y = Math.PI / 4; // Align edges
130
- roof.castShadow = true;
131
- roof.receiveShadow = true;
132
- group.add(roof);
133
-
134
  return group;
135
- }
136
-
137
- function createTree() {
138
  const group = new THREE.Group();
139
- const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }); // Brown
140
- const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 }); // Forest Green
141
-
142
- // Trunk (CylinderGeometry)
143
- const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial); // RadiusTop, RadiusBottom, Height, Segments
144
- trunk.position.y = 2 / 2;
145
- trunk.castShadow = true;
146
- trunk.receiveShadow = true;
147
- group.add(trunk);
148
-
149
- // Leaves (SphereGeometry or Icosahedron)
150
- const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial); // Radius, Detail (0=less poly)
151
- leaves.position.y = 2 + 0.8; // Place above trunk
152
- leaves.castShadow = true;
153
- leaves.receiveShadow = true;
154
- group.add(leaves);
155
-
156
  return group;
157
- }
158
-
159
- function createRock() {
160
- // Use Icosahedron for irregular shape, or Sphere with displacement later
161
  const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
162
- const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial); // Radius, Detail
163
- rock.position.y = 0.7 / 2; // Sit on ground (approx)
164
- rock.rotation.x = Math.random() * Math.PI;
165
- rock.rotation.y = Math.random() * Math.PI;
166
- rock.castShadow = true;
167
- rock.receiveShadow = true;
168
- return rock; // Simple mesh, no group needed
169
- }
170
-
171
- function createFencePost() {
172
- const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 }); // BurlyWood
173
- // Use BoxGeometry for a simple square post
174
  const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial);
175
- post.position.y = 1.5 / 2;
176
- post.castShadow = true;
177
- post.receiveShadow = true;
178
  return post;
179
- }
180
 
181
  // --- Event Handlers ---
182
-
183
  function onMouseMove(event) {
184
- // Calculate mouse position in normalized device coordinates (-1 to +1)
185
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
186
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
187
  }
188
 
189
  function onDocumentClick(event) {
190
- console.log("Click! Selected type:", selectedObjectType); // Debug
191
- if (selectedObjectType === "None") {
192
- console.log("No object type selected for placement.");
193
- return; // Don't place anything if "None" is selected
194
- }
195
 
196
- // Update the picking ray with the camera and mouse position
197
- raycaster.setFromCamera(mouse, camera);
198
 
199
- // Calculate objects intersecting the picking ray - only check the ground
 
200
  const intersects = raycaster.intersectObject(groundMesh);
201
 
202
  if (intersects.length > 0) {
203
  const intersectPoint = intersects[0].point;
204
- console.log("Intersection at:", intersectPoint); // Debug
205
-
206
  let newObject = null;
207
 
208
- // Call the correct creation function based on the selected type
209
- switch (selectedObjectType) {
210
- case "Simple House":
211
- newObject = createSimpleHouse();
212
- break;
213
- case "Tree":
214
- newObject = createTree();
215
- break;
216
- case "Rock":
217
- newObject = createRock();
218
- break;
219
- case "Fence Post":
220
- newObject = createFencePost();
221
- break;
222
- default:
223
- console.warn("Unknown object type:", selectedObjectType);
224
- return; // Don't place unknown types
225
  }
226
 
227
  if (newObject) {
228
- // Position the new object at the click point on the ground
229
- // The Y position is handled within the create functions to sit on origin
230
- newObject.position.x = intersectPoint.x;
231
- newObject.position.z = intersectPoint.z;
232
- // Y position might need slight adjustment depending on object origin
233
- // For objects created with base at y=0, intersectPoint.y is close enough
234
-
235
  scene.add(newObject);
236
- placedObjects.push(newObject); // Track the object
237
- console.log(`Placed ${selectedObjectType} at`, newObject.position);
238
  }
239
- } else {
240
- console.log("Click did not intersect ground.");
241
  }
242
  }
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  function onWindowResize() {
246
- const aspect = window.innerWidth / window.innerHeight;
247
- const viewSize = 30;
248
- camera.left = -viewSize * aspect / 2;
249
- camera.right = viewSize * aspect / 2;
250
- camera.top = viewSize / 2;
251
- camera.bottom = -viewSize / 2;
252
  camera.updateProjectionMatrix();
253
  renderer.setSize(window.innerWidth, window.innerHeight);
254
  }
255
 
256
  function animate() {
257
  requestAnimationFrame(animate);
258
- // Add any animations here later if needed
 
 
 
259
  renderer.render(scene, camera);
260
  }
261
 
 
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
  <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();
72
+
73
+ renderer = new THREE.WebGLRenderer({ antialias: true });
74
+ renderer.setSize(window.innerWidth, window.innerHeight);
75
+ renderer.shadowMap.enabled = true;
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
  }
239
 
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);
326
  }
327
 
328
  function animate() {
329
  requestAnimationFrame(animate);
330
+
331
+ updatePlayerMovement();
332
+ updateCamera(); // Make camera follow player
333
+
334
  renderer.render(scene, camera);
335
  }
336