Fence Kit

showcase/fence-kit/

A procedural post-and-rail fence section that fits its 1.60 m tile, with one-shell iron bands and a board brace housed in both posts, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

Rendered headless by the showcase piece itself. Select it to enlarge.

witnesses Recomputed: 1012 tris, two materials with 116 metal and 390 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.596×0.134×1.232 m inside the 1.60 m tile, grounded zmin, 2 iron shoes, brace housed 12 mm in both posts and bearing 2 mm on the middle rail, rail-to-post gap 0.93 mm, rail span 1.362 m, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 20 tris, hygiene all zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-shoes 16; --short-brace 17; --gap-rails 18; --long-rails 19; --float-brace 20; --wide-tile 21.

category Village

tags mesh export showcase

blender --background --python showcase/fence-kit/fence_kit.py --

A showcase piece, not an example. A procedural post-and-rail fence section (two posts, pyramidal caps, three rails, a kickboard, a board brace, iron shoes and U-bands), then the shipped pipeline: unique-cell UVs, a Cycles high-to-low normal bake, an LOD chain, a convex collider and a Unity glTF export.

Posts, rails, kick, brace and bands share named stations (POST_XS, RAIL_ZS, SHOE_H). The post stations come from the tile. The outer faces of the iron bands sit KIT_CLEAR (2 mm) inside the tile edge, so copies placed at the 1.60 m pitch meet band to band without interpenetrating or sharing a plane. Each band is one shell, mitred at the corners. A rail band is a U open toward the span, with its arms stopped inside the post's inner face. The brace is a board with plumb-cut ends, face-nailed to the middle rail and housed 12 mm into both posts. It clears the bottom rail at the left post and the top rail at the right one. Rails and kick tenon into the post volume.

Hero. The still shows a three-section run that reads as one fence, with one post at each joint. The leading section is the checked FenceLow mesh. Each follow-on section is a render-only copy with its first post group (post, cap, shoe and bands, found as mesh shells) removed, stepped at the post pitch 2·HX. Its rails and brace then tenon into the previous section's trailing post. Placed whole at the 1.60 m tile pitch, the sections meet band to band, which is the tile fit the budgets assert, but every joint shows a doubled post. The earlier hero did exactly that. The run stands in a render-only packed-earth strip, and the wood weathers in broad silver-grey patches (object-space noise) over the per-member tone and grain. The strip and the trimmed copies are staging; the asset sheet judges the single FenceLow section.

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check. This piece does not re-witness the modular-kit-snap snap contract.

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package). Hygiene combinatorics match examples/mesh-hygiene-audit (copied, not imported).

Intended size: 1.60 m tile, 1.14 m posts, 0.11 m post stock. Outer AABB 1.596 × 0.134 × 1.232 m.

Budgets#

Declared as named constants. Every gate recomputes its value from the mesh, materials, UVs, evaluated LOD, collider or export file.

AxisDeclaredMeasured (4.5.11 / 5.1.2 / 5.2.1)
Base triangles850–12001012 / 1012 / 1012
LOD1 ratio0.32–0.62 of base0.5000 / 0.5000 / 0.5000
LOD2 ratio0.10–0.35 of base0.2194 / 0.2194 / 0.2174
Materialsexactly 2 distinct, ≥48 metal, ≥48 wood2 slots, 116 metal / 390 wood
UVsin 0..1, AABB overlap ≤ 1e-5in range, overlap 0
Outer AABB(1.596, 0.134, 1.232) m ± 0.015(1.5960, 0.1340, 1.2320), zmin 0
Collider tris≤ 18020
Exportwritten, size > 074952 / 74952 / 74944 bytes (4.5.11 / 5.1.2 / 5.2.1)

Base triangles dropped from 1092 to 1012 in the second quality pass. Each band became one mitred shell instead of three or four separate plates, and the brace became a six-face board. The band was re-fitted around the measurement and narrowed from 1000–1400.

DECIMATE COLLAPSE triangle counts are not identical across series: 5.2.1 is more aggressive on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction is closed-form and plank tones are seeded, so repeated runs on one binary print identical measurements. Export byte counts differ by 12 B on 5.2.1 (glTF serializer), not a gated axis.

Hygiene#

