diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 7ee4004..cbf4fda 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -3270,16 +3270,21 @@ return { roofRows = 19, roofBack = 16, roofFront = 0, roofCycle = { 2, 13 }, slab = 3, frontEave = 0, ledge = nil, depth = 2, }, - -- F02: the computer bank on the lab's west table (OAKS_LAB cell - -- 0,1) -- the one FACADE-ONLY template: rows 0-14 are machines - -- drawn face-on (monitor and keys, the centre console, the right - -- cabinet) over the lab-table fascia and base bands, and nothing - -- is drawn from above. roofRows = 0, so the whole drawing - -- extrudes -- the house facade treatment with no roof band; the - -- roof fields are inert and the recess pass sinks every framed - -- pane (screens, keys, the cabinet's inset face) into relief on - -- its own. Same drawing, same grid, stands in the Hall of Fame - -- on the GYM atlas -- registered there below. + -- F02: the computer desk on the lab's west side (OAKS_LAB cell + -- 0,1) -- the one DESK-SET template: the pipeline's region + -- classification at PART granularity (see Buildings + -- deskSetModel). The desk is the sibling lab table (fascia rows + -- 16-18, base 19-21); on it stand a monitor over its keyboard + -- (left), a computer tower over a keyboard and mouse (middle), + -- and a sheet of paper LYING FLAT (right). Upright parts anchor + -- their drawn bottom row to the desk's top plane and wear their + -- own drawn tops as lids; flat parts lie one voxel proud at + -- drawn row = depth row -- the same 1:1 the tabletop itself is + -- drawn with. The roof fields are inert (roofRows = 0 keeps the + -- recess scan over the whole drawing, which is what sinks the + -- monitor's screen and the tower's slots). Same drawing, same + -- grid, stands in the Hall of Fame on the GYM atlas -- + -- registered there below. { id = "lab_computers", tiles = { @@ -3289,6 +3294,16 @@ return { }, roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 }, slab = 0, frontEave = 0, ledge = nil, depth = 2, + desk = { fascia = { 16, 18 }, base = { 19, 21 } }, + parts = { + { kind = "upright", x = { 2, 13 }, top = { 0, 2 }, + facade = { 3, 10 }, depth = 4 }, -- the monitor + { kind = "flat", x = { 1, 13 }, rows = { 11, 14 } }, -- keyboard + { kind = "upright", x = { 14, 21 }, top = { 0, 3 }, + facade = { 4, 10 }, depth = 6 }, -- the tower + { kind = "flat", x = { 14, 21 }, rows = { 11, 14 } }, -- keys+mouse + { kind = "flat", x = { 22, 30 }, rows = { 1, 14 } }, -- the paper + }, }, -- F03: the empty north table beside it (OAKS_LAB cell 2,1): the -- starter table's band table verbatim on a grid two tiles @@ -3307,8 +3322,8 @@ return { GYM = { -- F02 again: the Hall of Fame's recording machine is the lab's - -- computer bank drawing, tile for tile, on the GYM atlas (one - -- placement: HALL_OF_FAME cell 4,1). Same band table as the DOJO + -- computer desk drawing, tile for tile, on the GYM atlas (one + -- placement: HALL_OF_FAME cell 4,1). Same part table as the DOJO -- entry above. { id = "lab_computers", @@ -3319,6 +3334,16 @@ return { }, roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 }, slab = 0, frontEave = 0, ledge = nil, depth = 2, + desk = { fascia = { 16, 18 }, base = { 19, 21 } }, + parts = { + { kind = "upright", x = { 2, 13 }, top = { 0, 2 }, + facade = { 3, 10 }, depth = 4 }, -- the monitor + { kind = "flat", x = { 1, 13 }, rows = { 11, 14 } }, -- keyboard + { kind = "upright", x = { 14, 21 }, top = { 0, 3 }, + facade = { 4, 10 }, depth = 6 }, -- the tower + { kind = "flat", x = { 14, 21 }, rows = { 11, 14 } }, -- keys+mouse + { kind = "flat", x = { 22, 30 }, rows = { 1, 14 } }, -- the paper + }, }, }, }, diff --git a/lib/Buildings.lua b/lib/Buildings.lua index 644cb09..d378ec7 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -307,11 +307,154 @@ end -- ----------------------------------------------------------------- build -- +-- A desk with separately-classified objects on it (a template's `parts` +-- list): the methodology's region classification at part granularity. +-- Upright parts anchor their drawn bottom row to the desk's top plane +-- and wear their own drawn tops as lids; flat parts (a keyboard, a +-- sheet of paper) lie one voxel proud at drawn row = depth row -- the +-- same 1:1 the tabletop itself is drawn with, so an object's height ON +-- the drawing is its position ON the desk. The desk is the lab-table +-- slab + base; its lid is the one synthesized surface in the model +-- (the objects cover every drawn pixel of the tabletop), continued +-- from the sibling tables' pattern in the drawing's own shades. +-- tools/building_voxels.py `build_desk_set` is the reference twin. +local function deskSetModel(sp, pr, t) + local W, H, D = sp.W, sp.H, pr.D + local ground = pr.ground + local col, inside = sp.col, sp.inside + local vox = {} + local function key(x, y, z) return (y * D + z) * W + x end + local function put(x, y, z, i) vox[key(x, y, z)] = i end + + -- de-outline walk bounded to the part, so a part's side faces show + -- its own material and never the neighbour's (the sprite-wide walk + -- the facade path uses would cross the black seam between units) + local function interiorAt(sx, sy, lo, hi) + local i = sy * W + sx + if col[i] ~= BLACK then return sx end + local step = sx < math.floor((lo + hi) / 2) and 1 or -1 + for d = 1, 3 do + local nx = sx + step * d + if nx >= lo and nx <= hi then + local ni = sy * W + nx + if inside[ni] and col[ni] ~= BLACK then return nx end + end + end + return sx + end + + local f0, f1 = t.desk.fascia[1], t.desk.fascia[2] + local b0, b1 = t.desk.base[1], t.desk.base[2] + local plane = (b1 - b0 + 1) + (f1 - f0 + 1) + + -- the base band, extruded exactly like every lab table's + for sy = b0, b1 do + Budget.tick() + local y = ground - 1 - sy + for sx = 0, W - 1 do + if inside[sy * W + sx] then + local ix = interiorAt(sx, sy, 0, W - 1) + for z = 0, D - 1 do + local px = (z == 0 or z == D - 1) and sx or ix + put(sx, y, z, sy * W + px) + end + end + end + end + for i in pairs(pr.recess) do + local sy = math.floor(i / W) + if sy >= b0 and sy <= b1 then + vox[key(i % W, ground - 1 - sy, D - 1)] = nil + end + end + + -- the slab: fascia rows wrap every side; the lid continues the + -- sibling tables' top -- black rim, white highlight courses along + -- the north and west, grey field + for sy = f0, f1 do + Budget.tick() + local y = plane - 1 - (sy - f0) + for sx = 0, W - 1 do + for z = 0, D - 1 do put(sx, y, z, sy * W + sx) end + end + end + for sx = 0, W - 1 do + for z = 0, D - 1 do + local shade = GREY + if sx == 0 or sx == W - 1 or z == 0 or z == D - 1 then + shade = BLACK + elseif sx == 1 or z == 1 then + shade = WHITE + end + put(sx, plane - 1, z, pr.shadeTexel[shade]) + end + end + + local ytop = plane + for _, p in ipairs(t.parts) do + Budget.tick() + local x0, x1 = p.x[1], p.x[2] + if p.kind == "flat" then + for sy = p.rows[1], p.rows[2] do + if sy >= 0 and sy < D then + for sx = x0, x1 do + if inside[sy * W + sx] then put(sx, plane, sy, sy * W + sx) end + end + end + end + else + local tr0, tr1 = p.top[1], p.top[2] + local fr0, fr1 = p.facade[1], p.facade[2] + local pd = p.depth + local ytp = plane + (fr1 - fr0) + if ytp > ytop then ytop = ytp end + for sx = x0, x1 do + -- the lid: the part's drawn top laid across its depth from the + -- back, last row continuing forward; the front lid row is the + -- facade's own top row -- the drawn front-top edge + for z = 0, pd - 1 do + local front = z == pd - 1 + local sy = front and fr0 or math.min(tr0 + z, tr1) + while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end + local ok = sy <= tr1 or (front and inside[fr0 * W + sx]) + if ok then + put(sx, ytp, z, (front and fr0 or sy) * W + sx) + end + end + -- the body: facade rows anchored to the desk's top plane + for sy = fr0 + 1, fr1 do + local y = plane + (fr1 - sy) + local i = sy * W + sx + if inside[i] then + local ix = interiorAt(sx, sy, x0, x1) + for z = 0, pd - 1 do + if z == pd - 1 then + if not pr.recess[i] then put(sx, y, z, i) end + elseif z == 0 then + put(sx, y, z, i) + else + put(sx, y, z, sy * W + ix) + end + end + end + end + end + end + end + + return { at = function(x, y, z) + if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end + return vox[key(x, y, z)] + end, + W = W, ytop = ytop, zmin = 0, zmax = D - 1 } +end + -- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite -- pixel that voxel wears, or nil. Build ORDER is expressed as lookup -- order -- roof first, so it overwrites the walls it intersects, and walls -- are trimmed to its underside so nothing pokes through the surface. local function model(sp, pr, t) + if t.parts then return deskSetModel(sp, pr, t) end local W, H, D = sp.W, sp.H, pr.D local slab, roofRows = t.slab, t.roofRows local top, ytop, ground = pr.top, pr.ytop, pr.ground diff --git a/tools/building_voxels.py b/tools/building_voxels.py index a38f8c5..6cb00b7 100644 --- a/tools/building_voxels.py +++ b/tools/building_voxels.py @@ -569,15 +569,21 @@ TEMPLATES = { roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13), slab=3, front_eave=0, ledge=None, tileset="gym", depth=2, ), - # F02: the computer bank on Oak's lab's west table -- and the Hall - # of Fame's recording machine, the same drawing on the GYM atlas - # (one placement each; both registered in voxel_heights). The one - # FACADE-ONLY template: rows 0-14 are machines drawn face-on over - # the lab-table fascia and base bands, nothing is drawn from above, - # so roof_rows=0 and the whole drawing extrudes -- the house facade - # treatment with no roof band. The recess pass turns every framed - # pane (the monitor, its keys, the right cabinet's inset face) into - # relief on its own. + # F02: the computer desk in Oak's lab -- and the Hall of Fame's + # recording machine, the same drawing on the GYM atlas (one + # placement each; both registered in voxel_heights). The one + # DESK-SET template: the methodology's region classification at + # part granularity. The desk is the sibling lab table (fascia rows + # 16-18, base 19-21); on it stand a monitor over its keyboard + # (left), a computer tower over a keyboard and mouse (middle), and + # a sheet of paper LYING FLAT (right). Upright parts anchor their + # drawn bottom row to the desk's top plane and wear their own drawn + # tops as lids; flat parts lie one voxel proud with drawn row = + # depth row -- the same 1:1 the tabletop itself is drawn with, so + # an object's height ON the drawing is its position ON the desk. + # The desk's own top is the one synthesized surface (the objects + # cover every pixel of it), continued from the sibling tables' + # pattern in the drawing's own shades. "lab_computers": dict( tiles=[ [91, 92, 93, 94], @@ -586,6 +592,16 @@ TEMPLATES = { ], roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0), slab=0, front_eave=0, ledge=None, tileset="gym", depth=2, + desk=dict(fascia=(16, 18), base=(19, 21)), + parts=[ + dict(kind="upright", x=(2, 13), top=(0, 2), facade=(3, 10), + depth=4), # the monitor + dict(kind="flat", x=(1, 13), rows=(11, 14)), # its keyboard + dict(kind="upright", x=(14, 21), top=(0, 3), facade=(4, 10), + depth=6), # the tower + dict(kind="flat", x=(14, 21), rows=(11, 14)), # keys + mouse + dict(kind="flat", x=(22, 30), rows=(1, 14)), # the paper + ], ), # F03: the empty north table beside it -- the starter table's band # table verbatim on a grid two tiles narrower (one placement). @@ -741,10 +757,122 @@ def profile(sp, t): # --------------------------------------------------------------- stage 3 -- +def build_desk_set(sp, pr, t): + """A desk with separately-classified objects on it (the `parts` list): + upright parts stand on the desk's top plane wearing their own drawn + tops as lids, flat parts lie one voxel proud at drawn row = depth row, + and the desk itself is the lab-table slab + base with a synthesized + lid (the objects cover every drawn pixel of the tabletop).""" + W, H, D = pr["W"], pr["H"], pr["D"] + inside = pr["inside"] + col, src = sp["col"], sp["src"] + ground = pr["ground"] + vox = {} + + def put(x, y, z, sx, sy): + vox[(x, y, z)] = (col[sy][sx], src[sy][sx]) + + shade_px = {} + for sy in range(H): + for sx in range(W): + if inside(sx, sy): + shade_px.setdefault(col[sy][sx], (sx, sy)) + + def interior(sx, sy, lo, hi): + if col[sy][sx] != BLACK: + return sx + step = 1 if sx < (lo + hi) // 2 else -1 + for d in range(1, 4): + nx = sx + step * d + if lo <= nx <= hi and inside(nx, sy) and col[sy][nx] != BLACK: + return nx + return sx + + f0, f1 = t["desk"]["fascia"] + b0, b1 = t["desk"]["base"] + plane = (b1 - b0 + 1) + (f1 - f0 + 1) # the desk's top plane + + # the base band, extruded exactly like every lab table's + for sy in range(b0, b1 + 1): + y = ground - 1 - sy + for sx in range(W): + if not inside(sx, sy): + continue + ix = interior(sx, sy, 0, W - 1) + for z in range(D): + put(sx, y, z, sx if z in (0, D - 1) else ix, sy) + for sx, sy in pr["recess"]: + if b0 <= sy <= b1: + vox.pop((sx, ground - 1 - sy, D - 1), None) + + # the slab: the fascia rows wrap every side; the lid is the one + # synthesized surface in the model -- the drawing never paints the + # tabletop (its objects cover it), so the lid continues the sibling + # tables' pattern in the drawing's own shades: black rim, white + # highlight courses along the north and west, grey field + for i, sy in enumerate(range(f0, f1 + 1)): + y = plane - 1 - i + for sx in range(W): + for z in range(D): + put(sx, y, z, sx, sy) + for sx in range(W): + for z in range(D): + if sx in (0, W - 1) or z in (0, D - 1): + shade = BLACK + elif sx == 1 or z == 1: + shade = WHITE + else: + shade = GREY + px = shade_px.get(shade) or shade_px[BLACK] + put(sx, plane - 1, z, px[0], px[1]) + + for p in t["parts"]: + x0, x1 = p["x"] + if p["kind"] == "flat": + r0, r1 = p["rows"] + for sy in range(r0, r1 + 1): + if not 0 <= sy < D: + continue + for sx in range(x0, x1 + 1): + if inside(sx, sy): + put(sx, plane, sy, sx, sy) + continue + tr0, tr1 = p["top"] + fr0, fr1 = p["facade"] + pd = p["depth"] + ytp = plane + (fr1 - fr0) + for sx in range(x0, x1 + 1): + # the lid: the part's drawn top laid across its depth from + # the back, last row continuing forward; the front lid row + # is the facade's own top row -- the drawn front-top edge + for z in range(pd): + sy = fr0 if z == pd - 1 else min(tr0 + z, tr1) + while sy <= tr1 and not inside(sx, sy): + sy += 1 + if sy > tr1 and not (z == pd - 1 and inside(sx, fr0)): + continue + put(sx, ytp, z, sx, fr0 if z == pd - 1 else sy) + # the body: facade rows anchored to the desk's top plane + for sy in range(fr0 + 1, fr1 + 1): + y = plane + (fr1 - sy) + if not inside(sx, sy): + continue + ix = interior(sx, sy, x0, x1) + for z in range(pd): + if z == pd - 1: + if (sx, sy) not in pr["recess"]: + put(sx, y, z, sx, sy) + else: + put(sx, y, z, sx if z == 0 else ix, sy) + return vox + + def build(sp, pr, t): """The voxel model. Order is load-bearing: walls, ledge, recesses, then the roof solid overwrites what it intersects and the walls are trimmed to the roof's underside.""" + if t.get("parts"): + return build_desk_set(sp, pr, t) W, H, D = pr["W"], pr["H"], pr["D"] inside, top, ytop = pr["inside"], pr["top"], pr["ytop"] col, src = sp["col"], sp["src"] @@ -869,12 +997,13 @@ def verify(vox, sp, pr, t): for (x, y, z), _ in vox.items(): assert y <= T(x), f"voxel pokes through the roof at {x},{y},{z}" - # A facade-only template (roof_rows == 0: the lab computer bank) has - # no roof band, so there is no surface to hold level or slope and no - # coverage to demand of it; the poke-through assert above and the - # facade asserts below are what it answers to. Everything with a roof - # band answers the full set. - if t["roof_rows"] == 0: + # A desk set answers its own asserts; a facade-only template (roof_rows + # == 0) has no roof band, so there is no surface to hold level or slope + # and no coverage to demand of it; everything with a roof band answers + # the full set. + if t.get("parts"): + verify_desk_set(vox, pr, t) + elif t["roof_rows"] == 0: # its one geometric intent: a straight extrusion, so each column # of the drawing is solid from its lowest voxel to its top. NOT # from the ground -- the drawing's base band is inset like every @@ -899,6 +1028,46 @@ def verify(vox, sp, pr, t): return shell +def verify_desk_set(vox, pr, t): + W, D = pr["W"], pr["D"] + f0, f1 = t["desk"]["fascia"] + b0, b1 = t["desk"]["base"] + plane = (b1 - b0 + 1) + (f1 - f0 + 1) + + # the slab is a full solid under everything on it + for x in range(W): + for z in range(D): + for y in range(b1 - b0 + 1, plane): + assert (x, y, z) in vox, f"slab hole at {x},{y},{z}" + + # each part stands where authored -- upright bodies solid from the + # desk to their own drawn top (bar the front layer, whose panes + # sink) -- and nothing stands anywhere else + tops = {} + for p in t["parts"]: + x0, x1 = p["x"] + if p["kind"] == "flat": + r0, r1 = p["rows"] + for x in range(x0, x1 + 1): + for z in range(max(r0, 0), min(r1, D - 1) + 1): + tops[(x, z)] = max(tops.get((x, z), 0), plane) + else: + fr0, fr1 = p["facade"] + ytp = plane + (fr1 - fr0) + for x in range(x0, x1 + 1): + for z in range(p["depth"]): + tops[(x, z)] = max(tops.get((x, z), 0), ytp) + if z == p["depth"] - 1: + continue + ys = [y for y in range(plane, ytp + 1) + if (x, y, z) in vox] + assert ys == list(range(ys[0], ys[0] + len(ys))) \ + if ys else True, f"part column hole at {x},{z}" + for (x, y, z) in vox: + assert y <= tops.get((x, z), plane - 1), \ + f"voxel above its part at {x},{y},{z}" + + def verify_roof(vox, pr, t): W, H, D = pr["W"], pr["H"], pr["D"] ytop, top, slab = pr["ytop"], pr["top"], t["slab"]