ArturoNereu commited on
Commit
47dd8dd
·
1 Parent(s): 7aa89e4

Improved UX for object placement

Browse files
Files changed (3) hide show
  1. app.py +8 -18
  2. chat_client.py +14 -24
  3. frontend/game_viewer.html +98 -134
app.py CHANGED
@@ -176,7 +176,7 @@ def get_chat_client(provider: str = "openai"):
176
  return chat_client
177
 
178
 
179
- def chat_response(message, history, crosshair_position=None, provider="openai"):
180
  """Handle chat messages using LLM with tool calling"""
181
  global current_scene_id
182
 
@@ -187,7 +187,8 @@ def chat_response(message, history, crosshair_position=None, provider="openai"):
187
  I'm an AI assistant that can help you build 3D scenes using natural language.
188
 
189
  **What I can do:**
190
- - Add objects: "add a red cube at 2, 1, 0"
 
191
  - Change lighting: "set lighting to night"
192
  - Configure player: "set speed to 10" or "make the player move half as fast"
193
  - Add lights: "add a point light above the cube"
@@ -205,14 +206,14 @@ I'm an AI assistant that can help you build 3D scenes using natural language.
205
  - Press C in viewer to toggle FPS/Orbit camera
206
  - WASD to move, Space to jump in FPS mode
207
  - Click in viewer to enable mouse-look
 
208
 
209
  **LLM Provider:** Currently using {provider.upper()}
210
  """.format(provider=provider), None
211
 
212
  try:
213
  client = get_chat_client(provider)
214
- # Pass crosshair position to chat client for context-aware object placement
215
- response, action_data = client.chat(message, crosshair_position=crosshair_position)
216
  return response, action_data
217
  except Exception as e:
218
  import traceback
@@ -400,11 +401,6 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
400
  if (event.data && event.data.action === 'objectDeselected') {
401
  window.selectedObjectId = null;
402
  }
403
-
404
- if (event.data && event.data.action === 'crosshairPosition') {
405
- // Store crosshair position in window for JS access
406
- window.crosshairPosition = event.data.data;
407
- }
408
  });
409
 
410
  // Initialize toast container on load
@@ -420,9 +416,6 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
420
  </script>
421
  """)
422
 
423
- # State for crosshair position (received from JS via window.crosshairPosition)
424
- crosshair_state = gr.State("")
425
-
426
  # State for selected LLM provider
427
  provider_state = gr.State("openai")
428
 
@@ -553,12 +546,9 @@ with gr.Blocks(title="GCP - Game Context Protocol") as demo:
553
  else:
554
  user_message = content
555
 
556
- # Crosshair position is passed via JS (see js parameter on .then())
557
- # It will be injected by the JS code that runs before this function
558
- crosshair_pos_dict = None
559
-
560
- # Process command with crosshair context and selected provider
561
- bot_message, action_result = chat_response(user_message, [], crosshair_pos_dict, provider)
562
  history.append({"role": "assistant", "content": bot_message})
563
 
564
  # Handle action_result
 
176
  return chat_client
177
 
178
 
179
+ def chat_response(message, history, provider="openai"):
180
  """Handle chat messages using LLM with tool calling"""
181
  global current_scene_id
182
 
 
187
  I'm an AI assistant that can help you build 3D scenes using natural language.
188
 
189
  **What I can do:**
190
+ - Add objects: "add a red cube" (spawns in front of you, Minecraft-style!)
191
+ - Add at position: "add a blue sphere at 2, 1, 0"
192
  - Change lighting: "set lighting to night"
193
  - Configure player: "set speed to 10" or "make the player move half as fast"
194
  - Add lights: "add a point light above the cube"
 
206
  - Press C in viewer to toggle FPS/Orbit camera
207
  - WASD to move, Space to jump in FPS mode
208
  - Click in viewer to enable mouse-look
209
+ - Objects spawn in front of your camera when no position is specified!
210
 
211
  **LLM Provider:** Currently using {provider.upper()}
212
  """.format(provider=provider), None
213
 
214
  try:
215
  client = get_chat_client(provider)
216
+ response, action_data = client.chat(message)
 
217
  return response, action_data
218
  except Exception as e:
219
  import traceback
 
401
  if (event.data && event.data.action === 'objectDeselected') {
402
  window.selectedObjectId = null;
403
  }
 
 
 
 
 
404
  });
405
 
406
  // Initialize toast container on load
 
416
  </script>
417
  """)
418
 
 
 
 
419
  # State for selected LLM provider
420
  provider_state = gr.State("openai")
421
 
 
546
  else:
547
  user_message = content
548
 
549
+ # Process command with selected provider
550
+ # Objects will spawn in front of camera automatically (Minecraft-style)
551
+ bot_message, action_result = chat_response(user_message, [], provider)
 
 
 
552
  history.append({"role": "assistant", "content": bot_message})
553
 
554
  # Handle action_result
