From 627105555f2982161b07ba444773b6f8286e97f8 Mon Sep 17 00:00:00 2001 From: que01 Date: Mon, 3 Aug 2026 12:35:51 +0800 Subject: [PATCH] Refactor fountain assembly module --- .trellis/spec/blender/module-structure.md | 16 +++- .trellis/spec/guides/artifact-parity-guide.md | 2 +- .../08-03-extract-fountain-module/check.jsonl | 1 + .../08-03-extract-fountain-module/design.md | 53 ++++++++++++++ .../implement.jsonl | 1 + .../implement.md | 40 ++++++++++ .../08-03-extract-fountain-module/prd.md | 73 +++++++++++++++++++ .../08-03-extract-fountain-module/task.json | 26 +++++++ blender/generate_scene.py | 52 ++----------- blender/osmassets/fountain.py | 49 +++++++++++++ docs/changelog.md | 6 +- 11 files changed, 268 insertions(+), 51 deletions(-) create mode 100644 .trellis/tasks/08-03-extract-fountain-module/check.jsonl create mode 100644 .trellis/tasks/08-03-extract-fountain-module/design.md create mode 100644 .trellis/tasks/08-03-extract-fountain-module/implement.jsonl create mode 100644 .trellis/tasks/08-03-extract-fountain-module/implement.md create mode 100644 .trellis/tasks/08-03-extract-fountain-module/prd.md create mode 100644 .trellis/tasks/08-03-extract-fountain-module/task.json create mode 100644 blender/osmassets/fountain.py diff --git a/.trellis/spec/blender/module-structure.md b/.trellis/spec/blender/module-structure.md index e842853..89e1b0a 100644 --- a/.trellis/spec/blender/module-structure.md +++ b/.trellis/spec/blender/module-structure.md @@ -50,7 +50,8 @@ | `mesh.py` | bpy | `MeshBatch`、`make_prism`、`add_roof`、`add_wall_panel`、`add_polyline`、集合管理 | | `materials.py` | bpy | 把 `catalog` 的规格变成真实材质:`from_spec()`、贴图、程序化噪声、tint、alpha-clip | | `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 | -| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 要素的装配 | +| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 面要素的装配 | +| `fountain.py` | bpy | 单一 OSM 点要素 `amenity=fountain` 的装配 | ### `geom.py` 的两条隐含约定 @@ -62,7 +63,7 @@ ## 要素模块的统一形状 -`water.py` / `grass.py` / `scrub.py` 三个要素模块签名一致: +`water.py` / `grass.py` / `scrub.py` 三个面要素模块签名一致: ```python def assemble(ring, [way_id,] scene_xmin, scene_xmax, scene_ymin, scene_ymax, @@ -83,6 +84,17 @@ def assemble(ring, [way_id,] scene_xmin, scene_xmax, scene_ymin, scene_ymax, 的 `focus`) 4. **不自己找数据**。ring 由 `generate_scene.py` 传入,模块只负责装配 +`fountain.py` 是点要素模块,不收 ring 或裁剪边界: + +```python +def assemble(name, x, y, collection, materials): + ... +``` + +`generate_scene.py` 仍负责过滤 `amenity=fountain`、检查点是否在范围内、投影坐标、 +创建三张 fountain 材质,并维护 `counts["fountain_count"]`。模块只负责在已投影 +坐标上创建 basin / water / pedestal / spray objects,保持对象名和自定义属性不变。 + ### 加一种新 OSM 要素 目标形态:**新增一个模块 + 注册一行,不改 `build()`**。 diff --git a/.trellis/spec/guides/artifact-parity-guide.md b/.trellis/spec/guides/artifact-parity-guide.md index 49bf966..45732cb 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.py` 已拆出,但**没有 `features/` 注册表**,`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里 | +| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree/fountain.py` 已拆出,但**没有 `features/` 注册表**,`building` / `roads` 仍在 `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-extract-fountain-module/check.jsonl b/.trellis/tasks/08-03-extract-fountain-module/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-03-extract-fountain-module/design.md b/.trellis/tasks/08-03-extract-fountain-module/design.md new file mode 100644 index 0000000..add5648 --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/design.md @@ -0,0 +1,53 @@ +# Design + +## Architecture + +`fountain.py` will join the existing bpy-layer feature modules under +`blender/osmassets/`. It may import `bpy`, `math`, and the collection helper +needed to preserve current object linking behavior. + +The module will expose one function: + +```python +def assemble(name, x, y, collection, materials): + ... +``` + +The signature mirrors the existing inline `add_fountain(name, x, y, collection, +materials)` call site to keep the migration mechanical and parity-friendly. + +## Boundaries + +- `generate_scene.py` keeps OSM feature filtering, projection, material creation, + and count ownership. +- `fountain.py` owns only Blender object creation for one fountain at already + projected `(x, y)` coordinates. +- No pure-Python module may import `bpy`; this module is explicitly in the + bpy layer, like `water.py`, `grass.py`, `scrub.py`, and `tree.py`. + +## Data Flow + +1. `generate_scene.py` creates `fountain_mats` from `catalog.MATERIALS`. +2. `generate_scene.py` finds point features where `tags.amenity == "fountain"`. +3. `Projector.xy()` converts the OSM coordinate to local meters. +4. `fountain.assemble("Fountain_" + id, fx, fy, props_c, fountain_mats)` creates + the existing basin, water disk, pedestal, crown, and droplets. +5. `generate_scene.py` increments `counts["fountain_count"]`. + +## Compatibility + +The refactor must preserve all generated object names, primitive dimensions, +materials, smoothing flags, and custom properties. Cesium export reads the +resulting `.blend`; it should see an equivalent scene graph. + +## Trade-Offs + +A full feature registry is intentionally deferred. Starting with a one-module +extraction keeps the diff small and lets parity isolate any regression to the +fountain move before higher-risk building or road work. + +## Rollback + +Rollback is straightforward: remove `fountain.py`, restore the inline +`add_fountain()` function, remove the module import, and restore the original +call site. diff --git a/.trellis/tasks/08-03-extract-fountain-module/implement.jsonl b/.trellis/tasks/08-03-extract-fountain-module/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-03-extract-fountain-module/implement.md b/.trellis/tasks/08-03-extract-fountain-module/implement.md new file mode 100644 index 0000000..a6f30cd --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/implement.md @@ -0,0 +1,40 @@ +# Implementation Plan + +## Checklist + +1. Capture a parity baseline before product-code edits. +2. Add `blender/osmassets/fountain.py` with the current fountain assembly logic. +3. Import the module in `blender/generate_scene.py`. +4. Remove inline `add_fountain()` from `generate_scene.py`. +5. Replace the call site with `fountain.assemble(...)`. +6. Run Python validation: + - `python3 -m py_compile blender/osmassets/fountain.py` + - `python3 -m py_compile blender/generate_scene.py` + - `python3 -m unittest blender/tests/test_pure.py` +7. Capture parity after the refactor. +8. Compare before/after parity snapshots and inspect any non-ignored diff. +9. Update `.trellis/spec` or `docs/changelog.md` only if the work reveals a + durable new constraint or user-visible change. +10. Commit, archive the task, and record the journal entry. + +## Validation Commands + +```bash +node scripts/parity.js capture fountain-module-before --stages blender,cesium +python3 -m py_compile blender/osmassets/fountain.py +python3 -m py_compile blender/generate_scene.py +python3 -m unittest blender/tests/test_pure.py +node scripts/parity.js capture fountain-module-after --stages blender,cesium +node scripts/parity.js compare fountain-module-before fountain-module-after +``` + +## Risk Points + +- `link_object_to_collection()` must be used the same way as before so object + collection membership stays unchanged. +- The basin custom property must remain on the basin object only. +- Droplet order and names must stay stable. +- `math` usage may move with the function; remove the import from + `generate_scene.py` only if no remaining code uses it. +- Parity is mandatory because this is a pure refactor of bpy-layer scene + generation. diff --git a/.trellis/tasks/08-03-extract-fountain-module/prd.md b/.trellis/tasks/08-03-extract-fountain-module/prd.md new file mode 100644 index 0000000..3c6db09 --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/prd.md @@ -0,0 +1,73 @@ +# Extract fountain module + +## Goal + +Move the fountain assembly code out of `blender/generate_scene.py` into a +dedicated bpy-layer module, keeping the generated `.blend` / `.glb` structure +unchanged. This is the first low-risk step toward the documented P2 feature +module / registry direction. + +## Background + +- `.trellis/spec/guides/artifact-parity-guide.md` records P2 as partial: + `water.py`, `grass.py`, `scrub.py`, and `tree.py` exist, but `building`, + `fountain`, and `roads` still live in `generate_scene.py`. +- `blender/generate_scene.py:639` defines `add_fountain()` inline. +- `blender/generate_scene.py:895` dispatches OSM point features with + `amenity=fountain`, calls `add_fountain("Fountain_" + id, ...)`, and + increments `counts["fountain_count"]`. +- `blender/osmassets/catalog.py` already owns the three fountain materials: + `fountain_stone`, `fountain_water`, and `fountain_spray`. +- This task is a pure refactor. It must not change fountain geometry, object + names, materials, custom properties, counts, stdout markers, or Cesium export + behavior. + +## Requirements + +1. Add a new bpy-layer module at `blender/osmassets/fountain.py`. +2. Move the current fountain assembly behavior from `generate_scene.py` into + the new module. +3. Keep `generate_scene.py` responsible for: + - creating fountain materials from `catalog.MATERIALS`; + - filtering `point_features` for `amenity=fountain`; + - projecting coordinates; + - incrementing `counts["fountain_count"]`. +4. Preserve the existing object names: + - `Fountain__Basin` + - `Fountain__Water` + - `Fountain__Pedestal` + - `Fountain__Water_Crown` + - `Fountain__Droplet_` +5. Preserve the existing basin custom property: + `osm_feature = "amenity=fountain"`. +6. Do not introduce a full `features/` registry in this task. +7. Do not modify `building`, `roads`, material definitions, `MATERIALS` order, + `ROAD_LAYERS` order, stdout markers, or parity ignore lists. +8. Do not fix unrelated documented defects D1, D2, or D3. + +## Acceptance Criteria + +- [x] `blender/osmassets/fountain.py` contains the fountain assembly behavior + and imports only bpy-layer-safe dependencies. +- [x] `blender/generate_scene.py` imports and calls the new fountain module; + no inline `add_fountain()` function remains there. +- [x] Scene counts and `SCENE_DONE` JSON keys remain unchanged. +- [x] `python3 -m unittest blender/tests/test_pure.py` passes. +- [x] `python3 -m py_compile blender/osmassets/fountain.py` passes. +- [x] `python3 -m py_compile blender/generate_scene.py` passes. +- [x] A before/after parity comparison is run for the Blender/Cesium stages and + any non-ignored diff is either absent or explicitly explained as + expected. For this refactor, the expected result is no contract diff. + +## Out Of Scope + +- Full feature registry design or implementation. +- Building extraction. +- Road extraction. +- Fountain visual redesign or geometry changes. +- Material contract changes. +- Documentation-only cleanup of stale comments such as D3. + +## Open Questions + +None. diff --git a/.trellis/tasks/08-03-extract-fountain-module/task.json b/.trellis/tasks/08-03-extract-fountain-module/task.json new file mode 100644 index 0000000..c0dec61 --- /dev/null +++ b/.trellis/tasks/08-03-extract-fountain-module/task.json @@ -0,0 +1,26 @@ +{ + "id": "extract-fountain-module", + "name": "extract-fountain-module", + "title": "Extract fountain module", + "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 94ae44a..65a79d0 100644 --- a/blender/generate_scene.py +++ b/blender/generate_scene.py @@ -54,11 +54,11 @@ from osmassets.mesh import ( # noqa: E402 add_polyline, add_roof, add_wall_panel, - link_object_to_collection, make_prism, new_collection, ) from osmassets.osm import Projector, parse_height, parse_osm # noqa: E402 +from osmassets import fountain as _fountain # noqa: E402 from osmassets import water as _water # noqa: E402 from osmassets import grass as _grass # noqa: E402 from osmassets import scrub as _scrub # noqa: E402 @@ -67,8 +67,8 @@ from osmassets import tree as _tree # noqa: E402 # Building assembly stays in this file because it needs make_prism, add_roof, # add_wall_panel, and add_building_details — Blender geometry helpers that -# live a few lines above. The other features moved to osmassets/{water,grass, -# scrub}.py and take only pure-geometry primitives (MeshBatch / clip_polygon). +# live a few lines above. The other feature assembly code lives under +# osmassets/ and keeps build() focused on data dispatch, materials, and counts. def _assemble_building(ring, way_id, tag, args, buildings_c, building_mats): industrial = (tag.get("building") == "industrial" and way_id not in args["office_overrides"]) @@ -636,48 +636,6 @@ def sample_scrub_interior_trees(ring, way_id): return trees -def add_fountain(name, x, y, collection, materials): - def cylinder(part_name, radius, depth, z, material, vertices=48): - bpy.ops.mesh.primitive_cylinder_add( - vertices=vertices, radius=radius, depth=depth, - location=(x, y, z)) - obj = bpy.context.object - obj.name = name + "_" + part_name - link_object_to_collection(obj, collection) - obj.data.materials.append(material) - for polygon in obj.data.polygons: - polygon.use_smooth = True - return obj - - basin = cylinder("Basin", 3.0, 0.32, 0.16, - materials["fountain_stone"]) - basin["osm_feature"] = "amenity=fountain" - cylinder("Water", 2.52, 0.045, 0.335, - materials["fountain_water"]) - cylinder("Pedestal", 0.30, 0.78, 0.72, - materials["fountain_stone"], vertices=32) - - bpy.ops.mesh.primitive_uv_sphere_add( - segments=20, ring_count=10, radius=0.22, - location=(x, y, 1.30)) - crown = bpy.context.object - crown.name = name + "_Water_Crown" - link_object_to_collection(crown, collection) - crown.data.materials.append(materials["fountain_spray"]) - for index in range(8): - angle = math.tau * index / 8.0 - radius = 0.50 - bpy.ops.mesh.primitive_uv_sphere_add( - segments=12, ring_count=6, radius=0.075, - location=(x + math.cos(angle) * radius, - y + math.sin(angle) * radius, - 1.02 + 0.10 * math.sin(angle * 2.0))) - droplet = bpy.context.object - droplet.name = name + "_Droplet_" + str(index + 1) - link_object_to_collection(droplet, collection) - droplet.data.materials.append(materials["fountain_spray"]) - - def look_at(obj, target): obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler() @@ -898,8 +856,8 @@ def build(args): if not projector.inside(feature["coord"]): continue fx, fy = projector.xy(feature["coord"]) - add_fountain("Fountain_" + str(feature["id"]), fx, fy, - props_c, fountain_mats) + _fountain.assemble("Fountain_" + str(feature["id"]), fx, fy, + props_c, fountain_mats) counts["fountain_count"] += 1 bpy.ops.object.light_add(type="SUN", location=(0, 0, 500)) diff --git a/blender/osmassets/fountain.py b/blender/osmassets/fountain.py new file mode 100644 index 0000000..78a091f --- /dev/null +++ b/blender/osmassets/fountain.py @@ -0,0 +1,49 @@ +"""Fountain feature assembly (`amenity=fountain`).""" + +import math + +import bpy + +from osmassets.mesh import link_object_to_collection + + +def assemble(name, x, y, collection, materials): + def cylinder(part_name, radius, depth, z, material, vertices=48): + bpy.ops.mesh.primitive_cylinder_add( + vertices=vertices, radius=radius, depth=depth, + location=(x, y, z)) + obj = bpy.context.object + obj.name = name + "_" + part_name + link_object_to_collection(obj, collection) + obj.data.materials.append(material) + for polygon in obj.data.polygons: + polygon.use_smooth = True + return obj + + basin = cylinder("Basin", 3.0, 0.32, 0.16, + materials["fountain_stone"]) + basin["osm_feature"] = "amenity=fountain" + cylinder("Water", 2.52, 0.045, 0.335, + materials["fountain_water"]) + cylinder("Pedestal", 0.30, 0.78, 0.72, + materials["fountain_stone"], vertices=32) + + bpy.ops.mesh.primitive_uv_sphere_add( + segments=20, ring_count=10, radius=0.22, + location=(x, y, 1.30)) + crown = bpy.context.object + crown.name = name + "_Water_Crown" + link_object_to_collection(crown, collection) + crown.data.materials.append(materials["fountain_spray"]) + for index in range(8): + angle = math.tau * index / 8.0 + radius = 0.50 + bpy.ops.mesh.primitive_uv_sphere_add( + segments=12, ring_count=6, radius=0.075, + location=(x + math.cos(angle) * radius, + y + math.sin(angle) * radius, + 1.02 + 0.10 * math.sin(angle * 2.0))) + droplet = bpy.context.object + droplet.name = name + "_Droplet_" + str(index + 1) + link_object_to_collection(droplet, collection) + droplet.data.materials.append(materials["fountain_spray"]) diff --git a/docs/changelog.md b/docs/changelog.md index 86101ad..1f8b051 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,9 +11,13 @@ parity before/after 只有 `.blend` 新增 `props.cesium_export` 与 `.blend` bytes 变化, 无 GLB/material/metadata 结构漂移。 - 收尾临时 `osmassets` 重构施工计划:P0/P1/P3 的长期知识已经进入 changelog 和 Trellis spec; - P2 仍是后续重构方向(`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里, + 当时 P2 仍是后续重构方向(`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里, 尚未形成完整 `features/` 注册表)。删除临时计划文件,避免继续传播旧行号和 `catalog.CESIUM_EXPORT` 死代码等过期事实。 +- 抽出 fountain 要素装配:新增 `blender/osmassets/fountain.py`,`generate_scene.py` + 保留点要素过滤、投影、材质创建和计数。Blender/Cesium parity before/after 在 + `nantaizi-lake-innovation-valley` 与 `hanyang-block` 两个样本上均 identical;P2 + 剩余未拆出部分更新为 `building` / `roads`。 ## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