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

@@ -38,6 +38,7 @@
│ osmassets/materials.py 材质构建(消费 catalog 的声明) │
│ osmassets/tree.py 树实例化 │
│ osmassets/water.py grass.py scrub.py │
│ osmassets/features.py 要素分发注册 │
│ osmassets/building.py fountain.py roads.py 要素装配 │
│ │
│ generate_scene.py export_cesium.py 两个入口 │
@@ -59,7 +60,7 @@
| | `generate_scene.py` | `export_cesium.py` |
|---|---|---|
| 行数 | 999 | 624 |
| 行数 | 895 | 647 |
| 调用 | `--background --factory-startup --python` | `--background --python` |
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
| 输出 | `--output`(.blend)、`--render`(.png) | `--glb``--metadata`(.json) |
@@ -121,7 +122,7 @@
| 文件 | 行数 | 层 |
|---|---|---|
| `generate_scene.py` | 869 | bpy · 入口 |
| `generate_scene.py` | 895 | bpy · 入口 |
| `export_cesium.py` | 647 | bpy · 入口 |
| `osmassets/tree.py` | 318 | bpy |
| `osmassets/geom.py` | 241 | 纯 |
@@ -129,6 +130,7 @@
| `osmassets/catalog.py` | 197 | 纯 |
| `osmassets/mesh.py` | 126 | bpy |
| `osmassets/osm.py` | 89 | 纯 |
| `osmassets/features.py` | 20 | bpy · 分发 |
| `osmassets/building.py` / `fountain.py` / `roads.py` | 56 / 49 / 40 | bpy |
| `osmassets/water.py` / `grass.py` / `scrub.py` | 15 / 22 / 13 | bpy |
| `tools/scene_digest.py` | 171 | bpy · 工具 |

View File

@@ -24,6 +24,7 @@
osmassets/mesh.py MeshBatch、prism、polyline
osmassets/materials.py 材质构建
osmassets/tree.py 树实例化
osmassets/features.py 要素分发注册
osmassets/water.py grass.py scrub.py
osmassets/building.py fountain.py roads.py 要素装配
generate_scene.py export_cesium.py 两个入口脚本
@@ -50,6 +51,7 @@
| `catalog.py` | 纯 | `ROAD_LAYERS``MATERIALS``road_material_specs()``check_layers()` |
| `mesh.py` | bpy | `MeshBatch``make_prism``add_roof``add_wall_panel``add_polyline`、集合管理 |
| `materials.py` | bpy | 把 `catalog` 的规格变成真实材质:`from_spec()`、贴图、程序化噪声、tint、alpha-clip |
| `features.py` | bpy | `FeatureHandler``dispatch_ways()`,保持 OSM way 要素 first-match 分发顺序 |
| `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 |
| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 面要素的装配 |
| `building.py` | bpy | 单一 OSM 面要素 `building=*` 的装配 |
@@ -124,22 +126,48 @@ def assemble_osm_fallback(ways, projector, collection, material): ...
`scene["road_feature_counts"]` / `SCENE_DONE["road_features"]`。模块只负责把已选定的
GeoJSON layer 或 OSM fallback ways 变成 `Road_<layer_id>` / `OSM_Road_<way_id>` 对象。
### OSM way 要素注册
`features.py` 只负责分发机制,不拥有 feature state
```python
FeatureHandler = namedtuple("FeatureHandler", ("name", "matches", "handle"))
dispatch_ways(ways, projector, handlers)
```
`dispatch_ways()` 的契约:
- 对每个 way 先执行 `any(projector.inside(c) for c in way["coords"])`,不在范围内就跳过
- 只投影一次 `ring = projector.ring(way["coords"])`
- 按传入的 `handlers` 顺序检查
- 第一个 `matches(way, tag, ring)` 为真的 handler 执行 `handle(way, tag, ring)` 后停止
当前注册顺序必须等同旧 `if` / `elif` 链:
```text
water -> grass -> scrub -> tree_row -> building
```
这只是保守 registry不是 ownership 反转。`generate_scene.py` 仍负责 collection /
material 创建顺序、`counts``focus_points``tree_rows``scrub_trees`、scene metadata
`SCENE_DONE`。不要把这些状态藏进 `features.py` 的全局变量里。
### 加一种新 OSM 要素
目标形态:**新增一个模块 + 注册一,不改 `build()`**。
目标形态:**新增一个模块 + 在 OSM way handler 表里注册一,不改主分发循环**。
1. 新建 `osmassets/<feature>.py`,写 `assemble(...)`,签名照抄上面
2. 只 import 需要的:`from osmassets.geom import clip_polygon`
`from osmassets.mesh import MeshBatch`
3. 材质规格加进 `catalog.MATERIALS`**追加到末尾**,顺序决定 GLB 材质索引)
4. `generate_scene.py`要素分发处加一行调用
4. `generate_scene.py` OSM way handler 注册表里追加 handler保持顺序语义明确
5. 纯几何部分若有新函数,放 `geom.py` 并**补 `blender/tests/test_pure.py`**
---
## 两个入口脚本
| | `generate_scene.py` (899行) | `export_cesium.py` (647行) |
| | `generate_scene.py` (895行) | `export_cesium.py` (647行) |
|---|---|---|
| 调用 | `--background --factory-startup --python` | `--background --python` |
| 输入 | `--osm` + `--geojson` | `--blend` |

View File

@@ -163,7 +163,7 @@ capturedAt / durationMs / label
|---|---|---|
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree/fountain/building/roads.py` 已拆出,但**没有 `features/` 注册表** |
| P2 | 要素注册表 | ✅ 已完成(保守版)——`features.py` 注册 OSM way 分发顺序;`water/grass/scrub/tree/fountain/building/roads.py` 已拆出。材质、计数、metadata ownership 仍保留在 `generate_scene.py` |
| P3 | 材质契约化(自定义属性传递 spec | ✅ 已完成——`catalog.MATERIALS[*]["cesium"]``materials.from_spec()` 写入 `material["cesium_export"]``export_cesium.py` 优先读该属性;四张材质名表仅作旧 `.blend` 回退 |
### 已知缺陷(记录在案,本轮不修)

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": {}
}