awacke1 commited on
Commit
06b17d1
·
verified ·
1 Parent(s): 7825ef7

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +346 -0
index.html ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /* Removed Save Button - Triggered from Streamlit now */
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <script type="importmap">
13
+ {
14
+ "imports": {
15
+ "three": "https://unpkg.com/[email protected]/build/three.module.js",
16
+ "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
17
+ }
18
+ }
19
+ </script>
20
+
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
+ // --- Access State from Streamlit ---
31
+ const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
32
+ const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
33
+ const plotWidth = window.PLOT_WIDTH || 50.0;
34
+ const nextPlotXOffset = window.NEXT_PLOT_X_OFFSET || 0.0; // Where the next plot starts
35
+
36
+
37
+ function init() {
38
+ scene = new THREE.Scene();
39
+ scene.background = new THREE.Color(0xabcdef);
40
+
41
+ const aspect = window.innerWidth / window.innerHeight;
42
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 2000); // Increase far plane
43
+ camera.position.set(0, 15, 20);
44
+ camera.lookAt(0, 0, 0);
45
+ scene.add(camera);
46
+
47
+ setupLighting();
48
+ setupGround(); // Ground needs to be potentially very wide now
49
+ setupPlayer();
50
+
51
+ raycaster = new THREE.Raycaster();
52
+ mouse = new THREE.Vector2();
53
+
54
+ renderer = new THREE.WebGLRenderer({ antialias: true });
55
+ renderer.setSize(window.innerWidth, window.innerHeight);
56
+ renderer.shadowMap.enabled = true;
57
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
58
+ document.body.appendChild(renderer.domElement);
59
+
60
+ loadInitialObjects(); // Load ALL objects passed from Python
61
+
62
+ // Event Listeners
63
+ document.addEventListener('mousemove', onMouseMove, false);
64
+ document.addEventListener('click', onDocumentClick, false);
65
+ window.addEventListener('resize', onWindowResize, false);
66
+ document.addEventListener('keydown', onKeyDown);
67
+ document.addEventListener('keyup', onKeyUp);
68
+
69
+ // Define global functions needed by Python/streamlit-js-eval
70
+ window.teleportPlayer = teleportPlayer;
71
+ window.getSaveData = getSaveData;
72
+ window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;
73
+
74
+ console.log("Three.js Initialized. Ready for commands.");
75
+ animate();
76
+ }
77
+
78
+ function setupLighting() { /* ... unchanged ... */
79
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
80
+ scene.add(ambientLight);
81
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
82
+ directionalLight.position.set(50, 100, 75); // Adjust light position for wider world
83
+ directionalLight.castShadow = true;
84
+ directionalLight.shadow.mapSize.width = 2048*2; // Larger shadow map maybe needed
85
+ directionalLight.shadow.mapSize.height = 2048*2;
86
+ directionalLight.shadow.camera.near = 0.5;
87
+ directionalLight.shadow.camera.far = 500; // Increase shadow distance
88
+ // Adjust shadow camera frustum dynamically later if needed, make it wide for now
89
+ directionalLight.shadow.camera.left = -100;
90
+ directionalLight.shadow.camera.right = 100;
91
+ directionalLight.shadow.camera.top = 100;
92
+ directionalLight.shadow.camera.bottom = -100;
93
+ directionalLight.shadow.bias = -0.001;
94
+ scene.add(directionalLight);
95
+ // Optional shadow helper
96
+ // const shadowHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
97
+ // scene.add(shadowHelper);
98
+ }
99
+
100
+ function setupGround() {
101
+ // Ground needs to span the width of all loaded plots potentially
102
+ // Calculate width based on next offset (where the world ends visually for now)
103
+ const groundWidth = Math.max(plotWidth, nextPlotXOffset + plotWidth); // At least one plot wide, or cover all loaded + next slot
104
+ const groundDepth = 50; // Keep depth constant for now
105
+
106
+ const groundGeometry = new THREE.PlaneGeometry(groundWidth, groundDepth);
107
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1 });
108
+ groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
109
+ groundMesh.rotation.x = -Math.PI / 2;
110
+ groundMesh.position.y = -0.05;
111
+ // Center the ground based on calculated width
112
+ groundMesh.position.x = (groundWidth / 2.0) - (plotWidth / 2.0) ; // Adjust so x=0 is near the start
113
+
114
+ groundMesh.receiveShadow = true;
115
+ scene.add(groundMesh);
116
+ console.log(`Ground setup with width: ${groundWidth} centered near x=0`);
117
+ }
118
+
119
+ function setupPlayer() { /* ... unchanged ... */
120
+ const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
121
+ const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
122
+ playerMesh = new THREE.Mesh(playerGeo, playerMat);
123
+ playerMesh.position.set(2, 0.4 + 0.8/2, 5); // Start near origin (x=2 to be slightly in first plot)
124
+ playerMesh.castShadow = true;
125
+ playerMesh.receiveShadow = true;
126
+ scene.add(playerMesh);
127
+ }
128
+
129
+ function loadInitialObjects() {
130
+ console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
131
+ allInitialObjects.forEach(objData => {
132
+ let loadedObject = null;
133
+ // Need to deserialize object based on 'type' field from CSV
134
+ switch (objData.type) {
135
+ case "Simple House": loadedObject = createSimpleHouse(); break;
136
+ case "Tree": loadedObject = createTree(); break;
137
+ case "Rock": loadedObject = createRock(); break;
138
+ case "Fence Post": loadedObject = createFencePost(); break;
139
+ // Add other types if needed
140
+ default: console.warn("Unknown object type in loaded data:", objData.type); break;
141
+ }
142
+
143
+ if (loadedObject && objData.pos_x !== undefined) {
144
+ // Position is already WORLD position (offset applied in Python)
145
+ loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
146
+ // Apply rotation (assuming Euler order is XYZ, adjust if different)
147
+ if(objData.rot_x !== undefined){
148
+ loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
149
+ }
150
+ // Add unique ID if needed for later interaction
151
+ loadedObject.userData.obj_id = objData.obj_id || null;
152
+
153
+ scene.add(loadedObject);
154
+ // DO NOT add to newlyPlacedObjects here - these are pre-existing
155
+ }
156
+ });
157
+ console.log("Finished loading initial objects.");
158
+ }
159
+
160
+ // --- Object Creation Functions (MUST add userData.type and obj_id) ---
161
+ function createObjectBase(type) { // Helper to assign common data
162
+ const obj = { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
163
+ return obj;
164
+ }
165
+ function createSimpleHouse() {
166
+ const base = createObjectBase("Simple House"); // Get base object with data
167
+ const group = new THREE.Group();
168
+ Object.assign(group, base); // Copy properties like userData
169
+
170
+ const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
171
+ const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
172
+ const baseMesh = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
173
+ baseMesh.position.y = 1.5 / 2; baseMesh.castShadow = true; baseMesh.receiveShadow = true; group.add(baseMesh);
174
+ const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial);
175
+ roof.position.y = 1.5 + 1 / 2; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = true; group.add(roof);
176
+ return group;
177
+ }
178
+ function createTree() {
179
+ const base = createObjectBase("Tree");
180
+ const group = new THREE.Group();
181
+ Object.assign(group, base);
182
+
183
+ const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 });
184
+ const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 });
185
+ const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial);
186
+ trunk.position.y = 2 / 2; trunk.castShadow = true; trunk.receiveShadow = true; group.add(trunk);
187
+ const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial);
188
+ leaves.position.y = 2 + 0.8; leaves.castShadow = true; leaves.receiveShadow = true; group.add(leaves);
189
+ return group;
190
+ }
191
+ function createRock() {
192
+ const base = createObjectBase("Rock");
193
+ const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
194
+ const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial);
195
+ Object.assign(rock, base); // Add userData
196
+
197
+ rock.position.y = 0.7 / 2; rock.rotation.x = Math.random() * Math.PI; rock.rotation.y = Math.random() * Math.PI;
198
+ rock.castShadow = true; rock.receiveShadow = true;
199
+ return rock;
200
+ }
201
+ function createFencePost() {
202
+ const base = createObjectBase("Fence Post");
203
+ const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 });
204
+ const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial);
205
+ Object.assign(post, base); // Add userData
206
+
207
+ post.position.y = 1.5 / 2; post.castShadow = true; post.receiveShadow = true;
208
+ return post;
209
+ }
210
+
211
+
212
+ // --- Event Handlers ---
213
+ function onMouseMove(event) { /* ... unchanged ... */
214
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
215
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
216
+ }
217
+
218
+ function onDocumentClick(event) {
219
+ if (selectedObjectType === "None" || !groundMesh) return;
220
+
221
+ raycaster.setFromCamera(mouse, camera);
222
+ const intersects = raycaster.intersectObject(groundMesh); // Intersect ground ONLY
223
+
224
+ if (intersects.length > 0) {
225
+ const intersectPoint = intersects[0].point;
226
+ let newObject = null;
227
+
228
+ switch (selectedObjectType) { // Use create functions
229
+ case "Simple House": newObject = createSimpleHouse(); break;
230
+ case "Tree": newObject = createTree(); break;
231
+ case "Rock": newObject = createRock(); break;
232
+ case "Fence Post": newObject = createFencePost(); break;
233
+ default: return; // Don't place unknown types
234
+ }
235
+
236
+ if (newObject) {
237
+ // Position in WORLD coordinates where clicked
238
+ newObject.position.copy(intersectPoint);
239
+ // Adjust Y so base is on ground (mostly handled by create funcs)
240
+ if (newObject.geometry && newObject.geometry.boundingBox) {
241
+ // Optional fine tuning if needed
242
+ }
243
+ scene.add(newObject);
244
+ newlyPlacedObjects.push(newObject); // Add to list for saving
245
+ console.log(`Placed new ${selectedObjectType} at ${newObject.position.x.toFixed(2)}, ${newObject.position.z.toFixed(2)}. Total new: ${newlyPlacedObjects.length}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ function onKeyDown(event) { keysPressed[event.code] = true; }
251
+ function onKeyUp(event) { keysPressed[event.code] = false; }
252
+
253
+ // --- Functions called by Python via streamlit-js-eval ---
254
+ function teleportPlayer(targetX) {
255
+ console.log("JS teleportPlayer called with targetX:", targetX);
256
+ if (playerMesh) {
257
+ // Teleport to start of the plot (targetX) plus a small offset
258
+ playerMesh.position.x = targetX + 2.0; // Start slightly inside the plot
259
+ playerMesh.position.z = 5.0; // Reset Z position too
260
+ // Snap camera instantly - adjust Y height if needed
261
+ const offset = new THREE.Vector3(0, 15, 20); // Reset camera offset
262
+ const targetPosition = playerMesh.position.clone().add(offset);
263
+ camera.position.copy(targetPosition);
264
+ camera.lookAt(playerMesh.position);
265
+ console.log("Player teleported to:", playerMesh.position);
266
+ } else {
267
+ console.error("Player mesh not found for teleport.");
268
+ }
269
+ }
270
+
271
+ function getSaveData() {
272
+ console.log(`JS getSaveData called. Found ${newlyPlacedObjects.length} new objects.`);
273
+ const objectsToSave = newlyPlacedObjects.map(obj => {
274
+ if (!obj.userData || !obj.userData.type) {
275
+ console.warn("Skipping object with missing user data during save prep:", obj);
276
+ return null; // Skip objects without type/id
277
+ }
278
+ // Send WORLD positions to Python, it will make them relative
279
+ const rotation = {
280
+ _x: obj.rotation.x,
281
+ _y: obj.rotation.y,
282
+ _z: obj.rotation.z,
283
+ _order: obj.rotation.order
284
+ };
285
+ return {
286
+ obj_id: obj.userData.obj_id, // Send unique ID
287
+ type: obj.userData.type,
288
+ position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
289
+ rotation: rotation
290
+ };
291
+ }).filter(obj => obj !== null); // Remove nulls
292
+
293
+ console.log("Prepared data for saving:", objectsToSave);
294
+ return JSON.stringify(objectsToSave); // Return as JSON string
295
+ }
296
+
297
+ function resetNewlyPlacedObjects() {
298
+ console.log(`JS resetNewlyPlacedObjects called. Clearing ${newlyPlacedObjects.length} objects.`);
299
+ newlyPlacedObjects = []; // Clear the array after successful save
300
+ }
301
+
302
+
303
+ // --- Animation Loop ---
304
+ function updatePlayerMovement() { /* ... unchanged ... */
305
+ if (!playerMesh) return;
306
+ const moveDirection = new THREE.Vector3(0, 0, 0);
307
+ if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
308
+ if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
309
+ if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
310
+ if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
311
+
312
+ if (moveDirection.lengthSq() > 0) {
313
+ moveDirection.normalize().multiplyScalar(playerSpeed);
314
+ playerMesh.position.add(moveDirection);
315
+ // Basic ground clamping
316
+ playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
317
+ }
318
+ }
319
+
320
+ function updateCamera() { /* ... unchanged ... */
321
+ if (!playerMesh) return;
322
+ const offset = new THREE.Vector3(0, 15, 20);
323
+ const targetPosition = playerMesh.position.clone().add(offset);
324
+ camera.position.lerp(targetPosition, 0.08); // Slightly slower lerp
325
+ camera.lookAt(playerMesh.position);
326
+ }
327
+
328
+ function onWindowResize() { /* ... unchanged ... */
329
+ camera.aspect = window.innerWidth / window.innerHeight;
330
+ camera.updateProjectionMatrix();
331
+ renderer.setSize(window.innerWidth, window.innerHeight);
332
+ }
333
+
334
+ function animate() {
335
+ requestAnimationFrame(animate);
336
+ updatePlayerMovement();
337
+ updateCamera();
338
+ renderer.render(scene, camera);
339
+ }
340
+
341
+ // --- Start ---
342
+ init();
343
+
344
+ </script>
345
+ </body>
346
+ </html>