diff --git a/.trellis/spec/blender/index.md b/.trellis/spec/blender/index.md index 33b0223..1ea6d84 100644 --- a/.trellis/spec/blender/index.md +++ b/.trellis/spec/blender/index.md @@ -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 · 工具 | diff --git a/.trellis/spec/blender/module-structure.md b/.trellis/spec/blender/module-structure.md index 88a1c3f..edc589e 100644 --- a/.trellis/spec/blender/module-structure.md +++ b/.trellis/spec/blender/module-structure.md @@ -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_` / `OSM_Road_` 对象。 +### 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/.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` | diff --git a/.trellis/spec/guides/artifact-parity-guide.md b/.trellis/spec/guides/artifact-parity-guide.md index c3e6c44..87c8528 100644 --- a/.trellis/spec/guides/artifact-parity-guide.md +++ b/.trellis/spec/guides/artifact-parity-guide.md @@ -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` 回退 | ### 已知缺陷(记录在案,本轮不修) diff --git a/.trellis/tasks/08-03-design-feature-registry/check.jsonl b/.trellis/tasks/08-03-design-feature-registry/check.jsonl new file mode 100644 index 0000000..ba1057f --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/check.jsonl @@ -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."} diff --git a/.trellis/tasks/08-03-design-feature-registry/design.md b/.trellis/tasks/08-03-design-feature-registry/design.md new file mode 100644 index 0000000..cd55e47 --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/design.md @@ -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. diff --git a/.trellis/tasks/08-03-design-feature-registry/implement.jsonl b/.trellis/tasks/08-03-design-feature-registry/implement.jsonl new file mode 100644 index 0000000..35105d9 --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/implement.jsonl @@ -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."} diff --git a/.trellis/tasks/08-03-design-feature-registry/implement.md b/.trellis/tasks/08-03-design-feature-registry/implement.md new file mode 100644 index 0000000..ab72a98 --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/implement.md @@ -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. diff --git a/.trellis/tasks/08-03-design-feature-registry/prd.md b/.trellis/tasks/08-03-design-feature-registry/prd.md new file mode 100644 index 0000000..81acbcb --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/prd.md @@ -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. diff --git a/.trellis/tasks/08-03-design-feature-registry/task.json b/.trellis/tasks/08-03-design-feature-registry/task.json new file mode 100644 index 0000000..f656eb1 --- /dev/null +++ b/.trellis/tasks/08-03-design-feature-registry/task.json @@ -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": {} +} \ No newline at end of file diff --git a/blender/generate_scene.py b/blender/generate_scene.py index 2229de5..6411646 100644 --- a/blender/generate_scene.py +++ b/blender/generate_scene.py @@ -34,6 +34,7 @@ if _HERE not in sys.path: sys.path.insert(0, _HERE) 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) distance_to_ring, point_in_polygon, @@ -669,46 +670,71 @@ def build(args): return obj focus_points = [] - 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"] - if tag.get("natural") == "water" or tag.get("water") == "lake": - counts["lake_count"] += _water.assemble(ring, scene_xmin, scene_xmax, - scene_ymin, scene_ymax, - water_c, water_mat) - elif tag.get("landuse") == "grass": - added, tufts, ring_pts = _grass.assemble( - ring, way["id"], scene_xmin, scene_xmax, scene_ymin, scene_ymax, - green_c, grass_mat, tuft_variants, add_grass_tufts) - counts["grass_count"] += added - counts["grass_tuft_count"] += tufts - if ring_pts: - grass_rings.append(ring_pts) - focus_points.extend(ring_pts) - elif tag.get("natural") == "scrub" and len(ring) >= 3: - added, ring_pts = _scrub.assemble(ring, way["id"], scene_xmin, scene_xmax, - scene_ymin, scene_ymax, green_c, scrub_mat, - add_scrub_patch_with_bushes) - counts["scrub_count"] += added - if ring_pts: - focus_points.extend(ring_pts) - new_scrub_trees = sample_scrub_interior_trees(ring_pts, way["id"]) - scrub_trees.extend(new_scrub_trees) - counts["scrub_tree_count"] += len(new_scrub_trees) - elif tag.get("natural") == "tree_row": - tree_rows.append((ring, tag)) - focus_points.extend(ring) - elif "building" in tag and len(ring) >= 3: - added, ind_added, ring_pts = _building.assemble( - ring, str(way["id"]), tag, args["office_overrides"], - buildings_c, building_mats) - counts["building_count"] += added - counts["industrial_count"] += ind_added - if ring_pts: - focus_points.extend(ring_pts) + + def handle_water(way, tag, ring): + counts["lake_count"] += _water.assemble(ring, scene_xmin, scene_xmax, + scene_ymin, scene_ymax, + water_c, water_mat) + + def handle_grass(way, tag, ring): + added, tufts, ring_pts = _grass.assemble( + ring, way["id"], scene_xmin, scene_xmax, scene_ymin, scene_ymax, + green_c, grass_mat, tuft_variants, add_grass_tufts) + counts["grass_count"] += added + counts["grass_tuft_count"] += tufts + if ring_pts: + grass_rings.append(ring_pts) + focus_points.extend(ring_pts) + + def handle_scrub(way, tag, ring): + added, ring_pts = _scrub.assemble(ring, way["id"], scene_xmin, scene_xmax, + scene_ymin, scene_ymax, green_c, scrub_mat, + add_scrub_patch_with_bushes) + counts["scrub_count"] += added + if ring_pts: + focus_points.extend(ring_pts) + new_scrub_trees = sample_scrub_interior_trees(ring_pts, way["id"]) + scrub_trees.extend(new_scrub_trees) + counts["scrub_tree_count"] += len(new_scrub_trees) + + def handle_tree_row(way, tag, ring): + tree_rows.append((ring, tag)) + focus_points.extend(ring) + + def handle_building(way, tag, ring): + added, ind_added, ring_pts = _building.assemble( + ring, str(way["id"]), tag, args["office_overrides"], + buildings_c, building_mats) + counts["building_count"] += added + counts["industrial_count"] += ind_added + if 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") road_counts = {} diff --git a/blender/osmassets/features.py b/blender/osmassets/features.py new file mode 100644 index 0000000..7600184 --- /dev/null +++ b/blender/osmassets/features.py @@ -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 diff --git a/docs/changelog.md b/docs/changelog.md index 21343c7..fb3d127 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -27,6 +27,12 @@ 继续负责 `ROAD_LAYERS` 遍历、`catalog.check_layers()` warning、`road_counts`、scene metadata 和 `SCENE_DONE` JSON。Blender/Cesium parity before/after 在两个样本上均 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(三)远看发黑的真正原因:反照率没被提亮