feat(ui): nine-slice corners clamped to half the box in each direction, cut on whole pixels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 21:59:26 +03:00
parent 12f4ba8431
commit cf8b9e425e
9 changed files with 53 additions and 7 deletions

View file

@ -8,7 +8,7 @@ export property UiBackend {
text: fn(float, float, string, float, int, float) -> void = null # x y text size rgb alpha
measure: fn(string, float) -> float = null
image: fn(string, float, float, float, float, int, float) -> void = null # src x y w h tint alpha
nine: fn(string, float, float, float, float, float, float, int, float) -> void = null # src slice x y w h width tint alpha
nine: fn(string, float, float, float, float, float, float, int, float) -> void = null # src slice x y w h width tint alpha (ui_nine_cuts)
clip: fn(float, float, float, float) -> void = null
unclip: fn() -> void = null
scale: fn() -> float = null # screen pixels per design pixel

View file

@ -42,7 +42,7 @@ function fr_background(n: UiNode) -> void {
}
function fr_fill(n: UiNode, x: float, y: float, c: int, a: float) -> void {
if n.bi_src != "" and ui_be.nine != null {
ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, Math.min(n.bi_width, Math.min(n.cw, n.ch) / 2.0), c, a)
ui_be.nine(n.bi_src, n.bi_slice, x, y, n.cw, n.ch, n.bi_width, c, a)
return
}
fr_box(x, y, n.cw, n.ch, n.radius, c, a)

View file

@ -0,0 +1,19 @@
# draw_nine.ludic - where a nine-slice is cut: its corners as wide as border-image says, but never
# more than half the box across or half of it down (each direction on its own, so a small key cap
# keeps its round ends and has no seam), on whole pixels so the nine pieces meet exactly
# x0 x1 x2 x3 then y0 y1 y2 y3: the columns' and the rows' edges
export function ui_nine_cuts(x: float, y: float, w: float, h: float, dst: float) -> []float {
let dx = Math.max(Math.min(dst, w / 2.0), 0.0)
let dy = Math.max(Math.min(dst, h / 2.0), 0.0)
let out = new []float
push(out, nn_px(x))
push(out, nn_px(x + dx))
push(out, nn_px(x + w - dx))
push(out, nn_px(x + w))
push(out, nn_px(y))
push(out, nn_px(y + dy))
push(out, nn_px(y + h - dy))
push(out, nn_px(y + h))
return out
}
function nn_px(v: float) -> float { return Math.floor(v + 0.5) }

View file

@ -83,4 +83,5 @@ import "screen_class.ludic"
import "draw.ludic"
import "draw_content.ludic"
import "draw_order.ludic"
import "draw_nine.ludic"
import "dump.ludic"

View file

@ -1,14 +1,16 @@
# render3d_nine.ludic - border-image for the render3d backend: a nine-slice of a texture
module ludic_ui
# border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide; the
# slice is a share of the texture's own width across and its own height down, so it need not be square
# border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide (at
# most half the box each way, ui_nine_cuts); the slice is a share of the texture's own width across
# and its own height down, so it need not be square
function r3u_nine(src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void {
let t = r3u_tex(src)
if t == 0 { return }
let su = slice / float(Math.max(tex_width(t), 1))
let sv = slice / float(Math.max(tex_height(t), 1))
let xs = r3u_four(x, x + dst, x + w - dst, x + w)
let ys = r3u_four(y, y + dst, y + h - dst, y + h)
let cut = ui_nine_cuts(x, y, w, h, dst)
let xs = r3u_four(cut[0], cut[1], cut[2], cut[3])
let ys = r3u_four(cut[4], cut[5], cut[6], cut[7])
let us = r3u_four(0.0, su, 1.0 - su, 1.0)
let vs = r3u_four(0.0, sv, 1.0 - sv, 1.0)
for j in 0 .. 3 {