Broken UI system

This commit is contained in:
Jan Racek
2026-03-25 12:25:48 +01:00
parent bb8e0804f5
commit 8914ba35cc
28 changed files with 2094 additions and 25 deletions

346
Runtime/PSXUIExporter.cs Normal file
View File

@@ -0,0 +1,346 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SplashEdit.RuntimeCode
{
/// <summary>
/// Collects all PSXCanvas hierarchies in the scene, bakes RectTransform
/// coordinates into PS1 pixel space, and produces <see cref="PSXCanvasData"/>
/// arrays ready for binary serialization.
/// </summary>
public static class PSXUIExporter
{
/// <summary>
/// Collect all PSXCanvas components and their child UI elements,
/// converting RectTransform coordinates to PS1 pixel space.
/// Also collects and deduplicates custom fonts.
/// </summary>
/// <param name="resolution">Target PS1 resolution (e.g. 320×240).</param>
/// <param name="fonts">Output: collected custom font data (max 3).</param>
/// <returns>Array of canvas data ready for binary writing.</returns>
public static PSXCanvasData[] CollectCanvases(Vector2 resolution, out PSXFontData[] fonts)
{
// Collect and deduplicate all custom fonts used by text elements
List<PSXFontAsset> uniqueFonts = new List<PSXFontAsset>();
#if UNITY_EDITOR
PSXCanvas[] canvases = Object.FindObjectsByType<PSXCanvas>(FindObjectsSortMode.None);
#else
PSXCanvas[] canvases = Object.FindObjectsOfType<PSXCanvas>();
#endif
if (canvases == null || canvases.Length == 0)
{
fonts = new PSXFontData[0];
return new PSXCanvasData[0];
}
// First pass: collect unique fonts
foreach (PSXCanvas canvas in canvases)
{
PSXUIText[] texts = canvas.GetComponentsInChildren<PSXUIText>(true);
foreach (PSXUIText txt in texts)
{
PSXFontAsset font = txt.GetEffectiveFont();
if (font != null && !uniqueFonts.Contains(font) && uniqueFonts.Count < 3)
uniqueFonts.Add(font);
}
}
// Build font data with VRAM positions
// Font textures go at x=960 (same column as system font), stacking upward from y=464
// System font: (960, 464)-(1023, 511) → 64 wide, 48 tall
List<PSXFontData> fontDataList = new List<PSXFontData>();
ushort fontVramY = 0; // start from top of VRAM at x=960
foreach (PSXFontAsset fa in uniqueFonts)
{
byte[] pixelData = fa.ConvertTo4BPP();
if (pixelData == null) continue;
ushort texH = (ushort)fa.TextureHeight;
fontDataList.Add(new PSXFontData
{
Source = fa,
GlyphWidth = (byte)fa.GlyphWidth,
GlyphHeight = (byte)fa.GlyphHeight,
VramX = 960, // same column as system font (64 VRAM hwords for 256px 4bpp)
VramY = fontVramY,
TextureHeight = texH,
PixelData = pixelData
});
fontVramY += texH;
}
fonts = fontDataList.ToArray();
// Second pass: collect canvases with font index assignment
List<PSXCanvasData> result = new List<PSXCanvasData>();
foreach (PSXCanvas canvas in canvases)
{
Canvas unityCanvas = canvas.GetComponent<Canvas>();
if (unityCanvas == null) continue;
RectTransform canvasRect = canvas.GetComponent<RectTransform>();
float canvasW = canvasRect.rect.width;
float canvasH = canvasRect.rect.height;
if (canvasW <= 0) canvasW = resolution.x;
if (canvasH <= 0) canvasH = resolution.y;
float scaleX = resolution.x / canvasW;
float scaleY = resolution.y / canvasH;
List<PSXUIElementData> elements = new List<PSXUIElementData>();
CollectImages(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
CollectBoxes(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
CollectTexts(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements, uniqueFonts);
CollectProgressBars(canvas.transform, canvasRect, scaleX, scaleY, resolution, elements);
string name = canvas.CanvasName ?? "canvas";
if (name.Length > 24) name = name.Substring(0, 24);
result.Add(new PSXCanvasData
{
Name = name,
StartVisible = canvas.StartVisible,
SortOrder = canvas.SortOrder,
Elements = elements.ToArray()
});
}
return result.ToArray();
}
// ─── Coordinate baking helpers ───
/// <summary>
/// Convert a RectTransform into PS1 pixel-space layout values.
/// Handles anchor-based positioning and Y inversion.
/// </summary>
private static void BakeLayout(
RectTransform rt, RectTransform canvasRect,
float scaleX, float scaleY, Vector2 resolution,
out short x, out short y, out short w, out short h,
out byte anchorMinX, out byte anchorMinY,
out byte anchorMaxX, out byte anchorMaxY)
{
// Anchor values in 8.8 fixed point (0-255 maps to 0.0-~1.0)
anchorMinX = (byte)Mathf.Clamp(Mathf.RoundToInt(rt.anchorMin.x * 255f), 0, 255);
anchorMinY = (byte)Mathf.Clamp(Mathf.RoundToInt((1f - rt.anchorMax.y) * 255f), 0, 255); // Y invert
anchorMaxX = (byte)Mathf.Clamp(Mathf.RoundToInt(rt.anchorMax.x * 255f), 0, 255);
anchorMaxY = (byte)Mathf.Clamp(Mathf.RoundToInt((1f - rt.anchorMin.y) * 255f), 0, 255); // Y invert
if (Mathf.Approximately(rt.anchorMin.x, rt.anchorMax.x) &&
Mathf.Approximately(rt.anchorMin.y, rt.anchorMax.y))
{
// Fixed-size element with single anchor point
// anchoredPosition is the offset from the anchor in canvas pixels
float px = rt.anchoredPosition.x * scaleX;
float py = -rt.anchoredPosition.y * scaleY; // Y invert
float pw = rt.rect.width * scaleX;
float ph = rt.rect.height * scaleY;
// Adjust for pivot (anchoredPosition is at the pivot point)
px -= rt.pivot.x * pw;
py -= (1f - rt.pivot.y) * ph; // pivot Y inverted
x = (short)Mathf.RoundToInt(px);
y = (short)Mathf.RoundToInt(py);
w = (short)Mathf.Max(1, Mathf.RoundToInt(pw));
h = (short)Mathf.Max(1, Mathf.RoundToInt(ph));
}
else
{
// Stretched element: offsets from anchored edges
// offsetMin = distance from anchorMin corner, offsetMax = distance from anchorMax corner
float leftOff = rt.offsetMin.x * scaleX;
float rightOff = rt.offsetMax.x * scaleX;
float topOff = -rt.offsetMax.y * scaleY; // Y invert
float bottomOff = -rt.offsetMin.y * scaleY; // Y invert
// For stretched elements, x/y store the offset from the anchor start,
// and w/h store the combined inset (negative = shrink)
x = (short)Mathf.RoundToInt(leftOff);
y = (short)Mathf.RoundToInt(topOff);
w = (short)Mathf.RoundToInt(rightOff - leftOff);
h = (short)Mathf.RoundToInt(bottomOff - topOff);
}
}
private static string TruncateName(string name, int maxLen = 24)
{
if (string.IsNullOrEmpty(name)) return "";
return name.Length > maxLen ? name.Substring(0, maxLen) : name;
}
// ─── Collectors ───
private static void CollectImages(
Transform root, RectTransform canvasRect,
float scaleX, float scaleY, Vector2 resolution,
List<PSXUIElementData> elements)
{
PSXUIImage[] images = root.GetComponentsInChildren<PSXUIImage>(true);
foreach (PSXUIImage img in images)
{
RectTransform rt = img.GetComponent<RectTransform>();
if (rt == null) continue;
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
out short x, out short y, out short w, out short h,
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
var data = new PSXUIElementData
{
Type = PSXUIElementType.Image,
StartVisible = img.StartVisible,
Name = TruncateName(img.ElementName),
X = x, Y = y, W = w, H = h,
AnchorMinX = amin_x, AnchorMinY = amin_y,
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.r * 255f), 0, 255),
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.g * 255f), 0, 255),
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(img.TintColor.b * 255f), 0, 255),
};
// Image texture data is filled in after VRAM packing by
// FillImageTextureData() — see PSXSceneExporter integration
if (img.PackedTexture != null)
{
PSXTexture2D tex = img.PackedTexture;
int expander = 16 / (int)tex.BitDepth;
data.TexpageX = tex.TexpageX;
data.TexpageY = tex.TexpageY;
data.ClutX = (ushort)tex.ClutPackingX;
data.ClutY = (ushort)tex.ClutPackingY;
data.U0 = (byte)(tex.PackingX * expander);
data.V0 = (byte)tex.PackingY;
data.U1 = (byte)(tex.PackingX * expander + tex.Width * expander / ((int)tex.BitDepth / (int)PSXBPP.TEX_4BIT));
data.V1 = (byte)(tex.PackingY + tex.Height);
data.BitDepthIndex = tex.BitDepth switch
{
PSXBPP.TEX_4BIT => 0,
PSXBPP.TEX_8BIT => 1,
PSXBPP.TEX_16BIT => 2,
_ => 2
};
}
elements.Add(data);
}
}
private static void CollectBoxes(
Transform root, RectTransform canvasRect,
float scaleX, float scaleY, Vector2 resolution,
List<PSXUIElementData> elements)
{
PSXUIBox[] boxes = root.GetComponentsInChildren<PSXUIBox>(true);
foreach (PSXUIBox box in boxes)
{
RectTransform rt = box.GetComponent<RectTransform>();
if (rt == null) continue;
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
out short x, out short y, out short w, out short h,
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
elements.Add(new PSXUIElementData
{
Type = PSXUIElementType.Box,
StartVisible = box.StartVisible,
Name = TruncateName(box.ElementName),
X = x, Y = y, W = w, H = h,
AnchorMinX = amin_x, AnchorMinY = amin_y,
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.r * 255f), 0, 255),
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.g * 255f), 0, 255),
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(box.BoxColor.b * 255f), 0, 255),
});
}
}
private static void CollectTexts(
Transform root, RectTransform canvasRect,
float scaleX, float scaleY, Vector2 resolution,
List<PSXUIElementData> elements,
List<PSXFontAsset> uniqueFonts = null)
{
PSXUIText[] texts = root.GetComponentsInChildren<PSXUIText>(true);
foreach (PSXUIText txt in texts)
{
RectTransform rt = txt.GetComponent<RectTransform>();
if (rt == null) continue;
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
out short x, out short y, out short w, out short h,
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
string defaultText = txt.DefaultText ?? "";
if (defaultText.Length > 63) defaultText = defaultText.Substring(0, 63);
// Resolve font index: 0 = system font, 1+ = custom font
byte fontIndex = 0;
PSXFontAsset effectiveFont = txt.GetEffectiveFont();
if (effectiveFont != null && uniqueFonts != null)
{
int idx = uniqueFonts.IndexOf(effectiveFont);
if (idx >= 0) fontIndex = (byte)(idx + 1); // 1-based for custom fonts
}
elements.Add(new PSXUIElementData
{
Type = PSXUIElementType.Text,
StartVisible = txt.StartVisible,
Name = TruncateName(txt.ElementName),
X = x, Y = y, W = w, H = h,
AnchorMinX = amin_x, AnchorMinY = amin_y,
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.r * 255f), 0, 255),
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.g * 255f), 0, 255),
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(txt.TextColor.b * 255f), 0, 255),
DefaultText = defaultText,
FontIndex = fontIndex,
});
}
}
private static void CollectProgressBars(
Transform root, RectTransform canvasRect,
float scaleX, float scaleY, Vector2 resolution,
List<PSXUIElementData> elements)
{
PSXUIProgressBar[] bars = root.GetComponentsInChildren<PSXUIProgressBar>(true);
foreach (PSXUIProgressBar bar in bars)
{
RectTransform rt = bar.GetComponent<RectTransform>();
if (rt == null) continue;
BakeLayout(rt, canvasRect, scaleX, scaleY, resolution,
out short x, out short y, out short w, out short h,
out byte amin_x, out byte amin_y, out byte amax_x, out byte amax_y);
elements.Add(new PSXUIElementData
{
Type = PSXUIElementType.Progress,
StartVisible = bar.StartVisible,
Name = TruncateName(bar.ElementName),
X = x, Y = y, W = w, H = h,
AnchorMinX = amin_x, AnchorMinY = amin_y,
AnchorMaxX = amax_x, AnchorMaxY = amax_y,
// Fill color goes into primary color (used for the fill bar)
ColorR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.r * 255f), 0, 255),
ColorG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.g * 255f), 0, 255),
ColorB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.FillColor.b * 255f), 0, 255),
// Background color goes into progress-specific fields
BgR = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.r * 255f), 0, 255),
BgG = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.g * 255f), 0, 255),
BgB = (byte)Mathf.Clamp(Mathf.RoundToInt(bar.BackgroundColor.b * 255f), 0, 255),
ProgressValue = (byte)bar.InitialValue,
});
}
}
}
}