diff --git a/src/CortexEngine.App/ObjectManipulator.cs b/src/CortexEngine.App/ObjectManipulator.cs new file mode 100644 index 0000000..2466776 --- /dev/null +++ b/src/CortexEngine.App/ObjectManipulator.cs @@ -0,0 +1,173 @@ +using System.Numerics; +using Engine.Core; +using Engine.Core.Components; +using Engine.Physics; +using Flecs.NET.Core; +using Raylib_cs; +using EngineMesh = Engine.Core.Components.Mesh; +using EngineTransform = Engine.Core.Components.Transform; +using EngineRigidBody = Engine.Core.Components.RigidBody; + +namespace CortexEngine.App; + +/// +/// Unity-like object manipulation: left-click to pick, drag to move on ground plane. +/// Only active when ImGui is not capturing the mouse. +/// +public sealed class ObjectManipulator +{ + private Entity _selectedEntity; + private bool _isDragging; + private Vector3 _dragOffset; + private float _dragDepth; + + public string SelectedEntityName => _selectedEntity.IsValid() ? _selectedEntity.Name() : ""; + public bool IsDragging => _isDragging; + + /// + /// Process input for object picking and dragging. + /// Returns true if input was consumed (ImGui should be ignored). + /// + public bool ProcessInput(World world, Camera camera, IInputState input) + { + // Skip if ImGui is capturing mouse + if (ImGuiNET.ImGui.GetIO().WantCaptureMouse) + { + _isDragging = false; + return false; + } + + var mousePos = new Vector2(input.MouseX, input.MouseY); + var ray = Raylib.GetScreenToWorldRay(mousePos, ToRaylibCamera(camera)); + + // Left click — pick or start drag + if (input.MouseLeft && !_isDragging) + { + var hit = RaycastEntities(world, ray); + if (hit.IsValid()) + { + _selectedEntity = hit; + _isDragging = true; + // Store depth along camera forward axis and offset from object center + var objPos = hit.Get().Position; + var camForward = Vector3.Normalize(camera.Target - camera.Position); + _dragDepth = Vector3.Dot(objPos - camera.Position, camForward); + _dragOffset = objPos - ProjectToCameraPlane(ray, camera, _dragDepth); + return true; + } + } + + // Drag — move object on plane orthogonal to camera (screen-space movement) + if (_isDragging) + { + if (input.MouseLeft) + { + var targetPos = ProjectToCameraPlane(ray, camera, _dragDepth) + _dragOffset; + + if (_selectedEntity.IsValid() && _selectedEntity.Has()) + { + var t = _selectedEntity.Get(); + t.Position = targetPos; + _selectedEntity.Set(t); + } + return true; + } + else + { + _isDragging = false; + } + } + + return false; + } + + /// + /// After physics step, re-sync dragged entity position to physics body. + /// + public void SyncToPhysics(PhysicsWorld physicsWorld) + { + if (_isDragging && _selectedEntity.IsValid() && _selectedEntity.Has()) + { + var t = _selectedEntity.Get(); + physicsWorld.SyncToPhysics(_selectedEntity, t); + } + } + + /// + /// Get the entity currently being dragged (for physics sync skip). + /// + public Entity? GetDraggedEntity() => _isDragging ? _selectedEntity : null; + + private static Entity RaycastEntities(World world, Ray ray) + { + Entity closest = default; + var closestDist = float.MaxValue; + + world.Each((Entity e, ref EngineTransform t, ref EngineMesh _) => + { + var name = e.Name(); + if (string.IsNullOrEmpty(name) || name == "Grid" || name == "Floor") + return; + + // Simple sphere intersection using position + approximate radius + var radius = 1.0f; + if (e.Has()) + { + var rb = e.Get(); + radius = rb.ShapeSize.Length(); + } + + var toCenter = t.Position - ray.Position; + var proj = Vector3.Dot(toCenter, ray.Direction); + if (proj < 0) return; // behind camera + + var closestPoint = ray.Position + ray.Direction * proj; + var dist = Vector3.Distance(closestPoint, t.Position); + + if (dist <= radius) + { + var rayDist = Vector3.Distance(ray.Position, t.Position); + if (rayDist < closestDist) + { + closestDist = rayDist; + closest = e; + } + } + }); + + return closest; + } + + /// + /// Project a screen ray onto a plane orthogonal to the camera at the given depth. + /// This makes objects move in screen-space (like Unity's screen-space drag). + /// + private static Vector3 ProjectToCameraPlane(Ray ray, Camera camera, float depth) + { + var camForward = Vector3.Normalize(camera.Target - camera.Position); + var planePoint = camera.Position + camForward * depth; + + // Ray-plane intersection: plane through planePoint with normal = camForward + var denom = Vector3.Dot(ray.Direction, camForward); + if (MathF.Abs(denom) < 0.0001f) + return planePoint; + + var t = Vector3.Dot(planePoint - ray.Position, camForward) / denom; + if (t < 0) + return planePoint; + + return ray.Position + ray.Direction * t; + } + + private static Camera3D ToRaylibCamera(Camera camera) + { + return new Camera3D + { + Position = camera.Position, + Target = camera.Target, + Up = camera.Up, + FovY = camera.FieldOfView * 180.0f / MathF.PI, + Projection = CameraProjection.Perspective + }; + } +} diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 6222143..c6a0239 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -50,6 +50,7 @@ class Program // ImGui editor layer var imGuiLayer = new ImGuiLayer(); + var objectManipulator = new ObjectManipulator(); if (!cameraTour) { imGuiLayer.Initialize(); @@ -240,6 +241,13 @@ class Program } } + // Object manipulation (Unity-like drag) + if (!cameraTour) + { + var cam = cameraEntity.Get(); + objectManipulator.ProcessInput(world, cam, input); + } + // Physics: create bodies, step, sync transforms if (!cameraTour) { @@ -258,8 +266,13 @@ class Program e.Set(rb); } + // Sync dragged object to physics before stepping + objectManipulator.SyncToPhysics(physicsWorld); + physicsWorld.Update((float)timing.DeltaTime); - physicsWorld.SyncTransforms(world); + + // Sync all transforms EXCEPT the dragged object + physicsWorld.SyncTransforms(world, objectManipulator.IsDragging ? objectManipulator.GetDraggedEntity() : null); } // Capture a demo screenshot after the scene warms up (non-tour mode only). @@ -272,6 +285,10 @@ class Program // Feed frame data to ImGui before rendering. imGuiLayer.SetFrameData(world, timing, currentFps); + // Sync selection between manipulator and ImGui + if (objectManipulator.IsDragging && !string.IsNullOrEmpty(objectManipulator.SelectedEntityName)) + imGuiLayer.SetSelectedEntity(objectManipulator.SelectedEntityName); + renderer.RenderWorld(world); queue.CompletePendingScreenshots(); diff --git a/src/Engine.Graphics.Raylib/ImGuiLayer.cs b/src/Engine.Graphics.Raylib/ImGuiLayer.cs index 62c6b44..899d589 100644 --- a/src/Engine.Graphics.Raylib/ImGuiLayer.cs +++ b/src/Engine.Graphics.Raylib/ImGuiLayer.cs @@ -26,6 +26,16 @@ public sealed class ImGuiLayer : IDisposable private Timing? _timing; private int _fps; + /// + /// Set the selected entity name from external (e.g. ObjectManipulator). + /// + public void SetSelectedEntity(string name) { _selectedEntity = name; } + + /// + /// Get the currently selected entity name. + /// + public string GetSelectedEntity() => _selectedEntity; + /// /// Set per-frame data before calling RenderImGuiUI. /// diff --git a/src/Engine.Physics/PhysicsWorld.cs b/src/Engine.Physics/PhysicsWorld.cs index 35c3347..c39c617 100644 --- a/src/Engine.Physics/PhysicsWorld.cs +++ b/src/Engine.Physics/PhysicsWorld.cs @@ -121,7 +121,7 @@ public sealed class PhysicsWorld : IDisposable _physicsSystem.Update(deltaTime, collisionSteps, _jobSystem); } - public void SyncTransforms(World world) + public void SyncTransforms(World world, Entity? skipEntity = null) { // Collect updates first to avoid calling entity.Set() during dictionary iteration var updates = new List<(Entity entity, Vector3 pos, Quaternion rot)>(); @@ -130,6 +130,9 @@ public sealed class PhysicsWorld : IDisposable if ((ulong)entity.Id == 0) continue; + if (skipEntity.HasValue && skipEntity.Value.Id == entity.Id) + continue; + var pos = _bodyInterface.GetPosition(bodyId); var rot = _bodyInterface.GetRotation(bodyId); updates.Add((entity, pos, rot)); @@ -154,6 +157,8 @@ public sealed class PhysicsWorld : IDisposable return; _bodyInterface.SetPositionAndRotation(bodyId, transform.Position, transform.Rotation, Activation.Activate); + // Zero out velocity so the object doesn't accumulate momentum while being dragged + _bodyInterface.SetLinearAndAngularVelocity(bodyId, Vector3.Zero, Vector3.Zero); } private static Shape CreateShape(RigidBody rb)