diff --git a/Scripts/StandaloneMovementController.cpp b/Scripts/StandaloneMovementController.cpp new file mode 100644 index 0000000..73e4304 --- /dev/null +++ b/Scripts/StandaloneMovementController.cpp @@ -0,0 +1,199 @@ +#include "ScriptRuntime.h" +#include "SceneObject.h" +#include "ThirdParty/imgui/imgui.h" +#include + +namespace { +struct ControllerState { + float pitch = 0.0f; + float yaw = 0.0f; + float verticalVelocity = 0.0f; + bool initialized = false; +}; + +std::unordered_map g_states; + +glm::vec3 moveTuning = glm::vec3(4.5f, 7.5f, 6.5f); // walk speed, run speed, jump +glm::vec3 lookTuning = glm::vec3(0.12f, 200.0f, 0.0f); // sensitivity, max delta clamp, reserved +glm::vec3 capsuleTuning = glm::vec3(1.8f, 0.4f, 0.2f); // height, radius, ground snap +glm::vec3 gravityTuning = glm::vec3(-9.81f, 0.4f, 30.0f); // gravity, probe extra, max fall speed +bool enableMouseLook = true; +bool requireMouseButton = false; +bool enforceCollider = true; +bool enforceRigidbody = true; +bool showDebug = false; + +ControllerState& getState(int id) { + return g_states[id]; +} + +void ensureComponents(ScriptContext& ctx, float height, float radius) { + if (!ctx.object) return; + if (enforceCollider) { + ctx.object->hasCollider = true; + ctx.object->collider.enabled = true; + ctx.object->collider.type = ColliderType::Capsule; + ctx.object->collider.convex = true; + ctx.object->collider.boxSize = glm::vec3(radius * 2.0f, height, radius * 2.0f); + } + if (enforceRigidbody) { + ctx.object->hasRigidbody = true; + ctx.object->rigidbody.enabled = true; + ctx.object->rigidbody.useGravity = true; + ctx.object->rigidbody.isKinematic = false; + } +} +} // namespace + +extern "C" void Script_OnInspector(ScriptContext& ctx) { + ctx.AutoSetting("moveTuning", moveTuning); + ctx.AutoSetting("lookTuning", lookTuning); + ctx.AutoSetting("capsuleTuning", capsuleTuning); + ctx.AutoSetting("gravityTuning", gravityTuning); + ctx.AutoSetting("enableMouseLook", enableMouseLook); + ctx.AutoSetting("requireMouseButton", requireMouseButton); + ctx.AutoSetting("enforceCollider", enforceCollider); + ctx.AutoSetting("enforceRigidbody", enforceRigidbody); + ctx.AutoSetting("showDebug", showDebug); + + ImGui::TextUnformatted("Standalone Movement Controller"); + ImGui::Separator(); + + bool changed = false; + changed |= ImGui::DragFloat3("Walk/Run/Jump", &moveTuning.x, 0.05f, 0.0f, 25.0f, "%.2f"); + changed |= ImGui::DragFloat2("Look Sens/Clamp", &lookTuning.x, 0.01f, 0.0f, 500.0f, "%.2f"); + changed |= ImGui::DragFloat3("Height/Radius/Snap", &capsuleTuning.x, 0.02f, 0.0f, 5.0f, "%.2f"); + changed |= ImGui::DragFloat3("Gravity/Probe/MaxFall", &gravityTuning.x, 0.05f, -50.0f, 50.0f, "%.2f"); + changed |= ImGui::Checkbox("Enable Mouse Look", &enableMouseLook); + changed |= ImGui::Checkbox("Hold RMB to Look", &requireMouseButton); + changed |= ImGui::Checkbox("Force Collider", &enforceCollider); + changed |= ImGui::Checkbox("Force Rigidbody", &enforceRigidbody); + changed |= ImGui::Checkbox("Show Debug", &showDebug); + + if (changed) { + ctx.SaveAutoSettings(); + } +} + +void Begin(ScriptContext& ctx, float /*deltaTime*/) { + if (!ctx.object) return; + ControllerState& state = getState(ctx.object->id); + if (!state.initialized) { + state.pitch = ctx.object->rotation.x; + state.yaw = ctx.object->rotation.y; + state.verticalVelocity = 0.0f; + state.initialized = true; + } + ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y); +} + +void TickUpdate(ScriptContext& ctx, float deltaTime) { + if (!ctx.object) return; + + ControllerState& state = getState(ctx.object->id); + ensureComponents(ctx, capsuleTuning.x, capsuleTuning.y); + + const float walkSpeed = moveTuning.x; + const float runSpeed = moveTuning.y; + const float jumpStrength = moveTuning.z; + const float lookSensitivity = lookTuning.x; + const float maxMouseDelta = glm::max(5.0f, lookTuning.y); + const float height = capsuleTuning.x; + const float radius = capsuleTuning.y; + const float groundSnap = capsuleTuning.z; + const float gravity = gravityTuning.x; + const float probeExtra = gravityTuning.y; + const float maxFall = glm::max(1.0f, gravityTuning.z); + + if (enableMouseLook) { + bool allowLook = !requireMouseButton || ImGui::IsMouseDown(ImGuiMouseButton_Right); + if (allowLook) { + ImGuiIO& io = ImGui::GetIO(); + glm::vec2 delta(io.MouseDelta.x, io.MouseDelta.y); + float len = glm::length(delta); + if (len > maxMouseDelta) { + delta *= (maxMouseDelta / len); + } + state.yaw -= delta.x * 50.0f * lookSensitivity * deltaTime; + state.pitch -= delta.y * 50.0f * lookSensitivity * deltaTime; + state.pitch = std::clamp(state.pitch, -89.0f, 89.0f); + } + } + + glm::quat q = glm::quat(glm::radians(glm::vec3(state.pitch, state.yaw, 0.0f))); + glm::vec3 forward = glm::normalize(q * glm::vec3(0.0f, 0.0f, -1.0f)); + glm::vec3 right = glm::normalize(q * glm::vec3(1.0f, 0.0f, 0.0f)); + glm::vec3 planarForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z)); + glm::vec3 planarRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z)); + if (!std::isfinite(planarForward.x) || glm::length(planarForward) < 1e-3f) { + planarForward = glm::vec3(0.0f, 0.0f, -1.0f); + } + if (!std::isfinite(planarRight.x) || glm::length(planarRight) < 1e-3f) { + planarRight = glm::vec3(1.0f, 0.0f, 0.0f); + } + + glm::vec3 move(0.0f); + if (ImGui::IsKeyDown(ImGuiKey_W)) move += planarForward; + if (ImGui::IsKeyDown(ImGuiKey_S)) move -= planarForward; + if (ImGui::IsKeyDown(ImGuiKey_D)) move += planarRight; + if (ImGui::IsKeyDown(ImGuiKey_A)) move -= planarRight; + if (glm::length(move) > 0.001f) move = glm::normalize(move); + + bool sprint = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift); + float targetSpeed = sprint ? runSpeed : walkSpeed; + glm::vec3 velocity = move * targetSpeed; + + float capsuleHalf = std::max(0.1f, height * 0.5f); + glm::vec3 physVel; + bool havePhysVel = ctx.GetRigidbodyVelocity(physVel); + if (havePhysVel) state.verticalVelocity = physVel.y; + + glm::vec3 hitPos(0.0f); + glm::vec3 hitNormal(0.0f, 1.0f, 0.0f); + float hitDist = 0.0f; + float probeDist = capsuleHalf + probeExtra; + glm::vec3 rayStart = ctx.object->position + glm::vec3(0.0f, 0.1f, 0.0f); + bool hitGround = ctx.RaycastClosest(rayStart, glm::vec3(0.0f, -1.0f, 0.0f), probeDist, + &hitPos, &hitNormal, &hitDist); + bool grounded = hitGround && hitNormal.y > 0.25f && + hitDist <= capsuleHalf + groundSnap && + state.verticalVelocity <= 0.35f; + if (!hitGround) { + grounded = ctx.object->position.y <= capsuleHalf + 0.12f && state.verticalVelocity <= 0.35f; + } + + if (grounded) { + state.verticalVelocity = 0.0f; + if (!havePhysVel) { + if (hitGround) { + ctx.object->position.y = std::max(ctx.object->position.y, hitPos.y + capsuleHalf); + } else { + ctx.object->position.y = capsuleHalf; + } + } + if (ImGui::IsKeyDown(ImGuiKey_Space)) { + state.verticalVelocity = jumpStrength; + } + } else { + state.verticalVelocity += gravity * deltaTime; + } + + state.verticalVelocity = std::clamp(state.verticalVelocity, -maxFall, maxFall); + velocity.y = state.verticalVelocity; + + glm::vec3 rotation(state.pitch, state.yaw, 0.0f); + ctx.object->rotation = rotation; + ctx.SetRigidbodyRotation(rotation); + + if (!ctx.SetRigidbodyVelocity(velocity)) { + ctx.object->position += velocity * deltaTime; + } + + if (showDebug) { + ImGui::Text("Move (%.2f, %.2f, %.2f)", velocity.x, velocity.y, velocity.z); + ImGui::Text("Grounded: %s", grounded ? "yes" : "no"); + } +} + +void Spec(ScriptContext& /*ctx*/, float /*deltaTime*/) {} +void TestEditor(ScriptContext& /*ctx*/, float /*deltaTime*/) {} diff --git a/src/AudioSystem.cpp b/src/AudioSystem.cpp index 652a090..871cc64 100644 --- a/src/AudioSystem.cpp +++ b/src/AudioSystem.cpp @@ -159,7 +159,7 @@ void AudioSystem::update(const std::vector& objects, const Camera& } } -bool AudioSystem::playPreview(const std::string& path, float volume) { +bool AudioSystem::playPreview(const std::string& path, float volume, bool loop) { if (path.empty()) return false; if (!initialized && !init()) return false; @@ -169,6 +169,7 @@ bool AudioSystem::playPreview(const std::string& path, float volume) { std::cerr << "AudioSystem: preview load failed for " << path << " (" << res << ")\n"; return false; } + ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE); ma_sound_set_volume(&previewSound, volume); ma_sound_set_spatialization_enabled(&previewSound, MA_FALSE); previewPath = path; @@ -217,6 +218,12 @@ bool AudioSystem::seekPreview(const std::string& path, double seconds) { return res == MA_SUCCESS; } +bool AudioSystem::setPreviewLoop(bool loop) { + if (!previewActive) return false; + ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE); + return true; +} + bool AudioSystem::playObjectSound(const SceneObject& obj) { if (!obj.hasAudioSource || obj.audioSource.clipPath.empty() || !obj.audioSource.enabled) return false; if (!ensureSoundFor(obj)) return false; @@ -268,12 +275,18 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) { const ma_uint64 framesPerBucket = std::max(1, totalFrames / kPreviewBuckets); preview.waveform.assign(static_cast(kPreviewBuckets), 0.0f); + if (preview.channels >= 2) { + preview.waveformLeft.assign(static_cast(kPreviewBuckets), 0.0f); + preview.waveformRight.assign(static_cast(kPreviewBuckets), 0.0f); + } std::vector temp(kPreviewChunkFrames * preview.channels); ma_uint64 frameCursor = 0; size_t bucketIndex = 0; ma_uint64 bucketCursor = 0; float bucketMax = 0.0f; + float bucketMaxLeft = 0.0f; + float bucketMaxRight = 0.0f; while (frameCursor < totalFrames && bucketIndex < preview.waveform.size()) { ma_uint64 framesToRead = std::min(kPreviewChunkFrames, totalFrames - frameCursor); @@ -285,26 +298,43 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) { if (framesRead == 0) break; for (ma_uint64 f = 0; f < framesRead; ++f) { + size_t frameOffset = static_cast(f * preview.channels); for (ma_uint32 c = 0; c < preview.channels; ++c) { - float sample = temp[static_cast(f * preview.channels + c)]; + float sample = temp[frameOffset + c]; bucketMax = std::max(bucketMax, std::fabs(sample)); } + if (preview.channels >= 2) { + float leftSample = temp[frameOffset]; + float rightSample = temp[frameOffset + 1]; + bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(leftSample)); + bucketMaxRight = std::max(bucketMaxRight, std::fabs(rightSample)); + } bucketCursor++; frameCursor++; if (bucketCursor >= framesPerBucket) { if (bucketIndex < preview.waveform.size()) { preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f); + if (preview.channels >= 2) { + preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f); + preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f); + } bucketIndex++; } bucketCursor = 0; bucketMax = 0.0f; + bucketMaxLeft = 0.0f; + bucketMaxRight = 0.0f; } } } if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) { preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f); + if (preview.channels >= 2) { + preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f); + preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f); + } } ma_decoder_uninit(&decoder); diff --git a/src/AudioSystem.h b/src/AudioSystem.h index d747642..deccdec 100644 --- a/src/AudioSystem.h +++ b/src/AudioSystem.h @@ -14,6 +14,8 @@ struct AudioClipPreview { uint32_t sampleRate = 0; double durationSeconds = 0.0; std::vector waveform; // Normalized 0..1 amplitude envelope for drawing + std::vector waveformLeft; // Left channel envelope (if available) + std::vector waveformRight; // Right channel envelope (if available) }; class AudioSystem { @@ -26,12 +28,13 @@ public: void onPlayStop(); void update(const std::vector& objects, const Camera& listenerCamera, bool playing); - bool playPreview(const std::string& path, float volume = 1.0f); + bool playPreview(const std::string& path, float volume = 1.0f, bool loop = false); void stopPreview(); bool isPreviewing(const std::string& path) const; const AudioClipPreview* getPreview(const std::string& path); bool getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const; bool seekPreview(const std::string& path, double seconds); + bool setPreviewLoop(bool loop); // Scene audio control (runtime) bool playObjectSound(const SceneObject& obj); diff --git a/src/EditorUI.cpp b/src/EditorUI.cpp index d8d4fe8..07e4739 100644 --- a/src/EditorUI.cpp +++ b/src/EditorUI.cpp @@ -188,81 +188,82 @@ void applyModernTheme() { ImGuiStyle& style = ImGui::GetStyle(); ImVec4* colors = style.Colors; - ImVec4 slate = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); - ImVec4 panel = ImVec4(0.11f, 0.12f, 0.14f, 1.00f); - ImVec4 overlay = ImVec4(0.07f, 0.08f, 0.10f, 0.98f); - ImVec4 accent = ImVec4(0.33f, 0.63f, 0.98f, 1.00f); - ImVec4 accentMuted = ImVec4(0.25f, 0.46f, 0.78f, 1.00f); - ImVec4 highlight = ImVec4(0.18f, 0.23f, 0.30f, 1.00f); + ImVec4 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f); + ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f); + ImVec4 overlay = ImVec4(0.09f, 0.10f, 0.14f, 0.98f); + ImVec4 accent = ImVec4(0.58f, 0.34f, 0.78f, 1.00f); + ImVec4 accentMuted = ImVec4(0.46f, 0.30f, 0.68f, 1.00f); + ImVec4 highlight = ImVec4(0.20f, 0.22f, 0.30f, 1.00f); - colors[ImGuiCol_Text] = ImVec4(0.87f, 0.89f, 0.92f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.52f, 0.56f, 0.62f, 1.00f); + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.91f, 0.94f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.56f, 0.60f, 0.66f, 1.00f); - colors[ImGuiCol_WindowBg] = panel; - colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.13f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.13f, 0.18f, 1.00f); + colors[ImGuiCol_ChildBg] = panel; colors[ImGuiCol_PopupBg] = overlay; - colors[ImGuiCol_Border] = ImVec4(0.21f, 0.24f, 0.28f, 0.60f); + colors[ImGuiCol_Border] = ImVec4(0.20f, 0.22f, 0.30f, 0.70f); colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.09f, 0.10f, 0.14f, 1.00f); colors[ImGuiCol_Header] = highlight; - colors[ImGuiCol_HeaderHovered] = ImVec4(0.22f, 0.28f, 0.36f, 1.00f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.44f, 0.72f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.28f, 0.38f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.44f, 0.30f, 0.68f, 1.00f); - colors[ImGuiCol_Button] = ImVec4(0.17f, 0.19f, 0.22f, 1.00f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.23f, 0.27f, 0.33f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.36f, 0.46f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.24f, 0.26f, 0.34f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.32f, 0.36f, 0.48f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.40f, 0.28f, 0.60f, 1.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.15f, 0.18f, 1.00f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.32f, 0.41f, 1.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.19f, 0.26f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.26f, 0.36f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.32f, 0.26f, 0.44f, 1.00f); - colors[ImGuiCol_TitleBg] = ImVec4(0.06f, 0.07f, 0.08f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.12f, 0.14f, 1.00f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.04f, 0.05f, 0.06f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.10f, 0.11f, 0.16f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.14f, 0.15f, 0.21f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.08f, 0.09f, 0.13f, 1.00f); - colors[ImGuiCol_Tab] = ImVec4(0.12f, 0.13f, 0.15f, 1.00f); - colors[ImGuiCol_TabHovered] = ImVec4(0.20f, 0.28f, 0.38f, 1.00f); - colors[ImGuiCol_TabActive] = ImVec4(0.16f, 0.19f, 0.23f, 1.00f); - colors[ImGuiCol_TabUnfocused] = ImVec4(0.09f, 0.10f, 0.12f, 1.00f); - colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.13f, 0.15f, 0.18f, 1.00f); + colors[ImGuiCol_Tab] = ImVec4(0.13f, 0.14f, 0.20f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.30f, 0.26f, 0.42f, 1.00f); + colors[ImGuiCol_TabActive] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f); + colors[ImGuiCol_TabUnfocused] = ImVec4(0.10f, 0.11f, 0.16f, 1.00f); + colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.15f, 0.16f, 0.22f, 1.00f); - colors[ImGuiCol_Separator] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.24f, 0.32f, 0.42f, 1.00f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.44f, 0.60f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.22f, 0.30f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.28f, 0.40f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.50f, 0.32f, 0.70f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.07f, 0.08f, 0.10f, 1.00f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.21f, 0.23f, 0.27f, 1.00f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.26f, 0.30f, 0.36f, 1.00f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.30f, 0.36f, 0.44f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.11f, 0.15f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.22f, 0.24f, 0.32f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.32f, 0.44f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.40f, 0.32f, 0.54f, 1.00f); colors[ImGuiCol_CheckMark] = accent; colors[ImGuiCol_SliderGrab] = accent; colors[ImGuiCol_SliderGrabActive] = accentMuted; - colors[ImGuiCol_ResizeGrip] = ImVec4(0.22f, 0.26f, 0.33f, 1.00f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.32f, 0.46f, 0.62f, 0.80f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.28f, 0.38f, 1.00f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.42f, 0.32f, 0.62f, 0.80f); colors[ImGuiCol_ResizeGripActive] = accent; - colors[ImGuiCol_DockingPreview] = ImVec4(0.33f, 0.63f, 0.98f, 0.60f); - colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.05f, 0.06f, 0.07f, 1.00f); + colors[ImGuiCol_DockingPreview] = ImVec4(0.58f, 0.34f, 0.78f, 0.55f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.08f, 0.09f, 0.13f, 1.00f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.33f, 0.63f, 0.98f, 0.25f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.58f, 0.34f, 0.78f, 0.25f); colors[ImGuiCol_NavHighlight] = accent; - colors[ImGuiCol_TableHeaderBg] = ImVec4(0.15f, 0.17f, 0.20f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f); - style.WindowRounding = 5.0f; - style.ChildRounding = 5.0f; - style.FrameRounding = 6.0f; - style.PopupRounding = 6.0f; + style.WindowRounding = 10.0f; + style.ChildRounding = 12.0f; + style.FrameRounding = 10.0f; + style.PopupRounding = 12.0f; style.ScrollbarRounding = 10.0f; - style.GrabRounding = 6.0f; - style.TabRounding = 6.0f; + style.GrabRounding = 8.0f; + style.TabRounding = 10.0f; - style.WindowPadding = ImVec2(10.0f, 10.0f); - style.FramePadding = ImVec2(9.0f, 5.0f); - style.ItemSpacing = ImVec2(10.0f, 6.0f); - style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); + style.WindowPadding = ImVec2(12.0f, 12.0f); + style.FramePadding = ImVec2(10.0f, 6.0f); + style.ItemSpacing = ImVec2(10.0f, 8.0f); + style.ItemInnerSpacing = ImVec2(8.0f, 6.0f); style.IndentSpacing = 18.0f; style.WindowBorderSize = 1.0f; diff --git a/src/EditorWindows/FileBrowserWindow.cpp b/src/EditorWindows/FileBrowserWindow.cpp index e63d9b8..8f0ae43 100644 --- a/src/EditorWindows/FileBrowserWindow.cpp +++ b/src/EditorWindows/FileBrowserWindow.cpp @@ -21,268 +21,367 @@ #endif namespace FileIcons { + namespace { + ImU32 BlendColor(ImU32 a, ImU32 b, float t) { + int ar = a & 0xFF; + int ag = (a >> 8) & 0xFF; + int ab = (a >> 16) & 0xFF; + int aa = (a >> 24) & 0xFF; + int br = b & 0xFF; + int bg = (b >> 8) & 0xFF; + int bb = (b >> 16) & 0xFF; + int ba = (b >> 24) & 0xFF; + int r = ar + static_cast((br - ar) * t); + int g = ag + static_cast((bg - ag) * t); + int bch = ab + static_cast((bb - ab) * t); + int aout = aa + static_cast((ba - aa) * t); + return IM_COL32(r, g, bch, aout); + } + + void DrawPaperSpeckles(ImDrawList* drawList, ImVec2 min, ImVec2 max, ImU32 color) { + const ImVec2 points[] = { + ImVec2(0.18f, 0.22f), ImVec2(0.42f, 0.36f), ImVec2(0.66f, 0.28f), + ImVec2(0.28f, 0.62f), ImVec2(0.52f, 0.68f), ImVec2(0.74f, 0.58f), + ImVec2(0.36f, 0.82f), ImVec2(0.82f, 0.78f) + }; + float w = max.x - min.x; + float h = max.y - min.y; + float r = std::min(w, h) * 0.015f; + for (const auto& p : points) { + ImVec2 dot(min.x + w * p.x, min.y + h * p.y); + drawList->AddCircleFilled(dot, r, color); + } + } + + struct PaperFrame { + ImVec2 min; + ImVec2 max; + }; + + PaperFrame DrawPaperFileBase(ImDrawList* drawList, ImVec2 pos, float size, ImU32 accentColor) { + const ImU32 kPaperBase = IM_COL32(205, 185, 128, 255); + const ImU32 kPaperEdge = IM_COL32(122, 106, 72, 200); + const ImU32 kPaperFold = IM_COL32(223, 206, 150, 230); + const ImU32 kPaperShadow = IM_COL32(110, 95, 60, 80); + const ImU32 kPaperSpeck = IM_COL32(120, 110, 80, 55); + + float w = size * 0.78f; + float h = size * 0.95f; + float offsetX = (size - w) * 0.5f; + float offsetY = (size - h) * 0.5f; + float cornerSize = w * 0.22f; + float rounding = size * 0.08f; + float shadowOffset = size * 0.04f; + + ImVec2 min = ImVec2(pos.x + offsetX, pos.y + offsetY); + ImVec2 max = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h); + + drawList->AddRectFilled(ImVec2(min.x + shadowOffset, min.y + shadowOffset), + ImVec2(max.x + shadowOffset, max.y + shadowOffset), + kPaperShadow, rounding); + + drawList->AddRectFilled(min, max, kPaperBase, rounding); + drawList->AddRect(min, max, kPaperEdge, rounding, 0, 1.2f); + + ImVec2 foldA(max.x - cornerSize, min.y); + ImVec2 foldB(max.x, min.y + cornerSize); + drawList->AddTriangleFilled(foldA, foldB, ImVec2(max.x - cornerSize, min.y + cornerSize), kPaperFold); + + ImU32 band = BlendColor(accentColor, kPaperBase, 0.65f); + float bandH = h * 0.14f; + drawList->AddRectFilled(ImVec2(min.x, max.y - bandH), max, band, rounding); + + DrawPaperSpeckles(drawList, min, ImVec2(max.x, max.y - bandH), kPaperSpeck); + + ImVec2 contentMin(min.x + w * 0.12f, min.y + h * 0.16f); + ImVec2 contentMax(max.x - w * 0.12f, max.y - h * 0.22f); + return {contentMin, contentMax}; + } + + PaperFrame DrawSheetFileBase(ImDrawList* drawList, ImVec2 pos, float size, ImU32 accentColor) { + const ImU32 kSheetBase = IM_COL32(248, 248, 248, 255); + const ImU32 kSheetBack = IM_COL32(238, 238, 238, 235); + const ImU32 kSheetEdge = IM_COL32(185, 185, 185, 200); + const ImU32 kSheetFold = IM_COL32(232, 232, 232, 255); + const ImU32 kSheetShadow = IM_COL32(0, 0, 0, 55); + const ImU32 kSheetInner = IM_COL32(255, 255, 255, 80); + const ImU32 kFoldShadow = IM_COL32(0, 0, 0, 35); + + float w = size * 0.78f; + float h = size * 0.95f; + float offsetX = (size - w) * 0.5f; + float offsetY = (size - h) * 0.5f; + float cornerSize = w * 0.24f; + float rounding = size * 0.075f; + float shadowOffset = size * 0.04f; + float backOffset = size * 0.02f; + + ImVec2 min = ImVec2(pos.x + offsetX, pos.y + offsetY); + ImVec2 max = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h); + + ImVec2 backMin = ImVec2(min.x + backOffset, min.y + backOffset * 0.6f); + ImVec2 backMax = ImVec2(max.x + backOffset, max.y + backOffset * 0.6f); + drawList->AddRectFilled(backMin, backMax, kSheetBack, rounding); + + drawList->AddRectFilled(ImVec2(min.x + shadowOffset, min.y + shadowOffset), + ImVec2(max.x + shadowOffset, max.y + shadowOffset), + kSheetShadow, rounding); + + drawList->AddRectFilled(min, max, kSheetBase, rounding); + drawList->AddRect(min, max, kSheetEdge, rounding, 0, 1.2f); + drawList->AddRect(ImVec2(min.x + 1.0f, min.y + 1.0f), + ImVec2(max.x - 1.0f, max.y - 1.0f), + kSheetInner, rounding, 0, 1.0f); + + ImVec2 foldA(max.x - cornerSize, min.y); + ImVec2 foldB(max.x, min.y + cornerSize); + ImVec2 foldC(max.x - cornerSize, min.y + cornerSize); + drawList->AddTriangleFilled(ImVec2(foldA.x + 1.0f, foldA.y + 1.0f), + ImVec2(foldB.x + 1.0f, foldB.y + 1.0f), + ImVec2(foldC.x + 1.0f, foldC.y + 1.0f), + kFoldShadow); + drawList->AddTriangleFilled(foldA, foldB, ImVec2(max.x - cornerSize, min.y + cornerSize), kSheetFold); + drawList->AddTriangle(foldA, foldB, foldC, kSheetEdge, 1.0f); + + ImU32 band = BlendColor(accentColor, kSheetBase, 0.55f); + float bandH = h * 0.16f; + drawList->AddRectFilled(ImVec2(min.x, max.y - bandH), max, band, rounding); + + ImVec2 contentMin(min.x + w * 0.12f, min.y + h * 0.16f); + ImVec2 contentMax(max.x - w * 0.12f, max.y - h * 0.24f); + return {contentMin, contentMax}; + } + + ImU32 InkColor() { + return IM_COL32(92, 78, 52, 220); + } + } + // Draw a folder icon void DrawFolderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { + const ImU32 kFolderBase = IM_COL32(198, 178, 120, 255); + const ImU32 kFolderTab = IM_COL32(215, 197, 140, 255); + const ImU32 kFolderEdge = IM_COL32(120, 104, 70, 200); + const ImU32 kFolderShadow = IM_COL32(110, 95, 60, 70); + const ImU32 kFolderSpeck = IM_COL32(120, 110, 80, 55); + float w = size; - float h = size * 0.75f; - float tabW = w * 0.4f; - float tabH = h * 0.15f; - - // Folder body - drawList->AddRectFilled( - ImVec2(pos.x, pos.y + tabH), - ImVec2(pos.x + w, pos.y + h), - color, 3.0f - ); - // Folder tab - drawList->AddRectFilled( - ImVec2(pos.x, pos.y), - ImVec2(pos.x + tabW, pos.y + tabH + 2), - color, 2.0f - ); + float h = size * 0.72f; + float tabW = w * 0.46f; + float tabH = h * 0.28f; + float rounding = size * 0.08f; + float shadowOffset = size * 0.04f; + + ImVec2 bodyMin(pos.x, pos.y + tabH * 0.55f); + ImVec2 bodyMax(pos.x + w, pos.y + h); + ImVec2 tabMin(pos.x + w * 0.06f, pos.y); + ImVec2 tabMax(pos.x + tabW, pos.y + tabH); + + drawList->AddRectFilled(ImVec2(bodyMin.x + shadowOffset, bodyMin.y + shadowOffset), + ImVec2(bodyMax.x + shadowOffset, bodyMax.y + shadowOffset), + kFolderShadow, rounding); + + drawList->AddRectFilled(bodyMin, bodyMax, kFolderBase, rounding); + drawList->AddRect(bodyMin, bodyMax, kFolderEdge, rounding, 0, 1.2f); + + drawList->AddRectFilled(tabMin, tabMax, kFolderTab, rounding); + drawList->AddRect(tabMin, tabMax, kFolderEdge, rounding, 0, 1.0f); + + ImU32 band = BlendColor(color, kFolderBase, 0.7f); + float bandH = h * 0.14f; + drawList->AddRectFilled(ImVec2(bodyMin.x, bodyMax.y - bandH), bodyMax, band, rounding); + + DrawPaperSpeckles(drawList, bodyMin, ImVec2(bodyMax.x, bodyMax.y - bandH), kFolderSpeck); } // Draw a scene/document icon void DrawSceneIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float w = size * 0.8f; - float h = size; - float cornerSize = w * 0.25f; - - // Main document body - ImVec2 p1 = ImVec2(pos.x, pos.y); - ImVec2 p2 = ImVec2(pos.x + w - cornerSize, pos.y); - ImVec2 p3 = ImVec2(pos.x + w, pos.y + cornerSize); - ImVec2 p4 = ImVec2(pos.x + w, pos.y + h); - ImVec2 p5 = ImVec2(pos.x, pos.y + h); - - drawList->AddQuadFilled(p1, ImVec2(pos.x + w - cornerSize, pos.y), ImVec2(pos.x + w - cornerSize, pos.y + h), p5, color); - drawList->AddTriangleFilled(p2, p3, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), color); - drawList->AddRectFilled(ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p4, color); - - // Corner fold - drawList->AddTriangleFilled(p2, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p3, - IM_COL32(255, 255, 255, 60)); - - // Scene icon indicator (play triangle) - float cx = pos.x + w * 0.5f; - float cy = pos.y + h * 0.55f; - float triSize = size * 0.25f; + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + float w = frame.max.x - frame.min.x; + float h = frame.max.y - frame.min.y; + ImVec2 center(frame.min.x + w * 0.55f, frame.min.y + h * 0.55f); + float tri = std::min(w, h) * 0.35f; drawList->AddTriangleFilled( - ImVec2(cx - triSize * 0.4f, cy - triSize * 0.5f), - ImVec2(cx - triSize * 0.4f, cy + triSize * 0.5f), - ImVec2(cx + triSize * 0.5f, cy), - IM_COL32(255, 255, 255, 180) + ImVec2(center.x - tri * 0.4f, center.y - tri * 0.55f), + ImVec2(center.x - tri * 0.4f, center.y + tri * 0.55f), + ImVec2(center.x + tri * 0.6f, center.y), + ink ); } // Draw a 3D model icon (cube wireframe) void DrawModelIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float s = size * 0.8f; - float offset = size * 0.1f; - float depth = s * 0.3f; - - // Front face - ImVec2 f1 = ImVec2(pos.x + offset, pos.y + offset + depth); - ImVec2 f2 = ImVec2(pos.x + offset + s, pos.y + offset + depth); - ImVec2 f3 = ImVec2(pos.x + offset + s, pos.y + offset + s); - ImVec2 f4 = ImVec2(pos.x + offset, pos.y + offset + s); - - // Back face - ImVec2 b1 = ImVec2(f1.x + depth, f1.y - depth); - ImVec2 b2 = ImVec2(f2.x + depth, f2.y - depth); - ImVec2 b3 = ImVec2(f3.x + depth, f3.y - depth); - - // Fill front face - drawList->AddQuadFilled(f1, f2, f3, f4, color); - - // Fill top face - drawList->AddQuadFilled(f1, f2, b2, b1, IM_COL32( - (color & 0xFF) * 0.7f, - ((color >> 8) & 0xFF) * 0.7f, - ((color >> 16) & 0xFF) * 0.7f, - (color >> 24) & 0xFF - )); - - // Fill right face - drawList->AddQuadFilled(f2, b2, b3, f3, IM_COL32( - (color & 0xFF) * 0.5f, - ((color >> 8) & 0xFF) * 0.5f, - ((color >> 16) & 0xFF) * 0.5f, - (color >> 24) & 0xFF - )); - - // Edges - ImU32 edgeColor = IM_COL32(255, 255, 255, 100); - drawList->AddLine(f1, f2, edgeColor, 1.0f); - drawList->AddLine(f2, f3, edgeColor, 1.0f); - drawList->AddLine(f3, f4, edgeColor, 1.0f); - drawList->AddLine(f4, f1, edgeColor, 1.0f); - drawList->AddLine(f1, b1, edgeColor, 1.0f); - drawList->AddLine(f2, b2, edgeColor, 1.0f); - drawList->AddLine(b1, b2, edgeColor, 1.0f); - drawList->AddLine(f3, b3, edgeColor, 1.0f); - drawList->AddLine(b2, b3, edgeColor, 1.0f); + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + float s = std::min(w, h) * 0.6f; + ImVec2 origin(min.x + w * 0.25f, min.y + h * 0.25f); + float depth = s * 0.35f; + + ImVec2 f1(origin.x, origin.y + depth); + ImVec2 f2(origin.x + s, origin.y + depth); + ImVec2 f3(origin.x + s, origin.y + s + depth); + ImVec2 f4(origin.x, origin.y + s + depth); + + ImVec2 b1(f1.x + depth, f1.y - depth); + ImVec2 b2(f2.x + depth, f2.y - depth); + ImVec2 b3(f3.x + depth, f3.y - depth); + ImVec2 b4(f4.x + depth, f4.y - depth); + + drawList->AddLine(f1, f2, ink, 1.4f); + drawList->AddLine(f2, f3, ink, 1.4f); + drawList->AddLine(f3, f4, ink, 1.4f); + drawList->AddLine(f4, f1, ink, 1.4f); + drawList->AddLine(b1, b2, ink, 1.2f); + drawList->AddLine(b2, b3, ink, 1.2f); + drawList->AddLine(b3, b4, ink, 1.2f); + drawList->AddLine(b4, b1, ink, 1.2f); + drawList->AddLine(f1, b1, ink, 1.2f); + drawList->AddLine(f2, b2, ink, 1.2f); + drawList->AddLine(f3, b3, ink, 1.2f); + drawList->AddLine(f4, b4, ink, 1.2f); } // Draw a texture/image icon void DrawTextureIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float padding = size * 0.1f; - ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding); - ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding); - - // Frame - drawList->AddRectFilled(tl, br, color, 2.0f); - - // Mountain landscape - float midY = pos.y + size * 0.6f; + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + + ImVec2 frameMin(min.x + w * 0.08f, min.y + h * 0.12f); + ImVec2 frameMax(max.x - w * 0.08f, max.y - h * 0.1f); + drawList->AddRect(frameMin, frameMax, ink, 3.0f, 0, 1.2f); + + float midY = frameMax.y - h * 0.28f; drawList->AddTriangleFilled( - ImVec2(pos.x + size * 0.2f, br.y - padding), - ImVec2(pos.x + size * 0.45f, midY), - ImVec2(pos.x + size * 0.7f, br.y - padding), - IM_COL32(60, 60, 60, 255) + ImVec2(frameMin.x + w * 0.1f, frameMax.y), + ImVec2(frameMin.x + w * 0.35f, midY), + ImVec2(frameMin.x + w * 0.6f, frameMax.y), + ink ); drawList->AddTriangleFilled( - ImVec2(pos.x + size * 0.5f, br.y - padding), - ImVec2(pos.x + size * 0.7f, midY + size * 0.1f), - ImVec2(pos.x + size * 0.9f, br.y - padding), - IM_COL32(80, 80, 80, 255) + ImVec2(frameMin.x + w * 0.45f, frameMax.y), + ImVec2(frameMin.x + w * 0.7f, midY + h * 0.1f), + ImVec2(frameMin.x + w * 0.9f, frameMax.y), + ink ); - - // Sun - float sunR = size * 0.1f; - drawList->AddCircleFilled(ImVec2(pos.x + size * 0.75f, pos.y + size * 0.35f), sunR, IM_COL32(255, 220, 100, 255)); + + float sunR = std::min(w, h) * 0.12f; + drawList->AddCircleFilled(ImVec2(frameMax.x - w * 0.18f, frameMin.y + h * 0.2f), sunR, ink); } // Draw a shader icon (code brackets) void DrawShaderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float padding = size * 0.15f; - ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding); - ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding); - - // Background - drawList->AddRectFilled(tl, br, color, 3.0f); - - // Code lines - ImU32 lineColor = IM_COL32(255, 255, 255, 180); - float lineY = pos.y + size * 0.35f; - float lineH = size * 0.08f; - float lineSpacing = size * 0.15f; - - drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.7f, lineY + lineH), lineColor); - lineY += lineSpacing; - drawList->AddRectFilled(ImVec2(pos.x + size * 0.3f, lineY), ImVec2(pos.x + size * 0.8f, lineY + lineH), lineColor); - lineY += lineSpacing; - drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.55f, lineY + lineH), lineColor); + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + + float lineH = h * 0.12f; + float lineY = min.y + h * 0.2f; + drawList->AddRectFilled(ImVec2(min.x + w * 0.15f, lineY), ImVec2(min.x + w * 0.75f, lineY + lineH), ink); + lineY += h * 0.22f; + drawList->AddRectFilled(ImVec2(min.x + w * 0.2f, lineY), ImVec2(min.x + w * 0.85f, lineY + lineH), ink); + lineY += h * 0.22f; + drawList->AddRectFilled(ImVec2(min.x + w * 0.15f, lineY), ImVec2(min.x + w * 0.55f, lineY + lineH), ink); } // Draw an audio icon (speaker/waveform) void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - // Speaker body - float spkW = size * 0.25f; - float spkH = size * 0.3f; - float cx = pos.x + size * 0.35f; - float cy = pos.y + size * 0.5f; - + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + float spkW = w * 0.25f; + float spkH = h * 0.28f; + float cx = min.x + w * 0.35f; + float cy = min.y + h * 0.55f; + drawList->AddRectFilled( ImVec2(cx - spkW * 0.5f, cy - spkH * 0.5f), ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f), - color + ink ); - - // Speaker cone drawList->AddTriangleFilled( ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f), ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f), - ImVec2(cx + spkW * 1.2f, cy + spkH), - color + ImVec2(cx + spkW * 1.1f, cy + spkH * 0.9f), + ink ); drawList->AddTriangleFilled( ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f), - ImVec2(cx + spkW * 1.2f, cy - spkH), - ImVec2(cx + spkW * 1.2f, cy + spkH), - color + ImVec2(cx + spkW * 1.1f, cy - spkH * 0.9f), + ImVec2(cx + spkW * 1.1f, cy + spkH * 0.9f), + ink ); - - // Sound waves - ImU32 waveColor = IM_COL32(255, 255, 255, 150); - float waveX = cx + spkW * 1.5f; + + float waveX = cx + spkW * 1.4f; drawList->AddBezierQuadratic( - ImVec2(waveX, cy - size * 0.15f), - ImVec2(waveX + size * 0.1f, cy), - ImVec2(waveX, cy + size * 0.15f), - waveColor, 2.0f + ImVec2(waveX, cy - h * 0.12f), + ImVec2(waveX + w * 0.12f, cy), + ImVec2(waveX, cy + h * 0.12f), + ink, 1.6f ); - waveX += size * 0.12f; + waveX += w * 0.12f; drawList->AddBezierQuadratic( - ImVec2(waveX, cy - size * 0.22f), - ImVec2(waveX + size * 0.12f, cy), - ImVec2(waveX, cy + size * 0.22f), - waveColor, 2.0f + ImVec2(waveX, cy - h * 0.2f), + ImVec2(waveX + w * 0.14f, cy), + ImVec2(waveX, cy + h * 0.2f), + ink, 1.6f ); } // Draw a generic file icon void DrawFileIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float w = size * 0.7f; - float h = size * 0.9f; - float offsetX = (size - w) * 0.5f; - float offsetY = (size - h) * 0.5f; - float cornerSize = w * 0.25f; - - ImVec2 p1 = ImVec2(pos.x + offsetX, pos.y + offsetY); - ImVec2 p2 = ImVec2(pos.x + offsetX + w - cornerSize, pos.y + offsetY); - ImVec2 p3 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + cornerSize); - ImVec2 p4 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h); - ImVec2 p5 = ImVec2(pos.x + offsetX, pos.y + offsetY + h); - - // Main body - drawList->AddQuadFilled(p1, p2, ImVec2(p2.x, p4.y), p5, color); - drawList->AddTriangleFilled(p2, p3, ImVec2(p2.x, p3.y), color); - drawList->AddRectFilled(ImVec2(p2.x, p3.y), p4, color); - - // Corner fold - drawList->AddTriangleFilled(p2, ImVec2(p2.x, p3.y), p3, IM_COL32(255, 255, 255, 50)); + DrawSheetFileBase(drawList, pos, size, color); } // Draw a script/code icon void DrawScriptIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - float padding = size * 0.12f; - ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding); - ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding); - - // Background - drawList->AddRectFilled(tl, br, color, 3.0f); - - // Brackets < > - ImU32 bracketColor = IM_COL32(255, 255, 255, 200); - float cx = pos.x + size * 0.5f; - float cy = pos.y + size * 0.5f; - float bSize = size * 0.2f; - - // Left bracket < - drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.5f, cy), bracketColor, 2.5f); - drawList->AddLine(ImVec2(cx - bSize * 1.5f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), bracketColor, 2.5f); - - // Right bracket > - drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.5f, cy), bracketColor, 2.5f); - drawList->AddLine(ImVec2(cx + bSize * 1.5f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), bracketColor, 2.5f); + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + float cx = min.x + w * 0.5f; + float cy = min.y + h * 0.5f; + float bSize = std::min(w, h) * 0.28f; + + drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.3f, cy), ink, 1.8f); + drawList->AddLine(ImVec2(cx - bSize * 1.3f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), ink, 1.8f); + drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.3f, cy), ink, 1.8f); + drawList->AddLine(ImVec2(cx + bSize * 1.3f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), ink, 1.8f); } // Draw a text icon void DrawTextIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { - DrawFileIcon(drawList, pos, size, color); - - // Text lines - ImU32 lineColor = IM_COL32(255, 255, 255, 150); - float startX = pos.x + size * 0.25f; - float endX = pos.x + size * 0.65f; - float lineY = pos.y + size * 0.4f; - float lineH = size * 0.06f; - float spacing = size * 0.12f; - - for (int i = 0; i < 3; i++) { - float w = (i == 1) ? (endX - startX) * 0.7f : (endX - startX); - drawList->AddRectFilled(ImVec2(startX, lineY), ImVec2(startX + w, lineY + lineH), lineColor); + PaperFrame frame = DrawSheetFileBase(drawList, pos, size, color); + ImU32 ink = IM_COL32(90, 90, 90, 230); + ImVec2 min = frame.min; + ImVec2 max = frame.max; + float w = max.x - min.x; + float h = max.y - min.y; + float lineY = min.y + h * 0.25f; + float lineH = h * 0.12f; + float spacing = h * 0.2f; + + for (int i = 0; i < 3; ++i) { + float lineW = (i == 1) ? w * 0.55f : w * 0.75f; + drawList->AddRectFilled(ImVec2(min.x + w * 0.12f, lineY), + ImVec2(min.x + w * 0.12f + lineW, lineY + lineH), + ink); lineY += spacing; } } @@ -647,23 +746,7 @@ void Engine::renderFileBrowserPanel() { fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid; } if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View"); - ImGui::SameLine(); - if (ImGui::Button("Refresh", ImVec2(68, 0))) { - fileBrowser.needsRefresh = true; - } - ImGui::SameLine(); - if (ImGui::Button("New Mat", ImVec2(78, 0))) { - fs::path target = fileBrowser.currentPath / "NewMaterial.mat"; - int counter = 1; - while (fs::exists(target)) { - target = fileBrowser.currentPath / ("NewMaterial" + std::to_string(counter++) + ".mat"); - } - SceneObject temp("Material", ObjectType::Cube, -1); - temp.materialPath = target.string(); - saveMaterialToFile(temp); - fileBrowser.needsRefresh = true; - } ImGui::EndChild(); ImGui::PopStyleVar(2); diff --git a/src/EditorWindows/ProjectManagerWindow.cpp b/src/EditorWindows/ProjectManagerWindow.cpp index 7a5b594..f1bca45 100644 --- a/src/EditorWindows/ProjectManagerWindow.cpp +++ b/src/EditorWindows/ProjectManagerWindow.cpp @@ -125,9 +125,11 @@ void Engine::renderLauncher() { ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24.0f, 24.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 18.0f); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.09f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::SetNextWindowPos(ImVec2(0, 0)); @@ -143,27 +145,65 @@ void Engine::renderLauncher() { if (ImGui::Begin("Launcher", nullptr, flags)) { - float leftPanelWidth = 280.0f; + const float leftPanelWidth = 300.0f; + const float heroHeight = 120.0f; + const ImVec4 bgTopLeft = ImVec4(0.10f, 0.11f, 0.16f, 1.0f); + const ImVec4 bgTopRight = ImVec4(0.15f, 0.16f, 0.22f, 1.0f); + const ImVec4 bgBottomRight = ImVec4(0.07f, 0.08f, 0.12f, 1.0f); + const ImVec4 bgBottomLeft = ImVec4(0.08f, 0.09f, 0.13f, 1.0f); + const ImVec4 cardBg = ImVec4(0.14f, 0.15f, 0.21f, 0.98f); + const ImVec4 cardOutline = ImVec4(0.20f, 0.22f, 0.30f, 0.70f); + const ImVec4 accent = ImVec4(0.91f, 0.42f, 0.78f, 1.0f); + const ImVec4 accentCool = ImVec4(0.42f, 0.72f, 0.96f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.06f, 0.06f, 0.07f, 1.0f)); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 windowSize = ImGui::GetWindowSize(); + drawList->AddRectFilledMultiColor( + windowPos, + ImVec2(windowPos.x + windowSize.x, windowPos.y + windowSize.y), + ImGui::GetColorU32(bgTopLeft), + ImGui::GetColorU32(bgTopRight), + ImGui::GetColorU32(bgBottomRight), + ImGui::GetColorU32(bgBottomLeft) + ); + + ImGui::BeginChild("LauncherHero", ImVec2(0, heroHeight), true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground); + + ImDrawList* heroDraw = ImGui::GetWindowDrawList(); + ImVec2 heroPos = ImGui::GetWindowPos(); + ImVec2 heroSize = ImGui::GetWindowSize(); + heroDraw->AddRectFilled(heroPos, ImVec2(heroPos.x + heroSize.x, heroPos.y + heroSize.y), + ImGui::GetColorU32(cardBg), 18.0f); + heroDraw->AddRect(heroPos, ImVec2(heroPos.x + heroSize.x, heroPos.y + heroSize.y), + ImGui::GetColorU32(cardBg), 18.0f); + + ImGui::SetCursorPos(ImVec2(28.0f, 24.0f)); + ImGui::TextDisabled("Project Manager"); + ImGui::SetWindowFontScale(1.4f); + ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity"); + ImGui::SetWindowFontScale(1.0f); + ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Debug Build V0.7.0"); + + + ImGui::EndChild(); + + ImGui::Spacing(); + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, cardBg); ImGui::BeginChild("LauncherLeft", ImVec2(leftPanelWidth, 0), true); ImGui::PopStyleColor(); - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.45f, 0.72f, 0.95f, 1.0f), "MODULARITY"); - ImGui::TextDisabled("Game Engine"); - ImGui::Spacing(); - ImGui::Separator(); + ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "GET STARTED"); ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "GET STARTED"); - ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.32f, 0.22f, 0.54f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.43f, 0.30f, 0.70f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.26f, 0.60f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.38f, 0.55f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.48f, 0.68f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.42f, 0.60f, 1.0f)); - - if (ImGui::Button("New Project", ImVec2(-1, 36.0f))) + if (ImGui::Button("New Project", ImVec2(-1, 40.0f))) { projectManager.showNewProjectDialog = true; projectManager.errorMessage.clear(); @@ -186,7 +226,7 @@ void Engine::renderLauncher() { ImGui::Spacing(); - if (ImGui::Button("Open Project", ImVec2(-1, 36.0f))) + if (ImGui::Button("Open Project", ImVec2(-1, 40.0f))) { projectManager.showOpenProjectDialog = true; projectManager.errorMessage.clear(); @@ -194,44 +234,48 @@ void Engine::renderLauncher() { ImGui::PopStyleColor(3); - ImGui::Spacing(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "QUICK ACTIONS"); + ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "QUICK ACTIONS"); ImGui::Spacing(); - if (ImGui::Button("Documentation", ImVec2(-1, 30.0f))) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.22f, 0.32f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.30f, 0.42f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.24f, 0.28f, 0.40f, 1.0f)); + + if (ImGui::Button("Documentation", ImVec2(-1, 34.0f))) { #ifdef _WIN32 - system("start https://github.com"); + system("start https://docs.shockinteractive.xyz"); #else - system("xdg-open https://github.com &"); + system("xdg-open https://docs.shockinteractive.xyz &"); #endif } - if (ImGui::Button("Exit", ImVec2(-1, 30.0f))) + if (ImGui::Button("Exit", ImVec2(-1, 34.0f))) { glfwSetWindowShouldClose(editorWindow, GLFW_TRUE); } + ImGui::PopStyleColor(3); + ImGui::EndChild(); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.11f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, cardBg); ImGui::BeginChild("LauncherRight", ImVec2(0, 0), true); ImGui::PopStyleColor(); - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "RECENT PROJECTS"); + ImGui::TextColored(ImVec4(0.78f, 0.80f, 0.86f, 1.0f), "RECENT PROJECTS"); ImGui::Spacing(); if (projectManager.recentProjects.empty()) { ImGui::Spacing(); - ImGui::TextDisabled("No recent projects"); - ImGui::TextDisabled("Create a new project to get started!"); + ImGui::TextDisabled("There are no recent projects yet.\nCreate or open a project to get started."); } else { @@ -241,30 +285,12 @@ void Engine::renderLauncher() { const auto& rp = projectManager.recentProjects[i]; ImGui::PushID(static_cast(i)); - char label[512]; - std::snprintf(label, sizeof(label), "%s\n%s", - rp.name.c_str(), rp.path.c_str()); - - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f)); - - bool selected = ImGui::Selectable( - label, - false, - ImGuiSelectableFlags_AllowDoubleClick, - ImVec2(availWidth, 48.0f) - ); - - ImGui::PopStyleColor(3); - - // Dummy to extend window bounds properly - ImGui::Dummy(ImVec2(0, 0)); - - if (selected || ImGui::IsItemClicked(ImGuiMouseButton_Left)) - { - OpenProjectPath(rp.path); - } + const ImVec2 cardSize(availWidth, 72.0f); + const ImVec2 cardPos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("RecentCard", cardSize); + bool hovered = ImGui::IsItemHovered(); + bool clicked = ImGui::IsItemClicked(); + ImVec2 afterPos = ImGui::GetCursorScreenPos(); if (ImGui::BeginPopupContextItem("RecentProjectContext")) { @@ -287,6 +313,34 @@ void Engine::renderLauncher() { ImGui::EndPopup(); } + ImU32 cardCol = ImGui::GetColorU32(hovered ? ImVec4(0.18f, 0.19f, 0.27f, 1.0f) + : ImVec4(0.16f, 0.17f, 0.24f, 1.0f)); + ImDrawList* list = ImGui::GetWindowDrawList(); + list->AddRectFilled(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y), cardCol, 14.0f); + list->AddRect(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y), + ImGui::GetColorU32(cardOutline), 14.0f); + + ImVec2 textPos = ImVec2(cardPos.x + 16.0f, cardPos.y + 14.0f); + ImGui::SetCursorScreenPos(textPos); + ImGui::TextColored(ImVec4(0.92f, 0.93f, 0.96f, 1.0f), "%s", rp.name.c_str()); + ImGui::SetCursorScreenPos(ImVec2(textPos.x, textPos.y + 22.0f)); + ImGui::TextDisabled("%s", rp.path.c_str()); + + const float buttonWidth = 88.0f; + ImGui::SetCursorScreenPos(ImVec2(cardPos.x + cardSize.x - buttonWidth - 16.0f, cardPos.y + 20.0f)); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.24f, 0.28f, 0.40f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.32f, 0.38f, 0.55f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.30f, 0.35f, 0.50f, 1.0f)); + bool openClicked = ImGui::Button("Open", ImVec2(buttonWidth, 30.0f)); + ImGui::PopStyleColor(3); + + if ((clicked && !openClicked) || openClicked) + { + OpenProjectPath(rp.path); + } + + ImGui::SetCursorScreenPos(afterPos); + ImGui::PopID(); ImGui::Spacing(); } @@ -303,7 +357,7 @@ void Engine::renderLauncher() { ImGui::End(); ImGui::PopStyleColor(2); - ImGui::PopStyleVar(3); + ImGui::PopStyleVar(5); if (projectManager.showNewProjectDialog) renderNewProjectDialog(); diff --git a/src/EditorWindows/SceneWindows.cpp b/src/EditorWindows/SceneWindows.cpp index 007fe2d..0d35c15 100644 --- a/src/EditorWindows/SceneWindows.cpp +++ b/src/EditorWindows/SceneWindows.cpp @@ -18,6 +18,83 @@ #include #endif +namespace { + ImU32 GetHierarchyTypeColor(ObjectType type) { + switch (type) { + case ObjectType::Camera: return IM_COL32(110, 175, 235, 220); + case ObjectType::DirectionalLight: + case ObjectType::PointLight: + case ObjectType::SpotLight: + case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220); + case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220); + case ObjectType::OBJMesh: + case ObjectType::Model: return IM_COL32(120, 200, 150, 220); + case ObjectType::Mirror: return IM_COL32(180, 200, 210, 220); + case ObjectType::Plane: return IM_COL32(170, 180, 190, 220); + case ObjectType::Torus: return IM_COL32(155, 215, 180, 220); + default: return IM_COL32(140, 190, 235, 220); + } + } + + void DrawFileOutlineIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { + float w = size * 0.78f; + float h = size * 0.95f; + float offsetX = (size - w) * 0.5f; + float offsetY = (size - h) * 0.5f; + float corner = w * 0.28f; + ImVec2 min(pos.x + offsetX, pos.y + offsetY); + ImVec2 max(pos.x + offsetX + w, pos.y + offsetY + h); + ImVec2 foldA(max.x - corner, min.y); + ImVec2 foldB(max.x, min.y + corner); + ImVec2 foldC(max.x - corner, min.y + corner); + drawList->AddRect(min, max, color, size * 0.12f, 0, 1.2f); + drawList->AddTriangle(foldA, foldB, foldC, color, 1.2f); + drawList->AddLine(ImVec2(foldA.x, foldA.y), ImVec2(foldC.x, foldC.y), color, 1.2f); + } + + void DrawCubeOutlineIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { + float inset = size * 0.18f; + float backOffset = size * 0.16f; + ImVec2 frontMin(pos.x + inset, pos.y + inset + backOffset); + ImVec2 frontMax(pos.x + size - inset, pos.y + size - inset + backOffset); + ImVec2 backMin(pos.x + inset + backOffset, pos.y + inset); + ImVec2 backMax(pos.x + size - inset + backOffset, pos.y + size - inset); + drawList->AddRect(frontMin, frontMax, color, 0.0f, 0, 1.2f); + drawList->AddRect(backMin, backMax, color, 0.0f, 0, 1.2f); + drawList->AddLine(frontMin, backMin, color, 1.2f); + drawList->AddLine(ImVec2(frontMax.x, frontMin.y), ImVec2(backMax.x, backMin.y), color, 1.2f); + drawList->AddLine(ImVec2(frontMin.x, frontMax.y), ImVec2(backMin.x, backMax.y), color, 1.2f); + drawList->AddLine(frontMax, backMax, color, 1.2f); + } + + void DrawHierarchyLines(ImDrawList* drawList, const ImVec2& itemMin, const ImVec2& itemMax, + const std::vector& ancestorHasNext, int depth, bool isLast) { + if (depth <= 0) { + return; + } + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4 base = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled); + ImU32 lineColor = ImGui::ColorConvertFloat4ToU32(ImVec4(base.x, base.y, base.z, 0.6f)); + float indent = style.IndentSpacing; + float rowTop = itemMin.y; + float rowBottom = itemMax.y; + float rowMid = (rowTop + rowBottom) * 0.5f; + float baseX = itemMin.x - indent * depth; + + for (int i = 0; i < depth && i < static_cast(ancestorHasNext.size()); ++i) { + if (ancestorHasNext[i]) { + float x = baseX + indent * (i + 0.5f); + drawList->AddLine(ImVec2(x, rowTop), ImVec2(x, rowBottom), lineColor, 1.0f); + } + } + + float connectorX = baseX + indent * (depth - 0.5f); + float vertEnd = isLast ? rowMid : rowBottom; + drawList->AddLine(ImVec2(connectorX, rowTop), ImVec2(connectorX, vertEnd), lineColor, 1.0f); + drawList->AddLine(ImVec2(connectorX, rowMid), ImVec2(itemMin.x + 6.0f, rowMid), lineColor, 1.0f); + } +} + void Engine::renderHierarchyPanel() { ImGui::Begin("Hierarchy", &showHierarchy); @@ -96,11 +173,18 @@ void Engine::renderHierarchyPanel() { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 2.0f)); ImGui::BeginChild("HierarchyList", ImVec2(0, 0), true); + std::vector rootIndices; + rootIndices.reserve(sceneObjects.size()); for (size_t i = 0; i < sceneObjects.size(); i++) { - if (sceneObjects[i].parentId != -1) - continue; + if (sceneObjects[i].parentId == -1) { + rootIndices.push_back(i); + } + } - renderObjectNode(sceneObjects[i], filter); + std::vector ancestorHasNext; + for (size_t i = 0; i < rootIndices.size(); ++i) { + bool isLastRoot = (i + 1 == rootIndices.size()); + renderObjectNode(sceneObjects[rootIndices[i]], filter, ancestorHasNext, isLastRoot, 0); } if (ImGui::BeginPopupContextWindow("HierarchyBackground", @@ -115,6 +199,8 @@ void Engine::renderHierarchyPanel() { if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube"); if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere"); if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule"); + if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane"); + if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus"); if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror"); ImGui::EndMenu(); } @@ -149,7 +235,8 @@ void Engine::renderHierarchyPanel() { ImGui::End(); } -void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) { +void Engine::renderObjectNode(SceneObject& obj, const std::string& filter, + std::vector& ancestorHasNext, bool isLast, int depth) { std::string nameLower = obj.name; std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower); @@ -164,23 +251,23 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) { if (isSelected) flags |= ImGuiTreeNodeFlags_Selected; if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf; - const char* icon = ""; - switch (obj.type) { - case ObjectType::Cube: icon = "[#]"; break; - case ObjectType::Sphere: icon = "(O)"; break; - case ObjectType::Capsule: icon = "[|]"; break; - case ObjectType::OBJMesh: icon = "[M]"; break; - case ObjectType::Model: icon = "[A]"; break; - case ObjectType::Camera: icon = "(C)"; break; - case ObjectType::DirectionalLight: icon = "(D)"; break; - case ObjectType::PointLight: icon = "(P)"; break; - case ObjectType::SpotLight: icon = "(S)"; break; - case ObjectType::AreaLight: icon = "(L)"; break; - case ObjectType::PostFXNode: icon = "(FX)"; break; - case ObjectType::Mirror: icon = "[R]"; break; - } + std::string label = " " + obj.name; + bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s", label.c_str()); - bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str()); + ImVec2 itemMin = ImGui::GetItemRectMin(); + ImVec2 itemMax = ImGui::GetItemRectMax(); + DrawHierarchyLines(ImGui::GetWindowDrawList(), itemMin, itemMax, ancestorHasNext, depth, isLast); + + float lineHeight = itemMax.y - itemMin.y; + float iconSize = std::max(8.0f, lineHeight - 6.0f); + float labelStart = itemMin.x + ImGui::GetTreeNodeToLabelSpacing(); + ImVec2 iconPos(labelStart, itemMin.y + (lineHeight - iconSize) * 0.5f); + ImU32 iconColor = GetHierarchyTypeColor(obj.type); + if (obj.parentId == -1) { + DrawCubeOutlineIcon(ImGui::GetWindowDrawList(), iconPos, iconSize, iconColor); + } else { + DrawFileOutlineIcon(ImGui::GetWindowDrawList(), iconPos, iconSize, iconColor); + } if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift; @@ -290,13 +377,26 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter) { } if (nodeOpen) { + std::vector visibleChildren; + visibleChildren.reserve(obj.childIds.size()); for (int childId : obj.childIds) { auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), [childId](const SceneObject& o) { return o.id == childId; }); if (it != sceneObjects.end()) { - renderObjectNode(*it, filter); + std::string childLower = it->name; + std::transform(childLower.begin(), childLower.end(), childLower.begin(), ::tolower); + if (filter.empty() || childLower.find(filter) != std::string::npos) { + visibleChildren.push_back(&(*it)); + } } } + + ancestorHasNext.push_back(!isLast); + for (size_t i = 0; i < visibleChildren.size(); ++i) { + bool childLast = (i + 1 == visibleChildren.size()); + renderObjectNode(*visibleChildren[i], filter, ancestorHasNext, childLast, depth + 1); + } + ancestorHasNext.pop_back(); ImGui::TreePop(); } } @@ -348,8 +448,23 @@ void Engine::renderInspectorPanel() { inspectedMaterialValid = false; } + if (browserHasAudio) { + std::string selectedAudio = selectedAudioPath.string(); + if (selectedAudio != audioPreviewSelectedPath) { + audioPreviewSelectedPath = selectedAudio; + if (audioPreviewAutoPlay) { + audio.playPreview(selectedAudio, 1.0f, audioPreviewLoop); + } + } + } else { + audioPreviewSelectedPath.clear(); + } + auto drawWaveform = [&](const char* id, const AudioClipPreview* preview, const ImVec2& size, float progressRatio, float* seekRatioOut) { - if (!preview || preview->waveform.empty()) { + bool hasStereo = preview && preview->channels >= 2 + && !preview->waveformLeft.empty() + && !preview->waveformRight.empty(); + if (!preview || (!hasStereo && preview->waveform.empty())) { ImGui::Dummy(size); return; } @@ -360,14 +475,33 @@ void Engine::renderInspectorPanel() { dl->AddRectFilled(start, end, IM_COL32(30, 35, 45, 180), 4.0f); float midY = (start.y + end.y) * 0.5f; float usableHeight = size.y * 0.45f; - size_t count = preview->waveform.size(); + size_t count = hasStereo + ? std::min(preview->waveformLeft.size(), preview->waveformRight.size()) + : preview->waveform.size(); float step = count > 1 ? size.x / static_cast(count - 1) : size.x; - ImU32 color = IM_COL32(255, 180, 100, 200); - for (size_t i = 0; i < count; ++i) { - float amp = std::clamp(preview->waveform[i], 0.0f, 1.0f); - float x = start.x + step * static_cast(i); - float yOff = amp * usableHeight; - dl->AddLine(ImVec2(x, midY - yOff), ImVec2(x, midY + yOff), color, 1.2f); + if (hasStereo) { + ImU32 leftColor = IM_COL32(255, 190, 90, 200); + ImU32 rightColor = IM_COL32(100, 200, 255, 200); + float topMidY = start.y + size.y * 0.25f; + float bottomMidY = start.y + size.y * 0.75f; + float stereoHeight = size.y * 0.22f; + for (size_t i = 0; i < count; ++i) { + float leftAmp = std::clamp(preview->waveformLeft[i], 0.0f, 1.0f); + float rightAmp = std::clamp(preview->waveformRight[i], 0.0f, 1.0f); + float x = start.x + step * static_cast(i); + float leftOff = leftAmp * stereoHeight; + float rightOff = rightAmp * stereoHeight; + dl->AddLine(ImVec2(x, topMidY - leftOff), ImVec2(x, topMidY + leftOff), leftColor, 1.2f); + dl->AddLine(ImVec2(x, bottomMidY - rightOff), ImVec2(x, bottomMidY + rightOff), rightColor, 1.2f); + } + } else { + ImU32 color = IM_COL32(255, 180, 100, 200); + for (size_t i = 0; i < count; ++i) { + float amp = std::clamp(preview->waveform[i], 0.0f, 1.0f); + float x = start.x + step * static_cast(i); + float yOff = amp * usableHeight; + dl->AddLine(ImVec2(x, midY - yOff), ImVec2(x, midY + yOff), color, 1.2f); + } } if (progressRatio >= 0.0f && progressRatio <= 1.0f) { @@ -648,7 +782,19 @@ void Engine::renderInspectorPanel() { if (isPlayingPreview) { audio.stopPreview(); } else { - audio.playPreview(selectedAudioPath.string()); + audio.playPreview(selectedAudioPath.string(), 1.0f, audioPreviewLoop); + } + } + ImGui::SameLine(); + if (ImGui::Checkbox("Loop##AudioPreview", &audioPreviewLoop)) { + if (isPlayingPreview) { + audio.setPreviewLoop(audioPreviewLoop); + } + } + ImGui::SameLine(); + if (ImGui::Checkbox("Auto Play##AudioPreview", &audioPreviewAutoPlay)) { + if (audioPreviewAutoPlay && !selectedAudioPath.empty() && !isPlayingPreview) { + audio.playPreview(selectedAudioPath.string(), 1.0f, audioPreviewLoop); } } @@ -787,6 +933,8 @@ void Engine::renderInspectorPanel() { case ObjectType::AreaLight: typeLabel = "Area Light"; break; case ObjectType::PostFXNode: typeLabel = "Post FX Node"; break; case ObjectType::Mirror: typeLabel = "Mirror"; break; + case ObjectType::Plane: typeLabel = "Plane"; break; + case ObjectType::Torus: typeLabel = "Torus"; break; } ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeLabel); @@ -837,6 +985,7 @@ void Engine::renderInspectorPanel() { ImGui::Text("Position"); ImGui::PushItemWidth(-1); if (ImGui::DragFloat3("##Position", &obj.position.x, 0.1f)) { + syncLocalTransform(obj); projectManager.currentProject.hasUnsavedChanges = true; } ImGui::PopItemWidth(); @@ -847,6 +996,7 @@ void Engine::renderInspectorPanel() { ImGui::PushItemWidth(-1); if (ImGui::DragFloat3("##Rotation", &obj.rotation.x, 1.0f, -360.0f, 360.0f)) { obj.rotation = NormalizeEulerDegrees(obj.rotation); + syncLocalTransform(obj); projectManager.currentProject.hasUnsavedChanges = true; } ImGui::PopItemWidth(); @@ -856,6 +1006,7 @@ void Engine::renderInspectorPanel() { ImGui::Text("Scale"); ImGui::PushItemWidth(-1); if (ImGui::DragFloat3("##Scale", &obj.scale.x, 0.05f, 0.01f, 100.0f)) { + syncLocalTransform(obj); projectManager.currentProject.hasUnsavedChanges = true; } ImGui::PopItemWidth(); @@ -866,6 +1017,7 @@ void Engine::renderInspectorPanel() { obj.position = glm::vec3(0.0f); obj.rotation = glm::vec3(0.0f); obj.scale = glm::vec3(1.0f); + syncLocalTransform(obj); projectManager.currentProject.hasUnsavedChanges = true; } @@ -1110,7 +1262,7 @@ void Engine::renderInspectorPanel() { if (previewPlaying) { audio.stopPreview(); } else if (!src.clipPath.empty()) { - audio.playPreview(src.clipPath, src.volume); + audio.playPreview(src.clipPath, src.volume, src.loop); } } ImGui::SameLine(); @@ -1234,7 +1386,7 @@ void Engine::renderInspectorPanel() { if (ImGui::SliderFloat("Threshold", &obj.postFx.bloomThreshold, 0.0f, 3.0f, "%.2f")) { changed = true; } - if (ImGui::SliderFloat("Intensity", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) { + if (ImGui::SliderFloat("Intensity##Bloom", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) { changed = true; } if (ImGui::SliderFloat("Spread", &obj.postFx.bloomRadius, 0.5f, 3.5f, "%.2f")) { @@ -1279,7 +1431,7 @@ void Engine::renderInspectorPanel() { changed = true; } ImGui::BeginDisabled(!obj.postFx.vignetteEnabled); - if (ImGui::SliderFloat("Intensity", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) { + if (ImGui::SliderFloat("Intensity##Vignette", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) { changed = true; } if (ImGui::SliderFloat("Smoothness", &obj.postFx.vignetteSmoothness, 0.05f, 1.0f, "%.2f")) { @@ -1912,6 +2064,7 @@ void Engine::renderInspectorPanel() { obj.rigidbody.useGravity = true; obj.rigidbody.isKinematic = false; obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f); + syncLocalTransform(obj); componentChanged = true; } if (!obj.hasAudioSource && ImGui::MenuItem("Audio Source")) { diff --git a/src/EditorWindows/ViewportWindows.cpp b/src/EditorWindows/ViewportWindows.cpp index c0c6e6f..5f0c219 100644 --- a/src/EditorWindows/ViewportWindows.cpp +++ b/src/EditorWindows/ViewportWindows.cpp @@ -525,6 +525,9 @@ void Engine::renderMainMenuBar() { if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube"); if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere"); if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule"); + if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane"); + if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus"); + if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror"); if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera"); if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light"); if (ImGui::MenuItem("Point Light")) addObject(ObjectType::PointLight, "Point Light"); @@ -1066,6 +1069,14 @@ void Engine::renderViewport() { gizmoBoundsMin = glm::vec3(-0.35f, -0.9f, -0.35f); gizmoBoundsMax = glm::vec3(0.35f, 0.9f, 0.35f); break; + case ObjectType::Plane: + gizmoBoundsMin = glm::vec3(-0.5f, -0.5f, -0.02f); + gizmoBoundsMax = glm::vec3(0.5f, 0.5f, 0.02f); + break; + case ObjectType::Torus: + gizmoBoundsMin = glm::vec3(-0.5f); + gizmoBoundsMax = glm::vec3(0.5f); + break; case ObjectType::OBJMesh: { const auto* info = g_objLoader.getMeshInfo(selectedObj->meshId); if (info && info->boundsMin.x < info->boundsMax.x) { @@ -1167,12 +1178,33 @@ void Engine::renderViewport() { o.position = t; o.rotation = NormalizeEulerDegrees(glm::degrees(r)); o.scale = s; + syncLocalTransform(o); }; if (selectedObjectIds.size() <= 1) { applyDelta(*selectedObj); } else { + std::unordered_set selectedSet(selectedObjectIds.begin(), selectedObjectIds.end()); + auto getParentId = [&](int id) -> int { + auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [id](const SceneObject& o){ return o.id == id; }); + if (it == sceneObjects.end()) return -1; + return it->parentId; + }; + auto hasSelectedAncestor = [&](int id) -> bool { + int parentId = getParentId(id); + while (parentId != -1) { + if (selectedSet.count(parentId)) { + return true; + } + parentId = getParentId(parentId); + } + return false; + }; for (int id : selectedObjectIds) { + if (hasSelectedAncestor(id)) { + continue; + } auto itObj = std::find_if(sceneObjects.begin(), sceneObjects.end(), [id](const SceneObject& o){ return o.id == id; }); if (itObj != sceneObjects.end()) { @@ -1597,9 +1629,15 @@ void Engine::renderViewport() { case ObjectType::Capsule: hit = rayAabb(localOrigin, localDir, glm::vec3(-0.35f, -0.9f, -0.35f), glm::vec3(0.35f, 0.9f, 0.35f), hitT); break; + case ObjectType::Plane: + hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT); + break; case ObjectType::Mirror: hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT); break; + case ObjectType::Torus: + hit = raySphere(localOrigin, localDir, 0.5f, hitT); + break; case ObjectType::OBJMesh: { const auto* info = g_objLoader.getMeshInfo(obj.meshId); if (info && info->boundsMin.x < info->boundsMax.x) { diff --git a/src/Engine.cpp b/src/Engine.cpp index cd4d5fa..c213393 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -2,6 +2,7 @@ #include "ModelLoader.h" #include #include +#include #include #include @@ -153,6 +154,15 @@ glm::vec3 ExtractEulerXYZ(const glm::mat3& m) { // GLM's extractEulerAngleXYZ returns (-T1, -T2, -T3) return glm::vec3(-T1, -T2, -T3); } + +glm::quat QuatFromEulerXYZ(const glm::vec3& deg) { + glm::vec3 r = glm::radians(deg); + glm::mat4 m(1.0f); + m = glm::rotate(m, r.x, glm::vec3(1.0f, 0.0f, 0.0f)); + m = glm::rotate(m, r.y, glm::vec3(0.0f, 1.0f, 0.0f)); + m = glm::rotate(m, r.z, glm::vec3(0.0f, 0.0f, 1.0f)); + return glm::quat_cast(glm::mat3(m)); +} } void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) { @@ -165,11 +175,27 @@ void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& if (scale.x != 0.0f) rotMat[0] /= scale.x; if (scale.y != 0.0f) rotMat[1] /= scale.y; if (scale.z != 0.0f) rotMat[2] /= scale.z; + // Orthonormalize to reduce shear-induced rotation jitter. + rotMat[0] = glm::normalize(rotMat[0]); + rotMat[1] = glm::normalize(rotMat[1] - rotMat[0] * glm::dot(rotMat[0], rotMat[1])); + rotMat[2] = glm::normalize(glm::cross(rotMat[0], rotMat[1])); // Use explicit XYZ extraction so yaw isn't clamped to [-90, 90] like glm::yaw/pitch/roll. rot = ExtractEulerXYZ(rotMat); } +glm::mat4 Engine::ComposeTransform(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& scale) { + glm::mat4 m(1.0f); + m = glm::translate(m, position); + m *= glm::mat4_cast(rotation); + m = glm::scale(m, scale); + return m; +} + +glm::mat4 Engine::ComposeTransform(const glm::vec3& position, const glm::vec3& rotationDeg, const glm::vec3& scale) { + return ComposeTransform(position, QuatFromEulerXYZ(rotationDeg), scale); +} + void Engine::recordState(const char* /*reason*/) { SceneSnapshot snap; snap.objects = sceneObjects; @@ -327,11 +353,15 @@ void Engine::run() { updatePlayerController(deltaTime); } + updateHierarchyWorldTransforms(); + bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode)); if (simulatePhysics) { physics.simulate(deltaTime, sceneObjects); } + updateHierarchyWorldTransforms(); + bool audioShouldPlay = isPlaying || specMode || testMode; Camera listenerCamera = camera; for (const auto& obj : sceneObjects) { @@ -901,6 +931,162 @@ void Engine::updatePlayerController(float delta) { if (!physics.setLinearVelocity(player->id, velocity)) { player->position += velocity * delta; } + syncLocalTransform(*player); +} + +void Engine::updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) { + auto safeDiv = [](float v, float d) { return (std::abs(d) > 1e-6f) ? (v / d) : 0.0f; }; + auto unwrapNear = [](float angle, float reference) { + float result = angle; + while (result - reference > 180.0f) result -= 360.0f; + while (reference - result > 180.0f) result += 360.0f; + return result; + }; + + glm::quat invParent = glm::inverse(parentRot); + glm::vec3 localPos = invParent * (obj.position - parentPos); + localPos.x = safeDiv(localPos.x, parentScale.x); + localPos.y = safeDiv(localPos.y, parentScale.y); + localPos.z = safeDiv(localPos.z, parentScale.z); + + glm::quat worldRot = QuatFromEulerXYZ(obj.rotation); + glm::quat localRot = invParent * worldRot; + glm::vec3 localRotDeg = glm::degrees(ExtractEulerXYZ(glm::mat3_cast(localRot))); + glm::vec3 refRot = obj.localInitialized ? obj.localRotation : obj.rotation; + localRotDeg.x = unwrapNear(localRotDeg.x, refRot.x); + localRotDeg.y = unwrapNear(localRotDeg.y, refRot.y); + localRotDeg.z = unwrapNear(localRotDeg.z, refRot.z); + + glm::vec3 localScale(1.0f); + localScale.x = safeDiv(obj.scale.x, parentScale.x); + localScale.y = safeDiv(obj.scale.y, parentScale.y); + localScale.z = safeDiv(obj.scale.z, parentScale.z); + + obj.localPosition = localPos; + obj.localRotation = localRotDeg; + obj.localScale = localScale; + obj.localInitialized = true; +} + +void Engine::initializeLocalTransformsFromWorld(int sceneVersion) { + if (sceneObjects.empty()) return; + + if (sceneVersion >= 10) { + for (auto& obj : sceneObjects) { + if (!obj.localInitialized) { + obj.localPosition = obj.position; + obj.localRotation = NormalizeEulerDegrees(obj.rotation); + obj.localScale = obj.scale; + obj.localInitialized = true; + } + } + updateHierarchyWorldTransforms(); + return; + } + + std::unordered_map worldById; + worldById.reserve(sceneObjects.size()); + for (const auto& obj : sceneObjects) { + worldById[obj.id] = ComposeTransform(obj.position, obj.rotation, obj.scale); + } + + for (auto& obj : sceneObjects) { + if (obj.parentId == -1) { + obj.localPosition = obj.position; + obj.localRotation = NormalizeEulerDegrees(obj.rotation); + obj.localScale = obj.scale; + obj.localInitialized = true; + continue; + } + auto itParent = worldById.find(obj.parentId); + if (itParent == worldById.end()) { + obj.localPosition = obj.position; + obj.localRotation = NormalizeEulerDegrees(obj.rotation); + obj.localScale = obj.scale; + obj.localInitialized = true; + continue; + } + glm::vec3 pPos, pRotDeg, pScale; + DecomposeMatrix(itParent->second, pPos, pRotDeg, pScale); + updateLocalFromWorld(obj, pPos, QuatFromEulerXYZ(pRotDeg), pScale); + } + + updateHierarchyWorldTransforms(); +} + +void Engine::updateHierarchyWorldTransforms() { + if (sceneObjects.empty()) return; + + std::unordered_map indexById; + indexById.reserve(sceneObjects.size()); + for (size_t i = 0; i < sceneObjects.size(); ++i) { + indexById[sceneObjects[i].id] = i; + } + + auto unwrapNear = [](float angle, float reference) { + float result = angle; + while (result - reference > 180.0f) result -= 360.0f; + while (reference - result > 180.0f) result += 360.0f; + return result; + }; + + std::unordered_set visiting; + std::unordered_set visited; + visiting.reserve(sceneObjects.size()); + visited.reserve(sceneObjects.size()); + + std::function processNode = + [&](int id, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) { + if (visited.count(id)) return; + if (visiting.count(id)) return; + auto itIndex = indexById.find(id); + if (itIndex == indexById.end()) return; + + visiting.insert(id); + SceneObject& obj = sceneObjects[itIndex->second]; + if (!obj.localInitialized) { + obj.localPosition = obj.position; + obj.localRotation = NormalizeEulerDegrees(obj.rotation); + obj.localScale = obj.scale; + obj.localInitialized = true; + } + + bool useWorldAuthoritative = obj.hasRigidbody && obj.rigidbody.enabled && !obj.rigidbody.isKinematic; + glm::vec3 worldPos = obj.position; + glm::quat worldRot = QuatFromEulerXYZ(obj.rotation); + glm::vec3 worldScale = obj.scale; + if (useWorldAuthoritative) { + updateLocalFromWorld(obj, parentPos, parentRot, parentScale); + worldPos = obj.position; + worldRot = QuatFromEulerXYZ(obj.rotation); + worldScale = obj.scale; + } else { + glm::quat localRot = QuatFromEulerXYZ(obj.localRotation); + worldRot = parentRot * localRot; + worldScale = parentScale * obj.localScale; + worldPos = parentPos + parentRot * (parentScale * obj.localPosition); + glm::vec3 worldRotDeg = glm::degrees(ExtractEulerXYZ(glm::mat3_cast(worldRot))); + worldRotDeg.x = unwrapNear(worldRotDeg.x, obj.rotation.x); + worldRotDeg.y = unwrapNear(worldRotDeg.y, obj.rotation.y); + worldRotDeg.z = unwrapNear(worldRotDeg.z, obj.rotation.z); + obj.position = worldPos; + obj.rotation = worldRotDeg; + obj.scale = worldScale; + } + + for (int childId : obj.childIds) { + processNode(childId, worldPos, worldRot, worldScale); + } + + visiting.erase(id); + visited.insert(id); + }; + + for (const auto& obj : sceneObjects) { + if (obj.parentId == -1 || indexById.find(obj.parentId) == indexById.end()) { + processNode(obj.id, glm::vec3(0.0f), glm::quat(1.0f, 0.0f, 0.0f, 0.0f), glm::vec3(1.0f)); + } + } } void Engine::OpenProjectPath(const std::string& path) { try { @@ -999,7 +1185,9 @@ void Engine::loadRecentScenes() { fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName); if (fs::exists(scenePath)) { - if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) { + int sceneVersion = 9; + if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) { + initializeLocalTransformsFromWorld(sceneVersion); addConsoleMessage("Loaded scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success); } else { addConsoleMessage("Warning: Failed to load scene, starting fresh", ConsoleMessageType::Warning); @@ -1037,7 +1225,9 @@ void Engine::loadScene(const std::string& sceneName) { } fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName); - if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) { + int sceneVersion = 9; + if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) { + initializeLocalTransformsFromWorld(sceneVersion); undoStack.clear(); redoStack.clear(); projectManager.currentProject.currentSceneName = sceneName; @@ -1112,7 +1302,13 @@ void Engine::addObject(ObjectType type, const std::string& baseName) { sceneObjects.back().material.textureMix = 1.0f; sceneObjects.back().material.color = glm::vec3(1.0f); sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); + } else if (type == ObjectType::Plane) { + sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); } + sceneObjects.back().localPosition = sceneObjects.back().position; + sceneObjects.back().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation); + sceneObjects.back().localScale = sceneObjects.back().scale; + sceneObjects.back().localInitialized = true; setPrimarySelection(id); if (projectManager.currentProject.isLoaded) { projectManager.currentProject.hasUnsavedChanges = true; @@ -1150,6 +1346,10 @@ void Engine::duplicateSelected() { newObj.collider = it->collider; newObj.hasPlayerController = it->hasPlayerController; newObj.playerController = it->playerController; + newObj.localPosition = newObj.position; + newObj.localRotation = NormalizeEulerDegrees(newObj.rotation); + newObj.localScale = newObj.scale; + newObj.localInitialized = true; newObj.hasAudioSource = it->hasAudioSource; newObj.audioSource = it->audioSource; @@ -1202,6 +1402,19 @@ void Engine::setParent(int childId, int parentId) { newParentIt->childIds.push_back(childId); } } + { + glm::vec3 parentPos(0.0f); + glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 parentScale(1.0f); + if (parentId != -1) { + if (SceneObject* parent = findObjectById(parentId)) { + parentPos = parent->position; + parentRot = QuatFromEulerXYZ(parent->rotation); + parentScale = parent->scale; + } + } + updateLocalFromWorld(*childIt, parentPos, parentRot, parentScale); + } if (projectManager.currentProject.isLoaded) { projectManager.currentProject.hasUnsavedChanges = true; @@ -1309,6 +1522,30 @@ bool Engine::addRigidbodyAngularImpulseFromScript(int id, const glm::vec3& impul return physics.addAngularImpulse(id, impulse); } +bool Engine::setRigidbodyYawFromScript(int id, float yawDegrees) { + return physics.setActorYaw(id, yawDegrees); +} + +bool Engine::raycastClosestFromScript(const glm::vec3& origin, const glm::vec3& dir, float distance, + int ignoreId, glm::vec3* hitPos, glm::vec3* hitNormal, + float* hitDistance) const { + return physics.raycastClosest(origin, dir, distance, ignoreId, hitPos, hitNormal, hitDistance); +} + +void Engine::syncLocalTransform(SceneObject& obj) { + glm::vec3 parentPos(0.0f); + glm::quat parentRot(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 parentScale(1.0f); + if (obj.parentId != -1) { + if (SceneObject* parent = findObjectById(obj.parentId)) { + parentPos = parent->position; + parentRot = QuatFromEulerXYZ(parent->rotation); + parentScale = parent->scale; + } + } + updateLocalFromWorld(obj, parentPos, parentRot, parentScale); +} + bool Engine::playAudioFromScript(int id) { SceneObject* obj = findObjectById(id); if (!obj || !obj->hasAudioSource) return false; diff --git a/src/Engine.h b/src/Engine.h index f211ac8..b2f98f7 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -102,6 +102,9 @@ private: bool hierarchyShowTexturePreview = false; bool hierarchyPreviewNearest = false; std::unordered_map texturePreviewFilterOverrides; + bool audioPreviewLoop = false; + bool audioPreviewAutoPlay = false; + std::string audioPreviewSelectedPath; bool isPlaying = false; bool isPaused = false; bool showViewOutput = true; @@ -136,13 +139,17 @@ private: bool specMode = false; bool testMode = false; bool collisionWireframe = false; - // Private methods SceneObject* getSelectedObject(); glm::vec3 getSelectionCenterWorld(bool worldSpace) const; void setPrimarySelection(int id, bool additive = false); void clearSelection(); static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale); + static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& scale); + static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::vec3& rotationDeg, const glm::vec3& scale); + void updateHierarchyWorldTransforms(); + void updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale); + void initializeLocalTransformsFromWorld(int sceneVersion); void importOBJToScene(const std::string& filepath, const std::string& objectName); void importModelToScene(const std::string& filepath, const std::string& objectName); // Assimp import @@ -161,7 +168,8 @@ private: void renderEnvironmentWindow(); void renderCameraWindow(); void renderHierarchyPanel(); - void renderObjectNode(SceneObject& obj, const std::string& filter); + void renderObjectNode(SceneObject& obj, const std::string& filter, + std::vector& ancestorHasNext, bool isLast, int depth); void renderFileBrowserPanel(); void renderMeshBuilderPanel(); void renderInspectorPanel(); @@ -246,10 +254,15 @@ public: bool addRigidbodyImpulseFromScript(int id, const glm::vec3& impulse); bool addRigidbodyTorqueFromScript(int id, const glm::vec3& torque); bool addRigidbodyAngularImpulseFromScript(int id, const glm::vec3& impulse); + bool setRigidbodyYawFromScript(int id, float yawDegrees); + bool raycastClosestFromScript(const glm::vec3& origin, const glm::vec3& dir, float distance, + int ignoreId, glm::vec3* hitPos, glm::vec3* hitNormal, + float* hitDistance) const; // Audio control exposed to scripts bool playAudioFromScript(int id); bool stopAudioFromScript(int id); bool setAudioLoopFromScript(int id, bool loop); bool setAudioVolumeFromScript(int id, float volume); bool setAudioClipFromScript(int id, const std::string& path); + void syncLocalTransform(SceneObject& obj); }; diff --git a/src/PhysicsSystem.cpp b/src/PhysicsSystem.cpp index 8ea2fcd..84bdbca 100644 --- a/src/PhysicsSystem.cpp +++ b/src/PhysicsSystem.cpp @@ -203,6 +203,20 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject& tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic); break; } + case ObjectType::Plane: { + glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f)); + halfExtents.z = std::max(halfExtents.z, 0.01f); + shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true); + tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); + break; + } + case ObjectType::Torus: { + float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; + radius = std::max(radius, 0.01f); + shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true); + tuneShape(shape, radius * 2.0f, isDynamic); + break; + } default: break; } diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index dd1b138..7d92a24 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -258,7 +258,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath, if (!file.is_open()) return false; file << "# Scene File\n"; - file << "version=9\n"; + file << "version=10\n"; file << "nextId=" << nextId << "\n"; file << "objectCount=" << objects.size() << "\n"; file << "\n"; @@ -272,9 +272,9 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "layer=" << obj.layer << "\n"; file << "tag=" << obj.tag << "\n"; file << "parentId=" << obj.parentId << "\n"; - file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n"; - file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n"; - file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n"; + file << "position=" << obj.localPosition.x << "," << obj.localPosition.y << "," << obj.localPosition.z << "\n"; + file << "rotation=" << obj.localRotation.x << "," << obj.localRotation.y << "," << obj.localRotation.z << "\n"; + file << "scale=" << obj.localScale.x << "," << obj.localScale.y << "," << obj.localScale.z << "\n"; file << "hasRigidbody=" << (obj.hasRigidbody ? 1 : 0) << "\n"; if (obj.hasRigidbody) { file << "rbEnabled=" << (obj.rigidbody.enabled ? 1 : 0) << "\n"; @@ -409,7 +409,8 @@ bool SceneSerializer::saveScene(const fs::path& filePath, bool SceneSerializer::loadScene(const fs::path& filePath, std::vector& objects, - int& nextId) { + int& nextId, + int& outVersion) { try { std::ifstream file(filePath); if (!file.is_open()) return false; @@ -417,6 +418,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath, objects.clear(); std::string line; SceneObject* currentObj = nullptr; + int sceneVersion = 9; while (std::getline(file, line)) { size_t first = line.find_first_not_of(" \t\r\n"); @@ -445,7 +447,9 @@ bool SceneSerializer::loadScene(const fs::path& filePath, std::string key = line.substr(0, eqPos); std::string value = line.substr(eqPos + 1); - if (key == "nextId") { + if (key == "version") { + sceneVersion = std::stoi(value); + } else if (key == "nextId") { nextId = std::stoi(value); } else if (currentObj) { if (key == "id") { @@ -474,17 +478,23 @@ bool SceneSerializer::loadScene(const fs::path& filePath, ¤tObj->position.x, ¤tObj->position.y, ¤tObj->position.z); + currentObj->localPosition = currentObj->position; + currentObj->localInitialized = true; } else if (key == "rotation") { sscanf(value.c_str(), "%f,%f,%f", ¤tObj->rotation.x, ¤tObj->rotation.y, ¤tObj->rotation.z); currentObj->rotation = NormalizeEulerDegrees(currentObj->rotation); + currentObj->localRotation = currentObj->rotation; + currentObj->localInitialized = true; } else if (key == "scale") { sscanf(value.c_str(), "%f,%f,%f", ¤tObj->scale.x, ¤tObj->scale.y, ¤tObj->scale.z); + currentObj->localScale = currentObj->scale; + currentObj->localInitialized = true; } else if (key == "hasRigidbody") { currentObj->hasRigidbody = std::stoi(value) != 0; } else if (key == "rbEnabled") { @@ -752,6 +762,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath, } file.close(); + outVersion = sceneVersion; return true; } catch (const std::exception& e) { std::cerr << "Failed to load scene: " << e.what() << std::endl; diff --git a/src/ProjectManager.h b/src/ProjectManager.h index 2032648..ded45da 100644 --- a/src/ProjectManager.h +++ b/src/ProjectManager.h @@ -59,5 +59,6 @@ public: static bool loadScene(const fs::path& filePath, std::vector& objects, - int& nextId); + int& nextId, + int& outVersion); }; diff --git a/src/Rendering.cpp b/src/Rendering.cpp index 5430163..429b07f 100644 --- a/src/Rendering.cpp +++ b/src/Rendering.cpp @@ -232,6 +232,58 @@ std::vector generateCapsule(int segments, int rings) { return triangulated; } +std::vector generateTorus(int segments, int sides) { + std::vector vertices; + float majorRadius = 0.35f; + float minorRadius = 0.15f; + + for (int seg = 0; seg <= segments; ++seg) { + float u = seg * 2.0f * PI / segments; + float cosU = cos(u); + float sinU = sin(u); + + for (int side = 0; side <= sides; ++side) { + float v = side * 2.0f * PI / sides; + float cosV = cos(v); + float sinV = sin(v); + + float x = (majorRadius + minorRadius * cosV) * cosU; + float y = minorRadius * sinV; + float z = (majorRadius + minorRadius * cosV) * sinU; + + glm::vec3 normal = glm::normalize(glm::vec3(cosU * cosV, sinV, sinU * cosV)); + + vertices.push_back(x); + vertices.push_back(y); + vertices.push_back(z); + vertices.push_back(normal.x); + vertices.push_back(normal.y); + vertices.push_back(normal.z); + vertices.push_back((float)seg / segments); + vertices.push_back((float)side / sides); + } + } + + std::vector triangulated; + int stride = sides + 1; + for (int seg = 0; seg < segments; ++seg) { + for (int side = 0; side < sides; ++side) { + int current = seg * stride + side; + int next = current + stride; + + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[current * 8 + i]); + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[next * 8 + i]); + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(current + 1) * 8 + i]); + + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(current + 1) * 8 + i]); + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[next * 8 + i]); + for (int i = 0; i < 8; ++i) triangulated.push_back(vertices[(next + 1) * 8 + i]); + } + } + + return triangulated; +} + // Mesh implementation Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) { vertexCount = dataSizeBytes / (8 * sizeof(float)); @@ -435,6 +487,7 @@ Renderer::~Renderer() { delete sphereMesh; delete capsuleMesh; delete planeMesh; + delete torusMesh; delete skybox; delete postShader; delete brightShader; @@ -458,7 +511,6 @@ Renderer::~Renderer() { releaseRenderTarget(entry.second); } mirrorTargets.clear(); - mirrorSmooth.clear(); if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (viewportTexture) glDeleteTextures(1, &viewportTexture); if (rbo) glDeleteRenderbuffers(1, &rbo); @@ -555,6 +607,8 @@ void Renderer::initialize() { auto capsuleVerts = generateCapsule(); capsuleMesh = new Mesh(capsuleVerts.data(), capsuleVerts.size() * sizeof(float)); planeMesh = new Mesh(mirrorPlaneVertices, sizeof(mirrorPlaneVertices)); + auto torusVerts = generateTorus(); + torusMesh = new Mesh(torusVerts.data(), torusVerts.size() * sizeof(float)); skybox = new Skybox(); @@ -732,14 +786,6 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vectorfirst) == active.end()) { releaseRenderTarget(it->second); - mirrorSmooth.erase(it->first); it = mirrorTargets.erase(it); } else { ++it; @@ -972,9 +1007,15 @@ void Renderer::renderObject(const SceneObject& obj) { case ObjectType::Capsule: capsuleMesh->draw(); break; + case ObjectType::Plane: + if (planeMesh) planeMesh->draw(); + break; case ObjectType::Mirror: if (planeMesh) planeMesh->draw(); break; + case ObjectType::Torus: + if (torusMesh) torusMesh->draw(); + break; case ObjectType::OBJMesh: if (obj.meshId >= 0) { Mesh* objMesh = g_objLoader.getMesh(obj.meshId); @@ -1192,7 +1233,9 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector& scene if (allowHistory) { ensureRenderTarget(historyTarget, width, height); } + ensureRenderTarget(bloomTargetA, width, height); + ensureRenderTarget(bloomTargetB, width, height); if (target.fbo == 0 || target.texture == 0) { if (allowHistory) { displayTexture = sourceTexture; diff --git a/src/Rendering.h b/src/Rendering.h index aa06c6e..fe47ae4 100644 --- a/src/Rendering.h +++ b/src/Rendering.h @@ -13,6 +13,7 @@ extern float vertices[]; // Primitive generation functions std::vector generateSphere(int segments = 32, int rings = 16); std::vector generateCapsule(int segments = 16, int rings = 8); +std::vector generateTorus(int segments = 32, int sides = 16); class Mesh { private: @@ -73,11 +74,6 @@ private: RenderTarget historyTarget; RenderTarget bloomTargetA; RenderTarget bloomTargetB; - struct MirrorSmoothing { - glm::vec2 planar = glm::vec2(0.0f); - bool initialized = false; - }; - std::unordered_map mirrorSmooth; Shader* shader = nullptr; Shader* defaultShader = nullptr; Shader* postShader = nullptr; @@ -108,6 +104,7 @@ private: Mesh* sphereMesh = nullptr; Mesh* capsuleMesh = nullptr; Mesh* planeMesh = nullptr; + Mesh* torusMesh = nullptr; Skybox* skybox = nullptr; unsigned int quadVAO = 0; unsigned int quadVBO = 0; diff --git a/src/SceneObject.h b/src/SceneObject.h index ea36f27..f5ce0c8 100644 --- a/src/SceneObject.h +++ b/src/SceneObject.h @@ -14,7 +14,9 @@ enum class ObjectType { AreaLight, Camera, PostFXNode, - Mirror + Mirror, + Plane, + Torus }; struct MaterialProperties { @@ -161,6 +163,10 @@ public: glm::vec3 position; glm::vec3 rotation; glm::vec3 scale; + glm::vec3 localPosition; + glm::vec3 localRotation; + glm::vec3 localScale; + bool localInitialized = false; int id; int parentId = -1; std::vector childIds; @@ -190,5 +196,14 @@ public: AudioSourceComponent audioSource; SceneObject(const std::string& name, ObjectType type, int id) - : name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {} + : name(name), + type(type), + position(0.0f), + rotation(0.0f), + scale(1.0f), + localPosition(0.0f), + localRotation(0.0f), + localScale(1.0f), + localInitialized(true), + id(id) {} }; diff --git a/src/ScriptRuntime.cpp b/src/ScriptRuntime.cpp index 9cfb32e..0914330 100644 --- a/src/ScriptRuntime.cpp +++ b/src/ScriptRuntime.cpp @@ -87,6 +87,12 @@ bool ScriptContext::IsInLayer(int layer) const { void ScriptContext::SetPosition(const glm::vec3& pos) { if (object) { object->position = pos; + if (engine) { + engine->syncLocalTransform(*object); + } else { + object->localPosition = object->position; + object->localInitialized = true; + } MarkDirty(); } } @@ -94,6 +100,12 @@ void ScriptContext::SetPosition(const glm::vec3& pos) { void ScriptContext::SetRotation(const glm::vec3& rot) { if (object) { object->rotation = NormalizeEulerDegrees(rot); + if (engine) { + engine->syncLocalTransform(*object); + } else { + object->localRotation = object->rotation; + object->localInitialized = true; + } MarkDirty(); if (engine && HasRigidbody()) { engine->teleportPhysicsActorFromScript(object->id, object->position, object->rotation); @@ -104,6 +116,12 @@ void ScriptContext::SetRotation(const glm::vec3& rot) { void ScriptContext::SetScale(const glm::vec3& scl) { if (object) { object->scale = scl; + if (engine) { + engine->syncLocalTransform(*object); + } else { + object->localScale = object->scale; + object->localInitialized = true; + } MarkDirty(); } } @@ -152,9 +170,22 @@ bool ScriptContext::AddRigidbodyAngularImpulse(const glm::vec3& impulse) { return engine->addRigidbodyAngularImpulseFromScript(object->id, impulse); } +bool ScriptContext::SetRigidbodyYaw(float yawDegrees) { + if (!engine || !object || !HasRigidbody()) return false; + return engine->setRigidbodyYawFromScript(object->id, yawDegrees); +} + +bool ScriptContext::RaycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance, + glm::vec3* hitPos, glm::vec3* hitNormal, float* hitDistance) const { + if (!engine) return false; + int ignoreId = object ? object->id : -1; + return engine->raycastClosestFromScript(origin, dir, distance, ignoreId, hitPos, hitNormal, hitDistance); +} + bool ScriptContext::SetRigidbodyRotation(const glm::vec3& rotDeg) { if (!engine || !object || !HasRigidbody()) return false; object->rotation = NormalizeEulerDegrees(rotDeg); + engine->syncLocalTransform(*object); MarkDirty(); return engine->teleportPhysicsActorFromScript(object->id, object->position, object->rotation); } @@ -163,6 +194,7 @@ bool ScriptContext::TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rot if (!engine || !object) return false; object->position = pos; object->rotation = NormalizeEulerDegrees(rotDeg); + engine->syncLocalTransform(*object); MarkDirty(); return engine->teleportPhysicsActorFromScript(object->id, pos, object->rotation); } diff --git a/src/ScriptRuntime.h b/src/ScriptRuntime.h index 3e24a71..f033d07 100644 --- a/src/ScriptRuntime.h +++ b/src/ScriptRuntime.h @@ -45,6 +45,10 @@ struct ScriptContext { bool AddRigidbodyImpulse(const glm::vec3& impulse); bool AddRigidbodyTorque(const glm::vec3& torque); bool AddRigidbodyAngularImpulse(const glm::vec3& impulse); + bool SetRigidbodyYaw(float yawDegrees); + bool RaycastClosest(const glm::vec3& origin, const glm::vec3& dir, float distance, + glm::vec3* hitPos = nullptr, glm::vec3* hitNormal = nullptr, + float* hitDistance = nullptr) const; bool SetRigidbodyRotation(const glm::vec3& rotDeg); bool TeleportRigidbody(const glm::vec3& pos, const glm::vec3& rotDeg); // Audio helpers diff --git a/src/WinView/Window.cpp b/src/WinView/Window.cpp index af7c50d..97b561d 100644 --- a/src/WinView/Window.cpp +++ b/src/WinView/Window.cpp @@ -1,5 +1,6 @@ #include "../../include/Window/Window.h" #include "../../include/ThirdParty/stb_image.h" +#include int width, height, channels; @@ -7,7 +8,17 @@ GLFWwindow *Window::makeWindow() { unsigned char *pixels = stbi_load("Resources/Engine-Root/Modu-Logo.png", &width, &height, &channels, 4); #if defined(__linux__) - setenv("XDG_SESSION_TYPE", "x11", 1); + const char *wayland_display = std::getenv("WAYLAND_DISPLAY"); + const char *x11_display = std::getenv("DISPLAY"); + if (wayland_display && *wayland_display && + glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) { + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_WAYLAND); + } else if (x11_display && *x11_display && + glfwPlatformSupported(GLFW_PLATFORM_X11)) { + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); + } else { + glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM); + } #endif if (!glfwInit()) {