mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 19:54:07 +00:00
eea060b72d
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
845 lines
30 KiB
Markdown
845 lines
30 KiB
Markdown
# CLAUDE.md - Saikei (Civil Engineering) Module Context
|
||
|
||
## Project Identity
|
||
|
||
**Name:** Saikei (Civil Engineering module for Bonsai)
|
||
**Pronunciation:** "SIGH-kay" (盆景 - Japanese for "planted landscape")
|
||
**Tagline:** "The landscape around the buildings"
|
||
**Repository:** `IfcOpenShell/IfcOpenShell` (branch: `saikei`)
|
||
**Location in repo:** `src/bonsai/bonsai/` (integrated into Bonsai)
|
||
|
||
### Brand Philosophy
|
||
Saikei is the civil engineering complement to Bonsai in the open-source IFC ecosystem:
|
||
- **Bonsai** = Buildings (vertical construction)
|
||
- **Saikei** = Infrastructure (horizontal construction: roads, earthwork, drainage)
|
||
|
||
> "While Bonsai crafts the buildings, Saikei shapes the world around them."
|
||
|
||
---
|
||
|
||
## Current Status
|
||
|
||
**Saikei has been merged into Bonsai** as of January 2026. It is no longer a separate extension but is now part of the core Bonsai codebase, following Bonsai's architecture patterns.
|
||
|
||
### Code Locations
|
||
| Component | Path |
|
||
|-----------|------|
|
||
| Core logic (business rules) | `src/bonsai/bonsai/core/alignment.py` |
|
||
| Tool layer (implementations) | `src/bonsai/bonsai/tool/alignment.py` |
|
||
| UI module | `src/bonsai/bonsai/bim/module/alignment/` |
|
||
| GPU decorators | `src/bonsai/bonsai/bim/module/alignment/decorator.py` |
|
||
|
||
---
|
||
|
||
## Mission & Vision
|
||
|
||
### Mission
|
||
Democratize professional civil engineering tools by providing free, open-source alternatives to expensive commercial software like Civil 3D and OpenRoads.
|
||
|
||
### Target Users
|
||
- Small engineering firms seeking cost-effective tools
|
||
- Engineers in developing countries without software budgets
|
||
- Students and educators
|
||
- Land surveyors and GIS professionals
|
||
|
||
### Core Philosophy: Native IFC
|
||
**"We're not converting TO IFC. We ARE IFC."**
|
||
|
||
Unlike traditional CAD software that exports to IFC, Saikei works **IN** IFC format from the very first action. The IFC file is the single source of truth, and Blender is the visualization/interaction layer.
|
||
|
||
---
|
||
|
||
## Architecture (Bonsai Pattern)
|
||
|
||
Saikei follows Bonsai's three-layer architecture:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ BONSAI │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ bim/module/alignment/ (UI Layer) │ │
|
||
│ │ ├── ui.py Panels, UILists │ │
|
||
│ │ ├── operator.py Blender operators │ │
|
||
│ │ ├── prop.py PropertyGroups │ │
|
||
│ │ ├── data.py Cached data for UI │ │
|
||
│ │ └── decorator.py GPU drawing for visual feedback │ │
|
||
│ │ │ │
|
||
│ │ WHO: User interaction, visual feedback │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ tool/alignment.py (Tool Layer) │ │
|
||
│ │ │ │
|
||
│ │ HOW: ALL implementations including: │ │
|
||
│ │ • Math and calculations (geometry, vectors, stations) │ │
|
||
│ │ • Algorithms (curve fitting, segment generation) │ │
|
||
│ │ • IFC entity creation and manipulation │ │
|
||
│ │ • Blender object creation and management │ │
|
||
│ │ • Coordinate transformations │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ core/alignment.py (Core Layer) │ │
|
||
│ │ │ │
|
||
│ │ WHAT: Business logic and workflow orchestration ONLY │ │
|
||
│ │ • NO math, calculations, or algorithms │ │
|
||
│ │ • NO IFC entity creation (delegates to Tool) │ │
|
||
│ │ • NO bpy imports │ │
|
||
│ │ • Defines business rules and validation │ │
|
||
│ │ • Orchestrates workflow (calls Tool methods) │ │
|
||
│ │ • Testable outside Blender │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Layer Responsibilities
|
||
|
||
#### 1. Core Layer (`core/alignment.py`) — WHAT happens
|
||
|
||
**Purpose:** Business logic and workflow orchestration ONLY
|
||
|
||
**Contains:**
|
||
- Business rules ("an alignment must have at least 2 PIs")
|
||
- Validation logic ("radius must be positive")
|
||
- Workflow orchestration ("when PI moves, regenerate segments")
|
||
- Decision-making about what should happen
|
||
|
||
**Does NOT contain:**
|
||
- Math or calculations (no `T = R × tan(Δ/2)`)
|
||
- Algorithms (no curve fitting, no geometry)
|
||
- IFC entity creation (no `ifc.create_entity()`)
|
||
- Blender operations (no `bpy` imports)
|
||
|
||
**Example (CORRECT):**
|
||
```python
|
||
# core/alignment.py - Business logic ONLY
|
||
def add_curve_at_pi(ifc, alignment_tool, alignment_id, pi_index, radius):
|
||
"""Orchestrates adding a curve - delegates math to Tool."""
|
||
alignment = ifc.get_entity(alignment_id)
|
||
|
||
# Business rule validation
|
||
if not alignment_tool.can_add_curve_at_pi(alignment, pi_index):
|
||
raise ValueError("Cannot add curve at this PI")
|
||
|
||
if radius <= 0:
|
||
raise ValueError("Radius must be positive")
|
||
|
||
# Delegate ALL math and IFC creation to Tool layer
|
||
curve_data = alignment_tool.create_curve_at_pi(alignment, pi_index, radius)
|
||
|
||
# Business rule: trigger regeneration
|
||
alignment_tool.regenerate_segments(alignment)
|
||
|
||
return curve_data
|
||
```
|
||
|
||
#### 2. Tool Layer (`tool/alignment.py`) — HOW it happens
|
||
|
||
**Purpose:** All implementations - math, algorithms, IFC, Blender
|
||
|
||
**Contains:**
|
||
- Mathematical calculations (tangent length, arc length, deflection angles)
|
||
- Geometric algorithms (curve geometry, vector operations)
|
||
- IFC entity creation and manipulation
|
||
- Blender object creation and management
|
||
- Coordinate transformations
|
||
- Station calculations
|
||
|
||
**Example (CORRECT):**
|
||
```python
|
||
# tool/alignment.py - ALL implementations here
|
||
class Alignment:
|
||
@classmethod
|
||
def create_curve_at_pi(cls, alignment, pi_index, radius):
|
||
"""Tool layer - math and IFC creation happens here."""
|
||
pis = cls.get_pis(alignment)
|
||
p1, p2, p3 = pis[pi_index-1], pis[pi_index], pis[pi_index+1]
|
||
|
||
# Math calculations (THIS BELONGS IN TOOL)
|
||
t1 = (p2 - p1).normalized()
|
||
t2 = (p3 - p2).normalized()
|
||
deflection = math.acos(t1.dot(t2))
|
||
tangent_length = radius * math.tan(deflection / 2)
|
||
|
||
bc = p2 - t1 * tangent_length
|
||
ec = p2 + t2 * tangent_length
|
||
arc_length = radius * deflection
|
||
|
||
# IFC creation (THIS BELONGS IN TOOL)
|
||
ifc_file = tool.Ifc.get()
|
||
segment = ifc_file.create_entity("IfcAlignmentSegment", ...)
|
||
|
||
return {'bc': bc, 'ec': ec, 'arc_length': arc_length, 'segment': segment}
|
||
```
|
||
|
||
#### 3. UI Layer (`bim/module/alignment/`) — WHO interacts
|
||
|
||
**Purpose:** User interaction and visual feedback
|
||
|
||
**Contains:**
|
||
- Blender operators (button handlers)
|
||
- UI panels
|
||
- Property groups
|
||
- Cached data for UI performance
|
||
|
||
### Quick Reference: What Goes Where?
|
||
|
||
| Code Type | Layer | Example |
|
||
|-----------|-------|---------|
|
||
| Math formula | **Tool** | `T = R × tan(Δ/2)` |
|
||
| Vector operations | **Tool** | `v1.dot(v2)`, `v.normalized()` |
|
||
| IFC creation | **Tool** | `ifc.create_entity("IfcAlignment")` |
|
||
| Blender objects | **Tool** | `bpy.data.objects.new()` |
|
||
| Station calculations | **Tool** | `get_station_at_point()` |
|
||
| Coordinate transforms | **Tool** | `pyproj.transform()` |
|
||
| Mesh generation | **Tool** | `bmesh.ops.create_*()` |
|
||
| "Should we do X?" | **Core** | `can_add_pi(alignment)` |
|
||
| "When X, do Y" | **Core** | workflow orchestration |
|
||
| Validation rules | **Core** | `validate_minimum_radius()` |
|
||
| Button handler | **UI** | `class CIVIL_OT_add_pi` |
|
||
| Panel layout | **UI** | `class CIVIL_PT_alignment` |
|
||
|
||
**Simple Test:** If code contains a math formula or creates an IFC entity, it belongs in **Tool**, not **Core**.
|
||
|
||
---
|
||
|
||
## Code Patterns
|
||
|
||
### 1. IFC File Access
|
||
|
||
```python
|
||
import bonsai.tool as tool
|
||
|
||
# Get IFC file
|
||
ifc_file = tool.Ifc.get()
|
||
|
||
# Get Blender object for IFC element
|
||
obj = tool.Ifc.get_object(ifc_element)
|
||
|
||
# Link IFC element to Blender object
|
||
tool.Ifc.link(ifc_element, blender_obj)
|
||
```
|
||
|
||
### 2. Operator Pattern
|
||
|
||
```python
|
||
class AddAlignment(bpy.types.Operator, tool.Ifc.Operator):
|
||
bl_idname = "bim.add_alignment"
|
||
bl_label = "Add Alignment"
|
||
bl_options = {"REGISTER", "UNDO"}
|
||
|
||
def _execute(self, context):
|
||
# Use core function with tool injection
|
||
core.alignment.add_alignment(tool.Ifc, tool.Alignment)
|
||
return {"FINISHED"}
|
||
```
|
||
|
||
### 3. Core Function Pattern
|
||
|
||
```python
|
||
# In core/alignment.py - NO bpy imports, NO math!
|
||
def add_alignment(ifc, alignment):
|
||
"""
|
||
Business logic only - orchestrates alignment creation.
|
||
|
||
Args:
|
||
ifc: Tool class for IFC operations (injected)
|
||
alignment: Tool class for alignment operations (injected)
|
||
"""
|
||
# Business rule: check if we can add alignment
|
||
if not alignment.can_create_alignment():
|
||
raise ValueError("Cannot create alignment in current context")
|
||
|
||
# Delegate actual creation to Tool layer
|
||
result = alignment.create_alignment()
|
||
|
||
return result
|
||
```
|
||
|
||
### 4. Tool Method Pattern
|
||
|
||
```python
|
||
# In tool/alignment.py - ALL implementations here
|
||
class Alignment:
|
||
@classmethod
|
||
def create_alignment(cls):
|
||
"""Tool layer - IFC creation and math happens here."""
|
||
ifc_file = tool.Ifc.get()
|
||
|
||
# IFC entity creation
|
||
alignment = ifc_file.create_entity("IfcAlignment",
|
||
GlobalId=ifcopenshell.guid.new(),
|
||
Name="New Alignment"
|
||
)
|
||
|
||
# Blender object creation
|
||
obj = bpy.data.objects.new("Alignment", None)
|
||
tool.Ifc.link(alignment, obj)
|
||
|
||
return alignment
|
||
```
|
||
|
||
### 5. Data Caching (for UI)
|
||
|
||
```python
|
||
# In bim/module/alignment/data.py
|
||
class AlignmentData:
|
||
data = {}
|
||
is_loaded = False
|
||
|
||
@classmethod
|
||
def load(cls):
|
||
# Load data from IFC
|
||
cls.is_loaded = True
|
||
|
||
@classmethod
|
||
def refresh(cls):
|
||
cls.is_loaded = False
|
||
```
|
||
|
||
### 6. GPU Decorator Pattern (Visual Feedback)
|
||
|
||
GPU decorators provide ephemeral visual feedback during modal operations. They draw directly to the viewport using Blender's GPU module and disappear automatically when the modal ends.
|
||
|
||
**Location:** `bim/module/alignment/decorator.py`
|
||
|
||
```python
|
||
# In decorator.py - GPU drawing for modal operations
|
||
class PIPickerDecorator:
|
||
"""Decorator for visualizing PI placement during modal picking."""
|
||
|
||
# Class-level state (cleared on uninstall)
|
||
is_installed = False
|
||
handlers = []
|
||
pi_points = [] # List of Vector - positions in Blender coords
|
||
mouse_3d = None # Current cursor position
|
||
|
||
@classmethod
|
||
def install(cls, context, region, rv3d):
|
||
"""Install GPU draw handlers."""
|
||
if cls.is_installed:
|
||
cls.uninstall()
|
||
handler = cls()
|
||
cls.handlers.append(
|
||
SpaceView3D.draw_handler_add(handler.draw_tangent_lines, (context,), "WINDOW", "POST_PIXEL")
|
||
)
|
||
cls.is_installed = True
|
||
|
||
@classmethod
|
||
def uninstall(cls):
|
||
"""Remove handlers and clear state."""
|
||
for handler in cls.handlers:
|
||
try:
|
||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||
except ValueError:
|
||
pass
|
||
cls.handlers = []
|
||
cls.is_installed = False
|
||
|
||
@classmethod
|
||
def update(cls, pi_points_blender, mouse_3d):
|
||
"""Update state from modal operator."""
|
||
cls.pi_points = pi_points_blender
|
||
cls.mouse_3d = mouse_3d
|
||
|
||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||
"""Standard Bonsai draw pattern - MUST call validate first."""
|
||
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
|
||
return
|
||
# ... shader drawing code
|
||
```
|
||
|
||
**Key Bonsai Utilities for Decorators:**
|
||
|
||
| Utility | Purpose |
|
||
|---------|---------|
|
||
| `tool.Blender.validate_shader_batch_data()` | **Required** - prevents crashes on empty batches |
|
||
| `tool.Blender.scale_font_size()` | DPI-aware font scaling for HUD text |
|
||
| `draw_circle_2d()` from `gpu_extras.presets` | Built-in circle drawing |
|
||
| `location_3d_to_region_2d()` | Convert 3D coords to screen coords |
|
||
|
||
**Modal Operator Integration:**
|
||
|
||
```python
|
||
# In operator.py
|
||
def invoke(self, context, event):
|
||
# Install decorator for visual feedback
|
||
alignment_decorator.PIPickerDecorator.install(context, self._region, self._rv3d)
|
||
context.window_manager.modal_handler_add(self)
|
||
return {"RUNNING_MODAL"}
|
||
|
||
def modal(self, context, event):
|
||
if event.type == "MOUSEMOVE":
|
||
# Update rubber band position
|
||
alignment_decorator.PIPickerDecorator.update(pi_points, mouse_3d)
|
||
self._area.tag_redraw() # Trigger viewport redraw
|
||
|
||
if event.type in {"RIGHTMOUSE", "ESC"}:
|
||
alignment_decorator.PIPickerDecorator.uninstall() # Cleanup
|
||
return {"FINISHED"}
|
||
```
|
||
|
||
**Drawing Modes:**
|
||
- `POST_PIXEL` - 2D screen-space drawing (HUD text, lines in pixel coords)
|
||
- `POST_VIEW` - 3D world-space drawing (geometry in Blender coords)
|
||
|
||
---
|
||
|
||
## Property Naming Conventions
|
||
|
||
| Type | Prefix | Example |
|
||
|------|--------|---------|
|
||
| Scene PropertyGroup | `Civil*Properties` | `CivilAlignmentProperties` |
|
||
| Object PropertyGroup | `CivilObjectProperties` | - |
|
||
| Panel class | `CIVIL_PT_*` | `CIVIL_PT_alignments` |
|
||
| Operator class | `CIVIL_OT_*` | `CIVIL_OT_add_alignment` |
|
||
| UIList class | `CIVIL_UL_*` | `CIVIL_UL_alignments` |
|
||
|
||
**Note:** Use `Civil*` prefix, NOT `BIM*` or `BC_*` to avoid conflicts with Bonsai.
|
||
|
||
---
|
||
|
||
## IFC 4.3 Compliance
|
||
|
||
### Supported Entities
|
||
- `IfcAlignment`, `IfcAlignmentHorizontal`, `IfcAlignmentVertical`
|
||
- `IfcAlignmentSegment`, `IfcAlignmentHorizontalSegment`, `IfcAlignmentVerticalSegment`
|
||
- `IfcRoad`, `IfcSite`
|
||
- Segment types: LINE, CIRCULARARC, CLOTHOID, CONSTANTGRADIENT, PARABOLICARC
|
||
|
||
### Spatial Hierarchy
|
||
```
|
||
IfcProject
|
||
└── IfcSite
|
||
└── IfcRoad
|
||
└── IfcAlignment
|
||
├── IfcAlignmentHorizontal
|
||
│ └── IfcAlignmentSegment(s)
|
||
└── IfcAlignmentVertical
|
||
└── IfcAlignmentSegment(s)
|
||
```
|
||
|
||
### Key API
|
||
```python
|
||
import ifcopenshell.api.alignment as align_api
|
||
|
||
# Create alignment using PI method
|
||
alignment = align_api.create_by_pi_method(
|
||
ifc_file,
|
||
name="Main Road",
|
||
hpoints=[(0, 0), (100, 0), (200, 50)],
|
||
radii=[0, 150, 0],
|
||
start_station=0.0
|
||
)
|
||
|
||
# Get horizontal layout from alignment
|
||
h_layout = align_api.get_horizontal_layout(alignment)
|
||
```
|
||
|
||
---
|
||
|
||
## Development Setup
|
||
|
||
### Prerequisites
|
||
- Blender 5.0+
|
||
- Python 3.11
|
||
- Git
|
||
|
||
### Symlink for Development
|
||
```powershell
|
||
# Remove installed Bonsai extension
|
||
Remove-Item -Recurse "C:\Users\{USER}\AppData\Roaming\Blender Foundation\Blender\5.0\extensions\blender_org\bonsai"
|
||
|
||
# Create symlink to source
|
||
New-Item -ItemType SymbolicLink -Path "C:\Users\{USER}\AppData\Roaming\Blender Foundation\Blender\5.0\extensions\blender_org\bonsai" -Target "C:\GitHub\IfcOpenShell\src\bonsai\bonsai"
|
||
```
|
||
|
||
### Required Setup for Symlink
|
||
1. Update `blender_manifest.toml`: Change `platforms = ["os-arch"]` to `platforms = ["windows-x64"]`
|
||
2. Extract all wheels to site-packages (see Development Notes)
|
||
3. Mark manifest as skip-worktree: `git update-index --skip-worktree src/bonsai/bonsai/blender_manifest.toml`
|
||
|
||
### Git Workflow for Local-Only Files
|
||
|
||
This file (CLAUDE.md) is committed locally but should **never be pushed to origin**. A pre-push hook automatically enforces this.
|
||
|
||
**How it works:**
|
||
- CLAUDE.md is tracked in git (committed normally)
|
||
- The pre-push hook at `.git/hooks/pre-push` detects commits containing local-only files
|
||
- When you try to `git push`, it blocks and shows the exact command to push only feature commits
|
||
|
||
**To push your feature commits:**
|
||
```bash
|
||
# Option 1: Use the alias
|
||
git push-features
|
||
|
||
# Option 2: Manual command (shown by pre-push hook)
|
||
git push origin HEAD~N:branch_name # N = number of local-only commits at HEAD
|
||
```
|
||
|
||
**Workflow example:**
|
||
```bash
|
||
# Normal development
|
||
git add src/bonsai/...
|
||
git commit -m "Add new feature"
|
||
|
||
# Update CLAUDE.md (keeps it at tip of branch)
|
||
git add -f CLAUDE.md
|
||
git commit -m "Local: Update CLAUDE.md"
|
||
|
||
# Push feature commits only
|
||
git push-features # Pushes everything except CLAUDE.md commit
|
||
```
|
||
|
||
**If you need to set this up on a new machine:**
|
||
1. The pre-push hook is in `.git/hooks/pre-push` (not tracked by git)
|
||
2. Create the git alias: `git config alias.push-features "push origin HEAD~1:saikei"`
|
||
3. Force-add CLAUDE.md: `git add -f CLAUDE.md`
|
||
|
||
**Local-only files pattern:**
|
||
The hook checks for files listed in `LOCAL_ONLY_FILES` variable (currently just `CLAUDE.md`). Add more files there if needed.
|
||
|
||
---
|
||
|
||
## Development Progress
|
||
|
||
### Completed Features
|
||
- Horizontal alignment creation (PI method)
|
||
- All segment type visualization (LINE, CIRCULARARC, CLOTHOID, spirals via IfcOpenShell geometry engine)
|
||
- Individual segment selection (each segment is a selectable Blender curve)
|
||
- Alignment hierarchy in Blender (parent/child relationships)
|
||
- Stationing referents
|
||
- Zero-length terminator segment handling (invisible to users)
|
||
- Georeference/coordinate transformation support
|
||
- PI picker rubber band visualization (GPU decorator for visual feedback during modal picking)
|
||
|
||
### In Progress
|
||
- Vertical alignment support
|
||
- Corridor generation
|
||
- Cross-section profiles
|
||
|
||
### Planned
|
||
- Earthwork calculations
|
||
- Drainage design
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
```python
|
||
# In Blender Python console:
|
||
|
||
# Test IFC access
|
||
import bonsai.tool as tool
|
||
ifc = tool.Ifc.get()
|
||
print(f"IFC loaded: {ifc is not None}")
|
||
print(f"Schema: {ifc.schema if ifc else 'N/A'}")
|
||
|
||
# Test alignments
|
||
if ifc:
|
||
alignments = ifc.by_type("IfcAlignment")
|
||
print(f"Alignments: {len(alignments)}")
|
||
```
|
||
|
||
---
|
||
|
||
## Resources
|
||
|
||
### Documentation
|
||
- IFC 4.3 Spec: https://ifc43-docs.standards.buildingsmart.org/
|
||
- IfcOpenShell: https://docs.ifcopenshell.org/
|
||
- Bonsai Docs: https://docs.bonsaibim.org/
|
||
- Bonsai Wiki: https://wiki.osarch.org/
|
||
|
||
### Community
|
||
- OSArch Forum: https://community.osarch.org/
|
||
- buildingSMART: https://www.buildingsmart.org/
|
||
|
||
---
|
||
|
||
## Contact & Ownership
|
||
|
||
**Primary Developer:** Michael Yoder (Desert Springs Civil Engineering PLLC)
|
||
**Project:** Part of IfcOpenShell/Bonsai (open-source, community-driven)
|
||
**License:** GPL v3
|
||
|
||
---
|
||
|
||
*Last Updated: February 2026*
|
||
*Saikei - Civil Engineering for Bonsai*
|
||
|
||
---
|
||
|
||
## Architecture Clarification Note
|
||
|
||
**Source:** Dion Moult (Bonsai founder), January 2026
|
||
|
||
The three-layer architecture follows this strict separation:
|
||
|
||
| Layer | Contains | Simple Rule |
|
||
|-------|----------|-------------|
|
||
| **Core** | Business logic ONLY | "Decides WHAT happens" |
|
||
| **Tool** | Math, algorithms, IFC, Blender | "Implements HOW it happens" |
|
||
| **UI** | User interaction | "Handles WHO interacts" |
|
||
|
||
**Key insight:** If code contains a math formula or creates an IFC entity, it belongs in **Tool**, not **Core**. Core only orchestrates and validates - it never calculates.
|
||
|
||
---
|
||
|
||
## Geometry Pipeline (Alignment Visualization)
|
||
|
||
This section documents how alignment segments are converted from IFC entities to visible Blender curves.
|
||
|
||
### Entity Hierarchy
|
||
|
||
```
|
||
IFC Semantic Layer (Business Logic)
|
||
├── IfcAlignment
|
||
│ └── IfcAlignmentHorizontal (nested via IfcRelNests)
|
||
│ └── IfcAlignmentSegment (nested via IfcRelNests)
|
||
│ └── DesignParameters: IfcAlignmentHorizontalSegment
|
||
│ • StartPoint, StartDirection
|
||
│ • SegmentLength, PredefinedType (LINE, CIRCULARARC, CLOTHOID, etc.)
|
||
│ • StartRadiusOfCurvature, EndRadiusOfCurvature
|
||
|
||
IFC Geometric Layer (Representation)
|
||
├── IfcCompositeCurve (for horizontal)
|
||
│ └── IfcCurveSegment (one per alignment segment)
|
||
│ • Placement: IfcAxis2Placement2D
|
||
│ • ParentCurve: IfcLine, IfcCircle, IfcClothoid, etc.
|
||
│ • SegmentStart, SegmentLength
|
||
```
|
||
|
||
### Geometry Flow
|
||
|
||
```
|
||
IfcAlignmentSegment
|
||
│
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ align_api.get_mapped_segments │ (IfcOpenShell API)
|
||
│ Maps semantic → geometric │
|
||
└───────────────────────────────┘
|
||
│
|
||
▼
|
||
IfcCurveSegment (geometric representation)
|
||
│
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ align_util.evaluate_segment │ (IfcOpenShell geometry engine)
|
||
│ Returns 4x4 transform matrix │
|
||
│ at distance along segment │
|
||
└───────────────────────────────┘
|
||
│
|
||
▼
|
||
Transform Matrix (4x4, TRANSPOSED)
|
||
┌ ┐
|
||
│ Xx Xy Xz 0 │ Row 0: X-axis direction
|
||
│ Yx Yy Yz 0 │ Row 1: Y-axis direction
|
||
│ Zx Zy Zz 0 │ Row 2: Z-axis direction
|
||
│ Tx Ty Tz 1 │ Row 3: Translation (position)
|
||
└ ┘
|
||
│
|
||
▼
|
||
Blender CURVE object with vertices
|
||
```
|
||
|
||
### Key Implementation: `get_segment_vertices()`
|
||
|
||
Location: `src/bonsai/bonsai/tool/alignment.py`
|
||
|
||
This method extracts vertices from an IFC alignment segment using IfcOpenShell's geometry engine:
|
||
|
||
```python
|
||
@classmethod
|
||
def get_segment_vertices(
|
||
cls, segment: "ifcopenshell.entity_instance", distance_interval: float = 1.0
|
||
) -> Optional[List[Tuple[float, float, float]]]:
|
||
"""Get vertices for a single alignment segment using IfcOpenShell's geometry engine."""
|
||
import ifcopenshell.api.alignment as align_api
|
||
from ifcopenshell.api.alignment import util as align_util
|
||
|
||
# Get the mapped curve segment(s) for this alignment segment
|
||
mapped_segments = align_api.get_mapped_segments(segment)
|
||
# mapped_segments is tuple: (main_segment, spiral_in, spiral_out)
|
||
|
||
for curve_segment in [s for s in mapped_segments if s is not None]:
|
||
segment_length = curve_segment.SegmentLength.wrappedValue
|
||
num_points = max(2, int(segment_length / distance_interval) + 1)
|
||
|
||
for i in range(num_points):
|
||
dist_along = (i / (num_points - 1)) * segment_length
|
||
|
||
# Use IfcOpenShell geometry engine to evaluate position
|
||
transform_matrix = align_util.evaluate_segment(curve_segment, dist_along)
|
||
|
||
# CRITICAL: Matrix is transposed - position is in ROW 3, not column 3
|
||
x = float(transform_matrix[3, 0]) / unit_scale
|
||
y = float(transform_matrix[3, 1]) / unit_scale
|
||
z = float(transform_matrix[3, 2]) / unit_scale
|
||
|
||
vertices.append((x, y, z))
|
||
|
||
return vertices
|
||
```
|
||
|
||
**Critical Note:** The transform matrix returned by `evaluate_segment()` is transposed compared to standard OpenGL/Blender conventions. Position values are in **row 3** (indices `[3, 0]`, `[3, 1]`, `[3, 2]`), not column 3.
|
||
|
||
### Key Implementation: `_create_segment_curve()`
|
||
|
||
Location: `src/bonsai/bonsai/tool/alignment.py`
|
||
|
||
Creates a Blender CURVE object from segment vertices:
|
||
|
||
```python
|
||
@classmethod
|
||
def _create_segment_curve(
|
||
cls,
|
||
segment: "ifcopenshell.entity_instance",
|
||
segment_number: int,
|
||
parent_obj: "bpy.types.Object",
|
||
) -> Optional["bpy.types.Object"]:
|
||
"""Create a Blender curve object for an alignment segment."""
|
||
|
||
# Skip zero-length terminator segments
|
||
if cls.is_zero_length_segment(segment):
|
||
return None
|
||
|
||
# Get vertices using IfcOpenShell geometry engine
|
||
vertices = cls.get_segment_vertices(segment)
|
||
if not vertices:
|
||
return None
|
||
|
||
# Create Blender curve
|
||
curve_data = bpy.data.curves.new(name=curve_name, type='CURVE')
|
||
curve_data.dimensions = '3D'
|
||
spline = curve_data.splines.new('POLY')
|
||
spline.points.add(len(vertices) - 1)
|
||
|
||
for i, (x, y, z) in enumerate(vertices):
|
||
spline.points[i].co = (x, y, z, 1.0) # w=1.0 for 3D
|
||
|
||
obj = bpy.data.objects.new(curve_name, curve_data)
|
||
obj.parent = parent_obj
|
||
|
||
# Link to IFC element
|
||
tool.Ifc.link(segment, obj)
|
||
|
||
return obj
|
||
```
|
||
|
||
### Zero-Length Segment Handling
|
||
|
||
IFC alignments require a zero-length terminator segment at the end. These are handled invisibly:
|
||
|
||
```python
|
||
@classmethod
|
||
def is_zero_length_segment(cls, segment: "ifcopenshell.entity_instance") -> bool:
|
||
"""Check if segment is a zero-length terminator."""
|
||
design_params = segment.DesignParameters
|
||
if design_params.is_a("IfcAlignmentHorizontalSegment"):
|
||
return design_params.SegmentLength == 0.0
|
||
elif design_params.is_a("IfcAlignmentVerticalSegment"):
|
||
return design_params.HorizontalLength == 0.0
|
||
return False
|
||
|
||
@classmethod
|
||
def layout_has_real_segments(cls, layout: "ifcopenshell.entity_instance") -> bool:
|
||
"""Check if layout has segments beyond just the zero-length terminator."""
|
||
for rel in layout.IsNestedBy:
|
||
for segment in rel.RelatedObjects:
|
||
if not cls.is_zero_length_segment(segment):
|
||
return True
|
||
return False
|
||
```
|
||
|
||
**Behavior:**
|
||
- Zero-length segments are never visualized (no Blender object created)
|
||
- Empty layouts (only zero-length segment) skip geometry generation silently
|
||
- Warnings are suppressed for expected empty-layout scenarios
|
||
|
||
### IfcOpenShell Alignment API Functions Used
|
||
|
||
| Function | Location | Purpose |
|
||
|----------|----------|---------|
|
||
| `get_mapped_segments(segment)` | `ifcopenshell.api.alignment` | Maps IfcAlignmentSegment → IfcCurveSegment(s) |
|
||
| `evaluate_segment(curve_seg, dist)` | `ifcopenshell.api.alignment.util` | Returns 4x4 transform matrix at distance |
|
||
| `get_horizontal_layout(alignment)` | `ifcopenshell.api.alignment` | Gets IfcAlignmentHorizontal from alignment |
|
||
| `get_curve(alignment)` | `ifcopenshell.api.alignment` | Gets geometric representation curve |
|
||
| `has_zero_length_segment(layout)` | `ifcopenshell.api.alignment` | Checks for terminator segment |
|
||
|
||
### Coordinate Transformation
|
||
|
||
When georeference data exists, coordinates are transformed:
|
||
|
||
```python
|
||
# In tool/alignment.py
|
||
georeference = tool.Georeference.get_georeference()
|
||
if georeference:
|
||
vertices = [
|
||
tool.Georeference.xyz2local(v, georeference)
|
||
for v in vertices
|
||
]
|
||
```
|
||
|
||
This ensures alignment geometry displays correctly in Blender's coordinate system when the IFC file uses real-world coordinates.
|
||
|
||
---
|
||
|
||
## Troubleshooting & Debugging
|
||
|
||
### Common Issues and Solutions
|
||
|
||
| Symptom | Likely Cause | Solution |
|
||
|---------|--------------|----------|
|
||
| All vertices at (0,0,0) | Matrix index wrong (column vs row) | Use `matrix[3, 0]` not `matrix[0, 3]` for position |
|
||
| No visualization | Zero-length segment only | Check `layout_has_real_segments()` before generating |
|
||
| Segment not selectable | Created as EMPTY, not CURVE | Ensure `_create_segment_curve()` creates curve data |
|
||
| Wrong position in viewport | Missing georeference transform | Apply `tool.Georeference.xyz2local()` to vertices |
|
||
| `generate_vertices failed` | Empty IfcCompositeCurve | Skip geometry generation for empty layouts |
|
||
|
||
### Debugging with Blender MCP
|
||
|
||
Use Blender MCP to execute Python in Blender for live debugging:
|
||
|
||
```python
|
||
# Check if segments exist
|
||
import bonsai.tool as tool
|
||
ifc = tool.Ifc.get()
|
||
alignments = ifc.by_type("IfcAlignment")
|
||
for a in alignments:
|
||
for rel in a.IsNestedBy:
|
||
for child in rel.RelatedObjects:
|
||
if child.is_a("IfcAlignmentHorizontal"):
|
||
for rel2 in child.IsNestedBy:
|
||
print(f"Segments: {len(rel2.RelatedObjects)}")
|
||
|
||
# Test geometry evaluation
|
||
import ifcopenshell.api.alignment as align_api
|
||
from ifcopenshell.api.alignment import util as align_util
|
||
segment = ifc.by_type("IfcAlignmentSegment")[0]
|
||
mapped = align_api.get_mapped_segments(segment)
|
||
if mapped[0]:
|
||
matrix = align_util.evaluate_segment(mapped[0], 0.0)
|
||
print(f"Start position: ({matrix[3,0]}, {matrix[3,1]}, {matrix[3,2]})")
|
||
```
|
||
|
||
### Key Files to Check When Debugging
|
||
|
||
1. **`tool/alignment.py`** - Main implementation (geometry, Blender objects)
|
||
2. **`ifcopenshell/api/alignment/util.py`** - Geometry engine functions
|
||
3. **`ifcopenshell/api/alignment/get_mapped_segments.py`** - Semantic to geometric mapping
|
||
|
||
### Rebuilding After Breaking Changes
|
||
|
||
If the alignment visualization breaks:
|
||
|
||
1. **Verify IFC structure**: Check that IfcAlignmentSegment has DesignParameters and is nested correctly
|
||
2. **Verify geometry mapping**: Confirm `get_mapped_segments()` returns valid IfcCurveSegments
|
||
3. **Verify matrix extraction**: Remember matrix is TRANSPOSED - position in row 3
|
||
4. **Verify unit scaling**: Apply `calculate_unit_scale(file)` to coordinates
|
||
5. **Verify Blender object creation**: Ensure curve has spline points and is linked to IFC element
|