chat_client.py CHANGED
@@ -99,16 +99,16 @@ TOOLS = [
99
  "type": "function",
100
  "function": {
101
  "name": "add_object",
102
- "description": "Add a 3D object to the scene",
103
  "parameters": {
104
  "type": "object",
105
  "properties": {
106
  "scene_id": {"type": "string", "description": "ID of the scene"},
107
  "object_type": {"type": "string", "enum": ["cube", "sphere", "cylinder", "plane", "cone", "torus"], "description": "Type of object"},
108
  "name": {"type": "string", "description": "Name for the object"},
109
- "x": {"type": "number", "description": "X position"},
110
- "y": {"type": "number", "description": "Y position"},
111
- "z": {"type": "number", "description": "Z position"},
112
  "scale_x": {"type": "number", "description": "X scale (default: 1)"},
113
  "scale_y": {"type": "number", "description": "Y scale (default: 1)"},
114
  "scale_z": {"type": "number", "description": "Z scale (default: 1)"},
@@ -644,11 +644,16 @@ You help users create and modify 3D scenes using natural language. You have acce
644
  - Managing lights (ambient, directional, point, spot)
645
  - Updating materials (color, metalness, roughness, opacity, glow)
646
  - Setting backgrounds and fog effects
647
- - Post-processing effects (bloom, SSAO, color grading, vignette)
648
- - Camera effects (depth of field, motion blur, chromatic aberration)
649
 
650
  The current scene ID is: {scene_id}
651
 
 
 
 
 
 
 
 
652
  When users ask to modify something relatively (like "half the speed" or "make it twice as big"),
653
  ALWAYS first get the current state using the appropriate get_* function, then calculate the new value,
654
  then apply it.
@@ -892,14 +897,12 @@ Be concise but helpful. After making changes, briefly confirm what was done."""
892
  else:
893
  return {"error": f"Unknown tool: {name}"}
894
 
895
- def chat(self, user_message: str, crosshair_position: Optional[Dict[str, float]] = None) -> tuple[str, Optional[Dict[str, Any]]]:
896
  """
897
  Process a user message and return the response.
898
 
899
  Args:
900
  user_message: The user's message
901
- crosshair_position: Optional dict with x, y, z coordinates where the crosshair
902
- is pointing on the floor. Used for context-aware object placement.
903
 
904
  Returns:
905
  tuple: (response_text, action_data)
@@ -912,27 +915,14 @@ Be concise but helpful. After making changes, briefly confirm what was done."""
912
  "content": user_message
913
  })
914
 
915
- # Build system prompt with crosshair context
916
- system_prompt = self.system_prompt
917
- if crosshair_position:
918
- system_prompt += f"""
919
-
920
- IMPORTANT - Current crosshair position:
921
- The player is looking at position ({crosshair_position['x']}, {crosshair_position['y']}, {crosshair_position['z']}) on the floor.
922
- When the user asks to create/add an object WITHOUT specifying a position (e.g., "add a cube", "create a sphere"),
923
- place it at the crosshair position: x={crosshair_position['x']}, y={crosshair_position['y']}, z={crosshair_position['z']}.
924
- The y coordinate should be adjusted based on object size (e.g., y=0.5 for a cube of scale 1 so it sits on the floor).
925
-
926
- If the user DOES specify a position (e.g., "add a cube at 0, 0, 0"), use their specified position instead."""
927
-
928
  # Track actions for frontend
929
  actions = []
930
 
931
  # Route to appropriate provider
932
  if self.provider == "gemini":
933
- return self._chat_gemini(user_message, system_prompt, actions)
934
  else:
935
- return self._chat_openai(user_message, system_prompt, actions)
936
 
937
  def _chat_openai(self, user_message: str, system_prompt: str, actions: List) -> tuple[str, Optional[Dict[str, Any]]]:
938
  """Handle chat with OpenAI GPT"""
 
99
  "type": "function",
100
  "function": {
101
  "name": "add_object",
102
+ "description": "Add a 3D object to the scene. If no position (x, y, z) is specified, the object will spawn in front of the player's camera (Minecraft-style placement).",
103
  "parameters": {
104
  "type": "object",
105
  "properties": {
106
  "scene_id": {"type": "string", "description": "ID of the scene"},
107
  "object_type": {"type": "string", "enum": ["cube", "sphere", "cylinder", "plane", "cone", "torus"], "description": "Type of object"},
108
  "name": {"type": "string", "description": "Name for the object"},
109
+ "x": {"type": "number", "description": "X position (optional - omit to place in front of player)"},
110
+ "y": {"type": "number", "description": "Y position (optional - auto-calculated to sit on ground)"},
111
+ "z": {"type": "number", "description": "Z position (optional - omit to place in front of player)"},
112
  "scale_x": {"type": "number", "description": "X scale (default: 1)"},
113
  "scale_y": {"type": "number", "description": "Y scale (default: 1)"},
114
  "scale_z": {"type": "number", "description": "Z scale (default: 1)"},
 
644
  - Managing lights (ambient, directional, point, spot)
645
  - Updating materials (color, metalness, roughness, opacity, glow)
646
  - Setting backgrounds and fog effects
 
 
647
 
648
  The current scene ID is: {scene_id}
649
 
650
+ IMPORTANT - Object Placement:
651
+ When users ask to create/add an object WITHOUT specifying a position, DO NOT provide x, y, z coordinates.
652
+ Simply omit the position parameters entirely and the object will automatically spawn in front of the player
653
+ (Minecraft-style placement based on camera direction).
654
+
655
+ Only provide x, y, z when the user explicitly specifies a location (e.g., "add a cube at 5, 0, 3").
656
+
657
  When users ask to modify something relatively (like "half the speed" or "make it twice as big"),
658
  ALWAYS first get the current state using the appropriate get_* function, then calculate the new value,
659
  then apply it.
 
897
  else:
898
  return {"error": f"Unknown tool: {name}"}
899
 
900
+ def chat(self, user_message: str) -> tuple[str, Optional[Dict[str, Any]]]:
901
  """
902
  Process a user message and return the response.
903
 
904
  Args:
905
  user_message: The user's message
 
 
906
 
907
  Returns:
908
  tuple: (response_text, action_data)
 
915
  "content": user_message
916
  })
917
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  # Track actions for frontend
919
  actions = []
920
 
921
  # Route to appropriate provider
922
  if self.provider == "gemini":
923
+ return self._chat_gemini(user_message, self.system_prompt, actions)
924
  else:
925
+ return self._chat_openai(user_message, self.system_prompt, actions)
926
 
927
  def _chat_openai(self, user_message: str, system_prompt: str, actions: List) -> tuple[str, Optional[Dict[str, Any]]]:
928
  """Handle chat with OpenAI GPT"""
frontend/game_viewer.html CHANGED
@@ -232,13 +232,11 @@
232
  }
233
  sceneData = await response.json();
234
  }
