DramaticShapeVoxelMod archival mirror · read-only
1a283d67711d32a0f8492a7b3abc77a52c449d46

bump version

DramaticShape · 2026-08-02 · parent a7c9541ac4

bump version

Changed files

CHANGELOG.md                    | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 data/voxel_heights.lua          |  70 +++++++++++++++++++++
 main.lua                        |   2 +-
 manifest.json                   |   2 +-
 tests/bike_shop_shots.lua       |  79 ++++++++++++++++++++++++
 tests/bills_desk_shots.lua      |  77 +++++++++++++++++++++++
 tests/chief_house_shots.lua     |  81 ++++++++++++++++++++++++
 tests/heal_machine_shots.lua    |  78 +++++++++++++++++++++++
 tests/house_furniture_shots.lua |  98 +++++++++++++++++++++++++++++
 tests/mart_shots.lua            |  76 +++++++++++++++++++++++
 tests/potted_plant_shots.lua    |  91 +++++++++++++++++++++++++++
 tests/roof_curve_shots.lua      |  80 ++++++++++++++++++++++++
 tests/roof_span_probe.lua       |  47 ++++++++++++++
 tests/round_regress_shots.lua   |  75 ++++++++++++++++++++++
 tests/shelf_relief_shots.lua    |  92 +++++++++++++++++++++++++++
 tests/ship_can_shots.lua        |  78 +++++++++++++++++++++++
 tests/trash_can_shots.lua       |  80 ++++++++++++++++++++++++
 tests/water_curve_shots.lua     | 121 ++++++++++++++++++++++++++++++++++++
 tools/building_voxels.py        |  59 ++++++++++++++++++
 19 files changed, 1486 insertions(+), 2 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bb313c..e6f0ad3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,207 @@
 # Changelog
 
