瀏覽代碼

2.5D Editor Viewport for Mono C#

Aaron Franke 5 年之前
父節點
當前提交
d1ba41da5d

+ 3 - 2
mono/2.5d/2.5D Demo (Mono C#).csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -53,6 +53,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="addons\node25d-cs\Basis25D.cs" />
+    <Compile Include="addons\node25d-cs\main_screen\Gizmo25D.cs" />
     <Compile Include="addons\node25d-cs\Node25D.cs" />
     <Compile Include="addons\node25d-cs\ShadowMath25D.cs" />
     <Compile Include="addons\node25d-cs\Transform25D.cs" />
@@ -63,4 +64,4 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
-</Project>
+</Project>

+ 40 - 10
mono/2.5d/addons/node25d-cs/Node25D.cs

@@ -83,6 +83,10 @@ public class Node25D : Node2D, IComparable<Node25D>
     /// </summary>
     protected void Node25DProcess()
     {
+        if (transform25D.basis == new Basis25D())
+        {
+            SetViewMode(0);
+        }
         CheckViewMode();
         if (spatialNode != null)
         {
@@ -92,34 +96,60 @@ public class Node25D : Node2D, IComparable<Node25D>
         {
             spatialNode = GetChild<Spatial>(0);
         }
+        
         GlobalPosition = transform25D.FlatPosition;
     }
 
+    public void SetViewMode(int viewModeIndex)
+    {
+        switch (viewModeIndex)
+        {
+            case 0:
+                transform25D.basis = Basis25D.FortyFive * SCALE;
+                break;
+            case 1:
+                transform25D.basis = Basis25D.Isometric * SCALE;
+                break;
+            case 2:
+                transform25D.basis = Basis25D.TopDown * SCALE;
+                break;
+            case 3:
+                transform25D.basis = Basis25D.FrontSide * SCALE;
+                break;
+            case 4:
+                transform25D.basis = Basis25D.ObliqueY * SCALE;
+                break;
+            case 5:
+                transform25D.basis = Basis25D.ObliqueZ * SCALE;
+                break;
+        }
+    }
+
     private void CheckViewMode()
     {
-        if (Input.IsActionJustPressed("top_down_mode"))
+        if (Input.IsActionJustPressed("forty_five_mode"))
         {
-            transform25D.basis = Basis25D.TopDown * SCALE;
+            SetViewMode(0);
         }
-        else if (Input.IsActionJustPressed("front_side_mode"))
+        else if (Input.IsActionJustPressed("isometric_mode"))
         {
-            transform25D.basis = Basis25D.FrontSide * SCALE;
+            SetViewMode(1);
         }
-        else if (Input.IsActionJustPressed("forty_five_mode"))
+        else if (Input.IsActionJustPressed("top_down_mode"))
         {
-            transform25D.basis = Basis25D.FortyFive * SCALE;
+            SetViewMode(2);
         }
-        else if (Input.IsActionJustPressed("isometric_mode"))
+        else if (Input.IsActionJustPressed("front_side_mode"))
         {
-            transform25D.basis = Basis25D.Isometric * SCALE;
+            SetViewMode(3);
         }
         else if (Input.IsActionJustPressed("oblique_y_mode"))
         {
-            transform25D.basis = Basis25D.ObliqueY * SCALE;
+            SetViewMode(4);
         }
         else if (Input.IsActionJustPressed("oblique_z_mode"))
         {
-            transform25D.basis = Basis25D.ObliqueZ * SCALE;
+            SetViewMode(5);
         }
     }
 

+ 230 - 0
mono/2.5d/addons/node25d-cs/main_screen/.broken-cs-files/Viewport25D.cs

@@ -0,0 +1,230 @@
+using Godot;
+
+// This is identical to the GDScript version, yet it doesn't work.
+[Tool]
+public class Viewport25D : Control
+{
+    private int zoomLevel = 0;
+    private bool isPanning = false;
+    private Vector2 panCenter;
+    private Vector2 viewportCenter;
+    private int viewModeIndex = 0;
+
+    // The type or namespace name 'EditorInterface' could not be found (are you missing a using directive or an assembly reference?)
+    // No idea why this error shows up in VS Code. It builds fine...
+    public EditorInterface editorInterface; // Set in node25d_plugin.gd
+    private bool moving = false;
+
+    private Viewport viewport2d;
+    private Viewport viewportOverlay;
+    private ButtonGroup viewModeButtonGroup;
+    private Label zoomLabel;
+    private PackedScene gizmo25dScene;
+
+    public async override void _Ready()
+    {
+        // Give Godot a chance to fully load the scene. Should take two frames.
+        //yield(get_tree(), "idle_frame");
+        //yield(get_tree(), "idle_frame");
+        await ToSignal(GetTree(), "idle_frame");
+        await ToSignal(GetTree(), "idle_frame");
+        var editedSceneRoot = GetTree().EditedSceneRoot;
+        if (editedSceneRoot == null)
+        {
+            // Godot hasn't finished loading yet, so try loading the plugin again.
+            //editorInterface.SetPluginEnabled("node25d", false);
+            //editorInterface.SetPluginEnabled("node25d", true);
+            return;
+        }
+        // Alright, we're loaded up. Now check if we have a valid world and assign it.
+        var world2d = editedSceneRoot.GetViewport().World2d;
+        if (world2d == GetViewport().World2d)
+        {
+            return; // This is the MainScreen25D scene opened in the editor!
+        }
+        viewport2d.World2d = world2d;
+
+        // Onready vars.
+        viewport2d = GetNode<Viewport>("Viewport2D");
+        viewportOverlay = GetNode<Viewport>("ViewportOverlay");
+        viewModeButtonGroup = GetParent().GetNode("TopBar").GetNode("ViewModeButtons").GetNode<Button>("45Degree").Group;
+        zoomLabel = GetParent().GetNode("TopBar").GetNode("Zoom").GetNode<Label>("ZoomPercent");
+        gizmo25dScene = ResourceLoader.Load<PackedScene>("res://addons/node25d/main_screen/gizmo_25d.tscn");
+    }
+
+
+    public override void _Process(float delta)
+    {
+        if (editorInterface == null) // Something's not right... bail!
+        {
+            return;
+        }
+        
+        // View mode polling.
+        var viewModeChangedThisFrame = false;
+        var newViewMode = viewModeButtonGroup.GetPressedButton().GetIndex();
+        if (viewModeIndex != newViewMode)
+        {
+            viewModeIndex = newViewMode;
+            viewModeChangedThisFrame = true;
+            RecursiveChangeViewMode(GetTree().EditedSceneRoot);
+        }
+        
+        // Zooming.
+        if (Input.IsMouseButtonPressed((int)ButtonList.WheelUp))
+        {
+            zoomLevel += 1;
+        }
+        else if (Input.IsMouseButtonPressed((int)ButtonList.WheelDown))
+        {
+            zoomLevel -= 1;
+        }
+        float zoom = GetZoomAmount();
+        
+        // Viewport size.
+        Vector2 size = GetGlobalRect().Size;
+        viewport2d.Size = size;
+        
+        // Viewport transform.
+        Transform2D viewportTrans = Transform2D.Identity;
+        viewportTrans.x *= zoom;
+        viewportTrans.y *= zoom;
+        viewportTrans.origin = viewportTrans.BasisXform(viewportCenter) + size / 2;
+        viewport2d.CanvasTransform = viewportTrans;
+        viewportOverlay.CanvasTransform = viewportTrans;
+        
+        // Delete unused gizmos.
+        var selection = editorInterface.GetSelection().GetSelectedNodes();
+        var overlayChildren = viewportOverlay.GetChildren();
+        foreach (Gizmo25D overlayChild in overlayChildren)
+        {
+            bool contains = false;
+            foreach (Node selected in selection)
+            {
+                if (selected == overlayChild.node25d && !viewModeChangedThisFrame)
+                {
+                    contains = true;
+                }
+            }
+            if (!contains)
+            {
+                overlayChild.QueueFree();
+            }
+        }
+        // Add new gizmos.
+        foreach (Node sel in selection)
+        {
+            if (sel is Node25D selected)
+            {
+                var newNode = true;
+                foreach (Gizmo25D overlayChild2 in overlayChildren)
+                {
+                    if (selected == overlayChild2.node25d)
+                    {
+                        newNode = false;
+                    }
+                }
+                if (newNode)
+                {
+                    Gizmo25D gizmo = (Gizmo25D)gizmo25dScene.Instance();
+                    viewportOverlay.AddChild(gizmo);
+                    gizmo.node25d = selected;
+                    gizmo.Initialize();
+                }
+            }
+        }
+    }
+
+    // This only accepts input when the mouse is inside of the 2.5D viewport.
+    public override void _GuiInput(InputEvent inputEvent)
+    {
+        if (inputEvent is InputEventMouseButton mouseButtonEvent)
+        {
+            if (mouseButtonEvent.IsPressed())
+            {
+                if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.WheelUp)
+                {
+                    zoomLevel += 1;
+                    AcceptEvent();
+                }
+                else if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.WheelDown)
+                {
+                    zoomLevel -= 1;
+                    AcceptEvent();
+                }
+                else if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.Middle)
+                {
+                    isPanning = true;
+                    panCenter = viewportCenter - mouseButtonEvent.Position;
+                    AcceptEvent();
+                }
+                else if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.Left)
+                {
+                    var overlayChildren2 = viewportOverlay.GetChildren();
+                    foreach (Gizmo25D overlayChild in overlayChildren2)
+                    {
+                        overlayChild.wantsToMove = true;
+                    }
+                    AcceptEvent();
+                }
+            }
+            else if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.Middle)
+            {
+                isPanning = false;
+                AcceptEvent();
+            }
+            else if ((ButtonList)mouseButtonEvent.ButtonIndex == ButtonList.Left)
+            {
+                var overlayChildren3 = viewportOverlay.GetChildren();
+                foreach (Gizmo25D overlayChild in overlayChildren3)
+                {
+                    overlayChild.wantsToMove = false;
+                }
+                AcceptEvent();
+            }
+        }
+        else if (inputEvent is InputEventMouseMotion mouseEvent)
+        {
+            if (isPanning)
+            {
+                viewportCenter = panCenter + mouseEvent.Position;
+                AcceptEvent();
+            }
+        }
+    }
+
+    public void RecursiveChangeViewMode(Node currentNode)
+    {
+        // TODO
+        if (currentNode.HasMethod("SetViewMode"))
+        {
+            //currentNode.SetViewMode(viewModeIndex);
+        }
+        foreach (Node child in currentNode.GetChildren())
+        {
+            RecursiveChangeViewMode(child);
+        }
+    }
+
+    private float GetZoomAmount()
+    {
+        float zoomAmount = Mathf.Pow(1.05476607648f, zoomLevel); // 13th root of 2
+        zoomLabel.Text = Mathf.Round(zoomAmount * 1000) / 10 + "%";
+        return zoomAmount;
+    }
+
+    public void OnZoomOutPressed()
+    {
+        zoomLevel -= 1;
+    }
+
+    public void OnZoomInPressed()
+    {
+        zoomLevel += 1;
+    }
+
+    public void OnZoomResetPressed()
+    {
+        zoomLevel = 0;
+    }
+}

