# Extract road module ## Goal Move Blender road assembly out of `blender/generate_scene.py` into a dedicated bpy-layer module, keeping generated `.blend` / `.glb` / metadata structure unchanged. This completes the P2 feature-module extraction except for the deferred full `features/` registry. ## Background - `.trellis/spec/guides/artifact-parity-guide.md` records P2 as partial: `water.py`, `grass.py`, `scrub.py`, `tree.py`, `fountain.py`, and `building.py` are already split out, while roads still live in `generate_scene.py`. - `blender/generate_scene.py:148` defines `add_geojson_layer()`, which reads one osm2streets GeoJSON road layer, filters by bounds, projects coordinates, clips polygons, batches them as `Road_`, and returns a feature count. - `blender/generate_scene.py:656` creates `road_mats` by zipping `catalog.ROAD_LAYERS` with `catalog.road_material_specs()`. - `blender/generate_scene.py:741` runs `catalog.check_layers(geojson_dir)` and prints `Layer catalog warning:` messages before iterating `ROAD_LAYERS`. - `blender/generate_scene.py:750` falls back to simple OSM highway polylines when `road_counts["road_surface"]` is zero. - `blender/generate_scene.py:866` stores `scene["road_feature_counts"]`; the same `road_counts` object is emitted in `SCENE_DONE` as `road_features`. ## Requirements 1. Add a new bpy-layer module at `blender/osmassets/roads.py`. 2. Move the current roads assembly behavior into the new module: - GeoJSON layer mesh assembly currently in `add_geojson_layer()`; - simple OSM highway fallback currently in the `road_surface == 0` block. 3. Keep `generate_scene.py` responsible for: - creating `road_mats` from `catalog.ROAD_LAYERS` and `catalog.road_material_specs()`; - running `catalog.check_layers()` and printing existing warning text; - owning `road_counts`; - writing `scene["road_feature_counts"]`; - writing the existing `SCENE_DONE` JSON shape. 4. Preserve existing road object names and mesh names: - `Road_` - `OSM_Road_` 5. Preserve existing GeoJSON behavior: - missing layer file returns count `0`; - feature is skipped when `feature_in_bounds()` is false; - rings come from `geometry_rings()`; - projected rings are clipped with `clip_polygon()`; - only rings with at least three points are batched and counted. 6. Preserve existing OSM fallback behavior: - fallback triggers only when `road_counts.get("road_surface", 0) == 0`; - only ways with a `highway` tag and at least two coordinates are drawn; - widths remain `secondary: 7.0`, `residential: 5.5`, `service: 3.5`, default `4.0`; - fallback material remains `road_mats["road_surface"]`; - fallback z remains `0.03`; - fallback does not add to `road_counts`. 7. Do not introduce a full `features/` registry in this task. 8. Do not modify `ROAD_LAYERS`, `SCENE_LAYERS`, material definitions, `MATERIALS` order, GeoJSON schema, z-index/z-height values, stdout markers, parity ignore lists, or known unrelated defects D1-D3. ## Acceptance Criteria - [ ] `blender/osmassets/roads.py` contains road assembly behavior and imports only bpy-layer-safe dependencies. - [ ] `blender/generate_scene.py` imports and calls the new roads module; no inline `add_geojson_layer()` remains there. - [ ] `road_counts`, `scene["road_feature_counts"]`, and `SCENE_DONE` `road_features` remain unchanged. - [ ] Existing `Layer catalog warning:` print behavior remains in `generate_scene.py`. - [ ] `python3 -m py_compile blender/osmassets/roads.py` passes. - [ ] `python3 -m py_compile blender/generate_scene.py` passes. - [ ] `python3 -m unittest blender/tests/test_pure.py` passes. - [ ] A before/after parity comparison is run for the Blender/Cesium stages and any non-ignored diff is either absent or explicitly explained as expected. For this refactor, the expected result is no contract diff. ## Out Of Scope - Full `features/` registry implementation. - Any change to osm2streets road splitting, `SCENE_LAYERS`, or QGIS pipeline behavior. - Any change to `catalog.ROAD_LAYERS`, material names, color values, or order. - Any change to fallback highway widths, z values, object names, or counts. - Turning `catalog.check_layers()` warnings into errors. - Moving `road_mats` creation or `road_feature_counts` scene metadata out of `generate_scene.py`. ## Open Questions None.