Files

4.2 KiB

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:

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.