235
- console.log('Scene data loaded:', sceneData);
236
 
237
  // Apply world size from scene data
238
  if (sceneData.world_width) {
239
  WORLD_SIZE = sceneData.world_width;
240
  WORLD_HALF = WORLD_SIZE / 2;
241
- console.log('World size set to:', WORLD_SIZE);
242
  }
243
 
244
  // Apply player configuration from scene data
@@ -310,13 +308,9 @@
310
  outlinePass.hiddenEdgeColor.set('#ff4400'); // Darker orange for hidden edges
311
  composer.addPass(outlinePass);
312
 
313
- console.log('OutlinePass configured - orange outline ready');
314
-
315
  const outputPass = new OutputPass();
316
  composer.addPass(outputPass);
317
 
318
- console.log('PostProcessing composer initialized with OutlinePass');
319
-
320
  // Add lights from scene data
321
  // Best practices: Combine ambient (low intensity) + directional (sun) + point lights (accents)
322
  sceneData.lights.forEach(lightData => {
@@ -362,7 +356,6 @@
362
  if (light) {
363
  light.name = lightData.name || lightData.type;
364
  scene.add(light);
365
- console.log('Added light:', lightData.type, lightData.name, 'intensity:', lightData.intensity);
366
  }
367
  });
368
 
@@ -442,7 +435,12 @@
442
  renderer.domElement.addEventListener('mousedown', (event) => {
443
  if (controlMode === 'fps' && event.button === 0) {
444
  isMouseLocked = true;
445
- renderer.domElement.requestPointerLock();
 
 
 
 
 
446
  }
447
  });
448
 
@@ -474,15 +472,13 @@
474
  });
475
 
476
  document.addEventListener('pointerlockerror', () => {
477
- console.error('Pointer lock failed');
478
  isMouseLocked = false;
479
  });
480
 
481
- console.log('FPS controls initialized - click in viewer to enable mouse-look');
482
  }
483
 
484
  function setupPhysics() {
485
- console.log('Setting up physics world...');
486
 
487
  // Create physics world
488
  physicsWorld = new CANNON.World();
@@ -675,7 +671,6 @@
675
  }
676
  });
677
 
678
- console.log('Physics world created with ground, walls, and player');
679
  }
680
 
681
  function toggleControlMode() {
@@ -698,8 +693,6 @@
698
 
699
  // Show crosshair
700
  if (crosshair) crosshair.style.display = 'block';
701
-
702
- console.log('Switched to FPS controls - click in viewer to enable mouse-look, WASD to move');
703
  } else {
704
  // Switch to Orbit
705
  if (document.pointerLockElement) {
@@ -715,8 +708,6 @@
715
  if (outlinePass) outlinePass.selectedObjects = [];
716
  selectedObject = null;
717
  selectedObjectId = null;
718
-
719
- console.log('Switched to Orbit controls');
720
  }
721
  }
722
 
@@ -745,7 +736,6 @@
745
  }
746
 
747
  function renderGameObjects() {
748
- console.log('Rendering', sceneData.objects.length, 'game objects...');
749
 
750
  sceneData.objects.forEach(obj => {
751
  // Validate object is within bounds
@@ -832,7 +822,6 @@
832
  child.material = newMaterial;
833
  }
834
  });
835
- console.log(`Applied unlit material to model: ${obj.name}`);
836
  }
837
 
838
  model.userData = {
@@ -849,9 +838,6 @@
849
  // Track animated models
850
  if (obj.metadata?.animate) {
851
  animatedModels.push(model);
852
- console.log(`Loaded animated model: ${obj.name} from ${modelUrl}`);
853
- } else {
854
- console.log(`Loaded model: ${obj.name} from ${modelUrl}`);
855
  }
856
  },
857
  undefined,
@@ -918,11 +904,7 @@
918
  physicsWorld.addBody(physicsBody);
919
  objectBodies.set(obj.id, physicsBody);
920
 
921
- console.log('Added object:', obj.name || obj.id, obj.type, 'with physics collider');
922
  });
923
-
924
- // Camera position is managed by physics system in FPS mode
925
- console.log('Scene objects loaded. Camera controlled by player physics body.');
926
  }
927
 
