using Avalonia; using Avalonia.Media; using System; using System.Collections.Generic; using System.Globalization; namespace YZWater.Avalonia.Controls.Renderers; /// /// 水箱渲染器。 /// /// :简单矩形水箱(兼容模式) /// :剖面池(SCADA 拟真)— 池壁质感 + 液位填充 + /// 正弦波浪动画 + 液位大字 + 左侧刻度尺 + 彩色阈值线(启泵/停泵/保护液位) /// /// public static class TankRenderer { /// 阈值线定义(液位为与 waterLevel 同单位的原始值) public record ThresholdLine(double Level, Color Color, string Label); /// /// 绘制一个简单矩形水箱(兼容模式,保留原样式)。 /// public static void Draw( DrawingContext context, Rect bounds, double waterLevel, Color waterColor, string name, string code, IBrush bgBrush, IBrush borderBrush, IBrush textPrimaryBrush, IBrush textTertiaryBrush, IBrush warningBrush, IBrush dangerBrush) { var x = bounds.X; var y = bounds.Y; var w = bounds.Width; var h = bounds.Height; // 箱体 context.DrawRectangle(bgBrush, new Pen(borderBrush, 1.5), bounds, 3, 3); // 水位填充 var clampedLevel = Math.Clamp(waterLevel, 0, 100); if (clampedLevel > 0.5) { var waterH = (h - 4) * clampedLevel / 100.0; var waterRect = new Rect(x + 2, y + h - 2 - waterH, w - 4, waterH); var waterBrush = new SolidColorBrush(waterColor, 0.65); context.DrawRectangle(waterBrush, null, waterRect); // 水面波纹 var waveY = waterRect.Y; var waveBrush = new SolidColorBrush(waterColor, 0.35); for (int i = 0; i < 3; i++) { var wx = waterRect.X + 4 + i * (waterRect.Width / 3.0); context.DrawLine(new Pen(waveBrush, 1), new Point(wx, waveY), new Point(wx + waterRect.Width / 6.0, waveY + 2)); } } var centerX = x + w / 2; var centerY = y + h / 2; // 名称标签 var nameFontSize = Math.Max(8, Math.Min(w, h) * 0.077); DrawTextAt(context, centerX - 20, y + 6, name, nameFontSize, textPrimaryBrush, FontWeight.Bold); var codeFontSize = Math.Max(8, Math.Min(w, h) * 0.063); DrawTextAt(context, centerX - 14, y + 20, code, codeFontSize, textTertiaryBrush); // 液位百分比 var pctText = $"{clampedLevel:F1}%"; IBrush pctColor = clampedLevel > 80 ? warningBrush : clampedLevel < 20 ? dangerBrush : new SolidColorBrush(waterColor); var pctFontSize = Math.Max(8, Math.Min(w, h) * 0.092); DrawTextAt(context, centerX - 18, centerY + 8, pctText, pctFontSize, pctColor, FontWeight.Bold); } // ════════════════════════════════════════════════════════ // 剖面池(拟真模式) // ════════════════════════════════════════════════════════ /// /// 绘制剖面式水池:池壁(混凝土质感)+ 液位填充 + 正弦波浪动画 + /// 液位大字 + 左侧刻度尺 + 彩色阈值横线。 /// /// 绘图上下文 /// 池体矩形(屏幕坐标,含池壁) /// 当前液位(原始值,如米) /// 满量程液位(原始值) /// 水体颜色 /// 池名称 /// 波浪动画相位(0~360) /// 阈值线集合(按原始液位值) /// 主题画刷 public static void DrawSection( DrawingContext context, Rect bounds, double waterLevel, double maxLevel, Color waterColor, string name, double wavePhase, IReadOnlyList thresholds, SectionPalette palette) { var wall = Math.Max(4, Math.Min(bounds.Width, bounds.Height) * 0.06); // 池壁厚度 var inner = new Rect(bounds.X + wall, bounds.Y + wall * 0.6, bounds.Width - wall * 2, bounds.Height - wall * 0.6 - wall * 0.5); // ─── 池壁(外框,混凝土质感 + U 形实体壁)─── // 池底 var wallBrush = new SolidColorBrush(Color.FromArgb(180, 90, 98, 112)); context.DrawRectangle(wallBrush, new Pen(ThemeHelper.Border, 1.5), bounds, 2, 2); // U 形内壁(左/右/底) var innerWall = new SolidColorBrush(Color.FromArgb(140, 55, 62, 74)); // 左壁 context.DrawRectangle(innerWall, null, new Rect(inner.X - wall, inner.Y, wall, inner.Height)); // 右壁 context.DrawRectangle(innerWall, null, new Rect(inner.Right, inner.Y, wall, inner.Height)); // 池底 context.DrawRectangle(innerWall, null, new Rect(inner.X, inner.Bottom, inner.Width, wall)); // 内壁阴影线(增强立体感) context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5), new Point(inner.X, inner.Y), new Point(inner.X, inner.Bottom)); context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5), new Point(inner.Right, inner.Y), new Point(inner.Right, inner.Bottom)); context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(100, 40, 45, 55)), 1.5), new Point(inner.X, inner.Bottom), new Point(inner.Right, inner.Bottom)); // ─── 内腔背景 ─── context.DrawRectangle(palette.CavityBrush, null, inner); // ── 液位填充 + 正弦波浪 ── if (maxLevel <= 0) maxLevel = 1; var levelPct = Math.Clamp(waterLevel / maxLevel, 0, 1); var waterTop = inner.Bottom - inner.Height * levelPct; // 波浪幅度随池高缩放(比旧版更明显) var amp1 = Math.Max(4, inner.Height * 0.045); if (levelPct > 0.01) { var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop); var waterBrush = new LinearGradientBrush { StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative), GradientStops = { new GradientStop(Color.FromArgb(210, waterColor.R, waterColor.G, waterColor.B), 0), new GradientStop(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B), 1), } }; context.DrawRectangle(waterBrush, null, waterRect); // 水面亮线(液面与空气交界处的高光) context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8), new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop)); // 正弦波浪(3 层相位错开,半透明叠加) DrawWave(context, waterTop, inner, waterColor, wavePhase, 0.55, amp1, 1.0); DrawWave(context, waterTop + 2, inner, waterColor, wavePhase * 1.3 + 120, 0.32, amp1 * 1.3, 0.7); DrawWave(context, waterTop + 4, inner, waterColor, wavePhase * 0.8 + 240, 0.20, amp1 * 1.6, 0.5); // 池内气泡上升动画(6 个气泡,相位不同,持续上升) DrawBubbles(context, inner, waterRect, waterColor, wavePhase, levelPct); } else { // 空池:池底积水带 + 沉积示意,避免"死白框" var siltRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.04, inner.Width, inner.Height * 0.04); context.DrawRectangle(new SolidColorBrush(Color.FromArgb(90, 110, 120, 100)), null, siltRect); var residRect = new Rect(inner.X, inner.Bottom - inner.Height * 0.02, inner.Width, inner.Height * 0.02); context.DrawRectangle(new SolidColorBrush(Color.FromArgb(140, waterColor.R, waterColor.G, waterColor.B)), null, residRect); } // ─── 阈值线(画在水位之上,一眼看出距离)─── if (thresholds != null) { foreach (var th in thresholds) { if (th.Level <= 0 || th.Level > maxLevel) continue; var ty = inner.Bottom - inner.Height * Math.Clamp(th.Level / maxLevel, 0, 1); var pen = new Pen(new SolidColorBrush(th.Color, 0.9), 1.5, DashStyle.Dash); context.DrawLine(pen, new Point(inner.X, ty), new Point(inner.Right, ty)); // 阈值标签(左侧小字,紧贴线上方) var label = $"{th.Label} {th.Level:F2}"; var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold), Math.Max(8, inner.Height * 0.055), new SolidColorBrush(th.Color)); context.DrawText(ft, new Point(inner.X + 2, ty - ft.Height - 1)); } } // ─── 当前液位值(气泡浮标:跟随水面高度,贴池右侧)─── var valueText = $"{waterLevel:F2} m"; var valueSize = Math.Max(11, inner.Height * 0.085); var valueFt = new FormattedText(valueText, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Consolas", FontStyle.Normal, FontWeight.Bold), valueSize, palette.TextPrimaryBrush); var badgeW = valueFt.Width + 14; var badgeH = valueFt.Height + 8; var badgeX = inner.Right - badgeW - 6; var badgeY = Math.Clamp(waterTop - badgeH - 8, inner.Y + 2, inner.Bottom - badgeH - 2); // 浮标牌 + 指向水面的指针 var badgeRect = new Rect(badgeX, badgeY, badgeW, badgeH); context.DrawRectangle(palette.CavityBrush, new Pen(palette.BorderBrush, 1), badgeRect, 4, 4); context.DrawLine(new Pen(palette.BorderBrush, 1.2), new Point(badgeX + badgeW / 2, badgeY + badgeH), new Point(badgeX + badgeW / 2, waterTop)); context.DrawEllipse(new SolidColorBrush(waterColor), null, new Rect(badgeX + badgeW / 2 - 3, waterTop - 3, 6, 6)); context.DrawText(valueFt, new Point(badgeRect.Center.X - valueFt.Width / 2, badgeRect.Center.Y - valueFt.Height / 2)); // ─── 池名称(顶部)─── var nameSize = Math.Max(9, bounds.Height * 0.07); var nameFt = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, FontWeight.Bold), nameSize, palette.TextPrimaryBrush); context.DrawText(nameFt, new Point(bounds.X + (bounds.Width - nameFt.Width) / 2, bounds.Y - nameFt.Height - 2)); // ─── 水面亮线(液面与空气交界处的高光)─── if (levelPct > 0.01) { var waterRect = new Rect(inner.X, waterTop, inner.Width, inner.Bottom - waterTop); context.DrawLine(new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), 1.8), new Point(inner.X + 1, waterTop), new Point(inner.Right - 1, waterTop)); } // ─── 左侧刻度尺 ─── DrawRuler(context, bounds, inner, maxLevel, palette); } /// 池内气泡上升动画(6 个气泡,不同相位,从池底持续升起破裂) private static void DrawBubbles(DrawingContext context, Rect inner, Rect waterRect, Color waterColor, double phase, double levelPct) { const int bubbleCount = 6; var bubbleBrush = new SolidColorBrush(Color.FromArgb(120, waterColor.R, waterColor.G, waterColor.B)); for (int i = 0; i < bubbleCount; i++) { // 每个气泡的相位偏移不同 var p = (phase + i * 67) % 360; var t = p / 360.0; // 0→1 从池底到水面 var bx = inner.X + inner.Width * (0.15 + (i * 0.14 + Math.Sin(p * 0.05) * 0.08)); var by = inner.Bottom - inner.Height * levelPct * t - (inner.Height * 0.05); // 到水面以下 // 气泡大小随上升略缩小 var bSize = Math.Max(1.5, 3.5 * (1 - t * 0.3)); var bubbleAlpha = (byte)(140 * (1 - t * 0.6)); // 只在液面以下绘制 if (by >= inner.Y && by <= waterRect.Bottom) { context.DrawEllipse(new SolidColorBrush(Color.FromArgb(bubbleAlpha, waterColor.R, waterColor.G, waterColor.B)), null, new Rect(bx - bSize, by - bSize, bSize * 2, bSize * 2)); } } } /// 绘制单层正弦波浪(填充至池底,裁剪在内腔范围内) private static void DrawWave(DrawingContext context, double topY, Rect inner, Color waterColor, double phase, double alpha, double amplitude, double wavelengthScale) { if (topY < inner.Y - amplitude || topY > inner.Bottom) return; const int steps = 24; var geometry = new StreamGeometry(); using (var ctx = geometry.Open()) { ctx.BeginFigure(new Point(inner.X, inner.Bottom), isFilled: true); // 从左到右沿正弦曲线采样 for (int i = 0; i <= steps; i++) { var t = (double)i / steps; var px = inner.X + inner.Width * t; var py = topY + Math.Sin((t * Math.PI * 2 * wavelengthScale) + (phase * Math.PI / 180)) * amplitude; ctx.LineTo(new Point(px, py), isStroked: false); } // 闭合到池底 ctx.EndFigure(true); } var brush = new SolidColorBrush(waterColor, alpha); context.DrawGeometry(brush, null, geometry); } /// 左侧刻度尺(0~maxLevel,按 5 段标注) private static void DrawRuler(DrawingContext context, Rect bounds, Rect inner, double maxLevel, SectionPalette palette) { const int segments = 5; var tickLen = Math.Max(3, bounds.Width * 0.02); var fontSize = Math.Max(7, bounds.Height * 0.045); var pen = new Pen(palette.TextTertiaryBrush, 1); for (int i = 0; i <= segments; i++) { var value = maxLevel * i / segments; var y = inner.Bottom - inner.Height * ((double)i / segments); context.DrawLine(pen, new Point(bounds.X - tickLen, y), new Point(bounds.X - 1, y)); var label = value.ToString("F1", CultureInfo.CurrentCulture); var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Consolas"), fontSize, palette.TextTertiaryBrush); context.DrawText(ft, new Point(bounds.X - tickLen - ft.Width - 2, y - ft.Height / 2)); } } private static void DrawTextAt(DrawingContext context, double x, double y, string text, double size, IBrush brush, FontWeight weight = FontWeight.Normal) { var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Microsoft YaHei", FontStyle.Normal, weight), size, brush); context.DrawText(formatted, new Point(x, y)); } } /// 剖面池主题画刷集(从 ThemeHelper 取色后传入,控件不用 DynamicResource) public record SectionPalette( IBrush WallBrush, // 池壁(混凝土) IBrush WallShadowBrush, // 池壁内侧阴影线 IBrush CavityBrush, // 内腔背景 IBrush BorderBrush, // 边框 IBrush TextPrimaryBrush, // 主文字 IBrush TextTertiaryBrush // 辅助文字(刻度) );