diff --git a/Axiom/Renderer/Camera.cpp b/Axiom/Renderer/Camera.cpp index 121117cd..4f5f29f9 100644 --- a/Axiom/Renderer/Camera.cpp +++ b/Axiom/Renderer/Camera.cpp @@ -27,6 +27,16 @@ void Camera::SetPerspective(float VerticalFovDegrees, float AspectRatio, m_Projection = glm::perspective(glm::radians(VerticalFovDegrees), AspectRatio, NearPlane, FarPlane); m_Projection[1][1] *= -1.0f; + m_ProjectionType = CameraProjectionType::Perspective; +} + +void Camera::SetOrthographic(float OrthoHeight, float AspectRatio, + float NearPlane, float FarPlane) { + const float HalfH = OrthoHeight * 0.5f; + const float HalfW = HalfH * AspectRatio; + m_Projection = glm::ortho(-HalfW, HalfW, -HalfH, HalfH, NearPlane, FarPlane); + m_Projection[1][1] *= -1.0f; // Vulkan Y-flip + m_ProjectionType = CameraProjectionType::Orthographic; } void Camera::SetPosition(const glm::vec3 &Position) { diff --git a/Axiom/Renderer/Camera.h b/Axiom/Renderer/Camera.h index 3ef9cdbb..99043c48 100644 --- a/Axiom/Renderer/Camera.h +++ b/Axiom/Renderer/Camera.h @@ -4,6 +4,8 @@ #include namespace Axiom { +enum class CameraProjectionType { Perspective, Orthographic }; + class Camera { public: Camera() = default; @@ -12,6 +14,8 @@ class Camera { const glm::vec3 &Up = glm::vec3(0.0f, 1.0f, 0.0f)); void SetPerspective(float VerticalFovDegrees, float AspectRatio, float NearPlane, float FarPlane); + void SetOrthographic(float OrthoHeight, float AspectRatio, + float NearPlane, float FarPlane); void SetPosition(const glm::vec3 &Position); void SetRotation(float YawDegrees, float PitchDegrees); void MoveLocal(const glm::vec3 &LocalOffset); @@ -26,6 +30,10 @@ class Camera { const glm::mat4 &GetViewMatrix() const { return m_View; } const glm::mat4 &GetProjectionMatrix() const { return m_Projection; } glm::mat4 GetViewProjectionMatrix() const { return m_Projection * m_View; } + CameraProjectionType GetProjectionType() const { return m_ProjectionType; } + bool IsOrthographic() const { + return m_ProjectionType == CameraProjectionType::Orthographic; + } private: void UpdateOrientationVectors(); @@ -39,5 +47,6 @@ class Camera { float m_PitchDegrees{0.0f}; glm::mat4 m_View{1.0f}; glm::mat4 m_Projection{1.0f}; + CameraProjectionType m_ProjectionType{CameraProjectionType::Perspective}; }; } // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp b/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp index fa3f59da..62c90c1d 100644 --- a/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp +++ b/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp @@ -1118,7 +1118,8 @@ void VulkanRendererBackend::DrawBackground(VkCommandBuffer CommandBuffer) { const bool UseHDR = m_LoadedHDRSkyboxData != nullptr && m_HDRSkyboxDescriptorSet != VK_NULL_HANDLE && m_ActiveScene != nullptr && - m_ActiveScene->ActiveCamera != nullptr; + m_ActiveScene->ActiveCamera != nullptr && + !m_ActiveScene->ActiveCamera->IsOrthographic(); if (UseHDR) { vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, diff --git a/Axiom/Session/EditorCommand.h b/Axiom/Session/EditorCommand.h index 99c0c9e7..18d890c8 100644 --- a/Axiom/Session/EditorCommand.h +++ b/Axiom/Session/EditorCommand.h @@ -1,5 +1,6 @@ #pragma once +#include "Renderer/Camera.h" #include "Session/SessionTypes.h" #include @@ -128,12 +129,23 @@ struct ResumeSessionCommand {}; struct StopSessionCommand {}; +struct SetCameraProjectionCommand { + CameraProjectionType ProjectionType{CameraProjectionType::Perspective}; +}; + struct SetWorldSettingsCommand { EditorWorldSettings Settings; }; +struct PlaceActorCommand { + std::string ChildTemplateId; // empty = bare Actor, no child + std::string ChildMeshAssetPath; // if set, assigns this asset to the child mesh after creation + glm::vec3 Location{0.0f}; +}; + using EditorCommandPayload = std::variant; + StopSessionCommand, SetWorldSettingsCommand, + PlaceActorCommand>; struct EditorCommand { EditorCommandPayload Payload; diff --git a/Axiom/Session/EditorSession.cpp b/Axiom/Session/EditorSession.cpp index 6e294acb..e48c3f15 100644 --- a/Axiom/Session/EditorSession.cpp +++ b/Axiom/Session/EditorSession.cpp @@ -152,6 +152,9 @@ std::string CommandTypeName(const EditorCommandPayload &Payload) { if (std::holds_alternative(Payload)) { return "set_viewport_camera_pose"; } + if (std::holds_alternative(Payload)) { + return "set_camera_projection"; + } if (std::holds_alternative(Payload)) { return "set_look_active"; } @@ -215,6 +218,9 @@ std::string CommandTypeName(const EditorCommandPayload &Payload) { if (std::holds_alternative(Payload)) { return "set_world_settings"; } + if (std::holds_alternative(Payload)) { + return "place_actor"; + } return "set_transform"; } @@ -234,7 +240,8 @@ bool IsAuthoringMutationCommand(const EditorCommandPayload &Payload) { std::holds_alternative(Payload) || std::holds_alternative(Payload) || std::holds_alternative(Payload) || - std::holds_alternative(Payload); + std::holds_alternative(Payload) || + std::holds_alternative(Payload); } EditorSceneItemKind KindForClassName(std::string_view ClassName) { @@ -1691,6 +1698,23 @@ void EditorSession::HandleCommand( }}); } +void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetCameraProjectionCommand &Command) { + EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); + Viewport.ProjectionType = Command.ProjectionType; + if (Command.ProjectionType == CameraProjectionType::Orthographic) { + Viewport.Camera.SetOrthographic(Viewport.OrthoHeight, + m_Config.CameraAspectRatio, + m_Config.CameraNearPlane, + m_Config.CameraFarPlane); + } else { + Viewport.Camera.SetPerspective(m_Config.CameraVerticalFovDegrees, + m_Config.CameraAspectRatio, + m_Config.CameraNearPlane, + m_Config.CameraFarPlane); + } +} + void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, const SetLookActiveCommand &Command) { EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); @@ -2011,8 +2035,22 @@ void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, return; } - CookMeshAssetBestEffort(m_ContentDir, Command.AssetPath); - const std::filesystem::path FullPath = m_ContentDir / Command.AssetPath; + // Resolve "Engine/" prefix to the engine content directory. + const std::filesystem::path AssetRelative{Command.AssetPath}; + const bool IsEngineAsset = + !AssetRelative.empty() && *AssetRelative.begin() == "Engine"; + std::filesystem::path EffectiveContentDir = m_ContentDir; + std::filesystem::path EffectiveRelative = AssetRelative; + if (IsEngineAsset && !m_EngineContentDir.empty()) { + EffectiveContentDir = m_EngineContentDir; + auto It = AssetRelative.begin(); + ++It; // skip "Engine" + EffectiveRelative.clear(); + for (; It != AssetRelative.end(); ++It) EffectiveRelative /= *It; + } + + CookMeshAssetBestEffort(EffectiveContentDir, EffectiveRelative.string()); + const std::filesystem::path FullPath = EffectiveContentDir / EffectiveRelative; const auto SceneData = Assets::LoadBasicMeshAsset(FullPath); if (!SceneData.has_value() || SceneData->Instances.empty()) { A_CORE_WARN("SetMeshAsset: failed to load '{}' for object '{}'", @@ -2291,10 +2329,96 @@ void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, } } +void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaceActorCommand &Command) { + EnsurePresence(QueuedCommand.Context.User); + Instance *WorldFolder = EnsureWorldFolder(); + if (!WorldFolder) return; + + // Create the Actor parent + const std::string ActorId = BuildUniqueObjectId("Actor"); + const std::string ActorDisplayName = BuildUniqueDisplayName("Actor"); + const EditorTransformDetails ActorTransform{.Location = Command.Location}; + m_State.Scene.ObjectDetailsById.emplace( + ActorId, + EditorObjectDetails{ + .ObjectId = ActorId, + .DisplayName = ActorDisplayName, + .Kind = EditorSceneItemKind::Actor, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = ActorTransform, + .WorldTransform = ActorTransform, + }); + Instance *ActorNode = CreateInstanceForTemplate("Actor", ActorId); + if (ActorNode) ActorNode->SetParent(WorldFolder); + + // Create the child object (if a template was specified) + std::string ChildId; + std::string ChildDisplayName; + if (!Command.ChildTemplateId.empty()) { + const EditorSceneItemKind ChildKind = KindForTemplateId(Command.ChildTemplateId); + ChildId = BuildUniqueObjectId(Command.ChildTemplateId); + ChildDisplayName = BuildUniqueDisplayName(Command.ChildTemplateId); + const bool ChildTransformable = SupportsTransformForKind(ChildKind); + m_State.Scene.ObjectDetailsById.emplace( + ChildId, + EditorObjectDetails{ + .ObjectId = ChildId, + .DisplayName = ChildDisplayName, + .Kind = ChildKind, + .Visible = true, + .SupportsTransform = ChildTransformable, + .TransformReadOnly = false, + .Transform = ChildTransformable + ? std::optional{EditorTransformDetails{}} + : std::nullopt, + .WorldTransform = ChildTransformable + ? std::optional{EditorTransformDetails{}} + : std::nullopt, + }); + if (Instance *ChildNode = + CreateInstanceForTemplate(Command.ChildTemplateId, ChildId)) { + ChildNode->SetParent(ActorNode ? ActorNode : WorldFolder); + } + } + + SyncItemsFromTree(); + PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ActorId, + .DisplayName = ActorDisplayName, + }}); + if (!ChildId.empty()) { + PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ChildId, + .DisplayName = ChildDisplayName, + }}); + if (!Command.ChildMeshAssetPath.empty()) { + HandleCommand(QueuedCommand, SetMeshAssetCommand{ + .ObjectId = ChildId, + .AssetPath = Command.ChildMeshAssetPath, + }); + } + } + + // Apply world-space location to the actor + HandleCommand(QueuedCommand, SetTransformCommand{ + .ObjectId = ActorId, + .Location = Command.Location, + }); +} + void EditorSession::SetContentDir(std::filesystem::path ContentDir) { m_ContentDir = std::move(ContentDir); } +void EditorSession::SetEngineContentDir(std::filesystem::path EngineContentDir) { + m_EngineContentDir = std::move(EngineContentDir); +} + void EditorSession::PublishScriptError(const std::string &ObjectId, const std::string &Message) { PublishEvent({ScriptErrorEvent{.ObjectId = ObjectId, .Message = Message}}); diff --git a/Axiom/Session/EditorSession.h b/Axiom/Session/EditorSession.h index b0944e67..0b9a2ff5 100644 --- a/Axiom/Session/EditorSession.h +++ b/Axiom/Session/EditorSession.h @@ -34,6 +34,8 @@ struct EditorSessionConfig { struct EditorViewportState { Camera Camera; + CameraProjectionType ProjectionType{CameraProjectionType::Perspective}; + float OrthoHeight{20.0f}; bool IsLooking{false}; glm::dvec2 LastCursorPosition{0.0, 0.0}; bool HasLastCursorPosition{false}; @@ -170,6 +172,10 @@ class EditorSession final : public IEditorCommandSink { void SetContentDir(std::filesystem::path ContentDir); const std::filesystem::path &GetContentDir() const { return m_ContentDir; } + // Optional fallback for engine-bundled assets (paths prefixed with "Engine/"). + void SetEngineContentDir(std::filesystem::path EngineContentDir); + const std::filesystem::path &GetEngineContentDir() const { return m_EngineContentDir; } + void EnsureViewportState(SessionUserId User); void SetPresenceState(SessionUserId User, EditorUserPresenceState State); void SetSceneState(EditorSceneState SceneState); @@ -257,6 +263,8 @@ class EditorSession final : public IEditorCommandSink { const UpdateViewportCameraCommand &Command); void HandleCommand(const QueuedEditorCommand &QueuedCommand, const SetViewportCameraPoseCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetCameraProjectionCommand &Command); void HandleCommand(const QueuedEditorCommand &QueuedCommand, const SetLookActiveCommand &Command); void HandleCommand(const QueuedEditorCommand &QueuedCommand, @@ -301,6 +309,8 @@ class EditorSession final : public IEditorCommandSink { const StopSessionCommand &Command); void HandleCommand(const QueuedEditorCommand &QueuedCommand, const SetWorldSettingsCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaceActorCommand &Command); void ApplyWorldTransform(std::string_view ObjectId, const EditorTransformDetails &WorldTransform, SessionUserId User, bool PublishEvent); @@ -321,6 +331,7 @@ class EditorSession final : public IEditorCommandSink { EditorMessageBus m_MessageBus; std::unique_ptr m_SceneRoot; std::filesystem::path m_ContentDir; + std::filesystem::path m_EngineContentDir; std::optional m_RuntimeSceneSnapshot; std::unique_ptr m_PhysicsWorld; }; diff --git a/Axiom/Session/MeshPicking.h b/Axiom/Session/MeshPicking.h index 81e38f4b..f34e8c0d 100644 --- a/Axiom/Session/MeshPicking.h +++ b/Axiom/Session/MeshPicking.h @@ -37,7 +37,6 @@ BuildViewportRay(const Camera &Cam, uint32_t VpWidth, uint32_t VpHeight, return std::nullopt; } - const glm::vec3 RayOrigin = Cam.GetPosition(); const float NdcX = (MousePixel.x / static_cast(VpWidth)) * 2.0f - 1.0f; const float NdcY = (MousePixel.y / static_cast(VpHeight)) * 2.0f - 1.0f; const glm::vec4 WorldH = @@ -47,6 +46,17 @@ BuildViewportRay(const Camera &Cam, uint32_t VpWidth, uint32_t VpHeight, } const glm::vec3 WorldPt = glm::vec3(WorldH) / WorldH.w; + + if (Cam.IsOrthographic()) { + const glm::vec3 RayDir = glm::normalize(Cam.GetForward()); + if (!std::isfinite(RayDir.x) || !std::isfinite(RayDir.y) || + !std::isfinite(RayDir.z)) { + return std::nullopt; + } + return ViewportRay{.Origin = WorldPt, .Direction = RayDir}; + } + + const glm::vec3 RayOrigin = Cam.GetPosition(); const glm::vec3 RayDir = glm::normalize(WorldPt - RayOrigin); if (!std::isfinite(RayDir.x) || !std::isfinite(RayDir.y) || !std::isfinite(RayDir.z)) { @@ -148,6 +158,13 @@ inline float ComputeBillboardHalfSizeWorld(const Camera &Cam, return 0.1f; } + if (Cam.IsOrthographic()) { + // For ortho: |Projection[1][1]| = 1/HalfH, so world units per pixel = 2/(ProjectionY * VpHeight) + const float WorldUnitsPerPixel = + 2.0f / (ProjectionY * static_cast(VpHeight)); + return std::max(0.01f, PixelSize * 0.5f * WorldUnitsPerPixel); + } + const float TanHalfFov = 1.0f / ProjectionY; const float WorldUnitsPerPixel = (2.0f * Distance * TanHalfFov) / static_cast(VpHeight); diff --git a/Content/Engine/Cooked/AssetCookManifest.json b/Content/Engine/Cooked/AssetCookManifest.json new file mode 100644 index 00000000..a543271d --- /dev/null +++ b/Content/Engine/Cooked/AssetCookManifest.json @@ -0,0 +1,14 @@ +{ + "entries": [ + {"assetId":17869987657431928246,"kind":"material","relativePath":"Generated/MeshMaterials/Cube__0","cookedPath":"Cooked/Generated/MeshMaterials/Cube__0.wmat","formatVersion":1,"sourceHash":5263286426569856486}, + {"assetId":17473500555079411416,"kind":"mesh","relativePath":"Primitives/Cube.glb","cookedPath":"Cooked/Primitives/Cube.wmesh","formatVersion":2,"sourceHash":14886726218825273740}, + {"assetId":9525971645921137078,"kind":"material","relativePath":"Generated/MeshMaterials/Sphere__0","cookedPath":"Cooked/Generated/MeshMaterials/Sphere__0.wmat","formatVersion":1,"sourceHash":5263286426569856486}, + {"assetId":2642413918879317334,"kind":"mesh","relativePath":"Primitives/Sphere.glb","cookedPath":"Cooked/Primitives/Sphere.wmesh","formatVersion":2,"sourceHash":6741214548514332}, + {"assetId":13522777309111790592,"kind":"material","relativePath":"Generated/MeshMaterials/Cylinder__0","cookedPath":"Cooked/Generated/MeshMaterials/Cylinder__0.wmat","formatVersion":1,"sourceHash":5263286426569856486}, + {"assetId":6692227360735420864,"kind":"mesh","relativePath":"Primitives/Cylinder.glb","cookedPath":"Cooked/Primitives/Cylinder.wmesh","formatVersion":2,"sourceHash":15044282560946633958}, + {"assetId":17271692768927002155,"kind":"material","relativePath":"Generated/MeshMaterials/Cone__0","cookedPath":"Cooked/Generated/MeshMaterials/Cone__0.wmat","formatVersion":1,"sourceHash":5263286426569856486}, + {"assetId":8791640745736304361,"kind":"mesh","relativePath":"Primitives/Cone.glb","cookedPath":"Cooked/Primitives/Cone.wmesh","formatVersion":2,"sourceHash":18189565898652062489}, + {"assetId":11105005092375462530,"kind":"material","relativePath":"Generated/MeshMaterials/Plane__0","cookedPath":"Cooked/Generated/MeshMaterials/Plane__0.wmat","formatVersion":1,"sourceHash":5263286426569856486}, + {"assetId":13441880797815026060,"kind":"mesh","relativePath":"Primitives/Plane.glb","cookedPath":"Cooked/Primitives/Plane.wmesh","formatVersion":2,"sourceHash":14317949110025125343} + ] +} diff --git a/Content/Engine/Primitives/Cone.glb b/Content/Engine/Primitives/Cone.glb new file mode 100644 index 00000000..8a498178 Binary files /dev/null and b/Content/Engine/Primitives/Cone.glb differ diff --git a/Content/Engine/Primitives/Cube.glb b/Content/Engine/Primitives/Cube.glb new file mode 100644 index 00000000..74131124 Binary files /dev/null and b/Content/Engine/Primitives/Cube.glb differ diff --git a/Content/Engine/Primitives/Cylinder.glb b/Content/Engine/Primitives/Cylinder.glb new file mode 100644 index 00000000..be5b5faa Binary files /dev/null and b/Content/Engine/Primitives/Cylinder.glb differ diff --git a/Content/Engine/Primitives/Plane.glb b/Content/Engine/Primitives/Plane.glb new file mode 100644 index 00000000..f0e3e6bb Binary files /dev/null and b/Content/Engine/Primitives/Plane.glb differ diff --git a/Content/Engine/Primitives/Sphere.glb b/Content/Engine/Primitives/Sphere.glb new file mode 100644 index 00000000..88ce2c5b Binary files /dev/null and b/Content/Engine/Primitives/Sphere.glb differ diff --git a/Docs/DistributedWraithEngineDesign.md b/Docs/DistributedWraithEngineDesign.md index 1a60c79c..c8c236d3 100644 --- a/Docs/DistributedWraithEngineDesign.md +++ b/Docs/DistributedWraithEngineDesign.md @@ -83,6 +83,10 @@ - Physics authoring is now available through authoritative scene details: body type, collider type, box half extents, sphere radius, mass, friction, and restitution persist through save/load - Imported mesh assets now default to static box collision that covers the authored mesh bounds; older scenes are migrated to that default on load only when the mesh had no authored physics yet - Generated mesh children from multi-mesh imports remain read-only and inherit physics authoring from their imported root mesh object; the browser inspector now surfaces that inheritance instead of hiding physics entirely +- A configurable sky background is now wired end-to-end: `RenderScene::SkyboxColorTop/Bottom` plus an optional `HDRTextureSourceDataRef SkyboxHDRTexture` feed two `DrawBackground` paths in the Vulkan backend — the existing `gradient_color.comp` produces a vertical blend, and a new `skybox_hdr.comp` unprojects each pixel through `inverse(proj * view)` and equirectangularly samples a `VK_FORMAT_R32G32B32A32_SFLOAT` image so the HDR is preserved at full float precision and rotates with the camera; `SetWorldSettingsCommand` carries both the colors and a content-relative `SkyboxHDRPath`, `EditorSession` loads HDR data on path change (with previous-path caching to avoid redundant disk hits), and `HeadlessSessionLayer` republishes the scene's HDR ref each frame via `RenderCommand::SetSkyboxHDR`; HDR uploads/swaps are deferred onto the per-frame `DeletionQueue` so in-flight command buffers can't reference a freed image +- The cooked-texture format (`.wtex`) was extended to v2 with a `PixelFormat` field appended after the existing v1 header; legacy v1 LDR files continue to load unchanged, while HDR textures cook as v2 with full-float (`RGBA32F`) pixel data through new `SaveCookedHDRTextureAsset` / `LoadCookedHDRTextureAsset` / `CookHDRTextureAsset` paths, so the editor's authored HDR keeps its full dynamic range for future image-based lighting reuse +- `.hdr` is now a first-class asset extension: `LocalAssetSource` indexes it as `AssetKind::Texture`, `RemoteViewportServer` accepts it through the upload endpoint, and the content browser picker filters it natively +- A new `WorldDetailsPanel` (docked alongside the Details inspector) hosts a `Sky Gradient` section with linked color pickers and an `HDR Sky` section that combines a typed content-relative path input, drag-drop from the content browser (consuming the existing `axiom/asset-path` / `axiom/asset-kind` dataTransfer payload), and a folder-icon `AssetPickerButton` popover that lists every `.hdr` in the project searchably; the same `AssetPickerButton` is also wired into the script-class field on the Details panel so script attachment no longer requires typing a fully-qualified class name; the panel reuses the canonical Details-style section/input chrome and forces the shadcn popover surfaces into dark theme so they match the rest of the editor ## 1. Executive Summary WraithEngine will evolve from a single-process native editor into a distributed platform with one shared C++ engine runtime that supports two execution styles: diff --git a/Docs/HeadlessAxiomSessionPrototype.md b/Docs/HeadlessAxiomSessionPrototype.md index 2abd8b21..5175dc9b 100644 --- a/Docs/HeadlessAxiomSessionPrototype.md +++ b/Docs/HeadlessAxiomSessionPrototype.md @@ -139,8 +139,44 @@ Supported command types: - `load_startup_scene` - `set_view_mode` +- `set_camera_projection` - `set_look_active` - `update_viewport_camera` +- `set_viewport_camera_pose` +- `select_object` +- `rename_object` +- `set_object_visibility` +- `create_object` +- `duplicate_object` +- `delete_object` +- `reparent_object` +- `set_transform` +- `attach_script` +- `detach_script` +- `reload_scripts` +- `set_mesh_asset` +- `set_light_properties` +- `set_material_properties` +- `set_material_texture` +- `set_property` +- `drop_mesh` +- `drop_texture` +- `place_actor` +- `play_session` +- `pause_session` +- `resume_session` +- `stop_session` +- `set_world_settings` +- `list_assets` +- `get_schema` +- `save_scene` +- `set_gizmo_mode` +- `set_grid_snap` +- `gizmo_hover` +- `gizmo_drag_start` +- `gizmo_drag_update` +- `gizmo_drag_end` +- `heartbeat` - `render_frame` - `quit` @@ -312,6 +348,29 @@ Object locking, selection/lock visibility, presence indicators, and heartbeat-dr - elapsed ≥ 30 s and Away → `SetPresenceState(Disconnected)`, `ReleaseAllLocksForUser`, broadcast - the two-threshold design handles hard tab closes: JavaScript cleanup never fires, so the heartbeat simply stops, the client goes Away at 10 s and Disconnected at 30 s, and all locks are released at the Disconnected transition +## Orthographic Projection + +- `set_camera_projection` command switches the per-client viewport between `"perspective"` and `"orthographic"` modes +- orthographic projection uses `glm::ortho` with a Vulkan Y-flip, initialized to a 20-unit height +- the HDR skybox compute shader is disabled in orthographic mode (the shader's perspective ray reconstruction produces a solid smear for parallel rays); the gradient fallback is used instead +- billboard raycasting and screen-space size are corrected for orthographic cameras in `MeshPicking.h` +- the browser "Perspective" button in the viewport toolbar is now a live dropdown that sends `set_camera_projection` and reflects the current state + +## Place Actors + +- `place_actor` command creates an Actor parent node at the resolved world-space position, then optionally creates a child object of the given `templateId` +- if `meshAssetPath` is provided, the child Mesh object has that asset assigned atomically via `SetMeshAssetCommand` during the same handler +- mouse coordinates are optional: omitting them (or passing negative values) places the actor at the viewport center +- asset paths prefixed with `Engine/` resolve to the engine content directory (`AXIOM_CONTENT_DIR/Engine/`), allowing built-in primitive meshes to be referenced independently of the active project +- five engine-bundled primitive meshes live in `Content/Engine/Primitives/`: `Cube.glb`, `Sphere.glb`, `Cylinder.glb`, `Cone.glb`, `Plane.glb`; regenerate them with `python3 Tools/gen_primitives.py` +- the Place Actors browser panel supports click-to-place and drag-to-viewport placement; drag payloads are JSON-encoded `{templateId, meshAssetPath}` on the `application/x-place-actor` MIME type + +Example: + +```json +{"type":"place_actor","templateId":"Mesh","meshAssetPath":"Engine/Primitives/Cube.glb","mouseX":800,"mouseY":450} +``` + ## Next Priority - deeper WebRTC sender/playout latency tuning for the remote viewport stream diff --git a/EditorFrontend/components/engine/details.tsx b/EditorFrontend/components/engine/details.tsx index ae91865f..ae6bb5d8 100644 --- a/EditorFrontend/components/engine/details.tsx +++ b/EditorFrontend/components/engine/details.tsx @@ -10,6 +10,7 @@ import { } from "./remote-viewport-context" import { useProjectSession } from "./project-session-context" import { useDock } from "./dock/dock-context" +import { AssetPickerButton, type AssetPickerItem } from "@/components/panels/asset-picker" function fallbackUserLabel(userId: number) { return userId === 1 ? "Host" : `User ${userId - 1}` @@ -375,6 +376,17 @@ function ScriptSection({ [availableClasses, currentValue] ) + const scriptPickerItems = useMemo( + () => + availableClasses.map((entry) => ({ + key: entry.className, + label: entry.className, + sublabel: entry.path, + selectValue: entry.className, + })), + [availableClasses] + ) + async function applyScriptClass() { setIsSaving(true) try { @@ -410,15 +422,33 @@ function ScriptSection({
Class
- setDraftClass(event.target.value)} - placeholder="e.g. MyGame.RotatorScript" - type="text" - value={draftClass} - /> +
+ setDraftClass(event.target.value)} + placeholder="e.g. MyGame.RotatorScript" + type="text" + value={draftClass} + /> + setDraftClass(value)} + onOpen={() => void loadScriptClasses()} + triggerLabel="Browse project script classes" + searchPlaceholder="Search script classes..." + emptyMessage={ + classesLoading + ? "Loading classes..." + : "No attachable script classes found" + } + /> +
{availableClasses.map((entry) => (