Files
IfcOpenShell/CLAUDE.md
T

20 KiB
Raw Blame History

CLAUDE.md - Saikei Civil Context for IfcOpenShell Contributions

This document provides context for Claude Code when working on Saikei Civil contributions to IfcOpenShell, specifically horizontal alignment visualization.


Project Overview

Saikei Civil (formerly BlenderCivil) is an open-source Blender extension for native IFC 4.3 infrastructure design. The project aims to democratize professional civil engineering tools by providing free alternatives to expensive commercial software like Civil 3D ($2,500/year) and OpenRoads ($4,000/year).

Mission

  • Bonsai BIM = Buildings (vertical construction)
  • Saikei Civil = Infrastructure (horizontal construction: roads, earthwork, drainage)

"While Bonsai crafts the buildings, Saikei shapes the world around them."

Key Differentiator

Native IFC Philosophy: IFC files serve as the primary database rather than export targets. We're not converting TO IFC - we ARE IFC from the start.


Architecture Principles

Three-Layer Architecture (Matches Bonsai)

Saikei follows Bonsai's proven architecture with core/ and tool/ at the package root level:

saikei/
├── core/                      # Layer 1: Pure Python - NO bpy imports
│   └── alignment.py           # Business logic, math, validation
├── tool/                      # Layer 2: Blender implementations - HAS bpy
│   └── alignment.py           # Blender object creation, linking
└── civil/                     # Layer 3: UI (like Bonsai's bim/)
    └── module/
        └── alignment/
            ├── __init__.py    # Registration
            ├── operator.py    # Blender operators
            ├── ui.py          # UI panels
            ├── prop.py        # PropertyGroups
            └── data.py        # UI data caching

Layer Responsibilities:

┌─────────────────────────────────────────────────────────────┐
│  Layer 3: civil/module/{name}/ (UI Layer)                   │
│  - operator.py: User actions (bpy.types.Operator)           │
│  - ui.py: Interface panels (bpy.types.Panel)                │
│  - prop.py: UI state (bpy.types.PropertyGroup)              │
│  - Calls core functions, passing tool implementations       │
└─────────────────────────────────────────────────────────────┘
                           │ calls
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 1: core/ (Pure Python - NO bpy imports)              │
│  - All business logic, algorithms, mathematics              │
│  - Receives tool classes as parameters (dependency inject)  │
│  - MUST be testable outside Blender                         │
└─────────────────────────────────────────────────────────────┘
                           │ receives as parameters
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 2: tool/ (Blender implementations)                   │
│  - Concrete implementations with bpy                        │
│  - Blender object creation, scene manipulation              │
│  - Wraps Bonsai's tool.Ifc, tool.Collector, etc.            │
└─────────────────────────────────────────────────────────────┘

Bonsai's Dependency Injection Pattern

Core functions receive tool classes as parameters, enabling testability:

# In core/alignment.py (NO bpy imports)
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import ifcopenshell
    import saikei.tool as tool

def create_alignment_visualization(
    ifc: type[tool.Ifc],
    alignment_tool: type[tool.Alignment],
    alignment: ifcopenshell.entity_instance,
) -> list:
    """Pure business logic - tools passed as parameters."""
    segments = get_layout_segments(alignment)
    objects = []
    for segment in segments:
        obj = alignment_tool.create_segment_object(segment)
        ifc.link(obj, segment)
        objects.append(obj)
    return objects
# In civil/module/alignment/operator.py (HAS bpy)
import bpy
import saikei.tool as tool
import saikei.core.alignment as core

class SAIKEI_OT_visualize_alignment(bpy.types.Operator):
    def execute(self, context):
        alignment = self.get_active_alignment()
        # Call core logic, passing tool implementations
        core.create_alignment_visualization(
            tool.Ifc, tool.Alignment, alignment
        )
        return {'FINISHED'}

Golden Pattern for Native IFC

# 1. GET IFC FILE
ifc = NativeIfcManager.get_file()

# 2. CREATE/MODIFY IFC ENTITY FIRST
entity = ifc.create_entity("IfcAlignment", ...)

# 3. CREATE BLENDER VISUALIZATION SECOND
obj = create_blender_object(...)

# 4. LINK THEM (minimal storage in Blender)
NativeIfcManager.link_object(obj, entity)
# obj["ifc_definition_id"] = entity.id()
# obj["ifc_class"] = entity.is_a()
# obj["GlobalId"] = entity.GlobalId

# 5. SAVE = WRITE IFC FILE
NativeIfcManager.save_file("project.ifc")

