|
| 1 | +<!-- This file was generated with the assistance of an AI coding tool. --> |
| 2 | + |
| 3 | +# Terrain contours — dev note |
| 4 | + |
| 5 | +> Living design note for the `terrain-contours` branch, checked out in `C:\IfcOpenShell` |
| 6 | +> (Blender 5.1's linked tree), based on `v0.9.0`. |
| 7 | +> Status: **v1 tested in Blender 5.1; PR open against `v0.9.0`.** |
| 8 | +
|
| 9 | +## Implementation status (v1) |
| 10 | + |
| 11 | +| Piece | Where | Verified | |
| 12 | +|---|---|---| |
| 13 | +| Slice + chain (numpy, pure) | `tool/terrain.py` `slice_triangles`, `chain_segments` | Standalone script: cone → one closed loop at the right radius; slope → one ordered open line; level exactly on vertices → no zero-length steps; two hills → two loops; 80k tris × 48 levels ≈ 1.3 s | |
| 14 | +| Top-surface filter (BVH ray up) | `tool/terrain.py` `get_top_surface_triangles` | Blender 5.1, sculpted test terrain (see fix below) | |
| 15 | +| Core flow | `core/terrain.py` | `test/core/test_terrain.py`, 5 pass under plain pytest | |
| 16 | +| IFC writing (annotation, placement, polylines, psets, assign_product, container) | `tool/terrain.py` `create_contour` | Blender 5.1, IFC4 / feet test project | |
| 17 | +| Panel + 3 operators | `bim/module/terrain/` — Object properties → Parametric Geometry → Contours | not yet | |
| 18 | +| Drawings include contours | `tool/drawing.py` `get_drawing_elements` (2-line change) | Blender 5.1 plan drawing: each contour drawn once, `IndexContour` styled | |
| 19 | +| Geographic Element Tool header button | — | deferred — convenience only, add if the panel proves too far away | |
| 20 | + |
| 21 | +Decisions made while coding: |
| 22 | + |
| 23 | +- Terrain = active mesh object whose element is an `IfcGeographicElement` **or `IfcSite`** |
| 24 | + (many real files keep the terrain as the site's Body). |
| 25 | +- `root.create_entity(predefined_type="CONTOURLINE")` already does the schema switch: |
| 26 | + IFC4X3 → `PredefinedType`, IFC4 → `ObjectType = "CONTOURLINE"`. Kept upper-case in IFC4 |
| 27 | + (Bonsai convention; `get_predefined_type` reads both). `Pset_AnnotationContourLine` is |
| 28 | + attached in both schemas. |
| 29 | +- One annotation per elevation, `Model/Annotation/MODEL_VIEW` context (created if |
| 30 | + missing), placement at (terrain x, terrain y, contour z), polylines are |
| 31 | + `IfcIndexedPolyCurve`s from `ShapeBuilder.polyline`. |
| 32 | +- Index contours get `EPset_Annotation.Classes = "IndexContour"` → SVG class. |
| 33 | +- Levels are multiples of the interval in **project** Z (Blender Z + georeference |
| 34 | + `blender_offset_z`), so 2 ft contours land on true 2 ft elevations under a false origin. |
| 35 | +- Slicing runs before anything is deleted, so a too-small interval (> 2000 levels) |
| 36 | + errors without touching existing contours. |
| 37 | +- Contours are contained in the terrain's container (or the site itself) explicitly, |
| 38 | + because `assign_class` skips containment for any annotation while a drawing camera |
| 39 | + is active. |
| 40 | + |
| 41 | +### Fixed: broken / intermittent contours on sculpted terrain (2026-10-07) |
| 42 | + |
| 43 | +Test file: `D:\Dropbox\Gitea_OD\Community_Troubleshooting\Terrain-Contours` (box with a |
| 44 | +sculpted top, 1,090 quads). The first version ran the "visible from above" ray test per |
| 45 | +**polygon**, from the average of its vertices. On a warped quad that point can sit below the |
| 46 | +quad's own surface, so the ray hit the quad itself: 370 of 371 rejected faces were |
| 47 | +self-hits, which left holes and split each level into 4–21 fragments. Now the test runs per |
| 48 | +**loop triangle** (a triangle's centroid lies on it) and steps past hits on the triangle's |
| 49 | +own polygon. Result on the test file: 1–2 polylines per level; the only excluded triangles |
| 50 | +are the 4 side faces and the bottom; every open end lies on the terrain boundary. |
| 51 | + |
| 52 | +### Drawing path — checked |
| 53 | + |
| 54 | +`get_drawing_elements` dropped every annotation not in the drawing's group; contours are |
| 55 | +now let through. They may then be drawn **twice**: by the C++ SVG serializer (linework, |
| 56 | +annotation contexts — but it only emits annotations within the storey Z range in floor |
| 57 | +plans) and by `svgwriter.draw_misc_annotation` (annotation layer, which carries the |
| 58 | +`IndexContour` class). Checked in a real plan drawing: each contour appears once and |
| 59 | +`.PredefinedType-CONTOURLINE.IndexContour` styles the index lines. |
| 60 | + |
| 61 | +## Problem |
| 62 | + |
| 63 | +Drafting a site survey in Bonsai needs topographic contour lines generated from a terrain |
| 64 | +mesh: every *N* units (2 ft, 5 ft, 1 m …), with a heavier **index contour** every *k*th |
| 65 | +line, regenerated when the terrain changes, and appearing in plan drawings and sheets. |
| 66 | + |
| 67 | +There is nothing in Bonsai today that does this. Third-party Blender options exist |
| 68 | +(Parametric Shapes' *Contour Generator*, a Gumroad geometry-nodes tree) but they produce |
| 69 | +plain Blender geometry, which Bonsai drawings do not render — drawings only see IFC |
| 70 | +elements. |
| 71 | + |
| 72 | +## IFC representation |
| 73 | + |
| 74 | +IFC 4.3 has a dedicated class for this: |
| 75 | + |
| 76 | +- **`IfcAnnotation`, `PredefinedType = CONTOURLINE`** — "annotation used to illustrate |
| 77 | + lines connecting points of equal elevation". IFC4 has no predefined type on |
| 78 | + `IfcAnnotation`; there the type goes in `ObjectType` (see the schema table below). |
| 79 | +- **`Pset_AnnotationContourLine.ContourValue`** (`IfcLengthMeasure`) — the elevation of |
| 80 | + the line. |
| 81 | + |
| 82 | +### IFC4 vs IFC4X3 differences (checked against `IFC4.exp` and the repo's pset templates) |
| 83 | + |
| 84 | +| Concept | IFC4 ADD2 TC1 | IFC4X3 | |
| 85 | +|---|---|---| |
| 86 | +| `IfcAnnotation.PredefinedType` | **does not exist** (entity has no attributes beyond `IfcProduct`) | `CONTOURLINE` | |
| 87 | +| `Pset_AnnotationContourLine` | exists, applicable to `IfcAnnotation/ContourLine` (matched on `ObjectType`) | exists, applicable to `IfcAnnotation/CONTOURLINE` | |
| 88 | +| `IfcGeographicElementTypeEnum.TERRAIN` | exists | exists | |
| 89 | +| `IfcEarthworksCut` / `IfcEarthworksFill` | **do not exist** | exist | |
| 90 | + |
| 91 | +Consequences: in IFC4 set `ObjectType = "ContourLine"` (the spelling the IFC4 pset |
| 92 | +applicability uses — Bonsai's own annotation ObjectTypes are upper-case, so check |
| 93 | +whether pset-template filtering is case-sensitive before choosing); in IFC4X3 set |
| 94 | +`PredefinedType = CONTOURLINE`. Write a small schema switch, as |
| 95 | +`bim/module/model/product.py` already does for vegetation (IFC4 `USERDEFINED`/ |
| 96 | +`VEGETATION` vs IFC4X3 `VEGETATION`). |
| 97 | + |
| 98 | +Decision: each contour elevation is one **model-space** `IfcAnnotation.CONTOURLINE` |
| 99 | +(not assigned to a drawing group) whose body is 3D polylines at the true elevation. This |
| 100 | +means: |
| 101 | + |
| 102 | +- other IFC tools see real contour objects, not just a Bonsai drawing artefact; |
| 103 | +- they read correctly in 3D and in plan; |
| 104 | +- index vs. intermediate contours can be styled independently in drawing CSS (class or |
| 105 | + ObjectType distinction — to settle during implementation). |
| 106 | + |
| 107 | +Generation parameters live on the **terrain element** (`IfcGeographicElement`, TERRAIN) |
| 108 | +in a `BBIM_Contours` pset, following the `BBIM_Roof` / `BBIM_Stair` parametric pattern: |
| 109 | + |
| 110 | +| Property | Meaning | |
| 111 | +|---|---| |
| 112 | +| `Interval` | contour spacing, project length units | |
| 113 | +| `IndexInterval` | every *k*th contour is an index contour (default 5) | |
| 114 | +| `MinElevation` / `MaxElevation` | optional clamp; default = terrain Z range | |
| 115 | + |
| 116 | +The generated annotations need a link back to their terrain so Update can find and |
| 117 | +replace them. Candidate: `IfcRelAssignsToProduct` (annotation assigned to the terrain). |
| 118 | +To verify against how Bonsai already relates generated annotations before choosing. |
| 119 | + |
| 120 | +Ruled out: adding the contours as a second Plan/Annotation representation on the terrain |
| 121 | +itself. Simpler, but invisible to other tools as objects and awkward to style index |
| 122 | +contours differently. |
| 123 | + |
| 124 | +## UI placement |
| 125 | + |
| 126 | +1. **Object Properties → "Contours" panel**, polled on a selected terrain element: shows |
| 127 | + the `BBIM_Contours` values, **Generate / Update** and **Remove**. This is where the |
| 128 | + interval is changed later. |
| 129 | +2. **Geographic Element Tool header** (`bim.geographic_element_tool`, |
| 130 | + `bim/module/model/workspace.py`) — when a terrain is selected, show the same |
| 131 | + Update button, mirroring how the tool shows wall-specific actions when a wall is |
| 132 | + selected. |
| 133 | +3. **Drawings: no new machinery** if contours are annotations — plan drawings include |
| 134 | + them, CSS styles them. Elevation labels on index contours are phase 2 (decorator or |
| 135 | + generated TEXT annotations; label placement/collision is the hard part). |
| 136 | + |
| 137 | +Logic goes in `core/` + `tool/` (Bonsai's usual split) so it does not depend on which |
| 138 | +panel calls it. |
| 139 | + |
| 140 | +### Relation to PR #9304 (Saikei Civil CIVIL tab) |
| 141 | + |
| 142 | +PR #9304 adds a **CIVIL** Properties tab for alignment authoring. That tab would be the |
| 143 | +natural long-term home for site/terrain tools. Decision: **do not stack on it** — no code |
| 144 | +is shared (alignment curve math is unrelated to mesh slicing), its merge path is |
| 145 | +uncertain (reviewer is building an alternative alignment UI), and stacking would block a |
| 146 | +small PR on a large one. If the CIVIL tab lands, moving the Contours panel there is a |
| 147 | +`bl_context` / `bl_parent_id` change. |
| 148 | + |
| 149 | +## Updating |
| 150 | + |
| 151 | +- **v1:** explicit **Update Contours** — delete the annotations previously generated for |
| 152 | + this terrain, regenerate from the current mesh and pset. |
| 153 | +- **Later, opt-in:** regenerate when the terrain representation is saved on leaving Edit |
| 154 | + Mode. Opt-in because terrain meshes are heavy. |
| 155 | + |
| 156 | +## Algorithm sketch |
| 157 | + |
| 158 | +For each elevation `z` in `ceil(zmin/I)*I … zmax` step `I`: |
| 159 | + |
| 160 | +1. `bmesh.ops.bisect_plane` on a **copy** of the terrain mesh with a horizontal plane at |
| 161 | + `z` (transformed into the object's local space — see below), keeping only the cut |
| 162 | + edges (`geom_cut`). |
| 163 | +2. Restrict to edges on the **top surface** (see "volume terrain" below). |
| 164 | +3. Chain the cut edges into polylines (open where they hit the mesh border, closed |
| 165 | + loops otherwise) and write them as `IfcIndexedPolyCurve` / polyline items. |
| 166 | + |
| 167 | +Alternative to evaluate: Geometry Nodes Mesh Boolean "Intersecting Edges" against a |
| 168 | +stack of planes. Rejected for v1 — harder to drive from an IFC operator and to chain into |
| 169 | +ordered polylines. |
| 170 | + |
| 171 | +## Related local add-ons (same "site tools" family) |
| 172 | + |
| 173 | +Ryan has two standalone add-ons that may enter the ecosystem later. Both work on terrain |
| 174 | +and share hard-won facts this feature needs: |
| 175 | + |
| 176 | +**Mesh Outline Projector** |
| 177 | +(`D:\Dropbox\Gitea_OD\Utility_Apps\Blender\addons\Mesh Outline Projector`) — projects |
| 178 | +the top-down outline of objects (e.g. building footprints) onto a terrain's top surface, |
| 179 | +cuts it with vertical bisect planes and paints the covered faces with the source |
| 180 | +material. Directly reusable lessons: |
| 181 | + |
| 182 | +- **`bmesh` / `BVHTree` work in local space**; world-space planes must be transformed by |
| 183 | + `matrix_world.inverted()` (plane from three transformed points, not a transformed |
| 184 | + normal). |
| 185 | +- **`bisect_plane` leaves no face straddling the cut** — the same guarantee makes |
| 186 | + contour edges clean. |
| 187 | +- **"Top surface" cannot be found from normals** — IFC/imported meshes often have flipped |
| 188 | + winding. Its `make_top_face_predicate` (roughly-horizontal `|n.z|` **and** an upward |
| 189 | + ray-cast against a BVH snapshot hits nothing) is exactly what contours need for |
| 190 | + **volume terrain**: slicing a solid terrain horizontally otherwise also yields loops |
| 191 | + around the sides and bottom. Candidate to promote into a shared `tool.Terrain` helper. |
| 192 | + |
| 193 | +**Excavate** (`D:\Dropbox\Gitea_OD\Utility_Apps\Blender\addons\Excavate`) — turns |
| 194 | +selected terrain faces into a flat-bottomed pit (vertical walls, flat floor). In IFC |
| 195 | +terms this maps naturally to **`IfcEarthworksCut`** (IFC 4.3 feature subtraction; |
| 196 | +`IfcEarthworksFill` is its counterpart), which would keep the terrain unmodified and the |
| 197 | +cut as a separate, quantifiable element. **Both are IFC4X3-only**; an IFC4 equivalent |
| 198 | +would need an `IfcOpeningElement` (or a direct mesh edit), so this one is schema-gated. |
| 199 | + |
| 200 | +Possible shape of a future Bonsai "Terrain" module: contours, outline projection |
| 201 | +(footprints / paving zones onto terrain), earthworks cut/fill. Contours come first |
| 202 | +because they are the smallest and need no change to the terrain geometry itself. |
| 203 | +Note: projected *materials* in Bonsai would be per-face IFC styles, a harder problem — |
| 204 | +out of scope here. |
| 205 | + |
| 206 | +## Open questions |
| 207 | + |
| 208 | +- Existing vs. proposed grade: two terrains with status EXISTING / NEW → dashed vs. solid |
| 209 | + contours. Inherit status onto the generated annotations? |
| 210 | +- Clip contours to the site boundary, or the full terrain extent? |
| 211 | +- Which relationship links annotations to their terrain (see above)? |
| 212 | +- IFC2X3 support — probably not; contours are an IFC4+ feature. |
| 213 | + |
| 214 | +## To test (once implemented) |
| 215 | + |
| 216 | +- [ ] Heightfield terrain, imperial (2 ft / index 10 ft) and metric (1 m / index 5 m). |
| 217 | +- [ ] Volume (solid) terrain with flipped normals — only top-surface contours. |
| 218 | +- [ ] Rotated / translated terrain object — elevations are world Z. |
| 219 | +- [ ] Update after editing the terrain — old annotations replaced, none orphaned. |
| 220 | +- [ ] Contours appear in a plan drawing with index/intermediate styling. |
| 221 | +- [ ] Interval change 2 ft → 5 ft regenerates correctly. |
0 commit comments