Files
IfcOpenShell/CLAUDE.md
T
2026-02-02 19:09:58 -07:00

30 KiB
Raw Blame History

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):

# 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):

# 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

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

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

# 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

# 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)

# 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

# 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:

# 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

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
# 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"
  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:

# 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:

# 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

# 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

Community


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:

@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:

@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:

@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:

# 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:

# 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