Files
sporel-lib-core.render/init.lua
calic ce88d3c216 feat(render): per-entity sprite_scale on the Atlas-UV path
draw_entities path-1 reads an optional sprite_scale (number, default
1.0) and applies it as uniform scaleX/scaleY in
draw_sprite_transform around the visual center. Lets templates whose
native sprite resolution (e.g. TC_Basics 300×300 bed) doesn't match
the project tile-scale (~/sporel_tile_scale.md: 64 px = 0.5 m via the
puppet shoulder anchor) render at credible real-world sizes without
re-baking the atlas.

Consumers compute their per-atlas scale from
atlas_meta.pixels_per_meter (atlas-baker v0.4.0+ output) divided into
their project canonical px/m — one line in M.init, no per-entity
constants in the template.

Unset sprite_scale preserves v0.2.0 behavior — spine-prototype,
map-editor, vagrant's pre-TC_Basics items unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-21 11:42:17 +02:00

177 lines
7.3 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =====================================================================
-- lib-core.render v0.3.0 — Tile-Grid + Entity Rendering
-- See: meta/docs/superpowers/specs/2026-05-09-p0-lib-render-design.md
-- meta/docs/superpowers/specs/2026-06-09-phase-A-...
--
-- v0.3.0 additions:
-- - draw_entities path (1) reads optional `sprite_scale` (number,
-- default 1.0); applied uniformly as scaleX/scaleY in
-- draw_sprite_transform. Lets templates whose native sprite size
-- doesn't match the project tile-scale (e.g. TC_Basics 600×300
-- bed at 64 px = 0.5 m) scale down without per-entity baker
-- re-runs or render-time hacks. Default 1.0 preserves v0.2.0
-- behaviour for unset consumers.
--
-- v0.2.0 additions:
-- - draw_entities(filter) — tag-based entity-render via composition.
-- Supports two sprite-property paths and a fallback:
-- (1) sprite_atlas + sprite_uv.{x,y,w,h} → atlas-sub-rect render
-- via engine.render.draw_sprite_transform
-- (2) sprite_color + sprite_w + sprite_h → colored rect
-- via engine.render.draw_rect
-- (3) none of above → magenta fallback rect (debug-visible)
-- `position.x` and `position.y` are visual CENTER for all modes.
-- Texture-handles for sprite_atlas paths are cached internally.
--
-- v0.1.0 API unverändert: draw_map.
-- =====================================================================
local maps = require("lib-core.maps")
local composition = require("lib-core.composition")
local FALLBACK_COLOR = {255, 0, 255} -- magenta — "missing color" debug-marker
-- Texture-handle cache for sprite_atlas paths. lazy-loaded on first
-- draw_entities call that references the path. Lives for the engine
-- process; no eviction in v0.2.0.
local atlas_cache = {}
-- Fallback-rect size (px) for entities without any sprite-property.
-- Magenta is the debug-marker; size is small enough to be intrusive
-- without dominating the screen.
local FALLBACK_RECT_SIZE = 16
local function load_atlas(path)
local h = atlas_cache[path]
if h ~= nil then return h end
h = engine.asset.load_texture(path)
atlas_cache[path] = h
return h
end
-- Reads tile-color from TileType-table; fallback to magenta if missing or malformed.
local function tile_color(tile_type)
local c = tile_type.color
if type(c) ~= "table" or #c < 3 then return FALLBACK_COLOR end
return c
end
local M = {}
-- Iterates current (or specified) map's tiles, draws a colored rect per tile.
-- Must be called from inside a render hook (engine.render.* enforces this).
-- Returns the number of tiles drawn (for smoke verification + debug).
function M.draw_map(map_id)
local size = maps.size(map_id)
local tile_size = maps.tile_size(map_id)
local count = 0
for ty = 0, size.h - 1 do
for tx = 0, size.w - 1 do
local t = maps.tile_at(map_id, tx, ty)
if t ~= nil then
local c = tile_color(t)
local color = engine.render.rgb(c[1], c[2], c[3])
engine.render.draw_rect(
tx * tile_size, ty * tile_size,
tile_size, tile_size,
color)
count = count + 1
end
end
end
return count
end
-- ---------------------------------------------------------------------
-- v0.2.0 — Entity Rendering
-- ---------------------------------------------------------------------
-- Try to read an entity-property as a number; returns nil if absent
-- (instead of the engine's typed default), so callers can branch on
-- presence without conflating with default-zero.
--
-- engine.entity:get_property currently returns nil only if the property
-- isn't declared at all; declared-with-default-value yields the default.
-- v0.1 composition declares whatever's in the template — any property
-- not declared is genuinely absent and yields the engine fallback.
local function get_num(entity, key)
local v = entity:get_property(key)
if type(v) == "number" then return v end
return nil
end
local function get_str(entity, key)
local v = entity:get_property(key)
if type(v) == "string" and v ~= "" then return v end
return nil
end
-- Draws all entities tagged with `filter.tag`.
-- Filter shape: { tag = "<string>" } — anything else is loud-error.
-- Returns the number of entities drawn (for smoke verification).
function M.draw_entities(filter)
if type(filter) ~= "table" then
error("render.draw_entities: filter must be a table with field 'tag'")
end
if type(filter.tag) ~= "string" or filter.tag == "" then
error("render.draw_entities: filter.tag must be a non-empty string " ..
"(other filter-shapes deferred — see Phase-A Spec A-Q3)")
end
local list = composition.list_by_tag(filter.tag)
local count = 0
for _, e in ipairs(list) do
local px = get_num(e, "position.x")
local py = get_num(e, "position.y")
if px ~= nil and py ~= nil then
local atlas = get_str(e, "sprite_atlas")
if atlas ~= nil then
-- Path (1): atlas sub-rect.
local uvx = get_num(e, "sprite_uv.x")
local uvy = get_num(e, "sprite_uv.y")
local uvw = get_num(e, "sprite_uv.w")
local uvh = get_num(e, "sprite_uv.h")
if uvx and uvy and uvw and uvh then
local tex = load_atlas(atlas)
-- v0.3.0: optional sprite_scale; default 1.0 keeps
-- v0.2.0 behaviour for entities that don't set it.
local s = get_num(e, "sprite_scale") or 1.0
-- position is visual-center: origin = uv center.
-- Origin stays in source-uv coords (pre-scale), so
-- draw_sprite_transform applies scale around it.
engine.render.draw_sprite_transform(
tex, px, py, 0, s, s, uvw / 2, uvh / 2,
0xFFFFFFFF, uvx, uvy, uvw, uvh)
count = count + 1
end
else
local sc = e:get_property("sprite_color")
local sw = get_num(e, "sprite_w")
local sh = get_num(e, "sprite_h")
if type(sc) == "number" and sw and sh then
-- Path (2): colored rect, position = visual-center
engine.render.draw_rect(
px - sw / 2, py - sh / 2, sw, sh, sc)
count = count + 1
else
-- Path (3): magenta fallback
local fbcol = engine.render.rgb(
FALLBACK_COLOR[1], FALLBACK_COLOR[2], FALLBACK_COLOR[3])
local s = FALLBACK_RECT_SIZE
engine.render.draw_rect(px - s / 2, py - s / 2, s, s, fbcol)
count = count + 1
end
end
end
end
return count
end
-- Forward-compat stubs for later slices (kept as comments to avoid table-pollution):
-- DEPRECATED-MVP: function M.set_camera(camera) -- lib-core.camera slice
-- DEPRECATED-MVP: function M.push_layer(name, order) -- multi-layer slice
-- DEPRECATED-MVP: function M.register_pass(...) -- post-processing slice (spec §6)
-- DEPRECATED-MVP: function M.set_clear_color(rgb) -- per-layer clear-color (spec §4)
return M