using System.Drawing.Drawing2D;
using anjianjingling.Lua;
namespace anjianjingling.Forms
{
///
/// 纯 WinForms 自绘积木画布:插片耦合(榫卯)造型。
/// 顶部凹槽 + 底部凸榫互相咬合;容器块为 C 形插槽;参数显示为可插入的白色插槽。
///
public sealed class BlockCanvas : Panel
{
private enum RowKind { Block, Slot, Else, End }
private sealed class Row
{
public LuaBlock Block = null!;
public int Depth;
public Rectangle Bounds;
public List Owner = null!;
public int Index;
public RowKind Kind;
}
// ---- 几何(像素)----
private const int RowH = 30; // 语句块高度
private const int NotchH = 7; // 榫高:凸榫伸出 / 凹槽凹进
private const int NotchX = 26; // 榫相对块左边的起始位置
private const int NotchSlope = 7; // 斜边宽
private const int NotchFlat = 12; // 平台宽
private const int WallW = 22; // 容器左壁宽(子块缩进)
private const int FootH = 16; // 容器底部封口高
private const int ElseH = 22; // 否则分隔条高
private const int Radius = 4;
private const int MinInner = 24; // 空容器最小内腔高
private const int PadX = 12;
private const int PadTop = 12;
private readonly List _rows = new();
private LuaBlock? _selected;
private LuaBlock? _hover;
private Point _mouseDown;
private LuaBlock? _dragBlock;
private bool _dragging;
private (List List, int Index)? _dropTarget;
private int _dropY;
private int _dropX;
private static readonly Font TitleFont = new("Microsoft YaHei UI", 9.5F);
private static readonly Font SmallFont = new("Microsoft YaHei UI", 7.5F);
public List Blocks { get; } = new();
public event Action? Changed;
public event Action? SelectionChanged;
public event Action? EditRequested;
public LuaBlock? SelectedBlock => _selected;
public BlockCanvas()
{
DoubleBuffered = true;
BackColor = Color.FromArgb(244, 247, 251);
AutoScroll = true;
AllowDrop = true;
SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint | ControlStyles.UserMouse, true);
}
// ===================== 拖入 =====================
public const string DragFormat = "aj_block_key";
protected override void OnDragEnter(DragEventArgs e)
{
if (e.Data?.GetDataPresent(DragFormat) == true) e.Effect = DragDropEffects.Copy;
base.OnDragEnter(e);
}
protected override void OnDragOver(DragEventArgs e)
{
if (e.Data?.GetDataPresent(DragFormat) == true)
{
e.Effect = DragDropEffects.Copy;
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
}
base.OnDragOver(e);
}
protected override void OnDragLeave(EventArgs e)
{
_dropTarget = null;
Invalidate();
base.OnDragLeave(e);
}
protected override void OnDragDrop(DragEventArgs e)
{
if (e.Data?.GetData(DragFormat) is string key && BlockCatalog.Find(key) != null)
{
UpdateDropIndicator(PointToClient(new Point(e.X, e.Y)));
var block = new LuaBlock { Key = key };
InsertAtDropTarget(block);
Select(block);
}
_dropTarget = null;
Invalidate();
base.OnDragDrop(e);
}
// ===================== 数据操作 =====================
public void AddBlock(LuaBlock block, bool atEnd = true)
{
if (atEnd || _selected == null) Blocks.Add(block);
else InsertAfter(_selected, block);
Select(block);
Invalidate();
Changed?.Invoke();
}
private void InsertAfter(LuaBlock anchor, LuaBlock block)
{
foreach (var (list, _) in EnumerateLists(Blocks))
{
int i = list.IndexOf(anchor);
if (i >= 0) { list.Insert(i + 1, block); return; }
}
Blocks.Add(block);
}
private static IEnumerable<(List List, int Depth)> EnumerateLists(List list, int depth = 0)
{
yield return (list, depth);
foreach (var b in list)
{
foreach (var x in EnumerateLists(b.Children, depth + 1)) yield return x;
foreach (var x in EnumerateLists(b.ElseChildren, depth + 1)) yield return x;
}
}
public void ClearAll()
{
Blocks.Clear();
_selected = null;
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
/// 内容有变化(含参数修改)时由外部触发重绘与变更通知。
public void NotifyChanged()
{
Invalidate();
Changed?.Invoke();
}
public void LoadBlocks(IEnumerable blocks)
{
Blocks.Clear();
Blocks.AddRange(blocks);
_selected = null;
AutoScrollPosition = Point.Empty; // 载入后回到顶部,避免旧滚动位置把第一块顶出视野
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
public void Select(LuaBlock? b)
{
_selected = b;
SelectionChanged?.Invoke(b);
Invalidate();
}
public void RemoveSelected()
{
if (_selected == null) return;
RemoveFrom(Blocks, _selected);
_selected = null;
Invalidate();
Changed?.Invoke();
SelectionChanged?.Invoke(null);
}
public void DuplicateSelected()
{
if (_selected == null) return;
var copy = _selected.Clone();
InsertAfter(_selected, copy);
Select(copy);
Invalidate();
Changed?.Invoke();
}
public void MoveSelected(int dir)
{
if (_selected == null) return;
foreach (var (list, _) in EnumerateLists(Blocks))
{
int i = list.IndexOf(_selected);
if (i < 0) continue;
int j = i + dir;
if (j < 0 || j >= list.Count) return;
list.RemoveAt(i);
list.Insert(j, _selected);
Invalidate();
Changed?.Invoke();
return;
}
}
public void ToggleElse()
{
if (_selected == null) return;
var def = BlockCatalog.Find(_selected.Key);
if (def is not { Shape: BlockShape.If or BlockShape.IfExists }) return;
_selected.HasElse = !_selected.HasElse;
if (!_selected.HasElse) _selected.ElseChildren.Clear();
Invalidate();
Changed?.Invoke();
}
private static bool RemoveFrom(List list, LuaBlock target)
{
if (list.Remove(target)) return true;
foreach (var b in list)
{
if (RemoveFrom(b.Children, target)) return true;
if (RemoveFrom(b.ElseChildren, target)) return true;
}
return false;
}
private static bool IsListInside(LuaBlock root, List list)
{
foreach (var (l, _) in EnumerateLists(root.Children))
if (ReferenceEquals(l, list)) return true;
foreach (var (l, _) in EnumerateLists(root.ElseChildren))
if (ReferenceEquals(l, list)) return true;
return false;
}
// ===================== 鼠标 =====================
protected override void OnMouseDown(MouseEventArgs e)
{
Focus();
var p = ToLogical(e.Location);
var row = HitRow(p);
if (row == null || row.Kind is RowKind.Slot or RowKind.End)
{
// 空槽 / 封口行:不进入拖动,仅选中所属容器
Select(row?.Block);
base.OnMouseDown(e);
return;
}
if (e.Button == MouseButtons.Right)
{
Select(row.Block);
BuildMenu().Show(this, p);
return;
}
Select(row.Block);
_mouseDown = e.Location;
_dragBlock = row.Block;
_dragging = false;
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
var lp = ToLogical(e.Location);
var h = HitRow(lp);
var nb = h?.Block;
if (!ReferenceEquals(nb, _hover)) { _hover = nb; Cursor = nb == null ? Cursors.Default : Cursors.Hand; Invalidate(); }
if (_dragBlock != null && e.Button == MouseButtons.Left)
{
if (!_dragging && (Math.Abs(e.X - _mouseDown.X) > 6 || Math.Abs(e.Y - _mouseDown.Y) > 6))
_dragging = true;
if (_dragging)
{
Cursor = Cursors.SizeAll;
UpdateDropIndicator(lp);
}
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (_dragging && _dragBlock != null && _dropTarget is { } t)
{
var moving = _dragBlock;
if (!IsListInside(moving, t.List))
{
int oldIdx = t.List.IndexOf(moving);
RemoveFrom(Blocks, moving);
int idx = t.Index;
if (oldIdx >= 0 && oldIdx < idx) idx--;
idx = Math.Clamp(idx, 0, t.List.Count);
t.List.Insert(idx, moving);
Select(moving);
Changed?.Invoke();
}
}
_dragBlock = null;
_dragging = false;
_dropTarget = null;
Cursor = Cursors.Default;
Invalidate();
base.OnMouseUp(e);
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
var row = HitRow(ToLogical(e.Location));
if (row != null && row.Kind == RowKind.Block) EditRequested?.Invoke(row.Block);
base.OnMouseDoubleClick(e);
}
protected override void OnMouseLeave(EventArgs e)
{
if (_hover != null) { _hover = null; Invalidate(); }
base.OnMouseLeave(e);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (_selected != null)
{
switch (e.KeyCode)
{
case Keys.Delete: RemoveSelected(); e.Handled = true; break;
case Keys.Up when e.Control: MoveSelected(-1); e.Handled = true; break;
case Keys.Down when e.Control: MoveSelected(1); e.Handled = true; break;
}
}
base.OnKeyDown(e);
}
private ContextMenuStrip BuildMenu()
{
var m = new ContextMenuStrip { Font = new Font("Microsoft YaHei UI", 9F) };
m.Items.Add("编辑参数", null, (s, e) => { if (_selected != null) EditRequested?.Invoke(_selected); });
m.Items.Add("复制一份", null, (s, e) => DuplicateSelected());
m.Items.Add("上移", null, (s, e) => MoveSelected(-1));
m.Items.Add("下移", null, (s, e) => MoveSelected(1));
m.Items.Add(new ToolStripSeparator());
var def = _selected == null ? null : BlockCatalog.Find(_selected.Key);
if (def is { Shape: BlockShape.If or BlockShape.IfExists })
m.Items.Add(_selected!.HasElse ? "移除「否则」分支" : "添加「否则」分支", null, (s, e) => ToggleElse());
m.Items.Add("删除", null, (s, e) => RemoveSelected());
return m;
}
// ===================== 命中 / 插入点 =====================
private Point ToLogical(Point client)
=> new(client.X - AutoScrollPosition.X, client.Y - AutoScrollPosition.Y);
private Row? HitRow(Point p)
{
// 后画的在上面,倒序命中
for (int i = _rows.Count - 1; i >= 0; i--)
if (_rows[i].Bounds.Contains(p)) return _rows[i];
return null;
}
private void UpdateDropIndicator(Point p)
{
var hit = ComputeInsert(p);
_dropTarget = (hit.List, hit.Index);
_dropY = hit.Y;
_dropX = hit.X;
Invalidate();
}
private (List List, int Index, int Y, int X) ComputeInsert(Point p)
{
if (_rows.Count == 0) return (Blocks, 0, PadTop, PadX);
Row? hit = null;
foreach (var r in _rows)
if (p.Y >= r.Bounds.Top && p.Y <= r.Bounds.Bottom) { hit = r; break; }
if (hit == null)
{
for (int i = _rows.Count - 1; i >= 0; i--)
if (p.Y > _rows[i].Bounds.Bottom) { hit = _rows[i]; break; }
}
if (hit == null) return (Blocks, 0, PadTop, PadX);
int x = hit.Bounds.Left;
bool upper = p.Y < hit.Bounds.Top + hit.Bounds.Height / 2;
switch (hit.Kind)
{
case RowKind.Slot:
return (hit.Owner, hit.Index, hit.Bounds.Top + 2, x + WallW);
case RowKind.Else:
return upper ? (hit.Block.Children, hit.Block.Children.Count, hit.Bounds.Top + 1, x)
: (hit.Owner, hit.Index, hit.Bounds.Bottom + 1, x);
case RowKind.End:
return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left + WallW);
default:
if (upper) return (hit.Owner, hit.Index, hit.Bounds.Top + 1, hit.Bounds.Left);
var def = BlockCatalog.Find(hit.Block.Key);
if (def?.IsContainer == true)
return (hit.Block.Children, 0, hit.Bounds.Bottom + 2, hit.Bounds.Left + WallW);
return (hit.Owner, hit.Index + 1, hit.Bounds.Bottom + 1, hit.Bounds.Left);
}
}
private void InsertAtDropTarget(LuaBlock block)
{
if (_dropTarget is { } t)
{
int idx = Math.Clamp(t.Index, 0, t.List.Count);
t.List.Insert(idx, block);
}
else Blocks.Add(block);
Changed?.Invoke();
}
// ===================== 绘制 =====================
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.TranslateTransform(AutoScrollPosition.X, AutoScrollPosition.Y);
_rows.Clear();
int y = PadTop;
int w = Math.Max(200, ClientSize.Width - PadX - 20);
DrawList(g, Blocks, 0, ref y, PadX, w);
if (Blocks.Count == 0)
{
using var f = new Font("Microsoft YaHei UI", 10F);
using var b = new SolidBrush(Color.FromArgb(150, 160, 178));
g.DrawString("从左侧命令库双击或拖入积木,开始搭建流程", f, b, PadX + 8, PadTop + 6);
}
if (_dropTarget != null)
{
using var pen = new Pen(Color.FromArgb(37, 99, 235), 2);
int ww = Math.Max(140, ClientSize.Width - _dropX - 30);
g.DrawLine(pen, _dropX, _dropY, _dropX + ww, _dropY);
g.FillEllipse(Brushes.DodgerBlue, _dropX - 3, _dropY - 3, 6, 6);
}
int totalH = y + NotchH + 30;
if (AutoScrollMinSize.Height != totalH)
AutoScrollMinSize = new Size(0, totalH);
}
private static int MeasureList(List list)
{
int h = 0;
foreach (var b in list)
{
var def = BlockCatalog.Find(b.Key);
if (def == null) continue;
h += RowH;
if (def.IsContainer) h += InnerHeight(b, def) + FootH;
}
return h;
}
private static int InnerHeight(LuaBlock b, BlockDef def)
{
int h = MeasureList(b.Children);
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
h += ElseH + MeasureList(b.ElseChildren);
return Math.Max(MinInner, h);
}
private void DrawList(Graphics g, List list, int depth, ref int y, int x, int w)
{
for (int i = 0; i < list.Count; i++)
{
var b = list[i];
var def = BlockCatalog.Find(b.Key);
if (def == null) continue;
bool sel = ReferenceEquals(b, _selected);
bool dim = _dragging && ReferenceEquals(b, _dragBlock);
var color = CategoryColor(def.Category);
if (!def.IsContainer)
{
var rect = new Rectangle(x, y, w, RowH);
using (var path = PuzzlePath(x, y, w, RowH, NotchX))
{
FillBlock(g, path, rect, color, sel, dim);
}
DrawLabel(g, b, def, x + 12, y + 7, w - 24, color);
_rows.Add(new Row { Block = b, Depth = depth, Bounds = rect, Owner = list, Index = i, Kind = RowKind.Block });
y += RowH;
continue;
}
// ---------- 容器块(C 形插槽)----------
int innerH = InnerHeight(b, def);
int headTop = y;
int innerTop = headTop + RowH;
int footTop = innerTop + innerH;
int footBottom = footTop + FootH;
using (var shell = ContainerPath(x, headTop, w, RowH, innerH, FootH, NotchX))
{
FillBlock(g, shell, new Rectangle(x, headTop, w, footBottom - headTop), color, sel, dim);
}
// 内腔插槽底
using (var innerBg = new SolidBrush(Color.FromArgb(233, 238, 246)))
g.FillRectangle(innerBg, x + WallW, innerTop, w - WallW, innerH);
using (var lipPen = new Pen(Darken(color, 0.18), 1f))
g.DrawRectangle(lipPen, x + WallW, innerTop, w - WallW - 1, innerH - 1);
DrawLabel(g, b, def, x + 12, headTop + 7, w - 24, color);
_rows.Add(new Row
{
Block = b, Depth = depth,
Bounds = new Rectangle(x, headTop, w, RowH),
Owner = list, Index = i, Kind = RowKind.Block
});
int cy = innerTop;
int iw = w - WallW;
if (b.Children.Count == 0)
{
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, cy, iw, Math.Max(20, innerH - NotchH)),
Owner = b.Children, Index = 0, Kind = RowKind.Slot
});
DrawSlotHint(g, x + WallW + 8, cy + 6);
}
else
{
DrawList(g, b.Children, depth + 1, ref cy, x + WallW, iw);
}
var tailList = b.Children;
if (def.Shape is BlockShape.If or BlockShape.IfExists && b.HasElse)
{
int elseY = innerTop + NotchH + Math.Max(0, MeasureList(b.Children));
using var eb = new SolidBrush(Darken(color, 0.18));
g.FillRectangle(eb, x, elseY, w, ElseH);
using var ef = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
using var et = new SolidBrush(Color.FromArgb(240, 245, 252));
g.DrawString("否则", ef, et, x + WallW + 4, elseY + 3);
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, elseY, iw, ElseH),
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Else
});
int ey = elseY + ElseH;
if (b.ElseChildren.Count == 0)
{
_rows.Add(new Row
{
Block = b, Depth = depth + 1,
Bounds = new Rectangle(x + WallW, ey, iw, Math.Max(18, footTop - ey)),
Owner = b.ElseChildren, Index = 0, Kind = RowKind.Slot
});
DrawSlotHint(g, x + WallW + 8, ey + 4);
}
else
{
DrawList(g, b.ElseChildren, depth + 1, ref ey, x + WallW, iw);
}
tailList = b.ElseChildren;
}
_rows.Add(new Row
{
Block = b, Depth = depth,
Bounds = new Rectangle(x, footTop, w, FootH),
Owner = tailList, Index = tailList.Count, Kind = RowKind.End
});
y = footBottom;
}
}
private void FillBlock(Graphics g, GraphicsPath path, Rectangle r, Color color, bool sel, bool dim)
{
using (var fill = new LinearGradientBrush(
new Point(r.Left, r.Top), new Point(r.Left, r.Bottom),
Lighten(color, dim ? 0.42 : 0.16), dim ? Lighten(color, 0.42) : color))
{
g.FillPath(fill, path);
}
using var edge = new Pen(Darken(color, 0.32), 1f);
g.DrawPath(edge, path);
if (sel)
{
using var glow = new Pen(Color.FromArgb(250, 204, 21), 2.5f);
g.DrawPath(glow, path);
}
}
private static void DrawSlotHint(Graphics g, int x, int y)
{
using var f = new Font("Microsoft YaHei UI", 8F);
using var b = new SolidBrush(Color.FromArgb(158, 170, 190));
g.DrawString("把积木拖到这里", f, b, x, y);
}
// ---------- 标题(参数画成白色插槽)----------
private void DrawLabel(Graphics g, LuaBlock b, BlockDef def, int x, int y, int maxW, Color color)
{
float cx = x;
using var white = new SolidBrush(Color.White);
using var slotBg = new SolidBrush(Color.FromArgb(246, 248, 252));
using var slotFg = new SolidBrush(Color.FromArgb(48, 58, 74));
using var fmtC = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
foreach (var (text, isArg) in SplitTitle(def, b))
{
if (text.Length == 0) continue;
if (cx > x + maxW) break;
if (!isArg)
{
g.DrawString(text, TitleFont, white, cx, y);
cx += g.MeasureString(text, TitleFont).Width;
}
else
{
var sz = g.MeasureString(text, TitleFont);
float wpx = Math.Min(sz.Width + 14, Math.Max(48, maxW / 3));
var rect = new RectangleF(cx, y - 2f, wpx, 20);
using var path = Rounded(Rectangle.Round(rect), 5);
g.FillPath(slotBg, path);
using var pen = new Pen(Darken(color, 0.30), 1f);
g.DrawPath(pen, path);
var clip = new RectangleF(rect.Left + 2, rect.Top, rect.Width - 4, rect.Height);
g.DrawString(text, TitleFont, slotFg, clip, fmtC);
cx += wpx + 5;
}
}
}
private static List<(string Text, bool IsArg)> SplitTitle(BlockDef def, LuaBlock b)
{
var segs = new List<(string, bool)>();
if (def.Args.Length == 0) { segs.Add((def.Title, false)); return segs; }
int pos = 0;
for (int i = 0; i < def.Args.Length; i++)
{
string token = "{" + i + "}";
int idx = def.Title.IndexOf(token, StringComparison.Ordinal);
if (idx < 0 || idx < pos) continue;
if (idx > pos) segs.Add((def.Title[pos..idx], false));
segs.Add((b.Value(i), true));
pos = idx + token.Length;
}
if (pos < def.Title.Length) segs.Add((def.Title[pos..], false));
return segs;
}
// ---------- 路径:插片耦合造型 ----------
private static void TopEdge(GraphicsPath p, int x, int y, int w, int r, int nx)
{
p.AddLine(x + r, y, x + nx, y);
p.AddLine(x + nx, y, x + nx + NotchSlope, y + NotchH);
p.AddLine(x + nx + NotchSlope, y + NotchH, x + nx + NotchSlope + NotchFlat, y + NotchH);
p.AddLine(x + nx + NotchSlope + NotchFlat, y + NotchH, x + nx + 2 * NotchSlope + NotchFlat, y);
p.AddLine(x + nx + 2 * NotchSlope + NotchFlat, y, x + w - r, y);
p.AddArc(x + w - 2 * r, y, 2 * r, 2 * r, 270, 90);
}
private static void BottomEdge(GraphicsPath p, int x, int bottom, int w, int r, int nx)
{
int nEnd = x + nx + 2 * NotchSlope + NotchFlat;
p.AddLine(x + w - r, bottom, nEnd, bottom);
p.AddLine(nEnd, bottom, nEnd - NotchSlope, bottom + NotchH);
p.AddLine(nEnd - NotchSlope, bottom + NotchH, nEnd - NotchSlope - NotchFlat, bottom + NotchH);
p.AddLine(nEnd - NotchSlope - NotchFlat, bottom + NotchH, x + nx, bottom);
p.AddLine(x + nx, bottom, x + r, bottom);
p.AddArc(x, bottom - 2 * r, 2 * r, 2 * r, 90, 90);
}
/// 单块:顶部凹槽 + 底部凸榫。
private static GraphicsPath PuzzlePath(int x, int y, int w, int h, int nx)
{
var p = new GraphicsPath();
int d = Radius * 2;
int bottom = y + h;
p.StartFigure();
p.AddArc(x, y, d, d, 180, 90); // 左上
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
p.AddLine(x + w, y + Radius, x + w, bottom - Radius); // 右
p.AddArc(x + w - d, bottom - d, d, d, 0, 90); // 右下
BottomEdge(p, x, bottom, w, Radius, nx); // 底边(凸榫)+ 左下
p.CloseFigure();
return p;
}
/// 容器块:C 形,中间挖出插槽。
private static GraphicsPath ContainerPath(int x, int y, int w, int headH, int innerH, int footH, int nx)
{
var p = new GraphicsPath();
int d = Radius * 2;
int innerTop = y + headH;
int footTop = innerTop + innerH;
int footBottom = footTop + footH;
p.StartFigure();
p.AddArc(x, y, d, d, 180, 90); // 左上
TopEdge(p, x, y, w, Radius, nx); // 顶边(凹槽)+ 右上
p.AddLine(x + w, y + Radius, x + w, footBottom - Radius); // 右外墙
p.AddArc(x + w - d, footBottom - d, d, d, 0, 90); // 右下
BottomEdge(p, x, footBottom, w, Radius, nx); // 底边(凸榫)+ 左下
p.AddLine(x, footBottom - Radius, x, footTop + Radius); // 左外墙上返
p.AddArc(x, footTop, d, d, 90, 90); // 封口左上内角
p.AddLine(x + Radius, footTop, x + WallW, footTop); // 插槽底边
p.AddLine(x + WallW, footTop, x + WallW, innerTop); // 左壁内侧
p.AddLine(x + WallW, innerTop, x + Radius, innerTop); // 插槽顶边
p.AddArc(x, innerTop - d, d, d, 270, 90); // 插槽左上内角
p.CloseFigure();
return p;
}
private static GraphicsPath Rounded(Rectangle r, int radius)
{
var p = new GraphicsPath();
int d = radius * 2;
p.AddArc(r.Left, r.Top, d, d, 180, 90);
p.AddArc(r.Right - d, r.Top, d, d, 270, 90);
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
p.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
p.CloseFigure();
return p;
}
// ---------- 颜色 ----------
private static Color CategoryColor(string category) => category switch
{
"流程" => Color.FromArgb(59, 130, 246),
"鼠标" => Color.FromArgb(16, 175, 122),
"键盘" => Color.FromArgb(214, 140, 18),
"视觉" => Color.FromArgb(139, 92, 246),
"系统" => Color.FromArgb(226, 104, 72),
"浏览器" => Color.FromArgb(14, 160, 226),
_ => Color.FromArgb(100, 116, 139)
};
private static Color Lighten(Color c, double f) => Color.FromArgb(
(int)Math.Min(255, c.R + 255 * f),
(int)Math.Min(255, c.G + 255 * f),
(int)Math.Min(255, c.B + 255 * f));
private static Color Darken(Color c, double f) => Color.FromArgb(
(int)(c.R * (1 - f)), (int)(c.G * (1 - f)), (int)(c.B * (1 - f)));
}
}