+## 1.4.3
+
+### Added
+
+- **The furniture of the whole game goes through the building
+  pipeline.** 1.4.1 put four drawings through it; this is the rest of
+  the rooms. Every one of them is the same read -- the drawing's own
+  bands say what is a top seen from above, what is a face seen head-on,
+  and where the thing ends on the floor -- and every one of them
+  replaces a pinned box that wore its drawing as a decal. The pins all
+  stay as the degradation path, neutralized wherever a template stamps.
+
+  - **The bookcase, the commonest piece of furniture in the game** --
+    58 placements across two drawings on the town-house atlas (books
+    and a bowl on each shelf at the west end of eighteen homes, books
+    on both at the east), plus Red's and the Copycat's pair. Pinned
+    `desk` it was a 24px box with the books painted on its flat front.
+    Modelled it is 23 voxels of cabinet with its top seen from above,
+    and every book, bowl and door panel sunk a voxel behind the frame
+    the drawing seals it in.
+  - **Celadon's display cabinets** -- the tall one with the trophy
+    behind its glass and the short one beside it, band for band the
+    same object as the town house's on another atlas, which is what
+    makes the pair read as one line of furniture: 23 voxels and 15,
+    exactly the 8 rows of drawing between them.
+  - **The dining table, everywhere it is drawn** -- the generic town
+    house's at 18 placements, Red's and the Copycat's, and the chief's
+    long table at four cells wide. All of them the lab table's read at
+    a different width, all of them 6 voxels, all of them standing on
+    the ground line their legs are drawn stopping at rather than on
+    the grid's floor.
+  - **The stool at every one of those tables** -- 94 placements on the
+    house atlas alone, ten more in Red's and the Copycat's, and the Fan
+    Club's four members' chairs, a different drawing that is
+    pixel-identical from the seat down. The first template with no base
+    piece at all: a stool is drawn mid-cell over its own floor, so it
+    is a desk-set of exactly one part, seat lid over legs with the
+    floor showing between them.
+  - **The Pokemon Center's healing machine** -- two variants, 24
+    placements, plus the Indigo Plateau lobby's pair. A wall-height
+    cabinet with its monitor perched on the front of its top face,
+    drawn across two map rows because it towers over the 16px band
+    behind it, which the volume path could only read as more wall. The
+    hoses leaving its side are modelled as hoses, at the elevation and
+    the depth the two stacked motifs put them; the west machine's
+    keyboard is a shelf at counter height wearing its own top-view art.
+  - **Bill's desk, and the Silph president's** -- the same drawing in
+    both rooms. Its terminal is drawn in 2:1 isometric, turned 45
+    degrees to the map, and builds as a cube rather than the slab a 2:1
+    reading gives; the kinked dark run between keyboard and computer is
+    raised to the keyboard's height and reads as the cable it is. The
+    desk stops at its own two cells because the artist drew its apron
+    into the walkable cell in front, sharing tiles with the chair
+    pushed up to it -- so the chair is modelled as a part of the desk.
+  - **The Bike Shop's open toolbox.** The drawing looks down INTO the
+    tray, which is why every solid treatment failed it -- as a
+    `billboard` the whole cell went up as one 10-voxel slab wearing the
+    drawing as a decal. `tray` builds four walls, a floor and air
+    between them, with the lid standing open on its hinge.
+
+  What the template language grew to carry them: `tray`; a `desk` band
+  that lays its top face flat as a lid; the `box`, `flat` and `iso`
+  part kinds; `stretch` for a band mapped over a deeper plot than it
+  was drawn on; `inset` for a pane sunk by hand; `panes = false` where
+  the global recess pass has the polarity backwards; a `wall` element
+  so a template can keep the band behind it solid; `plane` for a height
+  the drawing states elsewhere; and `scrub`/`keep`/`support`, which let
+  a template model a surface while leaving an object standing on it to
+  its own standee -- Red's potted plant on the dining table.
+
+- **Round bins: the `can` class.** Vermilion Gym's switch puzzle stands
+  fifteen galvanised trash cans in a row, and the S.S. Anne redraws the
+  same object pixel for pixel as its galley barrels. Left to the thin
+  standee pool they were flat discs on edge -- fifteen coins standing
+  in a row; pinned a plain `cylinder` the drawing's base arc revolves
+  too and they came out as barrels balanced on a three-voxel stem.
+  `can` is the round hull cut at both ends, hollowed and tapered: the
+  drawn mouth ellipse projects across the top and down the well so you
+  look into the bin, the drawn base ellipse is ground contact rather
+  than body, and the plan narrows toward the floor. The two ellipses
+  are measured off the pixels; the height, the well and the taper are
+  authored, and the entry says why.
+
+- **The rock gyms' boulders are round.** 87 placements over Pewter's
+  walls and maze and Bruno's clusters, and every one of them was a
+  square bar wearing a boulder texture in relief -- the repeat-aware
+  scenery path extruding the whole drawing as one course. Each cell is
+  now a hull whose plan is its own drawn width profile turned in depth:
+  a dome full-width from the drawn shoulder down, tapering over the top
+  five rows exactly where the art tapers, with the floor's corner
+  diamonds opening between them the way the drawing has them. Still
+  16px, so nothing standing on or beside a rock moves.
+
+- **The potted plant stands as a plant.** The most repeated interior
+  prop in the game -- 78 placements over 13 maps, six per Pokemon
+  Center -- and its urn was rendering as a hollow black frame, because
+  the drawing's foot lies flush on the block's bottom edge and the
+  background vote took the plant's own darks away with the floor. Named
+  outright as light and white instead, it stands as one organic
+  silhouette 32px tall over its two stacked cells, crown overhanging
+  the stem. `planter` carries the same reading for a round drawing
+  stacked two cells high on one cell of plot.
+
+- **Bicycles, in both places the Bike Shop draws them.** The six on the
+  showroom floor get their own pool at two voxels rather than the thin
+  pool's five: a bike is a line drawing, and at five voxels every
+  stroke closes the gap to its neighbour with its own side faces, so
+  from any angle but dead-on the air inside the frames filled in and
+  the six came out as one dark lump. And the two against the north wall
+  get `mounted`, a new authored-mask escape for a thing drawn INTO a
+  wall band: it holds the wall's plane as a thin per-pixel slab instead
+  of standing up as a sprite card, and it keeps its drawn elevation, so
+  a bicycle hung clear of the floor stays hung. Its mask is measured
+  rather than hand-drawn -- the plain panel tile composited across the
+  same grid and the background flooded in through the pixels that still
+  match it, which separates bicycle from stripe exactly.
+
+- **The Marts' cash register is a machine, not a decal.** An authored
+  figure may now state a `depth`, which makes it an object rather than
+  a person: a per-pixel solid standing on the counter instead of the
+  flat card that turned edge-on with the camera. And the drawing is not
+  a box -- its black linework packs two facings, an L of base and arm
+  around a keypad that is the machine's deck seen from above. `flat`
+  lays that rect horizontal in the notch of the L, and `thin` gives the
+  receipt curl a paper's thickness where the body's would have made it
+  a wedge.
+
+- **Shelf fronts have relief.** Everything the `bookcase` collapse is
+  used for is a shelf, a rack or a display case, and all of them seal
+  their contents behind the drawing's own black frame -- so those
+  regions now sink a voxel, the same rule a facade's window panes are
+  recessed by, and 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`: the League's masonry
+  and pilasters, whose courses are the wall itself, and Bill's
+  transporter drums, whose light regions are a lit barrel.
+
+### Changed
+
+- **Class heights now follow the models under them.** A tileset's
+  `heights` gets stools at 5 and tables at 6 in the houses, Bill's desk
+  at 8, and cans at 9 -- each of them the drawn elevation the new
+  template or hull stands at, so whoever sits on a stool sits on the
+  seat, and whatever object sprite stands on a table lands on the
+  modelled top rather than three voxels over it or under it.
+- The healing machines' two flanks leave the `wall` pin for the thin
+  standee pool. They are equipment standing beside the console -- a
+  pair of pipes and a keyboard -- and as wall each was boxed into a
+  solid 16px half-cell wearing its drawing in relief.
+
+### Fixed
+
+- **Water no longer hides behind water.** The reflective pass writes no
+  depth -- the depth canvas is detached for the length of it so the
+  shader can read it -- so nothing put a lake in the buffer and no lake
+  could occlude another; the sheets were simply painted in mesh order.
+  Flat water never showed it, one plane, a farther sheet always landing
+  farther down the screen. The world curve ends that: it drops the far
+  side of the map into the near field of view, and a sea a hundred and
+  fifty tiles away came out rasterised on top of the pond at the
+  player's feet, tall grass and all -- water and terrain "from the
+  other side of the map", not reflected but there. The water meshes now
+  go down flat first, through the ordinary scene shader with depth
+  writes on, and the reflective pass draws over what survived. The
+  buffer holds the surface, so the pass's own test throws the far sheet
+  away; the reflection copy holds it too, so a ray grazing another part
+  of the lake reads water rather than the void behind it; and a frame
+  that cannot run the pass at all is unchanged, because the flat draw
+  is the fallback that was already there.
+- **Reflections under the world curve.** The bend tips the world away
+  and the things standing on it do not lean with it -- and a lake is
+  one of those things. Reflected off the bowl the bend makes, the far
+  half of a pond was a mirror tilted twenty degrees: it threw the ray
+  past the vertical, where the sky ramp's own measure swings from one
+  end to the other across a single column, and hard-edged patches of
+  the wrong sky stamped into the water; the same tilt sent the
+  screen-space march grazing along the bank rather than over it, which
+  is what smeared the dock and the roofs across the harbour. What the
+  water reflects is now worked out in the flat world, exactly as it
+  would be with the curve off, and every marched sample is bent on its
+  way to the screen by the vertex stage's own displacement -- so the
+  ray is straight where it should be and lands where the geometry did.
+  The wave columns are read on the flat sheet too: the relief walk is
+  built on an even slab over a level plane, and in the curved world
+  that slab is a bowl, which handed back a column a pixel or three off
+  per fragment -- a patch of noise in the middle of a pond.
+- **Merged runs tore open under the curve.** A quad's interior is the
+  chord of a parabola its neighbours draw the arc of, so a long run
+  hangs below the short quads butted against it. Nothing bounded a
+  run's length, and the ones that ran away were those wearing a
+  constant texel -- a roof's black eave outline, its fascia, its shaded
+  underside -- because a flat run has no art to break it. At 102px
+  across a gym the eave tore off the roof and the slot showed the
+  building's dark interior through it. Runs now stop at the next 8px
+  lattice line, which is the lattice buildings are stamped on and the
+  one every other quad in the scene already ends on, so every join is
+  vertex-for-vertex and the bend carries them together. It costs quads
+  whether the curve is on or not -- Cerulean's object stream goes from
+  35.7k to 41.6k -- and that is deliberate: the mesh is cached per map
+  and built over seconds, so meshing for the curve's sake only when the
+  curve is on would mean rebuilding every live map on a keypress.
+
 ## 1.4.1
 
 ### Added
diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua
index a98c059..55c075c 100644
--- a/data/voxel_heights.lua
+++ b/data/voxel_heights.lua
@@ -4198,6 +4198,60 @@ return {
         roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 },
         slab = 3, frontEave = 0, ledge = nil,
       },
+      -- F10: the town house's BOOKCASE -- the commonest piece of
+      -- furniture in the game at 58 placements across three drawings,
+      -- and the piece this family was really for: pinned `desk` it was a
+      -- 24px box with a flat front and the books PAINTED on it.  Band for
+      -- band it is the Celadon display cabinet (F05/F06 under MANSION)
+      -- on a different atlas, and it takes those numbers unchanged: rows
+      -- 0-8 the 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: two
+      -- shelves in their dark openings over the cupboard's two panelled
+      -- doors.  Every book, bowl and door panel is a non-black region the
+      -- drawing seals behind its own black frame, so the measured pane
+      -- pass sinks each one a voxel and the frames stand proud of them --
+      -- the relief the `bookcase` class now carries too, arrived at the
+      -- same way.
+      --
+      -- Nine drawn top rows over a 32px plot: 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 case is built into the room's north wall.  23 voxels tall,
+      -- one under the `desk` pin it replaces and exactly the Celadon
+      -- trophy case's stand; the `desk` pins stay as the degradation
+      -- path, neutralized wherever this stamps.
+      --
+      -- This drawing carries books and a bowl on each shelf: 36
+      -- placements, the west end of eighteen homes.
+      {
+        id = "house_bookcase_bowls",
+        tiles = {
+          { 38, 41 },
+          { 14, 15 },
+          { 14, 15 },
+          { 30, 31 },
+        },
+        roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 },
+        slab = 1, frontEave = 0, ledge = nil,
+      },
+      -- F10 again: its twin at the east end, books on both shelves -- 22
+      -- placements, and the only one of the two that Cerulean's trashed
+      -- house also puts at the west end.
+      {
+        id = "house_bookcase_books",
+        tiles = {
+          { 38, 41 },
+          { 48, 49 },
+          { 48, 49 },
+          { 30, 31 },
+        },
+        roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 },
+        slab = 1, 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
@@ -4268,6 +4322,22 @@ return {
         keep = { 40, 54, 55, 56, 57 },
         support = 6,
       },
+      -- F10 again: Red's and the Copycat's bookcase pair, cells (0,0)
+      -- and (1,0) of both maps (4 placements).  The same object as the
+      -- town house's, one shelf of each of its two drawings -- 48/49
+      -- books above, 34/35 below -- and the same band numbers; see HOUSE
+      -- above for the read.
+      {
+        id = "reds_bookcase",
+        tiles = {
+          { 38, 41 },
+          { 48, 49 },
+          { 34, 35 },
+          { 50, 51 },
+        },
+        roofRows = 10, roofBack = 8, roofFront = 2, roofCycle = { 2, 7 },
+        slab = 1, frontEave = 0, ledge = nil,
+      },
       -- 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.
diff --git a/main.lua b/main.lua
index 01aaf0f..f0b4883 100644
--- a/main.lua
+++ b/main.lua
@@ -890,7 +890,7 @@ mod.hooks:wrap("world.tod", function(next, tod, ctx)
   return DayNight.tod()
 end)
 
-mod.exports.version = "1.4.1"
+mod.exports.version = "1.4.3"
 -- exposed so a companion mod can pin its own tiles' shapes or read the
 -- camera without reaching into this mod's file layout
 mod.exports.lib = V
diff --git a/manifest.json b/manifest.json
index b2382d4..1f6184c 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
 {
   "id": "DRAMATIC_SHAPE",
   "name": "Dramatic Shape Voxel Mod",
-  "version": "1.4.1",
+  "version": "1.4.3",
   "api": 2,
   "entry": "main.lua",
   "profile": "content",
diff --git a/tests/bike_shop_shots.lua b/tests/bike_shop_shots.lua
new file mode 100644
index 0000000..7a896ed
--- /dev/null
+++ b/tests/bike_shop_shots.lua
@@ -0,0 +1,79 @@
+-- Scratch driver: shots of the Bike Shop showroom, for the bicycle
+-- voxelization.  Two viewpoints -- the north wall (the two bikes drawn
+-- INTO the wall band) and the showroom floor (the six standing bikes).
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/bike_shop_shots.lua \
+--   SHOT_DIR=.scratchpad/bikes AB_TAG=before lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/bikes")
+    .. "/" .. (os.getenv("AB_TAG") or "before")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[bike] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  -- cells: wall bikes ride cell row 0 (tile cols 1-3 and 6-8); the six
+  -- floor bikes stand in cell columns 0 and 2, rows 1-2 and 4-5
+  local SCENES = {
+    { x = 2, y = 2, face = "up",   label = "wall" },
+    { x = 3, y = 3, face = "up",   label = "room" },
+    { x = 2, y = 4, face = "left", label = "floor" },
+    { x = 3, y = 6, face = "up",   label = "wide" },
+    -- the two toolboxes, cells (6,6) and (7,7)
+    { x = 5, y = 6, face = "right", label = "tools" },
+    { x = 6, y = 4, face = "down",  label = "tools2" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, "BIKE_SHOP", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[bike] capture missed: " .. path) end
+    end
+  end
+  print(("[bike] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/bills_desk_shots.lua b/tests/bills_desk_shots.lua
new file mode 100644
index 0000000..f6c7898
--- /dev/null
+++ b/tests/bills_desk_shots.lua
@@ -0,0 +1,77 @@
+-- Scratch driver: shots of Bill's desk, for the `bills_desk`
+-- voxelization.  The desk fills cells (1,4) and (2,4) of Bill's house
+-- with its chair in the walkable cell (1,5) below it, so these are the
+-- angles you can actually stand at: head-on from the floor two cells
+-- south, and from either flank.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/bills_desk_shots.lua \
+--   SHOT_DIR=.scratchpad/billsdesk AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/billsdesk")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[bills] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { x = 1, y = 6, face = "up",    label = "headon" },
+    { x = 2, y = 6, face = "up",    label = "headon_e" },
+    { x = 4, y = 5, face = "left",  label = "east" },
+    { x = 0, y = 5, face = "right", label = "west" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, "BILLS_HOUSE", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[bills] capture missed: " .. path) end
+    end
+  end
+  print(("[bills] %d shots into %s"):format(shots, ROOT))
+  love.event.quit()
+end
diff --git a/tests/chief_house_shots.lua b/tests/chief_house_shots.lua
new file mode 100644
index 0000000..2fec3a8
--- /dev/null
+++ b/tests/chief_house_shots.lua
@@ -0,0 +1,81 @@
+-- Scratch driver: shots of the Celadon chief's house, for the display
+-- cabinet and long table voxelizations.  Three viewpoints -- the
+-- cabinet rank along the north wall, the long table in the middle of
+-- the room, and a wide shot with both in frame.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/chief_house_shots.lua \
+--   SHOT_DIR=.scratchpad/chief AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/chief")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[chief] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  -- the cabinets occupy cells 2..5 of rows 0-1; the long table cells
+  -- 2..5 of rows 3-4; the player walks rows 2 and 5
+  local SCENES = {
+    { x = 3, y = 2, face = "up",    label = "cabinets" },
+    { x = 5, y = 2, face = "up",    label = "bookcase" },
+    { x = 3, y = 5, face = "up",    label = "table" },
+    { x = 1, y = 5, face = "right", label = "wide" },
+    -- the same rank on CELADON_MANSION_1F, where it stands against the
+    -- interior partition and the grids start on an ODD tile row
+    { map = "CELADON_MANSION_1F", x = 2, y = 4, face = "up",
+      label = "mansion1f" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, s.map or "CELADON_CHIEF_HOUSE", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[chief] capture missed: " .. path) end
+    end
+  end
+  print(("[chief] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/heal_machine_shots.lua b/tests/heal_machine_shots.lua
new file mode 100644
index 0000000..7f148cd
--- /dev/null
+++ b/tests/heal_machine_shots.lua
@@ -0,0 +1,78 @@
+-- Scratch driver: shots of the Pokemon Center healing machines behind
+-- the counter, for the center_heal_machine voxelization.  The pair
+-- stands at cells (1,0):(2,1) and (6,0):(7,1) of every Center; the
+-- nurse aisle (row 2) is the row you can actually face them from, and
+-- the public floor south of the counter gives the wide view.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/heal_machine_shots.lua \
+--   SHOT_DIR=.scratchpad/healshots AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/healmachine")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[heal] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { x = 1, y = 2, face = "up",   label = "west_head" },
+    { x = 2, y = 2, face = "up",   label = "west_keyboard" },
+    { x = 3, y = 2, face = "left", label = "west_side" },
+    { x = 6, y = 2, face = "up",   label = "east_head" },
+    { x = 3, y = 4, face = "up",   label = "wide" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, "VIRIDIAN_POKECENTER", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[heal] capture missed: " .. path) end
+    end
+  end
+  print(("[heal] %d shots into %s"):format(shots, ROOT))
+  love.event.quit()
+end
diff --git a/tests/house_furniture_shots.lua b/tests/house_furniture_shots.lua
new file mode 100644
index 0000000..4a3a136
--- /dev/null
+++ b/tests/house_furniture_shots.lua
@@ -0,0 +1,98 @@
+-- Scratch driver: shots of the house dining tables and stools, for the
+-- band-table + no-desk-part voxelization.  Every generic home places the
+-- table at cells (3,3):(4,4) with four stools around it (Blue's house has
+-- Daisy seated at hers); Red's and the Copycat's ground floors place the
+-- same furniture one cell lower, with the potted plant CUTOUT standing on
+-- the tabletop -- the standee the table template must support, not
+-- swallow.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/house_furniture_shots.lua \
+--   SHOT_DIR=.scratchpad/housefurn AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/housefurn")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[housefurn] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    -- head on: below Blue's table looking north over a stool at it,
+    -- Daisy seated at the left one
+    { map = "BLUES_HOUSE", x = 3, y = 5, face = "up", label = "blues_front" },
+    -- from the east, table and both east stools in profile
+    { map = "BLUES_HOUSE", x = 6, y = 3, face = "left", label = "blues_side" },
+    -- from the north wall looking south down over the tabletop
+    { map = "BLUES_HOUSE", x = 4, y = 2, face = "down", label = "blues_over" },
+    -- close beside a stool: seat top, legs and the gap between them
+    { map = "BLUES_HOUSE", x = 2, y = 5, face = "up", label = "stool_close" },
+    -- Red's table head on from the south: the plant cutout standing on
+    -- the modelled tabletop
+    { map = "REDS_HOUSE_1F", x = 4, y = 6, face = "up", label = "reds_front" },
+    -- and from the east along the stool row, plant in profile
+    { map = "REDS_HOUSE_1F", x = 6, y = 4, face = "left", label = "reds_side" },
+    -- the Fan Club's four members' chairs round the boardroom table:
+    -- from the south of the west pair, both stools stacked in profile
+    { map = "POKEMON_FAN_CLUB", x = 1, y = 5, face = "up",
+      label = "club_west_pair" },
+    -- across the table from the west, both pairs and the octagon between
+    { map = "POKEMON_FAN_CLUB", x = 0, y = 3, face = "right",
+      label = "club_across" },
+    -- close on the east pair from the north, looking down over the seats
+    { map = "POKEMON_FAN_CLUB", x = 6, y = 2, face = "down",
+      label = "club_over" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, s.map, s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[housefurn] capture missed: " .. path) end
+    end
+  end
+  print(("[housefurn] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/mart_shots.lua b/tests/mart_shots.lua
new file mode 100644
index 0000000..428c4dc
--- /dev/null
+++ b/tests/mart_shots.lua
@@ -0,0 +1,76 @@
+-- Scratch driver: shots of a Poke Mart's clerk counter, for the cash
+-- register voxelization.  The register is drawn at cell (1,5) of the 4x4
+-- shop layout every Mart shares, in the middle of the counter's east arm,
+-- so these are the three angles you can actually stand at: head-on from
+-- the aisle, side-on from the east, and over the counter's south arm.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/mart_shots.lua \
+--   SHOT_DIR=.scratchpad/register AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/register")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[mart] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { x = 1, y = 7, face = "up",   label = "aisle" },
+    { x = 2, y = 5, face = "left", label = "side" },
+    { x = 2, y = 6, face = "left", label = "over" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, "VIRIDIAN_MART", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[mart] capture missed: " .. path) end
+    end
+  end
+  print(("[mart] %d shots into %s"):format(shots, ROOT))
+  love.event.quit()
+end
diff --git a/tests/potted_plant_shots.lua b/tests/potted_plant_shots.lua
new file mode 100644
index 0000000..533bdba
--- /dev/null
+++ b/tests/potted_plant_shots.lua
@@ -0,0 +1,91 @@
+-- Scratch driver: shots of the potted plants, for the plant standee
+-- voxelization.  Every Center places three side-by-side pairs on its
+-- bottom row -- crowns at cells (0,6)/(1,6), (6,6)/(7,6), (12,6)/(13,6),
+-- pots below at y=7 -- and INDIGO_PLATEAU_LOBBY (the MART tileset id,
+-- same atlas) lines four of them along its hall at cells (12,10)..(15,10).
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/potted_plant_shots.lua \
+--   SHOT_DIR=.scratchpad/plants AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/plants")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[plant] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    -- east of the left pair, looking west along the bottom row: both
+    -- plants in profile, crown overhang and pot silhouette side-on
+    { map = "VIRIDIAN_POKECENTER", x = 3, y = 6, face = "left",
+      label = "pair_side" },
+    -- north of the left pair, looking south down over the crowns
+    { map = "VIRIDIAN_POKECENTER", x = 1, y = 5, face = "down",
+      label = "pair_over" },
+    -- head on: standing below the middle pair looking north at it
+    { map = "VIRIDIAN_POKECENTER", x = 6, y = 7, face = "up",
+      label = "pair_front" },
+    -- close up beside the east pair's pot
+    { map = "VIRIDIAN_POKECENTER", x = 11, y = 7, face = "right",
+      label = "close" },
+    -- the Plateau lobby's row of four (MART tileset id), along the row
+    { map = "INDIGO_PLATEAU_LOBBY", x = 11, y = 10, face = "right",
+      label = "lobby_row" },
+    -- and head on from the hall below
+    { map = "INDIGO_PLATEAU_LOBBY", x = 13, y = 12, face = "up",
+      label = "lobby_front" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, s.map, s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[plant] capture missed: " .. path) end
+    end
+  end
+  print(("[plant] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/roof_curve_shots.lua b/tests/roof_curve_shots.lua
new file mode 100644
index 0000000..71c06b0
--- /dev/null
+++ b/tests/roof_curve_shots.lua
@@ -0,0 +1,80 @@
+-- Scratch driver: the Cerulean gym and the houses beside it, shot at
+-- several camera rungs with V-CURVE walked OFF..3, to see what the world
+-- bend does to a building's roof.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/roof_curve_shots.lua \
+--   SHOT_DIR=.scratchpad/roofcurve AB_TAG=before lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/roofcurve")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[roof] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+  local WorldCurve = V.require("WorldCurve")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(30)
+  end
+
+  -- the gym door is CERULEAN_CITY (30,19); the bike shop and the row of
+  -- houses along the west side give a second, smaller roof in frame
+  local SCENES = {
+    { map = "CERULEAN_CITY", x = 30, y = 20, face = "up",   label = "gym" },
+    { map = "CERULEAN_CITY", x = 27, y = 21, face = "up",   label = "gymwide" },
+    { map = "PALLET_TOWN",   x = 5,  y = 6,  face = "up",   label = "house" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      for _, curve in ipairs({ 0, 3 }) do
+        U.teleport(game, s.map, s.x, s.y, s.face)
+        Pipelines.setLevel("voxel", rung)
+        Pipelines.setLevel("tiltshift", 0)
+        WorldCurve.setting:setIndex(curve + 1, game)
+        settle()
+        local path = ("%s/%s_v%d_c%d.png"):format(ROOT, s.label, rung, curve)
+        game.capturePath = path
+        U.wait(8)
+        local f = io.open(path, "rb")
+        if f then f:close() shots = shots + 1
+        else print("[roof] capture missed: " .. path) end
+      end
+    end
+  end
+  print(("[roof] %d shots into %s"):format(shots, ROOT))
+  love.event.quit()
+end
diff --git a/tests/roof_span_probe.lua b/tests/roof_span_probe.lua
new file mode 100644
index 0000000..99e9563
--- /dev/null
+++ b/tests/roof_span_probe.lua
@@ -0,0 +1,47 @@
+-- Scratch probe: how long do a building model's merged quads get?
+--
+-- A quad's longest world-space edge is what decides how far its CHORD
+-- falls below the world curve's parabola, so this is the number that says
+-- whether the bend can crack the mesh open.
+--
+--   BUILD_MAP=CERULEAN_CITY POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/roof_span_probe.lua lovec .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local mapId = os.getenv("BUILD_MAP") or "CERULEAN_CITY"
+  U.teleport(game, mapId, tonumber(os.getenv("BUILD_X") or "30"),
+             tonumber(os.getenv("BUILD_Y") or "20"), "up")
+  U.wait(30)
+
+  local V = game.mods.exports["DRAMATIC_SHAPE"]
+  V = V and V.lib
+  local Structures = V and V.require("Structures")
+  local ow = game.overworld
+  if not (Structures and ow and ow.map) then
+    print("[span] mod or map unavailable")
+    love.event.quit()
+    return
+  end
+  local S = Structures.forMap(ow.map)
+  local hist, worst = {}, 0
+  for _, q in ipairs(S.objectQuads) do
+    local dx = math.max(q[1][1], q[2][1], q[3][1], q[4][1])
+             - math.min(q[1][1], q[2][1], q[3][1], q[4][1])
+    local dz = math.max(q[1][3], q[2][3], q[3][3], q[4][3])
+             - math.min(q[1][3], q[2][3], q[3][3], q[4][3])
+    local dy = math.max(q[1][2], q[2][2], q[3][2], q[4][2])
+             - math.min(q[1][2], q[2][2], q[3][2], q[4][2])
+    local span = math.max(dx, dz, dy)
+    local bucket = span <= 8 and "<=8" or (span <= 16 and "<=16"
+                  or (span <= 32 and "<=32" or (span <= 64 and "<=64" or ">64")))
+    bucket = bucket .. (q.own and " bld" or " prop")
+    hist[bucket] = (hist[bucket] or 0) + 1
+    if span > worst then worst = span end
+  end
+  print(("[span] %d object quads, longest edge %d px"):format(#S.objectQuads, worst))
+  for _, b in ipairs({ "<=8", "<=16", "<=32", "<=64", ">64" }) do
+    for _, kind in ipairs({ " bld", " prop" }) do
+      print(("[span]   %-10s %d"):format(b .. kind, hist[b .. kind] or 0))
+    end
+  end
+  love.event.quit()
+end
diff --git a/tests/round_regress_shots.lua b/tests/round_regress_shots.lua
new file mode 100644
index 0000000..246b302
--- /dev/null
+++ b/tests/round_regress_shots.lua
@@ -0,0 +1,75 @@
+-- Scratch driver: one shot of every OTHER user of the round-hull builder
+-- (tree canopies, boulders, hedges, stumps, the Center planter), to check
+-- that the `can` class's base cut is the identity it is supposed to be for
+-- everything that does not ask for it.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/round_regress_shots.lua \
+--   SHOT_DIR=.scratchpad/round AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/round")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then return end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { map = "VIRIDIAN_FOREST", x = 16, y = 20, face = "up",   label = "forest" },
+    { map = "PEWTER_GYM",      x = 4,  y = 10, face = "up",   label = "boulders" },
+    { map = "CELADON_GYM",     x = 4,  y = 8,  face = "up",   label = "hedges" },
+    { map = "VIRIDIAN_POKECENTER", x = 6, y = 5, face = "up", label = "planter" },
+    { map = "PALLET_TOWN",     x = 5,  y = 8,  face = "up",   label = "trees" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face)
+    if ok then
+      Pipelines.setLevel("voxel", 5)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s.png"):format(ROOT, s.label)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[round] capture missed: " .. path) end
+    else
+      print("[round] teleport failed: " .. s.map)
+    end
+  end
+  print(("[round] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/shelf_relief_shots.lua b/tests/shelf_relief_shots.lua
new file mode 100644
index 0000000..633fe37
--- /dev/null
+++ b/tests/shelf_relief_shots.lua
@@ -0,0 +1,92 @@
+-- Scratch driver: shots of the `bookcase` class across the tilesets that
+-- pin it, for the shelf-front relief.  Two of them are NOT shelves --
+-- the League's gate walls and the terraces on PLATEAU -- and are here as
+-- the control: their courses run edge to edge, so nothing should sink.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/shelf_relief_shots.lua \
+--   SHOT_DIR=.scratchpad/shelves AB_TAG=before lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/shelves")
+    .. "/" .. (os.getenv("AB_TAG") or "before")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[shelf] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { map = "OAKS_LAB",           x = 7, y = 2, face = "up", label = "dojo_lab" },
+    { map = "CELADON_MANSION_2F", x = 2, y = 4, face = "up", label = "mansion_2f" },
+    { map = "CELADON_MART_2F",    x = 5, y = 5, face = "up", label = "lobby_mart" },
+    { map = "MUSEUM_1F",          x = 2, y = 4, face = "up", label = "museum" },
+    { map = "VIRIDIAN_MART",      x = 3, y = 5, face = "up", label = "mart" },
+    { map = "SS_ANNE_CAPTAINS_ROOM", x = 5, y = 2, face = "up", label = "ship" },
+    -- the controls, both of them tilesets that borrow the collapse for
+    -- something that is NOT a shelf and say so with `bookcase_relief =
+    -- false`: the League's masonry gate walls, and Bill's transporter
+    -- drums.  Nothing in either may move.
+    { map = "INDIGO_PLATEAU",     x = 2, y = 5, face = "up", label = "plateau" },
+    { map = "BILLS_HOUSE",        x = 2, y = 3, face = "up", label = "bills" },
+    -- the house shelves: pinned `desk`, NOT `bookcase`, so they go
+    -- through the world mesher's box fold and this relief never reaches
+    -- them.  Here to show the gap.
+    { map = "REDS_HOUSE_1F",      x = 1, y = 2, face = "up", label = "reds" },
+    { map = "BLUES_HOUSE",        x = 1, y = 2, face = "up", label = "blues" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face)
+    -- twice: the first load of the session has no mesh to settle against
+    pcall(U.teleport, game, s.map, s.x, s.y, s.face)
+    if ok then
+      Pipelines.setLevel("voxel", 5)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s.png"):format(ROOT, s.label)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[shelf] capture missed: " .. path) end
+    else
+      print("[shelf] teleport failed: " .. s.map)
+    end
+  end
+  print(("[shelf] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/ship_can_shots.lua b/tests/ship_can_shots.lua
new file mode 100644
index 0000000..6eb535f
--- /dev/null
+++ b/tests/ship_can_shots.lua
@@ -0,0 +1,78 @@
+-- Scratch driver: shots of the SS Anne's galley barrels, which are Lt.
+-- Surge's trash can redrawn on the ship atlas.  Three down the kitchen's
+-- east wall at cells (13,5)/(13,7)/(13,9), one in the captain's room at
+-- (4,1), and one each in the two ship-interior houses at (7,7).
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/ship_can_shots.lua \
+--   SHOT_DIR=.scratchpad/ssanne AB_TAG=after lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/ssanne")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[ship] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    { map = "SS_ANNE_KITCHEN",       x = 12, y = 11, face = "up",    label = "galley_up" },
+    { map = "SS_ANNE_KITCHEN",       x = 12, y = 3,  face = "down",  label = "galley_down" },
+    { map = "SS_ANNE_KITCHEN",       x = 11, y = 7,  face = "right", label = "galley_side" },
+    { map = "SS_ANNE_CAPTAINS_ROOM", x = 4, y = 4,   face = "up",    label = "captain" },
+    { map = "CERULEAN_BADGE_HOUSE",  x = 6, y = 7,   face = "right", label = "house" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    local ok = pcall(U.teleport, game, s.map, s.x, s.y, s.face)
+    if ok then
+      Pipelines.setLevel("voxel", 5)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s.png"):format(ROOT, s.label)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[ship] capture missed: " .. path) end
+    else
+      print("[ship] teleport failed: " .. s.map)
+    end
+  end
+  print(("[ship] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/trash_can_shots.lua b/tests/trash_can_shots.lua
new file mode 100644
index 0000000..408165e
--- /dev/null
+++ b/tests/trash_can_shots.lua
@@ -0,0 +1,80 @@
+-- Scratch driver: shots of Vermilion Gym's trash cans, for the trash can
+-- voxelization.  The fifteen cans stand on odd cell columns 1..9 in cell
+-- rows 7, 9 and 11; the sixteenth is up at cell (6,1) beside the leader's
+-- platform.  Even columns are open floor, so the player can be parked
+-- between two cans and look along a row.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/trash_can_shots.lua \
+--   SHOT_DIR=.scratchpad/cans AB_TAG=before lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/cans")
+    .. "/" .. (os.getenv("AB_TAG") or "before")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[can] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  local SCENES = {
+    -- head on into the middle of the field, cans left, right and ahead
+    { x = 4, y = 12, face = "up",    label = "field" },
+    -- close up: standing between two cans of the bottom row
+    { x = 2, y = 11, face = "left",  label = "close" },
+    -- along the row, so the cans line up in depth
+    { x = 4, y = 13, face = "up",    label = "row" },
+    -- from the north, looking back down over all three rows
+    { x = 4, y = 6,  face = "down",  label = "over" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    for _, rung in ipairs({ 3, 5 }) do
+      U.teleport(game, "VERMILION_GYM", s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", rung)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
+      game.capturePath = path
+      U.wait(6)
+      local f = io.open(path, "rb")
+      if f then f:close() shots = shots + 1
+      else print("[can] capture missed: " .. path) end
+    end
+  end
+  print(("[can] %d shots into %s"):format(shots, ROOT))
+end
diff --git a/tests/water_curve_shots.lua b/tests/water_curve_shots.lua
new file mode 100644
index 0000000..26d8f1f
--- /dev/null
+++ b/tests/water_curve_shots.lua
@@ -0,0 +1,121 @@
+-- Scratch driver: water shot with V-CURVE walked OFF..3, to see what the
+-- world bend does to the reflective pass.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/water_curve_shots.lua \
+--   SHOT_DIR=.scratchpad/watercurve AB_TAG=before lovec.exe .
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR") or "shots/watercurve")
+    .. "/" .. (os.getenv("AB_TAG") or "after")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[water] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+  local WorldCurve = V.require("WorldCurve")
+  local Water = V.require("Water")
+  -- WATER_RUNG=sky drops the screen-space march and leaves the sky path, to
+  -- tell an artefact of the one from an artefact of the other
+  if os.getenv("WATER_RUNG") then Water.setting:sync(os.getenv("WATER_RUNG")) end
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync(os.getenv("WATER_TIME") or "night")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  -- Stand on the walkable cell just north of the widest run of water on the
+  -- map, so a scene is picked by where the water actually is rather than by
+  -- a coordinate guessed off the block list.
+  local TileShape = V.require("TileShape")
+  local function shore(map)
+    local def = map.def
+    local shapes = TileShape.forMap(map)
+    local function classAt(cx, cy)
+      local tx, ty = cx * 2, cy * 2
+      local s = TileShape.at(map, shapes, map:tileAt(tx, ty), tx, ty)
+      return s and s.class
+    end
+    local best, bestRun = nil, 0
+    for cy = 1, def.height * 2 - 1 do
+      local run, start = 0, nil
+      for cx = 0, def.width * 2 - 1 do
+        if classAt(cx, cy) == "water" then
+          start = start or cx
+          run = run + 1
+          if run > bestRun and classAt(cx, cy - 1) ~= "water" then
+            bestRun, best = run, { x = start + math.floor(run / 2), y = cy - 1 }
+          end
+        else
+          run, start = 0, nil
+        end
+      end
+    end
+    return best
+  end
+
+  local SCENES = {
+    { map = "PALLET_TOWN",     label = "pallet" },
+    { map = "CERULEAN_CITY",   label = "cerulean" },
+    { map = "VERMILION_CITY",  x = 18, y = 27, label = "vermilion" },
+    { map = "ROUTE_24",        label = "route24" },
+    { map = "VIRIDIAN_CITY",   label = "viridian" },
+  }
+
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+    if not s.x then
+      -- teleport once so the map is loaded, then let the scan place us
+      U.teleport(game, s.map, 1, 1, "down")
+      U.wait(4)
+      local spot = game.overworld and game.overworld.map and shore(game.overworld.map)
+      if spot then s.x, s.y = spot.x, spot.y else s.x, s.y = 1, 1 end
+      print(("[water] %s shore at (%d,%d)"):format(s.label, s.x, s.y))
+    end
+    for _, rung in ipairs({ 3, 4 }) do
+      for _, curve in ipairs({ 0, 3 }) do
+        U.teleport(game, s.map, s.x, s.y, s.face or "down")
+        Pipelines.setLevel("voxel", rung)
+        Pipelines.setLevel("tiltshift", 0)
+        WorldCurve.setting:setIndex(curve + 1, game)
+        settle()
+        local path = ("%s/%s_v%d_c%d.png"):format(ROOT, s.label, rung, curve)
+        game.capturePath = path
+        U.wait(8)
+        local f = io.open(path, "rb")
+        if f then f:close() shots = shots + 1
+        else print("[water] capture missed: " .. path) end
+      end
+    end
+  end
+  print(("[water] %d shots into %s"):format(shots, ROOT))
+  love.event.quit()
+end
diff --git a/tools/building_voxels.py b/tools/building_voxels.py
index bcc9078..c8718c0 100644
--- a/tools/building_voxels.py
+++ b/tools/building_voxels.py
@@ -723,6 +723,65 @@ TEMPLATES = {
         slab=3, front_eave=0, ledge=None, tileset="reds_house",
         scrub=[(10, 5, 21, 15)],
     ),
+    # F10: the town house's bookcase -- the commonest piece of furniture
+    # in the game at 58 placements across three drawings, and the reason
+    # this family exists: the `desk` box it used to be had a flat front
+    # with the books PAINTED on it. Band for band it is the Celadon
+    # cabinet (F05/F06) on a different atlas, and it takes those numbers
+    # unchanged: 0-8 the 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 -- so
+    # slab = 1 and that row folds into the roof band -- and 10-31 the
+    # front: two shelves in their dark openings over the cupboard's two
+    # panelled doors. Every one of those is a non-black region the
+    # drawing seals behind its own black frame, so the measured pane
+    # pass sinks each a voxel and the frames stand proud of them.
+    # Nine drawn top rows over a 32px plot: the rims map 1:1 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 case is built into the room's north wall. 23 voxels tall,
+    # one under the `desk` pin it replaces, and the same 23 the Celadon
+    # trophy case stands.
+    #
+    # This one carries books and a bowl on each shelf; 36 placements,
+    # the west end of eighteen homes.
+    "house_bookcase_bowls": dict(
+        tiles=[
+            [38, 41],
+            [14, 15],
+            [14, 15],
+            [30, 31],
+        ],
+        roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7),
+        slab=1, front_eave=0, ledge=None, tileset="house",
+    ),
+    # F10 again: its twin at the east end, books on both shelves --
+    # 22 placements, and the only drawing of the two that the trashed
+    # house in Cerulean puts at the west end as well.
+    "house_bookcase_books": dict(
+        tiles=[
+            [38, 41],
+            [48, 49],
+            [48, 49],
+            [30, 31],
+        ],
+        roof_rows=10, roof_back=8, roof_front=2, roof_cycle=(2, 7),
+        slab=1, front_eave=0, ledge=None, tileset="house",
+    ),
+    # F10 once more, on the reds_house atlas: Red's and the Copycat's
+    # ground-floor pair (4 placements). The same object, one shelf of
+    # each drawing -- 48/49 books above, 34/35 below -- and the same
+    # band numbers.
+    "reds_bookcase": dict(
+        tiles=[
+            [38, 41],
+            [48, 49],
+            [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="reds_house",
+    ),
     # F09: the stool at every one of those tables -- 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