Refactor building assembly module
This commit is contained in:
@@ -37,7 +37,8 @@
|
|||||||
│ osmassets/mesh.py MeshBatch 等几何构建 │
|
│ osmassets/mesh.py MeshBatch 等几何构建 │
|
||||||
│ 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/building.py fountain.py 要素装配 │
|
||||||
│ │
|
│ │
|
||||||
│ generate_scene.py export_cesium.py 两个入口 │
|
│ generate_scene.py export_cesium.py 两个入口 │
|
||||||
│ tools/scene_digest.py 结构摘要工具 │
|
│ tools/scene_digest.py 结构摘要工具 │
|
||||||
@@ -120,14 +121,15 @@
|
|||||||
|
|
||||||
| 文件 | 行数 | 层 |
|
| 文件 | 行数 | 层 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `generate_scene.py` | 999 | bpy · 入口 |
|
| `generate_scene.py` | 899 | bpy · 入口 |
|
||||||
| `export_cesium.py` | 624 | bpy · 入口 |
|
| `export_cesium.py` | 647 | bpy · 入口 |
|
||||||
| `osmassets/tree.py` | 318 | bpy |
|
| `osmassets/tree.py` | 318 | bpy |
|
||||||
| `osmassets/geom.py` | 241 | 纯 |
|
| `osmassets/geom.py` | 241 | 纯 |
|
||||||
| `osmassets/materials.py` | 218 | bpy |
|
| `osmassets/materials.py` | 232 | bpy |
|
||||||
| `osmassets/catalog.py` | 195 | 纯 |
|
| `osmassets/catalog.py` | 197 | 纯 |
|
||||||
| `osmassets/mesh.py` | 126 | bpy |
|
| `osmassets/mesh.py` | 126 | bpy |
|
||||||
| `osmassets/osm.py` | 89 | 纯 |
|
| `osmassets/osm.py` | 89 | 纯 |
|
||||||
|
| `osmassets/building.py` / `fountain.py` | 56 / 49 | 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 · 工具 |
|
||||||
| `tests/test_pure.py` | 372 | 纯 · 测试 |
|
| `tests/test_pure.py` | 372 | 纯 · 测试 |
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
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/water.py grass.py scrub.py 要素装配
|
osmassets/water.py grass.py scrub.py
|
||||||
|
osmassets/building.py fountain.py 要素装配
|
||||||
generate_scene.py export_cesium.py 两个入口脚本
|
generate_scene.py export_cesium.py 两个入口脚本
|
||||||
tools/scene_digest.py 结构摘要工具
|
tools/scene_digest.py 结构摘要工具
|
||||||
```
|
```
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
| `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 面要素的装配 |
|
||||||
|
| `building.py` | bpy | 单一 OSM 面要素 `building=*` 的装配 |
|
||||||
| `fountain.py` | bpy | 单一 OSM 点要素 `amenity=fountain` 的装配 |
|
| `fountain.py` | bpy | 单一 OSM 点要素 `amenity=fountain` 的装配 |
|
||||||
|
|
||||||
### `geom.py` 的两条隐含约定
|
### `geom.py` 的两条隐含约定
|
||||||
@@ -95,6 +97,20 @@ def assemble(name, x, y, collection, materials):
|
|||||||
创建三张 fountain 材质,并维护 `counts["fountain_count"]`。模块只负责在已投影
|
创建三张 fountain 材质,并维护 `counts["fountain_count"]`。模块只负责在已投影
|
||||||
坐标上创建 basin / water / pedestal / spray objects,保持对象名和自定义属性不变。
|
坐标上创建 basin / water / pedestal / spray objects,保持对象名和自定义属性不变。
|
||||||
|
|
||||||
|
`building.py` 是面要素模块,但不裁剪 ring;入口在调用前已经过滤
|
||||||
|
`len(ring) >= 3`,并且建筑要保持原始 OSM footprint 参与相机取景:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def assemble(ring, way_id, tag, office_overrides, collection, materials):
|
||||||
|
...
|
||||||
|
return added, industrial_added, ring_pts
|
||||||
|
```
|
||||||
|
|
||||||
|
`generate_scene.py` 仍负责解析 `--office-overrides`、创建六张 building 材质、
|
||||||
|
分发 `building=*` way、累加 `counts["building_count"]` /
|
||||||
|
`counts["industrial_count"]`,以及写 `scene["office_override_way_ids"]`。
|
||||||
|
模块只负责 prism / roof / windows / bevel / building custom properties。
|
||||||
|
|
||||||
### 加一种新 OSM 要素
|
### 加一种新 OSM 要素
|
||||||
|
|
||||||
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
||||||
@@ -110,7 +126,7 @@ def assemble(name, x, y, collection, materials):
|
|||||||
|
|
||||||
## 两个入口脚本
|
## 两个入口脚本
|
||||||
|
|
||||||
| | `generate_scene.py` (999行) | `export_cesium.py` (624行) |
|
| | `generate_scene.py` (899行) | `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.py` 已拆出,但**没有 `features/` 注册表**,`building` / `roads` 仍在 `generate_scene.py` 里 |
|
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree/fountain/building.py` 已拆出,但**没有 `features/` 注册表**,`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` 回退 |
|
||||||
|
|
||||||
### 已知缺陷(记录在案,本轮不修)
|
### 已知缺陷(记录在案,本轮不修)
|
||||||
|
|||||||
3
.trellis/tasks/08-03-extract-building-module/check.jsonl
Normal file
3
.trellis/tasks/08-03-extract-building-module/check.jsonl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".trellis/spec/blender/testing.md", "reason": "Defines pure test command and bpy-layer parity expectations for validation."}
|
||||||
|
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Defines compare contract and expected no-diff result for refactors."}
|
||||||
|
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Defines dependency direction and safe import boundaries to review after new module creation."}
|
||||||
68
.trellis/tasks/08-03-extract-building-module/design.md
Normal file
68
.trellis/tasks/08-03-extract-building-module/design.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Design
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
`building.py` will join the bpy-layer feature modules under
|
||||||
|
`blender/osmassets/`. It may import `MeshBatch`, `add_roof`,
|
||||||
|
`add_wall_panel`, and `make_prism` from `osmassets.mesh`, plus
|
||||||
|
`parse_height` from `osmassets.osm`.
|
||||||
|
|
||||||
|
The module will expose:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def assemble(ring, way_id, tag, office_overrides, collection, materials):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The function returns:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(added, industrial_added, ring_pts)
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps `generate_scene.py`'s count and focus-point handling unchanged while
|
||||||
|
removing building geometry construction from the entry script.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- `generate_scene.py` keeps CLI parsing, OSM way dispatch, material creation,
|
||||||
|
count accumulation, focus-point accumulation, scene custom properties, and
|
||||||
|
stdout markers.
|
||||||
|
- `building.py` owns only building object creation and building-specific facade
|
||||||
|
details for a ring that has already been projected.
|
||||||
|
- `building.py` is a bpy-layer module. It must not be imported by pure Python
|
||||||
|
modules or pure unit tests.
|
||||||
|
- `catalog.py` remains the material declaration source; no material entries or
|
||||||
|
order change in this task.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. `generate_scene.py` parses `args["office_overrides"]` into a set.
|
||||||
|
2. `generate_scene.py` creates `building_mats` from `catalog.MATERIALS`.
|
||||||
|
3. OSM ways are projected to local-meter rings in `generate_scene.py`.
|
||||||
|
4. For ways with a `building` tag and at least three ring points,
|
||||||
|
`building.assemble(ring, str(way["id"]), tag, args["office_overrides"],
|
||||||
|
buildings_c, building_mats)` creates the prism, roof, windows, custom
|
||||||
|
properties, and bevel.
|
||||||
|
5. `generate_scene.py` updates `counts["building_count"]`,
|
||||||
|
`counts["industrial_count"]`, and `focus_points` from the return tuple.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The move must preserve object names, mesh names, material assignments, bevel
|
||||||
|
modifier fields, custom properties, window-band geometry, and return values.
|
||||||
|
Cesium export consumes the resulting `.blend`, so parity should see no
|
||||||
|
non-ignored contract difference.
|
||||||
|
|
||||||
|
## Trade-Offs
|
||||||
|
|
||||||
|
The `office_overrides` set is passed into `building.assemble()` rather than
|
||||||
|
passing the whole `args` dict. This narrows the module contract while preserving
|
||||||
|
behavior. A full feature registry remains deferred until building and roads are
|
||||||
|
both isolated enough to compare safely.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Rollback is mechanical: move `assemble()` and its helper back into
|
||||||
|
`generate_scene.py`, restore direct calls, remove the `building.py` import, and
|
||||||
|
delete the module.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".trellis/spec/blender/module-structure.md", "reason": "Defines bpy-layer module boundaries and feature module responsibilities for moving building assembly."}
|
||||||
|
{"file": ".trellis/spec/blender/asset-generation.md", "reason": "Defines MeshBatch, material order, deterministic generation, and Cesium material constraints relevant to building assembly."}
|
||||||
|
{"file": ".trellis/spec/guides/artifact-parity-guide.md", "reason": "Defines mandatory before/after parity validation for pure Blender scene refactors."}
|
||||||
47
.trellis/tasks/08-03-extract-building-module/implement.md
Normal file
47
.trellis/tasks/08-03-extract-building-module/implement.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
1. Capture a parity baseline before product-code edits.
|
||||||
|
2. Add `blender/osmassets/building.py` with the current building assembly and
|
||||||
|
detail helper logic.
|
||||||
|
3. Import `building.py` in `blender/generate_scene.py`.
|
||||||
|
4. Remove inline `_assemble_building()` and `add_building_details()` from
|
||||||
|
`generate_scene.py`.
|
||||||
|
5. Replace the building call site with `building.assemble(...)`, passing
|
||||||
|
`args["office_overrides"]` instead of the full `args` dict.
|
||||||
|
6. Run Python validation:
|
||||||
|
- `python3 -m py_compile blender/osmassets/building.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` and `docs/changelog.md` only for durable status or
|
||||||
|
convention changes discovered during implementation.
|
||||||
|
10. Commit, archive the task, and record the journal entry.
|
||||||
|
|
||||||
|
## Validation Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/parity.js capture building-module-before --stages blender,cesium
|
||||||
|
python3 -m py_compile blender/osmassets/building.py
|
||||||
|
python3 -m py_compile blender/generate_scene.py
|
||||||
|
python3 -m unittest blender/tests/test_pure.py
|
||||||
|
node scripts/parity.js capture building-module-after --stages blender,cesium
|
||||||
|
node scripts/parity.js compare building-module-before building-module-after
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risk Points
|
||||||
|
|
||||||
|
- Passing `office_overrides` instead of `args` changes the function contract;
|
||||||
|
verify `office_override` custom properties and industrial classification via
|
||||||
|
parity.
|
||||||
|
- `Building_<way_id>_Windows` mesh construction depends on `MeshBatch` object
|
||||||
|
naming and `add_wall_panel()` geometry; copy it mechanically.
|
||||||
|
- `make_prism()` can return `None` on degenerate input, but the current call
|
||||||
|
site filters `len(ring) >= 3`. Preserve the existing behavior and return
|
||||||
|
tuple shape.
|
||||||
|
- Do not reorder building material creation or catalog entries; material order
|
||||||
|
affects GLB material indices.
|
||||||
|
- Parity is mandatory because this is a pure refactor of bpy-layer scene
|
||||||
|
generation.
|
||||||
93
.trellis/tasks/08-03-extract-building-module/prd.md
Normal file
93
.trellis/tasks/08-03-extract-building-module/prd.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Extract building module
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move building assembly out of `blender/generate_scene.py` into a dedicated
|
||||||
|
bpy-layer module, keeping generated `.blend` / `.glb` / metadata structure
|
||||||
|
unchanged. This continues the documented P2 feature-module direction after
|
||||||
|
`fountain.py`.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
- `.trellis/spec/guides/artifact-parity-guide.md` records P2 as partial:
|
||||||
|
`water.py`, `grass.py`, `scrub.py`, `tree.py`, and `fountain.py` are already
|
||||||
|
split out, while `building` and `roads` still live in `generate_scene.py`.
|
||||||
|
- `blender/generate_scene.py:72` defines `_assemble_building()`.
|
||||||
|
- `blender/generate_scene.py:179` defines `add_building_details()`, which
|
||||||
|
creates office / industrial window bands with `MeshBatch` and
|
||||||
|
`add_wall_panel()`.
|
||||||
|
- `blender/generate_scene.py:789` dispatches OSM ways with a `building` tag,
|
||||||
|
increments `counts["building_count"]` and `counts["industrial_count"]`, and
|
||||||
|
extends `focus_points` with the returned ring.
|
||||||
|
- `blender/generate_scene.py:710` creates `building_mats` from the existing
|
||||||
|
`catalog.MATERIALS["building_" + key]` entries.
|
||||||
|
- `blender/generate_scene.py:910` persists `office_override_way_ids` as a scene
|
||||||
|
custom property from `args["office_overrides"]`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. Add a new bpy-layer module at `blender/osmassets/building.py`.
|
||||||
|
2. Move the current building assembly behavior into the new module:
|
||||||
|
- `_assemble_building()`
|
||||||
|
- `add_building_details()`
|
||||||
|
3. Expose an `assemble(...)` function from the new module that returns the same
|
||||||
|
tuple shape currently returned by `_assemble_building()`:
|
||||||
|
`(added, industrial_added, ring_pts)`.
|
||||||
|
4. Keep `generate_scene.py` responsible for:
|
||||||
|
- parsing `--office-overrides`;
|
||||||
|
- creating `building_mats`;
|
||||||
|
- filtering OSM ways with a `building` tag and `len(ring) >= 3`;
|
||||||
|
- updating `building_count`, `industrial_count`, and `focus_points`;
|
||||||
|
- writing `scene["office_override_way_ids"]`;
|
||||||
|
- writing the existing `SCENE_DONE` JSON shape.
|
||||||
|
5. Preserve existing building object names and mesh names:
|
||||||
|
- `Building_<way_id>`
|
||||||
|
- `Building_<way_id>_Roof`
|
||||||
|
- `Building_<way_id>_Windows`
|
||||||
|
6. Preserve existing building custom properties:
|
||||||
|
- `osm_height`
|
||||||
|
- `render_height`
|
||||||
|
- `building_kind`
|
||||||
|
- `osm_building_tag`
|
||||||
|
- `office_override`
|
||||||
|
7. Preserve current height rules:
|
||||||
|
- industrial buildings use the parsed source height;
|
||||||
|
- non-industrial buildings under 30m render at 11.4m;
|
||||||
|
- source height has a 3.0m floor and `parse_height(tag, 12.0)` default.
|
||||||
|
8. Preserve current facade behavior:
|
||||||
|
- industrial buildings use one high horizontal band;
|
||||||
|
- office buildings use floor-based window bands;
|
||||||
|
- bevel modifier name, width, and segment count stay unchanged.
|
||||||
|
9. Do not introduce a full `features/` registry in this task.
|
||||||
|
10. Do not modify roads, material definitions, `MATERIALS` order,
|
||||||
|
`ROAD_LAYERS` order, stdout markers, parity ignore lists, or known
|
||||||
|
unrelated defects D1-D3.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `blender/osmassets/building.py` contains building assembly behavior and
|
||||||
|
imports only bpy-layer-safe dependencies.
|
||||||
|
- [x] `blender/generate_scene.py` imports and calls the new building module; no
|
||||||
|
inline `_assemble_building()` or `add_building_details()` remains there.
|
||||||
|
- [x] Building counts, industrial counts, focus point behavior, scene custom
|
||||||
|
properties, and `SCENE_DONE` JSON keys remain unchanged.
|
||||||
|
- [x] `python3 -m py_compile blender/osmassets/building.py` passes.
|
||||||
|
- [x] `python3 -m py_compile blender/generate_scene.py` passes.
|
||||||
|
- [x] `python3 -m unittest blender/tests/test_pure.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 implementation.
|
||||||
|
- Road extraction.
|
||||||
|
- Building visual redesign.
|
||||||
|
- Building material or Cesium export contract changes.
|
||||||
|
- Moving CLI parsing or `office_override_way_ids` scene metadata out of
|
||||||
|
`generate_scene.py`.
|
||||||
|
- Changing geometry helper APIs in `osmassets.mesh`.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
None.
|
||||||
26
.trellis/tasks/08-03-extract-building-module/task.json
Normal file
26
.trellis/tasks/08-03-extract-building-module/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "extract-building-module",
|
||||||
|
"name": "extract-building-module",
|
||||||
|
"title": "Extract building module",
|
||||||
|
"description": "Move building assembly out of blender/generate_scene.py into a dedicated bpy-layer module without changing generated artifacts.",
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
@@ -52,12 +52,10 @@ from osmassets.materials import ( # noqa: E402
|
|||||||
from osmassets.mesh import ( # noqa: E402
|
from osmassets.mesh import ( # noqa: E402
|
||||||
MeshBatch,
|
MeshBatch,
|
||||||
add_polyline,
|
add_polyline,
|
||||||
add_roof,
|
|
||||||
add_wall_panel,
|
|
||||||
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 building as _building # noqa: E402
|
||||||
from osmassets import fountain as _fountain # 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
|
||||||
@@ -65,35 +63,6 @@ from osmassets import scrub as _scrub # noqa: E402
|
|||||||
from osmassets import tree as _tree # noqa: E402
|
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 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"])
|
|
||||||
source_height = max(3.0, parse_height(tag, 12.0))
|
|
||||||
height = source_height if industrial or source_height >= 30.0 else 11.4
|
|
||||||
material = building_mats["industrial"] if industrial else building_mats["default"]
|
|
||||||
building_name = "Building_" + way_id
|
|
||||||
building_obj = make_prism(building_name, ring, 0.08, height,
|
|
||||||
material, buildings_c)
|
|
||||||
if building_obj:
|
|
||||||
building_obj["osm_height"] = source_height
|
|
||||||
building_obj["render_height"] = height
|
|
||||||
building_obj["building_kind"] = "industrial" if industrial else "office"
|
|
||||||
building_obj["osm_building_tag"] = tag.get("building", "")
|
|
||||||
building_obj["office_override"] = way_id in args["office_overrides"]
|
|
||||||
bevel = building_obj.modifiers.new("Soft facade edges", "BEVEL")
|
|
||||||
bevel.width = 0.16
|
|
||||||
bevel.segments = 2
|
|
||||||
roof_mat = (building_mats["industrial_roof"] if industrial
|
|
||||||
else building_mats["office_roof"])
|
|
||||||
add_roof(building_name, ring, height + 0.095, roof_mat, buildings_c)
|
|
||||||
add_building_details(building_name, ring, height, industrial,
|
|
||||||
building_mats, buildings_c)
|
|
||||||
return 1, int(industrial), ring
|
|
||||||
|
|
||||||
MODEL_ROOT = os.path.abspath(os.path.join(
|
MODEL_ROOT = os.path.abspath(os.path.join(
|
||||||
os.path.dirname(__file__), "..", "assets", "models", "polyhaven"
|
os.path.dirname(__file__), "..", "assets", "models", "polyhaven"
|
||||||
))
|
))
|
||||||
@@ -176,34 +145,6 @@ def cli_args():
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def add_building_details(name, ring, height, industrial, materials, collection):
|
|
||||||
footprint = ring[:-1] if len(ring) > 1 and ring[0] == ring[-1] else ring
|
|
||||||
if len(footprint) < 3:
|
|
||||||
return
|
|
||||||
glass_mat = materials["factory_glass"] if industrial else materials["glass"]
|
|
||||||
|
|
||||||
glass_batch = MeshBatch(name + "_Windows", collection, glass_mat)
|
|
||||||
edges = list(zip(footprint, footprint[1:] + footprint[:1]))
|
|
||||||
if industrial:
|
|
||||||
band_height = min(1.8, max(0.75, height * 0.16))
|
|
||||||
band_base = max(0.9, height * 0.52)
|
|
||||||
for start, end in edges:
|
|
||||||
add_wall_panel(glass_batch, start, end, band_base, band_height,
|
|
||||||
thickness=0.055, inset=0.12)
|
|
||||||
else:
|
|
||||||
floor_height = 3.25
|
|
||||||
floor_count = max(1, int((height - 0.7) / floor_height))
|
|
||||||
for floor in range(floor_count):
|
|
||||||
band_base = 0.55 + floor * floor_height + 0.95
|
|
||||||
if band_base + 1.25 > height - 0.18:
|
|
||||||
break
|
|
||||||
for start, end in edges:
|
|
||||||
add_wall_panel(glass_batch, start, end, band_base, 1.25,
|
|
||||||
thickness=0.045, inset=0.10)
|
|
||||||
glass_batch.finish()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_geojson_layer(path, layer, projector, collection, material, z):
|
def add_geojson_layer(path, layer, projector, collection, material, z):
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return 0
|
return 0
|
||||||
@@ -787,8 +728,9 @@ def build(args):
|
|||||||
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:
|
elif "building" in tag and len(ring) >= 3:
|
||||||
added, ind_added, ring_pts = _assemble_building(
|
added, ind_added, ring_pts = _building.assemble(
|
||||||
ring, str(way["id"]), tag, args, buildings_c, building_mats)
|
ring, str(way["id"]), tag, args["office_overrides"],
|
||||||
|
buildings_c, building_mats)
|
||||||
counts["building_count"] += added
|
counts["building_count"] += added
|
||||||
counts["industrial_count"] += ind_added
|
counts["industrial_count"] += ind_added
|
||||||
if ring_pts:
|
if ring_pts:
|
||||||
|
|||||||
56
blender/osmassets/building.py
Normal file
56
blender/osmassets/building.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""Building feature assembly (`building=*`)."""
|
||||||
|
|
||||||
|
from osmassets.mesh import MeshBatch, add_roof, add_wall_panel, make_prism
|
||||||
|
from osmassets.osm import parse_height
|
||||||
|
|
||||||
|
|
||||||
|
def add_details(name, ring, height, industrial, materials, collection):
|
||||||
|
footprint = ring[:-1] if len(ring) > 1 and ring[0] == ring[-1] else ring
|
||||||
|
if len(footprint) < 3:
|
||||||
|
return
|
||||||
|
glass_mat = materials["factory_glass"] if industrial else materials["glass"]
|
||||||
|
|
||||||
|
glass_batch = MeshBatch(name + "_Windows", collection, glass_mat)
|
||||||
|
edges = list(zip(footprint, footprint[1:] + footprint[:1]))
|
||||||
|
if industrial:
|
||||||
|
band_height = min(1.8, max(0.75, height * 0.16))
|
||||||
|
band_base = max(0.9, height * 0.52)
|
||||||
|
for start, end in edges:
|
||||||
|
add_wall_panel(glass_batch, start, end, band_base, band_height,
|
||||||
|
thickness=0.055, inset=0.12)
|
||||||
|
else:
|
||||||
|
floor_height = 3.25
|
||||||
|
floor_count = max(1, int((height - 0.7) / floor_height))
|
||||||
|
for floor in range(floor_count):
|
||||||
|
band_base = 0.55 + floor * floor_height + 0.95
|
||||||
|
if band_base + 1.25 > height - 0.18:
|
||||||
|
break
|
||||||
|
for start, end in edges:
|
||||||
|
add_wall_panel(glass_batch, start, end, band_base, 1.25,
|
||||||
|
thickness=0.045, inset=0.10)
|
||||||
|
glass_batch.finish()
|
||||||
|
|
||||||
|
|
||||||
|
def assemble(ring, way_id, tag, office_overrides, collection, materials):
|
||||||
|
industrial = (tag.get("building") == "industrial" and
|
||||||
|
way_id not in office_overrides)
|
||||||
|
source_height = max(3.0, parse_height(tag, 12.0))
|
||||||
|
height = source_height if industrial or source_height >= 30.0 else 11.4
|
||||||
|
material = materials["industrial"] if industrial else materials["default"]
|
||||||
|
building_name = "Building_" + way_id
|
||||||
|
building_obj = make_prism(building_name, ring, 0.08, height,
|
||||||
|
material, collection)
|
||||||
|
if building_obj:
|
||||||
|
building_obj["osm_height"] = source_height
|
||||||
|
building_obj["render_height"] = height
|
||||||
|
building_obj["building_kind"] = "industrial" if industrial else "office"
|
||||||
|
building_obj["osm_building_tag"] = tag.get("building", "")
|
||||||
|
building_obj["office_override"] = way_id in office_overrides
|
||||||
|
bevel = building_obj.modifiers.new("Soft facade edges", "BEVEL")
|
||||||
|
bevel.width = 0.16
|
||||||
|
bevel.segments = 2
|
||||||
|
roof_mat = (materials["industrial_roof"] if industrial
|
||||||
|
else materials["office_roof"])
|
||||||
|
add_roof(building_name, ring, height + 0.095, roof_mat, collection)
|
||||||
|
add_details(building_name, ring, height, industrial, materials, collection)
|
||||||
|
return 1, int(industrial), ring
|
||||||
@@ -18,6 +18,10 @@
|
|||||||
保留点要素过滤、投影、材质创建和计数。Blender/Cesium parity before/after 在
|
保留点要素过滤、投影、材质创建和计数。Blender/Cesium parity before/after 在
|
||||||
`nantaizi-lake-innovation-valley` 与 `hanyang-block` 两个样本上均 identical;P2
|
`nantaizi-lake-innovation-valley` 与 `hanyang-block` 两个样本上均 identical;P2
|
||||||
剩余未拆出部分更新为 `building` / `roads`。
|
剩余未拆出部分更新为 `building` / `roads`。
|
||||||
|
- 抽出 building 要素装配:新增 `blender/osmassets/building.py`,迁移 building prism、
|
||||||
|
roof、window bands、bevel 和 building custom properties;`generate_scene.py` 继续
|
||||||
|
负责 `office_overrides`、材质创建、way 分发、计数和 scene metadata。Blender/Cesium
|
||||||
|
parity before/after 在两个样本上均 identical;P2 剩余未拆出部分更新为 `roads`。
|
||||||
|
|
||||||
## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮
|
## 2026-07-31(三)远看发黑的真正原因:反照率没被提亮
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user