928
  function onWindowResize() {
@@ -960,10 +942,6 @@
960
  selectedObjectId = newSelected.userData.id;
961
  outlinePass.selectedObjects = [selectedObject];
962
 
963
- console.log('🎯 Object selected:', selectedObjectId, newSelected.userData.type,
964
- `distance: ${intersects[0].distance.toFixed(2)}m`,
965
- `outline array length: ${outlinePass.selectedObjects.length}`);
966
-
967
  // Send selection to parent (for chat commands)
968
  if (window.parent) {
969
  window.parent.postMessage({
@@ -979,7 +957,6 @@
979
  } else {
980
  // No object in view
981
  if (selectedObject) {
982
- console.log('🎯 Object deselected');
983
  outlinePass.selectedObjects = [];
984
  selectedObject = null;
985
  selectedObjectId = null;
@@ -1187,15 +1164,9 @@
1187
  * Toggle grid helper visibility
1188
  */
1189
  function toggleGrid(enabled) {
1190
- if (gridHelper) {
1191
- gridHelper.visible = enabled;
1192
- console.log('Grid helper:', enabled ? 'enabled' : 'disabled');
1193
- }
1194
  }
1195
 
1196
- /**
1197
- * Toggle wireframe mode for all objects
1198
- */
1199
  function toggleWireframe(enabled) {
1200
  wireframeEnabled = enabled;
1201
  scene.traverse((object) => {
@@ -1203,17 +1174,10 @@
1203
  object.material.wireframe = enabled;
1204
  }
1205
  });
1206
- console.log('Wireframe mode:', enabled ? 'enabled' : 'disabled');
1207
  }
1208
 
1209
- /**
1210
- * Toggle stats (FPS counter) display
1211
- */
1212
  function toggleStats(enabled) {
1213
- if (stats) {
1214
- stats.dom.style.display = enabled ? 'block' : 'none';
1215
- console.log('Stats:', enabled ? 'enabled' : 'disabled');
1216
- }
1217
  }
1218
 
1219
  /**
@@ -1240,8 +1204,6 @@
1240
  sceneName: sceneData?.name || 'scene'
1241
  }
1242
  }, '*');
1243
-
1244
- console.log('Screenshot captured and sent to parent window');
1245
  }
1246
 
1247
  /**
@@ -1308,8 +1270,6 @@
1308
  action: 'objectInspect',
1309
  data: objectInfo
1310
  }, '*');
1311
-
1312
- console.log('Object selected:', objectInfo);
1313
  }
1314
  }
1315
 
@@ -1324,7 +1284,6 @@
1324
  // if (event.origin !== window.location.origin) return;
1325
 
1326
  const { action, data } = event.data;
1327
- console.log('📨 Received postMessage:', action, data);
1328
 
1329
  switch (action) {
1330
  case 'addObject':
@@ -1375,42 +1334,26 @@
1375
  // Player configuration actions
1376
  case 'setPlayerSpeed':
1377
  WALK_SPEED = data.walk_speed || 5.0;
1378
- console.log('Player speed set to:', WALK_SPEED);
1379
  break;
1380
  case 'setJumpForce':
1381
  JUMP_VELOCITY = data.jump_force || 5.0;
1382
- console.log('Jump force set to:', JUMP_VELOCITY);
1383
  break;
1384
  case 'setGravity':
1385
- if (world) {
1386
- world.gravity.set(0, data.gravity || -9.82, 0);
1387
- console.log('Gravity set to:', data.gravity);
1388
- }
1389
  break;
1390
  case 'setCameraFov':
1391
  if (camera) {
1392
  camera.fov = data.fov || 75;
1393
  camera.updateProjectionMatrix();
1394
- console.log('Camera FOV set to:', camera.fov);
1395
  }
1396
  break;
1397
  case 'setMouseSensitivity':
1398
  MOUSE_SENSITIVITY = data.sensitivity || 0.002;
1399
- if (data.invert_y !== undefined) {
1400
- // Store invert_y preference (used in mouse movement handler)
1401
- window.INVERT_Y = data.invert_y;
1402
- }
1403
- console.log('Mouse sensitivity set to:', MOUSE_SENSITIVITY);
1404
  break;
1405
  case 'setPlayerDimensions':
1406
- // Update player dimensions
1407
  if (data.height) PLAYER_HEIGHT = data.height;
1408
  if (data.radius) PLAYER_RADIUS = data.radius;
1409
- if (data.eye_height) {
1410
- // Update camera Y offset relative to player body
1411
- // This will take effect on next frame
1412
- }
1413
- console.log('Player dimensions updated:', { height: PLAYER_HEIGHT, radius: PLAYER_RADIUS });
1414
  break;
1415
  // Skybox actions
1416
  case 'addSkybox':
@@ -1449,6 +1392,60 @@
1449
  }
1450
  });
1451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1452
  /**
1453
  * Dynamically add an object to the scene
1454
  */
@@ -1458,9 +1455,31 @@
1458
  return;
1459
  }
1460
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1461
  // Validate object is within bounds
1462
  if (Math.abs(objData.position.x) > WORLD_HALF || Math.abs(objData.position.z) > WORLD_HALF) {
1463
- console.error(`Cannot add object at (${objData.position.x}, ${objData.position.z}) - outside 10x10 world bounds`);
1464
  return;
1465
  }
1466
 
@@ -1542,7 +1561,6 @@
1542
  // Add highlight effect
1543
  animateObjectHighlight(mesh);
1544
 
1545
- console.log('Added object dynamically:', objData.name || objData.id);
1546
  }
1547
 
1548
  /**
@@ -1574,7 +1592,6 @@
1574
  // Remove from scene data
1575
  sceneData.objects = sceneData.objects.filter(obj => obj.id !== object_id);
1576
 
1577
- console.log('Removed object dynamically:', object_id);
1578
  }
1579
 
1580
  /**
@@ -1619,7 +1636,6 @@
1619
  // Update scene data
1620
  sceneData.lights = lights;
1621
 
1622
- console.log('Updated lighting dynamically');
1623
  }
1624
 
1625
  /**
@@ -1703,7 +1719,6 @@
1703
  // ==================== Rendering & Lighting Handler Functions ====================
1704
 
1705
  function addLightToScene(lightData) {
1706
- console.log('💡 addLightToScene called with:', lightData);
1707
  let light;
1708
 
1709
  if (lightData.light_type === 'ambient') {
@@ -1760,18 +1775,14 @@
1760
  if (light) {
1761
  light.name = lightData.name;
1762
  scene.add(light);
1763
- console.log('💡 Light added to scene:', lightData.name, 'type:', lightData.light_type, 'intensity:', lightData.intensity);
1764
  } else {
1765
- console.error('Failed to create light:', lightData);
1766
  }
1767
  }
1768
 
1769
  function removeLightFromScene(lightName) {
1770
  const light = scene.getObjectByName(lightName);
1771
- if (light) {
1772
- scene.remove(light);
1773
- console.log('Removed light:', lightName);
1774
- }
1775
  }
1776
 
1777
  function updateSceneLight(lightName, updates) {
@@ -1784,48 +1795,23 @@
1784
  light.position.set(updates.position.x, updates.position.y, updates.position.z);
1785
  }
1786
  if (updates.cast_shadow !== undefined) light.castShadow = updates.cast_shadow;
1787
-
1788
- console.log('Updated light:', lightName);
1789
  }
1790
 
1791
  function updateObjectMaterial(objectId, materialData) {
1792
- console.log('updateObjectMaterial called with:', objectId, materialData);
1793
  const obj = scene.children.find(child => child.userData.id === objectId);
1794
- if (!obj || !obj.material) {
1795
- console.error('Object not found or has no material:', objectId);
1796
- return;
1797
- }
1798
 
1799
- console.log('Found object:', obj.userData.name, 'Current color:', obj.material.color.getHexString());
1800
-
1801
- if (materialData.color) {
1802
- obj.material.color.set(materialData.color);
1803
- console.log('Set color to:', materialData.color);
1804
- }
1805
- if (materialData.metalness !== undefined) {
1806
- obj.material.metalness = materialData.metalness;
1807
- console.log('Set metalness to:', materialData.metalness);
1808
- }
1809
- if (materialData.roughness !== undefined) {
1810
- obj.material.roughness = materialData.roughness;
1811
- console.log('Set roughness to:', materialData.roughness);
1812
- }
1813
  if (materialData.opacity !== undefined) {
1814
  obj.material.opacity = materialData.opacity;
1815
  obj.material.transparent = materialData.opacity < 1.0;
1816
- console.log('Set opacity to:', materialData.opacity);
1817
- }
1818
- if (materialData.emissive) {
1819
- obj.material.emissive = new THREE.Color(materialData.emissive);
1820
- console.log('Set emissive to:', materialData.emissive);
1821
- }
1822
- if (materialData.emissive_intensity !== undefined) {
1823
- obj.material.emissiveIntensity = materialData.emissive_intensity;
1824
- console.log('Set emissive intensity to:', materialData.emissive_intensity);
1825
  }
 
 
1826
 
1827
  obj.material.needsUpdate = true;
1828
- console.log('Material updated successfully! New color:', obj.material.color.getHexString());
1829
  }
1830
 
1831
  function setSceneBackground(bgData) {
@@ -1847,25 +1833,20 @@
1847
  scene.background = new THREE.Color(bgData.background_color);
1848
  }
1849
 
1850
- console.log('Updated background');
1851
  }
1852
 
1853
  function setSceneFog(fogData) {
1854
  if (!fogData.enabled) {
1855
  scene.fog = null;
1856
- console.log('Fog disabled');
1857
  return;
1858
  }
1859
 
1860
  const color = new THREE.Color(fogData.color);
1861
-
1862
  if (fogData.type === 'exponential') {
1863
  scene.fog = new THREE.FogExp2(color, fogData.density);
1864
  } else {
1865
  scene.fog = new THREE.Fog(color, fogData.near, fogData.far);
1866
  }
1867
-
1868
- console.log('Fog enabled:', fogData.type);
1869
  }
1870
 
1871
  // ==================== Skybox Handlers ====================
@@ -1895,9 +1876,6 @@
1895
 
1896
  // Update scene background to use sky
1897
  scene.background = null; // Sky will render as background
1898
-
1899
- console.log('🌤️ Skybox added:', skyboxData.preset || 'custom',
1900
- `elevation=${skyboxData.sun_elevation}°`);
1901
  }
1902
 
1903
  function handleRemoveSkybox() {
@@ -1908,7 +1886,6 @@
1908
  // Revert to solid background
1909
  const bgColor = sceneData?.environment?.background_color || '#87CEEB';
1910
  scene.background = new THREE.Color(bgColor);
1911
- console.log('🌤️ Skybox removed');
1912
  }
1913
 
1914
  // ==================== Particle System Handlers ====================
@@ -1987,7 +1964,6 @@
1987
  endColor: new THREE.Color(config.color_end || config.color_start || '#ffffff')
1988
  });
1989
 
1990
- console.log('✨ Particles added:', id, config.preset);
1991
  }
1992
 
1993
  function handleRemoveParticles(particleId) {
@@ -1997,7 +1973,6 @@
1997
  system.geometry.dispose();
1998
  system.points.material.dispose();
1999
  particleSystems.delete(particleId);
2000
- console.log('✨ Particles removed:', particleId);
2001
  }
2002
  }
2003
 
@@ -2109,7 +2084,6 @@
2109
  uiContainer.appendChild(element);
2110
  uiElements.set(id, element);
2111
 
2112
- console.log('📝 UI text rendered:', id);
2113
  }
2114
 
2115
  function handleRenderBar(barData) {
@@ -2190,14 +2164,12 @@
2190
  uiContainer.appendChild(container);
2191
  uiElements.set(id, container);
2192
 
2193
- console.log('📊 UI bar rendered:', id);
2194
  }
2195
 
2196
  function handleRemoveUIElement(elementId) {
2197
  if (uiElements.has(elementId)) {
2198
  uiContainer.removeChild(uiElements.get(elementId));
2199
  uiElements.delete(elementId);
2200
- console.log('🗑️ UI element removed:', elementId);
2201
  }
2202
  }
2203
 
@@ -2236,7 +2208,6 @@
2236
  obj.userData.toonEnabled = true;
2237
  obj.userData.toonSettings = data;
2238
 
2239
- console.log('🎨 Toon material applied to:', data.object_id);
2240
  } else {
2241
  // Revert to standard material
2242
  const existingColor = obj.material?.color?.getHex() || 0xffffff;
@@ -2251,7 +2222,6 @@
2251
  obj.material = standardMaterial;
2252
  obj.userData.toonEnabled = false;
2253
 
2254
- console.log('🎨 Reverted to standard material:', data.object_id);
2255
  }
2256
  }
2257
 
@@ -2328,15 +2298,9 @@
2328
  // Add to scene data for tracking
2329
  if (!sceneData.objects) sceneData.objects = [];
2330
  sceneData.objects.push(brickData);
2331
-
2332
- console.log('🧱 Brick added:', brickData.brick_type, 'at', position);
2333
- },
2334
- (xhr) => {
2335
- console.log(`Loading brick: ${(xhr.loaded / xhr.total * 100).toFixed(0)}%`);
2336
  },
2337
- (error) => {
2338
- console.error('Error loading brick:', error);
2339
- }
2340
  );
2341
  }
2342
 
 
232
  }
233
  sceneData = await response.json();
234
  }
 
235
 
236
  // Apply world size from scene data
237
  if (sceneData.world_width) {
238
  WORLD_SIZE = sceneData.world_width;
239
  WORLD_HALF = WORLD_SIZE / 2;
 
240
  }
241
 
242
  // Apply player configuration from scene data
 
308
  outlinePass.hiddenEdgeColor.set('#ff4400'); // Darker orange for hidden edges
309
  composer.addPass(outlinePass);
310
 
 
 
311
  const outputPass = new OutputPass();
312
  composer.addPass(outputPass);
313
 
 
 
314
  // Add lights from scene data
315
  // Best practices: Combine ambient (low intensity) + directional (sun) + point lights (accents)
316
  sceneData.lights.forEach(lightData => {
 
356
  if (light) {
357
  light.name = lightData.name || lightData.type;
358
  scene.add(light);
 
359
  }
360
  });
361
 
 
435
  renderer.domElement.addEventListener('mousedown', (event) => {
436
  if (controlMode === 'fps' && event.button === 0) {
437
  isMouseLocked = true;
438
+ // Wrap in try-catch to handle SecurityError when user exits lock quickly
439
+ try {
440
+ renderer.domElement.requestPointerLock();
441
+ } catch (e) {
442
+ isMouseLocked = false;
443
+ }
444
  }
445
  });
446
 
 
472
  });
473
 
474
  document.addEventListener('pointerlockerror', () => {
475
+ // Silently handle pointer lock errors - common when user clicks away quickly
476
  isMouseLocked = false;
477
  });
478
 
 
479
  }
480
 
481
  function setupPhysics() {
 
482
 
483
  // Create physics world
484
  physicsWorld = new CANNON.World();
 
671
  }
672
  });
673
 
 
674
  }
675
 
676
  function toggleControlMode() {
 
693
 
694
  // Show crosshair
695
  if (crosshair) crosshair.style.display = 'block';
 
 
696
  } else {
697
  // Switch to Orbit
698
  if (document.pointerLockElement) {
 
708
  if (outlinePass) outlinePass.selectedObjects = [];
709
  selectedObject = null;
710
  selectedObjectId = null;
 
 
711
  }
712
  }
713
 
 
736
  }
737
 
738
  function renderGameObjects() {
 
739
 
740
  sceneData.objects.forEach(obj => {
741
  // Validate object is within bounds
 
822
  child.material = newMaterial;
823
  }
824
  });
 
825
  }
826
 
827
  model.userData = {
 
838
  // Track animated models
839
  if (obj.metadata?.animate) {
840
  animatedModels.push(model);
 
 
 
841
  }
842
  },
843
  undefined,
 
904
  physicsWorld.addBody(physicsBody);
905
  objectBodies.set(obj.id, physicsBody);
906
 
 
907
  });
 
 
 
908
  }
909
 
910
  function onWindowResize() {
 
942
  selectedObjectId = newSelected.userData.id;
943
  outlinePass.selectedObjects = [selectedObject];
944
 
 
 
 
 
945
  // Send selection to parent (for chat commands)
946
  if (window.parent) {
947
  window.parent.postMessage({
 
957
  } else {
958
  // No object in view
959
  if (selectedObject) {
 
960
  outlinePass.selectedObjects = [];
961
  selectedObject = null;
962
  selectedObjectId = null;
 
1164
  * Toggle grid helper visibility
1165
  */
1166
  function toggleGrid(enabled) {
1167
+ if (gridHelper) gridHelper.visible = enabled;
 
 
 
1168
  }
1169
 
 
 
 
1170
  function toggleWireframe(enabled) {
1171
  wireframeEnabled = enabled;
1172
  scene.traverse((object) => {
 
1174
  object.material.wireframe = enabled;
1175
  }
1176
  });
 
1177
  }
1178
 
 
 
 
1179
  function toggleStats(enabled) {
1180
+ if (stats) stats.dom.style.display = enabled ? 'block' : 'none';
 
 
 
1181
  }
1182
 
1183
  /**
 
1204
  sceneName: sceneData?.name || 'scene'
1205
  }
1206
  }, '*');
 
 
1207
  }
1208
 
1209
  /**
 
1270
  action: 'objectInspect',
1271
  data: objectInfo
1272
  }, '*');
 
 
1273
  }
1274
  }
1275
 
 
1284
  // if (event.origin !== window.location.origin) return;
1285
 
1286
  const { action, data } = event.data;
 
1287
 
1288
  switch (action) {
1289
  case 'addObject':
 
1334
  // Player configuration actions
1335
  case 'setPlayerSpeed':
1336
  WALK_SPEED = data.walk_speed || 5.0;
 
1337
  break;
1338
  case 'setJumpForce':
1339
  JUMP_VELOCITY = data.jump_force || 5.0;
 
1340
  break;
1341
  case 'setGravity':
1342
+ if (world) world.gravity.set(0, data.gravity || -9.82, 0);
 
 
 
1343
  break;
1344
  case 'setCameraFov':
1345
  if (camera) {
1346
  camera.fov = data.fov || 75;
1347
  camera.updateProjectionMatrix();
 
1348
  }
1349
  break;
1350
  case 'setMouseSensitivity':
1351
  MOUSE_SENSITIVITY = data.sensitivity || 0.002;
1352
+ if (data.invert_y !== undefined) window.INVERT_Y = data.invert_y;
 
 
 
 
1353
  break;
1354
  case 'setPlayerDimensions':
 
1355
  if (data.height) PLAYER_HEIGHT = data.height;
1356
  if (data.radius) PLAYER_RADIUS = data.radius;
 
 
 
 
 
1357
  break;
1358
  // Skybox actions
1359
  case 'addSkybox':
 
1392
  }
1393
  });
1394
 
1395
+ /**
1396
+ * Calculate spawn position in front of camera (Minecraft-style placement)
1397
+ * Distance is calculated based on object size so larger objects spawn further away
1398
+ *
1399
+ * @param {Object} scale - Object scale {x, y, z}
1400
+ * @param {boolean} snapToGround - Whether to snap object to ground plane
1401
+ * @returns {THREE.Vector3} - The calculated spawn position
1402
+ */
1403
+ function getForwardSpawnPosition(scale = {x: 1, y: 1, z: 1}, snapToGround = true) {
1404
+ const dir = new THREE.Vector3();
1405
+ camera.getWorldDirection(dir);
1406
+
1407
+ // Calculate object's bounding size (largest dimension)
1408
+ const objectSize = Math.max(scale.x || 1, scale.y || 1, scale.z || 1);
1409
+
1410
+ // Base distance + object size + small buffer
1411
+ // This ensures the object spawns in front of player without clipping
1412
+ const baseDistance = 2.0; // Minimum distance from player
1413
+ const sizeMultiplier = 1.2; // Extra padding based on size
1414
+ const distance = baseDistance + (objectSize * sizeMultiplier);
1415
+
1416
+ // Start from camera position and move forward
1417
+ const spawnPos = new THREE.Vector3()
1418
+ .copy(camera.position)
1419
+ .add(dir.clone().multiplyScalar(distance));
1420
+
1421
+ // Snap to ground if requested
1422
+ if (snapToGround) {
1423
+ // Cast ray downward to find ground
1424
+ const downRay = new THREE.Raycaster(
1425
+ new THREE.Vector3(spawnPos.x, spawnPos.y + 20, spawnPos.z), // Start above
1426
+ new THREE.Vector3(0, -1, 0) // Point down
1427
+ );
1428
+
1429
+ // Find ground mesh (look for the floor plane)
1430
+ const groundObjects = scene.children.filter(obj =>
1431
+ obj.userData.isGround || obj.name === 'ground' || obj.name === 'floor'
1432
+ );
1433
+
1434
+ if (groundObjects.length > 0) {
1435
+ const hits = downRay.intersectObjects(groundObjects);
1436
+ if (hits.length > 0) {
1437
+ spawnPos.y = hits[0].point.y;
1438
+ } else {
1439
+ spawnPos.y = 0;
1440
+ }
1441
+ } else {
1442
+ spawnPos.y = 0;
1443
+ }
1444
+ }
1445
+
1446
+ return spawnPos;
1447
+ }
1448
+
1449
  /**
1450
  * Dynamically add an object to the scene
1451
  */
 
1455
  return;
1456
  }
