773 lines
30 KiB
C#
773 lines
30 KiB
C#
using System.Drawing.Drawing2D;
|
|
using anjianjingling.Lua;
|
|
|
|
namespace anjianjingling.Forms
|
|
{
|
|
/// <summary>
|
|
/// 纯 WinForms 自绘积木画布:插片耦合(榫卯)造型。
|
|
/// 顶部凹槽 + 底部凸榫互相咬合;容器块为 C 形插槽;参数显示为可插入的白色插槽。
|
|
/// </summary>
|
|
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<LuaBlock> 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<Row> _rows = new();
|
|
private LuaBlock? _selected;
|
|
private LuaBlock? _hover;
|
|
private Point _mouseDown;
|
|
private LuaBlock? _dragBlock;
|
|
private bool _dragging;
|
|
private (List<LuaBlock> 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<LuaBlock> Blocks { get; } = new();
|
|
public event Action? Changed;
|
|
public event Action<LuaBlock?>? SelectionChanged;
|
|
public event Action<LuaBlock>? 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<LuaBlock> List, int Depth)> EnumerateLists(List<LuaBlock> 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);
|
|
}
|
|
|
|
/// <summary>内容有变化(含参数修改)时由外部触发重绘与变更通知。</summary>
|
|
public void NotifyChanged()
|
|
{
|
|
Invalidate();
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
public void LoadBlocks(IEnumerable<LuaBlock> 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<LuaBlock> 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<LuaBlock> 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<LuaBlock> 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<LuaBlock> 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<LuaBlock> 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);
|
|
}
|
|
|
|
/// <summary>单块:顶部凹槽 + 底部凸榫。</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>容器块:C 形,中间挖出插槽。</summary>
|
|
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)));
|
|
}
|
|
}
|