From 30b86a9ef773b6fe351eb00cf0a76589ae9374aa Mon Sep 17 00:00:00 2001 From: m3ultra Date: Mon, 3 Aug 2026 20:46:43 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20E=20R39=20(2/n):=20THE=20ARCADE,=20MEASU?= =?UTF-8?q?RED=20THEN=20DRESSED=20=E2=80=94=203=20skins=20live=20($0,=20on?= =?UTF-8?q?-device,=20green),=20and=20the=20instance=20count=20taken=20BEF?= =?UTF-8?q?ORE=20generating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MEASURED FIRST (pipeline/arcade_measure.mjs, over 5 seeds, off Lane A's own generatePlan): the arcade is ONE 42.00 m x 5 m edge, 2 blocks, 16-17 shops (17 at the default seed), frontage 3.00-4.94 m (mean 3.87-4.05), depth 5.1-8.0 m, 4-11 of them two-storey. Walkable 5.00 m between lot faces. AWNING_DEPTH 2.2 from each face means the two slabs already meet 0.60 m apart over the centreline — the arcade IS roofed, and the roof is an accident. ⚠ AND THE POSTS ARE IN THE WRONG PLACE: buildings.js:643 puts them 2.00 m out from each lot face, i.e. +/-0.50 m from the centreline — two rows of posts 1.00 m apart down a 5.00 m lane. No collider, so it walks; it just reads as a colonnade planted in its own doorway. Lane B ask filed. SHIPPED (all riding existing shared materials or stating their draw): facade-shuttered 768x595 (the 1.29:1 arcade lot aspect, not the pool's 1.78:1) — the dead tenant. +0 draws, +0 tris, 1 of the 14 FREE facade-atlas slots (22 of 36 used). ground-arcade-floor 512^2 terrazzo — +1 draw TOWN-WIDE, +0 tris (the 504 m2 lane quad moves out of footGeos). use:'arcade-floor' names a slot that does not exist yet, on purpose. ground-arcade-roof 512^2 pressed-metal + wired glass — +1 draw in the 1-2 arcade chunks, +2 tris. pipeline/seamless_tile.py: cut a regular-pattern skin to a whole number of its own periods instead of feathering the joint. It has a guard, because on this pair the naive version made things WORSE: the roof's x seam went 26.7 -> 8.3 (kept), the diffusion-drawn terrazzo floor is not periodic enough to cut on (autocorr r=0.26) and 27.9 -> 31.9 was rejected and passed through. Recorded, not hidden. manifest: facades 48->49, grounds 14->16. validate_manifest.py 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 --- pipeline/arcade_measure.mjs | 103 +++++++++++++++++++++++++ pipeline/build_manifest.py | 22 ++++++ pipeline/gen_skins.py | 54 +++++++++++++ pipeline/meshgod_batch_r39.json | 29 +++++++ pipeline/seamless_tile.py | 101 ++++++++++++++++++++++++ web/assets/gen/facade-shuttered.jpg | Bin 0 -> 89901 bytes web/assets/gen/ground-arcade-floor.jpg | Bin 0 -> 99269 bytes web/assets/gen/ground-arcade-roof.jpg | Bin 0 -> 35886 bytes web/assets/manifest.json | 15 ++++ 9 files changed, 324 insertions(+) create mode 100644 pipeline/arcade_measure.mjs create mode 100644 pipeline/meshgod_batch_r39.json create mode 100644 pipeline/seamless_tile.py create mode 100644 web/assets/gen/facade-shuttered.jpg create mode 100644 web/assets/gen/ground-arcade-floor.jpg create mode 100644 web/assets/gen/ground-arcade-roof.jpg diff --git a/pipeline/arcade_measure.mjs b/pipeline/arcade_measure.mjs new file mode 100644 index 0000000..1af68f2 --- /dev/null +++ b/pipeline/arcade_measure.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// PROCITY Lane E — arcade_measure.mjs (R39, item 39.4) +// +// THE SIZING LAW, MADE RUNNABLE. R38 paid for the law with `paling_fence`: a perfectly good +// 1,121-tri panel became a REJECTED asset the moment Lane B counted the placement rule (5 runs × +// 181 lots = 905 instances = 1.01 M triangles). The law is therefore: an asset's budget is +// `tris × the instance count the consuming lane's placement rule implies`, and you get that number +// BEFORE you generate. This script is that number for the arcade, so no later round has to trust a +// prose figure. +// +// node pipeline/arcade_measure.mjs [seed ...] +// +// Everything here is read out of Lane A's own `generatePlan` and Lane B's own constants; nothing +// is estimated. Constants mirrored from web/js/world/buildings.js are named at the point of use — +// if they move, this file is the second place to change (the R27 same-literal-in-two-files trap, +// declared rather than hidden). +import { generatePlan } from '../web/js/citygen/plan.js'; + +const AWNING_DEPTH = 2.2, AWNING_Y = 2.95, POST_INSET = 0.2; // buildings.js:21-22, :643 +const FOOT = 3.5; // ground.js:14 +const seeds = process.argv.slice(2).length ? process.argv.slice(2).map(Number) + : [20261990, 7, 123456, 999, 20260101]; + +const rows = []; +for (const s of seeds) { + const plan = generatePlan(s); + const arc = plan.districts.find((d) => d.kind === 'arcade'); + const nodeById = new Map(plan.streets.nodes.map((n) => [n.id, n])); + const aEdges = plan.streets.edges.filter((e) => e.kind === 'arcade'); + const len = aEdges.reduce((t, e) => { + const a = nodeById.get(e.a), b = nodeById.get(e.b); + return t + Math.hypot(b.x - a.x, b.z - a.z); + }, 0); + const width = aEdges[0].width; + const aBlocks = plan.blocks.filter((b) => b.district === arc.id); + const bIds = new Set(aBlocks.map((b) => b.id)); + const lots = plan.lots.filter((l) => bIds.has(l.block)); + const lotIds = new Set(lots.map((l) => l.id)); + const shops = plan.shops.filter((sh) => lotIds.has(sh.lot)); + const byType = {}; + for (const sh of shops) byType[sh.type] = (byType[sh.type] || 0) + 1; + const skins = new Set(plan.shops.map((sh) => String(sh.facadeSkin).replace(/\.jpe?g$/, '').replace(/^facade-/, ''))); + rows.push({ + seed: s, len: +len.toFixed(2), width, blocks: aBlocks.length, lots: lots.length, + shops: shops.length, byType, + frontage: [Math.min(...lots.map((l) => l.w)), Math.max(...lots.map((l) => l.w))], + meanFrontage: lots.reduce((t, l) => t + l.w, 0) / lots.length, + depth: [Math.min(...lots.map((l) => l.d)), Math.max(...lots.map((l) => l.d))], + twoStorey: shops.filter((sh) => sh.storeys >= 2).length, + townShops: plan.shops.length, townLots: plan.lots.length, + distinctSkins: skins.size, + unmarked: Math.max(2, Math.min(12, Math.round(plan.shops.length * 0.06))), + }); +} + +const f = (n, d = 2) => Number(n).toFixed(d); +console.log('THE ARCADE, MEASURED (synthetic only — plan_osm.js:305 is one unconditional'); +console.log('addDistrict("mainstreet"), so no cache town has an arcade DISTRICT. Real towns do have'); +console.log('1,102 arcade-KIND edges = 39 km of ordinary suburban footpath: key any arcade dressing'); +console.log('on district.kind, never on edge.kind. That is the v9 cut list as a code rule.\n'); +console.log('seed lane blocks lots shops 2-storey frontage(m) depth(m) town shops skins unmarked'); +for (const r of rows) { + console.log(`${String(r.seed).padEnd(11)} ${f(r.len)} m × ${r.width} m ${r.blocks} ${String(r.lots).padStart(2)} ${String(r.shops).padStart(2)} ${String(r.twoStorey).padStart(2)} ` + + `${f(r.frontage[0])}–${f(r.frontage[1])} (µ ${f(r.meanFrontage)}) ${f(r.depth[0])}–${f(r.depth[1])} ${String(r.townShops).padStart(4)} ${r.distinctSkins} ${r.unmarked}`); +} +const sh = rows.map((r) => r.shops); +console.log(`\nSHOPS PER ARCADE over ${rows.length} seeds: ${Math.min(...sh)}–${Math.max(...sh)} (charter says 17; seed 20261990 = ${rows[0].shops})`); +console.log('types seen:', JSON.stringify(rows.reduce((a, r) => { for (const [k, v] of Object.entries(r.byType)) a[k] = (a[k] || 0) + v; return a; }, {}))); + +// ── the geometry the dressing has to live inside ──────────────────────────────────────────────── +const half = rows[0].width / 2; +console.log('\nTHE LANE, from Lane B\'s own constants:'); +console.log(` lot faces stand at ±${f(half)} m from the centreline ⇒ WALKABLE ${f(half * 2)} m between shopfronts`); +console.log(` buildings.js gives every use:'shop' lot an awning ${AWNING_DEPTH} m deep at y=${AWNING_Y} m, so the two`); +console.log(` slabs reach to ±${f(half - AWNING_DEPTH)} m ⇒ THE ARCADE IS ALREADY ROOFED EXCEPT A ${f((half - AWNING_DEPTH) * 2)} m SLOT`); +console.log(` down the centreline. Ceiling height is ${AWNING_Y} m — a hanging sign must clear EYE 1.62 and sit under it.`); +console.log(` ⚠ THE POSTS ARE IN THE WRONG PLACE. buildings.js:643 plants the two awning posts at`); +console.log(` d/2 + ${AWNING_DEPTH} − ${POST_INSET} = ${f(AWNING_DEPTH - POST_INSET)} m out from the lot face, i.e. ±${f(half - (AWNING_DEPTH - POST_INSET))} m from the centreline —`); +console.log(` TWO ROWS OF POSTS ${f((half - (AWNING_DEPTH - POST_INSET)) * 2)} m APART DOWN THE MIDDLE OF A ${f(half * 2)} m LANE. They carry no collider`); +console.log(' (colliders.push(lotCollider(lot)) only), so the lane is walkable and the charter\'s 4.99 m'); +console.log(' stands — but visually the arcade is a colonnade planted in its own doorway. AWNING_DEPTH'); +console.log(' 2.2 was sized for a 3.5 m FOOT band, not a 2.5 m half-lane. → Lane B ask, see LANE_E_NOTES.'); +console.log(` ground.js pave: the arcade branch emits ONE footpath quad ${f(rows[0].len)} × ${f(rows[0].width + FOOT * 2)} m`); +console.log(` = ${f(rows[0].len * (rows[0].width + FOOT * 2), 0)} m² — the single biggest surface in the district, and it wears footSkin today.`); + +// ── the budget line every arcade asset has to answer ──────────────────────────────────────────── +console.log('\nINSTANCE COUNTS THE CONSUMING LANE IMPLIES (the number to generate against):'); +const n = rows[0].shops; +const rows2 = [ + ['hanging blade sign', 'one per arcade shop', n, 'synthetic only'], + ['shuttered facade skin', 'the dead tenant(s)', 1, '1–2 of the arcade\'s shops'], + ['arcade floor skin', 'one merged ground class', 1, 'one town-wide mesh'], + ['arcade roof/ceiling', 'one quad over the lane', 1, '1–2 chunks hold arcade lots'], + ['A-frame board', 'arcade ~4 + v9 unmarked cue', 4 + rows[0].unmarked, `≤${4 + rows[0].unmarked}/town (unmarked = clamp(round(shops×0.06),2,12))`], + ['key-cutter sign', 'the arcade\'s one trade sign', 1, 'landmark, not a repeat'], +]; +for (const [what, rule, cnt, note] of rows2) { + console.log(` ${what.padEnd(22)} ${String(cnt).padStart(2)} × ${rule.padEnd(28)} ${note}`); +} +console.log('\nA GLB CANNOT RIDE boxMats (BoxGeometry + one shared untextured shell material), so each'); +console.log('distinct GLB geometry is +1 draw. Ride it as ONE TOWN-WIDE InstancedMesh (magpie.js\'s'); +console.log('pattern, count 0 or 1..N) and it is +1 total; make it a per-chunk furniture class and it is'); +console.log('+1 × up to 25 live chunks, which at 8 draws of margin is a breach, not a cost.'); diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py index 2857d5c..e5f5924 100644 --- a/pipeline/build_manifest.py +++ b/pipeline/build_manifest.py @@ -104,6 +104,15 @@ FACADE_TYPES = { "market-fruit": ["stall"], "market-edge": ["stall"], "warehouse-roller": ["pawn", "general"], "warehouse-tin": ["pawn", "general"], "arcade-entry": ["general"], # arcade portal, not a shop — Lane B places by key + # ── R39 / v9 39.4 — the arcade's dead tenant. PLACED BY KEY, never drawn from a type pool: + # `shuttered` is deliberately absent from registry.js's SHOP_TYPES[*].facades, so no shop can + # roll it, and a shop that is OPEN must never wear a shutter. It is shot at 768x595 (1.29:1), + # the measured arcade lot aspect (mean frontage 3.87-4.05 m against FACADE_H 3.0), not the + # main-street pool's 1.78:1 — skins.js paints every facade into a SQUARE atlas slot, so a + # 1.78:1 source on a 1.29:1 quad is a 38% horizontal stretch. Costs +0 draws, +0 tris and one + # of the 14 FREE facade-atlas slots (the synthetic uses 22 of 36; skins.js:facadeAtlasUV warns + # and REUSES a slot past 36 — that is the real budget for a facade skin, not bytes). + "shuttered": ["general"], # residential fronts for use:'house' lots (incl. the milkbar corner-shop's street) "res-terrace": ["house"], "res-weatherboard": ["house"], "res-fibro": ["house"], "res-brickveneer": ["house"], @@ -126,6 +135,19 @@ GROUND_USE = { # without a vocabulary change. "grass-dry": "yard", "grass-temperate": "yard", "coastal-sand": "yard", "bitumen-reddust": "road", + # ── R39 / v9 39.4 — THE ARCADE'S TWO SURFACES. These two `use` values name slots that DO NOT + # EXIST YET, and unlike gravel→"verge" / reddust→"outback" (R38: selected by nothing, by + # accident) that is DELIBERATE and costed. The arcade cannot ride an existing slot: `footpath` + # is one town-wide merged mesh, so re-using it would lay terrazzo over every footpath in town. + # Each therefore needs its own class, and the price is stated: + # arcade-floor +1 draw TOWN-WIDE (ground.js builds once, not per chunk: a 6th merged mesh), + # +0 tris — the 42.00 x 12.00 m = 504 m2 lane quad MOVES out of footGeos. + # arcade-ceiling +1 draw in the 1-2 chunks that hold arcade lots, +2 tris (one 42 x 5 m quad). + # BOTH are synthetic-only by construction: plan_osm.js:305 is one unconditional + # addDistrict('mainstreet'), so no cache town has an arcade DISTRICT. Key on district.kind and + # never on edge.kind — the corpus has 1,102 arcade-KIND edges = 39 km of ordinary suburban + # footpath (v9 cut list). And both must be classic-gated: ?classic=1 does not move on dressing. + "arcade-floor": "arcade-floor", "arcade-roof": "arcade-ceiling", } # ── curated: interior floor / surface split of the tex-* skins ─────────────────────────── FLOOR_TEX = ["carpet-swirl", "carpet-mustard", "carpet-greygreen", "lino-check", "lino-cork", diff --git a/pipeline/gen_skins.py b/pipeline/gen_skins.py index a185810..5d78096 100644 --- a/pipeline/gen_skins.py +++ b/pipeline/gen_skins.py @@ -93,6 +93,42 @@ GROUNDS = { # slug (→ web/assets/gen/ground-.jpg) : subject "beach sand", } +# ── round-39 (v9 39.4): THE ARCADE'S SKINS ────────────────────────────────────────────────────── +# The synthetic's arcade is a 42.00 m covered lane, 16-17 shops, 2 blocks, already awning-roofed by +# accident (buildings.js AWNING_DEPTH 2.2 from each lot face, lane half-width 2.5 ⇒ the two slabs +# meet 0.6 m apart over the centreline) and dressed as NOTHING. These three are the cheapest three +# surfaces in it, in ascending draw cost: +# facade-shuttered +0 draws, +0 tris — one of the 14 FREE facade-atlas slots (the synthetic uses +# 22 of 36; skins.js:facadeAtlasUV warns and reuses a slot past 36) +# ground-arcade-tile +1 draw TOWN-WIDE (a 6th merged ground class; ground.js builds once, not per +# chunk), +0 tris — the lane quad moves out of footGeos into its own class +# tex-arcade-roof +1 draw in the 1-2 chunks that hold arcade lots, +2 tris (one 42x5 m quad) +# Aspect is not decoration: skins.js paints every facade into a SQUARE 341 px atlas slot, and arcade +# lots are 3.87 x 3.0 m ⇒ ~1.29:1, not the 1.78:1 the main-street pool is shot at. `facade-shuttered` +# is generated 4:3 so it lands on an arcade lot almost undistorted (1024x768 -> square -> 1.29:1 quad +# ≈ 1.03 net). Recorded, not retro-fitted to the other 48. +CEILING = ("Straight-up overhead photograph of {v}, camera pointing vertically at the ceiling, " + "completely flat-on with no perspective, evenly lit with no glare, no objects hanging " + "down, a uniform repeating surface texture filling the entire frame edge to edge") +SHUT = ("Straight-on photograph of a CLOSED and vacant small Australian shop front, {v}, no glass, " + "no people, no text or lettering anywhere, overcast daylight, flat front elevation filling " + "the entire frame edge to edge, nothing else visible") +ARCADE = { + # slug : (template, subject, width, height, style) + "ground-arcade-tile": (GROUND, + "the tiled floor of a 1970s Australian shopping arcade, small terrazzo and mosaic floor " + "tiles in beige, terracotta and cream laid in a simple repeating geometric pattern, " + "grouted joints, polished and worn smooth by foot traffic", 1024, 1024, "GROUND_STYLE"), + "tex-arcade-roof": (CEILING, + "the underside of a covered shopping arcade roof, cream pressed-metal ceiling panels in a " + "repeating square pattern between painted steel beams, with panels of ribbed wired glass " + "letting grey daylight through", 1024, 1024, "GROUND_STYLE"), + "facade-shuttered": (SHUT, + "a corrugated steel roller shutter pulled all the way down over the whole shopfront, faded " + "cream paint with rust streaks and scuff marks along the bottom, a blank empty signboard " + "above it, a scuffed tiled stall-riser at the base", 1024, 768, None), +} + PROMPTS = { # ── shop-type facades that the registry is thin on ────────────────────────────────── **{f"facade-{k}": FACADE.format(v=v) for k, v in { @@ -222,6 +258,24 @@ def harvest(): if __name__ == "__main__": if "--harvest" in sys.argv: harvest(); sys.exit(0) + if "--arcade" in sys.argv: # round-39 arcade kit (local only) — see ARCADE above + todo = {s: v for s, v in ARCADE.items() if not have(s)} + print(f"{len(todo)} arcade skins (MODELBEAST flux2-klein-4b, local·free)") + if "--dry-run" in sys.argv: + for s in sorted(todo): + print(f" {s} {todo[s][2]}x{todo[s][3]}") + sys.exit(0) + if not local_available(): + print(f"ERROR: MODELBEAST flux_local missing ({FLUX_RUN})."); sys.exit(2) + for i, (slug, (tpl, subj, w, h, sty)) in enumerate(sorted(todo.items()), 1): + try: + fn = gen_local(slug, tpl.format(v=subj), w=w, h=h, seed=39, + style=GROUND_STYLE if sty == "GROUND_STYLE" else None) + print(f"[{i}/{len(todo)}] {slug} {w}x{h} ({os.path.getsize(fn)//1024}KB)") + except Exception as e: + print(f"[{i}/{len(todo)}] {slug} FAILED: {str(e)[:90]}") + print("done. Review .genraw/, then --harvest → web/assets/gen/.") + sys.exit(0) if "--grounds" in sys.argv: # round-38 ground palette (square tile, local only) todo = {f"ground-{s}": GROUND.format(v=v) for s, v in GROUNDS.items() if not have(f"ground-{s}")} diff --git a/pipeline/meshgod_batch_r39.json b/pipeline/meshgod_batch_r39.json new file mode 100644 index 0000000..60352b7 --- /dev/null +++ b/pipeline/meshgod_batch_r39.json @@ -0,0 +1,29 @@ +{ + "_comment": "ROUND 39 (v9 39.4) THE ARCADE'S KIT for pipeline/gen_props.py --batch. The synthetic town's arcade is a 42.00 m covered lane, ONE edge of kind 'arcade' at width 5, 2 blocks, 16-17 shops (measured over 5 seeds: 17 @ 20261990, 16 @ 7/123456/999, 17 @ 20260101), mean frontage 3.87-4.05 m, depth 5.1-8.0 m. Path: flux_local -> bg_remove_local -> trellis2_mlx -> normalize.py -> bake_lowpoly.py. $0, on-device, no cloud key. GENERATE NOW, WIRE LATER (R39 brief): nothing here is on a lane's critical path this round.", + "_sizing_law": "R38's law, applied BEFORE generating: budget = tris x the instance count the placement rule implies, and can it ride an existing merged list? Measured counts from web/js/citygen/plan.js via pipeline/arcade_measure.mjs. blade-sign: 1 per arcade shop = 17, synthetic only. aframe-board: ~4 in the arcade PLUS v9 Layer 4's unmarked-shop cue at clamp(round(shops*0.06),2,12) = up to 12/town => size for <=16. keycutter-sign: 1. A GLB cannot ride boxMats (BoxGeometry, one shared untextured shell material), so each of these is +1 TOWN-WIDE InstancedMesh draw against the 8-draw margin - one per geometry, not one per chunk. State the draw before wiring.", + "_thin_structure_guard": "R38 proved thin structures are NOT a TRELLIS class (hills_hoist wire -> thousands of disconnected fragments; a voxel remesh coarse enough for 600 tris erases the pole). So every prompt here is deliberately CHUNKY: the blade sign is a boxy perspex light-box on a stub, not a plate on a wrought-iron arm; the key sign is a thick solid cut-out slab, not a wire silhouette. The bracket arms are Lane B's boxMats primitives at +0 draws.", + "_no_baked_text": "House law since v1: the game overlays the name, the asset never bakes it. Every sign face here is BLANK.", + "assets": [ + { + "name": "aframe-board", + "prompt": "a two-sided A-frame sandwich board sign standing open on a footpath, two thick hinged plywood panels leaning against each other, painted bright chipped yellow around a plain blank white sign face on each panel, a short chain between the legs, a cheap discount-shop pavement sign, whole object from feet to top hinge", + "height_m": 0.9, + "tris": 400, + "for": "39.4 the arcade's two-dollar shop AND v9 Layer 4's unmarked-shop cue (<=16 instances/town)" + }, + { + "name": "blade-sign", + "prompt": "a boxy rectangular illuminated perspex shop sign that projects out from a wall, a thick white light-box with a bright red plastic frame and completely blank empty faces, mounted on a short square stub at the back, a 1970s Australian arcade projecting sign, whole object", + "height_m": 0.55, + "tris": 400, + "for": "39.4 the arcade's hanging shopfront signage (17 instances) - SEE the +0-draw alternative in LANE_E_NOTES before wiring this" + }, + { + "name": "keycutter-sign", + "prompt": "a large thick flat shop sign cut out in the shape of an old-fashioned door key, a solid chunky slab silhouette of a key with a round scalloped bow and a simple square-toothed bit, painted worn brass gold with a dark outline, a key cutter's trade sign, whole object, no lettering", + "height_m": 0.6, + "tris": 500, + "for": "39.4 the arcade's one landmark trade sign (1 instance) - lowest value per draw in the kit, see the ranking" + } + ] +} diff --git a/pipeline/seamless_tile.py b/pipeline/seamless_tile.py new file mode 100644 index 0000000..270e488 --- /dev/null +++ b/pipeline/seamless_tile.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""PROCITY Lane E — seamless_tile.py (R39) + +Make a REGULAR-PATTERN skin tile without a seam, by cropping it to a whole number of its own +periods instead of blurring the joint. + +Why this exists: every ground skin in `web/assets/gen/` tiles through `RepeatWrapping` with the tile +scale baked into the mesh UVs (`ground.js` TILE = 5 m). Grass and bitumen hide the joint because +they have no structure. A TILED FLOOR does not — the arcade's 42 x 12 m lane is 504 m2, the single +biggest surface in the district, and a terrazzo grid whose lines jump at every repeat reads as a +mistake rather than as a floor. The usual fix (offset + feather) is wrong for a grid: it smears the +grout lines. The right fix is arithmetic — find the pattern's pitch and cut on it. + + PY=~/Documents/MODELBEAST/venvs/mflux/bin/python + $PY pipeline/seamless_tile.py IN.png OUT.png [--check OUT_3x3.png] + +Pitch is found by autocorrelating the mean-absolute column/row gradient (a grout line is a gradient +spike; the spacing of the spikes IS the pitch), searching 24..W/3 px. Prints the residual seam error +so the result is falsifiable: the mean |difference| between the left and right edge columns before +and after. If the image has no periodic structure the correlation is flat and the script says so and +copies the input through rather than cropping something arbitrary. +""" +import sys +import numpy as np +from PIL import Image + + +def pitch(sig, lo=24): + """Dominant period of a 1-D signal, by normalised autocorrelation.""" + x = sig - sig.mean() + n = len(x) + hi = max(lo + 1, n // 3) + ac = np.correlate(x, x, mode="full")[n - 1:] + ac = ac / (ac[0] or 1) + band = ac[lo:hi] + if band.max() < 0.12: # no periodic structure worth cutting on + return None, float(band.max()) + return int(lo + band.argmax()), float(band.max()) + + +def edge_err(a, axis): + """Mean |difference| across the wrap seam, 0-255.""" + if axis == 1: + return float(np.abs(a[:, 0].astype(np.float32) - a[:, -1].astype(np.float32)).mean()) + return float(np.abs(a[0].astype(np.float32) - a[-1].astype(np.float32)).mean()) + + +def main(): + src, dst = sys.argv[1], sys.argv[2] + im = Image.open(src).convert("RGB") + a = np.asarray(im).astype(np.float32) + g = a.mean(axis=2) + gx = np.abs(np.diff(g, axis=1)).mean(axis=0) # column gradient profile -> vertical lines + gy = np.abs(np.diff(g, axis=0)).mean(axis=1) # row gradient profile -> horizontal lines + px, cx = pitch(gx) + py, cy = pitch(gy) + before = (edge_err(np.asarray(im), 1), edge_err(np.asarray(im), 0)) + print(f"in {im.size} pitch x={px} (r={cx:.2f}) y={py} (r={cy:.2f}) seam before x={before[0]:.1f} y={before[1]:.1f}") + if px is None and py is None: + print("no periodic structure found — passing through unchanged") + im.save(dst) + return + + # PER-AXIS, AND ONLY IF IT ACTUALLY HELPS. Measured on the R39 arcade pair: a diffusion-drawn + # tiled floor is NOT periodic enough to cut on (autocorrelation peaked at r=0.26/0.30 and the + # "period" it found was not the grout pitch), and cropping to it made the seam WORSE — 27.9→31.9 + # across x. So the crop is a proposal that has to beat the measurement it is trying to improve, + # per axis, or it is discarded. A tool that can only make a number worse is worse than no tool. + def try_axis(p, axis, full): + if not p or full // p < 2: + return full, None + return (full // p) * p, None + W, _ = try_axis(px, 1, im.width) + H, _ = try_axis(py, 0, im.height) + cand = im.crop(((im.width - W) // 2, (im.height - H) // 2, + (im.width - W) // 2 + W, (im.height - H) // 2 + H)) + c = np.asarray(cand) + trial = (edge_err(c, 1), edge_err(c, 0)) + keep_x, keep_y = trial[0] < before[0], trial[1] < before[1] + if not keep_x: + W = im.width + if not keep_y: + H = im.height + x0, y0 = (im.width - W) // 2, (im.height - H) // 2 + out = im.crop((x0, y0, x0 + W, y0 + H)) + b = np.asarray(out) + after = (edge_err(b, 1), edge_err(b, 0)) + print(f"out {out.size} keep x={keep_x} y={keep_y} seam after x={after[0]:.1f} y={after[1]:.1f}" + + ("" if (keep_x or keep_y) else " ← NO IMPROVEMENT AVAILABLE, passed through")) + out.save(dst) + if "--check" in sys.argv: + chk = sys.argv[sys.argv.index("--check") + 1] + t = Image.new("RGB", (out.width * 3, out.height * 3)) + for i in range(3): + for j in range(3): + t.paste(out, (i * out.width, j * out.height)) + t.resize((out.width, out.height), Image.LANCZOS).save(chk) + print(f"3x3 tiling check -> {chk}") + + +main() diff --git a/web/assets/gen/facade-shuttered.jpg b/web/assets/gen/facade-shuttered.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa8eb43df185eef90bb2bf232af2b8a9fc461648 GIT binary patch literal 89901 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