DramaticShapeVoxelMod archival mirror · read-only
91cc2d6f51bc50ba5f8bf5ab4c6092dd1c4ad13b

bills, pewter gym, register, celadon mansion, tables, garbage cans

DramaticShape · 2026-08-01 · parent 47363b8d23

bills, pewter gym, register, celadon mansion, tables, garbage cans

Changed files

data/voxel_heights.lua        |  979 +++++++++++++++++++++++++++++++++++++++++++----
 lib/Buildings.lua             |  544 +++++++++++++++++++++++----
 lib/Structures.lua            | 1255 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 lib/TileShape.lua             |  213 +++++++++--
 lib/Water.lua                 |   70 +++-
 tests/dramatic_shape_test.lua |   35 +-
 tests/voxel_figure_test.lua   |  166 ++++++++
 tools/building_voxels.py      | 1005 ++++++++++++++++++++++++++++++++++++++++++++-----
 8 files changed, 3811 insertions(+), 456 deletions(-)

Diff

Diff truncated at 200.0 KB — fetch the full change with git show 91cc2d6f51 after cloning.

diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua
index 97f52f6..2c5ae61 100644
--- a/data/voxel_heights.lua
+++ b/data/voxel_heights.lua
@@ -58,12 +58,43 @@
 --                           collapses onto a one-cell-deep box at its
 --                           full drawn height; back rows become hidden
 --                           floor, and an unpinnable trim row above
