Add feature registry dispatch
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
│ osmassets/materials.py 材质构建(消费 catalog 的声明) │
|
│ osmassets/materials.py 材质构建(消费 catalog 的声明) │
|
||||||
│ osmassets/tree.py 树实例化 │
|
│ osmassets/tree.py 树实例化 │
|
||||||
│ osmassets/water.py grass.py scrub.py │
|
│ osmassets/water.py grass.py scrub.py │
|
||||||
|
│ osmassets/features.py 要素分发注册 │
|
||||||
│ osmassets/building.py fountain.py roads.py 要素装配 │
|
│ osmassets/building.py fountain.py roads.py 要素装配 │
|
||||||
│ │
|
│ │
|
||||||
│ generate_scene.py export_cesium.py 两个入口 │
|
│ generate_scene.py export_cesium.py 两个入口 │
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
|
|
||||||
| | `generate_scene.py` | `export_cesium.py` |
|
| | `generate_scene.py` | `export_cesium.py` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 行数 | 999 | 624 |
|
| 行数 | 895 | 647 |
|
||||||
| 调用 | `--background --factory-startup --python` | `--background --python` |
|
| 调用 | `--background --factory-startup --python` | `--background --python` |
|
||||||
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
|
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
|
||||||
| 输出 | `--output`(.blend)、`--render`(.png) | `--glb`、`--metadata`(.json) |
|
| 输出 | `--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 · 入口 |
|
| `export_cesium.py` | 647 | bpy · 入口 |
|
||||||
| `osmassets/tree.py` | 318 | bpy |
|
| `osmassets/tree.py` | 318 | bpy |
|
||||||
| `osmassets/geom.py` | 241 | 纯 |
|
| `osmassets/geom.py` | 241 | 纯 |
|
||||||
@@ -129,6 +130,7 @@
|
|||||||
| `osmassets/catalog.py` | 197 | 纯 |
|
| `osmassets/catalog.py` | 197 | 纯 |
|
||||||
| `osmassets/mesh.py` | 126 | bpy |
|
| `osmassets/mesh.py` | 126 | bpy |
|
||||||
| `osmassets/osm.py` | 89 | 纯 |
|
| `osmassets/osm.py` | 89 | 纯 |
|
||||||
|
| `osmassets/features.py` | 20 | bpy · 分发 |
|
||||||
| `osmassets/building.py` / `fountain.py` / `roads.py` | 56 / 49 / 40 | bpy |
|
| `osmassets/building.py` / `fountain.py` / `roads.py` | 56 / 49 / 40 | bpy |
|
||||||
| `osmassets/water.py` / `grass.py` / `scrub.py` | 15 / 22 / 13 | bpy |
|
| `osmassets/water.py` / `grass.py` / `scrub.py` | 15 / 22 / 13 | bpy |
|
||||||
| `tools/scene_digest.py` | 171 | bpy · 工具 |
|
| `tools/scene_digest.py` | 171 | bpy · 工具 |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
osmassets/mesh.py MeshBatch、prism、polyline
|
osmassets/mesh.py MeshBatch、prism、polyline
|
||||||
osmassets/materials.py 材质构建
|
osmassets/materials.py 材质构建
|
||||||
osmassets/tree.py 树实例化
|
osmassets/tree.py 树实例化
|
||||||
|
osmassets/features.py 要素分发注册
|
||||||
osmassets/water.py grass.py scrub.py
|
osmassets/water.py grass.py scrub.py
|
||||||
osmassets/building.py fountain.py roads.py 要素装配
|
osmassets/building.py fountain.py roads.py 要素装配
|
||||||
generate_scene.py export_cesium.py 两个入口脚本
|
generate_scene.py export_cesium.py 两个入口脚本
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
| `catalog.py` | 纯 | `ROAD_LAYERS`、`MATERIALS`、`road_material_specs()`、`check_layers()` |
|
| `catalog.py` | 纯 | `ROAD_LAYERS`、`MATERIALS`、`road_material_specs()`、`check_layers()` |
|
||||||
| `mesh.py` | bpy | `MeshBatch`、`make_prism`、`add_roof`、`add_wall_panel`、`add_polyline`、集合管理 |
|
| `mesh.py` | bpy | `MeshBatch`、`make_prism`、`add_roof`、`add_wall_panel`、`add_polyline`、集合管理 |
|
||||||
| `materials.py` | bpy | 把 `catalog` 的规格变成真实材质:`from_spec()`、贴图、程序化噪声、tint、alpha-clip |
|
| `materials.py` | bpy | 把 `catalog` 的规格变成真实材质:`from_spec()`、贴图、程序化噪声、tint、alpha-clip |
|
||||||
|
| `features.py` | bpy | `FeatureHandler` 和 `dispatch_ways()`,保持 OSM way 要素 first-match 分发顺序 |
|
||||||
| `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 |
|
| `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 |
|
||||||
| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 面要素的装配 |
|
| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 面要素的装配 |
|
||||||
| `building.py` | bpy | 单一 OSM 面要素 `building=*` 的装配 |
|
| `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"]`。模块只负责把已选定的
|
`scene["road_feature_counts"]` / `SCENE_DONE["road_features"]`。模块只负责把已选定的
|
||||||
GeoJSON layer 或 OSM fallback ways 变成 `Road_<layer_id>` / `OSM_Road_<way_id>` 对象。
|
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 要素
|
### 加一种新 OSM 要素
|
||||||
|
|
||||||
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
目标形态:**新增一个模块 + 在 OSM way handler 表里注册一项,不改主分发循环**。
|
||||||
|
|
||||||
1. 新建 `osmassets/<feature>.py`,写 `assemble(...)`,签名照抄上面
|
1. 新建 `osmassets/<feature>.py`,写 `assemble(...)`,签名照抄上面
|
||||||
2. 只 import 需要的:`from osmassets.geom import clip_polygon`、
|
2. 只 import 需要的:`from osmassets.geom import clip_polygon`、
|
||||||
`from osmassets.mesh import MeshBatch`
|
`from osmassets.mesh import MeshBatch`
|
||||||
3. 材质规格加进 `catalog.MATERIALS`(**追加到末尾**,顺序决定 GLB 材质索引)
|
3. 材质规格加进 `catalog.MATERIALS`(**追加到末尾**,顺序决定 GLB 材质索引)
|
||||||
4. `generate_scene.py` 的要素分发处加一行调用
|
4. `generate_scene.py` 的 OSM way handler 注册表里追加 handler,保持顺序语义明确
|
||||||
5. 纯几何部分若有新函数,放 `geom.py` 并**补 `blender/tests/test_pure.py`**
|
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` |
|
| 调用 | `--background --factory-startup --python` | `--background --python` |
|
||||||
| 输入 | `--osm` + `--geojson` | `--blend` |
|
| 输入 | `--osm` + `--geojson` | `--blend` |
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ capturedAt / durationMs / label
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
|
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
|
||||||
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
|
| 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` 回退 |
|
| P3 | 材质契约化(自定义属性传递 spec) | ✅ 已完成——`catalog.MATERIALS[*]["cesium"]` 经 `materials.from_spec()` 写入 `material["cesium_export"]`,`export_cesium.py` 优先读该属性;四张材质名表仅作旧 `.blend` 回退 |
|
||||||
|
|
||||||
### 已知缺陷(记录在案,本轮不修)
|
### 已知缺陷(记录在案,本轮不修)
|
||||||
|
|||||||
4
.trellis/tasks/08-03-design-feature-registry/check.jsonl
Normal file
4
.trellis/tasks/08-03-design-feature-registry/check.jsonl
Normal 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."}
|
||||||
103
.trellis/tasks/08-03-design-feature-registry/design.md
Normal file
103
.trellis/tasks/08-03-design-feature-registry/design.md
Normal 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.
|
||||||
@@ -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."}
|
||||||
57
.trellis/tasks/08-03-design-feature-registry/implement.md
Normal file
57
.trellis/tasks/08-03-design-feature-registry/implement.md
Normal 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.
|
||||||
94
.trellis/tasks/08-03-design-feature-registry/prd.md
Normal file
94
.trellis/tasks/08-03-design-feature-registry/prd.md
Normal 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.
|
||||||
26
.trellis/tasks/08-03-design-feature-registry/task.json
Normal file
26
.trellis/tasks/08-03-design-feature-registry/task.json
Normal 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": {}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ if _HERE not in sys.path:
|
|||||||
sys.path.insert(0, _HERE)
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
from osmassets import catalog # noqa: E402
|
from osmassets import catalog # noqa: E402
|
||||||
|
from osmassets import features as _features # noqa: E402
|
||||||
from osmassets.geom import ( # noqa: E402 (needs the sys.path line above)
|
from osmassets.geom import ( # noqa: E402 (needs the sys.path line above)
|
||||||
distance_to_ring,
|
distance_to_ring,
|
||||||
point_in_polygon,
|
point_in_polygon,
|
||||||
@@ -669,17 +670,13 @@ def build(args):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
focus_points = []
|
focus_points = []
|
||||||
for way in ways:
|
|
||||||
coords = way["coords"]
|
def handle_water(way, tag, ring):
|
||||||
if not any(projector.inside(c) for c in coords):
|
|
||||||
continue
|
|
||||||
ring = projector.ring(coords)
|
|
||||||
tag = way["tags"]
|
|
||||||
if tag.get("natural") == "water" or tag.get("water") == "lake":
|
|
||||||
counts["lake_count"] += _water.assemble(ring, scene_xmin, scene_xmax,
|
counts["lake_count"] += _water.assemble(ring, scene_xmin, scene_xmax,
|
||||||
scene_ymin, scene_ymax,
|
scene_ymin, scene_ymax,
|
||||||
water_c, water_mat)
|
water_c, water_mat)
|
||||||
elif tag.get("landuse") == "grass":
|
|
||||||
|
def handle_grass(way, tag, ring):
|
||||||
added, tufts, ring_pts = _grass.assemble(
|
added, tufts, ring_pts = _grass.assemble(
|
||||||
ring, way["id"], scene_xmin, scene_xmax, scene_ymin, scene_ymax,
|
ring, way["id"], scene_xmin, scene_xmax, scene_ymin, scene_ymax,
|
||||||
green_c, grass_mat, tuft_variants, add_grass_tufts)
|
green_c, grass_mat, tuft_variants, add_grass_tufts)
|
||||||
@@ -688,7 +685,8 @@ def build(args):
|
|||||||
if ring_pts:
|
if ring_pts:
|
||||||
grass_rings.append(ring_pts)
|
grass_rings.append(ring_pts)
|
||||||
focus_points.extend(ring_pts)
|
focus_points.extend(ring_pts)
|
||||||
elif tag.get("natural") == "scrub" and len(ring) >= 3:
|
|
||||||
|
def handle_scrub(way, tag, ring):
|
||||||
added, ring_pts = _scrub.assemble(ring, way["id"], scene_xmin, scene_xmax,
|
added, ring_pts = _scrub.assemble(ring, way["id"], scene_xmin, scene_xmax,
|
||||||
scene_ymin, scene_ymax, green_c, scrub_mat,
|
scene_ymin, scene_ymax, green_c, scrub_mat,
|
||||||
add_scrub_patch_with_bushes)
|
add_scrub_patch_with_bushes)
|
||||||
@@ -698,10 +696,12 @@ def build(args):
|
|||||||
new_scrub_trees = sample_scrub_interior_trees(ring_pts, way["id"])
|
new_scrub_trees = sample_scrub_interior_trees(ring_pts, way["id"])
|
||||||
scrub_trees.extend(new_scrub_trees)
|
scrub_trees.extend(new_scrub_trees)
|
||||||
counts["scrub_tree_count"] += len(new_scrub_trees)
|
counts["scrub_tree_count"] += len(new_scrub_trees)
|
||||||
elif tag.get("natural") == "tree_row":
|
|
||||||
|
def handle_tree_row(way, tag, ring):
|
||||||
tree_rows.append((ring, tag))
|
tree_rows.append((ring, tag))
|
||||||
focus_points.extend(ring)
|
focus_points.extend(ring)
|
||||||
elif "building" in tag and len(ring) >= 3:
|
|
||||||
|
def handle_building(way, tag, ring):
|
||||||
added, ind_added, ring_pts = _building.assemble(
|
added, ind_added, ring_pts = _building.assemble(
|
||||||
ring, str(way["id"]), tag, args["office_overrides"],
|
ring, str(way["id"]), tag, args["office_overrides"],
|
||||||
buildings_c, building_mats)
|
buildings_c, building_mats)
|
||||||
@@ -710,6 +710,32 @@ def build(args):
|
|||||||
if ring_pts:
|
if ring_pts:
|
||||||
focus_points.extend(ring_pts)
|
focus_points.extend(ring_pts)
|
||||||
|
|
||||||
|
way_handlers = (
|
||||||
|
_features.FeatureHandler(
|
||||||
|
"water",
|
||||||
|
lambda way, tag, ring: tag.get("natural") == "water"
|
||||||
|
or tag.get("water") == "lake",
|
||||||
|
handle_water),
|
||||||
|
_features.FeatureHandler(
|
||||||
|
"grass",
|
||||||
|
lambda way, tag, ring: tag.get("landuse") == "grass",
|
||||||
|
handle_grass),
|
||||||
|
_features.FeatureHandler(
|
||||||
|
"scrub",
|
||||||
|
lambda way, tag, ring: tag.get("natural") == "scrub"
|
||||||
|
and len(ring) >= 3,
|
||||||
|
handle_scrub),
|
||||||
|
_features.FeatureHandler(
|
||||||
|
"tree_row",
|
||||||
|
lambda way, tag, ring: tag.get("natural") == "tree_row",
|
||||||
|
handle_tree_row),
|
||||||
|
_features.FeatureHandler(
|
||||||
|
"building",
|
||||||
|
lambda way, tag, ring: "building" in tag and len(ring) >= 3,
|
||||||
|
handle_building),
|
||||||
|
)
|
||||||
|
_features.dispatch_ways(ways, projector, way_handlers)
|
||||||
|
|
||||||
geojson_dir = args.get("geojson")
|
geojson_dir = args.get("geojson")
|
||||||
road_counts = {}
|
road_counts = {}
|
||||||
if geojson_dir and os.path.isdir(geojson_dir):
|
if geojson_dir and os.path.isdir(geojson_dir):
|
||||||
|
|||||||
20
blender/osmassets/features.py
Normal file
20
blender/osmassets/features.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Feature registry and dispatch helpers for scene assembly phases."""
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
|
FeatureHandler = namedtuple("FeatureHandler", ("name", "matches", "handle"))
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_ways(ways, projector, handlers):
|
||||||
|
"""Dispatch OSM ways to the first matching handler, preserving order."""
|
||||||
|
for way in ways:
|
||||||
|
coords = way["coords"]
|
||||||
|
if not any(projector.inside(c) for c in coords):
|
||||||
|
continue
|
||||||
|
ring = projector.ring(coords)
|
||||||
|
tag = way["tags"]
|
||||||
|
for handler in handlers:
|
||||||
|
if handler.matches(way, tag, ring):
|
||||||
|
handler.handle(way, tag, ring)
|
||||||
|
break
|
||||||
@@ -27,6 +27,12 @@
|
|||||||
继续负责 `ROAD_LAYERS` 遍历、`catalog.check_layers()` warning、`road_counts`、scene
|
继续负责 `ROAD_LAYERS` 遍历、`catalog.check_layers()` warning、`road_counts`、scene
|
||||||
metadata 和 `SCENE_DONE` JSON。Blender/Cesium parity before/after 在两个样本上均
|
metadata 和 `SCENE_DONE` JSON。Blender/Cesium parity before/after 在两个样本上均
|
||||||
identical;P2 剩余工作更新为完整 `features/` 注册表设计。
|
identical;P2 剩余工作更新为完整 `features/` 注册表设计。
|
||||||
|
- 引入保守版 feature registry:新增 `blender/osmassets/features.py`,用
|
||||||
|
`FeatureHandler` / `dispatch_ways()` 固化 OSM way 要素 first-match 分发顺序
|
||||||
|
`water -> grass -> scrub -> tree_row -> building`;`generate_scene.py` 仍保留材质创建、
|
||||||
|
collection 顺序、counts、focus、road counts、scene metadata 和 `SCENE_DONE` ownership。
|
||||||
|
Blender/Cesium parity before/after 在两个样本上均 identical;P2 registry 缺口收口为
|
||||||
|
当前保守契约,未做全量 ownership 反转。
|
||||||
|
|
||||||
## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮
|
## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user