Files
VRIO/tools/VPlasma.Wire/PlasmaBitmap.cs
T
CydandClaude Opus 4.8 68e3d1f367 VPlasma.Wire: add diff + TeslaSuite plasma authoring tools
- 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>
2026-07-16 21:20:50 -05:00

183 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}