Key Principles

  1. IFC-First Design: ALL civil engineering data lives in the IFC file
  2. Minimal Blender Storage: Only 3 properties stored in Blender objects
  3. Separation of Concerns: Core logic has NO bpy imports
  4. IfcStore Pattern: Following Bonsai's proven pattern for IFC file management

IFC 4.3 Alignment Structure

Entity Hierarchy

IfcProject
└── IfcSite
    └── IfcRoad (via IfcRelAggregates)
        └── IfcAlignment (via IfcRelContainedInSpatialStructure)
            ├── IfcAlignmentHorizontal (via IfcRelNests)
            │   └── [IfcAlignmentSegment → IfcAlignmentHorizontalSegment]*
            ├── IfcAlignmentVertical (via IfcRelNests)
            │   └── [IfcAlignmentSegment → IfcAlignmentVerticalSegment]*
            └── IfcAlignmentCant (via IfcRelNests) [Future]
                └── [IfcAlignmentSegment → IfcAlignmentCantSegment]*

Critical IFC Rules

  1. Zero-Length Terminal Segments: The last IfcAlignmentSegment.DesignParameters in each layout MUST be zero length to provide the end point
  2. Ordered Collections: IfcRelNests.RelatedObjects maintains segment order (start to end)
  3. Nesting Order: Horizontal precedes Vertical precedes Cant in IfcRelNests

Semantic vs Geometric Representation

The IFC 4.3 alignment model has TWO parallel structures:

Semantic (Business Logic):

  • IfcAlignmentHorizontalSegment with PredefinedType (LINE, CIRCULARARC, CLOTHOID, etc.)
  • Contains design parameters (radius, length, direction)

Geometric (Shape):

  • IfcCurveSegmentParentCurve (IfcLine, IfcCircle, IfcClothoid)
  • Combined into IfcCompositeCurve (horizontal) or IfcGradientCurve (vertical)

Mapping Table:

Business Logic (Semantic) Geometric ParentCurve
LINE IfcLine
CIRCULARARC IfcCircle
CLOTHOID IfcClothoid
CUBIC IfcPolynomialCurve
PARABOLICARC IfcPolynomialCurve
CONSTANTGRADIENT IfcLine

The 2.5D Layered Model

IFC alignments use a "2.5D" approach - combining multiple 2D curves:

  1. Layer 1 - Horizontal: IfcCompositeCurve in X-Y (Easting-Northing) plane
  2. Layer 2 - Vertical: IfcGradientCurve in "Distance Along, Elevation" coordinate system
    • BaseCurve attribute points to horizontal IfcCompositeCurve
  3. Layer 3 - Cant: IfcSegmentedReferenceCurve for superelevation
    • BaseCurve typically points to IfcGradientCurve

Horizontal Alignment Implementation

PI-Driven Design Approach

Saikei Civil uses the Point of Intersection (PI) method, matching professional workflows in Civil 3D and OpenRoads:

    PI● is just an intersection point
         ╲
          ╲ Tangent
           BC (Begin Curve)
            ╲  
             ●   Curve (R=150m)
              ╲
           EC (End Curve)
           Tangent
         

Key Distinction:

  • PIs are just intersection points (NO radius property)
  • Curves are separate entities inserted between tangents (Curves HAVE radius)

Core Classes

class HorizontalAlignmentManager:
    """Core engine for horizontal alignment"""
    def add_pi(self, x: float, y: float) -> PI
    def insert_curve(self, pi_index: int, radius: float) -> Curve
    def generate_segments(self) -> List[Segment]
    
class PI:
    """Point of Intersection - just a position"""
    position: Tuple[float, float]
    # NO radius property
    
class Curve:
    """Curve inserted at a PI"""
    radius: float
    bc: Tuple[float, float]  # Begin Curve point
    ec: Tuple[float, float]  # End Curve point
    at_pi: int               # Which PI this curve is at

Civil Engineering Mathematics

Deflection Angle:

Δ = arccos(t₁ · t₂)

Where t₁ and t₂ are normalized incoming/outgoing tangent vectors.

Tangent Length:

T = R × tan(Δ/2)

Arc Length:

L = R × Δ  (radians)

BC/EC Points:

BC = PI - t₁ × T
EC = PI + t₂ × T

Segment Generation

def regenerate_segments(self):
    """Auto-generate tangent and curve segments from PIs"""
    segments = []
    
    for i, pi in enumerate(self.pis[:-1]):
        next_pi = self.pis[i + 1]
        
        # Check if curve at this PI
        curve = self.get_curve_at_pi(i)
        
        if curve:
            # Add incoming tangent (trimmed to BC)
            segments.append(create_line_segment(prev_end, curve.bc))
            # Add curve
            segments.append(create_arc_segment(curve))
            prev_end = curve.ec
        else:
            # Add full tangent
            segments.append(create_line_segment(prev_end, next_pi.position))
            prev_end = next_pi.position
    
    return segments