---                           becomes the cap
+--                           becomes the cap.  Its FRONT carries the
+--                           measured pane relief -- every non-black
+--                           region the drawing seals behind its own
+--                           black frame sinks one voxel, the same rule
+--                           lib/Buildings.lua recesses a facade with,
+--                           so the books stand in the shelf instead of
+--                           being painted on it.  A tileset that
+--                           borrows the collapse for something that is
+--                           not a shelf says `bookcase_relief = false`
+--   cylinder / canopy       round scenery (tree canopies): a voxel hull
+--   / stump                 cut from the art's own darkest-pixel outline,
+--                           round in depth -- one 16px cell, a 2x2-cell
+--                           group, or with the drawn top read as a cut face
+--   can                     that hull cut at both ends, hollowed and
+--                           tapered: an OPEN bin standing on a floor
+--                           (Vermilion Gym's trash cans).  The drawn mouth
+--                           ellipse projects across the round top and down
+--                           the well, the drawn base ellipse is ground
+--                           contact rather than body, the top rows keep a
+--                           wall at each end of their chord and lose the
+--                           middle, and the plan narrows toward the floor
+--                           (can_cap / can_base in art rows; can_height,
+--                           can_well, can_taper in voxels)
+--   planter                 the same hull for a round drawing stacked TWO
+--                           cells high on ONE cell of plot (the Centers'
+--                           potted plants): it stands in the lower cell and
+--                           the upper cell is the height it overhangs
 --   billboard / prop        a prop drawn face-on: a standing per-pixel
 --                           cutout, black-outline segmented; drawn above
 --                           a pinned box it stands ON that box (a
 --                           monitor on its desk).  prop is a second pool
 --                           so two touching cutouts stay separate
+--   bike                    the same cutout at TWO voxels: a vehicle drawn
+--                           side-on is a LINE drawing, and the air inside
+--                           its frame is what makes it read as one -- any
+--                           thicker and neighbouring strokes close those
+--                           gaps with their own side faces
 --
 -- A tileset entry may also carry `prop_ground` (not a class): a map of
 -- prop tile id -> ground tile id naming the tile painted under that
@@ -83,7 +114,20 @@
 -- and nothing automatic can either when the figure has no background
 -- margin to flood from and wears the same shades as what it sits on.
 -- The Pokemon Center's seated man is the case; see POKECENTER below for
--- the format.
+-- the format.  A figure entry that states a `depth` is not a person and
+-- stands as a per-pixel SOLID instead of a flat card -- the Marts' cash
+-- register, a machine set down on a counter (`thin` gives its top rows a
+-- thickness of their own, for the part of a drawing that is paper).
+--
+-- And `mounted` (not a class either): the same authored-mask escape for a
+-- thing painted INTO a WALL band, stood proud of the panel as a thin
+-- per-pixel slab instead of standing on its feet as a sprite card.  Where
+-- a figure is a person and reads face-on from any angle, a mounted object
+-- is an object drawn side-on and holds the wall's own plane; and it keeps
+-- its DRAWN elevation, so a thing hung clear of the floor stays hung.
+-- The Bike Shop's two wall bicycles are the case -- and the mask there is
+-- measured rather than drawn, by flooding the panel tile's own stripe out
+-- from behind them; see CLUB below.
 --
 -- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from
 -- above, a facade seen face-on and sloped ends as diagonal silhouettes,
@@ -117,6 +161,13 @@ return {
     desk = 24,
     prop = 16,
     cutout = 16,
+    -- a vehicle drawn side-on (the Bike Shop's bicycles): a standee like
+    -- the pools above, two voxels thin so the air inside its frame stays
+    -- air (see Structures' PINNED_DEPTH)
+    bike = 16,
+    -- a round drawing stacked two cells high on one cell of plot (the
+    -- Centers' potted plants): 32px of hull standing in its lower cell
+    planter = 32,
     relief = 3,
     bookcase = 32,
     stair_e = 16,
@@ -195,12 +246,8 @@ return {
     -- black-outline segmented -- backgrounds flood away, the pixels
     -- the outline encloses stay.  Everything stands on the gyms' main
     -- floor tile ($11), which is also Bruno's floor.
-    -- (The round boulder drawn beside some statues, $07/$08/$17/$18,
-    -- is NOT pinned: it also tiles wall-to-wall as Pewter's rock rows,
-    -- which are scenery for the detector, not a lone prop.  Probed:
-    -- the repeat-aware scenery path already gives every one of them a
-    -- single 16px course in Pewter's and Bruno's rock walls alike, so
-    -- a pin would only restate the derived answer.)
+    -- (The boulders, $07/$08/$17/$18, are pinned round -- see the
+    -- `cylinder` pool below.)
     --
     -- The rest of the tileset is the ten rooms' own furniture: the six
     -- badge gyms it serves (Pewter, Cerulean, Vermilion, Celadon,
@@ -270,13 +317,100 @@ return {
       -- from the darkest-pixel outline -- the same treatment the
       -- overworld's tree canopies take, and per-CELL, so a hedge row
       -- becomes a row of bushes instead of one boxed monolith.
-      cylinder = { 44, 45, 46, 47 },
-      -- The statues, the cans, and Celadon's three little trees
-      -- ($40/$41 canopy over $50/$51 trunk).  A trunk is not round, so
-      -- the tree cannot be a ball like the shrubs beside it -- it takes
-      -- the thin standee pool every interior plant takes, which is also
-      -- a pool apart from the cylinders it touches.
-      prop = { 2, 56, 18, 19, 11, 12, 27, 28,
+      --
+      -- The rock gyms' boulder ($07/$08 over $17/$18) is the same
+      -- reading: one cell is one rock, drawn as a lit dome -- white
+      -- highlight up the north-west, mid-grey body, black-and-grey
+      -- rubble dither filling the rest -- with the FLOOR showing
+      -- through all four corners, which is what makes a run of them
+      -- read as a pile of separate stones rather than a wall (tile
+      -- them and the gaps between four rocks draw a grey diamond).
+      -- Left derived it fell to the repeat-aware scenery path, which
+      -- extruded the whole drawing as one 16px course: Pewter's and
+      -- Bruno's rock rows came out as square bars wearing a boulder
+      -- texture in relief -- the extruded picture.  Round-pinned,
+      -- each cell is a hull whose plan view is its own drawn width
+      -- profile turned in depth: a dome 16 wide and 16 tall, full
+      -- width from the drawn shoulder down and tapering over the top
+      -- five rows exactly where the art tapers, and the corner
+      -- diamonds open up between them the way the drawing has them.
+      -- It stays 16px, so nothing that stands on or beside a rock
+      -- moves.  The perimeter rows take the same model as the maze
+      -- clusters -- one drawing, one rock, everywhere.
+      -- Scanned: across all 222 maps these four tiles occur 87 times
+      -- on this atlas and every one of them anchors a whole boulder
+      -- cell (87 tile-$07 hits, 87 grid matches), in exactly two maps
+      -- -- PEWTER_GYM's walls and rock maze, and BRUNOS_ROOM's
+      -- clusters.  DOJO shares gym.png but places none of them, so
+      -- the pin is not copied there.
+      cylinder = { 44, 45, 46, 47,
+                   7, 8, 23, 24 },
+      -- Vermilion Gym's trash cans ($0B/$0C over $1B/$1C), the switch
+      -- puzzle's fifteen cans plus the sixteenth beside the leader's
+      -- platform.  An open galvanised bin in the 3/4 view, and its plan is
+      -- measured: the silhouette runs straight down both flanks for art
+      -- rows 4-10 and is 11px wide there, so the can is 11 across -- and
+      -- being round in plan, 11 DEEP.  Above row 4 is the MOUTH seen from
+      -- above (the drawn ring is 9x5, a circle flattened to 55%, which is
+      -- what fixes it as a top view rather than a face-on dome); below row
+      -- 10 is the base circle's front arc, ground contact, with the
+      -- drawing's own $555 halo one pixel outside it as the grounding
+      -- shadow.
+      --
+      -- Left in the thin standee pool the can was a flat disc on edge --
+      -- fifteen coins standing in a row.  Pinned a plain `cylinder` it
+      -- revolves every drawn row, bottom arc included, and comes out a
+      -- barrel balanced on a three-voxel stem.  `can` is the hull cut at
+      -- both ends and hollowed: the mouth projects across the round top,
+      -- the base rows are ground rather than body, and the top can_well
+      -- voxel rows keep a wall at each end of every chord and lose their
+      -- middle, so the bin is open and you look down into it.
+      --
+      -- can_cap and can_base are the two ellipses in art rows and come off
+      -- the pixels.  The other three do NOT, and are the numbers taste
+      -- moves:
+      --
+      --   can_height 9    Un-projected strictly the drawing states a squat
+      --                   drum barely two rows of straight side tall,
+      --                   because the GB artist spent most of a 16px cell
+      --                   on the opening -- but a bin is taller than it is
+      --                   wide and the flat game reads as one, the drawing
+      --                   being 14px tall beside a 16px player.  The lowest
+      --                   surviving body row (art row 10: black rim, shaded
+      --                   flank, lit face) is repeated up to this, the
+      --                   plainest continuation of the material drawn.
+      --                   `heights` must follow it so anything riding a can
+      --                   lands on the rim.
+      --   can_well 5      How far down the mouth is hollowed.  The drawing
+      --                   paints an opening and cannot say how deep.
+      --   can_taper 4     Voxels of DIAMETER the base loses -- 11 at the
+      --                   rim to 7 on the floor, stepped twice over the
+      --                   height.  The drawing's base arc pulls in to 9px
+      --                   on its own, so the direction is drawn; the amount
+      --                   is not.  2 is one barely-visible step, 4 reads as
+      --                   a cone.
+      --
+      -- Scanned: the four ids occur as this grid 16 times on the GYM atlas
+      -- and only in VERMILION_GYM -- cells (1/3/5/7/9, 7), the same five
+      -- on rows 9 and 11, and (6,1).  DOJO shares gym.png and places none,
+      -- so the pin is not copied there.  The same four ids form the same
+      -- grid on eight OTHER atlases (the Bike Shop's crates, the overworld
+      -- roofs, the Centers' counters, ...) for 142 more hits; those are id
+      -- collisions between tilesets, and a pin is per tileset id, so they
+      -- are none of this entry's business.
+      can = { 11, 12, 27, 28 },
+      can_cap = 9,
+      can_base = 4,
+      can_height = 9,
+      can_well = 5,
+      can_taper = 4,
+      heights = { can = 9 },
+      -- The statues and Celadon's three little trees ($40/$41 canopy over
+      -- $50/$51 trunk).  A trunk is not round, so the tree cannot be a
+      -- ball like the shrubs beside it -- it takes the thin standee pool
+      -- every interior plant takes, which is also a pool apart from the
+      -- cylinders it touches.
+      prop = { 2, 56, 18, 19,
                64, 65, 80, 81 },
       -- The Hall of Fame's recording machine, the one piece of real
       -- furniture in the tileset.  It is drawn 32px wide and THREE tile
@@ -629,6 +763,11 @@ return {
     -- the floor above).  Bookcases, dining table with its flower pot,
     -- stools, the TV on the floor, and the staircase up.
     REDS_HOUSE_1 = {
+      -- the `house_stool` template below stands 5 voxels and the
+      -- `reds_house_table` model 6 (both the drawn elevation): whoever
+      -- sits on a stool cell and whatever object sprite stands on the
+      -- table rides these heights, not the 8/12px class defaults
+      heights = { stool = 5, table = 6 },
       wall = { 0, 36, 37, 52, 53 },
       stool = { 2, 3, 18, 19 },
       -- the dining table (38-44/58-60); its top row also caps the
@@ -656,6 +795,12 @@ return {
     -- wall-height volumes and towers the table, so every object is
     -- pinned to the shape it depicts.
     HOUSE = {
+      -- the `house_stool` template below stands 5 voxels and the
+      -- `house_table` model 6 (both the drawn elevation): whoever sits
+      -- on a stool cell -- Daisy at her table -- and whatever object
+      -- sprite stands on a table cell rides these heights, not the
+      -- 8/12px class defaults
+      heights = { stool = 5, table = 6 },
       -- the wall band stays one 16px face: blank courses, the window,
       -- the framed picture, and the schoolhouse blackboard (72-75/88-91)
       wall = { 0, 36, 45, 46, 52, 61, 62, 72, 73, 75, 88, 89, 90, 91 },
@@ -694,14 +839,17 @@ return {
       -- look-only), the pokeball poster (2/3/18/19), and the pillars
       -- (16/41) with their bases (4/5/20/21; 20 is the $14 water-fallback
       -- trap and would recess into a pond lip).  The healing machines'
-      -- bodies (72/76/77 console face, 6/22 button panel, 73 light edge,
-      -- 7/13 shadowed edge) are ALSO wall: drawn 16px tall against the
-      -- band, so wall height is their drawn height and they read as
-      -- consoles jutting from it; the near-black screen tiles must be
-      -- pinned or the void rule flattens them, and 72 is the $48
-      -- water-fallback trap
-      wall = { 2, 3, 4, 5, 6, 7, 13, 16, 18, 19, 20, 21, 22, 40, 41,
-               72, 73, 76, 77, 92, 93, 94, 95 },
+      -- console face (76/77) and button panel (6/22) are ALSO wall:
+      -- drawn 16px tall against the band, so wall height is their drawn
+      -- height and they read as equipment jutting from it.  (The
+      -- `center_heal_machine` template below claims those four tiles at
+      -- every placement and models the console properly; these pins are
+      -- the degradation path when the profile is absent.)  The
+      -- near-black screen tiles must be pinned or the void rule flattens
+      -- them.  What is NOT wall is the machines' two flanks -- see
+      -- `prop` below.
+      wall = { 2, 3, 4, 5, 6, 16, 18, 19, 20, 21, 22, 40, 41,
+               76, 77, 92, 93, 94, 95 },
       -- the counters, half a cell high: top band (8) with the nurse's
       -- tray (10), front face (24/25, the game's counterTiles), left end
       -- cap (56) and the Cable Club's light sections (90/91).  8px is
@@ -732,6 +880,29 @@ return {
       -- bodies so they stand ON them) and the PC (66/70/82/86), which
       -- stands on its pinned desk
       billboard = { 58, 59, 66, 70, 74, 75, 82, 86 },
+      -- The healing machines' two FLANKS, which are not the machine and
+      -- not the wall: thin equipment standing on the floor beside the
+      -- console, drawn against a plain light-grey ground with no floor
+      -- checker in it.  `wall` boxed each of them into a solid 16px
+      -- half-cell wearing its drawing in relief -- the billboard failure,
+      -- one tile wide.
+      --   72   the west flank of both machines, and 73 its mirror on the
+      --        east machine: a pair of PIPES leaving the console and
+      --        bending down into the floor in an L, the 8px motif drawn
+      --        once per pipe.  Round and thin, so the THIN pool's 5px
+      --        slab, per-pixel, is the whole of them; a box is a wall
+      --        stub with pipework printed on it.
+      --   7/13 the west machine's east flank: a KEYBOARD, one 16px panel
+      --        over two tiles (keys, and the wide bar down its right).
+      --        Same thin standee; the drawn panel IS its thickness.
+      -- All four ids are used at these flanks and nowhere else in the
+      -- game -- 12 maps, the same two cells in each -- so this pin cannot
+      -- reach anything but the machines.  72 is still the $48
+      -- water-fallback trap and still has to carry SOME pin; it just
+      -- wanted the thin one, not the wall.
+      prop = { 7, 13, 72, 73,
+               -- ...and the potted plants -- see THE POTTED PLANTS below
+               32, 33, 34, 35, 48, 49, 50, 51 },
       -- The man on the couch, cut out by hand and stood up (see `figures`
       -- below).  Nothing automatic reaches him.  A class pin resolves a
       -- whole 8x8 tile and he SHARES his tiles with the couch, so pinning
@@ -745,34 +916,35 @@ return {
       -- sofa he is painted into, so that is what `figures` carries.
       -- the PC's desk body, which the PC stands on
       table = { 9, 88 },
-      -- the potted plants: bush (32/33/48/49) over pot (34/35/50/51,
-      -- 50 is the $32 water-fallback trap).  Standing per-pixel
-      -- billboards in the THIN pool, like every other interior plant --
-      -- and a pool apart from the sofa, which touches the corner pair
-      -- and must stay a separate object.  The bush's light checker
-      -- highlights share the floor's shades, so the mask drains some of
-      -- them; the standee reads darker than the flat art, which is the
-      -- accepted trade for a real plant silhouette
-      prop = { 32, 33, 34, 35, 48, 49, 50, 51 },
-      -- ...but the POTS were draining outright.  The plant pair is drawn
-      -- as one 4x4-tile block and the pot's olive base is flush on that
-      -- block's bottom row, so the background vote -- which reads the
-      -- shades touching the drawing's own bounding box -- came back with
-      -- "dark" in it, and every dark pixel in the plant left with the
-      -- floor.  The pots rendered as hollow black frames around one or
-      -- two surviving pixels while the flat art has solid olive bodies.
-      --
-      -- So name the background outright for these eight tiles: the floor
-      -- IS light and white here, and nothing else is.  That restores 152
-      -- pixels of the block (55% -> 69% of it kept) and cannot move any
-      -- other prop, which matters -- the same call tileset-wide would pull
-      -- the dark wall band into the healing consoles' screens and strip
-      -- three pixels off the PC, because those two want the opposite
-      -- answer on the very same shades.
-      prop_bg = {
-        { tiles = { 32, 33, 34, 35, 48, 49, 50, 51 },
-          shades = { "light", "white" } },
-      },
+      -- THE POTTED PLANTS: a leaf crown (32/33/48/49) over an urn
+      -- (34/35/50/51; 50 is the $32 water-fallback trap), the most
+      -- repeated interior prop in the game -- 78 placements over 13 maps
+      -- across this id and MART (scan: six per Center, two in the
+      -- Celadon hotel's lounge rows, four in the Plateau lobby).
+      --
+      -- A plant is the THIN pool's textbook case: the drawing is ONE
+      -- organic silhouette -- crown, 3px stem, urn, all 8-connected,
+      -- 350 of the block's 512 pixels -- so it stands as a per-pixel
+      -- cutout 5 voxels deep at its real drawn height, 32px over the
+      -- two stacked cells.  Its lowest drawn row is the block's bottom
+      -- row, so it stands in the pot cell's south band -- drawn low,
+      -- near the front of its blocked plot -- and the crown overhangs
+      -- the stem the way the drawing says, which no box or hull cut to
+      -- cell granularity can do.  Both cells' tiles carry the pin (in
+      -- `prop` above); the pool clusters them into one drawing, and the
+      -- two side-by-side placements (every map pairs or quads them)
+      -- share one cluster whose per-pixel geometry is identical to two
+      -- separate ones.
+      -- The plant drawing has no floor margin to vote from: the urn's
+      -- #555 foot lies flush on the block's bottom edge, so the rim
+      -- vote reads "dark" as background and drains the urn's own body.
+      -- Name the background outright instead: the floor showing
+      -- through the crown's gaps is white+light and nothing else,
+      -- while the crown's own white/light highlights are sealed inside
+      -- its black/#555 outline -- a light+white flood from the ring
+      -- reproduces the extract mask exactly, leaves intact.
+      prop_bg = { { tiles = { 32, 33, 34, 35, 48, 49, 50, 51 },
+                    shades = { "light", "white" } } },
       -- THE MAN ON THE COUCH.  He is drawn INTO the lounge furniture and
       -- spills out of it in BOTH directions, which is why he needs three
       -- tile columns:
@@ -908,6 +1080,21 @@ return {
       -- part of that same work surface now that the machine has been cut
       -- off them as a sprite -- see `figures` below.
       counter = { 8, 14, 15, 16, 24, 25, 30, 31, 41, 56, 89 },
+      -- the same potted plants as POKECENTER above -- MART shares that
+      -- atlas, so this is the identical drawing tile for tile, and
+      -- INDIGO_PLATEAU_LOBBY (the only map on this id that places it)
+      -- puts four of them along the hall.  Across POKECENTER and MART
+      -- all eight of these ids appear exactly 78 times each, which is
+      -- exactly how often the 2x4 plant grid appears: they are never
+      -- anything but this plant, so pinning them by tile cannot catch
+      -- anything else.  Same reading as the POKECENTER entry: one
+      -- organic silhouette in the THIN standee pool, per-pixel, 5
+      -- voxels deep at its drawn 32px height, with the background
+      -- shades named because the urn's foot lies flush on the block's
+      -- bottom edge and votes the drawing's own darks away.
+      prop = { 32, 33, 34, 35, 48, 49, 50, 51 },
+      prop_bg = { { tiles = { 32, 33, 34, 35, 48, 49, 50, 51 },
+                    shades = { "light", "white" } } },
       -- Left to the derived default on purpose: the floor checker and
       -- its shadowed variants (1/11/17/26/27/54) and the exit mat
       -- (12/28) all sit in cells the ROM marks walkable, so the cell
@@ -915,13 +1102,14 @@ return {
       -- interior, used to be left to the volume path here; it is now the
       -- back wall's third band, because a run of four rows has to be
       -- contiguous for the wall to collapse into one box at all.)
-      -- THE CASH REGISTER: a keypad and a curl of receipt paper drawn
-      -- face-on across two tile rows in the middle of the counter's east
-      -- arm.  Like the Center's seated man it is a FIGURE drawn into the
-      -- furniture, so it gets the same treatment -- a flat sprite card
-      -- standing on the counter -- and for the same reason: no class pin
-      -- can separate it from the counter, because the counter draws its
-      -- own edging down columns 0 and 15 of the very same tiles.
+      -- THE CASH REGISTER: a till drawn across two tile rows in the middle
+      -- of the counter's east arm, and the same drawing in all nine maps
+      -- on this id (eight Marts plus the Indigo Plateau lobby), always at
+      -- cell (1,5).  Like the Center's seated man it is drawn INTO the
+      -- furniture, so it is cut out by the same authored mask, and for the
+      -- same reason: no class pin can separate it from the counter,
+      -- because the counter draws its own edging down columns 0 and 15 of
+      -- the very same tiles.
       --
       -- That edging is what defeated every automatic reading. Black
       -- always survives the shade flood, so the standee pools extruded
@@ -933,13 +1121,70 @@ return {
       -- to the top right, and the counter keeps its edging and its light
       -- work surface.
       --
+      -- But it is a MACHINE, not a person, so unlike the seated man it
+      -- carries a `depth` and stands as a solid rather than a sprite
+      -- card.  A flat card of a till is exactly the billboard the standee
+      -- pools exist to avoid -- it read as a decal on the counter, and
+      -- turned edge-on with the camera.
+      --
+      -- AND IT IS NOT A BOX.  The 16x16 packs two facings, and the black
+      -- linework says which is which: the keypad has its OWN complete
+      -- border (cols 2-8, rows 4-11) sealed inside the outer silhouette,
+      -- and what surrounds it is an L --
+      --
+      --     cols 2-----8 9--12
+      --     +-----------+-----+  row 4
+      --     |  keypad   | arm |    the L's upright arm: the printer and
+      --     +-----------+-----+  row 11    display the paper feeds out of
+      --     |   base / drawer  |  rows 12-15: the L's foot, drawn face-on
+      --     +------------------+
+      --
+      -- So the keypad is NOT a face.  It is the machine's DECK seen from
+      -- ABOVE -- the keys lie on it -- and `flat` lays it horizontal in
+      -- the notch of the L, at the height the base band leaves it and
+      -- with drawn row = depth row 1:1.  Extruding it instead stood that
+      -- surface on end and painted the keys up the front of a plain box:
+      -- the extruded-picture failure, and what the first pass shipped.
+      --
+      -- Everything else falls out of the drawing: the base band is 4 rows,
+      -- so the deck stands 4 above the counter; the arm is 8, so it rises
+      -- 8 above the deck; the paper reaches 4 more.
+      --
+      -- TWO AUTHORED NUMBERS.
+      --
+      -- `depth` 12, three quarters of the cell.  The keypad's own eight
+      -- drawn rows measure 8, and at 8 the machine read thin on the
+      -- counter, so the body runs half again deeper and the deck STRETCHES
+      -- over it -- 8 rows into 12 voxels, centre-sampled, so every second
+      -- row doubles and the keypad still covers the whole surface.  This
+      -- is the one place in the model where a texel is not 1:1 with a
+      -- drawn pixel; laying the panel 1:1 instead leaves a strip of the
+      -- base band's top showing behind the keys.  The model is anchored at
+      -- the counter cell's FRONT and grows north into the bare top behind
+      -- it, so deepening it can never push the till toward the aisle: at
+      -- 12 it still stops 4 voxels short of the cell's north edge.  This
+      -- is the number to move if it wants more or less presence.
+      --
+      -- `thin`: 4 rows at 2.  Row 4 is the machine's
+      -- own drawn top edge -- a black rule from column 3 to column 9 -- so
+      -- everything above it is the receipt strip, and paper is 2 voxels
+      -- like every other `cutout` here.  Centred in the body's band, so
+      -- the strip leaves the arm's top face by a slot in the middle of it
+      -- rather than flush with its front.  At the body's 8 the curl came
+      -- out as a chunky wedge on the corner and stopped reading as paper.
+      --
       -- `under` is 16/41 twice -- the plain work surface, which already
       -- carries the west edging (black over white) and the east (dark
       -- over black), so the counter closes up behind the machine with no
-      -- synthesis at all.
+      -- synthesis at all.  The `counter` pin on 14/15/30/31 stays: the
+      -- box under the till is still the counter, and the machine stands
+      -- on its 8px top plane.
       figures = {
         {
           w = 2,
+          depth = 12,
+          thin = { rows = 4, depth = 2 },
+          flat = { x = { 2, 8 }, rows = { 4, 11 } },
           tiles = { 14, 15,
                     30, 31 },
           under = { 16, 41,
@@ -1648,6 +1893,11 @@ return {
       -- footing.
       bookcase = { 7, 8, 9, 10, 23, 24, 25, 26, 39, 40, 41, 42,
                    55, 56, 57, 58 },
+      -- ...and the drums are the reason this tileset turns the shelf
+      -- relief OFF: the collapse here is borrowed for a MACHINE, whose
+      -- light regions are the barrel's own lit face and not panes
+      -- behind a frame.  Sinking them would dent the drum.
+      bookcase_relief = false,
       -- The big OCTAGONAL boardroom table -- the Fan Club's and Silph
       -- 11F's, the same drawing in both -- half a cell high, and half a
       -- cell on purpose.  A `counter` is ONE band: exactly the drawing's
@@ -1712,6 +1962,13 @@ return {
       -- drawn into the WALKABLE cell in front; 43/44 belong to the stool
       -- there, not to the desk.
       table = { 11, 12, 13, 14, 27, 28, 29, 30 },
+      -- and those eight ids are the ONLY `table` in this tileset, so the
+      -- height override is this desk's alone: 8px, the height its apron
+      -- is drawn and the plane the `bills_desk` template stands its top
+      -- on (see `buildings` below).  The pin is only the degradation
+      -- path here -- the template claims both placements -- but the two
+      -- must not disagree about where the desk's top is.
+      heights = { table = 8 },
       -- the seats: Bill's desk stool (43/44 over 59/60) and the Fan
       -- Club's four members' chairs (61/62 over 59/60), a whole cell
       -- each.  All of them sit in WALKABLE cells, so they were flat
@@ -1765,11 +2022,13 @@ return {
       -- rounded foot, 53, stands one row lower on the floor and is
       -- walkable, so it stays flat ground in front of the band).
       -- Drawn INTO the band and therefore wall as well: the two
-      -- bicycles hung on the Bike Shop's wall (1/2/3 over 17/18/19,
+      -- bicycles against the Bike Shop's wall (1/2/3 over 17/18/19,
       -- exactly two tile rows = exactly the band's height), the Cable
       -- Club's link poster (48/49/50/51 -- 50 is $32, the shore trap
       -- that had it recessing to -2), and the Trade Center's pair of
       -- lit notice boards (63/68/68/69 over 66/67/67/70).
+      -- The bicycles keep their `wall` pin as the degradation path, but
+      -- `mounted` below lifts them off the panel -- see there.
       wall = { 1, 2, 3, 6, 17, 18, 19, 48, 49, 50, 51, 52, 63, 66, 67,
                68, 69, 70 },
       -- Counters, half a cell high -- the house rule for anything you
@@ -1797,28 +2056,114 @@ return {
       -- character standing on one sits on it.
       stool = { 38, 39, 42, 43 },
       -- The showroom bicycles -- six of them standing on the Bike Shop
-      -- floor, in two columns, each drawn 3 tiles by 2 side-on.  The
-      -- THIN standee pool, not `bookcase`: a bike is nearly all air
-      -- (two wheel rings and a frame), and collapsing a rack of them
-      -- onto a one-cell-deep box at full height would trade six
-      -- bicycles for one blank 32px panel wearing a bicycle print.  As
-      -- cutouts they segment cleanly -- the drawing has floor margin on
-      -- three sides and its own black outline seals the wheels -- and
-      -- the standee builder's connected-component split gives each bike
-      -- its own feet in its own cell even where a lower bike's
-      -- handlebar stem touches the wheel of the one drawn above it.
+      -- floor, in two columns, each drawn 3 tiles by 2 side-on.  A THIN
+      -- standee pool, not `bookcase`: a bike is nearly all air (two
+      -- wheel rings and a frame), and collapsing a rack of them onto a
+      -- one-cell-deep box at full height would trade six bicycles for
+      -- one blank 32px panel wearing a bicycle print.  As cutouts they
+      -- segment cleanly -- the drawing has floor margin on three sides
+      -- and its own black outline seals the wheels -- and the standee
+      -- builder's connected-component split gives each bike its own
+      -- feet in its own cell even where a lower bike's handlebar stem
+      -- touches the wheel of the one drawn above it.
       -- 11/12/14 the saddle and bar row, 27/28/9 the frame and wheels.
-      prop = { 9, 11, 12, 14, 27, 28 },
-      -- The crate and the standing pump beside the shop's south wall
+      --
+      -- Their own pool at TWO voxels rather than `prop`'s five, which is
+      -- the whole reason `bike` exists.  The segmentation was never the
+      -- problem -- the mask the detector cuts is a clean bicycle, wheels,
+      -- forks, handlebars and all.  Depth was: every stroke of a line
+      -- drawing extruded five voxels closes the gap to its neighbour with
+      -- its own side faces, so off-axis (which is every camera rung but
+      -- flat) the air inside the frame filled in and the showroom's six
+      -- bicycles came out as one dark lump against the wall.  At two the
+      -- negative space survives and they read as bicycles again.
+      bike = { 9, 11, 12, 14, 27, 28 },
+      -- The toolbox and the standing pump beside the shop's south wall
       -- (29/13 over 21/22), twice.  A separate pool from the bicycles
-      -- on purpose, and the thicker one: a crate is a deliberate object
-      -- with a body, where a bike is a silhouette.
+      -- on purpose, and the thicker one: a toolbox is a deliberate
+      -- object with a body, where a bike is a silhouette.
+      --
+      -- Kept as the DEGRADATION PATH only.  The `bike_shop_toolbox`
+      -- building template (see `buildings.CLUB`) claims these cells and
+      -- models the drawing properly -- a per-pixel standee is the wrong
+      -- primitive for a box, and 10 voxels of it turned every black
+      -- outline pixel into a 10-deep bar.  Buildings runs before the
+      -- standee scan, so where the template stamps, this pin never
+      -- fires; without a profile it is still better than the volume
+      -- path.
       billboard = { 13, 21, 22, 29 },
       -- The Bike Shop floor's second checker phase.  It is NOT in the
       -- tileset's walkable list, so the cells it floors resolve by rule
       -- 4 and it rose as a wall stub -- or, worse, got swept into the
       -- bicycle beside it and dragged to 8px.  It is floor; it is flat.
       ground = { 30 },
+      -- THE TWO BICYCLES AGAINST THE SHOWROOM'S NORTH WALL.  Same object
+      -- as the six on the floor -- 24x16 of bicycle seen side-on, wheels
+      -- on the floor line -- but drawn INTO the wall band instead of onto
+      -- the floor, which is a different problem entirely:
+      --
+      --   a class pin resolves a whole 8x8 tile, so `wall` (what these
+      --   carried) paints the bicycle flat on the panel: a green wall
+      --   with a bicycle printed on it, which is what the shop looked
+      --   like;
+      --   and nothing automatic can cut it out either.  The panel behind
+      --   it is tile 6's stripe -- a #aaa field ruled with #555 every
+      --   four rows -- and #555 is a flood BOUNDARY, so a silhouette
+      --   flood comes back with three full-width wall stripes welded
+      --   across the bike (rows 3, 7 and 15 of the drawing).  Naming the
+      --   background shades instead (prop_bg) cannot work either: the
+      --   bicycle's own basket is drawn in the very #aaa and white the
+      --   wall field uses.
+      --
+      -- So the silhouette is authored -- but MEASURED, not drawn by hand.
+      -- 6 is the plain panel these three tile columns are painted over,
+      -- and it is perfectly regular, so compositing 6 across the same 3x2
+      -- grid and flooding the background in through the pixels that still
+      -- MATCH it separates bicycle from wall exactly: 247 pixels, one
+      -- connected component, no stripe left on it and no hole in it.
+      -- That is also why `under` is six 6s -- the wall wears the panel
+      -- the artist drew everywhere else along the same run.
+      --
+      -- `depth` 2 for the same reason the `bike` pool above is 2, and
+      -- because a family shares a silhouette: these are the showroom's
+      -- bicycles, leaning on the wall rather than standing in the room.
+      -- The slab juts 2 voxels SOUTH of the band, so they stand in front
+      -- of the wall and overhang the walkable cell a little -- which is
+      -- what a bicycle leaning on a wall does.  Collision is untouched.
+      --
+      -- Only BIKE_SHOP places this grid on the club atlas, at tile (1,0)
+      -- and (6,0).  The same six tile ids DO pair up in gate.png (both
+      -- Route gate upper floors and all four Safari rest houses), but
+      -- that is a different image and `mounted` is keyed per tileset, so
+      -- the pattern cannot reach them.
+      mounted = {
+        {
+          w = 3,
+          depth = 2,
+          tiles = {  1,  2,  3,
+                    17, 18, 19 },
+          under = {  6,  6,  6,
+                     6,  6,  6 },
+          pixels = {
+            ".........XXX............",
+            "..XXXXX.XXXX............",
+            "..XXXXXXXXXX............",
+            "..XXXXXXXXXX............",
+            "..XXXXXXXXXXXXX.........",
+            "..XXXXXXXXXXXXXX........",
+            "..XXXXXXXXXXXXXX........",
+            "..XXXXXXXXXXXXXX..XXX...",
+            ".XXXXXXXXXXXXXXXXXXXXXX.",
+            "XXXXXXXXX..XXXXXXXXXXXXX",
+            "XXXXXXXXX.XXXXXXXXXXXXXX",
+            "XXXXXXXXX.XXXXXXXXXXXXXX",
+            "XXXXXXXXX.XXXXXXXXXXXXXX",
+            "XXXXXXXXX..XXX..XXXXXXXX",
+            ".XXXXXXX....X...XXXXXXX.",
+            "...XXX.....XXX....XXX...",
+          },
+        },
+      },
       -- Left to the derived default on purpose:
       --   $0F $1F    the main floor checker -> walkable, ground
       --   $0A $1A    the dark slab under the Bike Shop's exit warp -> a
@@ -1920,11 +2265,34 @@ return {
       -- towered to 24.
       bookcase = { 43, 54 },
       -- The galley barrels -- three down the kitchen's east wall, one in
-      -- the captain's room, one in each house.  A per-pixel cutout in
-      -- the THIN pool, the treatment Lt. Surge's trash cans get: 72/73
-      -- the lid, 88/89 the banded body.  72 is $48, the shore-set trap
-      -- that had each barrel resolving to water at -2.
-      prop = { 72, 73, 88, 89 },
+      -- the captain's room, one in each house.  72/73 the mouth, 88/89
+      -- the banded body.  72 is $48, the shore-set trap that had each
+      -- barrel resolving to water at -2.
+      --
+      -- This is Lt. Surge's trash can REDRAWN PIXEL FOR PIXEL on the ship
+      -- atlas: lay the two masks side by side and the only difference is
+      -- that the gym's floor stripes cross the gym copy's background,
+      -- which the flood keeps and this one has no need of.  So it takes
+      -- the same treatment and the same numbers as GYM's can -- see there
+      -- for why the mouth and base ellipses are measured and the height,
+      -- well and taper are authored.
+      --
+      -- Scanned: six placements on the SHIP atlas and no others --
+      -- SS_ANNE_KITCHEN (13,5), (13,7) and (13,9) down the east wall,
+      -- SS_ANNE_CAPTAINS_ROOM (4,1), and one each in CERULEAN_BADGE_HOUSE
+      -- and FUCHSIA_GOOD_ROD_HOUSE at (7,7), both of which are ship
+      -- interiors reusing the tileset.  Twenty-one more hits are id
+      -- collisions on other atlases and no business of this entry; the one
+      -- worth naming is VERMILION_DOCK, because SHIP_PORT is a near-twin
+      -- of this image -- but its $48/$49/$58/$59 are a hull panel, not a
+      -- barrel (diffed pixel for pixel), so the pin is NOT copied there.
+      can = { 72, 73, 88, 89 },
+      can_cap = 9,
+      can_base = 4,
+      can_height = 9,
+      can_well = 5,
+      can_taper = 4,
+      heights = { can = 9 },
       -- The stairs, both flights, in every map that has them (1F, 2F,
       -- 3F, B1F and the captain's room).  Same two drawings the whole
       -- game uses, and the warp table says which is which: 41/42/57/58
@@ -2429,6 +2797,11 @@ return {
       -- PLATEAU, so the wall reads as set INTO the terrace instead of
       -- standing in front of a trench of synthesized ground.
       bookcase_backfill = "above",
+      -- and no shelf relief: what the collapse carries here is MASONRY,
+      -- whose courses are the wall itself rather than panes set behind
+      -- a frame.  Sinking them cut a stepped bevel into the League's
+      -- walls and the pilasters (shot before it was turned off).
+      bookcase_relief = false,
       -- ONE course, and it must stay one: $15/$16 + $05/$06 + $30/$31 is
       -- the pilaster -- cap, shaft, plaque base -- and $15/$16 over $30/$31
       -- IS the statue's plinth, the artist having drawn the same stone
@@ -3364,6 +3737,84 @@ return {
             z = 13 },                                       -- keyboard
         },
       },
+      -- F05: the healing machine behind the counter -- the pair in every
+      -- Center (cells (1,0):(2,1) and (6,0):(7,1)), 22 placements, plus
+      -- the Indigo Plateau lobby's pair under MART below (scan
+      -- "58,59;74,75;76,77;6,22").
+      --
+      -- The drawing is 4x4 tiles but only its middle COLUMN is this
+      -- template's.  The corners are the striped wall band (40), and
+      -- the outer tile columns are the flanking EQUIPMENT -- the hoses
+      -- (72 west, 73 east) and the control keyboard (7/13) -- which are
+      -- separate thin objects, not part of this console: they are
+      -- pinned into the THIN standee pool above and stay out of this
+      -- grid.  What no class can split is the middle -- a wall-height
+      -- cabinet with a monitor perched on its front top edge, drawn
+      -- across two map rows because it towers over the 16px band
+      -- behind it, which the volume path can only read as more wall.
+      --
+      -- Read off the pixels (local x, the whole drawing's x8..x23):
+      --   rows 15-31  the CABINET's front face, 16 wide, 17 tall:
+      --               black front-top edge with vent notches (15),
+      --               white top rail (16), black panel in a white
+      --               frame (17-28), white rail / light plinth / black
+      --               ground line (29-31).
+      --   rows 6-14   the cabinet's TOP FACE seen from above -- not a
+      --               second-story facade: a white expanse with a lit
+      --               west strip (x1 white) and a shaded east strip
+      --               (x14 #555) wrapping the monitor, full-width in
+      --               front of it (13-14), strips beside it (7-12).
+      --               Its 9 rows plus the front edge row 15 ARE the
+      --               cabinet's depth: 10.  `desk.top` lays the band
+      --               flat as the lid; where the monitor's drawing
+      --               occludes it, the lid continues the nearest strip
+      --               (the drawing's own pixels).
+      --   rows 1-13   the MONITOR, x2..x13: black back rim (1) and
+      --               white cap (2-3) seen from above -- 3 rows plus
+      --               the front edge, depth 4 -- then bezel, screen
+      --               and control strip (4-13) face-on, 10 tall,
+      --               standing on the cabinet top at its front: the
+      --               drawn base edge (13) is one band row up from the
+      --               front strip (14), so its front plane is one
+      --               voxel back from the cabinet's -- z 20..23.  The
+      --               screen interior (x5..x10, rows 6-8) is sealed
+      --               behind its own black frame and sinks one voxel
+      --               via `inset` -- the pane rule applied by hand,
+      --               because `panes = false` blocks the global pass.
+      --   rows 0-5    at the flanks: wall stripes and the machine's
+      --               cast shadow on the wall east of it -- BACKGROUND,
+      --               the same standing as the potted plants' floor.
+      --               The `wall` element keeps the band solid over the
+      --               back map row: a 16-tall, 16-deep block cycling
+      --               the drawing's own stripe unit (x0, rows 0-3),
+      --               what the neighbouring cells' wall pins render.
+      --
+      -- `depth = 4` is BOTH map rows: the back row is the wall's plot,
+      -- the front row the machine's own, and the cabinet leans against
+      -- the wall at z 16..25.  `panes = false`: the front panel seals
+      -- DARK behind LIGHT (a black panel in a white frame), the
+      -- opposite polarity to the pane rule, so the global recess pass
+      -- would sink the frame and leave the panel proud.
+      {
+        id = "center_heal_machine",
+        tiles = {
+          { 58, 59 },
+          { 74, 75 },
+          { 76, 77 },
+          {  6, 22 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil, depth = 4,
+        panes = false,
+        wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
+        desk = { fascia = { 15, 16 }, base = { 17, 31 }, z = 16,
+                 depthPx = 10, top = { 6, 14 } },
+        parts = {
+          { kind = "upright", x = { 2, 13 }, top = { 1, 3 },
+            facade = { 4, 13 }, z = 20, depth = 4,
+            inset = { x = { 5, 10 }, rows = { 6, 8 } } },    -- the monitor
+        },
+      },
     },
 
     MART = {
@@ -3388,6 +3839,31 @@ return {
             z = 13 },                                       -- keyboard
         },
       },
+      -- F05 again: the Indigo Plateau lobby has the Centers' healing
+      -- machine pair too (cells (5,4):(6,5) and (10,4):(11,5)), and its
+      -- MART tileset shares the POKECENTER atlas, so this is the same
+      -- drawing tile for tile. Same part table as the POKECENTER entry
+      -- above.
+      {
+        id = "center_heal_machine",
+        tiles = {
+          { 58, 59 },
+          { 74, 75 },
+          { 76, 77 },
+          {  6, 22 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil, depth = 4,
+        panes = false,
+        wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
+        desk = { fascia = { 15, 16 }, base = { 17, 31 }, z = 16,
+                 depthPx = 10, top = { 6, 14 } },
+        parts = {
+          { kind = "upright", x = { 2, 13 }, top = { 1, 3 },
+            facade = { 4, 13 }, z = 20, depth = 4,
+            inset = { x = { 5, 10 }, rows = { 6, 8 } } },    -- the monitor
+        },
+      },
     },
 
     GYM = {
@@ -3416,5 +3892,350 @@ return {
         },
       },
     },
+
+    CLUB = {
+      -- F06: the Bike Shop's OPEN TOOLBOX -- BIKE_SHOP cells (6,6) and
+      -- (7,7), and nowhere else in the game (2 placements).
+      --
+      -- The drawing looks down INTO the box: what fills its middle is not
+      -- a face but the inside of the tray, with a wrench lying in it, and
+      -- the lid standing open on the right.  That is why every solid
+      -- treatment fails it.  As a `billboard` (what these tiles carried)
+      -- the whole 16x16 went up as one 10-voxel per-pixel slab -- the
+      -- extruded picture in its pure form, a black monolith wearing the
+      -- drawing as a decal.  Reading the middle as a cabinet FRONT and
+      -- extruding that is the same mistake one level down: it puts a wall
+      -- where the opening is.  An open box has to be hollow, so `tray`
+      -- builds four walls, a floor slab and AIR between them.
+      --
+      -- The bands, and they measure out exactly (8 depth rows for a
+      -- one-tile-row plot):
+      --   row 4       the FAR rim, black across the back wall     -> z 0
+      --   rows 5-10   the tray's inside, seen from above, with the
+      --               wrench lying across it -- drawn row = depth row,
+      --               six rows for six rows                       -> z 1-6
+      --   row 11      the near wall's INNER face, in shadow
+      --   row 12      the near rim, black right across the box    -> z 7
+      --   rows 13-15  the near wall's outer face, seen face-on: a light
+      --               panel between its black rim and its black foot,
+      --               so the wall stands 4 voxels and the rim is its top
+      --
+      -- `x` is the box's outer span and `inner` the opening's, so the
+      -- one-column difference on each side IS the wall.  x12 is left out
+      -- of both on purpose: the #555 at x11-x12 on rows 13-15 is the 3/4
+      -- shear of the right wall, which un-projecting simply removes --
+      -- the wall is where the plan says it is, not where the projection
+      -- slides it to.  `floor = 0` puts the tray's bottom one voxel thick,
+      -- leaving three voxels of open box above it.
+      --
+      -- The LID is an upright part riding the rim, hinged on the right
+      -- and standing up: 12 tall as drawn, spanning the box's whole depth
+      -- (8) and 4 thick, which is the black outline plus the two columns
+      -- of white body the drawing gives it.  It overhangs east of the
+      -- hinge, which is what a hinged lid standing open does.
+      {
+        id = "bike_shop_toolbox",
+        tiles = {
+          { 29, 13 },
+          { 21, 22 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil, depthPx = 14,
+        tray = { top = { 4, 11 }, front = { 12, 15 }, x = { 2, 11 },
+                 inner = { 3, 9 }, floor = 0 },
+        parts = {
+          { kind = "upright", x = { 11, 14 }, top = { 0, 0 },
+            facade = { 0, 11 }, z = 0, depth = 14 },       -- the open lid
+        },
+      },
+    },
+
+    MANSION = {
+      -- F05: the TALL display cabinet, the one with the trophy behind
+      -- its glass -- CELADON_CHIEF_HOUSE cells 3,0 and 4,0 and
+      -- CELADON_MANSION_1F cell 2,2 (3 placements).  Its 32 rows read
+      -- like every band table's: 0-8 the cabinet top seen from above
+      -- (black rim, white highlight courses along the north and west,
+      -- grey field, the front corner shaded at row 8); row 9 the top's
+      -- own black front edge -- which is what the rim treatment paints,
+      -- so slab = 1 and that row folds into the roof band instead of
+      -- extruding under it; 10-31 the front face: the trophy in its
+      -- dark display opening, the nameplate under it, then the two
+      -- panelled doors of the base.  The contents come off the pixels,
+      -- not the band table: the trophy, the plate and both door panels
+      -- are non-black regions the drawing seals behind its own black
+      -- frame, so the measured recess pass sinks each of them a voxel
+      -- and the frames stay proud.  Nine drawn top rows over a 32px
+      -- plot, so the rims map 1:1 -- the drawn front-corner shading
+      -- lands one voxel behind the front edge -- and the uniform field
+      -- cycles between.  Both cell rows of the plot are BLOCKED and
+      -- both are cabinet drawing, so D is the whole grid: the rank is
+      -- built into the room's north wall, and its front stands flush
+      -- with the short case's beside it.  23 voxels tall.
+      {
+        id = "mansion_trophy_case",
+        tiles = {
+          { 38, 41 },
+          { 40, 21 },
+          { 56, 87 },
+          { 50, 51 },
+        },
+        roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 },
+        slab = 1, frontEave = 0, ledge = nil,
+      },
+      -- F06: the SHORT book case standing beside it -- the same
+      -- cabinet on a grid one tile row shorter (CELADON_CHIEF_HOUSE
+      -- cells 2,0 and 5,0, CELADON_MANSION_1F tiles 2,5 and 6,5; 4
+      -- placements).  Identical top band, identical panelled base;
+      -- only the display opening is shorter, a shelf of books where
+      -- the tall one has the trophy.  Same band numbers as F05 --
+      -- which is what makes the pair read as one line of furniture --
+      -- and 15 voxels tall against the tall one's 23, exactly the 8
+      -- rows of drawing between them.  The grid must carry the 38/41
+      -- cap row: {34,35} over {50,51} alone also stands on
+      -- CELADON_MANSION_2F, where it is the bottom of a taller
+      -- two-shelf bank and not this drawing at all.  The 8px of wall
+      -- behind the cap is not part of the cabinet and keeps its own
+      -- `wall` pin.
+      {
+        id = "mansion_bookcase",
+        tiles = {
+          { 38, 41 },
+          { 34, 35 },
+          { 50, 51 },
+        },
+        roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 },
+        slab = 1, frontEave = 0, ledge = nil,
+      },
+      -- F07: the long table in the middle of the chief's house
+      -- (CELADON_CHIEF_HOUSE cell 2,3), the lab table's read at four
+      -- cells wide and two deep.  Rows 0-23 are the tabletop seen from
+      -- above; 24-26 are the top slab's own front edge,
+      -- black/#555/black, and row 27 the #555 shadow that closes it --
+      -- exactly what the rim treatment paints, so slab = 3 and rows
+      -- 24-27 fold into the roof band.  That leaves 28-30 as the base,
+      -- the same three rows the lab table's is, and the two tables
+      -- stand the same 6 voxels: row 28 the apron running the whole
+      -- width, 29-30 the end legs under it.  The legs stop one row
+      -- short of the grid, so the measured ground line lands there and
+      -- the model does not float.  Both cell rows of the plot are
+      -- blocked, so D is the whole grid: the 24 drawn top rows map 1:1
+      -- from the north rim and the field's own rows cycle for the last
+      -- 8.  The `table` pin these tiles keep stays 12px -- it is
+      -- shared with the 2F/3F writing desks and the rooftop shed, and
+      -- nothing rides this table -- so it is only the degradation
+      -- path, neutralized wherever this template stamps.
+      {
+        id = "mansion_long_table",
+        tiles = {
+          { 38, 39, 39, 39, 39, 39, 39, 41 },
+          { 54, 55, 55, 55, 55, 55, 55, 57 },
+          { 54, 55, 55, 55, 55, 55, 55, 57 },
+          { 60, 58, 58, 58, 58, 58, 58, 59 },
+        },
+        roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 },
+        slab = 3, frontEave = 0, ledge = nil,
+      },
+    },
+
+    HOUSE = {
+      -- F08: the dining table of the generic town house -- 18
+      -- placements, every home's cells (3,3):(4,4), Blue's and the
+      -- Daycare among them -- the chief's long table (F07 above) at two
+      -- cells wide, the same read to the row.  Rows 0-23 are the rounded
+      -- tabletop seen from above (black rim, white highlight course,
+      -- grey field, #555 east rim); 24-26 the slab's own front edge,
+      -- black/#555/black, and 27 the #555 apron shadow that closes it,
+      -- folded into the band; 28-30 the base -- the apron's black
+      -- underrun and the corner legs -- stopping one row short of the
+      -- grid, so the measured ground line keeps the model on its plot.
+      -- The same 6 voxels the whole table family stands.  The `table`
+      -- pins stay as the degradation path, neutralized where this
+      -- stamps; the schoolhouse's half-width table with its book and the
+      -- trashed house's ransacked corner are different grids and keep
+      -- theirs.
+      {
+        id = "house_table",
+        tiles = {
+          { 38, 39, 39, 41 },
+          { 54, 47, 47, 57 },
+          { 54, 47, 47, 57 },
+          { 60, 58, 58, 59 },
+        },
+        roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 },
+        slab = 3, frontEave = 0, ledge = nil,
+      },
+      -- F09: the stool at every one of those tables (94 placements on
+      -- this tileset) -- the first template with NO base piece.  The
+      -- drawing is one object, a round seat on legs, drawn MID-CELL over
+      -- its own floor (rows 0-4 are the room behind it), and no band
+      -- split fits that: a roof band starts at the drawing's top row.
+      -- So it is a desk-set of exactly one upright part anchored to the
+      -- floor: rows 5-10 the seat seen from above (its lid), row 11 the
+      -- seat's own front edge, 12-15 the legs with the floor showing
+      -- between them, which the per-pixel facade cut keeps open.  The
+      -- drawing measures 7 deep (six seat rows plus the front edge at
+      -- drawn row = depth row); the shipped stool is grown two voxels
+      -- past that north AND south (z 3..13, developer-tuned against the
+      -- in-game read), with `stretch` mapping the seat band over the
+      -- deeper lid instead of smearing its last row.  `panes = false`:
+      -- a stool has no windows, and the pane rule would sink the legs'
+      -- lit faces behind their own outlines.  The 5-voxel stand is the
+      -- drawn elevation, and the tileset's `heights` above pins the
+      -- `stool` ride height to it, so whoever sits here sits ON the
+      -- seat; the old stool standee pins stay as the degradation path.
+      {
+        id = "house_stool",
+        tiles = {
+          { 2, 3 },
+          { 18, 19 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil,
+        panes = false,
+        parts = {
+          { kind = "upright", x = { 2, 13 }, top = { 5, 10 },
+            facade = { 11, 15 }, z = 3, depth = 11,
+            stretch = true },                             -- the stool
+        },
+      },
+    },
+
+    REDS_HOUSE_1 = {
+      -- F08 again: Red's and the Copycat's ground-floor dining table
+      -- (cells (3,4):(4,5) of both maps) -- the house table on the
+      -- reds_house atlas, with two differences the fields absorb.
+      --
+      -- Its tabletop field stops at row 22 and row 23 is the top's own
+      -- #555 south rim, which roofBack = 24 would lay MID-TABLE as a
+      -- dark stripe: back stops at 23 and the field cycles from there
+      -- (the drawn rim's place at the south edge is under the rim
+      -- treatment's own black, like every sibling's).
+      --
+      -- And the POTTED PLANT is drawn standing on the tabletop (rows
+      -- 5-15, x10..x21, tiles 40/55/56 with the cutout pool's 54/57
+      -- beside them).  It stays the cutout standee it has always been:
+      -- `keep` leaves its tiles unclaimed so the pin and its standee
+      -- survive, `scrub` replaces its pixels with the field shade so the
+      -- model tops out as the plain surface it sits on, and `support`
+      -- states the model's 6-voxel top plane so the standee stands ON
+      -- the modelled tabletop (Structures reads it off the claim; a
+      -- plain claim's h = 0 supports nothing).
+      {
+        id = "reds_house_table",
+        tiles = {
+          { 38, 39, 40, 41 },
+          { 54, 55, 56, 57 },
+          { 44, 42, 42, 43 },
+          { 60, 58, 58, 59 },
+        },
+        roofRows = 28, roofBack = 23, roofFront = 1, roofCycle = { 2, 22 },
+        slab = 3, frontEave = 0, ledge = nil,
+        scrub = { { 10, 5, 21, 15 } },
+        keep = { 40, 54, 55, 56, 57 },
+        support = 6,
+      },
+      -- F09 again: the stools around it (10 placements across Red's and
+      -- the Copycat's ground floors), pixel for pixel the house
+      -- tileset's drawing.  Same part table; see HOUSE above.
+      {
+        id = "house_stool",
+        tiles = {
+          { 2, 3 },
+          { 18, 19 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil,
+        panes = false,
+        parts = {
+          { kind = "upright", x = { 2, 13 }, top = { 5, 10 },
+            facade = { 11, 15 }, z = 3, depth = 11,
+            stretch = true },                             -- the stool
+        },
+      },
+    },
+
+    INTERIOR = {
+      -- F0x: Bill's desk, and the Silph president's -- one drawing, two
+      -- placements (BILLS_HOUSE cell 1,4 and SILPH_CO_11F cell 10,12,
+      -- both found by the scan and nothing else).  The whole 4x4-tile
+      -- block is one designed unit, but this template deliberately takes
+      -- only the desk's own two cells.
+      --
+      -- What the 16 drawn rows are: the TABLETOP seen from above, the
+      -- lab table's pattern tile for tile -- black rim, a white
+      -- highlight course inside it, grey field -- over a 16px plot, so
+      -- they map 1:1 and nothing cycles.  Standing on it, and drawn INTO
+      -- that top view the way lab_computers' objects are: a terminal
+      -- (screen head over a keypad panel, rows 3-13) and the ball
+      -- (rows 1-13).  Drawn row = depth row on a top view, so each part's
+      -- `z` is where the drawing puts it: the head's front row is 8, the
+      -- keypad picks up at 9, and the ball's front row is its drawn
+      -- bottom.  Only the two DEPTHS and how much of each blob is lid
+      -- rather than face are authored -- the projection cannot state
+      -- either.
+      --
+      -- Why the grid stops at two rows.  This drawing's front face is its
+      -- APRON, and the artist drew the apron into the WALKABLE cell in
+      -- front (43/44 + 91/92) -- exactly where the lab table's fascia and
+      -- base are.  But 43/44 is also the upper half of the CHAIR pushed
+      -- up to the desk (43/44 over 59/60, pinned `stool` in the tileset
+      -- section above), and a template claims whole TILES: reading the
+      -- apron would take the chair's back with it and leave a backless
+      -- stub in both rooms.  So the apron stays with the chair, `plane`
+      -- authors the desk's height instead, and the body under the lid is
+      -- synthesized in the drawing's own shades (the band table's rim
+      -- treatment -- a shaded box closed by the outline at the floor).
+      -- 8px is not a taste number: it is the apron's own drawn row count
+      -- and the `table` height these tiles are already pinned to, so the
+      -- desk stands where the degradation path stood it.
+      {
+        id = "bills_desk",
+        tiles = {
+          { 11, 12, 13, 14 },
+          { 27, 28, 29, 30 },
+          { 43, 44, 91, 92 },
+          { 59, 60, 31, 31 },
+        },
+        roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+        slab = 0, frontEave = 0, ledge = nil, depth = 4,
+        -- the desk's own plot is its two cells; the grid runs on because
+        -- its apron and the CHAIR share tiles 43/44
+        desk = { fascia = { 16, 18 }, base = { 19, 23 }, depth = 2 },
+        parts = {
+          -- the notes: two sheets lying on the desk, so PAPER -- one
+          -- voxel proud at drawn row = depth row, and nothing raised
+          { kind = "flat", x = { 2, 15 }, rows = { 3, 7 } },
+          -- the keyboard: a real slab, not a print on the desk.  Its
+          -- keys are drawn from ABOVE so they ride the top face across
+          -- its depth, and its drawn bottom frame row stands as the
+          -- front edge
+          { kind = "upright", x = { 4, 15 }, top = { 8, 12 },
+            facade = { 12, 13 }, z = 8, depth = 6 },
+          -- the cord: the kinked dark run the drawing puts between the
+          -- keyboard's top-right corner and the computer's left corner.
+          -- Raised to the keyboard's own height so it reads as a cable
+          -- spanning them rather than a scuff on the desk
+          { kind = "upright", x = { 16, 19 }, top = { 8, 8 },
+            facade = { 8, 9 }, z = 8, depth = 2 },
+          -- the computer: drawn in 2:1 ISOMETRIC, turned 45 degrees to
+          -- the map -- see the `iso` branch in buildParts.  `plan` = rx
+          -- makes its footprint a SQUARE turned 45, so from directly
+          -- above it is the cube the drawing depicts and not the slab
+          -- the 2:1 would otherwise build
+          { kind = "iso", x = { 19, 30 }, rows = { 1, 13 },
+            plan = 6, z = 9 },
+          -- the chair pushed up to the desk, drawn into the walkable
+          -- cell in front.  It stands on the FLOOR, not on the desk, so
+          -- its rise is the whole plane back down; two voxels deep
+          -- because it is a chair, and `inside` cuts it per pixel, so
+          -- this is the thin slab the `stool` standee was -- the desk
+          -- claiming tiles 43/44 for its apron is what makes the
+          -- template owe it
+          { kind = "upright", x = { 2, 13 }, top = { 20, 21 },
+            facade = { 22, 31 }, rise = -8, z = 22, depth = 10 },
+        },
+      },
+    },
   },
 }
diff --git a/lib/Buildings.lua b/lib/Buildings.lua
index b07c913..04a55df 100644
--- a/lib/Buildings.lua
+++ b/lib/Buildings.lua
@@ -68,6 +68,47 @@ local RECESS_MAX = 24
 local SHADE = { top = 0.95, south = 1.0, north = 0.68,
                 side = 0.78, bottom = 0.5 }
 
+-- ------- how far a merged run may reach: the tile lattice
+--
+-- Merging is what keeps a 90k-voxel house down to ~2k quads, and under a
+-- straight projection a run may be as long as it likes -- a straight line
+-- is a straight line however finely it is cut. THE WORLD CURVE IS NOT
+-- STRAIGHT. It drops every vertex by the square of its distance from the
+-- focus (see WorldCurve), so a quad's interior is the CHORD of a parabola
+-- its neighbours draw the arc of: a run of length L hangs k*L^2/4 below
+-- the short quads butted against it, and the join tears open.
+--
+-- Nothing bounded a run's length before, and the runs that ran away were
+-- the ones wearing a CONSTANT texel -- the roof's black eave outline, its
+-- fascia, the shaded underside -- because a flat run has no art to break
+-- it. Those reached 102px across a gym, which at V-CURVE 3 hangs some
+-- three world pixels under the roof surface beside it: the eave tore off
+-- the roof and the drop showed the building's dark interior through the
+-- slot. (Strip runs, the drawing marching along the atlas, break at the
+-- tileset's own boundaries and were never the problem.)
+--
+-- So a run stops at the next 8px lattice line. Buildings are stamped at
+-- tx*8 (see stamp), so the model's lattice IS the map's: every quad in the
+-- scene -- terrain, props, this -- now ends on the same lines, every join
+-- is vertex-for-vertex, and the bend carries them together. What is left
+-- is the sag WITHIN one cell, k*64/4, which is under a twentieth of a
+-- world pixel at any rung.
+--
+-- It costs quads on a dense city map (Cerulean's object stream goes from
+-- 35.7k to 41.6k, and its longest edge from 102px to 8px) and it costs them
+-- whether the curve is on or not, which is the deliberate trade: the mesh
+-- is cached per map and built asynchronously over seconds, so meshing for
+-- the curve's sake only when the curve is on would mean rebuilding every
+-- live map on a keypress.
+local CELL = 8
+
+-- How far a run starting at `a` may go before it crosses the next lattice
+-- line. Floor-mod, so the awning's negative z lands on the same lines the
+-- positive side does.
+local function runCap(a)
+  return CELL - a % CELL
+end
+
 local function keyOf(tx, ty)
   return (ty + 64) * 4096 + (tx + 64)
 end
@@ -170,6 +211,39 @@ local function read(t, data, perRow)
 
   local inside = {}
   for i = 0, W * H - 1 do inside[i] = not outside[i] end
+
+  -- `scrub` names pixel rects where the drawing paints an object standing
+  -- ON the surface (Red's potted plant on the dining tabletop). The object
+  -- keeps its own standee -- the template's `keep` leaves its tiles
+  -- unclaimed -- so the band beneath it is the one surface the drawing
+  -- implies but never paints clear: every rect pixel takes the field
+  -- shade, sourced from the first field texel outside the rects, and the
+  -- model's top comes out as the plain surface the object sat on.
+  if t.scrub then
+    local function inRect(x, y)
+      for _, r in ipairs(t.scrub) do
+        if x >= r[1] and x <= r[3] and y >= r[2] and y <= r[4] then
+          return true
+        end
+      end
+      return false
+    end
+    local donor = nil
+    for i = 0, W * H - 1 do
+      if col[i] == GREY and inside[i]
+         and not inRect(i % W, math.floor(i / W)) then
+        donor = i
+        break
+      end
+    end
+    for i = 0, W * H - 1 do
+      if inRect(i % W, math.floor(i / W)) then
+        col[i] = GREY
+        ax[i], ay[i] = ax[donor], ay[donor]
+        inside[i] = true
+      end
+    end
+  end
   return { W = W, H = H, col = col, ax = ax, ay = ay, inside = inside }
 end
 
@@ -276,6 +350,14 @@ local function measure(sp, t)
     end
   end
 
+  -- The pane rule reads a LIGHT region the drawing seals behind a BLACK
+  -- frame. A drawing built the other way round -- the healing machine's
+  -- dark screens sealed behind their own white bezels -- inverts under
+  -- it: every lit edge sinks and the black panes stand proud, a black
+  -- lattice a voxel off the face. `panes = false` says the drawing does
+  -- not carry the rule's polarity, so the facade stays flush.
+  if t.panes == false then recess = {} end
+
   -- One representative texel per shade, taken from the building's own art:
   -- the roof's fascia and its undersides are geometry the drawing implies
   -- but never paints, and they must still wear its palette (and pick up
@@ -300,7 +382,12 @@ local function measure(sp, t)
   -- onto ground the drawing merely stands its legs on: the lab table's
   -- third row is the walkable cell the player faces it from, and the
   -- full-grid depth would stand the model in their path.
-  return { top = top, ytop = ytop, D = (t.depth or #t.tiles) * 8,
+  -- `depth` names the plot in TILE ROWS, which is the right grain for a
+  -- building. `depthPx` names it in voxels, for an object whose real
+  -- depth is not a whole tile row -- the Bike Shop toolbox is a box
+  -- standing in the middle of its own cell, not a thing that fills a plot.
+  return { top = top, ytop = ytop,
+           D = t.depthPx or ((t.depth or #t.tiles) * 8),
            ground = ground,
            recess = recess, interior = interior, shadeTexel = shadeTexel }
 end
@@ -343,19 +430,308 @@ local function deskSetModel(sp, pr, t)
     return sx
   end
 
+  -- The parts list, shared by every base piece: a desk plane or an
+  -- open tray rim alike, `plane` is simply the height they ride.
+  local ytop = 0
+  local function buildParts(plane)
+    for _, p in ipairs(t.parts) do
+      Budget.tick()
+      local x0, x1 = p.x[1], p.x[2]
+      if p.kind == "flat" then
+        -- drawn row = depth row by default; `z` renames the origin when
+        -- the flat sits below the desk's own drawn top span (the Center
+        -- PC's keyboard)
+        local r0 = p.rows[1]
+        local z0 = p.z or r0
+        for sy = r0, p.rows[2] do
+          local z = z0 + (sy - r0)
+          if z >= 0 and z < D then
+            for sx = x0, x1 do
+              if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
+            end
+          end
+        end
+      elseif p.kind == "iso" then
+        -- An ISO part is drawn in 2:1 isometric -- a box TURNED 45
+        -- degrees to the map, so one rhombus carries its top, its front
+        -- and its side at once and no band or facade split can reach
+        -- them. Un-projecting it is that projection run backwards: the
+        -- box stands as a real diamond in plan and every voxel wears the
+        -- texel the drawing paints where that voxel projects TO. The
+        -- drawn top lands on the top, the screen on the screen-facing
+        -- side and the flank on the flank, and nothing is segmented by
+        -- hand -- which is the only way to get this right, because the
+        -- three faces meet on a diagonal no rectangle can name.
+        --
+        -- Everything but the depth centre falls out of the drawn rect,
+        -- because the projection fixes it: the half-width is the drawn
+        -- rhombus's x radius, HALF that again its z radius (2:1 is what
+        -- makes it isometric), the near corner's drawn row is the base
+        -- rhombus's front tip, and whatever drawn height is left once
+        -- that rhombus is accounted for is the box's own height. Bill's
+        -- computer: rx 6, rz 3, base centre row 10, and 6 voxels tall --
+        -- which puts its left corner's vertical edge at drawn rows
+        -- 4..10, exactly where the drawing paints one.
+        --
+        -- `plan` is the one thing the drawing CANNOT state: 2:1 is the
+        -- projection, not the object, so reading rz as the plan radius
+        -- too builds a box half as deep as it is wide -- a slab, not the
+        -- cube the drawing depicts. `plan` names the real z radius and
+        -- the drawn row is scaled into it, so a cube is `plan = rx` and
+        -- the drawing still lands on it pixel for pixel.
+        local pr0, pr1 = p.rows[1], p.rows[2]
+        local rx = math.floor((x1 - x0 + 1) / 2)
+        local rz = math.floor(rx / 2)
+        local plan = p.plan or rz
+        local oy = pr1 - rz
+        local h = oy - rz - pr0
+        local ytp = plane + h
+        if ytp > ytop then ytop = ytp end
+        for sx = x0, x1 do
+          -- doubled, so a rect of even width keeps its centre between
+          -- two columns instead of limping one to the left
+          local dx2 = 2 * sx - (x0 + x1)
+          for dz = -plan, plan do
+            local z = p.z + dz
+            local d2 = math.abs(dx2) * plan + 2 * math.abs(dz) * rx
+            if z >= 0 and z < D and d2 <= (2 * rx + 1) * plan then
+              -- the plan row scaled back into the drawn rhombus
+              local dzs = math.floor((2 * dz * rz + plan) / (2 * plan))
+              for y = 0, h do
+                local sy = oy + dzs - y
+                local i = sy * W + sx
+                if sy >= pr0 and sy <= pr1 and inside[i] then
+                  put(sx, plane + y, z, i)
+                end
+              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
+        -- `rise` lifts a part off the desk's top plane and `z` names its
+        -- back-most depth row (the field a flat part already carries). An
+        -- object STANDING on a desk needs neither: it starts on the plane
+        -- at the plot's back. The healing machine's console needs both --
+        -- it stands in the FRONT map row of a grid whose back row is the
+        -- wall band it leans against, and its screen head is MOUNTED on
+        -- the console's front two voxels above the body's top. Both come
+        -- off the drawing, not off taste.
+        local base = plane + (p.rise or 0)
+        local pz = p.z or 0
+        local ytp = base + (fr1 - fr0)
+        if ytp > ytop then ytop = ytp end
+        -- `inset` sinks an authored pane one voxel: the pane rule
+        -- applied by hand, for a part whose screen IS sealed behind its
+        -- own black frame while the template's `panes = false` (set for
+        -- the polarity-inverted panel elsewhere in the same drawing)
+        -- blocks the global pass. Same mechanism as a recess: the front
+        -- voxel is simply not placed.
+        local ins = p.inset
+        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.  `stretch`
+          -- maps the drawn band over the whole depth instead, the tray's
+          -- rule: for a part authored DEEPER than its drawing (the house
+          -- stool grown past its drawn seat), clamping would print the
+          -- last row as a long smear off the back band's edge.
+          for z = pz, pz + pd - 1 do
+            local front = z == pz + pd - 1
+            local sy
+            if front then
+              sy = fr0
+            elseif p.stretch then
+              sy = math.min(tr0 + math.floor((z - pz) * (tr1 - tr0 + 1)
+                                             / (pd - 1)), tr1)
+            else
+              sy = math.min(tr0 + z - pz, tr1)
+            end
+            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 and z >= 0 and z < D then
+              put(sx, ytp, z, (front and fr0 or sy) * W + sx)
+            end
+          end
+          -- the body: facade rows anchored to the part's own base
+          for sy = fr0 + 1, fr1 do
+            local y = base + (fr1 - sy)
+            local i = sy * W + sx
+            if inside[i] then
+              local ix = interiorAt(sx, sy, x0, x1)
+              for z = pz, pz + pd - 1 do
+                if z >= 0 and z < D then
+                  if z == pz + pd - 1 then
+                    local sunk = ins and sx >= ins.x[1] and sx <= ins.x[2]
+                                 and sy >= ins.rows[1] and sy <= ins.rows[2]
+                    if not sunk and not pr.recess[i] then put(sx, y, z, i) end
+                  elseif z == pz then
+                    put(sx, y, z, i)
+                  else
+                    put(sx, y, z, sy * W + ix)
+                  end
+                end
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+
+  -- A TRAY is an open container -- the drawing looks down INTO it, so its
+  -- top-view band is not a lid but the inside of the box, and the model
+  -- has to be hollow. Bands, all measured 1:1 like any other band table:
+  -- `top` is the opening (drawn row -> depth row), `front` the near wall
+  -- seen face-on (drawn row -> elevation), `x` the box's outer span and
+  -- `inner` the opening's, so the difference between them is the wall.
+  -- Four walls stand to the rim, the floor slab lies `floor` voxels thick
+  -- under the opening, and the cavity between them is left as AIR -- which
+  -- is the whole point, and what an extruded facade can never be. Parts (a
+  -- standing lid) then ride the rim like any object on a desk's plane.
+  if t.tray then
+    local tr = t.tray
+    local top0 = tr.top[1]
+    local fr0, fr1 = tr.front[1], tr.front[2]
+    local bx0, bx1 = tr.x[1], tr.x[2]
+    local ix0, ix1 = tr.inner[1], tr.inner[2]
+    local floor = tr.floor or 0
+    local plane = fr1 - fr0 + 1            -- the rim: the wall's height
+    -- Which drawn row lies at depth z. The far rim is the band's first
+    -- row and the near rim the front wall's own, and the drawn inside
+    -- STRETCHES over whatever depth is between them: a box deeper than
+    -- its drawing has rows to spare is the ordinary case once the plot
+    -- stops being the grid, and the alternative -- running out of rows
+    -- and repeating the last one -- would print the wrench twice.
+    local lo, hi = top0 + 1, tr.top[2] - 1     -- the drawn inside
+    local span = math.max(1, D - 3)            -- interior depth rows - 1
+    local function trayRow(z)
+      if z == 0 then return top0 end
+      if z == D - 1 then return fr0 end
+      return lo + math.floor((z - 1) * (hi - lo) / span)
+    end
+    for sx = bx0, bx1 do
+      Budget.tick()
+      for z = 0, D - 1 do
+        local hollow = sx >= ix0 and sx <= ix1 and z > 0 and z < D - 1
+        for y = 0, (hollow and floor or plane - 1) do
+          if hollow or y == plane - 1 then
+            -- the opening seen from above: the tray's own floor and
+            -- whatever lies in it -- and the rim is the same band where
+            -- the wall meets it
+            local i = trayRow(z) * W + sx
+            if inside[i] then put(sx, y, z, i) end
+          else
+            -- the wall below the rim: the front band folded up it, the
+            -- drawn face on the front and back layers and the de-outlined
+            -- interior between, exactly as a facade extrudes.
+            --
+            -- NO recess pass here, and it must stay that way: a pane sinks
+            -- by DELETING its front voxel so the one behind becomes the
+            -- pane, and a container's wall is one voxel thick -- there is
+            -- nothing behind it, so the front panel simply opened a hole
+            -- straight into the box and you could see the wrench through it.
+            local sy = fr1 - y
+            local i = sy * W + sx
+            if inside[i] then
+              local px = (z == 0 or z == D - 1) and sx
+                         or interiorAt(sx, sy, bx0, bx1)
+              put(sx, y, z, sy * W + px)
+            end
+          end
+        end
+      end
+    end
+    if plane > ytop then ytop = plane end
+    buildParts(plane)
+    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
+
+  -- No base piece at all: the drawing IS its parts (the house stool -- a
+  -- seat and its legs, nothing under them but floor). The plane the parts
+  -- anchor to is the ground itself.
+  if not t.desk then
+    buildParts(0)
+    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 desk's top plane. Usually the drawing states it: the fascia and
+  -- base rows it paints below the objects ARE the front face, and their
+  -- row count is the height. Bill's desk paints neither inside its grid
+  -- -- its apron is drawn into the WALKABLE cell in front, and that cell
+  -- is left out on purpose so the chair standing there keeps its own
+  -- tiles -- so `plane` names the height directly and the body below the
+  -- lid is synthesized: the band table's own rim treatment, a shaded box
+  -- closed by the outline where it meets the floor, in the drawing's
+  -- shades via shadeTexel.
   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 desk's own PLOT, when the grid holds more than the desk. Bill's
+  -- grid runs on into the walkable cell, because the drawing puts the
+  -- desk's apron AND the chair pushed up to it in the same tiles -- so
+  -- the desk box has to stop at its own cell (`depth`) and stand on its
+  -- own ground line rather than the grid's, which the chair's feet set
+  -- eight rows lower. The base band's last row IS that ground line by
+  -- definition, and for every desk drawn inside its own grid it is the
+  -- measured one to the row (lab table, lab computers, Center PC, the
+  -- Bike Shop toolbox), so this changes nothing for them.
+  -- ...and in voxels (`depthPx`) plus a back origin (`z`) when the desk
+  -- is shallower than a tile row and leans against something: the
+  -- healing machine's cabinet is 10 deep -- its drawn top band's 9 rows
+  -- plus the front edge -- standing against the wall band, so its box
+  -- runs z 16..25 of a 32-deep plot.
+  local deskD = t.desk.depthPx or (t.desk.depth and t.desk.depth * 8) or D
+  local dz0 = t.desk.z or 0
+  local dz1 = dz0 + deskD - 1
+  local deskG = b1 + 1
+
+  -- The WALL element: the band the machine backs onto, whose tiles this
+  -- grid claims. The drawing shows it only as the stripe background
+  -- around the tower (the same standing as the potted plants' floor),
+  -- so the block cycles the drawing's own stripe unit -- real pixels of
+  -- column `x`, rows `cycle` -- at wall-band height over the back plot,
+  -- exactly what the neighbouring cells' `wall` pins render.
+  if t.wall then
+    local wl = t.wall
+    local c0, c1 = wl.cycle[1], wl.cycle[2]
+    local cn = c1 - c0 + 1
+    local wx = wl.x or 0
+    for y = 0, wl.h - 1 do
+      Budget.tick()
+      local sy = c0 + (wl.h - 1 - y) % cn
+      for sx = 0, W - 1 do
+        for z = 0, wl.depthPx - 1 do
+          put(sx, y, z, sy * W + wx)
+        end
+      end
+    end
+  end
+
   -- the base band, extruded exactly like every lab table's
   for sy = b0, b1 do
     Budget.tick()
-    local y = ground - 1 - sy
+    local y = deskG - 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
+        for z = dz0, dz1 do
+          local px = (z == dz0 or z == dz1) and sx or ix
           put(sx, y, z, sy * W + px)
         end
       end
@@ -364,91 +740,72 @@ local function deskSetModel(sp, pr, t)
   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
+      vox[key(i % W, deskG - 1 - sy, dz1)] = 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
+  -- the slab: fascia rows wrap every side
   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
-  local field = t.desk.lid == "white" and WHITE or GREY
-  for sx = 0, W - 1 do
-    for z = 0, D - 1 do
-      local shade = field
-      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])
+      for z = dz0, dz1 do put(sx, y, z, sy * W + sx) end
     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
-      -- drawn row = depth row by default; `z` renames the origin when
-      -- the flat sits below the desk's own drawn top span (the Center
-      -- PC's keyboard)
-      local r0 = p.rows[1]
-      local z0 = p.z or r0
-      for sy = r0, p.rows[2] do
-        local z = z0 + (sy - r0)
-        if z >= 0 and z < D then
-          for sx = x0, x1 do
-            if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
+  if t.desk.top then
+    -- The lid wears the desk's own drawn top band -- the drawing DOES
+    -- paint this tabletop (the healing machine's white top face with
+    -- its lit west and shaded east strips), so nothing is synthesized
+    -- where it is visible: band rows map back-to-front, the first
+    -- fascia row is the drawn front-top edge, same rule as an upright
+    -- part's lid. Where a part's drawing occludes the band (the monitor
+    -- standing on it), the lid continues the nearest strip BESIDE the
+    -- part -- still the drawing's own pixels, the same sibling-pattern
+    -- rule every synthesized lid follows.
+    local tr0, tr1 = t.desk.top[1], t.desk.top[2]
+    for z = dz0, dz1 do
+      Budget.tick()
+      local sy = z == dz1 and f0 or math.min(tr0 + (z - dz0), tr1)
+      for sx = 0, W - 1 do
+        local px = sx
+        for _, p in ipairs(t.parts) do
+          local px0, px1 = p.x[1], p.x[2]
+          local r0, r1
+          if p.kind == "flat" or p.kind == "iso" then
+            r0, r1 = p.rows[1], p.rows[2]
+          else
+            r0, r1 = p.top[1], p.facade[2]
           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)
+          if sx >= px0 and sx <= px1 and sy >= r0 and sy <= r1 then
+            px = (sx - px0 < px1 - sx) and (px0 - 1) or (px1 + 1)
+            px = math.max(0, math.min(W - 1, px))
+            break
           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
+        put(sx, plane - 1, z, sy * W + px)
+      end
+    end
+  else
+    -- the lid continues the sibling tables' top -- black rim, white
+    -- highlight courses along the north and west, grey field
+    local field = t.desk.lid == "white" and WHITE or GREY
+    for sx = 0, W - 1 do
+      for z = dz0, dz1 do
+        local shade = field
+        if sx == 0 or sx == W - 1 or z == dz0 or z == dz1 then
+          shade = BLACK
+        elseif sx == 1 or z == dz0 + 1 then
+          shade = WHITE
         end
+        put(sx, plane - 1, z, pr.shadeTexel[shade])
       end
     end
   end
 
+  if plane > ytop then ytop = plane end
+  buildParts(plane)
+
   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)]
@@ -638,7 +995,8 @@ local function emit(m, sp, atlasW, atlasH)
   local function runX(y, z, dx, dy, dz, x)
     local i0 = ci(x, y, z)
     local strip, n = nil, 1
-    while true do
+    local cap = runCap(x)
+    while n < cap do
       local nx = x + n
       local i = ci(nx, y, z)
       if not i or ci(nx + dx, y + dy, z + dz) then break end
@@ -730,8 +1088,8 @@ local function emit(m, sp, atlasW, atlasH)
         while z <= zmax do
           local i = ci(x, y, z)
           if i and not ci(x + d, y, z) then
-            local n = 1
-            while z + n <= zmax do
+            local n, cap = 1, runCap(z)
+            while n < cap and z + n <= zmax do
               local j = ci(x, y, z + n)
               if j ~= i or ci(x + d, y, z + n) then break end
               n = n + 1
@@ -840,7 +1198,7 @@ function Buildings.build(S, map, data, perRow)
               end
               built = models[key]
             end
-            Buildings.stamp(S, map, built, tx, ty, bw, bh)
+            Buildings.stamp(S, map, built, tx, ty, bw, bh, t)
           end
         end
       end
@@ -850,9 +1208,24 @@ end
 
 -- One placement: claim its tiles (so the detector leaves them alone and
 -- the mesher paints ground under them) and copy the model into place.
-function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
-  local shape = { class = "building", h = 0, art = "building",
-                  flat = false, authored = true }
+--
+-- Two template fields alter what a claim means, for a drawing that
+-- carries a STANDEE on its surface (Red's potted plant on the dining
+-- table). `keep` names tile ids the stamp must NOT claim: their authored
+-- pins stay live, so the standee scan still stands the object exactly as
+-- it always did. `support` is the model's top plane in voxels: the claim
+-- shape carries it as its height, which is what tells that scan the
+-- standee's shelf -- a plain claim stays at h = 0, and Structures treats
+-- a building claim with height as a full model (skip, never a second
+-- box; see its support branches).
+function Buildings.stamp(S, map, quads, tx, ty, bw, bh, t)
+  local shape = { class = "building", h = (t and t.support) or 0,
+                  art = "building", flat = false, authored = true }
+  local keep = nil
+  if t and t.keep then
+    keep = {}
+    for _, id in ipairs(t.keep) do keep[id] = true end
+  end
 
   -- the ground the building stands on: the commonest flat tile around its
   -- feet, so a house on a path keeps its path
@@ -878,9 +1251,18 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
   for r = 0, bh - 1 do
     for c = 0, bw - 1 do
       local k = keyOf(tx + c, ty + r)
-      S.shapeAt[k] = shape
-      S.skip[k] = true
-      S.ground[k] = best or false
+      if keep and keep[S.tileAt[k]] then
+        -- unclaimed by request: the tile keeps its pin (the plant's
+        -- cutout pool) and the standee scan finds it there. Only the
+        -- ground is set now, so the scan's own claim of these tiles has
+        -- the building's floor to paint when no flat tile touches a
+        -- cluster ringed by its own furniture.
+        S.ground[k] = best or false
+      else
+        S.shapeAt[k] = shape
+        S.skip[k] = true
+        S.ground[k] = best or false
+      end
     end
   end
 
diff --git a/lib/Structures.lua b/lib/Structures.lua
index 2e3e70c..ad3d5ec 100644
--- a/lib/Structures.lua
+++ b/lib/Structures.lua
@@ -75,9 +75,12 @@ local OBJECT_DEPTH = 6             -- voxel thickness of a detected prop
 -- pure profile; `post` matches the 6px the detector gives the fence
 -- rows it finds on its own, so pinned and detected fences look alike;
 -- `signpost` is a plate on a stick -- 2 voxels, the thinnest that still
--- shows an edge
+-- shows an edge; `bike` is the same 2 for the same reason from the other
+-- direction -- a bicycle drawn side-on is a LINE drawing whose negative
+-- space is the drawing, and at the 5 voxels `prop` gives, the side faces
+-- of neighbouring strokes close every gap in it off-axis
 local PINNED_DEPTH = { billboard = 10, prop = 5, stool = 10, cutout = 1,
-                       console = 10, post = 6, signpost = 2 }
+                       console = 10, post = 6, signpost = 2, bike = 2 }
 
 local MAX_ROWS = 6                 -- volume height cap: 48px
 
@@ -297,7 +300,9 @@ function Structures.forMap(map)
   Structures.buildStairs(S, map, x0, x1, y0, y1)
 
   -- ---- bookcases: pinned shelves collapsed to one cell of depth ----
-  Structures.buildBookcases(S, map, x0, x1, y0, y1)
+  -- The atlas comes along so the shelf front can carry its own measured
+  -- relief: the panes it seals behind its black frames sink a voxel.
+  Structures.buildBookcases(S, map, x0, x1, y0, y1, pixels(tileset), perRow)
 
   -- ---- figures: a person drawn INTO furniture, lifted off it ----
   -- Before the region flood and the volume pass, so everything after this
@@ -307,6 +312,14 @@ function Structures.forMap(map)
   -- passes should see, and this needs no pixel access to do it.)
   Structures.buildFigures(S, map, x0, x1, y0, y1)
 
+  -- ---- mounted: a thing drawn INTO a wall band, stood proud of it ----
+  -- Here for the same reason and with the same guarantee as the figures
+  -- above: the repaint hands every pass below the plain panel the profile
+  -- says is behind the object, so the wall band it was painted into keeps
+  -- resolving as the wall it is -- without a second copy of the drawing
+  -- flat on its face.
+  Structures.buildMounted(S, map, x0, x1, y0, y1)
+
   -- ---- flood-fill regions of structural tiles ----
   local seen = {}
   local regions = {}
@@ -558,14 +571,51 @@ end
 local ROUND_SHADE = { front = 1.0, back = 0.68, side = 0.78,
                       top = 1.0, bottom = 0.55 }
 
-local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
-  N = N or 16                  -- art canvas: 16 = one cell, 32 = 2x2 cells
-  local N2 = N / 2
+-- The potted plant's ORGANIC HALF: the leaf crown (16 rows), then the
+-- trunk, its root flare and the strands draping over the pot's rim (8
+-- more) -- all of it stands as a slab this many voxels deep instead of
+-- revolving. `depth` 5 is the thin standee pool's depth, what every other
+-- interior plant already uses.
+--
+-- `rows` = 24 puts the slab/revolve boundary AT THE VESSEL'S RIM ROW, and
+-- that placement is what makes the pot read as a pot. The first cut put
+-- it at the cell seam (16), which let the root and drape rows revolve:
+-- their drawn spans are 8-12 wide, so they stacked 8-12-deep discs on top
+-- of the rim and the whole base read as one bulbous onion instead of a
+-- flat-mouthed planter with a trunk standing out of it. Only rows 24-31
+-- -- black rim edge, gold band, body, foot, the drawn flowerpot profile
+-- -- are the vessel, and only they revolve.
+local PLANTER_SPRAY = { rows = 24, depth = 5 }
+
+-- `spray`, when given, caps the chord over the canvas's top `rows` rows to
+-- `depth` voxels instead of revolving them.
+--
+-- Revolving a row turns its DRAWN WIDTH into depth, which only means
+-- something when the drawing states a width to turn -- the pot's rows do
+-- (a 3px stem opening to a 12px belly and closing to a 6px foot, an urn's
+-- profile), and a tree canopy's do (the ball's outline is drawn). A leaf
+-- crown's do NOT: the leaves are a spray that runs off all four sides of
+-- its tile, so every row measures the full canvas and the revolve can only
+-- produce a solid cylinder -- the "hedge column" a plant must never become,
+-- with one row of texels smeared down its whole top face. Where the drawing
+-- states no profile, the honest reading is the one the thin standee pools
+-- exist for: the foliage stands as a per-pixel slab and keeps the airy
+-- silhouette that makes it read as leaves.
+local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows,
+                             NYin, spray, baseRows, bodyRows, wellRows,
+                             taperVox)
+  -- The canvas is NX wide and NX DEEP (a hull is round in plan, so its
+  -- depth is its width) by NY tall. NX = 16 is one cell, 32 a 2x2-cell
+  -- group; NY defaults to NX -- a ball -- and NY = 2 * NX is a drawing
+  -- STACKED two cells high on one cell of plot (the potted plant).
+  local NX = N or 16
+  local NY = NYin or NX
+  local N2 = NX / 2
   local perRow = map.tileset.tilesPerRow or 16
   local atlasW = map.tileset.imageWidth or 128
   local atlasH = map.tileset.imageHeight or 48
 
-  -- cell-space art access (NxN, row 0 = top), anchored at cell (cx, cy)
+  -- cell-space art access (NX x NY, row 0 = top), anchored at cell (cx, cy)
   local function tileOf(px, py)
     return S.tileAt[keyOf(cx * 2 + math.floor(px / 8),
                           cy * 2 + math.floor(py / 8))]
@@ -576,19 +626,19 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
            math.floor(tile / perRow) * 8 + py % 8
   end
 
-  -- shade class of every canvas pixel, indexed py * N + px
+  -- shade class of every canvas pixel, indexed py * NX + px
   local cls = {}
-  for py = 0, N - 1 do
-    for px = 0, N - 1 do
+  for py = 0, NY - 1 do
+    for px = 0, NX - 1 do
       local ax, ay = texel(px, py)
       local r, g, b, a = data:getPixel(ax, ay)
-      cls[py * N + px] = a == 0 and "off"
-                         or Structures.shadeClass(math.min(r, g, b))
+      cls[py * NX + px] = a == 0 and "off"
+                          or Structures.shadeClass(math.min(r, g, b))
     end
   end
 
-  -- 4-connected flood from the canvas border through `passable` classes
-  local function floodOutside(passable)
+  -- 4-connected flood from a row band's border through `passable` classes
+  local function floodOutside(passable, y0, y1)
     local out, stack = {}, {}
     local function seed(i)
       if not out[i] and passable[cls[i]] then
@@ -596,39 +646,56 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
         stack[#stack + 1] = i
       end
     end
-    for i = 0, N - 1 do
-      seed(i); seed(N * (N - 1) + i); seed(i * N); seed(i * N + N - 1)
+    for px = 0, NX - 1 do
+      seed(y0 * NX + px); seed(y1 * NX + px)
+    end
+    for py = y0, y1 do
+      seed(py * NX); seed(py * NX + NX - 1)
     end
     while #stack > 0 do
       local i = table.remove(stack)
-      local px = i % N
+      local px, py = i % NX, math.floor(i / NX)
       if px > 0 then seed(i - 1) end
-      if px < N - 1 then seed(i + 1) end
-      if i >= N then seed(i - N) end
-      if i < N * (N - 1) then seed(i + N) end
+      if px < NX - 1 then seed(i + 1) end
+      if py > y0 then seed(i - NX) end
+      if py < y1 then seed(i + NX) end
     end
     return out
   end
 
-  -- the mask: darkest-pixel outline plus its enclosure; dither fallback
-  local out = floodOutside({ off = true, dark = true,
-                             light = true, white = true })
-  local mask, enclosed = {}, 0
-  for i = 0, N * N - 1 do
-    if not out[i] then
-      mask[i] = true
-      if cls[i] ~= "black" then enclosed = enclosed + 1 end
-    end
-  end
-  if enclosed < N * N / 8 then
-    out = floodOutside({ off = true, light = true, white = true })
-    mask = {}
-    for i = 0, N * N - 1 do
-      if not out[i] and cls[i] ~= "off" then mask[i] = true end
+  -- The mask -- darkest-pixel outline plus its enclosure, with the dither
+  -- rule as fallback -- computed per CELL BAND of NX rows.
+  --
+  -- A square canvas is ONE band, so this is exactly the whole-canvas rule
+  -- it replaces. A STACKED canvas needs it per band because its two halves
+  -- want opposite answers: the potted plant's leaf crown is a black-outlined
+  -- dither drawn over floor (outline enclosure keeps it), while its pot is a
+  -- solid DARK body whose base runs flush to the band's bottom edge (the
+  -- enclosure flood walks in through dark and guts it, and the fallback --
+  -- which the band's own `enclosed` count asks for -- keeps it). Measured on
+  -- the Center plant: one flood over both bands keeps 53% of the drawing and
+  -- leaves the pot a hollow black frame; per band keeps 68% and both read.
+  local mask = {}
+  for band = 0, NY / NX - 1 do
+    local y0, y1 = band * NX, band * NX + NX - 1
+    local out = floodOutside({ off = true, dark = true,
+                               light = true, white = true }, y0, y1)
+    local enclosed = 0
+    for i = y0 * NX, (y1 + 1) * NX - 1 do
+      if not out[i] then
+        mask[i] = true
+        if cls[i] ~= "black" then enclosed = enclosed + 1 end
+      end
+    end
+    if enclosed < NX * NX / 8 then
+      out = floodOutside({ off = true, light = true, white = true }, y0, y1)
+      for i = y0 * NX, (y1 + 1) * NX - 1 do
+        mask[i] = (not out[i] and cls[i] ~= "off") or nil
+      end
     end
   end
   local any = nil
-  for i = 0, N * N - 1 do any = any or mask[i] end
+  for i = 0, NX * NY - 1 do any = any or mask[i] end
   if not any then return {} end
 
   -- a CAPPED hull (the stump): the top capRows rows of the mask are the
@@ -638,24 +705,96 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
   local capY0, capY1 = nil, nil
   if capRows and capRows > 0 then
     local top = nil
-    for iy = 0, N - 1 do
-      for ix = 0, N - 1 do
-        if mask[iy * N + ix] then top = iy break end
+    for iy = 0, NY - 1 do
+      for ix = 0, NX - 1 do
+        if mask[iy * NX + ix] then top = iy break end
       end
       if top then break end
     end
     if top then
       capY0 = top
-      capY1 = math.min(top + capRows - 1, N - 2)
+      capY1 = math.min(top + capRows - 1, NY - 2)
       for iy = capY0, capY1 do
-        for ix = 0, N - 1 do mask[iy * N + ix] = nil end
+        for ix = 0, NX - 1 do mask[iy * NX + ix] = nil end
+      end
+      any = nil
+      for i = 0, NX * NY - 1 do any = any or mask[i] end
+      if not any then return {} end
+    end
+  end
+
+  -- a FLAT-BASED hull (the can): the bottom baseRows rows of the mask are
+  -- the BASE circle's front arc -- the drawing's mirror of the cut face
+  -- above, ground contact seen from above rather than body. A can is only
+  -- round in the horizontal plane, so the drop those rows make toward the
+  -- middle is DEPTH, not a narrowing of the plan: left as body they revolve
+  -- into ever smaller discs and the can ends up balanced on a stem three
+  -- voxels wide (which is exactly what the first build did). Strip them and
+  -- the foot rule below runs the last body row's full disc straight to the
+  -- floor; the rows keep their own texels there, so the front view is still
+  -- the drawing, base rim and all.
+  local baseArt = nil
+  if baseRows and baseRows > 0 then
+    local bot = nil
+    for iy = NY - 1, 0, -1 do
+      for ix = 0, NX - 1 do
+        if mask[iy * NX + ix] then bot = iy break end
+      end
+      if bot then break end
+    end
+    if bot then
+      baseArt = {}
+      for iy = math.max(bot - baseRows + 1, (capY1 or -1) + 2), bot do
+        for ix = 0, NX - 1 do
+          local i = iy * NX + ix
+          if mask[i] then baseArt[i] = true end
+          mask[i] = nil
+        end
       end
       any = nil
-      for i = 0, N * N - 1 do any = any or mask[i] end
+      for i = 0, NX * NY - 1 do any = any or mask[i] end
       if not any then return {} end
     end
   end
 
+  -- The can's HEIGHT, and the one place this file departs from the drawing
+  -- on purpose. Strictly un-projected, the drawing states a squat drum: cut
+  -- the mouth ellipse off the top and the base circle off the bottom and
+  -- barely two rows of straight side are left between them, because the GB
+  -- artist spent most of a 16px cell on the opening. A real bin is TALLER
+  -- than it is wide, and the flat game reads as one because the drawing is
+  -- 14px tall next to a 16px player -- so the height is authored (can_height
+  -- voxels) rather than measured, and the surviving body band is repeated
+  -- upward to fill it, bottom row first, which continues the drawn rib
+  -- rhythm instead of inventing a texel. Everything else still comes off
+  -- the pixels.
+  local artRow = {}
+  if bodyRows and bodyRows > 0 then
+    local body = {}
+    for iy = 0, NY - 1 do
+      for ix = 0, NX - 1 do
+        if mask[iy * NX + ix] then body[#body + 1] = iy break end
+      end
+    end
+    local nb = #body
+    if nb > 0 then
+      local top = body[1]
+      for iy = top - 1, math.max(NY - bodyRows, 0), -1 do
+        -- the LOWEST surviving body row, repeated: it is the widest and
+        -- plainest reading of the material (outline, shaded flank, lit
+        -- face) and stacks into a clean metal cylinder. Cycling the whole
+        -- surviving band instead stacks the drawn rim arcs into a barcode
+        -- of hoops, which is detail the drawing never states about the
+        -- side of the can.
+        local from = body[nb]
+        artRow[iy] = from
+        for ix = 0, NX - 1 do
+          mask[iy * NX + ix] = mask[from * NX + ix]
+        end
+      end
+    end
+  end
+
   -- the ground the ball stands on: the drawing's own background names
   -- it. Score every flat ground tile the map places against the cell's
   -- unmasked light pixels and keep the closest -- mid-forest trees have
@@ -670,11 +809,15 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
       local ox = (t % perRow) * 8
       local oy = math.floor(t / perRow) * 8
       local score, n = 0, 0
-      for py = 0, N - 1 do
-        for px = 0, N - 1 do
-          local i = py * N + px
+      for py = 0, NY - 1 do
+        for px = 0, NX - 1 do
+          local i = py * NX + px
           local c = cls[i]
-          if not mask[i] and (c == "light" or c == "white") then
+          -- a stripped base row is the OBJECT's own rim, not background:
+          -- scoring its whites against the floor tiles matches paper-white
+          -- ground under a can whose art stands on the gym's grey
+          if not mask[i] and not (baseArt and baseArt[i])
+             and (c == "light" or c == "white") then
             local ax, ay = texel(px, py)
             local r1, g1, b1 = data:getPixel(ax, ay)
             local r2, g2, b2 = data:getPixel(ox + px % 8, oy + py % 8)
@@ -691,14 +834,17 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
     end
   end
 
-  -- discs: per mask pixel a z chord [z0, z1), from its row's span circle
-  local z0, z1, src = {}, {}, {}
+  -- discs: per mask pixel a z chord [z0, z1), from its row's span circle.
+  -- z2/z3 is an optional SECOND chord for the same pixel, which only the
+  -- can's hollow mouth uses: a ring in plan needs a front wall and a back
+  -- wall at the same column, and one interval cannot say that.
+  local z0, z1, z2, z3, src, srcX = {}, {}, {}, {}, {}, {}
   local loRow, hiRow = {}, {}
   local yBot = nil
-  for iy = 0, N - 1 do
+  for iy = 0, NY - 1 do
     local lo, hi = nil, nil
-    for ix = 0, N - 1 do
-      if mask[iy * N + ix] then
+    for ix = 0, NX - 1 do
+      if mask[iy * NX + ix] then
         lo = lo or ix
         hi = ix
       end
@@ -709,7 +855,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
       local c = (lo + hi + 1) / 2
       local hw = (hi - lo + 1) / 2
       for ix = lo, hi do
-        local i = iy * N + ix
+        local i = iy * NX + ix
         if mask[i] then
           local dx = ix + 0.5 - c
           local n = 1
@@ -717,23 +863,148 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
             n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx)
                                        + 0.5))
           end
+          if spray and iy < spray.rows then n = math.min(n, spray.depth) end
           z0[i] = math.floor(N2 - n / 2 + 0.5)
           z1[i] = z0[i] + n
-          src[i] = iy
+          -- a row the can's body band was repeated into wears the row it
+          -- was copied from, never a texel of its own
+          src[i] = artRow[iy] or iy
+        end
+      end
+    end
+  end
+
+  -- Spray-gap BACKING: the drawing's own gap pixels, one voxel deep at
+  -- the slab's mid-plane. The flat crown is full of floor showing
+  -- between leaves; carved as an open slab those gaps became TUNNELS --
+  -- the Center couch, the man sitting on it and the void wall all read
+  -- as pink/orange/black confetti INSIDE the foliage, and the sparse
+  -- bottom rows (lone drawn leaf tips) floated as disconnected specks
+  -- against them. The drawing itself backs every gap with its own
+  -- pixels, so the hull does the same: each in-span gap below drawn
+  -- foliage takes ITS OWN texel as a plate recessed behind the leaf
+  -- relief. Coverage is monotone down a column, so the first backed
+  -- cell always sits directly under a leaf chord -- and every chord
+  -- spans the mid-plane, so no plate ever caps the crown's top: columns
+  -- open to the sky stay open and the silhouette keeps its notches.
+  if spray then
+    for iy = 1, math.min(spray.rows, NY) - 1 do
+      if loRow[iy] then
+        for ix = loRow[iy], hiRow[iy] do
+          local i = iy * NX + ix
+          if not z0[i] then
+            local covered = false
+            for iy2 = 0, iy - 1 do
+              if mask[iy2 * NX + ix] then covered = true break end
+            end
+            if covered then
+              z0[i], z1[i], src[i] = N2, N2 + 1, iy
+            end
+          end
         end
       end
     end
   end
 
   -- foot: rows under the mask repeat the bottom row's discs, wearing the
-  -- bottom row's (outline-dark) pixels
-  for iy = yBot + 1, N - 1 do
+  -- bottom row's (outline-dark) pixels -- except where a stripped base row
+  -- DREW something at that pixel, which keeps its own texel, so a can's
+  -- drawn base rim lands on the model's base instead of being painted over
+  -- by the body band above it
+  for iy = yBot + 1, NY - 1 do
     loRow[iy], hiRow[iy] = loRow[yBot], hiRow[yBot]
     for ix = loRow[yBot], hiRow[yBot] do
-      local b = yBot * N + ix
+      local b = yBot * NX + ix
       if z0[b] then
-        local i = iy * N + ix
-        z0[i], z1[i], src[i] = z0[b], z1[b], yBot
+        local i = iy * NX + ix
+        z0[i], z1[i] = z0[b], z1[b]
+        src[i] = (baseArt and baseArt[i]) and iy or yBot
+      end
+    end
+  end
+
+  -- the TAPER: a bin is a truncated cone, not a tube -- wide at the rim,
+  -- drawn in a couple of voxels toward the base. The drawing agrees as far
+  -- as it can (its own base arc pulls in to 9px from the 11px flanks), but
+  -- it cannot state the whole run, so taperVox is the diameter the base
+  -- loses and the rows in between interpolate. Every row keeps its plan
+  -- ROUND: narrow the span, then re-cut the chords from the narrowed span,
+  -- or the model comes out a cylinder with its corners shaved.
+  local stepped = {}
+  if taperVox and taperVox > 0 then
+    local yTopRow = nil
+    for iy = 0, NY - 1 do
+      if loRow[iy] then yTopRow = iy break end
+    end
+    local span = NY - 1 - (yTopRow or 0)
+    if yTopRow and span > 0 then
+      for iy = yTopRow, NY - 1 do
+        local inset = math.floor(taperVox / 2 * (iy - yTopRow) / span + 0.5)
+        if inset > 0 and loRow[iy] then
+          local lo = loRow[iy] + inset
+          local hi = hiRow[iy] - inset
+          if hi - lo < 1 then
+            lo = math.floor((loRow[iy] + hiRow[iy]) / 2)
+            hi = lo + 1
+          end
+          for ix = loRow[iy], hiRow[iy] do
+            if ix < lo or ix > hi then
+              local i = iy * NX + ix
+              z0[i], z1[i], z2[i], z3[i] = nil, nil, nil, nil
+            end
+          end
+          -- squeeze the row's ART into the narrowed span rather than
+          -- clipping its ends off: the drawn outline is the last column
+          -- either side, and dropping it leaves the taper's new edge
+          -- wearing an interior texel -- a white chip down the rim
+          for ix = lo, hi do
+            srcX[iy * NX + ix] = loRow[iy]
+              + math.floor((ix - lo) * (hiRow[iy] - loRow[iy])
+                           / (hi - lo) + 0.5)
+          end
+          loRow[iy], hiRow[iy] = lo, hi
+          stepped[iy] = true
+          local c = (lo + hi + 1) / 2
+          local hw = (hi - lo + 1) / 2
+          for ix = lo, hi do
+            local i = iy * NX + ix
+            if z0[i] then
+              local dx = ix + 0.5 - c
+              local n = 1
+              if hw * hw > dx * dx then
+                n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx)
+                                           + 0.5))
+              end
+              z0[i] = math.floor(N2 - n / 2 + 0.5)
+              z1[i] = z0[i] + n
+            end
+          end
+        end
+      end
+    end
+  end
+
+  -- the MOUTH: a bin is open, and a solid top wearing the drawn opening
+  -- only paints one. Hollow the top wellRows voxel rows -- every chord
+  -- long enough to hold two walls plus a gap keeps a wall at each end and
+  -- loses its middle, which is a ring in plan, so the model has a real rim
+  -- to look into. The short chords at the left and right of the row ARE
+  -- the ring's sides and stay solid on their own.
+  local wellTop = nil
+  if wellRows and wellRows > 0 then
+    for iy = 0, NY - 1 do
+      if loRow[iy] then wellTop = iy break end
+    end
+    local wall = 2
+    for iy = wellTop or 0, math.min((wellTop or 0) + wellRows - 1, NY - 1) do
+      if loRow[iy] then
+        for ix = loRow[iy], hiRow[iy] do
+          local i = iy * NX + ix
+          if z0[i] and z1[i] - z0[i] > wall * 2 then
+            z2[i], z3[i] = z1[i] - wall, z1[i]
+            z1[i] = z0[i] + wall
+          end
+        end
       end
     end
   end
@@ -742,31 +1013,47 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
   -- projection below
   local capTopRow, capZ0, capZ1 = nil, nil, nil
   if capY0 then
-    for iy = 0, N - 1 do
+    for iy = 0, NY - 1 do
       if loRow[iy] then capTopRow = iy break end
     end
     if capTopRow then
       for ix = loRow[capTopRow], hiRow[capTopRow] do
-        local i = capTopRow * N + ix
+        local i = capTopRow * NX + ix
         if z0[i] then
+          -- the OUTER extent, so a hollowed row still projects the mouth
+          -- across the whole opening and not just its front wall
+          local back = z3[i] or z1[i]
           capZ0 = math.min(capZ0 or z0[i], z0[i])
-          capZ1 = math.max(capZ1 or z1[i], z1[i])
+          capZ1 = math.max(capZ1 or back, back)
         end
       end
     end
   end
 
+  -- the art row the mouth projection puts at depth iz -- the drawn
+  -- opening's north arc at the far side of the hull, its south arc at the
+  -- near one. The top-face pass below reads the same mapping; this is the
+  -- vertical faces inside the well asking it the same question.
+  local function mouthRow(iz)
+    if not (capY0 and capZ0 and capZ1) then return 0 end
+    local t = capZ1 - 1 > capZ0 and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0
+    t = math.max(0, math.min(1, t))
+    return capY0 + math.floor(t * (capY1 - capY0) + 0.5)
+  end
+
   local function solidAt(ix, iy, iz)
-    if ix < 0 or ix > N - 1 or iy < 0 or iy > N - 1 then return false end
-    local i = iy * N + ix
-    return z0[i] ~= nil and iz >= z0[i] and iz < z1[i]
+    if ix < 0 or ix > NX - 1 or iy < 0 or iy > NY - 1 then return false end
+    local i = iy * NX + ix
+    if z0[i] == nil then return false end
+    if iz >= z0[i] and iz < z1[i] then return true end
+    return z2[i] ~= nil and iz >= z2[i] and iz < z3[i]
   end
 
   -- cap interiors sample the canopy a couple of rows below the rim,
   -- skipping outline-dark pixels
   local function deepTexel(ix, iy)
-    for iy2 = iy + 2, math.min(N - 1, iy + 4) do
-      local i = iy2 * N + ix
+    for iy2 = iy + 2, math.min(NY - 1, iy + 4) do
+      local i = iy2 * NX + ix
       if mask[i] and cls[i] ~= "black" then return texel(ix, iy2) end
     end
     return texel(ix, iy)
@@ -778,56 +1065,95 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
   -- ball paints solid black the moment the camera turns. The foot rows
   -- stay dark on purpose: their whole source row is outline-black.
   local function sideTexel(ix, iy)
-    local r = src[iy * N + ix]
+    -- A foot row's SIDE keeps the last body row's material even where its
+    -- FRONT wears a stripped base row (the can). The drawn base rim is
+    -- front-face art; walking the de-outline inside a row that is no longer
+    -- in the mask breaks at once and hands back the silhouette's own
+    -- outline, which painted every flank of the can solid black.
+    local r = (yBot and iy > yBot) and yBot or src[iy * NX + ix]
+    -- the walk runs in ART columns, so a tapered row starts from the drawn
+    -- pixel its squeezed span put here rather than from the model column
+    local a = srcX[iy * NX + ix] or ix
     local dir = ix + ix < loRow[iy] + hiRow[iy] and 1 or -1
     for step = 0, 3 do
-      local x2 = ix + dir * step
-      local i2 = r * N + x2
-      if x2 < 0 or x2 > N - 1 or not mask[i2] then break end
+      local x2 = a + dir * step
+      local i2 = r * NX + x2
+      if x2 < 0 or x2 > NX - 1 or not mask[i2] then break end
       if cls[i2] ~= "black" then return texel(x2, r) end
     end
-    return texel(ix, r)
+    return texel(a, r)
   end
 
   local quads = {}
 
-  for iy = 0, N - 1 do
+  for iy = 0, NY - 1 do
     if loRow[iy] then
-      local yB, yT = N - 1 - iy, N - iy
+      local yB, yT = NY - 1 - iy, NY - iy
 
       -- front and back: the drawing per-pixel, columns merged where they
       -- share a chord plane; a run never crosses the 8px atlas tile seam
       -- (its u range must interpolate inside one tile)
       local ix = loRow[iy]
       while ix <= hiRow[iy] do
-        local i = iy * N + ix
+        local i = iy * NX + ix
         if z0[i] then
           local ix2 = ix
           while ix2 + 1 <= hiRow[iy] do
-            local j = iy * N + ix2 + 1
-            if z0[j] == z0[i] and z1[j] == z1[i]
+            local j = iy * NX + ix2 + 1
+            -- src too: a can's foot row draws part of its span from the
+            -- stripped base rim and the rest from the body band above it,
+            -- so a run must not straddle two source rows (the u range is
+            -- interpolated from one row's texels)
+            if z0[j] == z0[i] and z1[j] == z1[i] and src[j] == src[i]
+               and z2[j] == z2[i] and z3[j] == z3[i]
                and math.floor((ix2 + 1) / 8) == math.floor(ix / 8) then
               ix2 = ix2 + 1
             else
               break
             end
           end
-          local ax0, ay = texel(ix, src[i])
-          local ax1 = (texel(ix2, src[i]))
-          local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW
-          local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH
           local x0, x1 = ix - N2, ix2 - N2 + 1
-          local zF, zB = z1[i] - N2, z0[i] - N2
-          quads[#quads + 1] = {
-            { x0, yB, zF }, { x1, yB, zF }, { x1, yT, zF }, { x0, yT, zF },
-            uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
-            shade = ROUND_SHADE.front,
-          }
-          quads[#quads + 1] = {
-            { x1, yB, zB }, { x0, yB, zB }, { x0, yT, zB }, { x1, yT, zB },
-            uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } },
-            shade = ROUND_SHADE.back,
-          }
+          -- one facing pair per chord, each face given the art row it
+          -- should wear. A hollowed mouth row has two chords, and the two
+          -- faces that look into the well take the drawn OPENING (via the
+          -- same projection the rim does) rather than the body band: the
+          -- drawing paints its mouth dark, and an inside-out white wall
+          -- across the opening is the one thing that stops a bin reading
+          -- as a bin.
+          local function facing(za, zb, rowF, rowB)
+            local zF, zB = zb - N2, za - N2
+            local function pair(z, row, shade, back)
+              local ax0, ay = texel(srcX[i] or ix, row)
+              local ax1 = (texel(srcX[iy * NX + ix2] or ix2, row))
+              local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW
+              local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH
+              if back then
+                quads[#quads + 1] = {
+                  { x1, yB, z }, { x0, yB, z }, { x0, yT, z }, { x1, yT, z },
+                  uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } },
+                  shade = shade,
+                }
+              else
+                quads[#quads + 1] = {
+                  { x0, yB, z }, { x1, yB, z }, { x1, yT, z }, { x0, yT, z },
+                  uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
+                  shade = shade,
+                }
+              end
+            end
+            pair(zF, rowF, ROUND_SHADE.front, false)
+            pair(zB, rowB, ROUND_SHADE.back, true)
+          end
+          local body = src[i]
+          if z2[i] then
+            -- z grows toward the viewer: the low chord is the can's FAR
+            -- wall, so its +z face is the inside you look across, and the
+            -- near chord's -z face is the inside of the wall facing you
+            facing(z0[i], z1[i], mouthRow(z1[i]), body)
+            facing(z2[i], z3[i], body, mouthRow(z2[i] - 1))
+          else
+            facing(z0[i], z1[i], body, body)
+          end
           ix = ix2 + 1
         else
           ix = ix + 1
@@ -837,19 +1163,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
       -- sides, steps, undersides: constant-texel quads over the z runs a
       -- neighbour doesn't cover
       for ix = loRow[iy], hiRow[iy] do
-        local i = iy * N + ix
+        local i = iy * NX + ix
         if z0[i] then
-          local ax, ay = texel(ix, src[i])
+          local ax, ay = texel(srcX[i] or ix, src[i])
           local u, v = (ax + 0.5) / atlasW, (ay + 0.5) / atlasH
           local x0, x1 = ix - N2, ix - N2 + 1
 
-          -- exposed z pieces against one neighbouring column
-          local function pieces(nx, ny, emit)
-            local iz = z0[i]
-            while iz < z1[i] do
+          -- exposed z pieces against one neighbouring column, over each of
+          -- the pixel's chords (a hollowed mouth row has two)
+          local function chordPieces(nx, ny, emit, zLo, zHi)
+            local iz = zLo
+            while iz < zHi do
               if not solidAt(nx, ny, iz) then
                 local iz2 = iz
-                while iz2 + 1 < z1[i] and not solidAt(nx, ny, iz2 + 1) do
+                while iz2 + 1 < zHi and not solidAt(nx, ny, iz2 + 1) do
                   iz2 = iz2 + 1
                 end
                 emit(iz - N2, iz2 - N2 + 1, iz, iz2)
@@ -859,6 +1186,10 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
               end
             end
           end
+          local function pieces(nx, ny, emit)
+            chordPieces(nx, ny, emit, z0[i], z1[i])
+            if z2[i] then chordPieces(nx, ny, emit, z2[i], z3[i]) end
+          end
 
           local sax, say = sideTexel(ix, iy)
           local su, sv = (sax + 0.5) / atlasW, (say + 0.5) / atlasH
@@ -881,7 +1212,12 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
                 u = tu, v = tv, shade = ROUND_SHADE.top,
               }
             end
-            if capTopRow and iy == capTopRow and capZ1 then
+            -- the whole hollowed band takes the projection, not just its
+            -- top row: the rim ring gets the mouth's outer arcs and the
+            -- floor of the well gets its middle, so looking in reads as
+            -- one opening rather than a lid with a hole punched in it
+            if capTopRow and capZ1
+               and iy >= capTopRow and iy <= capTopRow + (wellRows or 0) then
               -- the CUT FACE (a capped hull's top): project the drawn
               -- ellipse across the round cap voxel row by voxel row --
               -- its top arc at the cap's north rim, its bottom arc at
@@ -890,7 +1226,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
                 local t = capZ1 - 1 > capZ0
                           and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0
                 local ry = capY0 + math.floor(t * (capY1 - capY0) + 0.5)
-                local cax, cay = texel(ix, ry)
+                local cax, cay = texel(srcX[i] or ix, ry)
                 top(iz - N2, iz - N2 + 1,
                     (cax + 0.5) / atlasW, (cay + 0.5) / atlasH)
               end
@@ -900,11 +1236,21 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows)
               top(zA, zA + 1, u, v)
               top(zA + 1, zB - 1, (du + 0.5) / atlasW, (dv + 0.5) / atlasH)
               top(zB - 1, zB, u, v)
+            elseif stepped[iy] then
+              -- a taper STEP: the chord narrowing leaves a ring facing up
+              -- at the front of the can, and wearing the lit body band it
+              -- reads as a bright chip taken out of the wall. The drawing's
+              -- own rim column is black, so the step wears that and the
+              -- taper reads as a hoop line -- which is how the reference
+              -- object is banded anyway.
+              local rx = srcX[iy * NX + loRow[iy]] or loRow[iy]
+              local rax, ray = texel(rx, src[i])
+              top(zA, zB, (rax + 0.5) / atlasW, (ray + 0.5) / atlasH)
             else
               top(zA, zB, u, v)
             end
           end)
-          if iy < N - 1 then
+          if iy < NY - 1 then
             pieces(ix, iy + 1, function(zA, zB)
               quads[#quads + 1] = {
                 { x0, yB, zB }, { x1, yB, zB }, { x1, yB, zA }, { x0, yB, zA },
@@ -946,8 +1292,12 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
   local tsid = tostring(map.tileset.id or map.tileset.image or "?")
 
   -- the stump class's drawn-ellipse height, hand-authored per tileset
-  -- (the profile's stump_cap, in art rows)
-  local stumpCap = 6
+  -- (the profile's stump_cap, in art rows), and the can class's three: the
+  -- mouth ellipse over the top (can_cap) and the base ellipse under the
+  -- bottom (can_base), both in art rows, plus the authored can_height in
+  -- voxels the body band is repeated up to
+  local stumpCap, canCap, canBase, canHeight, canWell, canTaper
+    = 6, 9, 4, 9, 5, 4
   do
     local okP, prof = pcall(V.data, "voxel_heights")
     local entry = okP and type(prof) == "table" and prof.tilesets
@@ -955,6 +1305,21 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
     if entry and type(entry.stump_cap) == "number" then
       stumpCap = entry.stump_cap
     end
+    if entry and type(entry.can_cap) == "number" then
+      canCap = entry.can_cap
+    end
+    if entry and type(entry.can_base) == "number" then
+      canBase = entry.can_base
+    end
+    if entry and type(entry.can_height) == "number" then
+      canHeight = entry.can_height
+    end
+    if entry and type(entry.can_well) == "number" then
+      canWell = entry.can_well
+    end
+    if entry and type(entry.can_taper) == "number" then
+      canTaper = entry.can_taper
+    end
   end
 
   -- cells consumed by a 2x2 `canopy` group; the scan runs north to
@@ -1015,13 +1380,72 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
           grouped[ckey + 8192] = true
           grouped[ckey + 8193] = true
         end
+      elseif s and s.art == "planter" and near then
+        -- ONE 16x32x16 hull over a drawing stacked TWO CELLS HIGH on one
+        -- cell of plot: the Pokemon Centers' potted plants (a leaf crown
+        -- over a flared pot, 78 placements across 13 maps).
+        --
+        -- The anchor is the NORTH cell -- the crown, where the canvas
+        -- starts -- but the hull stands in the SOUTH cell, because that is
+        -- where the pot is drawn and an object's ground contact is its
+        -- plot. The crown is therefore HEIGHT, not depth: the north cell
+        -- is claimed and left as floor for the crown to overhang, which is
+        -- what un-projecting the 3/4 view means here. Pinning only one of
+        -- the two cells leaves the drawing partial (a map edit, a mod's
+        -- stray tile), so the anchor is left alone rather than carved into
+        -- half a plant.
+        local below = S.shapeAt[keyOf(cx * 2, (cy + 1) * 2)]
+        if below and below.art == "planter" then
+          local ground = false
+          if data then
+            local ids = {}
+            for dy = 0, 3 do
+              for dx = 0, 1 do
+                ids[#ids + 1] = S.tileAt[keyOf(cx * 2 + dx, cy * 2 + dy)]
+              end
+            end
+            local sig = tsid .. "|p32|" .. gsig .. "|"
+                        .. table.concat(ids, ":")
+            local tpl = roundCache[sig]
+            if not tpl then
+              local tq, tbg = roundTemplate(S, map, data, cx, cy,
+                                            groundTiles, 16, nil, 32,
+                                            PLANTER_SPRAY)
+              tpl = { quads = tq, bg = tbg }
+              roundCache[sig] = tpl
+            end
+            ground = tpl.bg or false
+            S.roundStamps[#S.roundStamps + 1] =
+              { quads = tpl.quads, mx = cx * 16 + 8,
+                mz = (cy + 1) * 16 + 8 }
+          end
+          for dy = 0, 3 do
+            for dx = 0, 1 do
+              local tk = keyOf(cx * 2 + dx, cy * 2 + dy)
+              S.skip[tk] = true
+              S.ground[tk] = ground
+            end
+          end
+          grouped[ckey + 8192] = true
+        end
       elseif s and s.art == "cylinder" and near then
         -- a `stump`-class cell is the same hull with a cut face: its
-        -- top capRows of drawing project onto the round top
-        local cap = s.class == "stump" and stumpCap or nil
+        -- top capRows of drawing project onto the round top. A `can`-class
+        -- cell is that hull cut at BOTH ends -- lid on top, base circle on
+        -- the floor -- which is what a drum standing on a floor is.
+        local cap = (s.class == "stump" and stumpCap)
+                    or (s.class == "can" and canCap) or nil
+        local base = s.class == "can" and canBase or nil
+        local tall = s.class == "can" and canHeight or nil
+        local well = s.class == "can" and canWell or nil
+        local taper = s.class == "can" and canTaper or nil
         local ground = false
         if data then
-          local sig = tsid .. (cap and ("|c" .. cap) or "") .. "|"
+          local sig = tsid .. (cap and ("|c" .. cap) or "")
+            .. (base and ("|b" .. base) or "")
+            .. (tall and ("|h" .. tall) or "")
+            .. (well and ("|w" .. well) or "")
+            .. (taper and ("|t" .. taper) or "") .. "|"
             .. gsig .. "|" .. table.concat({
             S.tileAt[k], S.tileAt[keyOf(cx * 2 + 1, cy * 2)],
             S.tileAt[keyOf(cx * 2, cy * 2 + 1)],
@@ -1029,7 +1453,8 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles)
           local tpl = roundCache[sig]
           if not tpl then
             local tq, tbg = roundTemplate(S, map, data, cx, cy,
-                                          groundTiles, 16, cap)
+                                          groundTiles, 16, cap, nil, nil,
+                                          base, tall, well, taper)
             tpl = { quads = tq, bg = tbg }
             roundCache[sig] = tpl
           end
@@ -1179,11 +1604,107 @@ end
 -- When the row just above a rank is undetected structure (a shared trim
 -- tile the profile cannot pin), the rank adopts it as its CAP: one more
 -- band of height and the art its top face wears.
-local BOOK_SHADE = { south = 1.0, north = 0.68, flank = 0.8, top = 0.85 }
+local BOOK_SHADE = { south = 1.0, north = 0.68, flank = 0.8, top = 0.85,
+                     -- a pane's reveal: the one-voxel side of the frame
+                     -- standing proud of it.  The sill catches the light
+                     -- the top face does; the lintel is in shadow.
+                     sill = 0.85, lintel = 0.5 }
+
+-- A pane is a shelf opening, a glass door or an inset panel: a non-black
+-- region the drawing SEALS OFF behind its own black frame.  Anything
+-- wider or taller than this is a band of the front itself -- a trim
+-- course, a plinth -- and stays flush.  The same number and the same
+-- rule lib/Buildings.lua measures a facade's panes with, so a shelf the
+-- band pipeline models and a shelf this class collapses carry the same
+-- relief.
+local BOOK_RECESS_MAX = 24
+
+-- The panes of a BANK of ranks -- every rank of the same height standing
+-- side by side -- as a mask over the bank's south face, plus the atlas
+-- pixel each face texel comes from.  Measured over the whole bank rather
+-- than per column, because a door panel drawn across two tiles is one
+-- region and not two halves, and because the size test that keeps a
+-- broad course flush has to see the course's real width.
+--
+-- `fx` runs across the bank and `fy` DOWN from its top, so the grid
+-- reads like the drawing: the rank folds its tiles up band by band, the
+-- southmost row lowest, and fy = 0 is the topmost drawn row.
+local function bookcasePanes(map, data, perRow, run, i, j)
+  if not data then return nil end
+  local bands = run[i].bands
+  local size = run[i].front - run[i].top + 1
+  local W, H = (j - i + 1) * 8, bands * 8
+  local light, srcU, srcV = {}, {}, {}
+  for fy = 0, H - 1 do
+    local band = bands - 1 - math.floor(fy / 8)
+    local row = fy % 8
+    for fx = 0, W - 1 do
+      local col = run[i + math.floor(fx / 8)]
+      local tile = band < size and map:tileAt(col.tx, col.front - band)
+                   or col.cap
+      if tile then
+        local k = fy * W + fx
+        local ax = (tile % perRow) * 8 + fx % 8
+        local ay = math.floor(tile / perRow) * 8 + row
+        srcU[k], srcV[k] = ax, ay
+        local r, g, b, a = data:getPixel(ax, ay)
+        light[k] = a ~= 0
+          and Structures.shadeClass(math.min(r, g, b)) ~= "black"
+      end
+    end
+  end
+
+  -- The drawing's non-black regions, split across its black frames.  A
+  -- region that reaches the face's own border is not sealed by anything
+  -- -- it is a course of the front running edge to edge, the way a
+  -- masonry band or a wall of siding does -- and it stays flush.  That
+  -- test is what keeps this rule to shelves: `bookcase` also collapses
+  -- the League's gate walls and the terraces, and their courses run off
+  -- the drawing, so nothing there sinks.
+  local pane, seen = {}, {}
+  for k0 = 0, W * H - 1 do
+    if light[k0] and not seen[k0] then
+      local cells, stack = {}, { k0 }
+      seen[k0] = true
+      local ax0, ax1 = k0 % W, k0 % W
+      local ay0, ay1 = math.floor(k0 / W), math.floor(k0 / W)
+      local edge = false
+      while #stack > 0 do
+        local k = table.remove(stack)
+        cells[#cells + 1] = k
+        local cx, cy = k % W, math.floor(k / W)
+        if cx < ax0 then ax0 = cx end
+        if cx > ax1 then ax1 = cx end
+        if cy < ay0 then ay0 = cy end
+        if cy > ay1 then ay1 = cy end
+        if cx == 0 or cx == W - 1 or cy == 0 or cy == H - 1 then
+          edge = true
+        end
+        for _, d in ipairs(DIRS4) do
+          local nx, ny = cx + d[1], cy + d[2]
+          if nx >= 0 and nx < W and ny >= 0 and ny < H then
+            local nk = ny * W + nx
+            if light[nk] and not seen[nk] then
+              seen[nk] = true
+              stack[#stack + 1] = nk
+            end
+          end
+        end
+      end
+      if not edge and ax1 - ax0 < BOOK_RECESS_MAX
+         and ay1 - ay0 < BOOK_RECESS_MAX then
+        for _, k in ipairs(cells) do pane[k] = true end
+      end
+    end
+  end
+  return pane, srcU, srcV, W, H
+end
 
-local function bookcaseRank(S, map, tx, northTy, frontTy, capTile)
+local function bookcaseRank(S, map, perRow, run, i, j, k, pane, srcU, srcV,
+                            bankW, bankH)
+  local r = run[k]
+  local tx, northTy, frontTy, capTile = r.tx, r.top, r.front, r.cap
   local quads = S.objectQuads
-  local perRow = map.tileset.tilesPerRow or 16
   local atlasW = map.tileset.imageWidth or 128
   local atlasH = map.tileset.imageHeight or 48
   local function uvRect(tile)
@@ -1194,12 +1715,13 @@ local function bookcaseRank(S, map, tx, northTy, frontTy, capTile)
   end
 
   local size = frontTy - northTy + 1
-  local bands = size + (capTile and 1 or 0)
+  local bands = r.bands
   local h = bands * 8
   local depth = math.min(2, size) * 8
   local x0, x1 = tx * 8, tx * 8 + 8
   local z1 = frontTy * 8 + 8
   local z0 = z1 - depth
+  local fx0 = (k - i) * 8            -- this rank's columns within the bank
 
   -- does the neighbouring column continue this shelf? (flanks only cap
   -- the ends of a run of bookcases standing side by side)
@@ -1208,14 +1730,101 @@ local function bookcaseRank(S, map, tx, northTy, frontTy, capTile)
     return ns ~= nil and ns.art == "bookcase"
   end
 
+  local function sunk(fx, fy)
+    if not pane or fx < 0 or fx >= bankW or fy < 0 or fy >= bankH then
+      return false
+    end
+    return pane[fy * bankW + fx] == true
+  end
+
   for band = 0, bands - 1 do
     local tile = band < size and map:tileAt(tx, frontTy - band) or capTile
     local u0, u1, v0, v1 = uvRect(tile)
     local y0, y1 = band * 8, band * 8 + 8
-    quads[#quads + 1] = { { x0, y0, z1 }, { x1, y0, z1 },
-      { x1, y1, z1 }, { x0, y1, z1 },
-      uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
-      shade = BOOK_SHADE.south }
+    local fyTop = (bands - 1 - band) * 8
+
+    -- The south face: the drawing folded upright.  A band with no pane
+    -- in it is the single quad it has always been; a band that seals
+    -- one splits into per-row runs of texels, and the pane's run sinks
+    -- a voxel behind the frame that stays proud around it.
+    local relief = false
+    if pane then
+      for row = 0, 7 do
+        for c = 0, 7 do
+          if sunk(fx0 + c, fyTop + row) then relief = true break end
+        end
+        if relief then break end
+      end
+    end
+    if not relief then
+      quads[#quads + 1] = { { x0, y0, z1 }, { x1, y0, z1 },
+        { x1, y1, z1 }, { x0, y1, z1 },
+        uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
+        shade = BOOK_SHADE.south }
+    else
+      local ax = (tile % perRow) * 8
+      local ay = math.floor(tile / perRow) * 8
+      for row = 0, 7 do
+        local fy = fyTop + row
+        local wy = y0 + 7 - row             -- the drawing's row 0 is the top
+        local c = 0
+        while c < 8 do
+          local s = sunk(fx0 + c, fy)
+          local n = 1
+          while c + n < 8 and sunk(fx0 + c + n, fy) == s do n = n + 1 end
+          local pz = s and z1 - 1 or z1
+          local qu0 = (ax + c + 0.05) / atlasW
+          local qu1 = (ax + c + n - 0.05) / atlasW
+          local qv0 = (ay + row + 0.05) / atlasH
+          local qv1 = (ay + row + 1 - 0.05) / atlasH
+          quads[#quads + 1] = { { x0 + c, wy, pz }, { x0 + c + n, wy, pz },
+            { x0 + c + n, wy + 1, pz }, { x0 + c, wy + 1, pz },
+            uv = { { qu0, qv1 }, { qu1, qv1 }, { qu1, qv0 }, { qu0, qv0 } },
+            shade = BOOK_SHADE.south }
+          c = c + n
+        end
+      end
+      -- the reveals: where a sunk texel meets a proud one, the frame's
+      -- own one-voxel side shows.  It wears the PROUD neighbour's texel,
+      -- because that is the block it belongs to.  A pane running off the
+      -- bank, or off the top or bottom of the rank, needs none: the
+      -- flank and top faces already close it.
+      for row = 0, 7 do
+        local fy = fyTop + row
+        local wy = y0 + 7 - row
+        for c = 0, 7 do
+          if sunk(fx0 + c, fy) then
+            local X = x0 + c
+            local function reveal(nfx, nfy, verts, shade)
+              if nfx < 0 or nfx >= bankW or nfy < 0 or nfy >= bankH then
+                return
+              end
+              if sunk(nfx, nfy) then return end
+              local nk = nfy * bankW + nfx
+              if not srcU[nk] then return end
+              quads[#quads + 1] = { verts[1], verts[2], verts[3], verts[4],
+                u = (srcU[nk] + 0.5) / atlasW, v = (srcV[nk] + 0.5) / atlasH,
+                shade = shade }
+            end
+            reveal(fx0 + c - 1, fy, {
+              { X, wy, z1 }, { X, wy, z1 - 1 },
+              { X, wy + 1, z1 - 1 }, { X, wy + 1, z1 } }, BOOK_SHADE.flank)
+            reveal(fx0 + c + 1, fy, {
+              { X + 1, wy, z1 - 1 }, { X + 1, wy, z1 },
+              { X + 1, wy + 1, z1 }, { X + 1, wy + 1, z1 - 1 } },
+              BOOK_SHADE.flank)
+            reveal(fx0 + c, fy + 1, {
+              { X, wy, z1 - 1 }, { X + 1, wy, z1 - 1 },
+              { X + 1, wy, z1 }, { X, wy, z1 } }, BOOK_SHADE.sill)
+            reveal(fx0 + c, fy - 1, {
+              { X, wy + 1, z1 }, { X + 1, wy + 1, z1 },
+              { X + 1, wy + 1, z1 - 1 }, { X, wy + 1, z1 - 1 } },
+              BOOK_SHADE.lintel)
+          end
+        end
+      end
+    end
+
     quads[#quads + 1] = { { x1, y0, z0 }, { x0, y0, z0 },
       { x0, y1, z0 }, { x1, y1, z0 },
       uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
@@ -1245,10 +1854,19 @@ local function bookcaseRank(S, map, tx, northTy, frontTy, capTile)
   end
 end
 
-function Structures.buildBookcases(S, map, x0, x1, y0, y1)
+function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow)
+  perRow = perRow or map.tileset.tilesPerRow or 16
   -- What to do with the rows a rank VACATES (see TileShape.bookcaseBackfill).
   -- Read once: it is a property of the tileset, not of the column.
   local backfill = TileShape.bookcaseBackfill(map.tileset.id)
+  -- the front's measured relief: on for a shelf, off for the tilesets
+  -- that borrow the collapse for masonry or machinery
+  if not TileShape.bookcaseRelief(map.tileset.id) then data = nil end
+  -- Ranks are collected here and emitted after the sweep: a rank's panes
+  -- are measured over the whole BANK it stands in (see bookcasePanes),
+  -- and the bank is only known once every column has been read.  Nothing
+  -- below this loop mutates what the sweep reads, so deferring is free.
+  local order, banks = {}, {}
   for tx = x0, x1 do
     local ty = y1
     while ty >= y0 do
@@ -1295,7 +1913,17 @@ function Structures.buildBookcases(S, map, x0, x1, y0, y1)
               S.ground[tk] = false
             end
           end
-          bookcaseRank(S, map, tx, top, front, capTile)
+          -- ranks of the same height standing side by side are one bank
+          local bands = (front - top + 1) + (capTile and 1 or 0)
+          local key = top .. ":" .. front .. ":" .. bands
+          local bank = banks[key]
+          if not bank then
+            bank = {}
+            banks[key] = bank
+            order[#order + 1] = key
+          end
+          bank[#bank + 1] = { tx = tx, top = top, front = front,
+                              cap = capTile, bands = bands }
           front = top - 1
         end
         ty = north - 1
@@ -1304,6 +1932,24 @@ function Structures.buildBookcases(S, map, x0, x1, y0, y1)
       end
     end
   end
+
+  -- tx ascends in the sweep above, so each bank's columns are already in
+  -- order; split them into the contiguous runs that actually touch
+  for _, key in ipairs(order) do
+    local run = banks[key]
+    local i = 1
+    while i <= #run do
+      local j = i
+      while j < #run and run[j + 1].tx == run[j].tx + 1 do j = j + 1 end
+      local pane, srcU, srcV, bankW, bankH =
+        bookcasePanes(map, data, perRow, run, i, j)
+      for k = i, j do
+        bookcaseRank(S, map, perRow, run, i, j, k,
+                     pane, srcU, srcV, bankW, bankH)
+      end
+      i = j + 1
+    end
+  end
 end
 
 -- ---- stairs: pinned cells that render as real steps ----
@@ -2023,8 +2669,14 @@ function Structures.buildObject(S, map, region, cluster,
     -- stacked box is still a box: the Plateau's gate pilasters carry a
     -- statue on 48 of their tops, and collapsing the pilaster to a stacked
     -- run made every one of them fail this test and drop to ground level.
+    -- A `building` claim supports too, when it carries a height: a
+    -- Buildings template that names `support` is furniture modelled in
+    -- full with a standee left standing on it (Red's dining table under
+    -- its potted plant), and the height it states is the model's top
+    -- plane.  A plain claim stays at h = 0 and supports nothing.
     if blocked and bs and bs.authored and (bs.h or 0) > 0
-       and (bs.art == "upright" or bs.art == "bookcase") then
+       and (bs.art == "upright" or bs.art == "bookcase"
+            or bs.class == "building") then
       baseY, support = bs.h, bs
     end
   end
@@ -2159,7 +2811,8 @@ function Structures.buildObject(S, map, region, cluster,
   for _, c in ipairs(cluster.tiles) do
     local k = keyOf(c[1], c[2])
     if support and (support.class == "wall" or support.class == "cliff"
-                    or support.art == "bookcase") then
+                    or support.art == "bookcase"
+                    or support.class == "building") then
       -- a figure drawn above a FULL-HEIGHT block (the gym statue on its
       -- plinth) is a statue on a pillar with ONE cell of footprint: the
       -- block below already carries the whole base, so the drawn cell
@@ -2174,8 +2827,14 @@ function Structures.buildObject(S, map, region, cluster,
       -- pilasters found this -- taking the furniture branch turned each
       -- statue's own two rows into a 32px box wearing the pilaster's art,
       -- so every one of them stood inside a slab of its own plinth.
+      -- A `building` support belongs here too: the template's stamped
+      -- model already carries every surface under the standee (that is
+      -- what its `support` height asserts), so a box here would stand
+      -- INSIDE the modelled tabletop.  Its stamp pre-painted the floor
+      -- under these tiles, which the `or` keeps when no flat tile
+      -- touches a cluster ringed by its own furniture.
       S.skip[k] = true
-      S.ground[k] = best
+      S.ground[k] = best or S.ground[k]
     elseif support then
       -- the claimed tile keeps rendering as the box the prop stands on,
       -- wearing the art its own ROW would have without the drawing (the
@@ -2204,7 +2863,167 @@ function Structures.buildObject(S, map, region, cluster,
   return true
 end
 
--- ---- figures: a person drawn INTO furniture, cut out and stood up ----
+-- ---- authored masks with a body ----
+
+-- One authored mask emitted as a per-pixel voxel slab in WORLD space --
+-- the treatment every solid standee in this file gets, driven by a hand
+-- drawn silhouette instead of a flood.
+--
+-- The caller owns placement entirely, because placement is the whole
+-- difference between the two things that use this: `x0` is the world x of
+-- the mask's west edge, `yOf(ly)` the world y a drawn row lands at, and
+-- `bandOf(ly)` its z span.  A bicycle hung on a wall keeps its drawn
+-- elevation and juts south of the band; a cash register stands on the
+-- counter's top plane and sits inside its own cell.
+--
+-- `bandOf` is per ROW rather than per object so one drawing can hold parts
+-- of different thickness (the register's receipt curl over its body).
+-- Where the band CHANGES between two stacked rows the lower row still gets
+-- its top face: without that the body would be open along the strip the
+-- thinner part does not cover, and you would see into the machine.
+--
+-- `omit` is a rect of the mask this pass does NOT extrude, because it is
+-- not a face at all -- maskPlate lays it flat instead.  It leaves the mask
+-- for good here, neighbours included, so the extrusion closes up around
+-- the notch exactly as if the drawing had never filled it.
+local function maskSlab(quads, m, perRow, atlasW, atlasH, x0, yOf, bandOf,
+                        yFloor, omit)
+  local bw, bh = m.w * 8, m.h * 8
+
+  local function at(lx, ly)
+    if lx < 0 or lx >= bw or ly < 0 or ly >= bh then return false end
+    if omit and lx >= omit.x0 and lx <= omit.x1
+       and ly >= omit.r0 and ly <= omit.r1 then return false end
+    return m.mask[ly * bw + lx] or false
+  end
+
+  for ly = 0, bh - 1 do
+    Budget.tick()
+    local z0, z1 = bandOf(ly)
+    local pz0, pz1 = bandOf(ly - 1)
+    local capped = (pz0 ~= z0 or pz1 ~= z1)
+    for lx = 0, bw - 1 do
+      if at(lx, ly) then
+        local tile = m.tiles[math.floor(ly / 8) * m.w
+                             + math.floor(lx / 8) + 1]
+        local u = ((tile % perRow) * 8 + lx % 8 + 0.5) / atlasW
+        local v = (math.floor(tile / perRow) * 8 + ly % 8 + 0.5) / atlasH
+        local x, y = x0 + lx, yOf(ly)
+        local function quad(c1, c2, c3, c4, shade)
+          quads[#quads + 1] = { c1, c2, c3, c4, u = u, v = v, shade = shade }
+        end
+        quad({ x, y, z1 }, { x + 1, y, z1 }, { x + 1, y + 1, z1 },
+             { x, y + 1, z1 }, OBJ_SHADE.front)
+        quad({ x + 1, y, z0 }, { x, y, z0 }, { x, y + 1, z0 },
+             { x + 1, y + 1, z0 }, OBJ_SHADE.back)
+        if capped or not at(lx, ly - 1) then
+          quad({ x, y + 1, z0 }, { x + 1, y + 1, z0 }, { x + 1, y + 1, z1 },
+               { x, y + 1, z1 }, OBJ_SHADE.top)
+        end
+        if y > yFloor and not at(lx, ly + 1) then
+          quad({ x, y, z1 }, { x + 1, y, z1 }, { x + 1, y, z0 },
+               { x, y, z0 }, OBJ_SHADE.bottom)
+        end
+        if not at(lx - 1, ly) then
+          quad({ x, y, z0 }, { x, y, z1 }, { x, y + 1, z1 },
+               { x, y + 1, z0 }, OBJ_SHADE.side)
+        end
+        if not at(lx + 1, ly) then
+          quad({ x + 1, y, z1 }, { x + 1, y, z0 }, { x + 1, y + 1, z0 },
+               { x + 1, y + 1, z1 }, OBJ_SHADE.side)
+        end
+      end
+    end
+  end
+end
+
+-- The other half of the same drawing: a rect of the mask that is a
+-- TOP-VIEW surface, laid HORIZONTAL instead of extruded.
+--
+-- This is the methodology's band classification at rect granularity, and
+-- the reason the register is not a box.  A GB cell packs several facings,
+-- and the register's keypad is drawn from ABOVE -- its keys lie on the
+-- machine's deck, sealed behind their own black border inside the outer
+-- silhouette.  Extruding it stands that surface on end and paints the keys
+-- up the machine's face, which is the extruded-picture failure exactly.
+--
+-- So the rect lands one voxel proud of what maskSlab left below it, at `y`,
+-- one voxel thick, filling the body's whole depth band (`z0`, `D`).
+--
+-- The rect STRETCHES over that band rather than laying its rows 1:1: it is
+-- the machine's whole deck, so it has to reach the machine's whole depth,
+-- and the alternative -- panel at the front, bare deck behind -- leaves a
+-- strip of the base band's top showing through where the keys should be.
+-- Sampled at the voxel's CENTRE, the same rule Stage 1 samples the atlas
+-- with, so a band scales by whole voxels and nothing blurs: at 8 rows over
+-- 12 voxels every second drawn row doubles.  The one place in the model
+-- where a texel is not 1:1 with a drawn pixel, and the reason `depth` is an
+-- authored number again.  No bottom faces: it rests on the box.
+local function maskPlate(quads, m, perRow, atlasW, atlasH, x0, r, y, z0, D)
+  local bw, bh = m.w * 8, m.h * 8
+  local rows = r.r1 - r.r0 + 1
+
+  -- depth voxel -> the drawn row it wears
+  local function rowAt(k)
+    if k < 0 or k >= D then return nil end
+    return r.r0 + math.min(rows - 1, math.floor((k + 0.5) * rows / D))
+  end
+
+  local function at(lx, k)
+    local ly = rowAt(k)
+    if not ly or lx < r.x0 or lx > r.x1 then return false end
+    return m.mask[ly * bw + lx] or false
+  end
+
+  -- The plate's rim, in the two directions the drawing treats differently.
+  -- ACROSS the rows the neighbour is the extrusion standing BESIDE the
+  -- notch (the register's display unit), which is tall and covers the
+  -- plate's edge, so that face must not be drawn twice.  ALONG them the
+  -- neighbour is the extrusion BELOW it (the base band, whose own front
+  -- face stops one voxel short), so the plate's front lip is exposed and
+  -- is the deck's own front edge.
+  local function beside(lx, ly)
+    if lx < 0 or lx >= bw or ly < 0 or ly >= bh then return false end
+    return m.mask[ly * bw + lx] or false
+  end
+
+  for k = 0, D - 1 do
+    Budget.tick()
+    local ly, z = rowAt(k), z0 + k
+    for lx = r.x0, r.x1 do
+      if at(lx, k) then
+        local tile = m.tiles[math.floor(ly / 8) * m.w
+                             + math.floor(lx / 8) + 1]
+        local u = ((tile % perRow) * 8 + lx % 8 + 0.5) / atlasW
+        local v = (math.floor(tile / perRow) * 8 + ly % 8 + 0.5) / atlasH
+        local x = x0 + lx
+        local function quad(c1, c2, c3, c4, shade)
+          quads[#quads + 1] = { c1, c2, c3, c4, u = u, v = v, shade = shade }
+        end
+        quad({ x, y + 1, z }, { x + 1, y + 1, z }, { x + 1, y + 1, z + 1 },
+             { x, y + 1, z + 1 }, OBJ_SHADE.top)
+        if not at(lx, k + 1) then
+          quad({ x, y, z + 1 }, { x + 1, y, z + 1 }, { x + 1, y + 1, z + 1 },
+               { x, y + 1, z + 1 }, OBJ_SHADE.front)
+        end
+        if not at(lx, k - 1) then
+          quad({ x + 1, y, z }, { x, y, z }, { x, y + 1, z },
+               { x + 1, y + 1, z }, OBJ_SHADE.back)
+        end
+        if not beside(lx - 1, ly) then
+          quad({ x, y, z }, { x, y, z + 1 }, { x, y + 1, z + 1 },
+               { x, y + 1, z }, OBJ_SHADE.side)
+        end
+        if not beside(lx + 1, ly) then
+          quad({ x + 1, y, z + 1 }, { x + 1, y, z }, { x + 1, y + 1, z },
+               { x + 1, y + 1, z + 1 }, OBJ_SHADE.side)
+        end
+      end
+    end
+  end
+end
+
+-- ---- figures: a thing drawn INTO furniture, cut out and stood up ----
 
 -- One authored figure at one matched position.
 --
@@ -2215,22 +3034,34 @@ end
 -- believe it.  Which also means figures build HEADLESS: unlike every
 -- other standee here, nothing below reads a pixel.
 --
--- A figure is a SPRITE, not a prop.  It gets exactly the treatment
--- SpriteBillboards gives a character: one flat plane of the drawing's own
--- pixels, no thickness, standing at its feet and leaned back by the
--- camera's pitch at draw time so it always reads face-on -- because that
--- is what the artwork is.  A seated man drawn face-on is a 2D icon like
--- every other Gen 1 figure; extruding him into a slab reconstructs a body
--- nobody drew (the ten-voxel version read as a wedge of furniture, and
--- even one voxel showed an edge the sprites never show).
+-- A PERSON is a SPRITE, not a prop, and an entry that states no `depth`
+-- gets exactly the treatment SpriteBillboards gives a character: one flat
+-- plane of the drawing's own pixels, no thickness, standing at its feet
+-- and leaned back by the camera's pitch at draw time so it always reads
+-- face-on -- because that is what the artwork is.  A seated man drawn
+-- face-on is a 2D icon like every other Gen 1 figure; extruding him into
+-- a slab reconstructs a body nobody drew (the ten-voxel version read as a
+-- wedge of furniture, and even one voxel showed an edge the sprites never
+-- show).
 --
--- So the quads are emitted in the card's OWN LOCAL SPACE -- x from the
+-- So the card's quads are emitted in its OWN LOCAL SPACE -- x from the
 -- mask's west edge, y from his feet, all at z = 0 -- and the placement
 -- (`wx`, `wz`, `y`) rides along for VoxelScene to build the lean matrix
 -- from.  One quad per pixel rather than one alpha-keyed texture: the
 -- tileset atlas has no alpha to key on, and per-pixel quads cut the exact
 -- same silhouette straight out of the live atlas, so every palette bake
 -- (SGB, RED++ per-tile groups, a mod's own art) textures him for free.
+--
+-- An entry that DOES state a `depth` is not a person, and takes the other
+-- branch: a per-pixel voxel slab in world space (maskSlab above), standing
+-- on the same furniture the card would have stood on.  The Marts' cash
+-- register is why -- a machine set down on a counter is a box seen from
+-- the front, and a card of it is the billboard failure the standee pools
+-- exist to avoid.  It keeps the card's anchoring exactly: its feet on the
+-- support's top plane, and its body in the 8px depth band of the tile row
+-- its lowest pixel is drawn in, which is where a character card would
+-- have pivoted.  So the machine sits at the FRONT of the counter cell it
+-- is drawn low in, and never leans into the aisle behind it.
 local function buildFigure(S, map, fig, tx, ty, perRow)
   local bw, bh = fig.w * 8, fig.h * 8
 
@@ -2273,34 +3104,76 @@ local function buildFigure(S, map, fig, tx, ty, perRow)
 
   local atlasW = map.tileset.imageWidth or 128
   local atlasH = map.tileset.imageHeight or 48
-  local quads = {}
-  for ly = 0, bh - 1 do
-    Budget.tick()
-    for lx = 0, bw - 1 do
-      if at(lx, ly) then
-        local tile = fig.tiles[math.floor(ly / 8) * fig.w
-                               + math.floor(lx / 8) + 1]
-        local u = ((tile % perRow) * 8 + lx % 8 + 0.5) / atlasW
-        local v = (math.floor(tile / perRow) * 8 + ly % 8 + 0.5) / atlasH
-        local x, y = lx - minX, lowY - ly
-        quads[#quads + 1] = { { x, y, 0 }, { x + 1, y, 0 },
-                              { x + 1, y + 1, 0 }, { x, y + 1, 0 },
-                              u = u, v = v, shade = 1 }
+
+  if fig.depth then
+    -- An OBJECT: the standee slab, standing on the FRONT edge of the tile
+    -- row its feet are drawn in -- the south face of the 8px band a
+    -- character card would have pivoted in.  It is anchored there and
+    -- grows NORTH rather than being centred, so that `depth` is free to
+    -- exceed the 8px band without the machine ever creeping toward the
+    -- aisle: a till drawn low on a counter is at the counter's front, and
+    -- a deeper one just eats more of the bare top behind it.  (At the
+    -- 8 the band itself is, the two rules agree.)
+    --
+    -- `thin` caps the top rows to their own thickness, centred in the
+    -- body's depth -- the register's receipt curl leaves the arm's top
+    -- face by a slot in the middle of it, not flush with its front.
+    local south = ty * 8 + math.floor(lowY / 8) * 8 + 8
+    local function bandOf(ly)
+      local z0 = south - fig.depth
+      if fig.thin and ly < fig.thin.rows then
+        local m = math.floor((fig.depth - fig.thin.depth) / 2)
+        return z0 + m, z0 + m + fig.thin.depth
+      end
+      return z0, south
+    end
+    local function yOf(ly) return baseY + lowY - ly end
+    maskSlab(S.objectQuads, fig, perRow, atlasW, atlasH, tx * 8,
+             yOf, bandOf, baseY, fig.flat)
+    if fig.flat then
+      -- The top-view rect lands on the plane its own BOTTOM row would
+      -- have stood at -- which is the top of whatever the extrusion left
+      -- under it (the register's base band), so the keys lie on the deck
+      -- and never float.
+      --
+      -- In depth it fills the body's whole band, STRETCHED to it: the rect
+      -- is the machine's deck, so it reaches as deep as the machine does,
+      -- and its last drawn row stays the deck's front edge directly over
+      -- the fascia below it -- an object drawn LOW on a surface is drawn
+      -- NEAR its front.
+      maskPlate(S.objectQuads, fig, perRow, atlasW, atlasH, tx * 8,
+                fig.flat, yOf(fig.flat.r1), south - fig.depth, fig.depth)
+    end
+  else
+    local quads = {}
+    for ly = 0, bh - 1 do
+      Budget.tick()
+      for lx = 0, bw - 1 do
+        if at(lx, ly) then
+          local tile = fig.tiles[math.floor(ly / 8) * fig.w
+                                 + math.floor(lx / 8) + 1]
+          local u = ((tile % perRow) * 8 + lx % 8 + 0.5) / atlasW
+          local v = (math.floor(tile / perRow) * 8 + ly % 8 + 0.5) / atlasH
+          local x, y = lx - minX, lowY - ly
+          quads[#quads + 1] = { { x, y, 0 }, { x + 1, y, 0 },
+                                { x + 1, y + 1, 0 }, { x, y + 1, 0 },
+                                u = u, v = v, shade = 1 }
+        end
       end
     end
-  end
 
-  -- Where the card stands.  `wz` is the MIDDLE of the tile row his feet are
-  -- drawn in, which is the same convention a character card uses (its feet
-  -- plane sits at its cell's middle) -- so he sorts against the couch and
-  -- against a player walking past exactly the way an NPC standing there
-  -- would.
-  S.figures[#S.figures + 1] = {
-    quads = quads,
-    wx = tx * 8 + minX,
-    wz = ty * 8 + math.floor(lowY / 8) * 8 + 4,
-    y = baseY,
-  }
+    -- Where the card stands.  `wz` is the MIDDLE of the tile row his feet
+    -- are drawn in, which is the same convention a character card uses
+    -- (its feet plane sits at its cell's middle) -- so he sorts against
+    -- the couch and against a player walking past exactly the way an NPC
+    -- standing there would.
+    S.figures[#S.figures + 1] = {
+      quads = quads,
+      wx = tx * 8 + minX,
+      wz = ty * 8 + math.floor(lowY / 8) * 8 + 4,
+      y = baseY,
+    }
+  end
 
   -- What each covered tile wears now that he is off it.  Only the ART
   -- changes: the couch tiles keep their `counter` box (they ARE the
@@ -2342,6 +3215,78 @@ function Structures.buildFigures(S, map, x0, x1, y0, y1)
   end
 end
 
+-- ---- mounted: a thing drawn INTO a wall band, stood proud of it ----
+
+-- One authored mounted object at one matched position.
+--
+-- Same authoring premise as a figure -- the mask IS the classification,
+-- because a drawing painted onto the wall it hangs on has no background
+-- margin for a flood to enter by, and here the wall's own #555 stripes
+-- are a flood boundary as well, so a silhouette comes back striped.
+-- Like a figure it therefore builds HEADLESS: nothing below reads a
+-- pixel.
+--
+-- But a mounted object is an OBJECT, so it is built the way every other
+-- standee here is -- a per-pixel voxel slab wearing the drawing's own
+-- texels, quads emitted in world space -- and not as a sprite card:
+--
+--   ELEVATION is the drawn one.  A figure stands on its own feet; this
+--   keeps the row it is painted in, because the band it is painted into
+--   is a measured 16px face rising off the floor.  So drawn row `ly`
+--   becomes world y = (band height - 1) - ly, and a bicycle whose wheels
+--   are drawn on the band's bottom row lands on the floor while one hung
+--   clear of it stays hung.
+--   DEPTH juts SOUTH of the band's own face (z0 at the drawing's south
+--   edge), so the object stands in front of the wall rather than inside
+--   it.  It overhangs the walkable cell in front, which is what a bicycle
+--   leaning on a wall does; nothing about collision changes.
+local function buildMountedAt(S, map, m, tx, ty, perRow)
+  local bh = m.h * 8
+  local z0 = (ty + m.h) * 8
+  local z1 = z0 + (m.depth or 2)
+
+  maskSlab(S.objectQuads, m, perRow, map.tileset.imageWidth or 128,
+           map.tileset.imageHeight or 48, tx * 8,
+           function(ly) return (bh - 1) - ly end,
+           function() return z0, z1 end, 0)
+
+  -- What the band wears now that the object is off it: the plain panel
+  -- the artist drew everywhere else along the same wall.  Only the ART
+  -- changes -- these tiles keep the `wall` box they always resolved to,
+  -- because they ARE the wall.
+  for i = 1, #m.tiles do
+    local dx, dy = (i - 1) % m.w, math.floor((i - 1) / m.w)
+    S.tileAt[keyOf(tx + dx, ty + dy)] = m.under[i]
+  end
+end
+
+-- Every authored mounted object, wherever the map draws it.  Matched by
+-- TILE PATTERN like a figure, and for the same reason -- one blockset
+-- entry can place the same drawing in several rooms -- and the repaint
+-- above replaces the pattern's own tiles, so a match never fires twice
+-- on one drawing.
+function Structures.buildMounted(S, map, x0, x1, y0, y1)
+  local list = TileShape.mounted(map.tileset.id)
+  if not list then return end
+  local perRow = map.tileset.tilesPerRow or 16
+  for _, m in ipairs(list) do
+    for ty = y0, y1 - m.h + 1 do
+      for tx = x0, x1 - m.w + 1 do
+        Budget.tick()
+        local hit = true
+        for i = 1, #m.tiles do
+          local dx, dy = (i - 1) % m.w, math.floor((i - 1) / m.w)
+          if S.tileAt[keyOf(tx + dx, ty + dy)] ~= m.tiles[i] then
+            hit = false
+            break
+          end
+        end
+        if hit then buildMountedAt(S, map, m, tx, ty, perRow) end
+      end
+    end
+  end
+end
+
 -- ---- tall grass ----
 
 -- A tall-grass CELL is four tufts: 2x2 tiles, and each 8x8 tile is one
diff --git a/lib/TileShape.lua b/lib/TileShape.lua
index 31e5916..0ea0ada 100644
--- a/lib/TileShape.lua
+++ b/lib/TileShape.lua
@@ -71,6 +71,21 @@ local FALLBACK_HEIGHTS = {
   -- body builds from the bark rows and the drawn ellipse projects onto
   -- the hull's round top
   stump = 16,
+  -- the same hull cut at both ends, hollowed and tapered: an OPEN bin
+  -- standing on a floor (the Vermilion Gym trash cans).  The drawn mouth
+  -- ellipse projects onto the round top and down the well, the drawn base
+  -- ellipse is ground contact rather than body, and the plan narrows toward
+  -- the floor.  Height is AUTHORED (the profile's can_height, which this
+  -- pin must be kept equal to so anything riding a can lands on its rim) --
+  -- the drawing's own straight run is only a couple of rows, because a GB
+  -- cell spends most of itself on the opening
+  can = 9,
+  -- round scenery drawn ONE cell wide and TWO cells TALL, standing on one
+  -- cell of plot: the Pokemon Centers' potted plants. Carved as one
+  -- 16x32x16 hull in the SOUTH (pot) cell -- the drawing's upper cell is
+  -- the object's height, not its depth. BOTH cells take the class; the
+  -- group build anchors on the north one (Structures.buildCylinders)
+  planter = 32,
   billboard = 16,
   signpost = 16,
   post = 16,
@@ -91,6 +106,10 @@ local FALLBACK_HEIGHTS = {
   desk = 24,
   prop = 16,
   cutout = 16,
+  -- a vehicle drawn SIDE-ON: the showroom bicycles.  Standee height like
+  -- every other cutout pool -- what differs is the thickness (see
+  -- Structures' PINNED_DEPTH)
+  bike = 16,
   console = 16,
   relief = 3,
   bookcase = 32,
@@ -127,6 +146,8 @@ local ART = {
   cylinder = "cylinder",
   canopy = "canopy",
   stump = "cylinder",
+  can = "cylinder",
+  planter = "planter",
   billboard = "billboard",
   -- signposts share the billboard treatment but as their own pool at a
   -- 2-voxel depth: a sign is a thin plate on a stick, and the standard
@@ -165,6 +186,13 @@ local ART = {
   desk = "upright",
   prop = "billboard",
   cutout = "billboard",
+  -- a bicycle is a LINE drawing seen side-on, and its negative space --
+  -- the air inside the frame, between the wheel and the fork -- is what
+  -- makes it read as a bicycle at all.  Its own pool at two voxels: any
+  -- thicker and the side faces of neighbouring strokes close those gaps
+  -- from every angle but dead-on, and six of them in a showroom come out
+  -- as one dark lump (which is what the 5px `prop` pool gave)
+  bike = "billboard",
   -- a machine standing on furniture: the billboard treatment with
   -- body, plus the one-object contract `cutout` has -- the drawing is
   -- ringed by the furniture it sits on, and those edges must not be
@@ -183,6 +211,7 @@ local ART = {
 local spec = nil          -- the loaded data file, or false when absent
 local cache = {}          -- tileset id -> resolved shape list
 local figCache = {}       -- tileset id -> parsed figure masks, or false
+local mntCache = {}       -- tileset id -> parsed mounted masks, or false
 local bgCache = {}        -- tileset id -> prop background shades, or false
 
 -- The shape profile ships with the mod (data/voxel_heights.lua) and is read
@@ -430,68 +459,158 @@ end
 -- pixel by pixel (see data/voxel_heights.lua):
 --
 --   figures = { { w      = <tiles across>,
+--                 depth  = <voxels of body; ABSENT for a person>,
+--                 thin   = { rows = <top rows>, depth = <voxels> },
+--                 flat   = { x = { <lx0>, <lx1> }, rows = { <r0>, <r1> } },
 --                 tiles  = { ...w*h tile ids, row-major... },
 --                 under  = { ...w*h ids: what each tile wears once the
 --                            figure is lifted off it... },
 --                 pixels = { ...h*8 strings of w*8 chars, "." = not the
 --                            figure... } } }
 --
--- No class: a figure is always a flat sprite card, drawn the way
--- SpriteBillboards draws a character (see Structures.buildFigures).
+-- No class -- what the entry carries instead is a `depth`, or does not:
+--
+--   WITHOUT one it is a flat sprite card, drawn the way SpriteBillboards
+--   draws a character.  That is the right reading for a PERSON: a Gen 1
+--   figure is a face-on 2D icon, and extruding one reconstructs a body
+--   nobody drew (see Structures.buildFigures).
+--   WITH one it is an OBJECT and gets the standee treatment every other
+--   solid here gets -- a per-pixel slab in world space, standing on the
+--   same furniture the card would have stood on.  The Marts' cash
+--   register is the case: a machine on a counter is a box, not an icon.
+--
+-- Two fields say which parts of such a drawing are NOT the extrusion,
+-- because a solid drawn in one 16x16 GB cell still packs more than one
+-- facing:
+--
+--   `thin` caps the thickness over the mask's top rows, for the part of
+--   the drawing that is not the machine (the register's receipt curl).
+--   `flat` names a rect of the mask that is a TOP-VIEW surface rather
+--   than a face -- the register's keypad, whose keys lie ON its deck.
+--   The rect lays horizontal one voxel proud of whatever the extrusion
+--   leaves below it, at the elevation its BOTTOM row would have had,
+--   with drawn row = depth row 1:1 (the mapping the lab tabletop is
+--   drawn with).  So a drawing whose front elevation is an L reads as
+--   one: body up the side and along the base, keys lying in the notch.
 --
 -- Returned normalized: `mask` as a set keyed by ly * (w * 8) + lx, so
 -- Structures can read it as a bitmap without re-parsing per position.
 -- A malformed entry is dropped rather than half-applied -- a typo in a
 -- mask should leave the couch alone, not carve a hole in it.
-function TileShape.figures(tilesetId)
-  local hit = figCache[tilesetId]
-  if hit ~= nil then return hit or nil end
-
-  local s = load()
-  local entry = s and s.tilesets and s.tilesets[tilesetId]
-  local list = entry and entry.figures
+--
+-- `mounted` (below) carries the same four fields, so the parse is shared,
+-- and so are the optional ones that give an authored mask a BODY: `depth`,
+-- `thin` and `flat` above.  `depth` is left nil when unstated, because
+-- absence is meaningful on a figure: no depth means the flat sprite card a
+-- person is drawn as.
+local function authoredMasks(list)
   local out = {}
-  if type(list) == "table" then
-    for _, f in ipairs(list) do
-      local ok = type(f) == "table" and type(f.w) == "number"
-                 and type(f.tiles) == "table" and type(f.under) == "table"
-                 and type(f.pixels) == "table"
-      local w = ok and math.floor(f.w) or 0
-      local h = (w >= 1) and (#f.tiles / w) or 0
-      ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
-           and #f.under == #f.tiles and #f.pixels == h * 8
-      if ok then
-        for i = 1, h * 8 do
-          local row = f.pixels[i]
-          if type(row) ~= "string" or #row ~= w * 8 then
-            ok = false
-            break
-          end
+  if type(list) ~= "table" then return out end
+  for _, f in ipairs(list) do
+    local ok = type(f) == "table" and type(f.w) == "number"
+               and type(f.tiles) == "table" and type(f.under) == "table"
+               and type(f.pixels) == "table"
+    local w = ok and math.floor(f.w) or 0
+    local h = (w >= 1) and (#f.tiles / w) or 0
+    ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
+         and #f.under == #f.tiles and #f.pixels == h * 8
+    if ok then
+      for i = 1, h * 8 do
+        local row = f.pixels[i]
+        if type(row) ~= "string" or #row ~= w * 8 then
+          ok = false
+          break
         end
       end
-      if ok then
-        local mask, n = {}, 0
-        for ly = 0, h * 8 - 1 do
-          local row = f.pixels[ly + 1]
-          for lx = 0, w * 8 - 1 do
-            if row:sub(lx + 1, lx + 1) ~= "." then
-              mask[ly * (w * 8) + lx] = true
-              n = n + 1
-            end
+    end
+    if ok then
+      local mask, n = {}, 0
+      for ly = 0, h * 8 - 1 do
+        local row = f.pixels[ly + 1]
+        for lx = 0, w * 8 - 1 do
+          if row:sub(lx + 1, lx + 1) ~= "." then
+            mask[ly * (w * 8) + lx] = true
+            n = n + 1
           end
         end
-        if n > 0 then
-          out[#out + 1] = { w = w, h = h, n = n, mask = mask,
-                            tiles = f.tiles, under = f.under }
-        end
+      end
+      local depth = tonumber(f.depth)
+      local thin = nil
+      if type(f.thin) == "table" and tonumber(f.thin.rows)
+         and tonumber(f.thin.depth) then
+        thin = { rows = math.floor(tonumber(f.thin.rows)),
+                 depth = math.floor(tonumber(f.thin.depth)) }
+      end
+      local flat = nil
+      if type(f.flat) == "table" and type(f.flat.x) == "table"
+         and type(f.flat.rows) == "table" then
+        flat = { x0 = math.floor(f.flat.x[1]), x1 = math.floor(f.flat.x[2]),
+                 r0 = math.floor(f.flat.rows[1]),
+                 r1 = math.floor(f.flat.rows[2]) }
+      end
+      if n > 0 then
+        out[#out + 1] = { w = w, h = h, n = n, mask = mask,
+                          tiles = f.tiles, under = f.under,
+                          depth = depth and math.floor(depth) or nil,
+                          thin = thin, flat = flat }
       end
     end
   end
+  return out
+end
+
+function TileShape.figures(tilesetId)
+  local hit = figCache[tilesetId]
+  if hit ~= nil then return hit or nil end
+
+  local s = load()
+  local entry = s and s.tilesets and s.tilesets[tilesetId]
+  local out = authoredMasks(entry and entry.figures)
 
   figCache[tilesetId] = (#out > 0) and out or false
   return figCache[tilesetId] or nil
 end
 
+-- Hand-authored MOUNTED objects for one tileset: a thing drawn INTO the
+-- wall band it hangs on, cut out by an explicit pixel mask and stood
+-- proud of the wall's face.
+--
+-- Same authoring problem as `figures` and the same answer -- a class pin
+-- resolves a whole 8x8 tile, and the detector cannot segment a drawing
+-- that has no background margin to flood from.  The Bike Shop's two wall
+-- bicycles are the case: the shop's striped wall panel runs BEHIND them,
+-- and its #555 stripes are a flood boundary, so a silhouette flood comes
+-- back with the stripes attached to the bike.
+--
+-- Two things differ from a figure, and both follow from the object being
+-- an object rather than a character:
+--
+--   it keeps its DRAWN ELEVATION.  A figure stands on its own feet; a
+--   mounted thing sits where the wall band draws it, so a bicycle hung
+--   clear of the floor stays hung.
+--   it has THICKNESS (`depth`, default 2), and it is built in world
+--   space as a per-pixel slab jutting south of the band -- not as a
+--   camera-facing sprite card.  A bicycle drawn side-on is a plane
+--   parallel to the wall, not a face-on icon.
+--
+--   mounted = { { w      = <tiles across>,
+--                 depth  = <voxels it juts into the room>,
+--                 tiles  = { ...w*h tile ids, row-major... },
+--                 under  = { ...w*h ids: what each tile wears once the
+--                            object is lifted off it (the plain panel)... },
+--                 pixels = { ...h*8 strings of w*8 chars, "." = wall... } } }
+function TileShape.mounted(tilesetId)
+  local hit = mntCache[tilesetId]
+  if hit ~= nil then return hit or nil end
+
+  local s = load()
+  local entry = s and s.tilesets and s.tilesets[tilesetId]
+  local out = authoredMasks(entry and entry.mounted)
+
+  mntCache[tilesetId] = (#out > 0) and out or false
+  return mntCache[tilesetId] or nil
+end
+
 -- Which GB shades count as BACKGROUND for a pinned per-pixel prop, per tile
 -- (a tileset entry's prop_bg). Returns tile id -> set of shade names, or nil.
 --
@@ -564,12 +683,30 @@ function TileShape.bookcaseBackfill(tilesetId)
   return mode == "above" and mode or nil
 end
 
+--- Does this tileset's `bookcase` run carry the measured pane RELIEF on
+--- its front (a tileset entry's bookcase_relief)?  Default yes: the class
+--- almost always collapses a shelf, a rack or a display case, and every
+--- one of those seals its contents behind a frame that should stand proud
+--- of them.
+---
+--- A tileset says `bookcase_relief = false` when it borrows the collapse
+--- for something that is NOT a shelf -- the League's gate walls and
+--- pilasters, Bill's transporter drums -- where the drawing's light
+--- regions are the masonry and the barrel, not panes, and sinking them
+--- carves the surface instead of describing it.
+function TileShape.bookcaseRelief(tilesetId)
+  local s = load()
+  local entry = s and s.tilesets and s.tilesets[tilesetId]
+  return not (entry and entry.bookcase_relief == false)
+end
+
 -- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
 -- record needs the next lookup to re-resolve (hot reload, mod toggle).
 function TileShape.invalidate()
   spec = nil
   cache = {}
   figCache = {}
+  mntCache = {}
   bgCache = {}
 end
 
diff --git a/lib/Water.lua b/lib/Water.lua
index db1db8e..4ab4022 100644
--- a/lib/Water.lua
+++ b/lib/Water.lua
@@ -381,9 +381,32 @@ Water.EDGE_FADE = 0.14         -- reflection eased off over this much of the fra
 --
 -- The scene shader's own vertex path, plus the world position the geometry
 -- was actually DRAWN at -- after the world curve, because that is the space
--- the depth buffer holds and therefore the space the march has to walk in.
--- (The curve only ever moves Y, so a fragment's world XZ is the same on both
--- sides of it and the ripple can be measured off this one too.)
+-- the surface the eye MEETS lives in: which wave column a screen pixel is
+-- looking at is a question about the geometry as drawn, and relief() answers
+-- it there. (The curve only ever moves Y, so a fragment's world XZ is the
+-- same on both sides of it and the ripple can be measured off this one too.)
+--
+-- WHAT IT REFLECTS is worked out on the other side of the bend, in the FLAT
+-- world, and this is the same rule the rest of the mode keeps: the curve
+-- tips the world away and the things standing on it do not lean with it (see
+-- WorldCurve -- buildings stay upright, shadows are resolved before the bend
+-- and ride along). A lake is one of those things. Reflect off the bowl the
+-- bend has made instead and the far half of a pond is a mirror tilted twenty
+-- degrees: it throws the ray past the vertical, where the sky ramp's own
+-- measure -- a screen row, through the frame's matrix -- swings from one end
+-- of the ramp to the other across a single column, and the pond comes out
+-- with hard-edged patches of the wrong sky stamped into it -- the overhead
+-- band and the horizon band abutting in the middle of a lake, which reads as
+-- something other than water showing through. The same tilt sends the
+-- screen-space march grazing along the bank instead of over it, which is the
+-- other half: the dock and the roofs smeared across the harbour.
+--
+-- So the reflection is taken with the flat view ray about the flat normal,
+-- exactly as it would be with the curve off -- and the MARCH still has to
+-- walk the world as drawn, because that is what the depth buffer holds. Both
+-- at once: the ray is straight in the flat world, and project() bends each
+-- sample on its way to the screen, which is the same displacement the vertex
+-- stage applies and therefore lands in the same place the geometry did.
 local SHADER_SRC = [[
 varying float vShade;
 varying vec3 vSun;
@@ -435,6 +458,20 @@ uniform vec3 eye;
 uniform vec2 screen;         // the canvas, in pixels
 uniform float cell;          // one diorama pixel, in canvas pixels
 uniform float pxAngle;       // radians of view one screen pixel subtends
+// The same bend the vertex stage applied. This stage has to undo it to get
+// back to the flat world it reflects in, and re-apply it on every marched
+// sample to get back to the screen. Declared in both stages, like `vp`, and
+// both are highp here.
+uniform vec3 curve;          // xy = the focus in world XZ, z = k; 0 = off
+
+// How far the bend has pushed the world down at world XZ `q` -- the vertex
+// stage's own displacement, as a number this stage can add and subtract.
+// Zero when the curve is off, which is the shader's "skip it" everywhere.
+float bendDrop(vec2 q) {
+  if (curve.z <= 0.0) return 0.0;
+  vec2 d = q - curve.xy;
+  return dot(d, d) * curve.z;
+}
 
 // the sun's own pass, exactly as the scene shader reads it
 uniform Image sunMap;
@@ -658,10 +695,14 @@ vec3 bodyAt(vec3 d, vec3 c, float parity) {
 // clip-space Y flip is already baked into `vp`, and a canvas texture's v runs
 // the same way its pixel rows do, so one 0.5x+0.5 answers for both.
 //
-// This is why the march walks in the world as DRAWN rather than as authored:
-// the depth buffer holds the curved world, so a straight line in that space
-// is the ray, and a straight line in the flat one would bend through it.
+// The point arrives in the FLAT world -- the space the ray is straight in --
+// and is bent here, by the same displacement the vertex stage applied, so it
+// lands exactly where the geometry it is being compared against landed. That
+// split is the whole trick: the reflection is worked out in a world that has
+// not been tipped, and every sample of it is tipped on the way to the screen,
+// so the march reads the depth buffer it actually has.
 vec4 project(vec3 p) {
+  p.y -= bendDrop(p.xz);
   vec4 c = vp * vec4(p, 1.0);
   if (c.w <= 1e-6) return vec4(0.0, 0.0, 0.0, 0.0);
   return vec4(c.xy / c.w * 0.5 + 0.5, c.z / c.w * 0.5 + 0.5, 1.0);
@@ -925,12 +966,23 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
   // drawn at, with no smooth shading anywhere across it. (The depth test
   // above is the one thing that stays per fragment: that is the hardware's
   // own question and it is asked in screen space.)
-  vec3 view = normalize(vBent - eye);
+  //
+  // Answered on the FLAT sheet, which is where the bars are a slab of even
+  // thickness over a level plane -- the one thing relief() is built on. The
+  // bend translates every bar straight down by its own column's drop, so the
+  // field keeps its shape and only its height moves; undo that here and the
+  // walk is the walk it was written for. Try it in the world as DRAWN
+  // instead and the slab is a bowl: the backward step up the ray climbs the
+  // bowl's near side as fast as it climbs out of the water, the walk starts
+  // inside the sheet, and it hands back a column a pixel or three off -- per
+  // fragment, differently, which is a patch of noise rather than parallax.
+  vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z);
+  vec3 view = normalize(sheet - eye);
   vec3 hit;
   vec2 col;
   float face;
   float axis;
-  relief(vBent, view, hit, col, face, axis);
+  relief(sheet, view, hit, col, face, axis);
   // and the bar's centre, so a column is sampled and reflected from one
   // place rather than from wherever inside it the fragment happened to land
   vec3 surf = vec3(col.x + 0.5, hit.y, col.y + 0.5);
@@ -1001,7 +1053,7 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
   vec3 rgb = mix(base, refl, clamp(f, 0.0, 1.0));
 
 #ifdef VOXEL_GRID
-  rgb *= 1.0 - gridDark * columnSeam(hit, vBent, axis);
+  rgb *= 1.0 - gridDark * columnSeam(hit, sheet, axis);
 #endif
   return vec4(rgb, 1.0) * color;
 }
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 0ca9283..2fb9094 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -1800,9 +1800,21 @@ T.check(plain:find("waveNormal(vec2 q, float tilt)", 1, true) ~= nil
         and plain:find("waveNormal(col,", 1, true) ~= nil,
   "still one answer per column, so the surface stays pixel-quantised in "
   .. "space while the value it reflects with is continuous")
-T.check(plain:find("relief(vBent, view, hit, col, face, axis)", 1, true) ~= nil,
+T.check(plain:find("relief(sheet, view, hit, col, face, axis)", 1, true) ~= nil,
   "and the visible column is found by walking the view ray through the "
   .. "slab, which is what makes a tall bar hide the short ones behind it")
+-- ...over the FLAT sheet, which is the one thing that walk is built on: an
+-- even slab over a level plane. The world curve drops each bar straight down
+-- by its own column's drop, so undoing that drop hands relief() the field it
+-- was written for. Walked in the world as DRAWN instead, the slab is a bowl:
+-- the backward step up the ray climbs the bowl's near side as fast as it
+-- climbs out of the water, the walk starts inside the sheet, and it returns a
+-- column a pixel or three off -- differently per fragment, which is a
+-- hard-edged patch of noise in the middle of a pond.
+T.check(plain:find("vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z)",
+                   1, true) ~= nil,
+  "and it walks the sheet the mesh was AUTHORED as, the bend taken back off, "
+  .. "because a slab walk over a bowl starts inside the water")
 -- the march's reach grows as one over the ray's descent, so a grazing camera
 -- asks for hundreds of world pixels of it from a fixed number of samples --
 -- which stepped over whole crests and smeared the surface into streaks
@@ -1830,7 +1842,7 @@ T.check(plain:find("waveUV(tc, col)", 1, true) ~= nil,
   "and the column is what is handed to it")
 
 -- the wireframe is ruled on the COLUMNS, not on the flat sheet they stand on
-T.check(gridded:find("columnSeam(hit, vBent, axis)", 1, true) ~= nil,
+T.check(gridded:find("columnSeam(hit, sheet, axis)", 1, true) ~= nil,
   "with V-GRID on, the seams outline the column the ray landed on -- every "
   .. "voxel of water its own block -- rather than ruling a grid across the "
   .. "flat quad underneath and ignoring the bars entirely")
@@ -1839,6 +1851,25 @@ T.check(gridded:find("vec3 w = fwidth(base);", 1, true) ~= nil,
   .. "between neighbouring fragments and its own derivative is a step")
 T.check(plain:find("march(surf, r)", 1, true) ~= nil,
   "the reflection marches from that column, not from the raw fragment")
+-- The two halves of the world curve, and they pull opposite ways. WHAT the
+-- lake reflects is worked out FLAT -- the same rule the rest of the mode
+-- keeps, that the world tips away and the things standing on it do not lean
+-- with it. Reflect off the bowl the bend has made instead and the far half
+-- of a pond is a mirror tilted twenty degrees, throwing the ray past the
+-- vertical, where the sky ramp's own measure (a screen row, through the
+-- frame's matrix) swings from one end of the ramp to the other across a
+-- single column and stamps hard-edged patches of the wrong sky into the
+-- water.  But WHERE it lands has to be found in the world as DRAWN, because
+-- that is what the depth buffer holds -- so the ray stays straight in the
+-- flat world and every sample of it is bent on the way to the screen, by the
+-- vertex stage's own displacement.
+T.check(plain:find("p.y -= bendDrop(p.xz);", 1, true) ~= nil,
+  "and every marched sample is bent into the world as DRAWN before it is "
+  .. "projected, because that is the world the depth buffer holds")
+T.check(plain:find("vec3 r = reflect(view, n);", 1, true) ~= nil
+        and plain:find("reflect(view, vec3(0.0, 1.0, 0.0))", 1, true) ~= nil,
+  "while the reflection itself is taken about the FLAT normal, so a curved "
+  .. "world does not tip the lake the way it does not lean the buildings")
 T.check(plain:find("mod(col.x + col.y, 2.0)", 1, true) ~= nil,
   "and the dither's checkerboard is cut from the columns too, so a camera "
   .. "pan slides the world through nothing")
diff --git a/tests/voxel_figure_test.lua b/tests/voxel_figure_test.lua
index 6f8e529..66e193e 100644
--- a/tests/voxel_figure_test.lua
+++ b/tests/voxel_figure_test.lua
@@ -214,6 +214,172 @@ Structures.buildFigures(twice, map, 0, 3, 8, 11)
 T.eq(#twice.figures, 1,
   "the repaint replaces the pattern, so a rescan cannot match it again")
 
+-- ------- a figure with a DEPTH is an object, not a card
+--
+-- The Marts' cash register: the same authored-mask escape, but a machine
+-- set down on a counter is a box seen from the front rather than a
+-- face-on icon, so it builds as a per-pixel solid.  Driven over a
+-- synthetic copy of the counter's east arm, as all nine maps on the MART
+-- id draw it at cell (1,5):
+--
+--     y=9    16 41      the work surface north of it
+--     y=10   14 15      the register: keypad and receipt curl
+--     y=11   30 31
+--     y=12   16 41      the work surface it stands on
+
+T.check(TileShape.figures("POKECENTER")[1].depth == nil,
+  "the seated man states no depth -- he stays a flat sprite card")
+
+local regs = TileShape.figures("MART")
+T.check(type(regs) == "table" and #regs == 1,
+  "MART carries exactly one figure")
+local reg = regs[1]
+T.eq(reg.w, 2, "the register is two tiles across")
+T.eq(reg.h, 2, "and two tall")
+T.eq(reg.n, 150, "the mask claims 150 pixels of the 256 it spans")
+T.eq(reg.depth, 12, "its body is 12 voxels deep -- three quarters of the cell")
+T.check(reg.thin and reg.thin.rows == 4 and reg.thin.depth == 2,
+  "the four rows above its drawn top edge are 2-voxel paper")
+T.check(reg.flat and reg.flat.x0 == 2 and reg.flat.x1 == 8
+        and reg.flat.r0 == 4 and reg.flat.r1 == 11,
+  "and the keypad is a TOP-VIEW rect, not a face")
+
+local MART_ROWS = { [9] = { 16, 41 }, [10] = { 14, 15 },
+                    [11] = { 30, 31 }, [12] = { 16, 41 } }
+local martS = { shapeAt = {}, tileAt = {}, figures = {}, skip = {},
+                ground = {}, runs = {}, objectQuads = {} }
+for ty, row in pairs(MART_ROWS) do
+  for i, tile in ipairs(row) do
+    martS.tileAt[keyOf(1 + i, ty)] = tile
+    martS.shapeAt[keyOf(1 + i, ty)] = COUNTER
+  end
+end
+local martMap = {
+  tileset = { id = "MART", tilesPerRow = 16,
+              imageWidth = 128, imageHeight = 48 },
+  isWalkableCell = function() return false end,
+}
+Structures.buildFigures(martS, martMap, 2, 3, 9, 12)
+
+T.eq(#martS.figures, 0, "no card was built -- it is a solid")
+T.eq(#martS.objectQuads, 351,
+  "and it landed in the standee channel as 351 quads")
+T.eq(martS.tileAt[keyOf(2, 10)], 16,
+  "its tiles wear the plain work surface now")
+T.eq(martS.tileAt[keyOf(3, 11)], 41, "all four of them")
+T.eq(martS.shapeAt[keyOf(2, 10)].class, "counter",
+  "and keep the counter box the machine stands on")
+
+local rx0, rx1, ry0, ry1, rz0, rz1
+for _, q in ipairs(martS.objectQuads) do
+  for c = 1, 4 do
+    local p = q[c]
+    rx0 = math.min(rx0 or p[1], p[1]); rx1 = math.max(rx1 or p[1], p[1])
+    ry0 = math.min(ry0 or p[2], p[2]); ry1 = math.max(ry1 or p[2], p[2])
+    rz0 = math.min(rz0 or p[3], p[3]); rz1 = math.max(rz1 or p[3], p[3])
+  end
+end
+T.eq(ry0, 8, "it stands ON the counter's 8px top plane, not the floor")
+T.eq(ry1, 24, "and is its drawn 16px tall")
+T.eq(rx0, 18, "west edge at the mask's column 2")
+T.eq(rx1, 30, "east edge at column 13, inside its own cell (16..32)")
+T.eq(rz1, 96, "its FRONT is the cell's own front edge, where it is drawn")
+T.eq(rz0, 84, "and it grows north from there, 4 short of the cell's back")
+
+-- the two thicknesses: the body at 8, the receipt curl at 2, the curl
+-- centred in the body's own band rather than flush with its front
+local bands = {}
+for _, q in ipairs(martS.objectQuads) do
+  for c = 1, 4 do bands[q[c][3]] = true end
+end
+for _, z in ipairs({ 84, 89, 91, 96 }) do
+  T.check(bands[z], "the model has a face at z = " .. z)
+end
+local curl = {}
+for _, q in ipairs(martS.objectQuads) do
+  local lo = math.min(q[1][2], q[2][2], q[3][2], q[4][2])
+  if lo >= 21 then for c = 1, 4 do curl[q[c][3]] = true end end
+end
+T.check(curl[89] and curl[91] and not curl[84] and not curl[96],
+  "clear of the arm's top only the 2-voxel paper band exists")
+
+-- THE L.  The base band (drawn rows 12-15) stands 4 above the counter and
+-- the keypad lies on it as a horizontal plate, so the whole machine is
+-- exactly three surfaces: a foot, an arm, and a deck in the notch.
+local plate, deckTop = {}, 0
+for _, q in ipairs(martS.objectQuads) do
+  local flatQuad = q[1][2] == q[2][2] and q[2][2] == q[3][2]
+                   and q[3][2] == q[4][2]
+  if flatQuad and q[1][2] == 13 then
+    plate[#plate + 1] = q
+  elseif flatQuad and q[1][2] == 12 then
+    deckTop = deckTop + 1
+  end
+end
+T.eq(#plate, 83,
+  "the keypad lies FLAT: one top quad per masked voxel of the deck")
+T.eq(deckTop, 7,
+  "on the base band's own top, which is 4 voxels up (drawn rows 12-15)")
+local dz0, dz1
+for _, q in ipairs(martS.objectQuads) do
+  if q[1][2] == 12 and q[3][2] == 12 then
+    for c = 1, 4 do
+      dz0 = math.min(dz0 or q[c][3], q[c][3])
+      dz1 = math.max(dz1 or q[c][3], q[c][3])
+    end
+  end
+end
+T.eq(dz0, 84, "and that deck runs the body's whole depth")
+T.eq(dz1, 96, "-- plain behind the panel, covered by it in front")
+
+local px0, px1, pz0, pz1
+for _, q in ipairs(plate) do
+  for c = 1, 4 do
+    px0 = math.min(px0 or q[c][1], q[c][1]); px1 = math.max(px1 or q[c][1], q[c][1])
+    pz0 = math.min(pz0 or q[c][3], q[c][3]); pz1 = math.max(pz1 or q[c][3], q[c][3])
+  end
+end
+T.eq(px0, 18, "the deck spans the mask's columns 2..8")
+T.eq(px1, 25, "-- the keypad panel and its own black rim")
+T.eq(pz1, 96, "the deck reaches the body's front edge")
+T.eq(pz0, 84, "and its back -- 8 drawn rows STRETCHED over 12 voxels")
+
+-- the stretch is by whole voxels, centre-sampled: 8 drawn rows over 12
+-- voxels of deck doubles every second one and blurs nothing
+local perRow16, atlasH16 = 16, 48
+local depthRow = {}
+for _, q in ipairs(plate) do
+  local z = math.min(q[1][3], q[2][3], q[3][3], q[4][3])
+  depthRow[z] = math.floor(q.v * atlasH16)
+end
+local seen = {}
+for z = 84, 95 do
+  T.check(depthRow[z] ~= nil, "deck voxel at z = " .. z .. " wears a texel")
+  seen[depthRow[z]] = (seen[depthRow[z]] or 0) + 1
+end
+T.eq(depthRow[95], 11, "the front voxel wears the keypad's own bottom rim")
+T.eq(depthRow[84], 4, "the back one wears its top rim")
+local doubled = 0
+for _, n in pairs(seen) do
+  T.check(n == 1 or n == 2, "no drawn row spreads over more than two voxels")
+  if n == 2 then doubled = doubled + 1 end
+end
+T.eq(doubled, 4, "exactly four of the eight rows double -- 8 into 12")
+
+
+-- and the arm still stands its drawn 8 rows above that deck, carrying
+-- the paper: nothing in the notch reaches higher than the plate
+local armTop, notchTop = 0, 0
+for _, q in ipairs(martS.objectQuads) do
+  for c = 1, 4 do
+    if q[c][1] >= 25 then armTop = math.max(armTop, q[c][2])
+    elseif q[c][1] <= 24 then notchTop = math.max(notchTop, q[c][2]) end
+  end
+end
+T.eq(armTop, 24, "the arm and its receipt curl reach the drawn 16px")
+T.eq(notchTop, 23,
+  "and west of it only the keys (13) and the paper overhanging them")
+
 -- ------- prop_bg: the shades a pinned prop treats as background
 --
 -- The potted plants needed this: their pot's olive base is drawn flush on
diff --git a/tools/building_voxels.py b/tools/building_voxels.py
index 1ca5fd2..a73fa9d 100644
--- a/tools/building_voxels.py
+++ b/tools/building_voxels.py
@@ -25,6 +25,7 @@ TILESETS = os.path.join(ROOT, "assets/generated/tilesets")
 PER_ROW = 16
 
 WHITE, GREY, DARK, BLACK = 0, 1, 2, 3          # by luminance, light first
+SHADE_NAMES = dict(white=WHITE, grey=GREY, dark=DARK, black=BLACK)
 
 # --- the shape profile, mirroring data/voxel_heights.lua's `buildings` ------
 TEMPLATES = {
@@ -614,6 +615,156 @@ 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,
     ),
+    # F05: the tall display cabinet, the one with the trophy behind its
+    # glass -- CELADON_CHIEF_HOUSE cells 3,0 and 4,0 and
+    # CELADON_MANSION_1F cell 2,2. Its 32 rows read like every band
+    # table's: 0-8 the cabinet top seen from above (black rim, white
+    # highlight along the north and west, grey field, the front corner
+    # shaded at row 8), row 9 the top's own black front edge -- so
+    # slab = 1 and that row folds into the rim treatment -- and 10-31
+    # the front face: the trophy in its dark display recess, the
+    # nameplate under it, then the two panelled doors of the base.
+    # Nine drawn top rows over a 32px plot, so the rims map 1:1 (the
+    # drawn front-corner shading lands one voxel behind the front edge)
+    # and the uniform field cycles between. Both cells of the plot are
+    # blocked and both are cabinet drawing, so D is the whole grid --
+    # the rank is built into the room's north wall.
+    "mansion_trophy_case": dict(
+        tiles=[
+            [38, 41],
+            [40, 21],
+            [56, 87],
+            [50, 51],
+        ],
+        roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7),
+        slab=1, front_eave=0, ledge=None, tileset="mansion",
+    ),
+    # F06: the short book case standing beside it -- the same cabinet on
+    # a grid one tile row shorter (CELADON_CHIEF_HOUSE cells 2,0 and
+    # 5,0, CELADON_MANSION_1F tiles 2,5 and 6,5). Identical top band and
+    # identical base; only the display opening is shorter, a shelf of
+    # books instead of the trophy. Same band numbers as F05, which is
+    # what makes the two read as one line of furniture: 15 voxels tall
+    # against the tall one's 23. The 8px of wall behind it is not part
+    # of the drawing and keeps its own `wall` pin.
+    "mansion_bookcase": dict(
+        tiles=[
+            [38, 41],
+            [34, 35],
+            [50, 51],
+        ],
+        roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7),
+        slab=1, front_eave=0, ledge=None, tileset="mansion",
+    ),
+    # F07: the long table in the middle of the chief's house
+    # (CELADON_CHIEF_HOUSE cell 2,3), the lab table's read at four
+    # cells wide and two deep. Rows 0-23 are the tabletop seen from
+    # above; 24-26 are the top slab's own front edge, black/#555/black
+    # -- exactly what the rim treatment paints -- and row 27 is the
+    # #555 shadow that closes it, so slab = 3 and rows 24-27 fold into
+    # the roof band instead of extruding under it. That leaves rows
+    # 28-30 as the base, the same three the lab table's is, and the two
+    # tables stand the same 6 voxels: row 28 the apron running the whole
+    # width, 29-30 the end legs under it. The legs stop one row short of
+    # the grid, so the measured ground line lands there and the model
+    # does not float. Both cell rows of the plot are blocked, so D is
+    # the whole grid: 24 drawn top rows map 1:1 from the north rim and
+    # the field's own rows cycle for the last 8.
+    "mansion_long_table": dict(
+        tiles=[
+            [38, 39, 39, 39, 39, 39, 39, 41],
+            [54, 55, 55, 55, 55, 55, 55, 57],
+            [54, 55, 55, 55, 55, 55, 55, 57],
+            [60, 58, 58, 58, 58, 58, 58, 59],
+        ],
+        roof_rows=28, roof_back=24, roof_front=0, roof_cycle=(2, 23),
+        slab=3, front_eave=0, ledge=None, tileset="mansion",
+    ),
+    # F08: the dining table of the generic town house -- 18 placements,
+    # every home's cells (3,3):(4,4) -- the chief's long table (F07) at
+    # two cells wide. The same read to the row: 0-23 the rounded
+    # tabletop seen from above (black rim, white highlight course, grey
+    # field, #555 east rim), 24-26 the slab's own black/#555/black
+    # front edge and 27 the #555 apron shadow that closes it, folded
+    # into the band, then 28-30 the base -- the apron's black underrun
+    # and the corner legs, stopping one row short of the grid so the
+    # measured ground line keeps the model on its plot. The same 6
+    # voxels the whole table family stands.
+    "house_table": dict(
+        tiles=[
+            [38, 39, 39, 41],
+            [54, 47, 47, 57],
+            [54, 47, 47, 57],
+            [60, 58, 58, 59],
+        ],
+        roof_rows=28, roof_back=24, roof_front=0, roof_cycle=(2, 23),
+        slab=3, front_eave=0, ledge=None, tileset="house",
+    ),
+    # F08 again: Red's and the Copycat's ground-floor dining table --
+    # the same drawing on the reds_house atlas, with two differences
+    # the numbers absorb. Its tabletop field stops at row 22 and row