Add feature registry dispatch

This commit is contained in:
2026-08-03 14:02:28 +08:00
parent dd50d3aae3
commit ff18217af5
12 changed files with 417 additions and 46 deletions

View File

@@ -0,0 +1,4 @@
{"file": ".trellis/spec/blender/index.md", "reason": "Check generated Blender entrypoint changes against layer and stdout marker constraints."}
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Verify registry placement and feature module boundaries remain consistent."}
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Verify required before/after parity and no unreviewed artifact contract drift."}
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "Check no duplicate ROAD_LAYERS/material/config facts were introduced."}

View File

@@ -0,0 +1,103 @@
# 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.

View File

@@ -0,0 +1,5 @@
{"file": ".trellis/spec/blender/index.md", "reason": "Blender layer overview, bpy/pure-Python boundary, parity expectations, and entrypoint contracts."}
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Feature module responsibilities and the stated goal of adding features through a registry line."}
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Pure refactor validation contract and P2 registry status."}
{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "Registry and single-source constraints; avoid duplicate road/material/catalog facts."}
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "Cross-stage stdout, material, and Blender/Cesium artifact contract risks."}

View File

@@ -0,0 +1,57 @@
# Implementation Plan
## Checklist
1. Capture a parity baseline before product-code edits:
`node scripts/parity.js capture feature-registry-before --stages blender,cesium`
2. Add `blender/osmassets/features.py` with a small `FeatureHandler` contract
and `dispatch_ways(...)`.
3. Import `features.py` in `blender/generate_scene.py`.
4. Convert the current OSM way `if` / `elif` chain into ordered local handlers:
water, grass, scrub, tree-row, building.
5. Keep handler bodies mechanically equivalent to the current branch bodies.
6. Leave road GeoJSON/fallback, tree placement, fountain point processing,
scene metadata, and `SCENE_DONE` behavior unchanged except for any small
phase comments needed to clarify ordering.
7. Run Python checks:
- `python3 -m py_compile blender/osmassets/features.py`
- `python3 -m py_compile blender/generate_scene.py`
- `python3 -m unittest blender/tests/test_pure.py`
8. Capture parity after the refactor:
`node scripts/parity.js capture feature-registry-after --stages blender,cesium`
9. Compare parity:
`node scripts/parity.js compare feature-registry-before feature-registry-after`
10. Update `.trellis/spec` and `docs/changelog.md` only for durable registry
conventions or P2 status changes discovered during implementation.
11. Commit, archive the task, and record the journal entry.
## Validation Commands
```bash
node scripts/parity.js capture feature-registry-before --stages blender,cesium
python3 -m py_compile blender/osmassets/features.py
python3 -m py_compile blender/generate_scene.py
python3 -m unittest blender/tests/test_pure.py
node scripts/parity.js capture feature-registry-after --stages blender,cesium
node scripts/parity.js compare feature-registry-before feature-registry-after
```
## Risk Points
- Handler order is equivalent to the old `elif` chain. Changing it can alter
which feature claims a way.
- Object creation phase order affects `.blend` and GLB digests.
- Material creation order affects GLB material indices.
- Tree rows and scrub interior trees feed the later tree placement phase; do
not place trees inside the way dispatch loop.
- `catalog.check_layers()` must stay warning-only and keep the exact printed
prefix.
- Do not add a new road layer id list; keep deriving road layers from
`catalog.ROAD_LAYERS`.
## Rollback Point
If parity shows a non-ignored difference, first compare object order, object
names, mesh/material names, `SCENE_DONE`, and scene custom properties. If the
cause is not obvious, revert the registry dispatch and return to the current
way loop before trying a narrower refactor.

View File