Visualization Layer

AlignmentVisualizer Class

class AlignmentVisualizer:
    """Create Blender visualization of IFC alignment"""
    
    def __init__(self, native_alignment):
        self.alignment = native_alignment
        self.collection = None
        self.pi_objects = []
        self.segment_objects = []
    
    def visualize_all(self):
        """Generate complete visualization"""
        self.setup_collection()
        self.create_pi_markers()
        self.create_segment_curves()

Color Coding Convention

PI Markers (Empties):

  • 🟢 Green = Tangent points (no curve)
  • 🟠 Orange = Curve PIs (curve inserted)

Segment Objects (Curves):

  • 🔵 Blue = Tangent segments (LINE)
  • 🔴 Red = Circular arcs (CIRCULARARC)

Object Linking Pattern

# Every Blender object stores only 3 properties
obj["ifc_definition_id"] = entity.id()
obj["ifc_class"] = entity.is_a()
obj["GlobalId"] = entity.GlobalId

# All other data comes from IFC
entity = NativeIfcManager.get_entity(obj)
params = entity.DesignParameters  # Real data from IFC!

Rick Brice's IfcOpenShell Alignment API

Rick Brice's alignment API was merged into IfcOpenShell v0.8.0 (PR #6234, March 14, 2025). This API provides comprehensive Python functions for IFC alignment creation.

Key Functions

Creation:

import ifcopenshell.api.alignment as align_api

# Create alignment with PI method
alignment = align_api.create_by_pi_method(
    ifc_file, 
    name='Main Alignment',
    hpoints=[(0,0), (100,50), (200,100)],  # Horizontal PI coordinates
    radii=[0, 150, 200],                    # Curve radii at PIs
    start_station=0.0
)

# Create alignment structure
alignment = align_api.create(
    ifc_file,
    name="Highway 101",
    horizontal_layout=horiz,
    vertical_layout=vert  # Optional
)

Layout Functions:

# Add to existing horizontal
align_api.layout_horizontal_alignment_by_pi_method(
    ifc_file, horiz_alignment, hpoints, radii
)

# Add vertical layout
align_api.add_vertical_layout(alignment, vertical_layout)

# Add zero-length terminator (required!)
align_api.add_zero_length_segment(layout)

Getters:

horiz = align_api.get_horizontal_layout(alignment)
vert = align_api.get_vertical_layout(alignment)
segments = align_api.get_layout_segments(layout)
curve = align_api.get_layout_curve(layout)  # IfcCompositeCurve/IfcGradientCurve

Integration Strategy

Recommended architecture for Saikei contributions:

Layer Rick's API Responsibility Saikei's Responsibility
IFC Backend All IFC entity creation, relationships, geometric representations None - use Rick's API
Business Logic Alignment math, segment generation, stationing Design validation, AASHTO rules
UI Layer None (Python API only) Full Blender panels, operators
Visualization None Real-time 3D preview, PI markers

Example Integration Pattern

# Saikei Civil operator using Rick's API backend
import ifcopenshell.api.alignment as align_api

class SAIKEI_OT_create_alignment(bpy.types.Operator):
    def execute(self, context):
        # Get PI data from Blender UI
        hpoints = [(pi.x, pi.y) for pi in context.scene.saikei_pis]
        radii = [pi.radius for pi in context.scene.saikei_pis]
        
        # Use Rick's API for IFC creation
        alignment = align_api.create_by_pi_method(
            self.ifc_file, 
            name='Main Alignment',
            hpoints=hpoints, 
            radii=radii,
            start_station=context.scene.saikei_start_station
        )
        
        # Saikei handles visualization
        self.visualizer.update_from_ifc(alignment)
        return {'FINISHED'}

Validation and Compliance

buildingSMART Validation

Saikei Civil has undergone extensive validation testing with buildingSMART International. Key compliance requirements:

  1. Zero-Length Terminal Segments: Every layout must end with a zero-length segment
  2. Spatial Hierarchy: Proper IfcRelAggregates and IfcRelContainedInSpatialStructure
  3. Segment Continuity: End of segment N must match start of segment N+1 (< 0.001m tolerance)
  4. Ordered Nesting: Segments in correct order via IfcRelNests.RelatedObjects

Validation Code Pattern

