Files
ParkingRobot/.task8-sweep/ParkrobTrajplanner/PathSmoothing/Visualization/SmoothingPngRenderer.cs
T

288 lines
16 KiB
C#
Raw Normal View History

2026-08-09 22:13:18 +08:00
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using MultiWheelC.TrajectoryPlanning.Mapping;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Visualization;
/// <summary>仅 Windows 运行时使用 GDI+ 将每张共享图形定义渲染为 600 dpi PNG;轨迹仅绘制离散点。</summary>
public sealed class SmoothingPngRenderer
{
public const int WidthPixels = 4296;
public const int HeightPixels = 3120;
public const uint PixelsPerMeter = 23622u;
private const float PixelsPerPoint = 600f / 72f;
public byte[] Render(SmoothingFigureModel model, SmoothingFontResolution fonts)
{
if (model == null) throw new ArgumentNullException(nameof(model));
return Render(new SmoothingFigureSetBuilder().Build(model).Figures[1], fonts);
}
public byte[] Render(SmoothingFigureDefinition figure, SmoothingFontResolution fonts)
{
if (figure == null) throw new ArgumentNullException(nameof(figure));
if (fonts == null) throw new ArgumentNullException(nameof(fonts));
using (var bitmap = new Bitmap(WidthPixels, HeightPixels, PixelFormat.Format32bppArgb))
{
bitmap.SetResolution(600f, 600f);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.White);
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
DrawFigure(graphics, figure, fonts);
}
return Encode(bitmap);
}
}
private static void DrawFigure(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts)
{
DrawMixedText(graphics, fonts, figure.Title + " — " + figure.Model.ScenarioLabel, PointX(figure.PlotXPoints), PointY(12d), 12f, Color.Black);
if (figure.IsCurvatureFigure) DrawCurvatureAxes(graphics, figure, fonts); else DrawOverheadAxes(graphics, figure, fonts);
GraphicsState state = graphics.Save();
graphics.SetClip(new RectangleF(PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints)));
if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawObstacles(graphics, figure);
if (figure.IsCurvatureFigure) DrawCurvaturePoints(graphics, figure); else DrawPathPoints(graphics, figure);
if (!figure.IsCurvatureFigure && figure.ShowsMapContext) DrawStartGoal(graphics, figure);
graphics.Restore(state);
DrawLegend(graphics, figure, fonts);
}
private static void DrawOverheadAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts)
{
DrawPlotFrame(graphics, figure);
using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint))
{
for (int index = 0; index < figure.XTicks.Count; index++)
{
float x = WorldX(figure, figure.XTicks[index]);
graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints));
DrawMixedText(graphics, fonts, Number(figure.XTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black);
}
for (int index = 0; index < figure.YTicks.Count; index++)
{
float y = WorldY(figure, figure.YTicks[index]);
graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y);
DrawMixedText(graphics, fonts, Number(figure.YTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black);
}
}
DrawMixedText(graphics, fonts, "X (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 11d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black);
DrawVerticalText(graphics, fonts, "Y (m)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 17d), 10f, Color.Black);
}
private static void DrawCurvatureAxes(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts)
{
DrawPlotFrame(graphics, figure);
using (var grid = new Pen(Color.FromArgb(230, 230, 230), 0.5f * PixelsPerPoint))
{
for (int index = 0; index < figure.CurvatureArcLengthTicks.Count; index++)
{
float x = CurvatureX(figure, figure.CurvatureArcLengthTicks[index]);
graphics.DrawLine(grid, x, PointY(figure.PlotYPoints), x, PointY(figure.PlotYPoints + figure.PlotHeightPoints));
DrawMixedText(graphics, fonts, Number(figure.CurvatureArcLengthTicks[index]), x - 10f * PixelsPerPoint, PointY(figure.PlotYPoints + figure.PlotHeightPoints + 5d), 8f, Color.Black);
}
for (int index = 0; index < figure.CurvatureTicks.Count; index++)
{
float y = CurvatureY(figure, figure.CurvatureTicks[index]);
graphics.DrawLine(grid, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y);
DrawMixedText(graphics, fonts, Number(figure.CurvatureTicks[index]), PointX(figure.PlotXPoints - 34d), y - 5f * PixelsPerPoint, 8f, Color.Black);
}
}
if (figure.CurvatureMinimumPerMeter < 0d && figure.CurvatureMaximumPerMeter > 0d)
{
float y = CurvatureY(figure, 0d);
using (var zero = new Pen(Color.FromArgb(77, 77, 77), 0.65f * PixelsPerPoint))
graphics.DrawLine(zero, PointX(figure.PlotXPoints), y, PointX(figure.PlotXPoints + figure.PlotWidthPoints), y);
}
DrawMixedText(graphics, fonts, "s (m)", PointX(figure.PlotXPoints + figure.PlotWidthPoints / 2d - 10d), PointY(figure.PlotYPoints + figure.PlotHeightPoints + 20d), 10f, Color.Black);
DrawVerticalText(graphics, fonts, "κ (m⁻¹)", PointX(12d), PointY(figure.PlotYPoints + figure.PlotHeightPoints / 2d + 22d), 10f, Color.Black);
}
private static void DrawPlotFrame(Graphics graphics, SmoothingFigureDefinition figure)
{
using (var border = new Pen(Color.Black, 0.75f * PixelsPerPoint))
graphics.DrawRectangle(border, PointX(figure.PlotXPoints), PointY(figure.PlotYPoints), PointX(figure.PlotWidthPoints), PointY(figure.PlotHeightPoints));
}
private static void DrawObstacles(Graphics graphics, SmoothingFigureDefinition figure)
{
using (var fill = new SolidBrush(Color.FromArgb(217, 217, 217)))
using (var outline = new Pen(Color.FromArgb(128, 128, 128), 0.35f * PixelsPerPoint))
{
for (int index = 0; index < figure.Model.Obstacles.Count; index++)
{
SmoothingFigureObstacle obstacle = figure.Model.Obstacles[index];
float x = WorldX(figure, obstacle.X);
float y = WorldY(figure, obstacle.Y + obstacle.Height);
float width = (float)(obstacle.Width * figure.WorldScalePointsPerMeter * PixelsPerPoint);
float height = (float)(obstacle.Height * figure.WorldScalePointsPerMeter * PixelsPerPoint);
graphics.FillRectangle(fill, x, y, width, height);
graphics.DrawRectangle(outline, x, y, width, height);
}
}
}
private static void DrawPathPoints(Graphics graphics, SmoothingFigureDefinition figure)
{
for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++)
{
SmoothingFigureSeriesView view = figure.Series[viewIndex];
DrawPoints(graphics, view, point => new PointF(WorldX(figure, point.X), WorldY(figure, point.Y)));
DrawViolations(graphics, figure, view);
}
}
private static void DrawCurvaturePoints(Graphics graphics, SmoothingFigureDefinition figure)
{
for (int viewIndex = 0; viewIndex < figure.Series.Count; viewIndex++)
{
SmoothingFigureSeriesView view = figure.Series[viewIndex];
DrawPoints(graphics, view, point => new PointF(CurvatureX(figure, point.ArcLength), CurvatureY(figure, point.VehicleCurvature)));
}
}
private static void DrawPoints(Graphics graphics, SmoothingFigureSeriesView view, Func<SmoothingFigurePoint, PointF> transform)
{
if (!view.Series.IsCurveVisible) return;
using (var fill = new SolidBrush(WithOpacity(ColorFromHex(view.Series.Color), view.Opacity)))
{
float radius = 1.35f * PixelsPerPoint;
for (int index = 0; index < view.Series.Points.Count; index++)
{
PointF point = transform(view.Series.Points[index]);
graphics.FillEllipse(fill, point.X - radius, point.Y - radius, radius * 2f, radius * 2f);
}
}
}
private static void DrawViolations(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFigureSeriesView view)
{
if (view.Series.ViolationMarkers.Count == 0) return;
using (var marker = new Pen(ColorFromHex(view.Series.Color), 1.1f * PixelsPerPoint))
{
float radius = 3f * PixelsPerPoint;
for (int index = 0; index < view.Series.ViolationMarkers.Count; index++)
{
SmoothingFigurePoint point = view.Series.ViolationMarkers[index];
float x = WorldX(figure, point.X), y = WorldY(figure, point.Y);
graphics.DrawLine(marker, x - radius, y - radius, x + radius, y + radius);
graphics.DrawLine(marker, x - radius, y + radius, x + radius, y - radius);
}
}
}
private static void DrawStartGoal(Graphics graphics, SmoothingFigureDefinition figure)
{
float startX = WorldX(figure, figure.Model.Start.X), startY = WorldY(figure, figure.Model.Start.Y), radius = 3.2f * PixelsPerPoint;
using (var startBrush = new SolidBrush(Color.FromArgb(240, 228, 66)))
using (var border = new Pen(Color.Black, 0.8f * PixelsPerPoint))
using (var goalBrush = new SolidBrush(ColorFromHex(IeeeFigureStyle.LimitColor)))
{
graphics.FillEllipse(startBrush, startX - radius, startY - radius, radius * 2f, radius * 2f);
graphics.DrawEllipse(border, startX - radius, startY - radius, radius * 2f, radius * 2f);
float goalX = WorldX(figure, figure.Model.Goal.X), goalY = WorldY(figure, figure.Model.Goal.Y), diamond = 4f * PixelsPerPoint;
PointF[] points = { new PointF(goalX, goalY - diamond), new PointF(goalX + diamond, goalY), new PointF(goalX, goalY + diamond), new PointF(goalX - diamond, goalY) };
graphics.FillPolygon(goalBrush, points);
graphics.DrawPolygon(border, points);
}
}
private static void DrawLegend(Graphics graphics, SmoothingFigureDefinition figure, SmoothingFontResolution fonts)
{
const double columnWidth = 198d;
for (int index = 0; index < figure.LegendEntries.Count; index++)
{
SmoothingFigureLegendEntry entry = figure.LegendEntries[index];
int column = index % 2, row = index / 2;
float x = PointX(figure.PlotXPoints + column * columnWidth);
float y = PointY(figure.LegendYPoints + row * 15d - 3d);
using (var fill = new SolidBrush(ColorFromHex(entry.Color))) graphics.FillEllipse(fill, x, y - 2.2f * PixelsPerPoint, 4.4f * PixelsPerPoint, 4.4f * PixelsPerPoint);
DrawMixedText(graphics, fonts, entry.Label, x + 10f * PixelsPerPoint, y - 5f * PixelsPerPoint, 8.5f, Color.Black);
}
}
private static void DrawVerticalText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float y, float points, Color color)
{
GraphicsState state = graphics.Save();
graphics.TranslateTransform(x, y);
graphics.RotateTransform(-90f);
DrawMixedText(graphics, fonts, text, 0f, 0f, points, color);
graphics.Restore(state);
}
private static void DrawMixedText(Graphics graphics, SmoothingFontResolution fonts, string text, float x, float top, float points, Color color)
{
var runs = SmoothingFontResolver.SplitRuns(text);
float emPixels = points * PixelsPerPoint, maxAscent = 0f;
for (int index = 0; index < runs.Count; index++)
{
FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily;
maxAscent = Math.Max(maxAscent, family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular));
}
float baseline = top + maxAscent;
using (var brush = new SolidBrush(color))
using (var format = (StringFormat)StringFormat.GenericTypographic.Clone())
{
format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
for (int index = 0; index < runs.Count; index++)
{
FontFamily family = runs[index].IsChinese ? fonts.ChineseFamily : fonts.LatinFamily;
using (Font font = fonts.CreateFont(family, points))
{
float runTop = baseline - family.GetCellAscent(FontStyle.Regular) * emPixels / family.GetEmHeight(FontStyle.Regular);
graphics.DrawString(runs[index].Text, font, brush, x, runTop, format);
x += graphics.MeasureString(runs[index].Text, font, PointF.Empty, format).Width;
}
}
}
}
private static byte[] Encode(Bitmap bitmap)
{
Rectangle rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
BitmapData data = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int sourceLength = checked(data.Stride * bitmap.Height);
var source = new byte[sourceLength];
Marshal.Copy(data.Scan0, source, 0, source.Length);
var rgba = new byte[checked(bitmap.Width * bitmap.Height * 4)];
for (int y = 0; y < bitmap.Height; y++)
{
int sourceRow = y * data.Stride, outputRow = y * bitmap.Width * 4;
for (int x = 0; x < bitmap.Width; x++)
{
int sourceOffset = sourceRow + x * 4, outputOffset = outputRow + x * 4;
rgba[outputOffset] = source[sourceOffset + 2];
rgba[outputOffset + 1] = source[sourceOffset + 1];
rgba[outputOffset + 2] = source[sourceOffset];
rgba[outputOffset + 3] = source[sourceOffset + 3];
}
}
using (var output = new MemoryStream())
{
ValidatedPngWriter.Write(rgba, bitmap.Width, bitmap.Height, output, PixelsPerMeter);
return output.ToArray();
}
}
finally { bitmap.UnlockBits(data); }
}
private static Color ColorFromHex(string value) { return ColorTranslator.FromHtml(value); }
private static Color WithOpacity(Color color, double opacity) { return Color.FromArgb((int)Math.Round(255d * opacity), color.R, color.G, color.B); }
private static float PointX(double points) { return (float)(points * PixelsPerPoint); }
private static float PointY(double points) { return (float)(points * PixelsPerPoint); }
private static float WorldX(SmoothingFigureDefinition figure, double x) { return PointX(figure.PlotXPoints + (x - figure.WorldXMinMeters) * figure.WorldScalePointsPerMeter); }
private static float WorldY(SmoothingFigureDefinition figure, double y) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (y - figure.WorldYMinMeters) * figure.WorldScalePointsPerMeter); }
private static float CurvatureX(SmoothingFigureDefinition figure, double arcLength) { return PointX(figure.PlotXPoints + arcLength / figure.CurvatureArcLengthMaximumMeters * figure.PlotWidthPoints); }
private static float CurvatureY(SmoothingFigureDefinition figure, double curvature) { return PointY(figure.PlotYPoints + figure.PlotHeightPoints - (curvature - figure.CurvatureMinimumPerMeter) / (figure.CurvatureMaximumPerMeter - figure.CurvatureMinimumPerMeter) * figure.PlotHeightPoints); }
private static string Number(double value) { return value.ToString("0.###", CultureInfo.InvariantCulture); }
}