feat: add basic canvas / improve inspector

This commit is contained in:
2026-10-18 20:42:04 +02:00
parent 7df65f52b5
commit ad16daea7c
47 changed files with 3142 additions and 405 deletions
@@ -40,6 +40,7 @@ public:
void AddImpulse(const glm::vec3& impulse);
void SetLinearVelocity(const glm::vec3& velocity);
void Teleport(const glm::vec3& worldPosition);
[[nodiscard]] glm::vec3 GetLinearVelocity() const;
[[nodiscard]] RigidbodyType GetType() const { return m_Type; }
@@ -0,0 +1,52 @@
#ifndef DESTRUM_CANVAS_H
#define DESTRUM_CANVAS_H
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
enum class CanvasScaleMode {
ConstantPixelSize,
ScaleWithScreenHeight,
};
// Draws the UIGraphics on its object and every active child, depth first, so later siblings draw on top.
// A child with its own Canvas is skipped and draws itself.
class Canvas final : public Component {
public:
explicit Canvas(GameObject& parent);
std::string GetTypeName() const override { return "Canvas"; }
void Start() override;
void Render(const RenderContext& ctx) override;
void Draw(const glm::vec2& screenSize);
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
[[nodiscard]] float GetScale(const glm::vec2& screenSize) const;
[[nodiscard]] static Canvas* FindFor(GameObject& object);
void SetScaleMode(CanvasScaleMode mode) { m_ScaleMode = mode; }
void SetReferenceHeight(float height) { m_ReferenceHeight = height; }
void SetScaleFactor(float factor) { m_ScaleFactor = factor; }
void SetDrawDebugRects(bool draw) { m_DrawDebugRects = draw; }
[[nodiscard]] CanvasScaleMode GetScaleMode() const { return m_ScaleMode; }
[[nodiscard]] float GetReferenceHeight() const { return m_ReferenceHeight; }
[[nodiscard]] float GetScaleFactor() const { return m_ScaleFactor; }
private:
void DrawObject(GameObject& object, const UIRect& rect, float scale, bool visible) const;
CanvasScaleMode m_ScaleMode{CanvasScaleMode::ScaleWithScreenHeight};
float m_ReferenceHeight{1080.0f};
float m_ScaleFactor{1.0f};
bool m_DrawDebugRects{false};
};
#endif // DESTRUM_CANVAS_H
@@ -0,0 +1,68 @@
#ifndef DESTRUM_RECTTRANSFORM_H
#define DESTRUM_RECTTRANSFORM_H
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
// Screen space rectangle in draw image pixels, origin top left, y down
struct UIRect {
glm::vec2 min{0.0f};
glm::vec2 size{0.0f};
[[nodiscard]] glm::vec2 Max() const { return min + size; }
[[nodiscard]] glm::vec2 Center() const { return min + size * 0.5f; }
[[nodiscard]] bool Contains(const glm::vec2& point) const;
};
// Anchors and pivot are normalized (0,0 top left), position and size are in canvas units
class RectTransform final : public Component {
public:
explicit RectTransform(GameObject& parent);
std::string GetTypeName() const override { return "RectTransform"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
[[nodiscard]] UIRect ComputeRect(const UIRect& parentRect, float scale) const;
void SetAnchors(const glm::vec2& anchorMin, const glm::vec2& anchorMax);
void SetAnchor(const glm::vec2& anchor) { SetAnchors(anchor, anchor); }
void SetPivot(const glm::vec2& pivot) { m_Pivot = pivot; }
void SetPosition(const glm::vec2& position) { m_Position = position; }
void SetSize(const glm::vec2& size) { m_Size = size; }
void Stretch();
[[nodiscard]] const glm::vec2& GetAnchorMin() const { return m_AnchorMin; }
[[nodiscard]] const glm::vec2& GetAnchorMax() const { return m_AnchorMax; }
[[nodiscard]] const glm::vec2& GetPivot() const { return m_Pivot; }
[[nodiscard]] const glm::vec2& GetPosition() const { return m_Position; }
[[nodiscard]] const glm::vec2& GetSize() const { return m_Size; }
// Where the canvas last drew this object
[[nodiscard]] const UIRect& GetScreenRect() const { return m_ScreenRect; }
[[nodiscard]] const UIRect& GetParentRect() const { return m_ParentRect; }
void SetScreenRect(const UIRect& rect, const UIRect& parentRect, float scale)
{
m_ScreenRect = rect;
m_ParentRect = parentRect;
m_LastScale = scale;
}
void ApplyAnchorPreset(const glm::vec2& preset);
private:
glm::vec2 m_AnchorMin{0.5f};
glm::vec2 m_AnchorMax{0.5f};
glm::vec2 m_Pivot{0.5f};
glm::vec2 m_Position{0.0f};
glm::vec2 m_Size{100.0f};
UIRect m_ScreenRect{};
UIRect m_ParentRect{};
float m_LastScale{1.0f};
};
#endif // DESTRUM_RECTTRANSFORM_H
@@ -0,0 +1,23 @@
#ifndef DESTRUM_UIGRAPHIC_H
#define DESTRUM_UIGRAPHIC_H
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
// Anything a Canvas draws into the rect of the object it sits on
class UIGraphic : public Component {
public:
virtual void DrawUI(const UIRect& rect, float scale) = 0;
[[nodiscard]] const glm::vec4& GetColor() const { return m_Color; }
void SetColor(const glm::vec4& color) { m_Color = color; }
protected:
UIGraphic(GameObject& parent, const std::string& name) : Component(parent, name) {}
glm::vec4 m_Color{1.0f};
};
#endif // DESTRUM_UIGRAPHIC_H
@@ -0,0 +1,51 @@
#ifndef DESTRUM_UIIMAGE_H
#define DESTRUM_UIIMAGE_H
#include <string>
#include <destrum/Components/UI/UIGraphic.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/ids.h>
enum class UIImageShape {
Rect,
Ellipse,
};
class UIImage final : public UIGraphic {
public:
explicit UIImage(GameObject& parent, const glm::vec4& color = glm::vec4{1.0f});
std::string GetTypeName() const override { return "UIImage"; }
void DrawUI(const UIRect& rect, float scale) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetShape(UIImageShape shape) { m_Shape = shape; }
void SetOutline(float thickness) { m_Outline = thickness; }
void SetCornerRadius(float radius) { m_CornerRadius = radius; }
void SetTexture(const std::string& assetPath) { m_TexturePath = assetPath; }
void SetFilter(TextureFilter filter) { m_Filter = filter; }
[[nodiscard]] UIImageShape GetShape() const { return m_Shape; }
[[nodiscard]] float GetOutline() const { return m_Outline; }
[[nodiscard]] float GetCornerRadius() const { return m_CornerRadius; }
[[nodiscard]] const std::string& GetTexture() const { return m_TexturePath; }
private:
ImageID ResolveTexture();
UIImageShape m_Shape{UIImageShape::Rect};
float m_Outline{0.0f};
float m_CornerRadius{0.0f};
std::string m_TexturePath;
TextureFilter m_Filter{TextureFilter::Linear};
std::string m_LoadedTexturePath;
ImageID m_Texture{NULL_IMAGE_ID};
};
#endif // DESTRUM_UIIMAGE_H
@@ -0,0 +1,52 @@
#ifndef DESTRUM_UITEXT_H
#define DESTRUM_UITEXT_H
#include <string>
#include <destrum/Components/UI/UIGraphic.h>
class Font;
enum class UITextAlign {
Start,
Center,
End,
};
class UIText final : public UIGraphic {
public:
explicit UIText(GameObject& parent, std::string text = {}, float fontSize = 24.0f);
std::string GetTypeName() const override { return "UIText"; }
void DrawUI(const UIRect& rect, float scale) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetText(std::string text) { m_Text = std::move(text); }
void SetFont(std::string assetPath) { m_FontPath = std::move(assetPath); }
void SetFontSize(float size) { m_FontSize = size; }
void SetOutline(float thickness, const glm::vec4& color);
void SetAlignment(UITextAlign horizontal, UITextAlign vertical);
[[nodiscard]] const std::string& GetText() const { return m_Text; }
[[nodiscard]] const std::string& GetFont() const { return m_FontPath; }
[[nodiscard]] float GetFontSize() const { return m_FontSize; }
[[nodiscard]] UITextAlign GetHorizontalAlign() const { return m_HorizontalAlign; }
[[nodiscard]] UITextAlign GetVerticalAlign() const { return m_VerticalAlign; }
[[nodiscard]] glm::vec2 PlaceText(const UIRect& rect, const glm::vec2& textSize) const;
private:
std::string m_Text;
std::string m_FontPath;
float m_FontSize{24.0f};
float m_Outline{0.0f};
glm::vec4 m_OutlineColor{0.0f, 0.0f, 0.0f, 1.0f};
UITextAlign m_HorizontalAlign{UITextAlign::Center};
UITextAlign m_VerticalAlign{UITextAlign::Center};
};
#endif // DESTRUM_UITEXT_H
@@ -0,0 +1,29 @@
#ifndef DESTRUM_FONTCACHE_H
#define DESTRUM_FONTCACHE_H
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <destrum/Graphics/Resources/Font.h>
#include <destrum/Singleton.h>
class FontCache final : public Singleton<FontCache> {
public:
friend class Singleton<FontCache>;
static constexpr std::string_view DefaultFont = "engine://fonts/Roboto-Medium.ttf";
[[nodiscard]] const Font* Get(std::string_view assetPath);
// The atlases belong to RenderResources, call before it is cleaned up
void Clear() { fonts.clear(); }
private:
FontCache() = default;
std::unordered_map<std::string, std::unique_ptr<Font>> fonts;
};
#endif // DESTRUM_FONTCACHE_H
@@ -59,6 +59,9 @@ public:
[[nodiscard]] const std::vector<Transform*>& GetChildren() const;
[[nodiscard]] GameObject* GetOwner() const;
[[nodiscard]] int GetSiblingIndex() const;
void SetSiblingIndex(int index);
private:
void AddChild(Transform* transform);
void RemoveChild(Transform* transform);
+10
View File
@@ -8,6 +8,7 @@
#include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Util/SceneInspector.h>
#include "destrum/Physics/JoltPhysicsWorld.h"
#include "destrum/Physics/PhysicsSceneBridge.h"
@@ -66,7 +67,15 @@ public:
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetObjects() const {
return m_objects;
}
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetPendingAdditions() const {
return m_pendingAdditions;
}
[[nodiscard]] GameObject* FindGameObject(ObjectId id) const;
// Reparents object (nullptr is the scene root) and puts it right before insertBefore, or last when that is
// nullptr. Also reorders the object list so the order survives a save. False if the move isn't allowed.
bool MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore = nullptr);
void SetSelectedObject(ObjectId id) { m_SelectedObjectId = id; }
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
void SetRegisterBindings(std::function<void()> registerBindings) {
@@ -150,6 +159,7 @@ private:
//Imgui vars
bool m_ShowDemoWindow{false};
ObjectId m_SelectedObjectId{InvalidObjectId};
SceneInspector m_Inspector;
ObjectId m_CurrentCameraId{InvalidObjectId};
@@ -12,6 +12,10 @@
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Components/UI/UIImage.h>
#include <destrum/Components/UI/UIText.h>
#define DESTRUM_ENGINE_COMPONENTS(X) \
X(MeshRendererComponent) \
@@ -25,5 +29,9 @@
X(BoxCollider) \
X(SphereCollider) \
X(CapsuleCollider) \
X(Canvas) \
X(RectTransform) \
X(UIImage) \
X(UIText) \
#endif // DESTRUM_ENGINECOMPONENTLIST_H
+16 -8
View File
@@ -3,14 +3,19 @@
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/ObjectRef.h>
class GameObject;
class Scene;
using ImGuiInputTextFlags = int;
namespace ImGuiUtils {
[[nodiscard]] bool DrawVec3Control(
@@ -18,14 +23,17 @@ namespace ImGuiUtils {
glm::vec3& value,
float speed = 0.1f);
void RenderSceneInspector(
std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objects,
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
ObjectId& selectedObjectId,
const std::function<GameObject*()>& createGameObject,
const std::function<void()>& drawSceneSettings,
bool* open = nullptr);
inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
bool InputStringMultiline(const char* label, std::string& value);
// Combo over the scene's objects, filter can hide objects that can't be picked
bool DrawObjectRefPicker(
const char* label,
ObjectRef& ref,
const Scene& scene,
const std::function<bool(const GameObject&)>& filter = {});
}
#endif // DESTRUM_IMGUIUTILS_H
+27
View File
@@ -0,0 +1,27 @@
#ifndef DESTRUM_RECTGIZMO_H
#define DESTRUM_RECTGIZMO_H
#include <glm/glm.hpp>
class GameObject;
class RectGizmo final {
public:
[[nodiscard]] static bool CanManipulate(GameObject& object);
// drawImageSize is what the canvas renders into, it is stretched over the main ImGui viewport
bool Manipulate(GameObject& object, const glm::vec2& drawImageSize);
[[nodiscard]] bool IsUsing() const { return m_ActiveHandle >= 0; }
bool snap{false};
float snapStep{5.0f};
private:
int m_ActiveHandle{-1};
glm::vec2 m_DragStartMouse{0.0f};
glm::vec2 m_DragStartPosition{0.0f};
glm::vec2 m_DragStartSize{0.0f};
};
#endif // DESTRUM_RECTGIZMO_H
@@ -0,0 +1,60 @@
#ifndef DESTRUM_SCENEINSPECTOR_H
#define DESTRUM_SCENEINSPECTOR_H
#include <functional>
#include <string>
#include <unordered_set>
#include <nlohmann/json.hpp>
#include <destrum/ObjectModel/ObjectId.h>
class Component;
class GameObject;
class Scene;
class SceneInspector final {
public:
void Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open);
private:
struct PendingMove {
ObjectId object{InvalidObjectId};
ObjectId parent{InvalidObjectId};
ObjectId anchor{InvalidObjectId};
bool after{false};
};
void DrawHierarchy(Scene& scene, ObjectId& selectedObjectId);
void DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId, std::unordered_set<const GameObject*>& visited);
void DrawRenameField(GameObject& object);
void DrawNodeDragDrop(GameObject& object);
void DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId);
void HandleShortcuts(Scene& scene, ObjectId& selectedObjectId);
void ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId);
void DrawObject(GameObject& object);
void DrawTransform(GameObject& object);
void DrawComponents(GameObject& object);
void DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable);
void DrawAddComponent(GameObject& object);
void BeginRename(const GameObject& object);
std::string m_Filter;
std::string m_ComponentFilter;
ObjectId m_RenamingId{InvalidObjectId};
std::string m_RenameBuffer;
bool m_FocusRename{false};
ObjectId m_OpenNextFrame{InvalidObjectId};
PendingMove m_PendingMove;
ObjectId m_PendingDelete{InvalidObjectId};
ObjectId m_PendingDuplicate{InvalidObjectId};
std::string m_ClipboardType;
nlohmann::json m_Clipboard;
};
#endif // DESTRUM_SCENEINSPECTOR_H