PIXELPUNK JOURNAL / CHAPTER 08 Editorial evidence captured 23 September 2026. These are historical project notes. Status and next-step instructions in these excerpts belong to their original development session. Performance and test results quoted here are from those sessions, not tests performed by the journal build. ======================================================================== docs/visual-handoff.md | original lines 5–177 ======================================================================== **Feel B0a Shared composition layer — part a (23 September 2026):** A new library, `scripts/composition.gd` (`class_name Composition`), which B3's clutter and B4's foliage are meant to place through. Nothing in the game calls it yet: `main.gd`, `world_view.gd`, `architecture.gd` and `scene_state.gd` are unchanged, and a test pins that. So no existing scene changes, and nothing is stored. - **Keep-clear map** (`keep_clear_zones`, `keep_clear`, `chunk_field`). Each source is a capsule with a smoothstep falloff. They are combined by maximum on a 0.5 m grid, over any rect or one 8 m terrain chunk. The sources: - the front door, found by the bay rule the fence uses (`front_door_x`, checked against `Architecture.fence_panels`); - path doors, arches and door-windows (`Openings.holes_by_side`), and tower doors; - windows with a sill under 1 m, at strength 0.5; - wall gates, both sides; - stair feet and landings, bridge landings and terrace flights; - the base and rotation handles (0.7) and the face knobs (0.4). - **Visual-mass heatmap** (`mass_map`). A body adds its height over its footprint plus a halo that fades over 1 to 3 m, depending on its height. A wall adds its height along its band, and a bridge or stair adds half its height. Every other part is a circle of half its longer side. `item(kind)` gives the circle radius and the mass (circle area × height). - **Packing** (`pack`): circles packed largest first, keeping clear of the obstacles given. - **Groups** (`groups`): - a *lane* is a 4-connected run of path cells with two or more ground buildings within 1.5 m; - a *courtyard* is a 4-connected patch of open ground of 8 to 256 cells (houses and wall bands block cells, which is what splits a yard off the meadow) with at least one ground house or wall band within 1.5 m of it. That is not a test of enclosure: one house beside a small patch is enough. A bare patch with nothing beside it, and larger open ground, are open country and are not dressed. - A group's seed hashes its key, its members' seeds and the seeds of the parts standing on it, so a `--seeds` sweep recomposes it. - **Composition gravity** (`compose`): `CANDIDATES` = 30 seeded layouts per group, and the best score is kept. - Each piece takes the best of 14 samples. The sample cost adds keep-clear, mass heat and the imbalance after adding the piece. It also adds a pull toward the other pieces, the lane's walk line or the courtyard's edge, and seeded noise. - A layout scores 3 × |imbalance − 0.15|, plus its mean keep-clear and heat, plus 0.5 per piece that found no room. The target is 0.15 of the group radius, not zero, so the most balanced candidate is passed over. - **Focal anchor:** - Each group gets one anchor, placed first with keep-clear ≤ 0.5. In a lane it goes beside a door; in a courtyard, a third of the way out from the centre. - An existing tree, pergola, café table or similar standing in the group is adopted as the anchor instead. - A piece may carry at most half the anchor's mass, must be smaller than it, and keeps 0.45 m from it. - **Other API:** `signature()` covers everything except the measured µs. `bake()` writes a layout as ordinary objects; only the audit generators use it. `debug_image()` draws the top-down review map. - The anchor and piece palettes are stand-ins for B3 and B4: - lane anchors: café table, umbrella, tree; lane pieces: bench, pots, planter, bin, bollard, lantern, flower patch; - courtyard anchors: tree, blossom, pergola; courtyard pieces: shrubs, flower patches, pots, bench, planter. New audit scenes (their generators are in `artifacts/visual-audit/`): - `lane.json` (`make_lane.gd`): nine houses in four styles on a 4 m cobbled lane, with a piazza, an earth alley, a stair to a terrace, a low window and a lantern. It reads as one lane group of 164 cells. - `garden.json` (`make_garden.gd`): a courtyard of three houses and a low wall with a gated path, a stair and existing planting (89 cells), and a ring-walled lawn with a tree that is adopted as its anchor (54 cells). - The runner passes no user arguments, so `AUDIT_OUT` stands in for `--out=`. With the library present, both generators also write `artifacts/visual-audit/composed/{lane,garden}-s0..s5.json` (s0 is the groups' own seeds). - `make_composition_maps.gd` writes the maps (log `artifacts/feel/07-B0/a-maps.log`). Files: `scripts/composition.gd` (new), `tests/test_composition.gd` (new), `tools/test.sh` and `tools/Test.ps1` (suite added), and the three generators above. Pre-edit copies are in `artifacts/feel/07-B0/a-bak/`. Snapshot: `build/backup-feel/07-B0/`. Tests (logs in `artifacts/feel/07-B0/`): - Test first: `a-testfirst-test_composition.log(.out)`, 1 check, 1 failure (the library was missing, loaded by path so nothing hangs). - After: `a-run2-test_composition.log`, 105 checks, 0 failures. - Full `tools/test.sh` in one foreground call (`a-full-test.log`): 20 suites, 6,406 checks, 0 failures (`a-full-counts.txt`; 6,301 before, plus the 105 new ones), and `PIXELPUNK_INTEGRATION []` (`a-full-verify.log`). - The suite covers: - field geometry; - every keep-clear source, with its monotone soft falloff; - mass ordering and removal; - packing order, overlap, bounds and determinism; - lane and courtyard detection, with exact cell counts on fixtures; - 30 candidates, with the best kept but not the most balanced; - new mass landing opposite a heavy west side; - placements on the group's cells, outside keep-clear, clear of houses, doors and each other; - the focal hierarchy and the adopted tree; - deterministic output over a save round trip, and at least 5 of 6 seeds distinct; - the document untouched by `compose`; - both audit scenes; - baked objects valid and stored as floats. - Three fixture corrections after the test-first run, none of which weakens a check: - the test window moved from y 0.9 to 1.3, because `Openings.window_groups` makes a window at 0.9 a door-window; - the gate fixture uses `paint_area`, because `paint_line` with radius 0 paints nothing; - `overlap_free` no longer counts pairs of parts that already stood in the scene. Changed expectations: none. It is a new suite, and no existing assertion moved. Captures: `artifacts/feel-journey/07-B0/pkg/`, 1600x1000, made by `artifacts/feel/07-B0/a-cap.sh`. "Before" is the pre-stage tree (`tools/feel_snapshot.sh tree 07-B0`) loading the same JSON. - `before-/after-{lane,lanewide,garden}-{day,night}`: counts are identical, apart from timing. 0.5 to 1.5 % of pixels differ (`a-diff.log`, `diff-*.png`), and they are rain streaks and sparkle only (looked at `diff-garden-night`, `diff-lane-day`). - `composed-garden-s{0,1,2}-day`, `-s0-night`, `-s0-grey`: one anchor per garden with smaller pieces round it. - s0: a new tree in the courtyard, and the ring lawn's existing tree with pots and patches. - s1: a pergola with benches. - In greyscale the tree is each garden's focal point. - `composed-lane-s{0,1,2}-day`, `-s0-night`, `-s0-grey`: the lane camera sees mostly roofs. - s0: only a bin, a pot, a shrub and a bench show; the tree anchor stands at the piazza mouth, out of frame. - s1: the café-table anchor shows mid-lane, with a bench and pots at the stair end. - s2: a shrub, pots, a bin, a bench, and a lantern at the far end of the alley. - Greyscale gives no focal read at this camera. - `maps/lane-lane-5_-10.png`, `maps/garden-courtyard-{-7_-5,10_-11}.png`: seeds 0 to 5, three across by two down; the legend is in `make_composition_maps.gd`. These maps are the lane's review evidence. - The lane's door zones cover most of its 4 m width, so the pieces gather in the piazza, the alley and the ends. `DOOR_ZONE` is the knob for B3. Measurements (same sitting): | | Before | After | |---|---|---| | Dense fixture mean frame, 3 alternating runs, median (`a-dense.txt`, `a-dense-*`) | 13.03 ms (12.51–13.13) | 13.08 ms (12.47–13.10) | | Dense fixture p95, median | 13.33 ms | 13.33 ms | | Dense fixture draw calls | 2035 | 2035 | - The library is not called, so the dense difference is run-to-run noise. - `compose` cost per group, 30 candidates, one headless run (`a-maps.log`, `a-run2` log): - lane.json lane: 62 to 68 ms; - garden courtyards: 44 to 53 ms; - yard fixture: 22 ms. - The per-call context (both maps plus the allowed mask) is rebuilt on every call. - No drag-harness run: this is a track B part. Review fix (23 September 2026, one must-fix finding): - Found: a courtyard needed no member. Any 8 to 256-cell patch of open ground with no house or wall near it became a courtyard and was dressed. The reviewer's bare 10 x 10 meadow gave `courtyard:0,0 cells=100 members=[]` and a blossom anchor with 9 placements (`artifacts/feel/07-B0/review/rv_checks.log`), which contradicted the rule text above. - Fixed: `Composition.groups` skips a courtyard whose member list is empty (`if edges.is_empty(): continue`), the counterpart of the lane's two-house guard. The rule text above now says what the code does. Pre-fix copies: `artifacts/feel/07-B0/fix-bak/`. - Test first: two new checks in `group_checks`. The bare patch is no group, and the same patch with one house beside it is a courtyard with one member. `fix-testfirst-test_composition.log`: 107 checks, 1 failure (the bare patch). After: `fix-run1-test_composition.log`, 107 checks, 0 failures. The reviewer's script re-run (`fix-rv_checks.log`) gives `lone patch groups: []`; its 9 other failures are its byte-for-byte round-trip checks on the composed JSON, which were not a must-fix finding and are not touched. - Full `tools/test.sh` (`fix-full-test.log`, `fix-full-counts.txt`): 20 suites, 6,408 checks, 0 failures (6,406 plus the 2 new ones). The `--verify` step inside that run left only the engine header in its log (`fix-full-verify-truncated.log`), so it was run again alone: `fix-verify2.log`, `PIXELPUNK_INTEGRATION []`. - Audit scenes unchanged: both generators re-run into `artifacts/feel/07-B0/fix-regen/` (logs `fix-regen-{garden,lane}.log`). `lane.json`, `garden.json` and all twelve `composed/*-s0..s5.json` are byte-identical to the published ones (garden's courtyards have 2 and 4 members). So no capture changed and none was retaken. - Changed expectations: none. Review: . Not done: - Reroll, the stored seed override, the `--seeds`/greyscale sweep review and placement cost on the dense fixture are part b. - Terrace and single-facade groups are not derived; a caller can pass its own group. - A canal front's generated ground-floor bay windows are not keep-clear sources. Only placed windows and path openings are. - Lane seeds with a tree anchor miss the imbalance target (0.44 and 0.32 at s0 and s2). The anchor sites are beside doors, and a tree outweighs everything else in the lane. - At 44 to 68 ms per group, `compose` is too slow to run inside an edit as it stands. Platforms: Mac run; Windows not run. ======================================================================== docs/visual-handoff.md | original lines 562–600 ======================================================================== **Feel C1g Creatures owner gate: the mallard — part a0 (23 September 2026):** This part adds the mallard only: one procedural low-poly mesh (272 triangles, `artifacts/feel/06-C1g/probe_tris.log`; flat faces, one palette colour per face) and three poses, paddle, dabble and wake, bent in the vertex shader. It swims on Harbour water only when `--creatures` is passed, as three birds in front of the south quay, one per pose. It then stops for the owner's verdict. No other species, no steering, no reactions. - **Look:** - Size: 1.5 times a real mallard, 0.87 m from tail to bill, with the head top 0.55 m above the water. - A big head: 0.24 m wide against a 0.40 m body. - Palette: taken from 3x3 averages of the sheet's day 8x cells (`artifacts/feel/06-C1g/probe_palette.log`, `probe_bill.log`). Head `288b5c`, chest `7b4d3b`, body `4f5a71`, cream wing patch and belly `f7e8d2`, bill `ec8608` (the sheet's mid orange, not its yellow highlight), feet `be7338`, tail `141824`, back `202330`. The collar is white `eef2e6`, as the owner asked; the sheet's collar is pale green. - Lighting: the body is lit by the scene (`diffuse_burley`), so night comes from the renderer. - Outline: a one-art-pixel dark outline pass (back faces pushed out in clip space) keeps the silhouette readable at 1x, as the sheet's outline does. This is my choice, not the owner's, and one constant turns it off (`Mallard.OUTLINE_PIXELS`). - Ripples: a lit shader effect on one flat quad per bird (amendment 2 of the 23 September owner decisions): two rings when paddling, tighter rings when dabbling, rings plus a V trail when in wake. - **Poses** (`shaders/mallard_pose.gdshaderinc`, mirrored by `Mallard.pose_point`): - Paddle: a 1.2 cm bob, a nod and a tail flick. - Dabble: the neck drops, then the bird tips 70° (±4° rocking) nose-down about the front of the hull. The tail stands about 0.6 m up and the orange feet come out. Whatever bends under the water is discarded at the waterline. The dabbling bird faces away from the camera (heading 1.7), so its belly and rump face the viewer as on the sheet. - Wake: the neck is stretched 7 cm forward and 5 cm lower, with a 5° trim and a quicker bob. - The clock: an automated run holds the creature clock at `Mallard.CAPTURE_TIME`, so captures repeat; in the editor it runs. - **Runtime only.** Nothing is saved, nothing goes into undo, and the document is unchanged (tested). Painting land over a spot takes that bird away on the next sync. - **GL Compatibility finding:** a multimesh without instance colours multiplies the vertex colour by black, so the first capture drew black birds (`artifacts/feel/06-C1g/probe-day-first-black/`). `make_flock` sets `use_colors` and gives every instance white. For C1a, start from `scripts/mallard.gd`: - `build_mesh()` / `build_arrays()`: the mesh. Vertex colour is the palette, UV holds the head and tail pose weights, CUSTOM0 holds the smooth outline normal. ======================================================================== scripts/composition.gd | original lines 1–73 ======================================================================== extends RefCounted class_name Composition # Feel B0, the shared composition layer (docs/tiny-glade-feel-plan.md B0). B3's clutter and B4's # foliage place through it, so set dressing reads as composed rather than scattered. The pipeline # is Opara's [O4 slides 11-12, 17-18, 35; O2 11:36-20:47]: # 1. a keep-clear map: doors, path openings, stair feet, landings, window fronts and the editor # handles' reach, each a capsule with a soft falloff, combined by maximum on a CELL grid; # 2. a visual-mass heatmap: every placed or derived object adds mass by footprint and height, # with a halo that grows with its height, so new pieces prefer the quiet cells; # 3. footprints reduced to circles and packed largest first; # 4. composition gravity: CANDIDATES seeded layouts per group, each piece placed where it best # rebalances the group round its centre, and the layout kept whose residual imbalance is # nearest TARGET_IMBALANCE, so the result stops short of perfect balance on purpose [O2 18:32]; # 5. one focal anchor per lane or courtyard group (a door with clutter, a tree, a lamp), placed # first, with pieces no heavier than PIECE_SHARE of it that keep ANCHOR_GAP from it. # Everything here is derived (plan rule 4): nothing writes the document except bake(), which the # audit generators use to write composed variants, and reroll(), which writes the one stored # exception the plan allows, an optional seed override on the group's owner (B0b; see OVERRIDE). Output is a pure function of the # scene and the seed: RandomNumberGenerator seeded from string hashes, cells sorted, no clocks # (the reported usec is measurement only and is left out of signature()). # Groups: a lane is a connected run of path cells fronted by two or more buildings; a courtyard is # an enclosed piece of open ground between GROUP_MIN and COURTYARD_MAX cells. Terraces and # single facades are not derived yet; a caller can pass its own group dictionary to compose(). const CELL = 0.5 # keep-clear, mass and placement grid, metres const CHUNK = 8.0 # the terrain chunk (Architecture.terrain_chunk) a chunk field covers const MARGIN = 3.0 # a group's fields reach this far past its cells const CANDIDATES = 30 # layouts weighed per group (plan: "about 30") const SAMPLES = 14 # positions tried per piece const ANCHOR_TRIES = 12 # positions tried round an anchor site const PACK_TRIES = 80 # positions tried per circle by pack() const TARGET_IMBALANCE = 0.15 # of the group radius: the balance the layout aims for, not zero const KEEP_HARD = 0.85 # a piece's centre never stands where the keep-clear value is above this const ANCHOR_KEEP = 0.5 # nor an anchor's above this const GAP = 0.15 # between two circles const ANCHOR_GAP = 0.45 # between a piece and the anchor: room for the focal point to breathe const PIECE_SHARE = 0.5 # no piece carries more than this of the anchor's mass const MASS_SOFT = 2.0 # mass density at which the normalised heat reaches one half const COURTYARD_MAX = 256 # open ground larger than this (cells) is open country, not a courtyard const GROUP_MIN = 8 # smaller pockets are not dressed const LANE_FRONT = 1.5 # a building within this of a path fronts it const AREA_PER_PIECE = {"lane": 10.0, "courtyard": 12.0} const MIN_PIECES = 3 const MAX_PIECES = 9 # Keep-clear shapes, metres: [reach out from the face, half width, falloff, strength]. const DOOR_ZONE = [1.2, 0.8, 1.5, 1.0] const WINDOW_ZONE = [0.7, 0.5, 0.6, 0.5] const OPENING_ZONE = [1.5, 0.6, 1.2, 1.0] const STAIR_ZONE = [1.0, 0.7, 1.2, 1.0] const HANDLE_ZONE = [0.0, 0.3, 0.4, 0.7] const FACE_HANDLE = 0.4 # strength of a face knob's reach (it floats at mid height) const LOW_WINDOW = 1.0 # a window whose sill is under this is at the foot of the wall # Piece costs (per sample) and layout scores (per candidate). const W_KEEP = 2.0 const W_MASS = 1.0 const W_GRAV = 1.5 const W_CLUMP = 0.8 const W_WALK = 1.2 const W_EDGE = 0.5 const W_NOISE = 0.6 const S_BALANCE = 3.0 const S_KEEP = 1.0 const S_MASS = 1.0 const S_MISSING = 0.5 # The palettes the audit scenes are dressed from. B3 and B4 bring their own kinds; these stand # for "a door with clutter, a tree, a lamp" and their smaller company. const ANCHORS = {"lane": ["cafe_table", "umbrella", "tree"], "courtyard": ["tree", "blossom", "pergola"]} const PIECES = {"lane": ["bench", "pot", "pot", "planter", "litter_bin", "bollard", "lantern", "flower_patch"], "courtyard": ["shrub", "shrub", "flower_patch", "flower_patch", "pot", "bench", "planter", "pot"]} const ANCHOR_KINDS = ["tree", "blossom", "pergola", "tree_seat", "cafe_table", "umbrella"] const SKIP_MASS = ["window"] # B0b, curation. A group is owned by its lowest-id member; a reroll writes the group's new seed into # that member's optional OVERRIDE dictionary under the group's kind, and nothing else. Rerolls walk