From db1a51ae14b139fee06e57299db4b3e1ddfe0bc8 Mon Sep 17 00:00:00 2001 From: Skiull Date: Thu, 16 Jul 2026 17:42:52 -0400 Subject: [PATCH 1/2] Add text wrapping and alignment features to PSXUIText component --- Editor/Inspectors/PSXUIEditors.cs | 125 ++++++++++++++++++------- Runtime/PSXUIText.cs | 148 ++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 31 deletions(-) diff --git a/Editor/Inspectors/PSXUIEditors.cs b/Editor/Inspectors/PSXUIEditors.cs index c8ddff2..2af6396 100644 --- a/Editor/Inspectors/PSXUIEditors.cs +++ b/Editor/Inspectors/PSXUIEditors.cs @@ -147,8 +147,6 @@ static void DrawText(PSXUIText text, bool selected) Color fill = new Color(0, 0, 0, selected ? 0.6f : 0.4f); Handles.DrawSolidRectangleWithOutline(corners, fill, borderColor); - string label = string.IsNullOrEmpty(text.DefaultText) ? "[empty]" : text.DefaultText; - PSXFontAsset font = text.GetEffectiveFont(); int glyphW = font != null ? font.GlyphWidth : 8; int glyphH = font != null ? font.GlyphHeight : 16; @@ -169,42 +167,74 @@ static void DrawText(PSXUIText text, bool selected) Color tintColor = text.TextColor; tintColor.a = selected ? 1f : 0.8f; + var layout = text.GetWrappedLayout(font, glyphW, glyphH, psxPixelScale, guiW); + float startY = guiY; + if (layout.BlockHeight < guiH) + { + switch (text.VerticalAlignment) + { + case PSXTextVerticalAlignment.CenterY: + startY = guiY + (guiH - layout.BlockHeight) * 0.5f; + break; + case PSXTextVerticalAlignment.BottomY: + startY = guiY + guiH - layout.BlockHeight; + break; + } + } + if (font != null && font.FontTexture != null && font.SourceFont != null) { Texture2D fontTex = font.FontTexture; int glyphsPerRow = font.GlyphsPerRow; float cellScreenH = glyphH * psxPixelScale; - float cursorX = guiX; GUI.color = tintColor; - foreach (char ch in label) + for (int lineIndex = 0; lineIndex < layout.Lines.Length; lineIndex++) { - if (ch < 32 || ch > 126) continue; - int charIdx = ch - 32; - int col = charIdx % glyphsPerRow; - int row = charIdx / glyphsPerRow; - - float advance = glyphW; - if (font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length) - advance = font.AdvanceWidths[charIdx]; - - if (ch != ' ') + string line = layout.Lines[lineIndex]; + float lineWidth = text.MeasureLineWidth(line, font, glyphW, psxPixelScale); + float currentX = guiX; + switch (text.HorizontalAlignment) { - float uvX = (float)(col * glyphW) / fontTex.width; - float uvY = 1f - (float)((row + 1) * glyphH) / fontTex.height; - float uvW = (float)glyphW / fontTex.width; - float uvH = (float)glyphH / fontTex.height; - - float spriteScreenW = advance * psxPixelScale; - Rect screenRect = new Rect(cursorX, guiY, spriteScreenW, cellScreenH); - float uvWScaled = uvW * (advance / glyphW); - Rect uvRect = new Rect(uvX, uvY, uvWScaled, uvH); - - if (screenRect.xMax > guiX && screenRect.x < guiX + guiW) - GUI.DrawTextureWithTexCoords(screenRect, fontTex, uvRect); + case PSXTextHorizontalAlignment.CenterX: + currentX = guiX + (guiW - lineWidth) * 0.5f; + break; + case PSXTextHorizontalAlignment.RightX: + currentX = guiX + guiW - lineWidth; + break; } - cursorX += advance * psxPixelScale; + float currentY = startY + lineIndex * layout.LineHeight; + float cursorX = currentX; + foreach (char ch in line) + { + if (ch < 32 || ch > 126) continue; + int charIdx = ch - 32; + int col = charIdx % glyphsPerRow; + int row = charIdx / glyphsPerRow; + + float advance = glyphW; + if (font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length) + advance = font.AdvanceWidths[charIdx]; + + if (ch != ' ') + { + float uvX = (float)(col * glyphW) / fontTex.width; + float uvY = 1f - (float)((row + 1) * glyphH) / fontTex.height; + float uvW = (float)glyphW / fontTex.width; + float uvH = (float)glyphH / fontTex.height; + + float spriteScreenW = advance * psxPixelScale; + Rect screenRect = new Rect(cursorX, currentY, spriteScreenW, cellScreenH); + float uvWScaled = uvW * (advance / glyphW); + Rect uvRect = new Rect(uvX, uvY, uvWScaled, uvH); + + if (screenRect.xMax > guiX && screenRect.x < guiX + guiW) + GUI.DrawTextureWithTexCoords(screenRect, fontTex, uvRect); + } + + cursorX += advance * psxPixelScale; + } } GUI.color = Color.white; } @@ -213,14 +243,41 @@ static void DrawText(PSXUIText text, bool selected) int fSize = Mathf.Clamp(Mathf.RoundToInt(glyphH * psxPixelScale * 0.75f), 6, 72); GUIStyle style = new GUIStyle(EditorStyles.label); style.normal.textColor = tintColor; - style.alignment = TextAnchor.UpperLeft; style.fontSize = fSize; style.wordWrap = false; style.clipping = TextClipping.Clip; - Rect guiRect = new Rect(guiX, guiY, guiW, guiH); - GUI.color = tintColor; - GUI.Label(guiRect, label, style); + TextAnchor anchor = TextAnchor.UpperLeft; + switch (text.HorizontalAlignment) + { + case PSXTextHorizontalAlignment.CenterX: + anchor = TextAnchor.UpperCenter; + break; + case PSXTextHorizontalAlignment.RightX: + anchor = TextAnchor.UpperRight; + break; + } + style.alignment = anchor; + + for (int lineIndex = 0; lineIndex < layout.Lines.Length; lineIndex++) + { + string line = layout.Lines[lineIndex]; + float lineWidth = text.MeasureLineWidth(line, font, glyphW, psxPixelScale); + float currentX = guiX; + switch (text.HorizontalAlignment) + { + case PSXTextHorizontalAlignment.CenterX: + currentX = guiX + (guiW - lineWidth) * 0.5f; + break; + case PSXTextHorizontalAlignment.RightX: + currentX = guiX + guiW - lineWidth; + break; + } + + Rect guiRect = new Rect(currentX, startY + lineIndex * layout.LineHeight, lineWidth, layout.LineHeight); + GUI.color = tintColor; + GUI.Label(guiRect, line, style); + } GUI.color = Color.white; } Handles.EndGUI(); @@ -418,6 +475,12 @@ public override void OnInspectorGUI() "Text render color.")); EditorGUILayout.PropertyField(serializedObject.FindProperty("fontOverride"), new GUIContent("Font Override", "Custom font for this text element. If empty, uses the canvas default font or built-in system font (8x16).")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("wrapText"), new GUIContent("TextWrapped", + "Automatically wrap text onto additional lines when it exceeds the element width.")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("horizontalAlignment"), new GUIContent("TextXAlignment", + "How each wrapped line is aligned within the element bounds.")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("verticalAlignment"), new GUIContent("TextYAlignment", + "How the full wrapped text block is aligned within the element bounds.")); EditorGUILayout.PropertyField(serializedObject.FindProperty("startVisible")); PSXEditorStyles.EndCard(); diff --git a/Runtime/PSXUIText.cs b/Runtime/PSXUIText.cs index 972dfe3..5a9f459 100644 --- a/Runtime/PSXUIText.cs +++ b/Runtime/PSXUIText.cs @@ -1,7 +1,23 @@ +using System.Collections.Generic; +using System.Text; using UnityEngine; namespace SplashEdit.RuntimeCode { + public enum PSXTextHorizontalAlignment + { + LeftX, + CenterX, + RightX + } + + public enum PSXTextVerticalAlignment + { + TopY, + CenterY, + BottomY + } + /// /// A text UI element for PSX export. /// Rendered via psyqo::Font::chainprintf on PS1 hardware. @@ -28,6 +44,23 @@ public class PSXUIText : MonoBehaviour [Tooltip("Custom font override. If null, uses the canvas default font (or built-in system font).")] [SerializeField] private PSXFontAsset fontOverride; + [Tooltip("Whether text should wrap onto additional lines when it exceeds the element width.")] + [SerializeField] private bool wrapText = false; + + [Tooltip("Horizontal alignment of wrapped text within the element bounds.")] + [SerializeField] private PSXTextHorizontalAlignment horizontalAlignment = PSXTextHorizontalAlignment.LeftX; + + [Tooltip("Vertical alignment of the full wrapped text block within the element bounds.")] + [SerializeField] private PSXTextVerticalAlignment verticalAlignment = PSXTextVerticalAlignment.TopY; + + public struct WrappedTextLayout + { + public string[] Lines; + public float LineHeight; + public float BlockHeight; + public float MaxLineWidth; + } + /// Element name for Lua access. public string ElementName => elementName; @@ -46,6 +79,15 @@ public class PSXUIText : MonoBehaviour /// public PSXFontAsset FontOverride => fontOverride; + /// Whether text should automatically wrap to additional lines. + public bool WrapText => wrapText; + + /// Horizontal alignment of wrapped text within the element bounds. + public PSXTextHorizontalAlignment HorizontalAlignment => horizontalAlignment; + + /// Vertical alignment of the full wrapped text block within the element bounds. + public PSXTextVerticalAlignment VerticalAlignment => verticalAlignment; + /// /// Resolve the effective font for this text element. /// Checks: fontOverride → parent PSXCanvas.DefaultFont → null (system font). @@ -57,5 +99,111 @@ public PSXFontAsset GetEffectiveFont() if (canvas != null && canvas.DefaultFont != null) return canvas.DefaultFont; return null; // system font } + + /// + /// Build a wrapped line layout for the current text based on the available render width. + /// The block height is based on the full wrapped set of lines so vertical alignment works correctly. + /// + public WrappedTextLayout GetWrappedLayout(PSXFontAsset font, int glyphWidth, int glyphHeight, float pixelScale, float containerWidth) + { + string text = string.IsNullOrEmpty(defaultText) ? "[empty]" : defaultText; + float lineHeight = glyphHeight * pixelScale; + if (containerWidth <= 0.001f) + { + return new WrappedTextLayout + { + Lines = [text], + LineHeight = lineHeight, + BlockHeight = lineHeight, + MaxLineWidth = MeasureLineWidth(text, font, glyphWidth, pixelScale) + }; + } + + var lines = new List(); + var builder = new StringBuilder(); + float currentLineWidth = 0f; + float maxLineWidth = 0f; + + for (int i = 0; i < text.Length; i++) + { + char ch = text[i]; + if (ch == '\n') + { + if (builder.Length > 0) + { + lines.Add(builder.ToString()); + builder.Clear(); + } + else + { + lines.Add(string.Empty); + } + + currentLineWidth = 0f; + continue; + } + + if (ch < 32 || ch > 126) + continue; + + int charIdx = ch - 32; + float advance = glyphWidth; + if (font != null && font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length) + advance = font.AdvanceWidths[charIdx]; + advance *= pixelScale; + + if (wrapText && builder.Length > 0 && currentLineWidth + advance > containerWidth) + { + lines.Add(builder.ToString()); + builder.Clear(); + currentLineWidth = 0f; + } + + builder.Append(ch); + currentLineWidth += advance; + } + + if (builder.Length > 0) + { + lines.Add(builder.ToString()); + } + else if (lines.Count == 0) + { + lines.Add(string.Empty); + } + + foreach (string line in lines) + { + float lineWidth = MeasureLineWidth(line, font, glyphWidth, pixelScale); + if (lineWidth > maxLineWidth) + maxLineWidth = lineWidth; + } + + return new WrappedTextLayout + { + Lines = [.. lines], + LineHeight = lineHeight, + BlockHeight = Mathf.Max(lineHeight, lines.Count * lineHeight), + MaxLineWidth = maxLineWidth + }; + } + + public float MeasureLineWidth(string line, PSXFontAsset font, int glyphWidth, float pixelScale) + { + float width = 0f; + foreach (char ch in line) + { + if (ch < 32 || ch > 126) + continue; + + int charIdx = ch - 32; + float advance = glyphWidth; + if (font != null && font.AdvanceWidths != null && charIdx < font.AdvanceWidths.Length) + advance = font.AdvanceWidths[charIdx]; + width += advance * pixelScale; + } + + return width; + } } } From e9b3feb5da07a53876067f3e3fb472c52cb06012 Mon Sep 17 00:00:00 2001 From: Skiull Date: Thu, 16 Jul 2026 22:39:52 -0400 Subject: [PATCH 2/2] Use Empty for PSXObjectExporter --- Editor/PSXObjectExporterEditor.cs | 42 ++++++++++++++++++------------- Runtime/PSXObjectExporter.cs | 6 ++--- Runtime/PSXSceneWriter.cs | 10 +++++--- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Editor/PSXObjectExporterEditor.cs b/Editor/PSXObjectExporterEditor.cs index b1861f9..65c1383 100644 --- a/Editor/PSXObjectExporterEditor.cs +++ b/Editor/PSXObjectExporterEditor.cs @@ -97,7 +97,7 @@ private void DrawMeshSummary() { if (meshFilter == null || meshFilter.sharedMesh == null) { - EditorGUILayout.LabelField("No mesh on this object.", PSXEditorStyles.InfoBox); + EditorGUILayout.LabelField("No Mesh Filter or mesh was found on this object. Lua-only export is still valid, but geometry will be skipped.", PSXEditorStyles.InfoBox); return; } @@ -128,28 +128,36 @@ private void DrawExportSection() EditorGUI.indentLevel++; - EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth")); + if (meshFilter != null && meshFilter.sharedMesh != null) + { + EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth")); - EditorGUILayout.PropertyField(smoothNormalsProp, new GUIContent("Smooth Normals", - "Smooth normals for lighting. Disable for flat/faceted shading.")); + EditorGUILayout.PropertyField(smoothNormalsProp, new GUIContent("Smooth Normals", + "Smooth normals for lighting. Disable for flat/faceted shading.")); - EditorGUILayout.PropertyField(vertexColorModeProp, new GUIContent("Vertex Colors")); - var vcMode = (VertexColorMode)vertexColorModeProp.enumValueIndex; - if (vcMode == VertexColorMode.FlatColor) - { - EditorGUILayout.PropertyField(flatVertexColorProp, new GUIContent("Flat Color")); - } - else if (vcMode == VertexColorMode.MeshVertexColors) - { - var exporter = target as PSXObjectExporter; - var mf = exporter.GetComponent(); - if (mf != null && mf.sharedMesh != null && (mf.sharedMesh.colors == null || mf.sharedMesh.colors.Length == 0)) + EditorGUILayout.PropertyField(vertexColorModeProp, new GUIContent("Vertex Colors")); + var vcMode = (VertexColorMode)vertexColorModeProp.enumValueIndex; + if (vcMode == VertexColorMode.FlatColor) { - EditorGUILayout.HelpBox("This mesh has no vertex colors. Will fall back to gray (128,128,128).", MessageType.Warning); + EditorGUILayout.PropertyField(flatVertexColorProp, new GUIContent("Flat Color")); } + else if (vcMode == VertexColorMode.MeshVertexColors) + { + var exporter = target as PSXObjectExporter; + var mf = exporter.GetComponent(); + if (mf != null && mf.sharedMesh != null && (mf.sharedMesh.colors == null || mf.sharedMesh.colors.Length == 0)) + { + EditorGUILayout.HelpBox("This mesh has no vertex colors. Will fall back to gray (128,128,128).", MessageType.Warning); + } + } + + EditorGUILayout.PropertyField(uvOffsetMaterialProp, new GUIContent("UV Offset Material")); + } + else + { + EditorGUILayout.HelpBox("This object has no mesh. Add a Mesh Filter to use a Lua script for script-only behavior.", MessageType.Warning); } - EditorGUILayout.PropertyField(uvOffsetMaterialProp, new GUIContent("UV Offset Material")); EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script")); if (luaFileProp.objectReferenceValue != null) diff --git a/Runtime/PSXObjectExporter.cs b/Runtime/PSXObjectExporter.cs index 0708201..05d4314 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -19,15 +19,13 @@ public enum VertexColorMode MeshVertexColors = 2 } - [RequireComponent(typeof(MeshFilter))] - [RequireComponent(typeof(MeshRenderer))] [Icon("Packages/net.psxsplash.splashedit/Icons/PSXObjectExporter.png")] public class PSXObjectExporter : MonoBehaviour, IPSXExportable { public LuaFile LuaFile => luaFile; [FormerlySerializedAs("IsActive")] - [SerializeField] private bool isActive = true; + [SerializeField] private bool isActive = true; public bool IsActive => isActive; public List Textures { get; set; } = new List(); @@ -119,6 +117,8 @@ public PSXTexture2D GetTexture(int index) public void CreatePSXMesh(float GTEScaling) { Renderer renderer = GetComponent(); + Mesh = new PSXMesh { Triangles = new List() }; + if (renderer != null) { Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures, vertexColorMode, flatVertexColor, smoothNormals); diff --git a/Runtime/PSXSceneWriter.cs b/Runtime/PSXSceneWriter.cs index 63afd42..5207cf3 100644 --- a/Runtime/PSXSceneWriter.cs +++ b/Runtime/PSXSceneWriter.cs @@ -351,7 +351,8 @@ public static void Write(string path, in SceneData scene, Action()) { // Vertex positions (3 × 6 bytes) WriteVertexPosition(writer, tri.v0); @@ -656,7 +658,7 @@ public static void Write(string path, in SceneData scene, Action