@@ -0,0 +1,94 @@
# Design feature registry
## Goal
Introduce a conservative Blender feature registry so `generate_scene.py` no
longer hard-codes every OSM feature dispatch branch, while keeping generated
`.blend` / `.glb` / scene metadata and stdout contracts unchanged.
The user value is maintainability: adding or moving a Blender feature should
become a registration-level edit plus a feature module implementation, not a
new branch woven through the main scene builder.
## Background
- P2 module extraction is now complete for current feature assembly modules:
`water.py`, `grass.py`, `scrub.py`, `tree.py`, `fountain.py`,
`building.py`, and `roads.py` exist under `blender/osmassets/`.
- `.trellis/spec/guides/artifact-parity-guide.md` records the remaining P2
gap as the missing `features/` registry.
- `blender/generate_scene.py` still owns the orchestration sequence:
material creation, OSM way dispatch, road GeoJSON/fallback dispatch, tree
collection/fallback, fountain point dispatch, camera focus points, scene
metadata, save/render, and `SCENE_DONE`.
- Artifact parity is mandatory because this is intended as a pure refactor.
## Requirements
1. Add a new Blender-side registry module under `blender/osmassets/`, expected
path `blender/osmassets/features.py`.
2. Replace the inline OSM way `if` / `elif` feature dispatch in
`generate_scene.py` with registry-driven dispatch while preserving exact
behavior and order:
- water before grass;
- grass before scrub;
- scrub before tree-row collection;
- tree-row collection before building;
- first matching handler wins, as the current `elif` chain does.
3. Make the current feature phases explicit in code:
- OSM way feature phase;
- road GeoJSON / OSM fallback phase;
- tree placement phase;
- point prop phase for fountains.
4. Keep `generate_scene.py` responsible for high-level scene contracts:
- CLI parsing and tree-style validation;
- collection creation and collection order;
- material creation order;
- `counts`, `road_counts`, tree counts, and `focus_points` ownership;
- `catalog.check_layers()` and exact `Layer catalog warning:` print text;
- scene custom properties;
- `SCENE_DONE` marker and JSON shape.
5. Do not change current feature module assembly signatures unless the change
is a mechanical adapter around the same behavior.
6. Do not reorder `catalog.MATERIALS`, `catalog.ROAD_LAYERS`, collection
creation, object creation phases, or `SCENE_DONE` fields.
7. Do not introduce a second list of osm2streets road layer ids. Road layer
iteration must still derive from `catalog.ROAD_LAYERS`.
8. Do not fix unrelated known defects D1-D3 in this task.
## Acceptance Criteria
- [ ] `blender/osmassets/features.py` exists and contains the registry /
dispatcher contract for Blender feature phases.
- [ ] `generate_scene.py` uses the registry for OSM way feature dispatch; the
previous inline `if` / `elif` way feature chain is removed or reduced to
small handler callbacks.
- [ ] Current generated object names, mesh names, material creation order,
collection order, scene properties, and `SCENE_DONE` JSON remain
unchanged.
- [ ] `road_counts`, `counts`, tree counts, and `focus_points` continue to be
owned by `generate_scene.py` or by explicit objects passed from it, not
hidden in global module state.
- [ ] `catalog.check_layers()` warning behavior remains in `generate_scene.py`
with the exact `Layer catalog warning:` text.
- [ ] `python3 -m py_compile blender/osmassets/features.py` passes.
- [ ] `python3 -m py_compile blender/generate_scene.py` passes.
- [ ] `python3 -m unittest blender/tests/test_pure.py` passes.
- [ ] Blender/Cesium parity before/after is run for
`nantaizi-lake-innovation-valley` and `hanyang-block`; expected compare
result is `identical` for both.
## Out Of Scope
- Changing feature behavior, geometry, z values, material definitions, material
order, or catalog declarations.
- Moving material creation into feature modules.
- Moving scene metadata / `SCENE_DONE` construction out of `generate_scene.py`.
- Reworking tree model loading, procedural tree fallback, grass tuft loading,
scrub bush loading, or camera focus logic beyond adapter-level plumbing.
- Adding new rendered feature types.
- Changing parity ignore lists or stdout marker parsing.
## Open Questions
None.

View File

@@ -0,0 +1,26 @@
{
"id": "design-feature-registry",
"name": "design-feature-registry",
"title": "Design feature registry",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-03",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}