skin studio updates, save sync CLOSES #1533

This commit is contained in:
bryanthaboi
2026-08-19 05:57:44 -04:00
parent fd73ab2a11
commit 93374fbbbb
54 changed files with 9762 additions and 271 deletions
+214
View File
@@ -0,0 +1,214 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local TouchSkin = require("src.core.TouchSkin")
local Playfield = require("src.render.Playfield")
local Renderer = require("src.render.Renderer")
local Chrome = require("src.ui.gen2.Chrome")
local Zoom = require("src.render.Zoom")
local EPS = 1e-6
local function setWindow(w, h)
love.graphics.getDimensions = function() return w, h end
love.graphics.getPixelDimensions = function() return w, h end
end
local function cfg(viewport, extra)
return ([[
overlays = 1
overlay0_name = "bezel"
overlay0_full_screen = true
overlay0_normalized = true
overlay0_viewport = "%s"
%s
overlay0_descs = 1
overlay0_desc0 = "nul,0.5,0.5,rect,0.02,0.02"
]]):format(viewport, extra or "")
end
local function useSkin(viewport, extra)
local skin = assert(TouchSkin.parse(cfg(viewport, extra)))
TouchSkin.setActive(skin)
TouchSkin.setOverlayLive(false)
return skin
end
local function inside(x, y, w, h, bx, by, bw, bh)
return x >= bx - EPS and y >= by - EPS
and x + w <= bx + bw + EPS and y + h <= by + bh + EPS
end
setWindow(640, 576)
TouchSkin.setActive(nil)
Renderer:init()
local plain = Renderer:frameRects()
eq(plain.cut, false, "no skin: no cutout")
eq(plain.vux, 0, "no skin: the picture starts at the window origin")
eq(plain.vuw, 640, "no skin: the picture is the whole window")
eq(plain.Sp, 4, "no skin: 640x576 fits four whole GB pixels")
eq(plain.uox, 0, "no skin: the UI letterbox fills the window")
eq(select(3, Playfield.rect(640, 576)), 640, "no skin: the playfield is the window")
eq(Chrome.fitScale(640, 576), 4, "no skin: Gold fits the window the same way")
local WINDOWS = {
{ 640, 576 }, { 1280, 720 }, { 1920, 1080 },
{ 800, 480 }, { 480, 800 }, { 360, 640 },
}
local VIEWPORTS = {
"0.2335,0.0855,0.5335,0.830",
"0.2,0.15,0.6,0.5",
"0.05,0.05,0.9,0.35",
"0.3,0.1,0.4,0.8",
}
local UI_SIZES = { { 160, 144 }, { 304, 144 } }
local escapes, uncut, cases = 0, 0, 0
for _, win in ipairs(WINDOWS) do
setWindow(win[1], win[2])
for _, vp in ipairs(VIEWPORTS) do
useSkin(vp)
Renderer:init()
for _, size in ipairs(UI_SIZES) do
Renderer:setUISize(size[1], size[2])
for off = -8, 8 do
Zoom.offset = off
for _, fill in ipairs({ false, true }) do
for _, centered in ipairs({ true, false }) do
Renderer.uiFill = fill
Renderer.uiCentered = centered
Renderer.worldActive = true
cases = cases + 1
local r = Renderer:frameRects()
local ux, uy, uw, uh = Renderer.clipToView(r, r.uox, r.uoy,
r.uvpw, r.uvph)
if not inside(ux, uy, uw, uh, r.vux, r.vuy, r.vuw, r.vuh) then
escapes = escapes + 1
end
if uw < r.uvpw - EPS or uh < r.uvph - EPS then uncut = uncut + 1 end
local vw, vh = Renderer:worldViewSize()
local sp = Zoom.scale(r.Sp)
if vw * sp > r.vuw + 2 * sp + EPS
or vh * sp > r.vuh + 2 * sp + EPS then
escapes = escapes + 1
end
end
end
end
end
end
end
check(cases > 1000, "the sweep covers every window x cutout x zoom x layout")
eq(escapes, 0, "no zoom, battle surface or UI layout puts a rect past the cutout")
eq(uncut, 0, "and the UI was sized to fit, so the clip never has to cut it")
setWindow(1280, 720)
useSkin("0.25,0.1,0.5,0.6")
Renderer:init()
Renderer:setUISize(160, 144)
Renderer.uiFill, Renderer.uiCentered = false, true
Zoom.offset = 0
local r = Renderer:frameRects()
eq(r.cut, true, "the skin's cutout is folded into the frame")
eq(r.vux, 320, "cutout x")
eq(r.vuy, 72, "cutout y")
eq(r.vuw, 640, "cutout width")
eq(r.vuh, 432, "cutout height")
eq(r.Sp, 3, "the fit is measured against the cutout, not the window")
check(inside(r.uox, r.uoy, r.uvpw, r.uvph, r.vux, r.vuy, r.vuw, r.vuh),
"the UI letterbox sits inside the cutout")
check(inside(r.ox, r.oy, r.vpw, r.vph, r.vux, r.vuy, r.vuw, r.vuh),
"so does the world letterbox")
local lo, hi = Zoom.offsetRange(r.Sp)
for off = lo, hi do
Zoom.offset = off
local z = Renderer:frameRects()
check(inside(z.uox, z.uoy, z.uvpw, z.uvph, z.vux, z.vuy, z.vuw, z.vuh),
"zoom " .. Zoom.offsetLabel(off) .. " keeps the UI in the cutout")
local vw, vh = Renderer:worldViewSize()
local sp = Zoom.scale(z.Sp)
check(vw * sp <= z.vuw + 2 * sp and vh * sp <= z.vuh + 2 * sp,
"zoom " .. Zoom.offsetLabel(off) .. " keeps the world pass capped")
end
Zoom.offset = 0
local capped = select(1, Renderer:worldViewSize())
useSkin("0.25,0.1,0.5,0.6", "overlay0_viewport_expand = true")
local expanded = select(1, Renderer:worldViewSize())
check(expanded > capped,
"viewport_expand lets the survey world fill the cutout instead of the GB box")
useSkin("0.25,0.1,0.5,0.6")
setWindow(480, 800)
useSkin("0.3,0.1,0.4,0.3")
Renderer:init()
Renderer:setUISize(304, 144)
Renderer.uiFill, Renderer.uiCentered = false, true
local tight = Renderer:frameRects()
check(tight.vuw < 304, "the cutout cannot hold the WIDE battle at 1x")
check(inside(tight.uox, tight.uoy, tight.uvpw, tight.uvph,
tight.vux, tight.vuy, tight.vuw, tight.vuh),
"so the surface is scaled down to the cutout rather than over the bezel")
Renderer:setUISize(160, 144)
setWindow(1280, 720)
useSkin("0.25,0.1,0.5,0.6")
local px, py, pw, ph, active = Playfield.rect(1280, 720)
eq(active, true, "Gold sees the cutout too")
eq(pw, 480, "the playfield is a whole multiple of 160")
eq(ph, 432, "and of 144")
check(inside(px, py, pw, ph, 320, 72, 640, 432),
"centred inside the cutout")
eq(Chrome.fitScale(1280, 720), 3, "Chrome fits the playfield")
local cox, coy = Chrome.fitOrigin(1280, 720)
eq(cox, px, "and centres the panel on it")
eq(coy, py, "on both axes")
useSkin("0.25,0.1,0.5,0.6", "overlay0_viewport_expand = true")
local ex, ey, ew, eh = Playfield.rect(1280, 720)
eq(ew, 640, "expand hands the picture the full cutout width")
eq(eh, 432, "and its full height")
eq(ex, 320, "at the cutout origin")
eq(ey, 72, "on both axes")
useSkin("0.25,0.1,0.5,0.6")
local ew2, eh2, ex2, ey2, act2 = Playfield.push(1280, 720)
eq(act2, true, "push reports the frame is contained")
eq(ex2, px, "push translates to the playfield origin")
eq(ey2, py, "on both axes")
eq(ew2, pw, "and hands the scene the playfield size")
eq(Playfield.cutout(ew2, eh2), nil, "inside the frame there is no cutout left")
eq(select(3, Playfield.rect(ew2, eh2)), pw, "so the playfield is the surface")
eq(Chrome.fitScale(ew2, eh2), 3, "and Chrome fits it without re-applying")
eq(select(1, Chrome.fitOrigin(ew2, eh2)), 0, "at a local origin")
eq(select(1, Playfield.dimensions()), pw, "screens read the playfield as the display")
Playfield.pop()
eq(Playfield.entered, false, "pop leaves the frame")
eq(select(1, Playfield.cutout(1280, 720)), 320, "and the cutout is visible again")
useSkin("0.4,0.4,0.1,0.1")
local sx, sy, sw, sh = Playfield.rect(1280, 720)
check(inside(sx, sy, sw, sh, 512, 288, 128, 72),
"a cutout smaller than 160x144 still bounds the playfield")
check(sw <= 128 and sh <= 72, "the playfield never exceeds the cutout")
TouchSkin.setActive(nil)
eq(Playfield.cutout(1280, 720), nil, "no skin, no cutout")
eq(select(3, Playfield.rect(1280, 720)), 1280, "and the playfield is the window")
local saved = TouchSkin.viewport
TouchSkin.viewport = function() error("boom") end
eq(Playfield.cutout(1280, 720), nil, "a throwing viewport is no cutout")
TouchSkin.viewport = function() return 10, 10, 0, 0 end
eq(Playfield.cutout(1280, 720), nil, "a zero-sized cutout is no cutout")
TouchSkin.viewport = function() return -50, -50, 200, 200 end
eq(select(1, Playfield.cutout(1280, 720)), 0, "a cutout off the surface is clamped")
eq(select(3, Playfield.cutout(1280, 720)), 150, "to what is left of it")
TouchSkin.viewport = saved
TouchSkin.setActive(nil)
setWindow(640, 576)
T.finish("skin_viewport_containment")