Refactor fountain assembly module
This commit is contained in:
@@ -50,7 +50,8 @@
|
|||||||
| `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 |
|
||||||
| `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 |
|
| `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` 的两条隐含约定
|
### `geom.py` 的两条隐含约定
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
|
|
||||||
## 要素模块的统一形状
|
## 要素模块的统一形状
|
||||||
|
|
||||||
`water.py` / `grass.py` / `scrub.py` 三个要素模块签名一致:
|
`water.py` / `grass.py` / `scrub.py` 三个面要素模块签名一致:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def assemble(ring, [way_id,] scene_xmin, scene_xmax, scene_ymin, scene_ymax,
|
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`)
|
的 `focus`)
|
||||||
4. **不自己找数据**。ring 由 `generate_scene.py` 传入,模块只负责装配
|
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 要素
|
### 加一种新 OSM 要素
|
||||||
|
|
||||||
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
||||||
|
|||||||
@@ -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.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` 回退 |
|
| P3 | 材质契约化(自定义属性传递 spec) | ✅ 已完成——`catalog.MATERIALS[*]["cesium"]` 经 `materials.from_spec()` 写入 `material["cesium_export"]`,`export_cesium.py` 优先读该属性;四张材质名表仅作旧 `.blend` 回退 |
|
||||||
|
|
||||||
### 已知缺陷(记录在案,本轮不修)
|
### 已知缺陷(记录在案,本轮不修)
|
||||||
|
|||||||
1
.trellis/tasks/08-03-extract-fountain-module/check.jsonl
Normal file
1
.trellis/tasks/08-03-extract-fountain-module/check.jsonl
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. 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."}
|
||||||
53
.trellis/tasks/08-03-extract-fountain-module/design.md
Normal file
53
.trellis/tasks/08-03-extract-fountain-module/design.md
Normal file
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. 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."}
|
||||||
40
.trellis/tasks/08-03-extract-fountain-module/implement.md
Normal file
40
.trellis/tasks/08-03-extract-fountain-module/implement.md
Normal file
@@ -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.
|
||||||
73
.trellis/tasks/08-03-extract-fountain-module/prd.md
Normal file
73
.trellis/tasks/08-03-extract-fountain-module/prd.md
Normal file
@@ -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_<id>_Basin`
|
||||||
|
- `Fountain_<id>_Water`
|
||||||
|
- `Fountain_<id>_Pedestal`
|
||||||
|
- `Fountain_<id>_Water_Crown`
|
||||||
|
- `Fountain_<id>_Droplet_<n>`
|
||||||
|
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.
|
||||||
26
.trellis/tasks/08-03-extract-fountain-module/task.json
Normal file
26
.trellis/tasks/08-03-extract-fountain-module/task.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -54,11 +54,11 @@ from osmassets.mesh import ( # noqa: E402
|
|||||||
add_polyline,
|
add_polyline,
|
||||||
add_roof,
|
add_roof,
|
||||||
add_wall_panel,
|
add_wall_panel,
|
||||||
link_object_to_collection,
|
|
||||||
make_prism,
|
make_prism,
|
||||||
new_collection,
|
new_collection,
|
||||||
)
|
)
|
||||||
from osmassets.osm import Projector, parse_height, parse_osm # noqa: E402
|
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 water as _water # noqa: E402
|
||||||
from osmassets import grass as _grass # noqa: E402
|
from osmassets import grass as _grass # noqa: E402
|
||||||
from osmassets import scrub as _scrub # 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,
|
# Building assembly stays in this file because it needs make_prism, add_roof,
|
||||||
# add_wall_panel, and add_building_details — Blender geometry helpers that
|
# add_wall_panel, and add_building_details — Blender geometry helpers that
|
||||||
# live a few lines above. The other features moved to osmassets/{water,grass,
|
# live a few lines above. The other feature assembly code lives under
|
||||||
# scrub}.py and take only pure-geometry primitives (MeshBatch / clip_polygon).
|
# osmassets/ and keeps build() focused on data dispatch, materials, and counts.
|
||||||
def _assemble_building(ring, way_id, tag, args, buildings_c, building_mats):
|
def _assemble_building(ring, way_id, tag, args, buildings_c, building_mats):
|
||||||
industrial = (tag.get("building") == "industrial" and
|
industrial = (tag.get("building") == "industrial" and
|
||||||
way_id not in args["office_overrides"])
|
way_id not in args["office_overrides"])
|
||||||
@@ -636,48 +636,6 @@ def sample_scrub_interior_trees(ring, way_id):
|
|||||||
return trees
|
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):
|
def look_at(obj, target):
|
||||||
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()
|
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()
|
||||||
|
|
||||||
@@ -898,7 +856,7 @@ def build(args):
|
|||||||
if not projector.inside(feature["coord"]):
|
if not projector.inside(feature["coord"]):
|
||||||
continue
|
continue
|
||||||
fx, fy = projector.xy(feature["coord"])
|
fx, fy = projector.xy(feature["coord"])
|
||||||
add_fountain("Fountain_" + str(feature["id"]), fx, fy,
|
_fountain.assemble("Fountain_" + str(feature["id"]), fx, fy,
|
||||||
props_c, fountain_mats)
|
props_c, fountain_mats)
|
||||||
counts["fountain_count"] += 1
|
counts["fountain_count"] += 1
|
||||||
|
|
||||||
|
|||||||
49
blender/osmassets/fountain.py
Normal file
49
blender/osmassets/fountain.py
Normal file
@@ -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"])
|
||||||
@@ -11,9 +11,13 @@
|
|||||||
parity before/after 只有 `.blend` 新增 `props.cesium_export` 与 `.blend` bytes 变化,
|
parity before/after 只有 `.blend` 新增 `props.cesium_export` 与 `.blend` bytes 变化,
|
||||||
无 GLB/material/metadata 结构漂移。
|
无 GLB/material/metadata 结构漂移。
|
||||||
- 收尾临时 `osmassets` 重构施工计划:P0/P1/P3 的长期知识已经进入 changelog 和 Trellis spec;
|
- 收尾临时 `osmassets` 重构施工计划:P0/P1/P3 的长期知识已经进入 changelog 和 Trellis spec;
|
||||||
P2 仍是后续重构方向(`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里,
|
当时 P2 仍是后续重构方向(`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里,
|
||||||
尚未形成完整 `features/` 注册表)。删除临时计划文件,避免继续传播旧行号和
|
尚未形成完整 `features/` 注册表)。删除临时计划文件,避免继续传播旧行号和
|
||||||
`catalog.CESIUM_EXPORT` 死代码等过期事实。
|
`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(三)远看发黑的真正原因:反照率没被提亮
|
## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user