# Design ## Architecture `roads.py` will join the bpy-layer feature modules under `blender/osmassets/`. It may import `os`, `json`, `MeshBatch`, `add_polyline`, and geometry helpers required by current road assembly: ```python from osmassets.geom import clip_polygon, feature_in_bounds, geometry_rings from osmassets.mesh import MeshBatch, add_polyline ``` The module will expose two functions: ```python def assemble_geojson_layer(path, layer_id, projector, collection, material, z): ... def assemble_osm_fallback(ways, projector, collection, material): ... ``` `assemble_geojson_layer()` returns the same count as the current `add_geojson_layer()`. `assemble_osm_fallback()` returns no count because the current fallback also leaves `road_counts` unchanged. ## Boundaries - `generate_scene.py` keeps catalog ownership: `road_mats`, `ROAD_LAYERS` iteration, `check_layers()` warnings, `road_counts`, scene metadata, and stdout markers. - `roads.py` owns only object/mesh construction for already chosen road inputs. - `catalog.py` and `scripts/lib/scene-layers.js` remain untouched; road layer collection/order is not part of this task. - No new pure-Python logic is introduced. Existing geometry helpers are reused. ## Data Flow 1. `generate_scene.py` creates `road_mats` from `catalog.ROAD_LAYERS` and `catalog.road_material_specs()`. 2. If `geojson_dir` exists, `generate_scene.py` runs `catalog.check_layers()` and prints warnings exactly as before. 3. For each `catalog.ROAD_LAYERS` entry, `generate_scene.py` calls `roads.assemble_geojson_layer(...)` and stores the returned count under the layer id in `road_counts`. 4. If `road_counts.get("road_surface", 0) == 0`, `generate_scene.py` calls `roads.assemble_osm_fallback(...)` with OSM ways and the road surface material. 5. `generate_scene.py` writes `road_counts` into scene metadata and `SCENE_DONE`. ## Compatibility The move must preserve road object names, mesh names, geometry clipping, feature counting, layer iteration order, warning behavior, fallback trigger, fallback widths, z values, material assignments, and metadata JSON. Cesium export consumes the resulting `.blend`, so parity should see no non-ignored contract difference. ## Trade-Offs `catalog.check_layers()` intentionally stays in `generate_scene.py`. Moving it into `roads.py` would mix cross-language catalog validation with object assembly and increase the blast radius of this refactor. The full feature registry remains deferred. Roads are the last high-risk assembly extraction; registry design should be evaluated after this commit when the remaining `generate_scene.py` responsibilities are clearer. ## Rollback Rollback is mechanical: move `assemble_geojson_layer()` and `assemble_osm_fallback()` logic back into `generate_scene.py`, restore the direct call sites, remove the `roads.py` import, and delete the module.