# Design ## Architecture Add `blender/osmassets/features.py` as a bpy-layer-safe orchestration helper. It should not import `bpy` directly unless implementation proves that necessary. The preferred shape is a small registry / dispatcher contract: ```python FeatureHandler = namedtuple("FeatureHandler", ("name", "matches", "handle")) def dispatch_ways(ways, projector, handlers): ... ``` `generate_scene.py` will create local handler callbacks that close over the current build state: collections, materials, `counts`, `focus_points`, `grass_rings`, `scrub_trees`, `tree_rows`, and user args. The registry owns ordering and first-match dispatch; `generate_scene.py` owns the state and exact side effects. This is intentionally thinner than moving all feature ownership into modules. The current feature signatures are uneven for good reasons: - `water.assemble()` returns only a count. - `grass.assemble()` returns count, tuft count, and focus points. - `scrub.assemble()` returns count and focus points, then feeds scrub-tree sampling owned by `generate_scene.py`. - `building.assemble()` consumes `office_overrides` and returns building / industrial counts plus footprint points. - `roads.py` owns road object construction but `generate_scene.py` owns `ROAD_LAYERS`, `road_counts`, and warning behavior. - tree placement is a phase after way and road processing because tree rows, scrub interior trees, individual point trees, and model fallback combine into one `trees` list. ## Boundaries `features.py` may own: - feature handler data structures; - way dispatch loop mechanics; - optional small phase helpers if they preserve phase order; - registration order for current high-level feature phases. `generate_scene.py` keeps: - material creation and its order; - collection creation and its order; - `catalog.check_layers()` warning text; - all counters and scene metadata keys; - `SCENE_DONE` JSON; - tree fallback decision and tree material creation; - loading of grass tuft and scrub bush variants. Existing feature modules keep object assembly only. They should not gain global state or start reading CLI args, area config, catalog layer lists, or scene metadata. ## Data Flow 1. `generate_scene.py` parses OSM, creates `Projector`, collections, materials, variant assets, counters, and focus containers exactly as now. 2. `generate_scene.py` builds an ordered tuple of `FeatureHandler` instances for OSM ways and passes it to `features.dispatch_ways(...)`. 3. `features.dispatch_ways(...)` preserves the current outer loop behavior: skip ways with no coordinate inside bounds, project the ring once, check handlers in order, run the first match, then continue to the next way. 4. `generate_scene.py` runs road GeoJSON/fallback dispatch after way dispatch, preserving `catalog.ROAD_LAYERS` ownership. 5. `generate_scene.py` gathers individual tree points, combines them with tree rows and scrub trees, runs model/procedural tree placement, then processes fountains. 6. Scene properties, save/render, and `SCENE_DONE` remain unchanged. ## Compatibility The refactor must be artifact-neutral. The following are load-bearing: - handler order must match the existing `if` / `elif` chain; - object creation phase order must remain way features -> roads -> trees -> fountains -> lights/camera/save; - material creation order must not change; - `tree_style_used` fallback semantics must not change; - `road_features` and all non-road counts must remain identical; - no new global mutable registry state may leak across Blender runs. ## Trade-Offs A full ownership inversion where every module owns its materials, counts, and metadata would make `generate_scene.py` smaller, but it would touch material order, scene metadata, and several feature-specific side effects at once. That is too much blast radius for a parity-preserving refactor. The conservative registry gives the next feature a stable insertion point and removes the main `elif` dispatch chain without pretending all current feature modules have the same contract. ## Rollback Rollback is mechanical: inline the handler registration back into the existing way loop, delete the `features.py` import and module, and keep feature module calls unchanged.