- diff — compare two frames/images/encodings (.bin ESC P / .png / .txt
bitmap=), each reduced to 128x32; reports differing dots and match %, writes
a red-on-mismatch image, exits 1 if they differ. Closes the differential-test
loop: diff a cropped panel/replica photo against the vPLASMA golden image.
- text / encode / decode — ported from TeslaSuite's three plasma tools (Plasma
Font Tool, Bitmap Decoder, Editor) via PlasmaBitmap.cs:
* text — render text in an auto-sized Windows font to a plasma bitmap.
* encode — an image → plasma bitmap.
* decode — the game's bitmap= hex encoding → a preview PNG.
Each bridges to an ESC P wire stream (and/or the bitmap= hex encoding), so
authored content replays straight to vPLASMA, the replica, or the real panel.
Verified: text "ALERT 12" → ESC P → vPLASMA renders it; the ESC P and hex
paths diff as byte-identical (504/504 lit, 100% match).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
7.4 KiB
C#
183 lines
7.4 KiB
C#
using System.Drawing;
|
||
using System.Drawing.Drawing2D;
|
||
using System.Drawing.Imaging;
|
||
using System.Drawing.Text;
|
||
using System.Text;
|
||
using VPlasma.Core.Device;
|
||
using VPlasma.Core.Protocol;
|
||
|
||
namespace VPlasma.Wire;
|
||
|
||
/// <summary>
|
||
/// Plasma bitmap authoring — ported from TeslaSuite's <c>PlasmaBitmaps</c>
|
||
/// (the console's Plasma Font Tool / Bitmap Decoder / Editor). Renders text in
|
||
/// a Windows font to a 1bpp panel bitmap, converts bitmaps to and from the
|
||
/// game's <c>bitmap=</c> hex encoding, and bridges either to an <c>ESC P</c>
|
||
/// wire stream the display (or vPLASMA / the replica) can show.
|
||
/// </summary>
|
||
internal static class PlasmaBitmap
|
||
{
|
||
public const string DefaultFont = "Microsoft Sans Serif";
|
||
|
||
/// <summary>
|
||
/// Render <paramref name="text"/> centered in a <paramref name="width"/>×
|
||
/// <paramref name="height"/> 1bpp image, auto-shrinking the font to fit —
|
||
/// the Plasma Font Tool's behavior.
|
||
/// </summary>
|
||
public static Bitmap RenderText(int width, int height, string fontName, string text)
|
||
{
|
||
var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
|
||
using var g = Graphics.FromImage(bmp);
|
||
g.SmoothingMode = SmoothingMode.None;
|
||
g.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||
g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
|
||
g.Clear(Color.Black);
|
||
|
||
var rect = new Rectangle(0, 0, width, height);
|
||
using var fmt = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
|
||
for (int size = 24; ; size = size <= 2 ? size / 2 : size - 2)
|
||
{
|
||
using var font = new Font(fontName, Math.Max(1, size));
|
||
SizeF sz = g.MeasureString(text, font);
|
||
if (size <= 1 || (sz.Width <= width && sz.Height <= height))
|
||
{
|
||
g.DrawString(text, font, Brushes.White, rect, fmt);
|
||
return bmp;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Threshold an image to a <paramref name="w"/>×<paramref name="h"/> on/off grid.</summary>
|
||
public static bool[,] ToGrid(Bitmap src, int w, int h, double threshold)
|
||
{
|
||
using var scaled = new Bitmap(w, h, PixelFormat.Format24bppRgb);
|
||
using (var g = Graphics.FromImage(scaled))
|
||
{
|
||
g.InterpolationMode = src.Width == w && src.Height == h
|
||
? InterpolationMode.NearestNeighbor : InterpolationMode.HighQualityBicubic;
|
||
g.DrawImage(src, 0, 0, w, h);
|
||
}
|
||
var grid = new bool[w, h];
|
||
for (int y = 0; y < h; y++)
|
||
for (int x = 0; x < w; x++)
|
||
grid[x, y] = scaled.GetPixel(x, y).GetBrightness() >= threshold;
|
||
return grid;
|
||
}
|
||
|
||
/// <summary>The game's <c>bitmap=</c> hex encoding (MSB-first, 8 px/byte).</summary>
|
||
public static string ToHexEncoding(bool[,] grid)
|
||
{
|
||
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||
if (w % 8 != 0)
|
||
throw new ArgumentException("width must be a multiple of 8");
|
||
var sb = new StringBuilder();
|
||
for (int y = 0; y < h; y++)
|
||
{
|
||
sb.Append("bitmap=");
|
||
for (int j = 0; j < w / 8; j++)
|
||
{
|
||
byte b = 0;
|
||
for (int k = 0; k < 8; k++)
|
||
if (grid[j * 8 + k, y]) b |= (byte)(0x80 >> k);
|
||
sb.Append(b.ToString("X2"));
|
||
}
|
||
sb.Append('\n');
|
||
}
|
||
sb.Append($"x={w}\n");
|
||
sb.Append($"y={h}\n");
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>Parse a <c>bitmap=</c>/<c>x=</c>/<c>y=</c> encoding back to a grid.</summary>
|
||
public static bool[,] FromHexEncoding(string text)
|
||
{
|
||
var rows = new List<byte[]>();
|
||
int declaredX = 0, declaredY = 0;
|
||
foreach (string raw in text.Split('\n'))
|
||
{
|
||
string line = raw.Trim();
|
||
if (line.StartsWith("bitmap=", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
string hex = line.Substring("bitmap=".Length).Trim();
|
||
var bytes = new byte[hex.Length / 2];
|
||
for (int i = 0; i < bytes.Length; i++)
|
||
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||
rows.Add(bytes);
|
||
}
|
||
else if (line.StartsWith("x=", StringComparison.OrdinalIgnoreCase))
|
||
int.TryParse(line.Substring(2), out declaredX);
|
||
else if (line.StartsWith("y=", StringComparison.OrdinalIgnoreCase))
|
||
int.TryParse(line.Substring(2), out declaredY);
|
||
}
|
||
if (rows.Count == 0)
|
||
throw new ArgumentException("no 'bitmap=' rows found");
|
||
int w = declaredX > 0 ? declaredX : rows[0].Length * 8;
|
||
int h = declaredY > 0 ? declaredY : rows.Count;
|
||
var grid = new bool[w, h];
|
||
for (int y = 0; y < h && y < rows.Count; y++)
|
||
for (int j = 0; j < rows[y].Length && j * 8 < w; j++)
|
||
for (int k = 0; k < 8 && j * 8 + k < w; k++)
|
||
grid[j * 8 + k, y] = (rows[y][j] & (0x80 >> k)) != 0;
|
||
return grid;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Pack a grid into an <c>ESC P</c> wire stream (clear + cursor-off + one
|
||
/// full-width row per line), the way L4PLASMA streams a frame.
|
||
/// </summary>
|
||
public static byte[] ToEscP(bool[,] grid)
|
||
{
|
||
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||
int wbytes = (w + 7) / 8;
|
||
var b = new List<byte>
|
||
{
|
||
PlasmaProtocol.Esc, PlasmaProtocol.CmdCursorMode, 0x00, // hide cursor
|
||
PlasmaProtocol.Esc, PlasmaProtocol.CmdClearScreen,
|
||
};
|
||
for (int y = 0; y < h; y++)
|
||
{
|
||
b.Add(PlasmaProtocol.Esc);
|
||
b.Add(PlasmaProtocol.CmdGraphicsWrite);
|
||
b.Add(0); // screen
|
||
b.Add((byte)y); // y
|
||
b.Add(0); // x byte
|
||
b.Add((byte)wbytes); // width in bytes
|
||
b.Add(1); // one row
|
||
for (int j = 0; j < wbytes; j++)
|
||
{
|
||
byte v = 0;
|
||
for (int k = 0; k < 8; k++)
|
||
if (j * 8 + k < w && grid[j * 8 + k, y]) v |= (byte)(0x80 >> k);
|
||
b.Add(v);
|
||
}
|
||
}
|
||
return b.ToArray();
|
||
}
|
||
|
||
/// <summary>Render a grid to a plasma-orange PNG (for previews / decoding).</summary>
|
||
public static void SaveGridPng(bool[,] grid, string path, int scale)
|
||
{
|
||
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||
int pitch = scale, dot = Math.Max(1, scale - 2);
|
||
using var bmp = new Bitmap(w * pitch, h * pitch, PixelFormat.Format24bppRgb);
|
||
using var g = Graphics.FromImage(bmp);
|
||
g.Clear(Color.FromArgb(26, 14, 6));
|
||
using var unlit = new SolidBrush(Color.FromArgb(46, 24, 10));
|
||
using var lit = new SolidBrush(Color.FromArgb(255, 106, 26));
|
||
for (int y = 0; y < h; y++)
|
||
for (int x = 0; x < w; x++)
|
||
g.FillRectangle(grid[x, y] ? lit : unlit, x * pitch, y * pitch, dot, dot);
|
||
bmp.Save(path, ImageFormat.Png);
|
||
}
|
||
|
||
/// <summary>Reduce a rendered vPLASMA frame (flag bytes) to an on/off grid.</summary>
|
||
public static bool[,] FrameToGrid(byte[] frame)
|
||
{
|
||
var grid = new bool[VPlasmaDevice.Width, VPlasmaDevice.Height];
|
||
for (int y = 0; y < VPlasmaDevice.Height; y++)
|
||
for (int x = 0; x < VPlasmaDevice.Width; x++)
|
||
grid[x, y] = (frame[y * VPlasmaDevice.Width + x] & VPlasmaDevice.PixelLit) != 0;
|
||
return grid;
|
||
}
|
||
}
|