370 lines
12 KiB
C#
370 lines
12 KiB
C#
using System.ComponentModel;
|
|
|
|
namespace Lity3DEditor;
|
|
|
|
public class MainForm : Form
|
|
{
|
|
private List<SceneNode> _nodes = new();
|
|
private string? _currentPath;
|
|
private bool _modified;
|
|
|
|
private TreeView _tree = null!;
|
|
private PropertyGrid _propertyGrid = null!;
|
|
private StatusStrip _status = null!;
|
|
private ToolStripButton _btnSave = null!;
|
|
|
|
public MainForm()
|
|
{
|
|
Text = "Lity3D Engine Scene Editor";
|
|
Size = new Size(1280, 800);
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
BackColor = Color.FromArgb(30, 30, 38);
|
|
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
|
|
|
|
BuildMenu();
|
|
BuildToolbar();
|
|
BuildPanels();
|
|
BuildStatus();
|
|
|
|
Load += (_, _) =>
|
|
{
|
|
foreach (var c in Controls.OfType<SplitContainer>())
|
|
{
|
|
if (c.Orientation == Orientation.Vertical)
|
|
{
|
|
c.Panel1MinSize = 180;
|
|
c.Panel2MinSize = 300;
|
|
c.SplitterDistance = 260;
|
|
}
|
|
else
|
|
{
|
|
c.Panel1MinSize = 200;
|
|
c.Panel2MinSize = 120;
|
|
c.SplitterDistance = c.Height - 220;
|
|
}
|
|
}
|
|
TryOpenScene(Path.Combine(Application.StartupPath, "scene.txt"));
|
|
};
|
|
FormClosing += (_, e) => { if (!PromptSave()) e.Cancel = true; };
|
|
}
|
|
|
|
private void BuildMenu()
|
|
{
|
|
var menu = new MenuStrip();
|
|
var file = new ToolStripMenuItem("文件(&F)");
|
|
file.DropDownItems.Add(new ToolStripMenuItem("打开...", null, (_, _) => OpenScene()) { ShortcutKeys = Keys.Control | Keys.O });
|
|
file.DropDownItems.Add(new ToolStripMenuItem("保存", null, (_, _) => SaveScene()) { ShortcutKeys = Keys.Control | Keys.S });
|
|
file.DropDownItems.Add(new ToolStripMenuItem("另存为...", null, (_, _) => SaveSceneAs()));
|
|
file.DropDownItems.Add(new ToolStripSeparator());
|
|
file.DropDownItems.Add(new ToolStripMenuItem("退出", null, (_, _) => Close()) { ShortcutKeys = Keys.Alt | Keys.F4 });
|
|
menu.Items.Add(file);
|
|
|
|
var edit = new ToolStripMenuItem("编辑(&E)");
|
|
edit.DropDownItems.Add(new ToolStripMenuItem("添加立方体", null, (_, _) => AddNode(MeshType.CUBE, "cube")));
|
|
edit.DropDownItems.Add(new ToolStripMenuItem("添加球体", null, (_, _) => AddNode(MeshType.SPHERE, "sphere")));
|
|
edit.DropDownItems.Add(new ToolStripMenuItem("添加平面", null, (_, _) => AddNode(MeshType.PLANE, "plane")));
|
|
edit.DropDownItems.Add(new ToolStripSeparator());
|
|
edit.DropDownItems.Add(new ToolStripMenuItem("删除选中", null, (_, _) => DeleteSelected()) { ShortcutKeys = Keys.Delete });
|
|
menu.Items.Add(edit);
|
|
|
|
MainMenuStrip = menu;
|
|
Controls.Add(menu);
|
|
}
|
|
|
|
private void BuildToolbar()
|
|
{
|
|
var tool = new ToolStrip();
|
|
tool.GripStyle = ToolStripGripStyle.Hidden;
|
|
tool.BackColor = Color.FromArgb(40, 40, 50);
|
|
tool.ForeColor = Color.White;
|
|
tool.Renderer = new DarkToolStripRenderer();
|
|
|
|
_btnSave = new ToolStripButton("保存", null, (_, _) => SaveScene()) { Enabled = false };
|
|
tool.Items.Add(_btnSave);
|
|
tool.Items.Add(new ToolStripSeparator());
|
|
tool.Items.Add(new ToolStripButton("添加立方体", null, (_, _) => AddNode(MeshType.CUBE, "cube")));
|
|
tool.Items.Add(new ToolStripButton("添加球体", null, (_, _) => AddNode(MeshType.SPHERE, "sphere")));
|
|
tool.Items.Add(new ToolStripButton("添加平面", null, (_, _) => AddNode(MeshType.PLANE, "plane")));
|
|
tool.Items.Add(new ToolStripSeparator());
|
|
tool.Items.Add(new ToolStripButton("删除", null, (_, _) => DeleteSelected()));
|
|
|
|
tool.Dock = DockStyle.Top;
|
|
Controls.Add(tool);
|
|
}
|
|
|
|
private void BuildPanels()
|
|
{
|
|
var split = new SplitContainer
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = Color.FromArgb(30, 30, 38)
|
|
};
|
|
split.Panel1.BackColor = Color.FromArgb(35, 35, 45);
|
|
split.Panel2.BackColor = Color.FromArgb(35, 35, 45);
|
|
|
|
_tree = new TreeView
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = Color.FromArgb(35, 35, 45),
|
|
ForeColor = Color.White,
|
|
BorderStyle = BorderStyle.None,
|
|
FullRowSelect = true,
|
|
HideSelection = false,
|
|
ShowLines = true,
|
|
ShowPlusMinus = true,
|
|
LineColor = Color.FromArgb(100, 120, 160)
|
|
};
|
|
_tree.AfterSelect += (_, _) => OnNodeSelected();
|
|
split.Panel1.Controls.Add(_tree);
|
|
|
|
var rightSplit = new SplitContainer
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
Orientation = Orientation.Horizontal,
|
|
BackColor = Color.FromArgb(30, 30, 38)
|
|
};
|
|
rightSplit.Panel1.BackColor = Color.FromArgb(35, 35, 45);
|
|
rightSplit.Panel2.BackColor = Color.FromArgb(28, 28, 36);
|
|
|
|
_propertyGrid = new PropertyGrid
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = Color.FromArgb(35, 35, 45),
|
|
ForeColor = Color.White,
|
|
CategoryForeColor = Color.FromArgb(180, 180, 200),
|
|
CommandsBackColor = Color.FromArgb(35, 35, 45),
|
|
CommandsForeColor = Color.White,
|
|
DisabledItemForeColor = Color.Gray,
|
|
HelpBackColor = Color.FromArgb(28, 28, 36),
|
|
HelpForeColor = Color.White,
|
|
ViewBackColor = Color.FromArgb(35, 35, 45),
|
|
ViewForeColor = Color.White,
|
|
LineColor = Color.FromArgb(60, 60, 80),
|
|
ToolbarVisible = false,
|
|
PropertySort = PropertySort.Categorized
|
|
};
|
|
_propertyGrid.PropertyValueChanged += (_, _) => OnPropertyChanged();
|
|
rightSplit.Panel1.Controls.Add(_propertyGrid);
|
|
|
|
var previewLabel = new Label
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
Text = "3D 预览区\n(由 Lity3D Engine 运行时渲染)",
|
|
ForeColor = Color.FromArgb(120, 120, 140),
|
|
BackColor = Color.FromArgb(28, 28, 36),
|
|
TextAlign = ContentAlignment.MiddleCenter,
|
|
Font = new Font(Font.FontFamily, 11f, FontStyle.Regular)
|
|
};
|
|
rightSplit.Panel2.Controls.Add(previewLabel);
|
|
|
|
split.Panel2.Controls.Add(rightSplit);
|
|
|
|
Controls.Add(split);
|
|
}
|
|
|
|
private void BuildStatus()
|
|
{
|
|
_status = new StatusStrip();
|
|
_status.BackColor = Color.FromArgb(40, 40, 50);
|
|
_status.ForeColor = Color.White;
|
|
_status.Items.Add(new ToolStripStatusLabel("就绪"));
|
|
Controls.Add(_status);
|
|
}
|
|
|
|
private void TryOpenScene(string path)
|
|
{
|
|
if (!File.Exists(path)) return;
|
|
try
|
|
{
|
|
_nodes = SceneIO.Load(path);
|
|
_currentPath = path;
|
|
RebuildTree();
|
|
SetModified(false);
|
|
SetStatus($"已打开 {path}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, ex.Message, "打开失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void OpenScene()
|
|
{
|
|
if (!PromptSave()) return;
|
|
using var dlg = new OpenFileDialog
|
|
{
|
|
Filter = "Lity3D 场景|*.txt|所有文件|*.*",
|
|
Title = "打开场景"
|
|
};
|
|
if (dlg.ShowDialog(this) == DialogResult.OK)
|
|
TryOpenScene(dlg.FileName);
|
|
}
|
|
|
|
private bool SaveScene()
|
|
{
|
|
if (string.IsNullOrEmpty(_currentPath))
|
|
return SaveSceneAs();
|
|
try
|
|
{
|
|
SceneIO.Save(_currentPath, _nodes);
|
|
SetModified(false);
|
|
SetStatus($"已保存 {_currentPath}");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, ex.Message, "保存失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool SaveSceneAs()
|
|
{
|
|
using var dlg = new SaveFileDialog
|
|
{
|
|
Filter = "Lity3D 场景|*.txt|所有文件|*.*",
|
|
Title = "另存为"
|
|
};
|
|
if (dlg.ShowDialog(this) != DialogResult.OK) return false;
|
|
_currentPath = dlg.FileName;
|
|
return SaveScene();
|
|
}
|
|
|
|
private bool PromptSave()
|
|
{
|
|
if (!_modified) return true;
|
|
var r = MessageBox.Show(this, "场景已修改,是否保存?", "保存确认",
|
|
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
|
|
if (r == DialogResult.Cancel) return false;
|
|
if (r == DialogResult.Yes) return SaveScene();
|
|
return true;
|
|
}
|
|
|
|
private void AddNode(MeshType type, string baseName)
|
|
{
|
|
int idx = 1;
|
|
string name = baseName;
|
|
while (_nodes.Any(n => n.Name == name))
|
|
name = $"{baseName}_{idx++}";
|
|
|
|
var parentIdx = -1;
|
|
if (_tree.SelectedNode?.Tag is SceneNode sel && sel.Name != "root")
|
|
parentIdx = _nodes.IndexOf(sel);
|
|
else if (_nodes.Count > 0)
|
|
parentIdx = 0; // root
|
|
|
|
var node = new SceneNode
|
|
{
|
|
Name = name,
|
|
ParentIndex = parentIdx,
|
|
Mesh = type,
|
|
PosY = type == MeshType.PLANE ? -1.0f : 0.0f
|
|
};
|
|
_nodes.Add(node);
|
|
RebuildTree();
|
|
SelectNode(node);
|
|
SetModified(true);
|
|
SetStatus($"添加 {name}");
|
|
}
|
|
|
|
private void DeleteSelected()
|
|
{
|
|
if (_tree.SelectedNode?.Tag is not SceneNode node || node.Name == "root")
|
|
{
|
|
MessageBox.Show(this, "不能删除根节点", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
return;
|
|
}
|
|
_nodes.Remove(node);
|
|
RebuildTree();
|
|
SetModified(true);
|
|
SetStatus($"删除 {node.Name}");
|
|
}
|
|
|
|
private void RebuildTree()
|
|
{
|
|
_tree.Nodes.Clear();
|
|
var treeMap = new Dictionary<int, TreeNode>();
|
|
for (int i = 0; i < _nodes.Count; i++)
|
|
{
|
|
var n = _nodes[i];
|
|
var tn = new TreeNode(n.Name) { Tag = n, ForeColor = Color.White };
|
|
treeMap[i] = tn;
|
|
if (n.ParentIndex >= 0 && treeMap.TryGetValue(n.ParentIndex, out var parent))
|
|
parent.Nodes.Add(tn);
|
|
else
|
|
_tree.Nodes.Add(tn);
|
|
}
|
|
_tree.ExpandAll();
|
|
}
|
|
|
|
private void SelectNode(SceneNode node)
|
|
{
|
|
foreach (TreeNode root in _tree.Nodes)
|
|
{
|
|
var found = FindNode(root, node);
|
|
if (found != null)
|
|
{
|
|
_tree.SelectedNode = found;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static TreeNode? FindNode(TreeNode parent, SceneNode node)
|
|
{
|
|
if (parent.Tag == node) return parent;
|
|
foreach (TreeNode c in parent.Nodes)
|
|
{
|
|
var r = FindNode(c, node);
|
|
if (r != null) return r;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void OnNodeSelected()
|
|
{
|
|
_propertyGrid.SelectedObject = _tree.SelectedNode?.Tag;
|
|
if (_tree.SelectedNode?.Tag is SceneNode n)
|
|
SetStatus(n.Name);
|
|
}
|
|
|
|
private void OnPropertyChanged()
|
|
{
|
|
SetModified(true);
|
|
_tree.Refresh();
|
|
}
|
|
|
|
private void SetModified(bool modified)
|
|
{
|
|
_modified = modified;
|
|
_btnSave.Enabled = modified;
|
|
Text = "Lity3D Engine Scene Editor" + (modified ? " *" : "") + (string.IsNullOrEmpty(_currentPath) ? "" : $" - {_currentPath}");
|
|
}
|
|
|
|
private void SetStatus(string msg)
|
|
{
|
|
if (_status.Items[0] is ToolStripStatusLabel lbl)
|
|
lbl.Text = msg;
|
|
}
|
|
}
|
|
|
|
public class DarkToolStripRenderer : ToolStripProfessionalRenderer
|
|
{
|
|
public DarkToolStripRenderer() : base(new DarkColorTable()) { }
|
|
}
|
|
|
|
public class DarkColorTable : ProfessionalColorTable
|
|
{
|
|
public override Color ToolStripBorder => Color.FromArgb(60, 60, 80);
|
|
public override Color ToolStripGradientBegin => Color.FromArgb(40, 40, 50);
|
|
public override Color ToolStripGradientEnd => Color.FromArgb(40, 40, 50);
|
|
public override Color ToolStripGradientMiddle => Color.FromArgb(45, 45, 55);
|
|
public override Color ButtonSelectedHighlight => Color.FromArgb(70, 70, 90);
|
|
public override Color ButtonSelectedBorder => Color.FromArgb(100, 120, 160);
|
|
public override Color ButtonPressedHighlight => Color.FromArgb(60, 80, 120);
|
|
public override Color ButtonCheckedHighlight => Color.FromArgb(60, 80, 120);
|
|
public override Color MenuBorder => Color.FromArgb(60, 60, 80);
|
|
public override Color MenuItemSelected => Color.FromArgb(70, 70, 90);
|
|
public override Color MenuItemBorder => Color.FromArgb(100, 120, 160);
|
|
}
|