+ 168 - 0
mono/2.5d/addons/node25d-cs/main_screen/Gizmo25D.cs

@@ -0,0 +1,168 @@
+using Godot;
+
+// This is identical to the GDScript version, yet it doesn't work.
+[Tool]
+public class Gizmo25D : Node2D
+{
+    // Not pixel perfect for all axes in all modes, but works well enough.
+    // Rounding is not done until after the movement is finished.
+    private const bool RoughlyRoundToPixels = true;
+
+    // Set when the node is created.
+    public Node25D node25d;
+    public Spatial spatialNode;
+
+    // Input from Viewport25D, represents if the mouse is clicked.
+    public bool wantsToMove = false;
+
+    // Used to control the state of movement.
+    private bool _moving = false;
+    private Vector2 _startPosition = Vector2.Zero;
+
+    // Stores state of closest or currently used axis.
+    private int dominantAxis;
+
+    private Node2D linesRoot;
+    private Line2D[] lines = new Line2D[3];
+
+    public override void _Ready()
+    {
+        linesRoot = GetChild<Node2D>(0);
+        lines[0] = linesRoot.GetChild<Line2D>(0);
+        lines[1] = linesRoot.GetChild<Line2D>(1);
+        lines[2] = linesRoot.GetChild<Line2D>(2);
+    }
+
+    public override void _Process(float delta)
+    {
+        if (lines == null)
+        {
+            return; // Somehow this node hasn't been set up yet.
+        }
+        if (node25d == null)
+        {
+            return; // We're most likely viewing the Gizmo25D scene.
+        }
+        // While getting the mouse position works in any viewport, it doesn't do
+	    // anything significant unless the mouse is in the 2.5D viewport.
+	    Vector2 mousePosition = GetLocalMousePosition();
+    	if (!_moving)
+        {
+    		// If the mouse is farther than this many pixels, it won't grab anything.
+    		float closestDistance = 20.0f;
+    		dominantAxis = -1;
+    		for (int i = 0; i < 3; i++)
+            {
+                // Unrelated, but needs a loop too.
+                Color modulateLine = lines[i].Modulate;
+    			modulateLine.a = 0.8f;
+                lines[i].Modulate = modulateLine;
+
+    			var distance = DistanceToSegmentAtIndex(i, mousePosition);
+    			if (distance < closestDistance)
+                {
+    				closestDistance = distance;
+    				dominantAxis = i;
+                }
+            }
+    		if (dominantAxis == -1)
+            {
+    			// If we're not hovering over a line, ensure they are placed correctly.
+    			linesRoot.GlobalPosition = node25d.GlobalPosition;
+    			return;
+            }
+        }
+	
+        Color modulate = lines[dominantAxis].Modulate;
+    	modulate.a = 1;
+        lines[dominantAxis].Modulate = modulate;
+
+    	if (!wantsToMove)
+        {
+    		_moving = false;
+        }
+    	else if (wantsToMove && !_moving)
+        {
+    		_moving = true;
+    		_startPosition = mousePosition;
+        }
+	
+    	if (_moving)
+        {
+    		// Change modulate of unselected axes.
+            modulate = lines[(dominantAxis + 1) % 3].Modulate;
+    		modulate.a = 0.5f;
+            lines[(dominantAxis + 1) % 3].Modulate = modulate;
+            lines[(dominantAxis + 2) % 3].Modulate = modulate;
+
+    		// Calculate mouse movement and reset for next frame.
+    		var mouseDiff = mousePosition - _startPosition;
+    		_startPosition = mousePosition;
+    		// Calculate movement.
+    		var projectedDiff = mouseDiff.Project(lines[dominantAxis].Points[1]);
+    		var movement = projectedDiff.Length() / Node25D.SCALE;
+    		if (Mathf.IsEqualApprox(Mathf.Pi, projectedDiff.AngleTo(lines[dominantAxis].Points[1])))
+            {
+    			movement *= -1;
+            }
+    		// Apply movement.
+            Transform t = spatialNode.Transform;
+    		t.origin += t.basis[dominantAxis] * movement;
+            spatialNode.Transform = t;
+        }
+    	else
+        {
+    		// Make sure the gizmo is located at the object.
+    		GlobalPosition = node25d.GlobalPosition;
+    		if (RoughlyRoundToPixels)
+            {
+                Transform t = spatialNode.Transform;
+    			t.origin = (t.origin * Node25D.SCALE).Round() / Node25D.SCALE;
+                spatialNode.Transform = t;
+            }
+        }
+    	// Move the gizmo lines appropriately.
+    	linesRoot.GlobalPosition = node25d.GlobalPosition;
+    	node25d.PropertyListChangedNotify();
+    }
+
+    // Initializes after _ready due to the onready vars, called manually in Viewport25D.gd.
+    // Sets up the points based on the basis values of the Node25D.
+    public void Initialize()
+    {
+	    var basis = node25d.Basis25D;
+	    for (int i = 0; i < 3; i++)
+        {
+		    lines[i].Points[1] = basis[i] * 3;
+        }
+	    GlobalPosition = node25d.GlobalPosition;
+	    spatialNode = node25d.GetChild<Spatial>(0);
+    }
+
+
+    // Figures out if the mouse is very close to a segment. This method is
+    // specialized for this script, it assumes that each segment starts at
+    // (0, 0) and it provides a deadzone around the origin.
+    private float DistanceToSegmentAtIndex(int index, Vector2 point)
+    {
+	    if (lines == null)
+        {
+		    return Mathf.Inf;
+        }
+    	if (point.LengthSquared() < 400)
+        {
+		    return Mathf.Inf;
+        }
+	
+	    Vector2 segmentEnd = lines[index].Points[1];
+	    float lengthSquared = segmentEnd.LengthSquared();
+	    if (lengthSquared < 400)
+        {
+    		return Mathf.Inf;
+        }
+	
+	    var t = Mathf.Clamp(point.Dot(segmentEnd) / lengthSquared, 0, 1);
+    	var projection = t * segmentEnd;
+	    return point.DistanceTo(projection);
+    }
+}