1457
 
1458
+ // If position is at origin (0,0,0) or use_camera_position flag is set,
1459
+ // spawn in front of the camera (Minecraft-style)
1460
+ const isDefaultPosition =
1461
+ objData.position.x === 0 &&
1462
+ objData.position.y === 0 &&
1463
+ objData.position.z === 0;
1464
+
1465
+ if (objData.use_camera_position || isDefaultPosition) {
1466
+ const scale = objData.scale || {x: 1, y: 1, z: 1};
1467
+ const spawnPos = getForwardSpawnPosition(scale, true);
1468
+
1469
+ // Offset Y by half the object height so it sits on ground
1470
+ const halfHeight = (scale.y || 1) / 2;
1471
+ spawnPos.y += halfHeight;
1472
+
1473
+ objData.position = {
1474
+ x: spawnPos.x,
1475
+ y: spawnPos.y,
1476
+ z: spawnPos.z
1477
+ };
1478
+ }
1479
+
1480
  // Validate object is within bounds
1481
  if (Math.abs(objData.position.x) > WORLD_HALF || Math.abs(objData.position.z) > WORLD_HALF) {
1482
+ console.error(`Cannot add object at (${objData.position.x}, ${objData.position.z}) - outside world bounds`);
1483
  return;
1484
  }
