update voxelizations of celadon diner stools and tables. Update square table voxelization in celadon mansion. Update mansion computer desks

This commit is contained in:
DramaticShape
2026-08-06 17:52:59 -04:00
parent 442e9d26d5
commit 91d9a37e8f
4 changed files with 559 additions and 29 deletions
@@ -61,6 +61,10 @@ cues generalize:
| Band containing window/door frames | Vertical facade | Straight extrusion |
| Full-width band with a black underline sitting above an inset band | Ledge / awning overhang | Extrusion + protrusion |
| Dark `#555` runs beside a facade under a taper | Shadow on the wall beneath an eave | Leave as wall — the geometry above produces the shadow's meaning |
| Scattered light shapes on a dark field, bracketed by TWO full-width black rims, shallow band below the lower rim | The **inside of an open container** seen from above, with contents lying in it | Hollow tray: walls to the rims, floor slab, air between — never an extrusion |
| Ellipse drawn wider than tall (e.g. 9x5) | A horizontal circle seen from above — a mouth, a lid, a pot rim | Cut face of a round hull; the aspect ratio is the proof of the top view |
| Arcs above/below a round object's straight flanks, lowest point at the centre column, often a 1px #555 halo outside | The SAME circles seen curving — ground contact and mouth back-edge, i.e. depth, not narrowing | Strip them from the revolve; run the last body row's disc to the floor |
| A side band shearing sideways as it descends (¾-view) | The projection sliding a receding wall, not the wall's position | Un-project: the wall goes where the plan says |
The band table for Red's house, which Blue's house shares verbatim:
@@ -156,9 +160,12 @@ Tooling: `voxel_build_verify.py` (builds, asserts, renders previews).
1. Obtain the sprite; sample to native resolution via block centers.
2. Extract palette + silhouette (light-only flood fill, threshold 130);
review the ASCII mask.
3. Segment rows into bands using the Stage-2 cues; write the band table
before writing any geometry code.
review the ASCII mask — rendered large, not hand-counted.
3. Name the real object first (including whether it is hollow, round or
thin — see "Beyond the house"), then segment rows into bands using the
Stage-2 cues; write the band table as prose, one line per row range
with where each band lands, before writing any geometry code. The
correct reading makes the row arithmetic land exactly.
4. Measure taper rates from the mask; derive `T(x)`, `YTOP`, overhangs, `D`.
5. Build: extrude verticals (de-outlined interiors) → ledges → recesses →
flat top (mid-row cycling) → sloped solids (overwrite, then trim) →
@@ -214,3 +221,75 @@ right for the raw GB palette but comes out white once the atlas is
recoloured, turning every sloped end into a black-and-white zip. The
drawing's own eave is black / `#555` / black, and using that reads correctly
under every palette.
## Beyond the house: the forms later objects added
The house is all solid masses — every band either lies flat or extrudes.
Later objects forced the taxonomy open, and each addition came from the
same root move: **name the real 3D form first, then ask which surfaces the
drawing shows.** The recurring failure at every step was the *extruded
picture* — and it has a second-order form that survives re-segmentation.
The Bike Shop's toolbox was re-read from "a prop" into "a cabinet with a
pump beside it": named parts, correct plot, de-outlined sides, and still
wrong, because the region read as a cabinet *front* was the inside of an
open box seen from above. Naming the parts is not enough; every REGION
must answer "what surface of the real object is this?" The reliable
arbiter is arithmetic: the correct reading makes the drawn row counts land
exactly (the toolbox: 1 back-wall rim + 6 interior rows + 1 front rim = 8
= the one-tile plot depth). Forcing rows to fit means the reading is wrong.
**Hollow forms.** An open container is the one shape whose model must
contain AIR, which no band table or extrusion can produce. The tray
treatment builds four walls to the drawn rims, lays the top-view band on
the floor of the cavity (its contents — a wrench — come along free, since
they are just pixels of that band), and leaves the space between empty.
Two rules only containers hit: the pane-recess pass must never run on a
one-voxel wall (it deletes the front voxel to expose the one behind, and
there is nothing behind — the wall becomes a hole), and the hollowness
needs its own verification assert, because a later change that refills the
cavity leaves every count looking plausible.
**Round forms.** A drawn ellipse wider than tall is a horizontal circle
seen from above — that one aspect-ratio measurement settles the whole
reading. Straight flanks give diameter and height at once (round in plan,
so drawn width IS depth — the one depth never authored). The arcs above
and below the straight run are the same top and base circles seen curving:
ground contact and mouth edge, not narrowing — revolving them puts the
object on a stem. The hull's chord representation stores one z-interval
per column/row, so a taper is expressible (re-cut the chords, squeeze the
art into the narrowed span so the rim outline survives) but a hollow ring
needs a second chord. Voxel resolution bounds taste: on an 11-wide object
a one-step taper reads as damage and two steps as a cone; pick the step
count and derive the amount.
**Thin forms.** A line drawing cannot be thick. The air inside a bicycle's
frame is what makes it read as a bicycle; extrude each stroke 5 voxels and
the side faces of neighbouring strokes close every gap off-axis — six
bikes become one dark mass. Standee thickness is a vocabulary
(`PINNED_DEPTH`: 1 for paper, 2 for plates and side-on vehicles, 5 for
silhouettes, 10 for objects with a body), and when a standee looks wrong
the first move is to dump the detector's mask — if the mask is a clean
object, thickness is the problem, not segmentation.
**Authored masks.** When a drawing shares its tiles and shades with what
it is painted into, nothing automatic can separate them; the profile
carries a pixel mask instead. A person becomes a `figures` card (flat,
leaning with the camera, standing on its feet — because GB character art
is face-on iconography); an object becomes a `mounted` slab (fixed in the
world, holding the wall's plane, keeping its drawn elevation — because a
side-on drawing is a plane parallel to the wall). And when the backdrop is
a *regular* pattern, the mask should be MEASURED, not hand-drawn:
composite the plain backdrop tile over the same grid and flood from the
border through pixels that still match it — what the flood cannot reach is
the object, sprite-pure and exact.
**Verification, extended.** Isometric previews miss what only the game
shows: shoot in-game at both the ¾ rung and the low rung (front-face holes
and proportion errors are invisible from above), crop and NEAREST-upscale
before judging, and remember the flat rung renders no model at all. Two
cheap renders beat argument: the front-most voxel per (x, y) laid beside
the composited drawing catches anchoring and texel leaks instantly, and
the same render with sunk voxels flagged turns the recess pass into
something you look at. When shared builder code moves, a saved count
baseline diffed after every edit (mind the line endings) is what proves a
generalization is an identity for every model that already shipped.
+179 -23
View File
@@ -197,7 +197,25 @@ return {
-- cutouts both read wrong for them; the cylinder archetype carves
-- one voxel ball per 16x16 cell from the canopy's darkest-pixel
-- outline, round in depth, so tree rows become rows of real canopies
cylinder = { 42, 43, 58, 59, 64, 65, 80, 81 },
--
-- The cuttable tree ($2D/$2E/$3D/$3E, the four tiles Cut deletes)
-- joins them: it is Celadon Gym's little tree redrawn pixel-for-
-- pixel on this atlas (same 146/256 silhouette, only canopy
-- highlight texels differ), and takes the identical hull -- the
-- scraggly canopy revolves into a gapped ball, the 2-4px trunk
-- into a thin round column, the root flare into a round mound.
-- It used to sit in the `prop` pool as a 5-voxel standee; see
-- GYM's cylinder entry for the reading. Scanned: the 2x2 grid
-- occurs 32 times on this atlas across the towns and routes, and
-- per-tile counts equal the grid count (32 hits for $2D alone),
-- so no stray occurrence renders as a lone hull. The same four
-- ids form grids on FOREST (8, Safari Zone ground art), HOUSE,
-- MANSION, SHIP_PORT and REDS_HOUSE_2 -- different drawings, id
-- collisions, none of this entry's business. Cut itself only
-- swaps the map block to plain grass, so the pin never sees a
-- cut stump.
cylinder = { 42, 43, 58, 59, 64, 65, 80, 81,
45, 46, 61, 62 },
-- the town sign (blockset 8's SE cell): a standing per-pixel slab
-- 2 voxels thin, transparency respected -- never a solid box
signpost = { 70, 71, 86, 87 },
@@ -220,15 +238,11 @@ return {
-- instead -- see there.)
wall = { 2, 36 },
-- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes
-- -- across the whole tileset they appear only in the five
-- cut-tree blocks): a standing per-pixel cutout 5 voxels deep,
-- black-outline segmented with the pixels the outline encloses
-- kept, its drawn grass dither flooding away as background
prop = { 45, 46, 61, 62 },
-- the ground painted under those pinned props, by the prop tile's
-- own id: the bush stands on plain grass ($2C) -- the very tile
-- Cut leaves behind (field.cutTreeSwaps' after-blocks) -- rather
-- (the cuttable tree $2D/$2E/$3D/$3E moved to the cylinder hull
-- above)
-- the ground painted under the claimed tree cells, by tile id:
-- the tree stands on plain grass ($2C) -- the very tile Cut
-- leaves behind (field.cutTreeSwaps' after-blocks) -- rather
-- than whatever flat tile its neighbours vote
prop_ground = { [45] = 44, [46] = 44, [61] = 44, [62] = 44 },
},
@@ -343,8 +357,25 @@ return {
-- -- 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.
-- Celadon's three little trees ($40/$41 canopy over $50/$51
-- trunk) round out the same family: a scraggly canopy over a
-- 2-4px trunk flaring into a round root mound. Every drawn row
-- states its own width, which is exactly what the hull revolves
-- -- the canopy turns into a ball with its drawn gaps kept, the
-- trunk into a thin round column, the mound into a round foot --
-- so the tree stands beside the hedge balls as a solid object
-- of the same construction. It used to sit in the `prop` pool
-- ("a trunk is not round"), but the standee rendered as the
-- whole 16x16 cell extruded, background and all -- the extruded
-- picture -- and the trunk IS round; the hull reads it right.
-- Scanned: the 2x2 grid occurs 3 times on this atlas and only
-- in CELADON_GYM -- cells (2,4), (7,5), (5,7) -- and per-tile
-- counts equal the grid count (3 hits for $40 alone), so no
-- stray occurrence renders as a lone hull. DOJO shares gym.png
-- and places none, so the pin is not copied there.
cylinder = { 44, 45, 46, 47,
7, 8, 23, 24 },
7, 8, 23, 24,
64, 65, 80, 81 },
-- 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
@@ -405,13 +436,9 @@ return {
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 statues. (Celadon's trees $40/$41/$50/$51 lived here
-- too until they moved to the cylinder hull above.)
prop = { 2, 56, 18, 19 },
-- The Hall of Fame's recording machine, the one piece of real
-- furniture in the tileset. It is drawn 32px wide and THREE tile
-- rows tall against the north band, and the detector made a mess
@@ -685,7 +712,15 @@ return {
-- per-cell hulls rather than boxes
canopy = { 4 },
cylinder = { 5, 6, 7, 21, 22, 23,
35, 36, 37, 38, 39, 53, 54 },
35, 36, 37, 38, 39, 53, 54,
-- the Safari Zone's small round trees ($54/$55/$56/$57,
-- one cell, 376 placements across the four safari maps
-- and nowhere else on this tileset): drawn as a canopy
-- ball like the overworld's lone tree, and the detector
-- was boxing them into 16px dither-textured crates.
-- One voxel ball per cell, the same hull the big trees'
-- quarter tiles degrade to
84, 85, 86, 87 },
-- the stumps ($02/$03/$12/$13): a hull whose drawn top is a CUT
-- FACE. The body builds from the bark rows alone, and the drawn
-- ellipse of growth rings projects onto the hull's round flat
@@ -1488,9 +1523,14 @@ return {
-- terrace: their north rim (9/25 = $09/$19) and the
-- pedestal course at the south (85/86/87).
--
-- THE ROUND TABLES in full, because the shape is a compromise. The
-- drawing (block 29, and the same four rows split across blocks 45
-- and 49 in the diner) is
-- THE ROUND TABLES in full, because the shape is a compromise.
-- (The `diner_round_table` template under `buildings` below now
-- models all four placements in full -- octagonal top on its
-- pedestal -- by matching the whole 4x4 grid, which is what a
-- per-tile pin can never do. Everything here stays as its
-- degradation path and as the record of why the pins look the
-- way they do.) The drawing (block 29, and the same four rows
-- split across blocks 45 and 49 in the diner) is
-- $09 $27 $27 $19 an octagonal top seen from above, with
-- $36 $37 $37 $39 a pedestal drawn below its southern
-- $46 $37 $37 $47 rim
@@ -1511,7 +1551,7 @@ return {
-- 8px is the FAR rim (9/25, and the shared 39/54/57) and the
-- pedestal (85/86/87): the far rim is occluded by the 16px top in
-- front of it, and the pedestal is meant to sit low. 16px is also
-- the right height against the 8px `stool` chairs drawn around it
-- the right height against the seat-high `stool` chairs around it
-- -- a terrace table you sit at, not a footstool.
counter = { 9, 21, 25, 36, 37, 38, 39, 41, 48, 49, 52, 53, 54,
57, 85, 86, 87 },
@@ -1541,7 +1581,15 @@ return {
-- drawing carries a full black outline with the floor dither
-- showing at all four corners, so the standee segments cleanly --
-- and `stool` keeps its own pool, apart from anything it touches.
-- The `diner_stool` template (see `buildings` below) now models
-- every placement in full, like the house stool it copies; these
-- pins are its degradation path.
stool = { 7, 8, 23, 24 },
-- the `diner_stool` template stands 5 voxels (the drawn
-- elevation: the seat's front edge at row 10 over legs 11-14),
-- as the house stool does: whoever sits on a stool cell rides
-- this height, not the 8px class default
heights = { stool = 5 },
-- Deliberately NOT pinned:
-- $37 (55) is three different things -- the light half of the
-- checkerboard floor, the interior of the round tables, and
@@ -4169,6 +4217,26 @@ return {
roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 },
slab = 3, frontEave = 0, ledge = nil,
},
-- F07b: the SQUARE table of CELADON_MANSION_1F cells (0,6):(1,7)
-- (1 placement, scan.lua) -- the long table (F07) at two cells
-- wide, the same drawing to the tile everywhere but the interior
-- column count, and the same read to the row: 0-23 the tabletop
-- seen from above, 24-26 the slab's black/#555/black front edge,
-- 27 the #555 shadow that closes it (slab = 3, folded into the
-- band), 28-30 the base with the legs stopping one row short of
-- the grid. Family numbers unchanged; the `table` pin stays as
-- the degradation path, neutralized where this stamps.
{
id = "mansion_square_table",
tiles = {
{ 38, 39, 39, 41 },
{ 54, 55, 55, 57 },
{ 54, 55, 55, 57 },
{ 60, 58, 58, 59 },
},
roofRows = 28, roofBack = 24, roofFront = 0, roofCycle = { 2, 23 },
slab = 3, frontEave = 0, ledge = nil,
},
},
HOUSE = {
@@ -4480,5 +4548,93 @@ return {
},
},
},
LOBBY = {
-- F09 on the lobby atlas: the Celadon department store's stools
-- -- the diner's chairs, the roof terrace's, the Game Corner's
-- six rows and the four on 1F (58 placements, the scan's only
-- matches on this atlas). A DIFFERENT drawing from the house
-- stool -- it sits one row HIGHER in the tile (seat top rows 4-9
-- over its front edge at 10 and the legs at 11-14, with a clear
-- floor row below) and its leg detail differs -- but the same
-- object band for band, so it takes the house part table with
-- the bands shifted up one row. The measured ground line (15
-- here, 16 in the house) shifts with them, so the stand is the
-- same 5 voxels, and the tileset's `stool = 5` height override
-- keeps whoever sits here ON the seat. The old stool standee
-- pins stay as the degradation path.
{
id = "diner_stool",
tiles = {
{ 7, 8 },
{ 23, 24 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil,
panes = false,
parts = {
{ kind = "upright", x = { 2, 13 }, top = { 4, 9 },
facade = { 10, 14 }, z = 3, depth = 11,
stretch = true }, -- the stool
},
},
-- F11: the ROUND TABLE of the diner and the roof terrace -- 4
-- placements, all on this atlas (CELADON_DINER cells (0,2) and
-- (0,5), CELADON_MART_ROOF (4,2) and (8,4); scan
-- "9,39,39,25;54,55,55,57;70,55,55,71;85,86,87,55" matches
-- nowhere else). This is the drawing the long `counter` note
-- above calls a compromise -- its four interior tiles are $37,
-- unpinnable, so the flat treatment let the whole top BE a 16px
-- disc. The template matches the exact 4x4 grid instead, which
-- is what a per-tile pin can never do, and un-projects the three
-- facings: rows 0-23 the OCTAGONAL top seen from above (24
-- top-view rows = 24 depth rows, so the plan is the silhouette
-- itself, a `plan` slab 32x24), rows 24-25 the slab's #555/black
-- fascia folded down its rim, rows 26-31 the PEDESTAL seen under
-- the front edge -- two flattened circles, i.e. horizontal discs:
-- the base (diameter 16, drawn cols 8-23, side rows 30-31, its
-- top wearing the drawn shadow-and-white rows 26-29) and the dark
-- column (diameter 6, cols 13-18, rows 26-27 repeating up the
-- shaft), both on the drawn centre x 16 / plan centre z 12.
-- MEASURED: plan, diameters, centres, slab 3. AUTHORED: tabletop
-- plane 8 -- counter height, developer-tuned (the first cut
-- stood it at the flat compromise's 16px and it read too tall)
-- -- plus base height 2 and the 3-voxel column between. `depth`
-- 3 keeps the plot to the drawn plan; the grid's 4th tile row is
-- the pedestal's own drawing plus one floor tile ($37 again, at
-- the southeast corner), which the claim paints as ground.
-- `scrub` repoints the top's interior field -- the four $37
-- tiles, ALSO the checkerboard floor's light half, which carry
-- the floor's palette in a colorized atlas -- at the same grey
-- sourced from the rim's own field, so the whole top wears the
-- table's palette (the drawn field there is uniform grey;
-- nothing drawn is lost). The old wall/counter pins on the rim
-- tiles stay as the degradation path, and `support` carries the
-- top plane so anything the standee scan finds on these cells
-- rides the tabletop.
{
id = "diner_round_table",
tiles = {
{ 9, 39, 39, 25 },
{ 54, 55, 55, 57 },
{ 70, 55, 55, 71 },
{ 85, 86, 87, 55 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil, depth = 3,
panes = false, support = 8,
scrub = { { 8, 8, 23, 23 } },
parts = {
{ kind = "disc", cx2 = 32, cz2 = 24, r = 8, rise = 0, h = 2,
side = { rows = { 30, 31 }, x = { 13, 18 } },
cap = { rows = { 26, 29 }, x = { 9, 22 } } }, -- the base
{ kind = "disc", cx2 = 32, cz2 = 24, r = 3, rise = 2, h = 3,
side = { rows = { 26, 27 }, x = { 14, 17 } } }, -- the column
{ kind = "plan", x = { 0, 31 }, rows = { 0, 23 },
fascia = { 24, 25 }, fasciaX = { 8, 23 },
rise = 5 }, -- the top
},
},
},
},
}
+90 -1
View File
@@ -436,7 +436,8 @@ local function deskSetModel(sp, pr, t)
local function buildParts(plane)
for _, p in ipairs(t.parts) do
Budget.tick()
local x0, x1 = p.x[1], p.x[2]
local x0 = p.x and p.x[1] or 0
local x1 = p.x and p.x[2] or (W - 1)
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
@@ -561,6 +562,94 @@ local function deskSetModel(sp, pr, t)
end
end
end
elseif p.kind == "plan" then
-- A PLAN part is a slab whose plan IS the drawn top view: the
-- band's silhouette becomes the footprint pixel for pixel
-- (drawn row = depth row, the same 1:1 every tabletop is drawn
-- with), so an octagonal top stands as an octagon rather than
-- the box no rectangular band can escape. The top layer wears
-- the band itself, outline and all; the rim layers below wear
-- the drawn fascia rows folded down the edge (x clamped into
-- the drawn fascia's span), and the slab's unseen interior the
-- field's dark texel.
local r0, r1 = p.rows[1], p.rows[2]
local f0, f1 = p.fascia[1], p.fascia[2]
local fx0, fx1 = p.fasciaX[1], p.fasciaX[2]
local rise = p.rise or 0
local h = (f1 - f0 + 1) + 1
if rise + h > ytop then ytop = rise + h end
local function drawn(sx, z)
return sx >= x0 and sx <= x1 and z >= 0 and z <= r1 - r0
and inside[(r0 + z) * W + sx]
end
for z = 0, r1 - r0 do
if z >= 0 and z < D then
local sy = r0 + z
for sx = x0, x1 do
if inside[sy * W + sx] then
put(sx, rise + h - 1, z, sy * W + sx)
local edge = not (drawn(sx - 1, z) and drawn(sx + 1, z)
and drawn(sx, z - 1) and drawn(sx, z + 1))
for y = rise, rise + h - 2 do
if edge then
local fsx = math.max(fx0, math.min(fx1, sx))
put(sx, y, z, (f0 + (rise + h - 2 - y)) * W + fsx)
else
put(sx, y, z, pr.shadeTexel[DARK])
end
end
end
end
end
end
elseif p.kind == "disc" then
-- A DISC part is ROUND IN PLAN -- the pedestal column and base
-- the projection can only draw from the front. Centre and
-- radius are measured off the drawn widths (a flattened arc is
-- a horizontal circle seen from above); the circular footprint
-- is synthesized like any continued geometry, and every voxel
-- still wears the drawing: the side folds the drawn face-on
-- rows around the hull (x clamped into the drawn span, rows
-- repeating up the height), and `cap` lays the drawn top-view
-- rows over the top layer's interior, drawn north rows to the
-- plan's north. `cx2`/`cz2` are DOUBLED plan centres, so an
-- even diameter keeps its centre between two voxels instead of
-- limping one off.
local r, rise, h = p.r, p.rise or 0, p.h
local s0, s1 = p.side.rows[1], p.side.rows[2]
local sa0, sa1 = p.side.x[1], p.side.x[2]
local sn = s1 - s0 + 1
if rise + h > ytop then ytop = rise + h end
local function inDisc(x, z)
local dx = 2 * x + 1 - p.cx2
local dz = 2 * z + 1 - p.cz2
return dx * dx + dz * dz <= 4 * r * r
end
local zlo = math.floor((p.cz2 - 2 * r) / 2)
for x = math.floor((p.cx2 - 2 * r) / 2),
math.floor((p.cx2 + 2 * r) / 2) do
for z = math.max(0, zlo),
math.min(D - 1, math.floor((p.cz2 + 2 * r) / 2)) do
if inDisc(x, z) then
local edge = not (inDisc(x - 1, z) and inDisc(x + 1, z)
and inDisc(x, z - 1) and inDisc(x, z + 1))
for y = rise, rise + h - 1 do
local sx, sy
if p.cap and y == rise + h - 1 and not edge then
local c0, c1 = p.cap.rows[1], p.cap.rows[2]
sy = math.min(c1, c0 + math.floor((z - zlo)
* (c1 - c0 + 1)
/ (2 * r)))
sx = math.max(p.cap.x[1], math.min(p.cap.x[2], x))
else
sy = s0 + (rise + h - 1 - y) % sn
sx = math.max(sa0, math.min(sa1, x))
end
put(x, y, z, sy * W + sx)
end
end
end
end
else
local tr0, tr1 = p.top[1], p.top[2]
local fr0, fr1 = p.facade[1], p.facade[2]
+208 -2
View File
@@ -680,6 +680,23 @@ TEMPLATES = {
roof_rows=28, roof_back=24, roof_front=0, roof_cycle=(2, 23),
slab=3, front_eave=0, ledge=None, tileset="mansion",
),
# F07b: the SQUARE table of CELADON_MANSION_1F cells (0,6):(1,7)
# (1 placement) -- the long table (F07) at two cells wide, the same
# drawing to the tile everywhere but the interior column count, and
# the same read to the row: 0-23 the tabletop seen from above, 24-26
# the slab's black/#555/black front edge, 27 the #555 shadow that
# closes it (slab = 3, folded into the band), 28-30 the base with the
# legs stopping one row short of the grid. Family numbers unchanged.
"mansion_square_table": dict(
tiles=[
[38, 39, 39, 41],
[54, 55, 55, 57],
[54, 55, 55, 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="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
@@ -853,6 +870,79 @@ TEMPLATES = {
z=3, depth=11, stretch=True), # the stool
],
),
# F09 on the lobby atlas: the Celadon department store's stools --
# the diner's chairs, the roof terrace's, the Game Corner's six
# rows and the four on 1F (58 placements). A DIFFERENT drawing from
# the house stool -- it sits one row HIGHER in the tile (seat top
# rows 4-9 over front edge 10 and legs 11-14, with a clear floor
# row below) and its leg detail differs -- but the same object band
# for band, so it takes the same part table with the bands shifted
# up one row. The measured ground line (15 here, 16 in the house)
# shifts with them, so the stand is the same 5 voxels.
"diner_stool": dict(
tiles=[
[7, 8],
[23, 24],
],
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
slab=0, front_eave=0, ledge=None, tileset="lobby",
panes=False,
parts=[
dict(kind="upright", x=(2, 13), top=(4, 9), facade=(10, 14),
z=3, depth=11, stretch=True), # the stool
],
),
# F11: the ROUND TABLE of the Celadon diner and the roof terrace --
# 4 placements, all on the lobby atlas (CELADON_DINER cells (0,2)
# and (0,5), CELADON_MART_ROOF (4,2) and (8,4); scan
# "9,39,39,25;54,55,55,57;70,55,55,71;85,86,87,55", no matches on
# any other atlas). The drawing packs three facings no band split
# can reach: rows 0-23 are the OCTAGONAL top seen from above (24
# top-view rows = 24 depth rows, the same 1:1 every tabletop is
# drawn with -- so the plan is the silhouette itself, 32 wide by 24
# deep), rows 24-25 the slab's own fascia (#555 over black, folded
# down the rim under the band's drawn outline -> slab 3), and rows
# 26-31 the PEDESTAL seen under the front edge: the base's top
# surface with the dark column rising from its middle (26-27), its
# lit south half (28-29), and its front arc curving to the floor
# (30-31). The flattened arcs are horizontal CIRCLES seen from
# above -- depth, not narrowing -- so the pedestal is two discs:
# base diameter 16 (drawn cols 8-23), column diameter 6 (the dark
# blob's cols 13-18), both centred on the drawn centre x=16, plan
# centre z=12. MEASURED: plan, diameters, centre, slab 3. AUTHORED:
# tabletop plane 8 -- counter height, developer-tuned (the first
# cut stood it at the flat compromise's 16px and it read too tall)
# -- plus base height 2 and the 3-voxel column between (the
# projection cannot state either; the drawn base arc suggests a
# low disc). depth 3 keeps the plot to the drawn plan's 24 rows;
# the grid's 4th tile row is the pedestal's own drawing and the
# floor tile at its southeast corner, which the claim paints as
# ground. `scrub` repoints the top's interior field -- the four
# $37 tiles, which are ALSO the checkerboard floor's light half
# and carry the floor's palette in a colorized atlas -- at the
# same grey sourced from the rim's own field, so the whole top
# wears the table's palette (the drawn field there is uniform
# grey; nothing drawn is lost).
"diner_round_table": dict(
tiles=[
[9, 39, 39, 25],
[54, 55, 55, 57],
[70, 55, 55, 71],
[85, 86, 87, 55],
],
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
slab=0, front_eave=0, ledge=None, tileset="lobby", depth=3,
panes=False, scrub=[(8, 8, 23, 23)],
parts=[
dict(kind="disc", cx2=32, cz2=24, r=8, rise=0, h=2,
side=dict(rows=(30, 31), x=(13, 18)),
cap=dict(rows=(26, 29), x=(9, 22))), # the base
dict(kind="disc", cx2=32, cz2=24, r=3, rise=2, h=3,
side=dict(rows=(26, 27), x=(14, 17))), # the column
dict(kind="plan", x=(0, 31), rows=(0, 23),
fascia=(24, 25), fascia_x=(8, 23), rise=5), # the top
],
),
# F04: the Pokemon Center's PC -- every Center's northeast corner
# (11 placements) plus the Indigo Plateau lobby, whose MART tileset
# shares this atlas. The lab desk-set read again: a
@@ -1333,7 +1423,7 @@ def build_desk_set(sp, pr, t):
def build_parts(plane):
for p in t["parts"]:
x0, x1 = p["x"]
x0, x1 = p.get("x", (0, W - 1))
if p["kind"] == "flat":
r0, r1 = p["rows"]
# `at` names the sheet's own height when it does not lie
@@ -1445,6 +1535,91 @@ def build_desk_set(sp, pr, t):
if pr0 <= sy <= pr1 and inside(sx, sy):
put(sx, plane + y, z, sx, sy)
continue
if p["kind"] == "plan":
# A PLAN part is a slab whose plan IS the drawn top view:
# the band's silhouette becomes the footprint pixel for
# pixel (drawn row = depth row, the same 1:1 every
# tabletop is drawn with), so an octagonal top stands as
# an octagon rather than the box no rectangular band can
# escape. The top layer wears the band itself, outline
# and all; the rim layers below wear the drawn fascia
# rows folded down the edge (x clamped into the drawn
# fascia's span), and the slab's unseen interior the
# field's dark texel.
r0, r1 = p["rows"]
f0, f1 = p["fascia"]
fx0, fx1 = p["fascia_x"]
rise = p.get("rise", 0)
h = (f1 - f0 + 1) + 1
dark = shade_px.get(DARK) or shade_px[BLACK]
def drawn(sx, z):
return (x0 <= sx <= x1 and 0 <= z <= r1 - r0
and inside(sx, r0 + z))
for z in range(r1 - r0 + 1):
if not 0 <= z < D:
continue
sy = r0 + z
for sx in range(x0, x1 + 1):
if not inside(sx, sy):
continue
put(sx, rise + h - 1, z, sx, sy)
edge = not (drawn(sx - 1, z) and drawn(sx + 1, z)
and drawn(sx, z - 1) and drawn(sx, z + 1))
for y in range(rise, rise + h - 1):
if edge:
put(sx, y, z, max(fx0, min(fx1, sx)),
f0 + (rise + h - 2 - y))
else:
put(sx, y, z, dark[0], dark[1])
continue
if p["kind"] == "disc":
# A DISC part is ROUND IN PLAN -- the pedestal column
# and base the projection can only draw from the front.
# Centre and radius are measured off the drawn widths
# (a flattened arc is a horizontal circle seen from
# above); the circular footprint is synthesized like any
# continued geometry, and every voxel still wears the
# drawing: the side folds the drawn face-on rows around
# the hull (x clamped into the drawn span, rows
# repeating up the height), and `cap` lays the drawn
# top-view rows over the top layer's interior, drawn
# north rows to the plan's north. `cx2`/`cz2` are
# DOUBLED plan centres, so an even diameter keeps its
# centre between two voxels instead of limping one off.
r, rise, h = p["r"], p.get("rise", 0), p["h"]
s0, s1 = p["side"]["rows"]
sa0, sa1 = p["side"]["x"]
sn = s1 - s0 + 1
cap = p.get("cap")
def in_disc(x, z):
dx = 2 * x + 1 - p["cx2"]
dz = 2 * z + 1 - p["cz2"]
return dx * dx + dz * dz <= 4 * r * r
zlo = (p["cz2"] - 2 * r) // 2
for x in range((p["cx2"] - 2 * r) // 2,
(p["cx2"] + 2 * r) // 2 + 1):
for z in range(max(0, zlo),
min(D - 1, (p["cz2"] + 2 * r) // 2) + 1):
if not in_disc(x, z):
continue
edge = not (in_disc(x - 1, z) and in_disc(x + 1, z)
and in_disc(x, z - 1)
and in_disc(x, z + 1))
for y in range(rise, rise + h):
if cap and y == rise + h - 1 and not edge:
c0, c1 = cap["rows"]
sy = min(c1, c0 + ((z - zlo)
* (c1 - c0 + 1)) // (2 * r))
sx = max(cap["x"][0], min(cap["x"][1], x))
else:
sy = s0 + (rise + h - 1 - y) % sn
sx = max(sa0, min(sa1, x))
put(x, y, z, sx, sy)
continue
tr0, tr1 = p["top"]
fr0, fr1 = p["facade"]
pd = p["depth"]
@@ -1933,7 +2108,7 @@ def verify_desk_set(vox, pr, t):
# sink) -- and nothing stands anywhere else
tops = {}
for p in t["parts"]:
x0, x1 = p["x"]
x0, x1 = p.get("x", (0, W - 1))
if p["kind"] == "flat":
r0, r1 = p["rows"]
z0 = p.get("z", r0)
@@ -1979,6 +2154,37 @@ def verify_desk_set(vox, pr, t):
for y in range(plane, plane + h + 1):
assert (x, y, z) in vox, \
f"iso box hole at {x},{y},{z}"
elif p["kind"] == "plan":
# its one geometric intent: the plan IS the drawn band's
# silhouette -- a solid slab column wherever the band draws
r0, r1 = p["rows"]
h = (p["fascia"][1] - p["fascia"][0] + 1) + 1
rise = p.get("rise", 0)
for x in range(x0, x1 + 1):
for z in range(min(r1 - r0 + 1, D)):
if not pr["inside"](x, r0 + z):
continue
tops[(x, z)] = max(tops.get((x, z), 0), rise + h - 1)
for y in range(rise, rise + h):
assert (x, y, z) in vox, \
f"plan slab hole at {x},{y},{z}"
elif p["kind"] == "disc":
# its one geometric intent: a solid circle in plan at every
# layer, symmetric about the authored centre
r, rise, h = p["r"], p.get("rise", 0), p["h"]
in_disc = lambda x, z: ((2 * x + 1 - p["cx2"]) ** 2
+ (2 * z + 1 - p["cz2"]) ** 2
<= 4 * r * r)
for x in range((p["cx2"] - 2 * r) // 2,
(p["cx2"] + 2 * r) // 2 + 1):
for z in range(D):
if not in_disc(x, z):
continue
assert in_disc(p["cx2"] - 1 - x, z), \
f"disc asymmetric at {x},{z}"
tops[(x, z)] = max(tops.get((x, z), 0), rise + h - 1)
for y in range(rise, rise + h):
assert (x, y, z) in vox, f"disc hole at {x},{y},{z}"
else:
fr0, fr1 = p["facade"]
ytp = plane + p.get("rise", 0) + (fr1 - fr0)