| name | fs-skia-layout-readability |
| description | Work on generated game HUD/status layout readability, gameplay-region bounds, and public scene/host/update naming guidance. |
FS Skia Layout Readability Capability
Scope
Use this skill for tasks that change generated game HUD/status layout,
gameplay-region bounds, readable-layout proof, generated layout validation, or
public scene/host/update naming guidance. This is the layout-design half of
the former fs-skia-layout-evidence skill; the deterministic-evidence-mode and
host-warning-classification half lives in fs-skia-evidence-mode.
Such tasks must declare fs-skia-layout-readability in tasks.deps.yml and
mirror it on the matching tasks.md line. Resolve this skill before
implementation starts and record the resolved path in the active feature's
readiness evidence.
Public Contract
Public evidence contracts must start in .fsi files before implementation.
Readable-layout evidence must report HUD region, gameplay region, text or entity
bounds, and overlap status.
Use the ReadableLayout proof level only when HUD region, gameplay region, HUD
text bounds, gameplay entity bounds, and non-overlap diagnostics are all
present. When only render hashes or scene metadata exist — not readability —
defer to fs-skia-evidence-mode and its DeterministicRenderOnly level.
Public guidance must use app-owned names when showing consumer signatures or
tests:
Product.Program.view for the scene-returning function.
Product.Program.generatedHost for the generated host value.
Product.Program.update for reducer tests or signatures.
Runnable example
Capture and assert scene layout bounds against the readable-layout contract. The
driven surface is the Layout/Scene product packages; an evidence check confirms
the HUD region and gameplay entity bounds do not overlap:
open FS.Skia.UI.Scene
open FS.Skia.UI.Testing
// Render the app scene and capture readable-layout evidence.
let scene = Product.Program.view Product.Program.initialModel
let evidence = LayoutEvidence.capture scene
// Assert reserved HUD region and gameplay entity bounds, then prove non-overlap.
let hud = evidence |> LayoutEvidence.requireRegion "hud"
let gameplay = evidence |> LayoutEvidence.requireRegion "gameplay"
match LayoutEvidence.proofLevel evidence with
| ReadableLayout when not (Bounds.intersects hud gameplay) ->
printfn "ReadableLayout proven: HUD %A clear of gameplay %A" hud gameplay
| _ -> failwith "not readable-layout proof; classify with fs-skia-evidence-mode"
HUD / gameplay-region layout pattern
Reach the readable-layout proof by design, not by gate-driven trial and error.
The intended pattern has three parts, applied in order:
- Reserve a HUD band. Carve a fixed-height status/HUD band off one edge of the
surface (top or bottom) before placing any gameplay content. The HUD region is a
named rectangle (
hud) whose bounds are derived from the surface size, not from
where text happens to land.
- Confine and clamp gameplay to the gameplay region. The remaining surface after
the HUD band is the named
gameplay region. Every gameplay coordinate —
spawning, movement, wrapping, clamping, collision, and active-entity bounds —
must be expressed in and clamped to the gameplay region, so entities can never
stray under the HUD band. Clamp at the policy boundary, not after rendering.
- Overdraw the HUD last. Render gameplay first, then draw the HUD band on top, so
that even if a transient gameplay frame reaches the band edge the HUD remains
legible. Overdraw is the safety net; the clamp in step 2 is the guarantee.
This pattern makes ReadableLayout the natural outcome: the HUD region and gameplay
region are distinct named rectangles by construction, and the clamp keeps
NoLayoutOverlap true at both default and constrained sizes. A layout that proves
readable only after nudging specific pixel offsets has skipped step 1 or step 2.
Shipped helper: FS.Skia.UI.SkillSupport.Hud.reserveHudBand
Every arcade demo re-implemented step 1 the same way, so as of feature 062 (FR-010)
it is shipped real API in FS.Skia.UI.SkillSupport.Hud — no longer a
re-derived per-game convention. Reach for it before hand-rolling the band math:
open FS.Skia.UI.SkillSupport
// surface = full extent along the axis (e.g. window height); bandSize = HUD thickness.
let layout = Hud.reserveHudBand 600.0 48.0 Hud.Top
// layout.HudBand = { Offset = 0.0; Size = 48.0 }
// layout.Gameplay = { Offset = 48.0; Size = 552.0 } // clamp all gameplay to this
It returns plain float bands (no Scene.Rect dependency — convert to your own
geometry at the call site), clamps HudBand.Size = min bandSize surface, and
guarantees Gameplay.Size = surface − HudBand.Size ≥ 0 with the two bands
partitioning the surface. Clamp every gameplay coordinate to layout.Gameplay
(step 2) and overdraw the HUD last (step 3). The reference implementation below
shows the same split if you need a Scene.Rect-typed variant in your own code:
// gameplayRegion = surface − reserved band; the band is anchored to one edge.
let reserveHudBand (edge: Edge) (bandHeight: float) (surface: Rect) : Rect * Rect =
let band, gameplay =
match edge with
| Edge.Top -> { surface with Height = bandHeight },
{ surface with Y = surface.Y + bandHeight; Height = surface.Height - bandHeight }
| Edge.Bottom -> { surface with Y = surface.Bottom - bandHeight; Height = bandHeight },
{ surface with Height = surface.Height - bandHeight }
band, gameplay // name them `hud` / `gameplay`; clamp all gameplay to `gameplay`; overdraw `hud` last
Derive the band height from the surface size (not from where text lands), clamp
every gameplay coordinate to the returned gameplay rectangle (step 2), and
overdraw the hud rectangle last (step 3). Prefer the shipped
Hud.reserveHudBand above for the band math; this Rect-typed form is only the
reference for a consumer that wants the result already in Scene.Rect geometry.
Shipped helper: FS.Skia.UI.SkillSupport.Wrap.wrapDeltaX
Toroidal (wrap-around) worlds — Asteroids, Space Invaders, Lunar Lander — kept
re-deriving the same shortest-wrap-aware delta, so as of feature 063 (FR-010) it is
shipped real API in FS.Skia.UI.SkillSupport.Wrap. Reach for it instead of
hand-rolling the seam arithmetic in your update:
open FS.Skia.UI.SkillSupport
// Shortest signed delta from fromX to toX on a toroidal axis of width worldWidth.
Wrap.wrapDeltaX 100.0 90.0 10.0 // 20.0 (wraps forward across the seam, not -80)
Wrap.wrapDeltaX 100.0 10.0 90.0 // -20.0 (wraps backward, not +80)
Wrap.wrapDeltaX 100.0 42.0 42.0 // 0.0 (identity)
It is pure scalar arithmetic (no state, no I/O, no Scene/Layout dependency, so
SkillSupport stays dependency-light), deterministic, and returns the signed distance of
least magnitude in (-worldWidth/2, worldWidth/2]. Thread it through your pure Elmish
update (e.g. camera-relative or AI targeting on a wrap-around world).
Deferred (documented, not shipped): camera-centered projection
A camera-centered projection (world → screen with the camera following the player,
e.g. LunarLander1/src/LunarLander1/View.fs:60-61) recurred too, but it is documented
here and deliberately NOT shipped. Rationale: it is a closure over per-game state
(player position, view scale, screen center), it returns a Scene.Point (a soft Scene
dependency SkillSupport deliberately avoids), and its shape varies per game
(zoom-centered here, parallax or fixed elsewhere) — it is a View concern, not a
shippable dependency-light scalar helper. Next-recurrence bar: ship a camera helper
only if a stable, game-agnostic projection signature (one that does not pull Scene
into SkillSupport) recurs across ≥3 demos; until then, write it inline in your View.
Build Commands
Prefer repository targets over ad-hoc command sequences:
./fake.sh build -t GeneratedGuidanceCheck
./fake.sh build -t GeneratedProductCheck
./fake.sh build -t TemplateCheck
./fake.sh build -t EvidenceGraph
Generated Product
Generated game samples that claim readability must reserve a named HUD/status
region, keep active gameplay entities in the gameplay region, validate default
and constrained sizes, and fail when HUD/HUD or HUD/gameplay overlap is
detected. Unsupported host or font/layout facts must be explicit and must not be
reported as readable layout proof.
Once a HUD region is reserved, movement, wrapping, spawning, clamping,
collisions, and active entity bounds must use gameplay-region coordinates.
Before you swap the scaffold game model, read docs/scaffold-map.md — it names
which generated src/Product/** files are durable vs replaceable, the
GovernanceTests-durable / BehaviorTests-replaceable split, the must-survive
source-scan strings, and the pre-design fs-skia-scene record-label-collision
pointer.
Package Boundary
Keep pure layout classifiers in Scene or Testing contracts. Do not move viewer
launch, filesystem, package restore, process, font host, or window-system
effects into pure layout helpers. When layout evidence collection needs I/O,
model the request and result explicitly and keep execution at the interpreter or
build-target edge.
Related
- [[fs-skia-evidence-mode]]
- [[fs-skia-scene]]
Sources / links
Persistent problems
When a problem outlasts reasonable in-repo attempts, extensive external research is
mandatory — consult official online docs first (the F#/.NET docs and the driven
library's own documentation/API reference), then community sources (forums, Reddit, Q&A
sites, issue trackers and changelogs). Record the findings and resolving links in the
feature's specs/<feature>/feedback/ folder and, for durable lessons, in this skill's
Sources line. Offline, the mandate degrades to recording "research blocked — "
rather than hard-failing the phase.