TankRenderer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. using Avalonia;
  2. using Avalonia.Media;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. namespace YZWater.Avalonia.Controls.Renderers;
  7. /// <summary>
  8. /// 水箱渲染器。
  9. /// <list type="bullet">
  10. /// <item><see cref="Draw"/>:简单矩形水箱(兼容模式)</item>
  11. /// <item><see cref="DrawSection"/>:剖面池(SCADA 拟真)— 池壁质感 + 液位填充 +
  12. /// 正弦波浪动画 + 液位大字 + 左侧刻度尺 + 彩色阈值线(启泵/停泵/保护液位)</item>
  13. /// </list>
  14. /// </summary>
  15. public static class TankRenderer
  16. {
  17. /// <summary>阈值线定义(液位为与 waterLevel 同单位的原始值)</summary>
  18. public record ThresholdLine(double Level, Color Color, string Label);
  19. /// <summary>
  20. /// 绘制一个简单矩形水箱(兼容模式,保留原样式)。
  21. /// </summary>
  22. public static void Draw(
  23. DrawingContext context,
  24. Rect bounds,
  25. double waterLevel,
  26. Color waterColor,
  27. string name,
  28. string code,
  29. IBrush bgBrush,
  30. IBrush borderBrush,
  31. IBrush textPrimaryBrush,
  32. IBrush textTertiaryBrush,
  33. IBrush warningBrush,
  34. IBrush dangerBrush)
  35. {
  36. var x = bounds.X;
  37. var y = bounds.Y;
  38. var w = bounds.Width;
  39. var h = bounds.Height;
  40. // 箱体
  41. context.DrawRectangle(bgBrush, new Pen(borderBrush, 1.5), bounds, 3, 3);
  42. // 水位填充
  43. var clampedLevel = Math.Clamp(waterLevel, 0, 100);
  44. if (clampedLevel > 0.5)
  45. {
  46. var waterH = (h - 4) * clampedLevel / 100.0;
  47. var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH);
  48. var waterBrush = new SolidColorBrush(waterColor, 0.65);
  49. context.DrawRectangle(waterBrush, null, waterRect);
  50. // 水面波纹
  51. var waveY = waterRect.Y;
  52. var waveBrush = new SolidColorBrush(waterColor, 0.35);
  53. for (int i = 0; i < 3; i++)
  54. {
  55. var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0);
  56. context.DrawLine(new Pen(waveBrush, 1),
  57. new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2));
  58. }
  59. }
  60. var centerX = x + w / 2;
  61. var centerY = y + h / 2;
  62. // 名称标签
  63. var nameFontSize = Math.Max(8, Math.Min(w, h) * 0.077);
  64. DrawTextAt(context, centerX - 20, y + 6, name, nameFontSize, textPrimaryBrush, FontWeight.Bold);
  65. var codeFontSize = Math.Max(8, Math.Min(w, h) * 0.063);
  66. DrawTextAt(context, centerX - 14, y + 20, code, codeFontSize, textTertiaryBrush);
  67. // 液位百分比
  68. var pctText = $"{clampedLevel:F1}%";
  69. IBrush pctColor = clampedLevel > 80 ? warningBrush :
  70. clampedLevel < 20 ? dangerBrush :
  71. new SolidColorBrush(waterColor);
  72. var pctFontSize = Math.Max(8, Math.Min(w, h) * 0.092);
  73. DrawTextAt(context, centerX - 18, centerY + 8, pctText, pctFontSize, pctColor, FontWeight.Bold);
  74. }
  75. // ════════════════════════════════════════════════════════
  76. // 剖面池(拟真模式)
  77. // ════════════════════════════════════════════════════════
  78. /// <summary>
  79. /// 绘制剖面式水池:池壁(混凝土质感)+ 液位填充 + 正弦波浪动画 +
  80. /// 液位大字 + 左侧刻度尺 + 彩色阈值横线。
  81. /// </summary>
  82. /// <param name="context">绘图上下文</param>
  83. /// <param name="bounds">池体矩形(屏幕坐标,含池壁)</param>
  84. /// <param name="waterLevel">当前液位(原始值,如米)</param>
  85. /// <param name="maxLevel">满量程液位(原始值)</param>
  86. /// <param name="waterColor">水体颜色</param>
  87. /// <param name="name">池名称</param>
  88. /// <param name="wavePhase">波浪动画相位(0~360)</param>
  89. /// <param name="thresholds">阈值线集合(按原始液位值)</param>
  90. /// <param name="palette">主题画刷</param>
  91. public static void DrawSection(
  92. DrawingContext context,
  93. Rect bounds,
  94. double waterLevel,
  95. double maxLevel,
  96. Color waterColor,
  97. string name,
  98. double wavePhase,
  99. IReadOnlyList<ThresholdLine> thresholds,
  100. SectionPalette palette)
  101. {
  102. var wall = Math.Max(4, Math.Min(bounds.Width, bounds.Height) * 0.06); // 池壁厚度
  103. var inner = new Rect(bounds.X + wall, bounds.Y + wall * 0.6,
  104. bounds.Width - wall * 2, bounds.Height - wall * 0.6 - wall * 0.5);
  105. // ─── 池壁(外框,混凝土质感 + U 形实体壁)───
  106. // 池底
  107. var wallBrush = new SolidColorBrush(Color.FromArgb(180, 90, 98, 112));
  108. context.DrawRectangle(wallBrush, new Pen(ThemeHelper.Border, 1.5), bounds, 2, 2);
  109. // U 形内壁(左/右/底)
  110. var innerWall = new SolidColorBrush(Color.FromArgb(140, 55, 62, 74));
  111. // 左壁
  112. context.DrawRectangle(innerWall, null,
  113. new Rect(inner.X - wall, inner.Y, wall, inner.Height));
  114. // 右壁
  115. context.DrawRectangle(innerWall, null,
  116. new Rect(inner.Right, inner.Y, wall, inner.Height));
  117. // 池底
  118. context.DrawRectangle(innerWall, null,
  119. new Rect(inner.X, inner.Bottom, inner.Width, wall));
  120. // 内壁阴影线(增强立体感)
  121. context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
  122. new Point(inner.X, inner.Y), new Point(inner.X, inner.Bottom));
  123. context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
  124. new Point(inner.Right, inner.Y), new Point(inner.Right, inner.Bottom));
  125. context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5),
  126. new Point(inner.X, inner.Bottom), new Point(inner.Right, inner.Bottom));
  127. // ─── 内腔背景 ───
  128. context.DrawRectangle(palette.CavityBrush, null, inner);
  129. // ── 液位填充 + 正弦波浪 ──
  130. if (maxLevel <= 0) maxLevel = 1;
  131. var levelPct = Math.Clamp(waterLevel / maxLevel, 0, 1);
  132. var waterTop = inner.Bottom - inner.Height * levelPct;
  133. // 波浪幅度随池高缩放(比旧版更明显)
  134. var amp1 = Math.Max(4, inner.Height * 0.045);
  135. if (levelPct > 0.01)
  136. {
  137. var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop);
  138. var waterBrush = new LinearGradientBrush
  139. {
  140. StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
  141. EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
  142. GradientStops =
  143. {
  144. new GradientStop(Color.FromArgb(210, waterColor.R, waterColor.G, waterColor.B), 0),
  145. new GradientStop(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B), 1),
  146. }
  147. };
  148. context.DrawRectangle(waterBrush, null, waterRect);
  149. // 水面亮线(液面与空气交界处的高光)
  150. context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8),
  151. new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop));
  152. // 正弦波浪(3 层相位错开,半透明叠加)
  153. DrawWave(context, waterTop, inner, waterColor, wavePhase, 0.55, amp1, 1.0);
  154. DrawWave(context, waterTop + 2, inner, waterColor, wavePhase * 1.3 + 120, 0.32, amp1 * 1.3, 0.7);
  155. DrawWave(context, waterTop + 4, inner, waterColor, wavePhase * 0.8 + 240, 0.20, amp1 * 1.6, 0.5);
  156. // 池内气泡上升动画(6 个气泡,相位不同,持续上升)
  157. DrawBubbles(context, inner, waterRect, waterColor, wavePhase, levelPct);
  158. }
  159. else
  160. {
  161. // 空池:池底积水带 + 沉积示意,避免"死白框"
  162. var siltRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.04, inner.Width, inner.Height * 0.04);
  163. context.DrawRectangle(new SolidColorBrush(Color.FromArgb(90, 110, 120, 100)), null, siltRect);
  164. var residRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.02, inner.Width, inner.Height * 0.02);
  165. context.DrawRectangle(new SolidColorBrush(Color.FromArgb(140, waterColor.R, waterColor.G, waterColor.B)), null, residRect);
  166. }
  167. // ─── 阈值线(画在水位之上,一眼看出距离)───
  168. if (thresholds != null)
  169. {
  170. foreach (var th in thresholds)
  171. {
  172. if (th.Level <= 0 || th.Level > maxLevel) continue;
  173. var ty = inner.Bottom - inner.Height * Math.Clamp(th.Level / maxLevel, 0, 1);
  174. var pen = new Pen(new SolidColorBrush(th.Color, 0.9), 1.5, DashStyle.Dash);
  175. context.DrawLine(pen, new Point(inner.X, ty), new Point(inner.Right, ty));
  176. // 阈值标签(左侧小字,紧贴线上方)
  177. var label = $"{th.Label} {th.Level:F2}";
  178. var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  179. new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold),
  180. Math.Max(8, inner.Height * 0.055),
  181. new SolidColorBrush(th.Color));
  182. context.DrawText(ft, new Point(inner.X + 2, ty - ft.Height - 1));
  183. }
  184. }
  185. // ─── 当前液位值(气泡浮标:跟随水面高度,贴池右侧)───
  186. var valueText = $"{waterLevel:F2} m";
  187. var valueSize = Math.Max(11, inner.Height * 0.085);
  188. var valueFt = new FormattedText(valueText, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  189. new Typeface("Consolas", FontStyle.Normal, FontWeight.Bold), valueSize, palette.TextPrimaryBrush);
  190. var badgeW = valueFt.Width + 14;
  191. var badgeH = valueFt.Height + 8;
  192. var badgeX = inner.Right - badgeW - 6;
  193. var badgeY = Math.Clamp(waterTop - badgeH - 8, inner.Y + 2, inner.Bottom - badgeH - 2);
  194. // 浮标牌 + 指向水面的指针
  195. var badgeRect = new Rect(badgeX, badgeY, badgeW, badgeH);
  196. context.DrawRectangle(palette.CavityBrush, new Pen(palette.BorderBrush, 1), badgeRect, 4, 4);
  197. context.DrawLine(new Pen(palette.BorderBrush, 1.2),
  198. new Point(badgeX + badgeW / 2, badgeY + badgeH),
  199. new Point(badgeX + badgeW / 2, waterTop));
  200. context.DrawEllipse(new SolidColorBrush(waterColor), null,
  201. new Rect(badgeX + badgeW / 2 - 3, waterTop - 3, 6, 6));
  202. context.DrawText(valueFt,
  203. new Point(badgeRect.Center.X - valueFt.Width / 2, badgeRect.Center.Y - valueFt.Height / 2));
  204. // ─── 池名称(顶部)───
  205. var nameSize = Math.Max(9, bounds.Height * 0.07);
  206. var nameFt = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  207. new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold), nameSize, palette.TextPrimaryBrush);
  208. context.DrawText(nameFt, new Point(bounds.X + (bounds.Width - nameFt.Width) / 2, bounds.Y - nameFt.Height - 2));
  209. // ─── 水面亮线(液面与空气交界处的高光)───
  210. if (levelPct > 0.01)
  211. {
  212. var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop);
  213. context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8),
  214. new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop));
  215. }
  216. // ─── 左侧刻度尺 ───
  217. DrawRuler(context, bounds, inner, maxLevel, palette);
  218. }
  219. /// <summary>池内气泡上升动画(6 个气泡,不同相位,从池底持续升起破裂)</summary>
  220. private static void DrawBubbles(DrawingContext context, Rect inner, Rect waterRect,
  221. Color waterColor, double phase, double levelPct)
  222. {
  223. const int bubbleCount = 6;
  224. var bubbleBrush = new SolidColorBrush(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B));
  225. for (int i = 0; i < bubbleCount; i++)
  226. {
  227. // 每个气泡的相位偏移不同
  228. var p = (phase + i * 67) % 360;
  229. var t = p / 360.0; // 0→1 从池底到水面
  230. var bx = inner.X + inner.Width * (0.15 + (i * 0.14 + Math.Sin(p * 0.05) * 0.08));
  231. var by = inner.Bottom - inner.Height * levelPct * t - (inner.Height * 0.05); // 到水面以下
  232. // 气泡大小随上升略缩小
  233. var bSize = Math.Max(1.5, 3.5 * (1 - t * 0.3));
  234. var bubbleAlpha = (byte)(140 * (1 - t * 0.6));
  235. // 只在液面以下绘制
  236. if (by >= inner.Y && by <= waterRect.Bottom)
  237. {
  238. context.DrawEllipse(new SolidColorBrush(Color.FromArgb(bubbleAlpha,
  239. waterColor.R, waterColor.G, waterColor.B)), null,
  240. new Rect(bx - bSize, by - bSize, bSize * 2, bSize * 2));
  241. }
  242. }
  243. }
  244. /// <summary>绘制单层正弦波浪(填充至池底,裁剪在内腔范围内)</summary>
  245. private static void DrawWave(DrawingContext context, double topY, Rect inner,
  246. Color waterColor, double phase, double alpha, double amplitude, double wavelengthScale)
  247. {
  248. if (topY < inner.Y - amplitude || topY > inner.Bottom) return;
  249. const int steps = 24;
  250. var geometry = new StreamGeometry();
  251. using (var ctx = geometry.Open())
  252. {
  253. ctx.BeginFigure(new Point(inner.X, inner.Bottom), isFilled: true);
  254. // 从左到右沿正弦曲线采样
  255. for (int i = 0; i <= steps; i++)
  256. {
  257. var t = (double)i / steps;
  258. var px = inner.X + inner.Width * t;
  259. var py = topY + Math.Sin((t * Math.PI * 2 * wavelengthScale) + (phase * Math.PI / 180)) * amplitude;
  260. ctx.LineTo(new Point(px, py), isStroked: false);
  261. }
  262. // 闭合到池底
  263. ctx.EndFigure(true);
  264. }
  265. var brush = new SolidColorBrush(waterColor, alpha);
  266. context.DrawGeometry(brush, null, geometry);
  267. }
  268. /// <summary>左侧刻度尺(0~maxLevel,按 5 段标注)</summary>
  269. private static void DrawRuler(DrawingContext context, Rect bounds, Rect inner,
  270. double maxLevel, SectionPalette palette)
  271. {
  272. const int segments = 5;
  273. var tickLen = Math.Max(3, bounds.Width * 0.02);
  274. var fontSize = Math.Max(7, bounds.Height * 0.045);
  275. var pen = new Pen(palette.TextTertiaryBrush, 1);
  276. for (int i = 0; i <= segments; i++)
  277. {
  278. var value = maxLevel * i / segments;
  279. var y = inner.Bottom - inner.Height * ((double)i / segments);
  280. context.DrawLine(pen, new Point(bounds.X - tickLen, y), new Point(bounds.X - 1, y));
  281. var label = value.ToString("F1", CultureInfo.CurrentCulture);
  282. var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  283. new Typeface("Consolas"), fontSize, palette.TextTertiaryBrush);
  284. context.DrawText(ft, new Point(bounds.X - tickLen - ft.Width - 2, y - ft.Height / 2));
  285. }
  286. }
  287. private static void DrawTextAt(DrawingContext context, double x, double y, string text,
  288. double size, IBrush brush, FontWeight weight = FontWeight.Normal)
  289. {
  290. var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
  291. new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush);
  292. context.DrawText(formatted, new Point(x, y));
  293. }
  294. }
  295. /// <summary>剖面池主题画刷集(从 ThemeHelper 取色后传入,控件不用 DynamicResource)</summary>
  296. public record SectionPalette(
  297. IBrush WallBrush, // 池壁(混凝土)
  298. IBrush WallShadowBrush, // 池壁内侧阴影线
  299. IBrush CavityBrush, // 内腔背景
  300. IBrush BorderBrush, // 边框
  301. IBrush TextPrimaryBrush, // 主文字
  302. IBrush TextTertiaryBrush // 辅助文字(刻度)
  303. );