Commit
·
47dd8dd
1
Parent(s):
7aa89e4
Improved UX for object placement
Browse files- app.py +8 -18
- chat_client.py +14 -24
- 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,
|
| 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
|
|
|
|
| 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 |
-
|
| 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 |
-
#
|
| 557 |
-
#
|
| 558 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
}
|
| 447 |
});
|
| 448 |
|
|
@@ -474,15 +472,13 @@
|
|
| 474 |
});
|
| 475 |
|
| 476 |
document.addEventListener('pointerlockerror', () => {
|
| 477 |
-
|
| 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
|
| 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('
|
| 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 |
-
|
| 1800 |
-
|
| 1801 |
-
if (materialData.
|
| 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 |
-
|
| 2338 |
-
|
| 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 |
|