+ 23 - 0
mono/2.5d/addons/node25d-cs/main_screen/gizmo_25d.tscn

@@ -0,0 +1,23 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://addons/node25d-cs/main_screen/Gizmo25D.cs" type="Script" id=1]
+
+[node name="Gizmo25D" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="Lines" type="Node2D" parent="."]
+
+[node name="X" type="Line2D" parent="Lines"]
+modulate = Color( 1, 1, 1, 0.8 )
+points = PoolVector2Array( 0, 0, 100, 0 )
+default_color = Color( 0.91, 0.273, 0, 1 )
+
+[node name="Y" type="Line2D" parent="Lines"]
+modulate = Color( 1, 1, 1, 0.8 )
+points = PoolVector2Array( 0, 0, 0, -100 )
+default_color = Color( 0, 0.91, 0.273, 1 )
+
+[node name="Z" type="Line2D" parent="Lines"]
+modulate = Color( 1, 1, 1, 0.8 )
+points = PoolVector2Array( 0, 0, 0, 100 )
+default_color = Color( 0.3, 0, 1, 1 )

+ 173 - 0
mono/2.5d/addons/node25d-cs/main_screen/main_screen_25d.tscn

@@ -0,0 +1,173 @@
+[gd_scene load_steps=5 format=2]
+
+[ext_resource path="res://addons/node25d-cs/main_screen/viewport_25d.gd" type="Script" id=1]
+[ext_resource path="res://addons/node25d-cs/main_screen/view_mode_button_group.tres" type="ButtonGroup" id=2]
+
+[sub_resource type="ViewportTexture" id=1]
+viewport_path = NodePath("Viewport25D/Viewport2D")
+
+[sub_resource type="ViewportTexture" id=2]
+viewport_path = NodePath("Viewport25D/ViewportOverlay")
+
+[node name="MainScreen25D" type="VBoxContainer"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="TopBar" type="HBoxContainer" parent="."]
+margin_right = 1600.0
+margin_bottom = 32.0
+rect_min_size = Vector2( 0, 32 )
+size_flags_horizontal = 3
+
+[node name="ViewModeButtons" type="HBoxContainer" parent="TopBar"]
+margin_right = 798.0
+margin_bottom = 32.0
+size_flags_horizontal = 3
+
+[node name="45Degree" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_right = 94.0
+margin_bottom = 32.0
+pressed = true
+group = ExtResource( 2 )
+text = "45 Degree"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Isometric" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_left = 98.0
+margin_right = 188.0
+margin_bottom = 32.0
+group = ExtResource( 2 )
+text = "Isometric"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="TopDown" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_left = 192.0
+margin_right = 283.0
+margin_bottom = 32.0
+group = ExtResource( 2 )
+text = "Top Down"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="FrontSide" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_left = 287.0
+margin_right = 379.0
+margin_bottom = 32.0
+group = ExtResource( 2 )
+text = "Front Side"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ObliqueY" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_left = 383.0
+margin_right = 473.0
+margin_bottom = 32.0
+group = ExtResource( 2 )
+text = "Oblique Y"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ObliqueZ" type="CheckBox" parent="TopBar/ViewModeButtons"]
+margin_left = 477.0
+margin_right = 568.0
+margin_bottom = 32.0
+group = ExtResource( 2 )
+text = "Oblique Z"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Zoom" type="HBoxContainer" parent="TopBar"]
+margin_left = 802.0
+margin_right = 1600.0
+margin_bottom = 32.0
+size_flags_horizontal = 3
+alignment = 2
+
+[node name="ZoomOut" type="Button" parent="TopBar/Zoom"]
+margin_left = 680.0
+margin_right = 710.0
+margin_bottom = 32.0
+rect_min_size = Vector2( 30, 0 )
+text = "-"
+
+[node name="ZoomPercent" type="Label" parent="TopBar/Zoom"]
+margin_left = 714.0
+margin_top = 9.0
+margin_right = 764.0
+margin_bottom = 23.0
+rect_min_size = Vector2( 50, 0 )
+text = "100%"
+align = 1
+clip_text = true
+
+[node name="ZoomReset" type="Button" parent="TopBar/Zoom/ZoomPercent"]
+modulate = Color( 1, 1, 1, 0 )
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ZoomIn" type="Button" parent="TopBar/Zoom"]
+margin_left = 768.0
+margin_right = 798.0
+margin_bottom = 32.0
+rect_min_size = Vector2( 30, 0 )
+text = "+"
+
+[node name="Viewport25D" type="ColorRect" parent="."]
+margin_top = 36.0
+margin_right = 1600.0
+margin_bottom = 900.0
+rect_clip_content = true
+size_flags_horizontal = 3
+size_flags_vertical = 3
+color = Color( 0.301961, 0.301961, 0.301961, 1 )
+script = ExtResource( 1 )
+
+[node name="Viewport2D" type="Viewport" parent="Viewport25D"]
+size = Vector2( 1600, 864 )
+transparent_bg = true
+disable_3d = true
+usage = 1
+render_target_v_flip = true
+
+[node name="ViewportOverlay" type="Viewport" parent="Viewport25D"]
+size = Vector2( 1600, 864 )
+transparent_bg = true
+disable_3d = true
+usage = 1
+render_target_v_flip = true
+
+[node name="ViewportTexture" type="TextureRect" parent="Viewport25D"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+texture = SubResource( 1 )
+expand = true
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Overlay" type="TextureRect" parent="Viewport25D/ViewportTexture"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+texture = SubResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+[connection signal="pressed" from="TopBar/Zoom/ZoomOut" to="Viewport25D" method="_on_ZoomOut_pressed"]
+[connection signal="pressed" from="TopBar/Zoom/ZoomPercent/ZoomReset" to="Viewport25D" method="_on_ZoomReset_pressed"]
+[connection signal="pressed" from="TopBar/Zoom/ZoomIn" to="Viewport25D" method="_on_ZoomIn_pressed"]

+ 3 - 0
mono/2.5d/addons/node25d-cs/main_screen/view_mode_button_group.tres

@@ -0,0 +1,3 @@
+[gd_resource type="ButtonGroup" format=2]
+
+[resource]

+ 149 - 0
mono/2.5d/addons/node25d-cs/main_screen/viewport_25d.gd

@@ -0,0 +1,149 @@
+tool
+extends Control
+
+var zoom_level := 0
+var is_panning = false
+var pan_center: Vector2
+var viewport_center: Vector2
+var view_mode_index := 0
+
+var editor_interface: EditorInterface # Set in node25d_plugin.gd
+var moving = false
+
+onready var viewport_2d = $Viewport2D
+onready var viewport_overlay = $ViewportOverlay
+onready var view_mode_button_group: ButtonGroup = $"../TopBar/ViewModeButtons/45Degree".group
+onready var zoom_label: Label = $"../TopBar/Zoom/ZoomPercent"
+onready var gizmo_25d_scene = preload("res://addons/node25d-cs/main_screen/gizmo_25d.tscn")
+
+func _ready():
+	# Give Godot a chance to fully load the scene. Should take two frames.
+	yield(get_tree(), "idle_frame")
+	yield(get_tree(), "idle_frame")
+	var edited_scene_root = get_tree().edited_scene_root
+	if !edited_scene_root:
+		# Godot hasn't finished loading yet, so try loading the plugin again.
+		editor_interface.set_plugin_enabled("node25d", false)
+		editor_interface.set_plugin_enabled("node25d", true)
+		return
+	# Alright, we're loaded up. Now check if we have a valid world and assign it.
+	var world_2d = edited_scene_root.get_viewport().world_2d
+	if world_2d == get_viewport().world_2d:
+		return # This is the MainScreen25D scene opened in the editor!
+	viewport_2d.world_2d = world_2d
+
+
+func _process(delta):
+	if !editor_interface: # Something's not right... bail!
+		return
+	
+	# View mode polling.
+	var view_mode_changed_this_frame = false
+	var new_view_mode = view_mode_button_group.get_pressed_button().get_index()
+	if view_mode_index != new_view_mode:
+		view_mode_index = new_view_mode
+		view_mode_changed_this_frame = true
+		_recursive_change_view_mode(get_tree().edited_scene_root)
+	
+	# Zooming.
+	if Input.is_mouse_button_pressed(BUTTON_WHEEL_UP):
+		zoom_level += 1
+	elif Input.is_mouse_button_pressed(BUTTON_WHEEL_DOWN):
+		zoom_level -= 1
+	var zoom = _get_zoom_amount()
+	
+	# Viewport size.
+	var size = get_global_rect().size
+	viewport_2d.size = size
+	
+	# Viewport transform.
+	var viewport_trans = Transform2D.IDENTITY
+	viewport_trans.x *= zoom
+	viewport_trans.y *= zoom
+	viewport_trans.origin = viewport_trans.basis_xform(viewport_center) + size / 2
+	viewport_2d.canvas_transform = viewport_trans
+	viewport_overlay.canvas_transform = viewport_trans
+	
+	# Delete unused gizmos.
+	var selection = editor_interface.get_selection().get_selected_nodes()
+	var overlay_children = viewport_overlay.get_children()
+	for overlay_child in overlay_children:
+		var contains = false
+		for selected in selection:
+			if selected == overlay_child.get("node25d") and !view_mode_changed_this_frame:
+				contains = true
+		if !contains:
+			overlay_child.queue_free()
+	
+	# Add new gizmos.
+	for selected in selection:
+		if selected.has_method("Node25DReady"):
+			var new = true
+			for overlay_child in overlay_children:
+				if selected == overlay_child.get("node25d"):
+					new = false
+			if new:
+				var gizmo = gizmo_25d_scene.instance()
+				viewport_overlay.add_child(gizmo)
+				gizmo.set("node25d", selected)
+				gizmo.call("Initialize")
+
+
+# This only accepts input when the mouse is inside of the 2.5D viewport.
+func _gui_input(event):
+	if event is InputEventMouseButton:
+		if event.is_pressed():
+			if event.button_index == BUTTON_WHEEL_UP:
+				zoom_level += 1
+				accept_event()
+			elif event.button_index == BUTTON_WHEEL_DOWN:
+				zoom_level -= 1
+				accept_event()
+			elif event.button_index == BUTTON_MIDDLE:
+				is_panning = true
+				pan_center = viewport_center - event.position
+				accept_event()
+			elif event.button_index == BUTTON_LEFT:
+				var overlay_children = viewport_overlay.get_children()
+				for overlay_child in overlay_children:
+					overlay_child.set("wantsToMove", true)
+				accept_event()
+		elif event.button_index == BUTTON_MIDDLE:
+			is_panning = false
+			accept_event()
+		elif event.button_index == BUTTON_LEFT:
+			var overlay_children = viewport_overlay.get_children()
+			for overlay_child in overlay_children:
+				overlay_child.set("wantsToMove", false)
+			accept_event()
+	elif event is InputEventMouseMotion:
+		if is_panning:
+			viewport_center = pan_center + event.position
+			accept_event()
+
+
+func _recursive_change_view_mode(current_node):
+	if current_node.has_method("set_view_mode"):
+		current_node.set_view_mode(view_mode_index) # GDScript.
+	if current_node.has_method("SetViewMode"):
+		current_node.call("SetViewMode", view_mode_index) # C#.
+	for child in current_node.get_children():
+		_recursive_change_view_mode(child)
+
+
+func _get_zoom_amount():
+	var zoom_amount = pow(1.05476607648, zoom_level) # 13th root of 2.
+	zoom_label.text = str(round(zoom_amount * 1000) / 10) + "%"
+	return zoom_amount
+
+
+func _on_ZoomOut_pressed():
+	zoom_level -= 1
+
+
+func _on_ZoomIn_pressed():
+	zoom_level += 1
+
+
+func _on_ZoomReset_pressed():
+	zoom_level = 0

+ 34 - 2
mono/2.5d/addons/node25d-cs/node25d_plugin.gd

@@ -1,14 +1,46 @@
 tool
 extends EditorPlugin
 
+const MainPanel = preload("res://addons/node25d-cs/main_screen/main_screen_25d.tscn")
+
+var main_panel_instance
+
 func _enter_tree():
-	# When this plugin node enters tree, add the custom types
+	main_panel_instance = MainPanel.instance()
+	#main_panel_instance.get_child(1).set("editorInterface", get_editor_interface()) # For C#
+	main_panel_instance.get_child(1).editor_interface = get_editor_interface()
+	
+	# Add the main panel to the editor's main viewport.
+	get_editor_interface().get_editor_viewport().add_child(main_panel_instance)
+	
+	# Hide the main panel.
+	make_visible(false)
+	
+	# When this plugin node enters tree, add the custom types.
 	add_custom_type("Node25D", "Node2D", preload("Node25D.cs"), preload("icons/node_25d_icon.png"))
 	add_custom_type("YSort25D", "Node", preload("YSort25D.cs"), preload("icons/y_sort_25d_icon.png"))
 	add_custom_type("ShadowMath25D", "KinematicBody", preload("ShadowMath25D.cs"), preload("icons/shadow_math_25d_icon.png"))
 
+
 func _exit_tree():
-	# When the plugin node exits the tree, remove the custom types
+	main_panel_instance.queue_free()
+	
+	# When the plugin node exits the tree, remove the custom types.
 	remove_custom_type("ShadowMath25D")
 	remove_custom_type("YSort25D")
 	remove_custom_type("Node25D")
+
+
+func has_main_screen():
+	return true
+
+
+func make_visible(visible):
+	if visible:
+		main_panel_instance.show()
+	else:
+		main_panel_instance.hide()
+
+
+func get_plugin_name():
+	return "2.5D"

+ 4 - 6
mono/2.5d/assets/demo_scene.tscn

@@ -26,18 +26,16 @@ extents = Vector3( 5, 0.5, 5 )
 [node name="Overlay" parent="." instance=ExtResource( 3 )]
 
 [node name="Player25D" parent="." instance=ExtResource( 4 )]
-position = Vector2( 0, -226.274 )
 z_index = -3952
 
 [node name="Shadow25D" parent="." instance=ExtResource( 5 )]
 visible = true
-position = Vector2( 1.00261e-06, 11.2685 )
-z_index = -3958
-spatialPosition = Vector3( 3.13315e-08, -0.498, 3.13315e-08 )
+position = Vector2( 0, -226.274 )
+z_index = -3954
 
 [node name="Platform0" type="Node2D" parent="."]
 position = Vector2( -256, -113.137 )
-z_index = -3954
+z_index = -3956
 script = ExtResource( 6 )
 __meta__ = {
 "_editor_icon": ExtResource( 7 )
@@ -62,7 +60,7 @@ script = ExtResource( 9 )
 
 [node name="Platform1" type="Node2D" parent="."]
 position = Vector2( -256, -339.411 )
-z_index = -3956
+z_index = -3958
 script = ExtResource( 6 )
 __meta__ = {
 "_editor_icon": ExtResource( 7 )

+ 5 - 0
mono/2.5d/assets/player/PlayerSprite.cs

@@ -5,6 +5,7 @@ using real_t = System.Double;
 using real_t = System.Single;
 #endif
 
+[Tool]
 public class PlayerSprite : Sprite
 {
     private static Texture _stand = ResourceLoader.Load<Texture>("res://assets/player/textures/stand.png");
@@ -25,6 +26,10 @@ public class PlayerSprite : Sprite
 
     public override void _Process(real_t delta)
     {
+	    if (Engine.EditorHint)
+        {
+		    return; // Don't run this in the editor.
+        }
         SpriteBasis();
         bool movement = CheckMovement(); // Always run to get direction, but don't always use return bool.