3d-game-editor / index.html
WhoIsAbishag's picture
Add 2 files
b906422 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Game Editor</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/DragControls.min.js"></script>
<style>
:root {
--primary: #4a6bff;
--secondary: #1a1e2d;
--light-bg: #2d334b;
--dark-bg: #1a1e2d;
--text: #e9ecff;
--highlight: #ff7e5e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--dark-bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
touch-action: none;
}
header {
background-color: var(--secondary);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo i {
color: var(--primary);
font-size: 24px;
}
.logo h1 {
font-size: 20px;
font-weight: 600;
}
.controls {
display: flex;
gap: 15px;
}
.btn {
background-color: var(--light-bg);
color: var(--text);
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.btn:hover {
background-color: var(--primary);
}
.btn i {
font-size: 14px;
}
.btn.active {
background-color: var(--primary);
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 300px;
background-color: var(--secondary);
border-right: 1px solid rgba(255, 255, 255, 0.1);
padding: 20px;
overflow-y: auto;
}
.section-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 15px;
color: var(--primary);
font-weight: 600;
}
.object-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.object-item {
background-color: var(--light-bg);
border-radius: 4px;
padding: 10px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.object-item:hover {
transform: translateY(-3px);
}
.object-item i {
font-size: 20px;
margin-bottom: 5px;
color: var(--highlight);
display: block;
}
.property-panel {
margin-top: 30px;
}
.property-group {
margin-bottom: 15px;
}
.property-group label {
display: block;
margin-bottom: 5px;
font-size: 13px;
}
.property-group input, .property-group select {
width: 100%;
padding: 8px;
background-color: var(--light-bg);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: var(--text);
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.viewport {
flex: 1;
background-color: #000;
position: relative;
overflow: hidden;
}
#gameCanvas {
width: 100%;
height: 100%;
display: block;
}
.code-editor {
height: 200px;
background-color: #1e1e1e;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: none;
position: relative;
}
#codeArea {
width: 100%;
height: 100%;
background-color: #1e1e1e;
color: #f8f8f2;
padding: 15px;
border: none;
resize: none;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
}
.code-samples {
position: absolute;
right: 10px;
top: 10px;
display: flex;
gap: 10px;
z-index: 10;
}
.code-sample-btn {
background-color: var(--primary);
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.drag-highlight {
pointer-events: none;
position: absolute;
border: 2px solid var(--highlight);
border-radius: 4px;
z-index: 100;
transition: all 0.1s;
display: none;
}
.tab-bar {
display: flex;
background-color: var(--secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
font-size: 14px;
}
.tab.active {
border-bottom: 2px solid var(--primary);
color: var(--primary);
}
.tab:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.status-bar {
background-color: var(--secondary);
padding: 5px 15px;
font-size: 12px;
display: flex;
justify-content: space-between;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.control-modes {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
gap: 10px;
z-index: 10;
}
.control-btn {
width: 40px;
height: 40px;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
font-size: 16px;
}
.control-info {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: white;
z-index: 10;
pointer-events: none;
}
.coordinate-display {
position: absolute;
top: 40px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: white;
z-index: 10;
pointer-events: none;
}
.object-count {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
color: white;
z-index: 10;
pointer-events: none;
}
/* Tooltip */
.tooltip {
position: relative;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
/* Touch controls */
.touch-controls {
position: fixed;
bottom: 70px;
right: 20px;
display: none;
z-index: 100;
}
.touch-joystick {
width: 80px;
height: 80px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.touch-joystick-inner {
width: 40px;
height: 40px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.sidebar {
width: 250px;
}
}
@media (max-width: 768px) {
.main-container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: 300px;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.touch-controls {
display: flex;
}
}
</style>
</head>
<body>
<header>
<div class="logo">
<i class="fas fa-cube"></i>
<h1>3D Game Editor</h1>
</div>
<div class="controls">
<button class="btn" id="runBtn"><i class="fas fa-play"></i> Run</button>
<button class="btn" id="saveBtn"><i class="fas fa-save"></i> Save</button>
<button class="btn" id="codeBtn"><i class="fas fa-code"></i> Code</button>
</div>
</header>
<div class="main-container">
<div class="sidebar">
<h3 class="section-title">Objects</h3>
<div class="object-list">
<div class="object-item tooltip" data-type="cube">
<i class="fas fa-cube"></i>
<span>Cube</span>
<span class="tooltiptext">Add a 3D cube</span>
</div>
<div class="object-item tooltip" data-type="sphere">
<i class="fas fa-circle"></i>
<span>Sphere</span>
<span class="tooltiptext">Add a 3D sphere</span>
</div>
<div class="object-item tooltip" data-type="cylinder">
<i class="fas fa-database"></i>
<span>Cylinder</span>
<span class="tooltiptext">Add a 3D cylinder</span>
</div>
<div class="object-item tooltip" data-type="plane">
<i class="fas fa-square"></i>
<span>Plane</span>
<span class="tooltiptext">Add a 3D plane</span>
</div>
<div class="object-item tooltip" data-type="light">
<i class="fas fa-lightbulb"></i>
<span>Light</span>
<span class="tooltiptext">Add a light source</span>
</div>
<div class="object-item tooltip" data-type="character">
<i class="fas fa-user-astronaut"></i>
<span>Player</span>
<span class="tooltiptext">Add player character</span>
</div>
</div>
<div class="property-panel">
<h3 class="section-title">Properties</h3>
<div class="property-group">
<label for="objName">Name</label>
<input type="text" id="objName" placeholder="object_name">
</div>
<div class="property-group">
<label for="objPosX">Position X</label>
<input type="number" id="objPosX" value="0" step="0.1">
</div>
<div class="property-group">
<label for="objPosY">Position Y</label>
<input type="number" id="objPosY" value="0" step="0.1">
</div>
<div class="property-group">
<label for="objPosZ">Position Z</label>
<input type="number" id="objPosZ" value="0" step="0.1">
</div>
<div class="property-group">
<label for="objRotX">Rotation X</label>
<input type="number" id="objRotX" value="0" step="1">
</div>
<div class="property-group">
<label for="objRotY">Rotation Y</label>
<input type="number" id="objRotY" value="0" step="1">
</div>
<div class="property-group">
<label for="objRotZ">Rotation Z</label>
<input type="number" id="objRotZ" value="0" step="1">
</div>
<div class="property-group">
<label for="objScale">Scale</label>
<input type="number" id="objScale" value="1" step="0.1">
</div>
<div class="property-group">
<label for="objColor">Color</label>
<input type="color" id="objColor" value="#4a6bff">
</div>
<button class="btn" id="applyProps" style="margin-top: 15px; width: 100%;">
<i class="fas fa-check"></i> Apply
</button>
<button class="btn" id="deleteObj" style="margin-top: 10px; width: 100%; background-color: #ff4a4a;">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
<div class="editor-container">
<div class="tab-bar">
<div class="tab active">3D View</div>
<div class="tab">Game View</div>
</div>
<div class="viewport">
<canvas id="gameCanvas"></canvas>
<div class="drag-highlight" id="dragHighlight"></div>
<div class="control-info" id="controlInfo">
Camera Mode: Orbit
</div>
<div class="coordinate-display" id="coordDisplay">
Selected: None | Camera: (0, 5, 10)
</div>
<div class="object-count">
Objects: 0
</div>
<div class="control-modes">
<div class="control-btn tooltip active" id="orbitMode" title="Orbit Camera (O)">
<i class="fas fa-globe"></i>
<span class="tooltiptext">Orbit Camera (O)</span>
</div>
<div class="control-btn tooltip" id="panMode" title="Pan Camera (P)">
<i class="fas fa-arrows-alt"></i>
<span class="tooltiptext">Pan Camera (P)</span>
</div>
<div class="control-btn tooltip" id="moveMode" title="Move Objects (M)">
<i class="fas fa-hand-pointer"></i>
<span class="tooltiptext">Move Objects (M)</span>
</div>
</div>
<div class="touch-controls">
<div class="touch-joystick" id="touchJoystick">
<div class="touch-joystick-inner"></div>
</div>
</div>
</div>
<div class="code-editor">
<textarea id="codeArea" placeholder="// Type your 3D game code here...&#10;// Example:&#10;// Create a cube at position (0, 0, 0)&#10;// const cube = new THREE.Mesh(&#10;// new THREE.BoxGeometry(),&#10;// new THREE.MeshStandardMaterial({color: 0x4a6bff})&#10;// );&#10;// cube.position.set(0, 0, 0);&#10;// scene.add(cube);"></textarea>
<div class="code-samples">
<button class="code-sample-btn" id="rotateCode">Rotation</button>
<button class="code-sample-btn" id="animationCode">Animation</button>
<button class="code-sample-btn" id="physicsCode">Physics</button>
</div>
</div>
</div>
</div>
<div class="status-bar">
<div>Ready</div>
<div>FPS: 0</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// UI Elements
const codeBtn = document.getElementById('codeBtn');
const codeEditor = document.querySelector('.code-editor');
const runBtn = document.getElementById('runBtn');
const applyPropsBtn = document.getElementById('applyProps');
const deleteObjBtn = document.getElementById('deleteObj');
const objectItems = document.querySelectorAll('.object-item');
const canvas = document.getElementById('gameCanvas');
const coordDisplay = document.getElementById('coordDisplay');
const controlInfo = document.getElementById('controlInfo');
const objectCountDisplay = document.querySelector('.object-count');
const touchJoystick = document.getElementById('touchJoystick');
const dragHighlight = document.getElementById('dragHighlight');
// Code example buttons
const rotateCodeBtn = document.getElementById('rotateCode');
const animationCodeBtn = document.getElementById('animationCode');
const physicsCodeBtn = document.getElementById('physicsCode');
// Control mode buttons
const controlModes = {
orbit: document.getElementById('orbitMode'),
pan: document.getElementById('panMode'),
move: document.getElementById('moveMode')
};
const tabs = document.querySelectorAll('.tab');
// Three.js setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050505);
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
// Camera setup
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
camera.position.set(0, 5, 10);
// Controls setup
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.25;
orbitControls.screenSpacePanning = false;
orbitControls.minDistance = 2;
orbitControls.maxDistance = 50;
// Drag controls
let dragControls;
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 15, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Grid helper
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
scene.add(gridHelper);
// Axes helper (for orientation)
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// Initialize variables
let selectedObject = null;
let objects = [];
let currentMode = 'orbit';
let isDragging = false;
let startX = 0;
let startY = 0;
let dragStartPos = new THREE.Vector3();
// Code examples
const codeExamples = {
rotate: `// Make selected object rotate continuously
if (selectedObject) {
const obj = selectedObject.threeObj;
obj.userData.update = function(delta) {
this.rotation.y += delta * 2; // Rotate 2 radians per second
};
alert("Rotation applied to selected object!");
} else {
alert("No object selected!");
}`,
animation: `// Make selected object bounce up and down
if (selectedObject) {
const obj = selectedObject.threeObj;
const startY = obj.position.y;
let time = 0;
obj.userData.update = function(delta) {
time += delta;
this.position.y = startY + Math.abs(Math.sin(time * 2)) * 2;
};
alert("Bounce animation applied to selected object!");
} else {
alert("No object selected!");
}`,
physics: `// Apply simple physics to selected object
if (selectedObject) {
const obj = selectedObject.threeObj;
const speed = { x: 0, z: 0 };
let gravity = -9.8;
let velocityY = 0;
let grounded = false;
const floorY = 0; // Ground level
// Throw the object forward
speed.z = -5;
velocityY = 5;
obj.userData.update = function(delta) {
// Apply gravity
velocityY += gravity * delta;
this.position.y += velocityY * delta;
// Move in X/Z directions
this.position.x += speed.x * delta;
this.position.z += speed.z * delta;
// Ground collision
if (this.position.y < floorY) {
this.position.y = floorY;
velocityY = -velocityY * 0.6; // Bounce with energy loss
if (Math.abs(velocityY) < 0.5) {
velocityY = 0;
grounded = true;
}
}
// Update object properties in UI
if (obj === selectedObject?.threeObj) {
updateObjectPositionInUI(this.position);
}
};
alert("Physics applied to selected object!");
} else {
alert("No object selected!");
}
function updateObjectPositionInUI(pos) {
document.getElementById('objPosX').value = pos.x.toFixed(2);
document.getElementById('objPosY').value = pos.y.toFixed(2);
document.getElementById('objPosZ').value = pos.z.toFixed(2);
const obj = objects.find(o => o.threeObj === selectedObject?.threeObj);
if (obj) {
obj.x = pos.x;
obj.y = pos.y;
obj.z = pos.z;
}
}`
};
// Initialize scene with default objects
function initScene() {
// Create ground plane
const groundGeo = new THREE.PlaneGeometry(20, 20);
const groundMat = new THREE.MeshStandardMaterial({
color: 0x222222,
side: THREE.DoubleSide,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Add to objects array
objects.push({
name: 'Ground',
type: 'plane',
threeObj: ground,
x: 0,
y: 0,
z: 0,
rotX: -90,
rotY: 0,
rotZ: 0,
scale: 1,
color: '#222222'
});
updateObjectCount();
}
// Initialize drag controls
function initDragControls() {
if (dragControls) {
dragControls.dispose();
}
const dragObjects = objects
.map(obj => obj.threeObj)
.filter(obj => obj !== objects[0].threeObj); // Exclude ground
dragControls = new THREE.DragControls(dragObjects, camera, renderer.domElement);
dragControls.addEventListener('dragstart', function(event) {
orbitControls.enabled = false;
selectedObject = objects.find(o => o.threeObj === event.object);
updateSelectedObjectUI(selectedObject);
dragStartPos.copy(event.object.position);
// Show drag highlight
const bbox = new THREE.Box3().setFromObject(event.object);
const size = bbox.max.clone().sub(bbox.min).length();
dragHighlight.style.width = `${size * 50}px`;
dragHighlight.style.height = `${size * 50}px`;
dragHighlight.style.display = 'block';
});
dragControls.addEventListener('drag', function(event) {
// Update object properties in UI
if (selectedObject) {
selectedObject.x = event.object.position.x;
selectedObject.y = event.object.position.y;
selectedObject.z = event.object.position.z;
document.getElementById('objPosX').value = selectedObject.x.toFixed(2);
document.getElementById('objPosY').value = selectedObject.y.toFixed(2);
document.getElementById('objPosZ').value = selectedObject.z.toFixed(2);
}
// Update coordinate display
updateCoordinateDisplay();
});
dragControls.addEventListener('dragend', function() {
orbitControls.enabled = currentMode !== 'move';
dragHighlight.style.display = 'none';
});
// Deactivate by default
dragControls.enabled = false;
}
// Update object's world position in drag highlight
function updateDragHighlightPosition(obj) {
if (!obj) return;
const position = new THREE.Vector3();
obj.getWorldPosition(position);
position.project(camera);
const x = (position.x * 0.5 + 0.5) * canvas.clientWidth;
const y = (-(position.y * 0.5) + 0.5) * canvas.clientHeight;
dragHighlight.style.left = `${x - 25}px`;
dragHighlight.style.top = `${y - 25}px`;
}
// Create a 3D object
function create3DObject(type, name = `${type}_${objects.length + 1}`) {
let geometry, material, mesh;
material = new THREE.MeshStandardMaterial({
color: new THREE.Color(document.getElementById('objColor').value),
roughness: 0.7,
metalness: 0.1
});
switch(type) {
case 'cube':
geometry = new THREE.BoxGeometry(1, 1, 1);
break;
case 'sphere':
geometry = new THREE.SphereGeometry(0.5, 32, 32);
break;
case 'cylinder':
geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32);
break;
case 'plane':
geometry = new THREE.PlaneGeometry(1, 1);
break;
case 'light':
// Point light
const light = new THREE.PointLight(0xffffff, 1, 10);
light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
scene.add(light);
const lightHelper = new THREE.PointLightHelper(light, 0.2);
scene.add(lightHelper);
return {
name: name,
type: 'light',
threeObj: light,
helper: lightHelper,
x: 0,
y: 2,
z: 0,
rotX: 0,
rotY: 0,
rotZ: 0,
scale: 1,
color: '#ffffff'
};
case 'character':
// Simple character (sphere with eyes)
const charGeo = new THREE.SphereGeometry(0.5, 32, 32);
const charMat = new THREE.MeshStandardMaterial({ color: 0x4a6bff });
const character = new THREE.Mesh(charGeo, charMat);
character.castShadow = true;
// Eyes
const eyeGeo = new THREE.SphereGeometry(0.1, 16, 16);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
leftEye.position.set(-0.2, 0.1, 0.45);
character.add(leftEye);
const rightEye = new THREE.Mesh(eyeGeo, eyeMat);
rightEye.position.set(0.2, 0.1, 0.45);
character.add(rightEye);
scene.add(character);
return {
name: name,
type: 'character',
threeObj: character,
x: 0,
y: 0.5,
z: 0,
rotX: 0,
rotY: 0,
rotZ: 0,
scale: 1,
color: '#4a6bff'
};
}
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
scene.add(mesh);
const newObj = {
name: name,
type: type,
threeObj: mesh,
x: (Math.random() * 4 - 2),
y: type === 'plane' ? -0.01 : type === 'cube' ? 0.5 : 0.5,
z: (Math.random() * 4 - 2),
rotX: 0,
rotY: 0,
rotZ: 0,
scale: 1,
color: document.getElementById('objColor').value
};
mesh.position.set(newObj.x, newObj.y, newObj.z);
return newObj;
}
// Update object count display
function updateObjectCount() {
objectCountDisplay.textContent = `Objects: ${objects.length}`;
}
// Handle window resize
function onWindowResize() {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
// Update drag highlight position if dragging
if (selectedObject && dragHighlight.style.display === 'block') {
updateDragHighlightPosition(selectedObject.threeObj);
}
}
// Update coordinate display
function updateCoordinateDisplay() {
if (selectedObject) {
const obj = selectedObject;
coordDisplay.textContent =
`Selected: ${obj.name} | ` +
`Pos: (${obj.x.toFixed(1)}, ${obj.y.toFixed(1)}, ${obj.z.toFixed(1)}) | ` +
`Rot: (${obj.rotX.toFixed(0)}, ${obj.rotY.toFixed(0)}, ${obj.rotZ.toFixed(0)})`;
} else {
const pos = camera.position;
coordDisplay.textContent =
`Selected: None | Camera: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`;
}
}
// Raycasting for object selection
function getIntersectedObject(event) {
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
// Calculate pointer position in normalized device coordinates
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Set raycaster from camera
raycaster.setFromCamera(pointer, camera);
// Calculate objects intersecting the picking ray
const intersectableObjects = objects.map(obj => obj.threeObj);
const intersects = raycaster.intersectObjects(intersectableObjects);
if (intersects.length > 0) {
const intersectedObj = intersects[0].object;
return objects.find(obj => obj.threeObj === intersectedObj);
}
return null;
}
// Set control mode
function setControlMode(mode) {
currentMode = mode;
// Update UI
Object.values(controlModes).forEach(btn => btn.classList.remove('active'));
controlModes[mode].classList.add('active');
// Update info display
const modeNames = {
orbit: 'Orbit Camera (Left Click + Drag)',
pan: 'Pan Camera (Left Click + Drag)',
move: 'Move Objects (Left Click to Select, Drag to Move)'
};
controlInfo.textContent = `Control Mode: ${modeNames[mode]}`;
// Set orbit controls behavior
if (mode === 'orbit') {
orbitControls.enableRotate = true;
orbitControls.enablePan = false;
if (dragControls) dragControls.enabled = false;
} else if (mode === 'pan') {
orbitControls.enableRotate = false;
orbitControls.enablePan = true;
if (dragControls) dragControls.enabled = false;
} else if (mode === 'move') {
orbitControls.enableRotate = false;
orbitControls.enablePan = false;
if (dragControls) dragControls.enabled = true;
}
}
// Update selected object properties in UI
function updateSelectedObjectUI(obj) {
if (!obj) {
// Clear fields if no object selected
document.getElementById('objName').value = '';
document.getElementById('objPosX').value = '0';
document.getElementById('objPosY').value = '0';
document.getElementById('objPosZ').value = '0';
document.getElementById('objRotX').value = '0';
document.getElementById('objRotY').value = '0';
document.getElementById('objRotZ').value = '0';
document.getElementById('objScale').value = '1';
document.getElementById('objColor').value = '#4a6bff';
return;
}
document.getElementById('objName').value = obj.name;
document.getElementById('objPosX').value = obj.x;
document.getElementById('objPosY').value = obj.y;
document.getElementById('objPosZ').value = obj.z;
document.getElementById('objRotX').value = obj.rotX;
document.getElementById('objRotY').value = obj.rotY;
document.getElementById('objRotZ').value = obj.rotZ;
document.getElementById('objScale').value = obj.scale;
document.getElementById('objColor').value = obj.color;
}
// Apply properties from UI to selected object
function applyObjectProperties() {
if (!selectedObject) return;
const obj = selectedObject;
obj.name = document.getElementById('objName').value;
// Position
obj.x = parseFloat(document.getElementById('objPosX').value);
obj.y = parseFloat(document.getElementById('objPosY').value);
obj.z = parseFloat(document.getElementById('objPosZ').value);
obj.threeObj.position.set(obj.x, obj.y, obj.z);
// Rotation (in degrees)
obj.rotX = parseFloat(document.getElementById('objRotX').value);
obj.rotY = parseFloat(document.getElementById('objRotY').value);
obj.rotZ = parseFloat(document.getElementById('objRotZ').value);
obj.threeObj.rotation.set(
THREE.MathUtils.degToRad(obj.rotX),
THREE.MathUtils.degToRad(obj.rotY),
THREE.MathUtils.degToRad(obj.rotZ)
);
// Scale
obj.scale = parseFloat(document.getElementById('objScale').value);
obj.threeObj.scale.set(obj.scale, obj.scale, obj.scale);
// Color
obj.color = document.getElementById('objColor').value;
if (obj.threeObj.material) { // Not all objects have materials (like lights)
obj.threeObj.material.color.set(new THREE.Color(obj.color));
obj.threeObj.material.needsUpdate = true;
}
if (obj.type === 'light') {
obj.helper.update();
}
updateCoordinateDisplay();
}
// Delete selected object
function deleteSelectedObject() {
if (!selectedObject) return;
// Don't allow deleting the ground
if (selectedObject.name === 'Ground') {
alert("Cannot delete the ground plane!");
return;
}
// Remove from scene
scene.remove(selectedObject.threeObj);
// Remove helper if exists
if (selectedObject.helper) {
scene.remove(selectedObject.helper);
}
// Remove from objects array
objects = objects.filter(obj => obj !== selectedObject);
// Clear selection
selectedObject = null;
updateSelectedObjectUI(null);
updateObjectCount();
updateCoordinateDisplay();
// Reinitialize drag controls
initDragControls();
}
// Move selected object during drag
function moveSelectedObject(event) {
if (!selectedObject || currentMode !== 'move' || !isDragging) return;
const rect = canvas.getBoundingClientRect();
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
// Create a plane at the object's current position for intersection
const planeNormal = new THREE.Vector3(0, 1, 0);
const plane = new THREE.Plane(planeNormal, 0);
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
// Update object position
if (intersectionPoint) {
selectedObject.x = intersectionPoint.x;
selectedObject.y = intersectionPoint.y;
selectedObject.z = intersectionPoint.z;
selectedObject.threeObj.position.copy(intersectionPoint);
// Update UI
document.getElementById('objPosX').value = selectedObject.x.toFixed(2);
document.getElementById('objPosY').value = selectedObject.y.toFixed(2);
document.getElementById('objPosZ').value = selectedObject.z.toFixed(2);
updateCoordinateDisplay();
}
}
// Handle mouse/touch events
function handlePointerDown(event) {
// Prevent default for touch events
if (event.touches) {
event.preventDefault();
event = event.touches[0];
}
// Only process left mouse button
if (event.button !== undefined && event.button !== 0) return;
isDragging = true;
startX = event.clientX;
startY = event.clientY;
// In move mode, check for object selection
if (currentMode === 'move') {
selectedObject = getIntersectedObject(event);
updateSelectedObjectUI(selectedObject);
if (selectedObject) {
dragStartPos.copy(selectedObject.threeObj.position);
}
} else {
// If not in move mode, clear selection
selectedObject = null;
updateSelectedObjectUI(null);
}
updateCoordinateDisplay();
}
function handlePointerMove(event) {
// Prevent default for touch events
if (event.touches) {
event.preventDefault();
event = event.touches[0];
}
if (!isDragging) return;
// Handle object movement
if (currentMode === 'move' && selectedObject) {
moveSelectedObject(event);
// Update drag highlight position
updateDragHighlightPosition(selectedObject.threeObj);
}
// Update coordinate display in any case
updateCoordinateDisplay();
}
function handlePointerUp() {
if (isDragging && currentMode === 'move' && selectedObject) {
const dragEndPos = selectedObject.threeObj.position;
const distance = dragStartPos.distanceTo(dragEndPos);
// If very little movement, assume it was a click (not a drag)
if (distance < 0.5) {
selectedObject = getIntersectedObject(event);
updateSelectedObjectUI(selectedObject);
}
}
isDragging = false;
dragHighlight.style.display = 'none';
}
// Animation loop
let lastDelta = 0;
function animate() {
requestAnimationFrame(animate);
// Calculate delta time
const time = performance.now();
const delta = Math.min((time - lastDelta) / 1000, 0.1); // Cap at 100ms
lastDelta = time;
// Update orbit controls if needed
if (currentMode === 'orbit' || currentMode === 'pan') {
orbitControls.update();
}
// Update all objects that have an update function
objects.forEach(obj => {
if (obj.threeObj.userData.update) {
obj.threeObj.userData.update(delta);
// Update properties if this is the selected object
if (selectedObject && obj.threeObj === selectedObject.threeObj) {
obj.x = obj.threeObj.position.x;
obj.y = obj.threeObj.position.y;
obj.z = obj.threeObj.position.z;
obj.rotX = THREE.MathUtils.radToDeg(obj.threeObj.rotation.x);
obj.rotY = THREE.MathUtils.radToDeg(obj.threeObj.rotation.y);
obj.rotZ = THREE.MathUtils.radToDeg(obj.threeObj.rotation.z);
updateSelectedObjectUI(selectedObject);
}
}
});
renderer.render(scene, camera);
updateCoordinateDisplay();
// Update FPS counter
updateFPS();
}
// FPS counter
let lastTime = 0;
let frameCount = 0;
let fps = 0;
function updateFPS() {
const now = performance.now();
frameCount++;
if (now >= lastTime + 1000) {
fps = Math.round((frameCount * 1000) / (now - lastTime));
document.querySelector('.status-bar div:last-child').textContent = `FPS: ${fps}`;
frameCount = 0;
lastTime = now;
}
}
// Initialize scene
initScene();
initDragControls();
// Set initial control mode
setControlMode('orbit');
// Event listeners
codeBtn.addEventListener('click', function() {
codeEditor.style.display = codeEditor.style.display === 'none' ? 'block' : 'none';
onWindowResize();
});
runBtn.addEventListener('click', function() {
const code = document.getElementById('codeArea').value;
try {
// Execute the code in a controlled environment
const executeCode = new Function('scene', 'THREE', 'selectedObject', 'objects', code);
executeCode(scene, THREE, selectedObject, objects);
// Add new objects to our tracking (simple version - in reality would need parsing)
scene.children.forEach(child => {
if (child.isMesh && !objects.find(o => o.threeObj === child)) {
const newObj = {
name: `obj_${objects.length + 1}`,
type: 'custom',
threeObj: child,
x: child.position.x,
y: child.position.y,
z: child.position.z,
rotX: THREE.MathUtils.radToDeg(child.rotation.x),
rotY: THREE.MathUtils.radToDeg(child.rotation.y),
rotZ: THREE.MathUtils.radToDeg(child.rotation.z),
scale: child.scale.x,
color: '#ffffff'
};
objects.push(newObj);
updateObjectCount();
// Reinitialize drag controls to include new object
initDragControls();
}
});
} catch (e) {
alert("Error in code: " + e.message);
}
});
// Add object when clicking on object items
objectItems.forEach(item => {
item.addEventListener('click', function() {
const type = this.getAttribute('data-type');
const newObj = create3DObject(type);
objects.push(newObj);
// Select the new object
selectedObject = newObj;
updateSelectedObjectUI(selectedObject);
updateObjectCount();
updateCoordinateDisplay();
// Reinitialize drag controls to include new object
initDragControls();
});
});
applyPropsBtn.addEventListener('click', function() {
applyObjectProperties();
});
deleteObjBtn.addEventListener('click', function() {
deleteSelectedObject();
});
// Control mode switching
Object.entries(controlModes).forEach(([mode, btn]) => {
btn.addEventListener('click', function() {
setControlMode(mode);
});
});
// Code examples
rotateCodeBtn.addEventListener('click', function() {
document.getElementById('codeArea').value = codeExamples.rotate;
});
animationCodeBtn.addEventListener('click', function() {
document.getElementById('codeArea').value = codeExamples.animation;
});
physicsCodeBtn.addEventListener('click', function() {
document.getElementById('codeArea').value = codeExamples.physics;
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Only process if not in a text input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch(e.key.toLowerCase()) {
case 'o':
setControlMode('orbit');
break;
case 'p':
setControlMode('pan');
break;
case 'm':
setControlMode('move');
break;
case 'delete':
deleteSelectedObject();
break;
}
});
// Mouse/touch events
canvas.addEventListener('mousedown', handlePointerDown);
canvas.addEventListener('mousemove', handlePointerMove);
canvas.addEventListener('mouseup', handlePointerUp);
canvas.addEventListener('mouseleave', handlePointerUp);
// Touch events
canvas.addEventListener('touchstart', handlePointerDown);
canvas.addEventListener('touchmove', handlePointerMove, { passive: false });
canvas.addEventListener('touchend', handlePointerUp);
// Window resize
window.addEventListener('resize', onWindowResize);
// Detect mobile
if (/Mobi|Android/i.test(navigator.userAgent)) {
document.querySelector('.touch-controls').style.display = 'flex';
}
// Start animation
onWindowResize(); // Initial size setup
animate();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
</html>