Recomputed from the generated mesh, not asserted about the script.

AxisDeclaredMeasured (all three)
Non-manifold edges00
Loose verts / edges0 / 00 / 0
Doubles merged at 1e-500
Zero-area faces00
N-gons00
Coplanar disjoint face pairs00
Grounded: zminwithin 1e-4 of 00.0000
Named shoes2, each zmin ≤ 0.0022, 0.00000

Joint fit, bearing and tiling#

AxisDeclaredMeasured (all three)
Brace-to-post BVH gap (both posts)≤ 0.008 m0.00531
Rail-to-post BVH gap≤ 0.008 m0.00093
Rail span vs 2·RAIL_HALF± 0.04 m1.3620 vs 1.362
Brace housing (exit 20): deepest brace vertex inside each post≥ 0.004 m0.01200
Brace bearing (exit 20): brace back plane past the middle rail's front plane, where their heights overlap≥ 0.001 m0.00200
Tile fit (exit 21): section width along the tiling axis1.590–1.600 m1.5960

The brace bearing is measured plane to plane. The brace and the middle rail cross mid-span, and neither has a vertex there, so a vertex-depth metric reports a false gap of 0.15 m.

Falsifiers#

Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1 and returned the same code on each.

FlagBudget violatedExit
--skip-decimateLOD1 ratio band9
--stray-vertloose vertex count is 015
--lift-zbounding box zmin is 016
--short-shoesnamed shoes plant at z=016
--short-bracebrace-to-post BVH gap (both posts)17
--gap-railsrail-to-post BVH gap18
--long-railsrail span vs 2·RAIL_HALF19
--float-bracebrace bearing on the middle rail (brace 6 mm forward; measured −0.00400)20
--wide-tiletile fit (the first build's post stations; measured 1.6240)21

--short-shoes lifts only the iron shoes. After recentring, a post or the kick is the AABB zmin, so the AABB gate would still pass. The named-shoe stations fail. --short-brace shortens the left end; brace_gap is the maximum of the per-post minima, so a brace that still reaches the right post cannot sneak through. --long-rails adds an extra spanning board, and the tenoned rails stay put, so the gap budget still passes. --wide-tile moves the posts out to where the first build had them and moves the rails with them. It is checked before the AABB gate, which it would also fail.

Run#

blender --background --python fence_kit.py --
blender --background --python fence_kit.py -- --skip-decimate
blender --background --python fence_kit.py -- --stray-vert
blender --background --python fence_kit.py -- --lift-z
blender --background --python fence_kit.py -- --short-shoes
blender --background --python fence_kit.py -- --short-brace
blender --background --python fence_kit.py -- --gap-rails
blender --background --python fence_kit.py -- --long-rails
blender --background --python fence_kit.py -- --float-brace
blender --background --python fence_kit.py -- --wide-tile
blender --background --python fence_kit.py -- --output fence.png

Smoke passes no flags.

Exit codes#

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path. 15–19 are the hygiene and joint-fit family. 20 and 21 are this piece's own.

CodeMeaning
0Success
1Uncaught exception (FATAL wrapper)
2argparse / usage
3Mesh did not build / no UV layer
4Base triangle count outside range
5Material count ≠ 2 distinct slots, or wood/metal faces missing
6UVs outside 0..1
7UV AABB overlap above tolerance
8World AABB off declared outer size
9LOD ratio band (--skip-decimate lands here)
10Framing gate (render path only)
11Collider triangle count above ceiling
12Bake did not finish or image has no data
13Export file missing or empty
14--output produced no file
15Hygiene (--stray-vert lands here)
16Grounded zmin / named shoes (--lift-z, --short-shoes)
17Brace-to-post gap (--short-brace)
18Rail-to-post gap (--gap-rails)
19Rail span (--long-rails)
20Brace not housed in both posts or not bearing on the middle rail (--float-brace)
21Section does not fit its tile (--wide-tile)
22Gallery asset-quality violation (render path; the floors' 11 is remapped here)

Source

showcase/fence-kit/fence_kit.py View the script on GitHub →
A weathered post-and-rail fence run of three sections on a packed-earth strip, one capped post at each joint, each bay with a diagonal board brace and dark iron bands.