def validate_alignment(alignment):
    """Validate IFC alignment structure"""
    errors = []
    warnings = []
    
    # Check basic structure
    if not alignment:
        errors.append("No IfcAlignment entity")
        return errors, warnings
    
    # Get horizontal layout
    horiz = get_horizontal_layout(alignment)
    if not horiz:
        errors.append("No IfcAlignmentHorizontal")
        return errors, warnings
    
    # Check segments
    segments = get_layout_segments(horiz)
    if len(segments) < 2:
        warnings.append("Need at least 2 segments")
    
    # Check zero-length terminator
    last_seg = segments[-1].DesignParameters
    if last_seg.SegmentLength > 0.0001:
        errors.append("Missing zero-length terminal segment")
    
    # Check continuity
    for i in range(len(segments) - 1):
        gap = calculate_gap(segments[i], segments[i+1])
        if gap > 0.001:
            errors.append(f"Gap of {gap}m between segments {i} and {i+1}")
    
    return errors, warnings

PR Focus: Horizontal Alignment Visualization

For your first PR focused on horizontal alignment visualization, focus on:

Core Requirements

  1. Read IFC alignment data using Rick's API getters
  2. Generate Blender visualization objects (curves/empties)
  3. Link objects to IFC entities with minimal storage
  4. Color-code by segment type (LINE=blue, CIRCULARARC=red)
  5. Support real-time updates via depsgraph handlers

Key Files to Create/Modify

Following Bonsai's architecture with core/ and tool/ at package root:

saikei/
├── core/                          # NEW - Pure Python (NO bpy)
│   ├── __init__.py
│   └── alignment.py               # Business logic, math, validation
├── tool/                          # NEW - Blender implementations
│   ├── __init__.py
│   └── alignment.py               # Blender object creation, linking
└── civil/                         # EXISTING - UI layer
    └── module/
        └── alignment/
            ├── __init__.py        # Registration
            ├── operator.py        # Operators call core.*, passing tool.*
            ├── ui.py              # UI panels
            ├── prop.py            # PropertyGroups
            └── data.py            # UI data caching

Minimal Visualization Implementation

def visualize_horizontal_alignment(alignment, collection):
    """Create Blender visualization from IFC alignment"""
    
    # Get segments from IFC
    horiz = align_api.get_horizontal_layout(alignment)
    segments = align_api.get_layout_segments(horiz)
    
    objects = []
    for i, segment in enumerate(segments):
        params = segment.DesignParameters
        
        if params.PredefinedType == "LINE":
            obj = create_line_curve(params, name=f"Tangent_{i}")
            set_material_color(obj, BLUE)
        elif params.PredefinedType == "CIRCULARARC":
            obj = create_arc_curve(params, name=f"Curve_{i}")
            set_material_color(obj, RED)
        
        # Link to IFC
        obj["ifc_definition_id"] = segment.id()
        obj["ifc_class"] = segment.is_a()
        obj["GlobalId"] = segment.GlobalId
        
        collection.objects.link(obj)
        objects.append(obj)
    
    return objects

Development Guidelines

Code Style

  • Follow PEP 8
  • Type annotations for all public functions
  • Docstrings with examples
  • No bpy imports in core/ modules

Testing

  • Unit tests for all core logic (outside Blender)
  • Integration tests with sample IFC files
  • Validation against buildingSMART checker

Commit Messages

feat(alignment): Add horizontal alignment visualization

- Create AlignmentVisualizer class for Blender curve generation
- Support LINE and CIRCULARARC segment types
- Implement IFC entity linking pattern
- Add color coding by segment type

Refs: #123

Resources

Documentation

Community

Key Contacts

  • Rick Brice (WSDOT) - IfcOpenShell alignment API author
  • Dion Moult - Bonsai BIM founder
  • Will Sharp (HDR) - buildingSMART committee co-chair

Quick Reference

Entity Creation Pattern

# Always create IFC first, then Blender, then link
entity = ifc.create_entity("IfcAlignmentSegment", ...)
obj = bpy.data.objects.new("Segment", curve_data)
obj["ifc_definition_id"] = entity.id()

Segment Types

  • LINE → IfcLine (blue visualization)
  • CIRCULARARC → IfcCircle (red visualization)
  • CLOTHOID → IfcClothoid (future)
  • CONSTANTGRADIENT → IfcLine (vertical)
  • PARABOLICARC → IfcPolynomialCurve (vertical)

Required Terminal Segment

# Last segment MUST be zero-length
align_api.add_zero_length_segment(layout)

Minimal Object Storage

# ONLY these 3 properties in Blender
obj["ifc_definition_id"] = entity.id()
obj["ifc_class"] = entity.is_a()
obj["GlobalId"] = entity.GlobalId

Document Version: 1.1 Updated: January 2026 Changes: Updated architecture to match Bonsai's actual pattern (core/ and tool/ at package root) For: Saikei Civil IfcOpenShell PR - Horizontal Alignment Visualization