File size: 16,948 Bytes
06b17d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
<!DOCTYPE html>
<html>
<head>
    <title>Three.js Shared World</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
        /* Removed Save Button - Triggered from Streamlit now */
    </style>
    </head>
<body>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/[email protected]/build/three.module.js",
                 "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';

        let scene, camera, renderer, groundMesh = null, playerMesh;
        let raycaster, mouse;
        const keysPressed = {};
        const playerSpeed = 0.15;
        let newlyPlacedObjects = []; // Track objects added THIS session for saving

        // --- Access State from Streamlit ---
        const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
        const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
        const plotWidth = window.PLOT_WIDTH || 50.0;
        const nextPlotXOffset = window.NEXT_PLOT_X_OFFSET || 0.0; // Where the next plot starts


        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xabcdef);

            const aspect = window.innerWidth / window.innerHeight;
            camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 2000); // Increase far plane
            camera.position.set(0, 15, 20);
            camera.lookAt(0, 0, 0);
            scene.add(camera);

            setupLighting();
            setupGround(); // Ground needs to be potentially very wide now
            setupPlayer();

            raycaster = new THREE.Raycaster();
            mouse = new THREE.Vector2();

            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);

            loadInitialObjects(); // Load ALL objects passed from Python

            // Event Listeners
            document.addEventListener('mousemove', onMouseMove, false);
            document.addEventListener('click', onDocumentClick, false);
            window.addEventListener('resize', onWindowResize, false);
            document.addEventListener('keydown', onKeyDown);
            document.addEventListener('keyup', onKeyUp);

            // Define global functions needed by Python/streamlit-js-eval
            window.teleportPlayer = teleportPlayer;
            window.getSaveData = getSaveData;
            window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;

            console.log("Three.js Initialized. Ready for commands.");
            animate();
        }

        function setupLighting() { /* ... unchanged ... */
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
            directionalLight.position.set(50, 100, 75); // Adjust light position for wider world
            directionalLight.castShadow = true;
            directionalLight.shadow.mapSize.width = 2048*2; // Larger shadow map maybe needed
            directionalLight.shadow.mapSize.height = 2048*2;
            directionalLight.shadow.camera.near = 0.5;
            directionalLight.shadow.camera.far = 500; // Increase shadow distance
            // Adjust shadow camera frustum dynamically later if needed, make it wide for now
            directionalLight.shadow.camera.left = -100;
            directionalLight.shadow.camera.right = 100;
            directionalLight.shadow.camera.top = 100;
            directionalLight.shadow.camera.bottom = -100;
            directionalLight.shadow.bias = -0.001;
            scene.add(directionalLight);
            // Optional shadow helper
            // const shadowHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
            // scene.add(shadowHelper);
        }

        function setupGround() {
            // Ground needs to span the width of all loaded plots potentially
            // Calculate width based on next offset (where the world ends visually for now)
            const groundWidth = Math.max(plotWidth, nextPlotXOffset + plotWidth); // At least one plot wide, or cover all loaded + next slot
            const groundDepth = 50; // Keep depth constant for now

            const groundGeometry = new THREE.PlaneGeometry(groundWidth, groundDepth);
            const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, roughness: 0.9, metalness: 0.1 });
            groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
            groundMesh.rotation.x = -Math.PI / 2;
            groundMesh.position.y = -0.05;
             // Center the ground based on calculated width
            groundMesh.position.x = (groundWidth / 2.0) - (plotWidth / 2.0) ; // Adjust so x=0 is near the start

            groundMesh.receiveShadow = true;
            scene.add(groundMesh);
            console.log(`Ground setup with width: ${groundWidth} centered near x=0`);
        }

        function setupPlayer() { /* ... unchanged ... */
            const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
            const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
            playerMesh = new THREE.Mesh(playerGeo, playerMat);
            playerMesh.position.set(2, 0.4 + 0.8/2, 5); // Start near origin (x=2 to be slightly in first plot)
            playerMesh.castShadow = true;
            playerMesh.receiveShadow = true;
            scene.add(playerMesh);
        }

        function loadInitialObjects() {
            console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
            allInitialObjects.forEach(objData => {
                let loadedObject = null;
                // Need to deserialize object based on 'type' field from CSV
                switch (objData.type) {
                    case "Simple House": loadedObject = createSimpleHouse(); break;
                    case "Tree": loadedObject = createTree(); break;
                    case "Rock": loadedObject = createRock(); break;
                    case "Fence Post": loadedObject = createFencePost(); break;
                    // Add other types if needed
                    default: console.warn("Unknown object type in loaded data:", objData.type); break;
                }

                if (loadedObject && objData.pos_x !== undefined) {
                    // Position is already WORLD position (offset applied in Python)
                    loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
                    // Apply rotation (assuming Euler order is XYZ, adjust if different)
                    if(objData.rot_x !== undefined){
                         loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
                    }
                    // Add unique ID if needed for later interaction
                    loadedObject.userData.obj_id = objData.obj_id || null;

                    scene.add(loadedObject);
                    // DO NOT add to newlyPlacedObjects here - these are pre-existing
                }
            });
             console.log("Finished loading initial objects.");
        }

        // --- Object Creation Functions (MUST add userData.type and obj_id) ---
        function createObjectBase(type) { // Helper to assign common data
             const obj = { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
             return obj;
        }
         function createSimpleHouse() {
            const base = createObjectBase("Simple House"); // Get base object with data
            const group = new THREE.Group();
            Object.assign(group, base); // Copy properties like userData

            const mainMaterial = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.8 });
            const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xaa5533, roughness: 0.7 });
            const baseMesh = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 2.5), mainMaterial);
            baseMesh.position.y = 1.5 / 2; baseMesh.castShadow = true; baseMesh.receiveShadow = true; group.add(baseMesh);
            const roof = new THREE.Mesh(new THREE.ConeGeometry(1.8, 1, 4), roofMaterial);
            roof.position.y = 1.5 + 1 / 2; roof.rotation.y = Math.PI / 4; roof.castShadow = true; roof.receiveShadow = true; group.add(roof);
            return group;
         }
         function createTree() {
            const base = createObjectBase("Tree");
            const group = new THREE.Group();
             Object.assign(group, base);

            const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 });
            const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22, roughness: 0.8 });
            const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.4, 2, 8), trunkMaterial);
            trunk.position.y = 2 / 2; trunk.castShadow = true; trunk.receiveShadow = true; group.add(trunk);
            const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 0), leavesMaterial);
            leaves.position.y = 2 + 0.8; leaves.castShadow = true; leaves.receiveShadow = true; group.add(leaves);
            return group;
         }
         function createRock() {
             const base = createObjectBase("Rock");
             const rockMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.8, metalness: 0.1 });
             const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7, 0), rockMaterial);
             Object.assign(rock, base); // Add userData

             rock.position.y = 0.7 / 2; rock.rotation.x = Math.random() * Math.PI; rock.rotation.y = Math.random() * Math.PI;
             rock.castShadow = true; rock.receiveShadow = true;
             return rock;
         }
         function createFencePost() {
             const base = createObjectBase("Fence Post");
             const postMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.9 });
             const post = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.2), postMaterial);
             Object.assign(post, base); // Add userData

             post.position.y = 1.5 / 2; post.castShadow = true; post.receiveShadow = true;
             return post;
         }


        // --- Event Handlers ---
        function onMouseMove(event) { /* ... unchanged ... */
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
        }

        function onDocumentClick(event) {
             if (selectedObjectType === "None" || !groundMesh) return;

            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObject(groundMesh); // Intersect ground ONLY

            if (intersects.length > 0) {
                const intersectPoint = intersects[0].point;
                let newObject = null;

                switch (selectedObjectType) { // Use create functions
                    case "Simple House": newObject = createSimpleHouse(); break;
                    case "Tree": newObject = createTree(); break;
                    case "Rock": newObject = createRock(); break;
                    case "Fence Post": newObject = createFencePost(); break;
                    default: return; // Don't place unknown types
                }

                if (newObject) {
                    // Position in WORLD coordinates where clicked
                    newObject.position.copy(intersectPoint);
                    // Adjust Y so base is on ground (mostly handled by create funcs)
                    if (newObject.geometry && newObject.geometry.boundingBox) {
                        // Optional fine tuning if needed
                    }
                    scene.add(newObject);
                    newlyPlacedObjects.push(newObject); // Add to list for saving
                    console.log(`Placed new ${selectedObjectType} at ${newObject.position.x.toFixed(2)}, ${newObject.position.z.toFixed(2)}. Total new: ${newlyPlacedObjects.length}`);
                }
            }
        }

        function onKeyDown(event) { keysPressed[event.code] = true; }
        function onKeyUp(event) { keysPressed[event.code] = false; }

        // --- Functions called by Python via streamlit-js-eval ---
        function teleportPlayer(targetX) {
            console.log("JS teleportPlayer called with targetX:", targetX);
            if (playerMesh) {
                // Teleport to start of the plot (targetX) plus a small offset
                playerMesh.position.x = targetX + 2.0; // Start slightly inside the plot
                playerMesh.position.z = 5.0; // Reset Z position too
                // Snap camera instantly - adjust Y height if needed
                 const offset = new THREE.Vector3(0, 15, 20); // Reset camera offset
                 const targetPosition = playerMesh.position.clone().add(offset);
                 camera.position.copy(targetPosition);
                 camera.lookAt(playerMesh.position);
                 console.log("Player teleported to:", playerMesh.position);
            } else {
                console.error("Player mesh not found for teleport.");
            }
        }

        function getSaveData() {
             console.log(`JS getSaveData called. Found ${newlyPlacedObjects.length} new objects.`);
             const objectsToSave = newlyPlacedObjects.map(obj => {
                 if (!obj.userData || !obj.userData.type) {
                     console.warn("Skipping object with missing user data during save prep:", obj);
                     return null; // Skip objects without type/id
                 }
                 // Send WORLD positions to Python, it will make them relative
                 const rotation = {
                     _x: obj.rotation.x,
                     _y: obj.rotation.y,
                     _z: obj.rotation.z,
                     _order: obj.rotation.order
                 };
                 return {
                     obj_id: obj.userData.obj_id, // Send unique ID
                     type: obj.userData.type,
                     position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
                     rotation: rotation
                 };
             }).filter(obj => obj !== null); // Remove nulls

             console.log("Prepared data for saving:", objectsToSave);
             return JSON.stringify(objectsToSave); // Return as JSON string
        }

        function resetNewlyPlacedObjects() {
             console.log(`JS resetNewlyPlacedObjects called. Clearing ${newlyPlacedObjects.length} objects.`);
             newlyPlacedObjects = []; // Clear the array after successful save
        }


        // --- Animation Loop ---
        function updatePlayerMovement() { /* ... unchanged ... */
            if (!playerMesh) return;
            const moveDirection = new THREE.Vector3(0, 0, 0);
            if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
            if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
            if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
            if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;

            if (moveDirection.lengthSq() > 0) {
                moveDirection.normalize().multiplyScalar(playerSpeed);
                 playerMesh.position.add(moveDirection);
                 // Basic ground clamping
                 playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
            }
        }

        function updateCamera() { /* ... unchanged ... */
            if (!playerMesh) return;
            const offset = new THREE.Vector3(0, 15, 20);
            const targetPosition = playerMesh.position.clone().add(offset);
            camera.position.lerp(targetPosition, 0.08); // Slightly slower lerp
            camera.lookAt(playerMesh.position);
        }

        function onWindowResize() { /* ... unchanged ... */
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            requestAnimationFrame(animate);
            updatePlayerMovement();
            updateCamera();
            renderer.render(scene, camera);
        }

        // --- Start ---
        init();

    </script>
</body>
</html>