using System.ComponentModel; namespace Lity3DEditor; public class MainForm : Form { private List _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()) { 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(); 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); }