1485
 
 
1561
  // Add highlight effect
1562
  animateObjectHighlight(mesh);
1563
 
 
1564
  }
1565
 
1566
  /**
 
1592
  // Remove from scene data
1593
  sceneData.objects = sceneData.objects.filter(obj => obj.id !== object_id);
1594
 
 
1595
  }
1596
 
1597
  /**
 
1636
  // Update scene data
1637
  sceneData.lights = lights;
1638
 
 
1639
  }
1640
 
1641
  /**
 
1719
  // ==================== Rendering & Lighting Handler Functions ====================
1720
 
1721
  function addLightToScene(lightData) {
 
1722
  let light;
1723
 
1724
  if (lightData.light_type === 'ambient') {
 
1775
  if (light) {
1776
  light.name = lightData.name;
1777
  scene.add(light);
 
1778
  } else {
1779
+ console.error('Failed to create light:', lightData);
1780
  }
1781
  }
1782
 
1783
  function removeLightFromScene(lightName) {
1784
  const light = scene.getObjectByName(lightName);
1785
+ if (light) scene.remove(light);
 
 
 
1786
  }
1787
 
1788
  function updateSceneLight(lightName, updates) {
 
1795
  light.position.set(updates.position.x, updates.position.y, updates.position.z);
1796
  }
1797
  if (updates.cast_shadow !== undefined) light.castShadow = updates.cast_shadow;
 
 
1798
  }
1799
 
1800
  function updateObjectMaterial(objectId, materialData) {
 
1801
  const obj = scene.children.find(child => child.userData.id === objectId);
1802
+ if (!obj || !obj.material) return;
 
 
 
1803
 
1804
+ if (materialData.color) obj.material.color.set(materialData.color);
1805
+ if (materialData.metalness !== undefined) obj.material.metalness = materialData.metalness;
1806
+ if (materialData.roughness !== undefined) obj.material.roughness = materialData.roughness;
 
 
 
 
 
 
 
 
 
 
 
1807
  if (materialData.opacity !== undefined) {
1808
  obj.material.opacity = materialData.opacity;
1809
  obj.material.transparent = materialData.opacity < 1.0;
 
 
 
 
 
 
 
 
 
1810
  }
1811
+ if (materialData.emissive) obj.material.emissive = new THREE.Color(materialData.emissive);
1812
+ if (materialData.emissive_intensity !== undefined) obj.material.emissiveIntensity = materialData.emissive_intensity;
1813
 
1814
  obj.material.needsUpdate = true;
 
1815
  }
1816
 
1817
  function setSceneBackground(bgData) {
 
1833
  scene.background = new THREE.Color(bgData.background_color);
1834
  }
1835
 
 
1836
  }
1837
 
1838
  function setSceneFog(fogData) {
1839
  if (!fogData.enabled) {
1840
  scene.fog = null;
 
1841
  return;
1842
  }
1843
 
1844
  const color = new THREE.Color(fogData.color);
 
1845
  if (fogData.type === 'exponential') {
1846
  scene.fog = new THREE.FogExp2(color, fogData.density);
1847
  } else {
1848
  scene.fog = new THREE.Fog(color, fogData.near, fogData.far);
1849
  }
 
 
1850
  }
1851
 
1852
  // ==================== Skybox Handlers ====================
 
1876
 
1877
  // Update scene background to use sky
1878
  scene.background = null; // Sky will render as background
 
 
 
1879
  }
1880
 
1881
  function handleRemoveSkybox() {
 
1886
  // Revert to solid background
1887
  const bgColor = sceneData?.environment?.background_color || '#87CEEB';
1888
  scene.background = new THREE.Color(bgColor);
 
1889
  }
1890
 
1891
  // ==================== Particle System Handlers ====================
 
1964
  endColor: new THREE.Color(config.color_end || config.color_start || '#ffffff')
1965
  });
1966
 
 
1967
  }
1968
 
1969
  function handleRemoveParticles(particleId) {
 
1973
  system.geometry.dispose();
1974
  system.points.material.dispose();
1975
  particleSystems.delete(particleId);
 
1976
  }
1977
  }
1978
 
 
2084
  uiContainer.appendChild(element);
2085
  uiElements.set(id, element);
2086
 
 
2087
  }
2088
 
2089
  function handleRenderBar(barData) {
 
2164
  uiContainer.appendChild(container);
2165
  uiElements.set(id, container);
2166
 
 
2167
  }
2168
 
2169
  function handleRemoveUIElement(elementId) {
2170
  if (uiElements.has(elementId)) {
2171
  uiContainer.removeChild(uiElements.get(elementId));
2172
  uiElements.delete(elementId);
 
2173
  }
2174
  }
2175
 
 
2208
  obj.userData.toonEnabled = true;
2209
  obj.userData.toonSettings = data;
2210
 
 
2211
  } else {
2212
  // Revert to standard material
2213
  const existingColor = obj.material?.color?.getHex() || 0xffffff;
 
2222
  obj.material = standardMaterial;
2223
  obj.userData.toonEnabled = false;
2224
 
 
2225
  }
2226
  }
2227
 
 
2298
  // Add to scene data for tracking
2299
  if (!sceneData.objects) sceneData.objects = [];
2300
  sceneData.objects.push(brickData);
 
 
 
 
 
2301
  },
2302
+ undefined,
2303
+ (error) => console.error('Error loading brick:', error)
 
2304
  );
2305
  }
2306