diff --git a/Scenes/test.tscn b/Scenes/test.tscn
index 17ef2f97..10675170 100644
--- a/Scenes/test.tscn
+++ b/Scenes/test.tscn
@@ -1,7 +1,6 @@
-[gd_scene load_steps=19 format=4 uid="uid://bv451a8wgty4u"]
+[gd_scene load_steps=18 format=4 uid="uid://bv451a8wgty4u"]
[ext_resource type="PackedScene" uid="uid://bghghp5ep4w2j" path="res://Scenes/player.tscn" id="2_8mh54"]
-[ext_resource type="PackedScene" uid="uid://cxmcqehjjy82j" path="res://Scenes/reisen.tscn" id="3_8k37m"]
[ext_resource type="PackedScene" uid="uid://rp4jhx0tuh24" path="res://Scenes/fragola.tscn" id="4_s7wq6"]
[ext_resource type="PackedScene" uid="uid://bj28qiai2x2ar" path="res://Scenes/Barrel.tscn" id="5_3uba3"]
[ext_resource type="PackedScene" uid="uid://uaf5r6cd71hu" path="res://Scenes/Furniture/LargeTank.tscn" id="6_nkauc"]
@@ -280,6 +279,7 @@ position = Vector2(-766, -74)
[node name="CameraController" type="Camera2D" parent="."]
script = ExtResource("6_t8ide")
+pixel_snap = false
[node name="Player" parent="." instance=ExtResource("2_8mh54")]
position = Vector2(-790, -165)
@@ -301,8 +301,5 @@ polygon = PackedVector2Array(95, 57, 46, 58, -83, 61, -91, -37, 88, -37, 114, -1
[node name="Fragola" parent="." instance=ExtResource("4_s7wq6")]
position = Vector2(-743, -117)
-[node name="CharacterBody2D" parent="." instance=ExtResource("3_8k37m")]
-position = Vector2(78, -15)
-
[node name="Enemy" parent="." instance=ExtResource("18_ixcwn")]
position = Vector2(-687, -10)
diff --git a/Scripts/Enemy.cs b/Scripts/Enemy.cs
index 88e91cc5..b833d1bb 100644
--- a/Scripts/Enemy.cs
+++ b/Scripts/Enemy.cs
@@ -27,6 +27,7 @@ public partial class Enemy : Area2D, IDestructible
private Timer _cooldownTimer;
+ [DebugGUIPrint]
private int _ammo = 0;
// Called when the node enters the scene tree for the first time.
diff --git a/Scripts/MainMenu.cs b/Scripts/MainMenu.cs
index 9e7151a2..1d040c01 100644
--- a/Scripts/MainMenu.cs
+++ b/Scripts/MainMenu.cs
@@ -9,7 +9,7 @@ public partial class MainMenu : Control
[Export]
public string MainMenuScene { get; set; }
-
+
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
diff --git a/Scripts/PlayerMovement.cs b/Scripts/PlayerMovement.cs
index ebd4423f..d6521358 100644
--- a/Scripts/PlayerMovement.cs
+++ b/Scripts/PlayerMovement.cs
@@ -39,6 +39,7 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
[Export] public float Health = 4f;
+ [DebugGUIPrint]
private float _currentHealth = 0f;
[Export] public double RateOfFire = 0.4f;
diff --git a/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs b/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs
new file mode 100644
index 00000000..6e0983d4
--- /dev/null
+++ b/addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs
@@ -0,0 +1,33 @@
+using Godot;
+using System;
+
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public class DebugGUIGraphAttribute : Attribute
+{
+ public float min { get; private set; }
+ public float max { get; private set; }
+ public Color color { get; private set; }
+ public int group { get; private set; }
+ public bool autoScale { get; private set; }
+
+ public DebugGUIGraphAttribute(
+ // Line color
+ float r = 1,
+ float g = 1,
+ float b = 1,
+ // Values at top/bottom of graph
+ float min = 0,
+ float max = 1,
+ // Offset position on screen
+ int group = 0,
+ // Auto-adjust min/max to fit the values
+ bool autoScale = true
+ )
+ {
+ color = new Color(r, g, b, 0.9f);
+ this.min = min;
+ this.max = max;
+ this.group = group;
+ this.autoScale = autoScale;
+ }
+}
\ No newline at end of file
diff --git a/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs b/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs
new file mode 100644
index 00000000..c20ac6e9
--- /dev/null
+++ b/addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs
@@ -0,0 +1,4 @@
+using System;
+
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public class DebugGUIPrintAttribute : Attribute { }
\ No newline at end of file
diff --git a/addons/DebugGUI/DebugGUI.cs b/addons/DebugGUI/DebugGUI.cs
new file mode 100644
index 00000000..cf5d8938
--- /dev/null
+++ b/addons/DebugGUI/DebugGUI.cs
@@ -0,0 +1,313 @@
+using Godot;
+using System.IO;
+using WeavUtils;
+
+public partial class DebugGUI : Control
+{
+ // Other scripts may use us right off the bat, so we make sure we initialize first
+ public DebugGUI()
+ {
+ ProcessPhysicsPriority = int.MinValue;
+ }
+
+ static DebugGUI Instance;
+
+ #region Settings
+
+ public static class Settings
+ {
+ const string DEBUGGUI_SETTINGS_DIR = "DebugGUI/Settings/";
+
+ public static void Init()
+ {
+ if (!Engine.IsEditorHint()) return;
+
+ // Inits defaults or load current if present
+ Load();
+
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", enableGraphs);
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", enableLogs);
+
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", backgroundColor);
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", scrubberColor);
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", graphWidth);
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", graphHeight);
+ ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", temporaryLogLifetime);
+
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", true);
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", true);
+
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", true);
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", true);
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", true);
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", true);
+ ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", true);
+
+ var err = ProjectSettings.Save();
+ if(err != Error.Ok)
+ {
+ GD.PrintErr(err);
+ }
+ }
+
+ public static void Load()
+ {
+ textFont = ThemeDB.FallbackFont;
+
+ enableGraphs = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}",
+ true
+ ).AsBool();
+ enableLogs = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}",
+ true
+ ).AsBool();
+
+ backgroundColor = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}",
+ new Color(0f, 0f, 0f, 0.7f)
+ ).AsColor();
+ scrubberColor = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}",
+ new Color(1f, 1f, 0f, 0.7f)
+ ).AsColor();
+ graphWidth = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}",
+ 300
+ ).AsInt32();
+ graphHeight = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}",
+ 100
+ ).AsInt32();
+ temporaryLogLifetime = ProjectSettings.GetSetting(
+ $"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}",
+ 5
+ ).AsDouble();
+ }
+
+ public static bool enableGraphs;
+ public static bool enableLogs;
+
+ public static Color backgroundColor;
+ public static Color scrubberColor;
+ public static int graphWidth;
+ public static int graphHeight;
+ public static double temporaryLogLifetime;
+
+ public static Font textFont;
+ }
+
+ #endregion
+
+ #region Graph
+
+ ///
+ /// Set the properties of a graph.
+ ///
+ /// The graph's key
+ /// The graph's label
+ /// Value at the bottom of the graph box
+ /// Value at the top of the graph box
+ /// The graph's ordinal position on screen
+ /// The graph's color
+ public static void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale)
+ {
+ if (Settings.enableGraphs)
+ Instance?.graphWindow.SetGraphProperties(key, label, min, max, group, color, autoScale);
+ }
+
+ ///
+ /// Set the properties of a graph.
+ ///
+ /// The graph's key
+ /// The graph's label
+ /// Value at the bottom of the graph box
+ /// Value at the top of the graph box
+ /// The graph's ordinal position on screen
+ /// The graph's color
+ public static void SetGraphProperties(GodotObject key, string label, float min, float max, int group, Color color, bool autoScale)
+ {
+ SetGraphProperties((object)key, label, min, max, group, color, autoScale);
+ }
+
+ ///
+ /// Add a data point to a graph.
+ ///
+ /// The graph's key
+ /// Value to be added
+ public static void Graph(object key, float val)
+ {
+ if (Settings.enableGraphs)
+ Instance?.graphWindow.Graph(key, val);
+ }
+
+ ///
+ /// Add a data point to a graph.
+ ///
+ /// The graph's key
+ /// Value to be added
+ public static void Graph(GodotObject key, float val)
+ {
+ Graph((object)key, val);
+ }
+
+ ///
+ /// Remove an existing graph.
+ ///
+ /// The graph's key
+ public static void RemoveGraph(object key)
+ {
+ if (Settings.enableGraphs)
+ Instance?.graphWindow.RemoveGraph(key);
+ }
+
+ ///
+ /// Remove an existing graph.
+ ///
+ /// The graph's key
+ public static void RemoveGraph(GodotObject key)
+ {
+ RemoveGraph((object)key);
+ }
+
+ ///
+ /// Resets a graph's data.
+ ///
+ /// The graph's key
+ public static void ClearGraph(object key)
+ {
+ if (Settings.enableGraphs)
+ Instance?.graphWindow.ClearGraph(key);
+ }
+
+ ///
+ /// Resets a graph's data.
+ ///
+ /// The graph's key
+ public static void ClearGraph(GodotObject key)
+ {
+ ClearGraph((object)key);
+ }
+
+ ///
+ /// Export graphs to a json file. See path in log.
+ ///
+ public static void ExportGraphs()
+ {
+ if (Instance == null || !Settings.enableGraphs)
+ return;
+
+ string dateTimeStr = Time.GetDatetimeStringFromSystem().Replace(':', '-');
+ string filename = $"debuggui_graph_export_{dateTimeStr}.json";
+
+ using var file = Godot.FileAccess.Open(
+ "user://" + filename,
+ Godot.FileAccess.ModeFlags.Write
+ );
+
+ if (file == null)
+ {
+ GD.Print("DebugGUI graph export failed: " + Godot.FileAccess.GetOpenError());
+ }
+ else
+ {
+ file.StoreString(Instance.graphWindow.ToJson());
+ GD.Print($"Wrote graph data to {Path.Combine(OS.GetUserDataDir(), filename)}");
+ }
+ }
+
+ #endregion
+
+ #region Log
+
+ ///
+ /// Create or update an existing message with the same key.
+ ///
+ public static void LogPersistent(object key, string message)
+ {
+ if (Settings.enableLogs)
+ Instance?.logWindow.LogPersistent(key, message);
+ }
+
+ ///
+ /// Create or update an existing message with the same key.
+ ///
+ public static void LogPersistent(GodotObject key, string message)
+ {
+ LogPersistent((object)key, message);
+ }
+
+ ///
+ /// Remove an existing persistent message.
+ ///
+ public static void RemovePersistent(object key)
+ {
+ if (Settings.enableLogs)
+ Instance?.logWindow.RemovePersistent(key);
+ }
+
+ ///
+ /// Remove an existing persistent message.
+ ///
+ public static void RemovePersistent(GodotObject key)
+ {
+ RemovePersistent((object)key);
+ }
+
+ ///
+ /// Clears all persistent logs.
+ ///
+ public static void ClearPersistent()
+ {
+ if (Settings.enableLogs)
+ Instance?.logWindow.ClearPersistent();
+ }
+
+ ///
+ /// Print a temporary message.
+ ///
+ public static void Log(object message)
+ {
+ Log(message.ToString());
+ }
+
+ ///
+ /// Print a temporary message.
+ ///
+ public static void Log(string message)
+ {
+ if (Settings.enableLogs)
+ Instance?.logWindow.Log(message);
+ }
+
+ #endregion
+
+ ///
+ /// Re-scans for DebugGUI attribute holders (i.e. [DebugGUIGraph] and [DebugGUIPrint])
+ ///
+ public static void ForceReinitializeAttributes()
+ {
+ if (Instance == null) return;
+
+ Instance.graphWindow.ReinitializeAttributes();
+ Instance.logWindow.ReinitializeAttributes();
+ }
+
+ GraphWindow graphWindow;
+ LogWindow logWindow;
+
+ public override void _Ready()
+ {
+ Instance = this;
+ Settings.Load();
+
+ if (Settings.enableGraphs)
+ {
+ AddChild(graphWindow = new());
+ }
+ if (Settings.enableGraphs)
+ {
+ AddChild(logWindow = new());
+ }
+ }
+}
diff --git a/addons/DebugGUI/DebugGUISettingsInitializer.cs b/addons/DebugGUI/DebugGUISettingsInitializer.cs
new file mode 100644
index 00000000..bbbcd885
--- /dev/null
+++ b/addons/DebugGUI/DebugGUISettingsInitializer.cs
@@ -0,0 +1,20 @@
+#if TOOLS
+using Godot;
+
+[Tool]
+public partial class DebugGUISettingsInitializer : EditorPlugin
+{
+ const string DEBUGGUI_RES_PATH = "res://addons/DebugGUI/DebugGUI.cs";
+
+ public override void _EnterTree()
+ {
+ DebugGUI.Settings.Init();
+ AddAutoloadSingleton(nameof(DebugGUI), DEBUGGUI_RES_PATH);
+ }
+
+ public override void _ExitTree()
+ {
+ RemoveAutoloadSingleton(nameof(DebugGUI));
+ }
+}
+#endif
diff --git a/addons/DebugGUI/Examples/DebugGUIExamples.cs b/addons/DebugGUI/Examples/DebugGUIExamples.cs
new file mode 100644
index 00000000..89fc7d25
--- /dev/null
+++ b/addons/DebugGUI/Examples/DebugGUIExamples.cs
@@ -0,0 +1,148 @@
+using Godot;
+using System.Collections.Generic;
+using System.Linq;
+
+public partial class DebugGUIExamples : Node
+{
+ /* * * *
+ *
+ * [DebugGUIGraph]
+ * Renders the variable in a graph on-screen. Attribute based graphs will updates every _Process.
+ * Lets you optionally define:
+ * max, min - The range of displayed values
+ * r, g, b - The RGB color of the graph (0~1)
+ * group - Graphs can be grouped into the same window and overlaid
+ * autoScale - If true the graph will readjust min/max to fit the data
+ *
+ * [DebugGUIPrint]
+ * Draws the current variable continuously on-screen as
+ * $"{GameObject name} {variable name}: {value}"
+ *
+ * For more control, these features can be accessed manually.
+ * DebugGUI.SetGraphProperties(key, ...) - Set the properties of the graph with the provided key
+ * DebugGUI.Graph(key, value) - Push a value to the graph
+ * DebugGUI.LogPersistent(key, value) - Print a persistent log entry on screen
+ * DebugGUI.Log(value) - Print a temporary log entry on screen
+ *
+ * See DebugGUI.cs for more info
+ *
+ * * * */
+
+ // Disable Field Unused warning
+#pragma warning disable 0414
+
+ // Works with regular fields
+ [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 0, autoScale: true)]
+ float SinField;
+
+ // As well as properties
+ [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 1, autoScale: true)]
+ float CosProperty { get { return Mathf.Cos(time * 6); } }
+
+ // Also works for expression-bodied properties
+ [DebugGUIGraph(min: -1, max: 1, r: 1, g: 0.3f, b: 1)]
+ float SinProperty => Mathf.Sin((time + Mathf.Pi / 2) * 6);
+
+ // User inputs, print and graph in one!
+ [DebugGUIPrint, DebugGUIGraph(group: 1, r: 1, g: 0.3f, b: 0.3f)]
+ float mouseX;
+ [DebugGUIPrint, DebugGUIGraph(group: 1, r: 0, g: 1, b: 0)]
+ float mouseY;
+
+ Queue deltaTimeBuffer = new();
+ double smoothDeltaTime => deltaTimeBuffer.Sum() / deltaTimeBuffer.Count;
+ float time;
+ float physicsTime;
+ bool wasMouseDown;
+
+ public override void _Ready()
+ {
+
+ // Init smooth DT
+ for (int i = 0; i < 10; i++)
+ {
+ deltaTimeBuffer.Enqueue(0);
+ }
+
+ // Log (as opposed to LogPersistent) will disappear automatically after some time.
+ DebugGUI.Log("Hello! I will disappear after some time!");
+
+ // Set up graph properties using our graph keys
+ DebugGUI.SetGraphProperties("smoothFrameRate", "SmoothFPS", 0, 200, 2, new Color(0, 1, 1), false);
+ DebugGUI.SetGraphProperties("frameRate", "FPS", 0, 200, 2, new Color(1, 0.5f, 1), false);
+ DebugGUI.SetGraphProperties("fixedFrameRateSin", "FixedSin", -1, 1, 3, new Color(1, 1, 0), true);
+ }
+
+ public override void _Process(double delta)
+ {
+ time += (float)delta;
+
+ // Update smooth delta time queue
+ deltaTimeBuffer.Dequeue();
+ deltaTimeBuffer.Enqueue(delta);
+
+ // Update the fields our attributes are graphing
+ SinField = Mathf.Sin(time * 6);
+
+ // Update graphed mouse XY values
+ var mousePos = GetViewport().GetMousePosition();
+ var viewportRect = GetViewport().GetVisibleRect();
+ mouseX = Mathf.Clamp(mousePos.X, 0, viewportRect.Size.X);
+ mouseY = Mathf.Clamp(mousePos.Y, 0, viewportRect.Size.Y);
+
+ // Manual persistent logging
+ DebugGUI.LogPersistent("smoothFrameRate", "SmoothFPS: " + (1 / smoothDeltaTime).ToString("F3"));
+ DebugGUI.LogPersistent("frameRate", "FPS: " + (1 / delta).ToString("F3"));
+
+ // Manual logging of mouse clicks
+ if (Input.IsMouseButtonPressed(MouseButton.Left))
+ {
+ if (!wasMouseDown)
+ {
+ wasMouseDown = true;
+ DebugGUI.Log(string.Format(
+ "Mouse down ({0}, {1})",
+ mouseX.ToString("F3"),
+ mouseY.ToString("F3")
+ ));
+ }
+ }
+ else
+ {
+ wasMouseDown = false;
+ }
+
+ if (smoothDeltaTime != 0)
+ {
+ DebugGUI.Graph("smoothFrameRate", 1 / (float)smoothDeltaTime);
+ }
+ if (delta != 0)
+ {
+ DebugGUI.Graph("frameRate", 1 / (float)delta);
+ }
+
+ if (Input.IsKeyPressed(Key.Space))
+ {
+ QueueFree();
+ }
+ }
+
+ public override void _PhysicsProcess(double delta)
+ {
+ physicsTime += (float)delta;
+
+ // Manual graphing
+ DebugGUI.Graph("fixedFrameRateSin", Mathf.Sin(physicsTime * 6));
+ }
+
+ public override void _ExitTree()
+ {
+ // Clean up our logs and graphs when this object leaves tree
+ DebugGUI.RemoveGraph("frameRate");
+ DebugGUI.RemoveGraph("fixedFrameRateSin");
+ DebugGUI.RemoveGraph("smoothFrameRate");
+
+ DebugGUI.RemovePersistent("frameRate");
+ DebugGUI.RemovePersistent("smoothFrameRate");
+ }
+}
\ No newline at end of file
diff --git a/addons/DebugGUI/Examples/DebugGUIExamples.tscn b/addons/DebugGUI/Examples/DebugGUIExamples.tscn
new file mode 100644
index 00000000..807a8c39
--- /dev/null
+++ b/addons/DebugGUI/Examples/DebugGUIExamples.tscn
@@ -0,0 +1,16 @@
+[gd_scene load_steps=3 format=3 uid="uid://cwxrep5n7yml0"]
+
+[ext_resource type="Script" path="res://addons/DebugGUI/Examples/DebugGUIExamples.cs" id="1_herxx"]
+[ext_resource type="Script" path="res://addons/DebugGUI/Examples/debugGUI_examples.gd" id="2_acgyp"]
+
+[node name="C# Example" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_herxx")
+
+[node name="gdscript example" type="Node" parent="."]
+script = ExtResource("2_acgyp")
diff --git a/addons/DebugGUI/Examples/debugGUI_examples.gd b/addons/DebugGUI/Examples/debugGUI_examples.gd
new file mode 100644
index 00000000..7b6dfb78
--- /dev/null
+++ b/addons/DebugGUI/Examples/debugGUI_examples.gd
@@ -0,0 +1,9 @@
+extends Node
+
+func _ready():
+ DebugGUI.SetGraphProperties(self, "from gdscript", 0.0, 10.0, 1, Color.WHITE, true)
+ DebugGUI.Log(self)
+ DebugGUI.Log("This can be done from gdscript too!")
+
+func _process(_delta):
+ DebugGUI.Graph(self, sin(Time.get_ticks_msec() / 100.0))
diff --git a/addons/DebugGUI/Windows/DebugGUIWindow.cs b/addons/DebugGUI/Windows/DebugGUIWindow.cs
new file mode 100644
index 00000000..f6493f8d
--- /dev/null
+++ b/addons/DebugGUI/Windows/DebugGUIWindow.cs
@@ -0,0 +1,61 @@
+using Godot;
+using System;
+
+namespace WeavUtils
+{
+ // Draggable window clamped to the corners
+ public abstract partial class DebugGUIWindow : Control
+ {
+ protected const int outOfScreenClampPadding = 30;
+
+ static bool dragInProgress;
+ bool dragged;
+
+ new public virtual Rect2 GetRect()
+ {
+ return base.GetRect();
+ }
+
+ public override void _Input(InputEvent @event)
+ {
+ if (@event is InputEventMouseButton mb)
+ {
+ if (!dragInProgress && mb.Pressed && mb.ButtonIndex == MouseButton.Middle)
+ {
+ if (GetRect().HasPoint(mb.Position))
+ {
+ dragged = true;
+ dragInProgress = true;
+ }
+ }
+ if (mb.IsReleased() && mb.ButtonIndex == MouseButton.Middle)
+ {
+ if (dragged) dragInProgress = false;
+ dragged = false;
+ }
+ }
+
+ if (@event is InputEventMouseMotion motion)
+ {
+ if (dragged)
+ {
+ Move(motion.Relative);
+ }
+ }
+ }
+
+ protected void Move(Vector2 delta = default)
+ {
+ Position += delta;
+
+ var viewportRect = GetViewportRect();
+
+ // Limit graph window offset so we can't get lost off screen
+ Position = Position.Clamp(
+ -GetRect().Size + Vector2.One * outOfScreenClampPadding,
+ viewportRect.Size - Vector2.One * outOfScreenClampPadding
+ );
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/addons/DebugGUI/Windows/GraphWindow.cs b/addons/DebugGUI/Windows/GraphWindow.cs
new file mode 100644
index 00000000..b4e99962
--- /dev/null
+++ b/addons/DebugGUI/Windows/GraphWindow.cs
@@ -0,0 +1,693 @@
+using Godot;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using static DebugGUI.Settings;
+
+namespace WeavUtils
+{
+ public partial class GraphWindow : DebugGUIWindow
+ {
+ const int graphLabelFontSize = 12;
+ const int graphLabelPadding = 5;
+ const int graphBlockPadding = 3;
+ const int scrubberBackgroundWidth = 55;
+ const int windowOutOfScreenPadding = 30;
+
+ List graphs = new();
+ HashSet attributeContainers = new();
+ Dictionary typeInstanceCounts = new();
+ Dictionary