Table of Contents

Extreme Roof Framing Tool (ERFT) - Complete Technical Specification

Overview

ERFT is a Revit add-in that adjusts structural columns and beams to match the slope of a target floor or roof element. Users select a target element (floor/roof from host or linked model), then select source framing elements (columns/beams from host document). The tool analyzes the framing hierarchy, computes required vertical adjustments based on closest-point projection to the target surface, and applies offset parameter changes with configurable rounding tolerance.

Tool Location: src/Tools/Testing/ERFT/ Ribbon Location: DB Tools > Testing > ERFT


Table of Contents

  1. Core Concepts
  2. User Workflow
  3. Data Models
  4. Architecture
  5. Selection & Validation
  6. Geometry Analysis
  7. Framing Hierarchy Detection
  8. Adjustment Algorithm
  9. 3D Preview System
  10. UI Components
  11. ExtensibleStorage Schema
  12. Contextual Ribbon Integration
  13. Error Handling
  14. Test Strategy
  15. Implementation Phases

1. Core Concepts

1.1 Purpose

ERFT solves the problem of manually adjusting structural framing to follow sloped roofs or floors. Instead of tediously calculating and applying offsets to each element, users:

  1. Select the target slope surface (floor/roof)
  2. Select the framing elements to adjust (columns and beams)
  3. Configure rounding tolerance (e.g., nearest 1/4")
  4. Preview the adjustments in 3D with status highlighting
  5. Apply changes atomically with a single click

1.2 Key Principles

  • Offset-Only Adjustments: Never modify Level parameters; only adjust offset values
  • Closest-Point Projection: Each endpoint is projected to the nearest point on the target surface
  • Auto-Detect Hierarchy: Automatically determine which beams frame to columns vs. other beams
  • Cascading Adjustments: Columns first → beams-to-columns → beams-to-beams
  • SSOT Pattern: Single ErftPlan object holds all state; UI mutates it; Apply reads it

1.3 Workflow Stages

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  1. PICK TARGET │───>│  2. PICK SOURCE │───>│  3. CONFIGURE   │───>│   4. APPLY      │
│  - Floor/Roof   │    │  - Columns      │    │  - Preview 3D   │    │  - Adjust cols  │
│  - Host/Linked  │    │  - Beams        │    │  - Tolerance    │    │  - Adjust beams │
└─────────────────┘    └─────────────────┘    │  - Round up/dn  │    │  - Tag elements │
                                              └─────────────────┘    └─────────────────┘

2. User Workflow

2.1 Initial Flow (New)

  1. User clicks "ERFT" ribbon button
  2. Yellow banner: "Select a LINKED floor or roof target (press ESC to switch to HOST target selection)."
  3. User picks linked floor/roof OR presses ESC to continue to host phase
  4. Yellow banner (host fallback): "Select a HOST floor or roof target (press ESC to cancel ERFT launch)."
  5. User picks host floor/roof OR presses ESC to cancel
  6. Yellow banner: "Select source structural columns and beams to adjust, then click Finish."
  7. User picks multiple columns/beams, clicks Finish 7b. Concrete filter: exclude any selected members whose Structural Material name matches \bconcrete\b (case-insensitive).
  8. Validation: Must have at least 1 column OR 1 beam framing to a column
  9. Progress overlay appears: "Analyzing geometry..."
  10. Pre-analysis runs:
  • Extract target surface geometry (mesh + slope data)
  • Analyze source element positions
  • Detect framing hierarchy (what frames to what)
  • Warm profile loops for 3D preview
  • Compute initial adjustment deltas
  1. UI window opens with:
  • 3D preview showing target surface + framing
  • Tolerance controls (presets + custom)
  • Round up/down toggle
  • Element list with status indicators
  1. User adjusts tolerance as needed
  2. Preview updates in real-time

Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:22 14. User clicks "Apply" 15. Transaction group commits all changes 16. ExtensibleStorage tags applied to modified elements

Picker-phase banners are shown with BannerManager.ShowTopWarning(...) and hidden in finally cleanup after each picker stage.

Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:231 Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:313

2.2 Edit Flow (via Contextual Ribbon)

  1. User selects elements with ERFT ExtensibleStorage data
  2. Contextual "Edit ERFT" button appears in Modify tab
  3. User clicks button
  4. Click handler validates edit preconditions:
    • at least one ERFT SystemId is available in current selection
    • contextual ExternalEvent exists
    • ExternalEvent.Raise() returns Accepted
  5. External-event handler runs edit orchestration through ISafeExecutor with notifier enabled (NotifyKind=Error)
  6. Edit flow loads target context from DataStorage for selected SystemId
  7. If stored context or plan payload cannot be reconstructed, edit launch fails immediately with explicit banner feedback ([SpecFail/Edit/StoredPlanMissing]) and does not fall back to new-flow selection
  8. Edit flow creates AppRuntime.CreateRunScope(..., RevitRunScopeProfile.InlineUi) and resolves ErftRunner from that run scope
  9. UI opens with previous settings populated
  10. User can:
  • Re-pick target floor/roof
  • Adjust tolerances
  • Re-apply changes
  1. Apply overwrites previous offsets (not cumulative)

Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:566 Source: src/Tools/Testing/ERFT/Features/Commands/ErftRunner.cs:95


3. Data Models

3.1 ErftPlan (Single Source of Truth)

/// <summary>
/// Central domain model holding all ERFT state.
/// UI binds to this; Apply reads from it.
/// </summary>
public sealed class ErftPlan
{
    // === TARGET CONTEXT ===
    public int? HostTargetId { get; set; }          // Floor/Roof in host doc
    public int? LinkedTargetId { get; set; }        // Floor/Roof in linked doc
    public int? LinkInstanceId { get; set; }        // RevitLinkInstance if linked
    public TargetSurfaceGeometry? TargetGeometry { get; set; }
    
    // === SOURCE ELEMENTS ===
    public List<ErftColumnItem> Columns { get; init; } = new();
    public List<ErftBeamItem> Beams { get; init; } = new();
    
    // === FRAMING HIERARCHY ===
    public ErftFramingGraph FramingGraph { get; set; } = new();
    
    // === TOLERANCE SETTINGS ===
    public double ToleranceFeet { get; set; } = 1.0 / 48.0;  // 1/4" default
    public RoundingMode RoundingMode { get; set; } = RoundingMode.Nearest;
    
    // === PREVIEW STATE ===
    public IReadOnlyDictionary<int, MemberProfileLoops> ProfilesByTypeId { get; set; }
    
    // === METADATA ===
    public string SystemId { get; set; } = Guid.NewGuid().ToString();
    public int Version { get; set; } = 1;
}

3.2 ErftColumnItem

/// <summary>
/// Represents a structural column in the adjustment plan.
/// </summary>
public sealed class ErftColumnItem
{
    public int ElementId { get; init; }
    public int TypeId { get; init; }
    public string TypeName { get; init; } = string.Empty;
    
    // Original state (read from Revit)
    public Vector3 TopPoint { get; init; }              // Current top coordinate
    public double OriginalTopOffsetFeet { get; init; }  // Current Top Offset parameter
    public int TopLevelId { get; init; }                // Top Level (never changed)
    
    // Computed adjustment
    public double TargetElevationFeet { get; set; }     // Z of closest point on target
    public double DeltaFeet { get; set; }               // Raw delta (target - current)
    public double RoundedDeltaFeet { get; set; }        // After tolerance rounding
    public double NewTopOffsetFeet { get; set; }        // Original + RoundedDelta
    
    // Status
    public AdjustmentStatus Status { get; set; }
}

3.3 ErftBeamItem

/// <summary>
/// Represents a structural beam in the adjustment plan.
/// </summary>
public sealed class ErftBeamItem
{
    public int ElementId { get; init; }
    public int TypeId { get; init; }
    public string TypeName { get; init; } = string.Empty;
    
    // Original state
    public Vector3 StartPoint { get; init; }
    public Vector3 EndPoint { get; init; }
    public double OriginalStartOffsetFeet { get; init; }
    public double OriginalEndOffsetFeet { get; init; }
    public int ReferenceLevelId { get; init; }
    
    // Connection classification
    public BeamEndConnection StartConnection { get; set; }  // Column, Beam, or Free
    public BeamEndConnection EndConnection { get; set; }
    public int? StartConnectionElementId { get; set; }      // Column or Beam it connects to
    public int? EndConnectionElementId { get; set; }
    
    // Computed adjustment
    public double StartTargetElevationFeet { get; set; }
    public double EndTargetElevationFeet { get; set; }
    public double StartDeltaFeet { get; set; }
    public double EndDeltaFeet { get; set; }
    public double RoundedStartDeltaFeet { get; set; }
    public double RoundedEndDeltaFeet { get; set; }
    public double NewStartOffsetFeet { get; set; }
    public double NewEndOffsetFeet { get; set; }
    
    // Status
    public AdjustmentStatus Status { get; set; }
}

3.4 Supporting Types

public enum BeamEndConnection
{
    Free,           // Endpoint not connected to anything in selection
    ToColumn,       // Frames into a column top
    ToBeam          // Frames into another beam's location line
}

public enum AdjustmentStatus
{
    Pending,        // Will be modified
    NoChangeNeeded, // Already at correct elevation (within tolerance)
    Error           // Cannot compute adjustment (missing geometry, etc.)
}

public enum RoundingMode
{
    Nearest,        // Round to nearest increment
    Up,             // Always round up (toward target)
    Down            // Always round down (away from target)
}

/// <summary>
/// Extracted geometry of the target floor/roof surface.
/// </summary>
public sealed class TargetSurfaceGeometry
{
    public IReadOnlyList<Vector3> Vertices { get; init; }
    public IReadOnlyList<int> TriangleIndices { get; init; }
    public BoundingBox Bounds { get; init; }
    public Transform? LinkTransform { get; init; }  // null if host element
}

/// <summary>
/// Graph representing framing connectivity.
/// </summary>
public sealed class ErftFramingGraph
{
    public IReadOnlyDictionary<int, List<int>> ColumnToBeams { get; set; }  // columnId → beamIds framing to it
    public IReadOnlyDictionary<int, List<int>> BeamToBeams { get; set; }    // beamId → beamIds framing to it
    public IReadOnlyList<int> ProcessingOrder { get; set; }  // Topological sort for cascading
}

4. Architecture

4.1 Folder Structure

src/Tools/Testing/ERFT/
├── Assets/
│   └── erft_icon.png                    # 32x32 tool icon
├── Common/
│   ├── Domain/
│   │   ├── ErftPlan.cs                  # SSOT plan object
│   │   ├── ErftColumnItem.cs            # Column data
│   │   ├── ErftBeamItem.cs              # Beam data
│   │   ├── TargetSurfaceGeometry.cs     # Mesh representation
│   │   ├── ErftFramingGraph.cs          # Connectivity graph
│   │   ├── ErftStoredPlan.cs            # Serialization snapshot
│   │   ├── Vector3.cs                   # (reuse from SGT or create)
│   │   └── MemberProfileLoops.cs        # (reuse from SGT)
│   ├── Models/
│   │   ├── ErftSchemaDefinition.cs      # ExtensibleStorage constants
│   │   └── TolerancePreset.cs           # Rounding preset options
│   ├── Revit/
│   │   └── ErftSystemRepository.cs      # ExtensibleStorage CRUD
│   └── State/
│       └── ErftPlanStore.cs             # Thread-safe plan holder
├── Features/
│   ├── Commands/
│   │   ├── ErftNewCommand.cs            # Ribbon entry point
│   │   ├── ErftRunner.cs                # Modal flow orchestration
│   │   ├── ErftContextualRibbonInjector.cs  # Edit button injection
│   │   └── ErftMode.cs                  # New/Edit enum
│   ├── Analysis/
│   │   ├── Logic/
│   │   │   ├── TargetSurfaceAnalyzer.cs    # Extract mesh from floor/roof
│   │   │   ├── FramingHierarchyBuilder.cs  # Build connectivity graph
│   │   │   └── AdjustmentCalculator.cs     # Compute deltas
│   │   └── Revit/
│   │       ├── FloorRoofGeometryService.cs # Revit API geometry extraction
│   │       ├── ColumnQueryService.cs       # Read column properties
│   │       └── BeamQueryService.cs         # Read beam properties
│   ├── Apply/
│   │   ├── Logic/
│   │   │   └── ErftOrchestrator.cs         # Apply orchestration
│   │   └── Revit/
│   │       ├── ErftDomainWriter.cs         # Write offset parameters
│   │       └── ErftStorageMapper.cs        # JSON serialization
│   └── Preview/
│       ├── Logic/
│       │   └── ErftPreviewSceneBuilder.cs  # Build 3D meshes
│       └── UI/
│           └── ErftPreview3DRenderer.cs    # HelixToolkit rendering
├── Shell/
│   ├── Composition/
│   │   └── ErftServiceCollectionExtensions.cs  # DI registration
│   ├── Revit/
│   │   ├── ErftTargetSelectionFilter.cs    # Floor/Roof picker filter
│   │   └── ErftSourceSelectionFilter.cs    # Column/Beam picker filter
│   ├── Services/
│   │   └── DialogService.cs                # (reuse pattern from SGT)
│   └── UI/
│       ├── ViewModels/
│       │   └── ErftWindowViewModel.cs      # Main VM
│       └── Views/
│           └── ErftWindow.xaml[.cs]        # Main UI window
├── Shared/
│   ├── Contracts/
│   │   ├── IErftPlanStore.cs
│   │   ├── ITargetSurfaceAnalyzer.cs
│   │   └── IFramingHierarchyBuilder.cs
│   └── Geometry/
│       └── ClosestPointService.cs          # Point-to-mesh projection
├── Tests/
│   ├── ClosestPointServiceTests.cs
│   ├── FramingHierarchyBuilderTests.cs
│   ├── AdjustmentCalculatorTests.cs
│   ├── ErftStorageMapperTests.cs
│   └── ErftPreviewSceneBuilderTests.cs
├── ErftToolModule.cs                       # Tool registration + hooks
├── ErftServiceExtensions.cs                # Bootstrap
├── manifest.yml                            # Tool manifest
├── DBTools.ERFT.csproj                     # Project file
└── GlobalSuppressions.cs                   # Analyzer suppressions

4.2 Dependencies

  • HelixToolkit.SharpDX.Core.Wpf - 3D preview rendering (already in repo)
  • CSharpFunctionalExtensions - Result<T,E> pattern (already in repo)
  • System.Text.Json - Serialization (already in repo)

5. Selection & Validation

5.1 Target Selection Filter

/// <summary>
/// ISelectionFilter for picking floor/roof from host or linked models.
/// </summary>
public sealed class ErftTargetSelectionFilter : ISelectionFilter
{
    private readonly Document _doc;
    
    public bool AllowElement(Element elem)
    {
        // Allow RevitLinkInstance (for linked targets)
        if (elem is RevitLinkInstance) return true;
        // Allow host Floor or Roof
        return elem is Floor || elem is RoofBase;
    }
    
    public bool AllowReference(Reference reference, XYZ position)
    {
        if (reference == null) return false;
        
        // Check if it's a linked element
        var hostElem = _doc.GetElement(reference.ElementId);
        if (hostElem is RevitLinkInstance link)
        {
            var linkDoc = link.GetLinkDocument();
            var linked = linkDoc?.GetElement(reference.LinkedElementId);
            return linked is Floor || linked is RoofBase;
        }
        
        // Host element
        return hostElem is Floor || hostElem is RoofBase;
    }
}

5.2 Source Selection Filter

/// <summary>
/// ISelectionFilter for picking structural columns and beams from host document.
/// </summary>
public sealed class ErftSourceSelectionFilter : ISelectionFilter
{
    public bool AllowElement(Element elem)
    {
        if (elem is not FamilyInstance fi) return false;
        
        var cat = elem.Category?.BuiltInCategory;
        return cat == BuiltInCategory.OST_StructuralColumns ||
               cat == BuiltInCategory.OST_StructuralFraming;
    }
    
    public bool AllowReference(Reference reference, XYZ position)
    {
        return false; // No linked sources allowed
    }
}

5.3 Validation Rules

After source selection completes, validate:

  1. At least one column must be selected, OR
  2. At least one beam must frame directly to a column (detected via proximity)
  3. If only beams are selected with no columns, show error: "Select at least one column or beams that frame to columns"

Implementation note:

  • Beam-only validation uses ERFT framing-graph logic (IErftFramingHierarchyBuilder) and is evaluated on cloned beam items so runtime plan connection state is not mutated during validation.

Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:98 Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionValidationRules.cs:24


6. Geometry Analysis

6.1 Target Surface Extraction

/// <summary>
/// Extracts triangulated mesh from Floor or RoofBase bottom face.
/// </summary>
public sealed class FloorRoofGeometryService
{
    /// <summary>
    /// Gets the bottom face geometry of a floor or roof, transformed to host coordinates if linked.
    /// </summary>
    public Result<TargetSurfaceGeometry, string> ExtractBottomSurface(
        Element target,
        RevitLinkInstance? link)
    {
        var options = new Options { ComputeReferences = false, DetailLevel = ViewDetailLevel.Medium };
        var geomElem = target.get_Geometry(options);
        
        // Find bottom-facing planar faces and non-planar faces
        // Triangulate using Revit's Tessellate() method
        // Transform vertices if link != null: link.GetTotalTransform()
        
        return new TargetSurfaceGeometry
        {
            Vertices = vertices,
            TriangleIndices = indices,
            Bounds = bounds,
            LinkTransformMatrix = linkTransformMatrix // 16-value homogeneous matrix
        };
    }
}

TargetSurfaceGeometry.LinkTransformMatrix uses a 16-value column-major 4x4 contract.

Source: src/Tools/Testing/ERFT/Common/Domain/TargetSurfaceGeometry.cs:23 Source: src/Tools/Testing/ERFT/Common/Domain/TargetSurfaceGeometry.cs:52 Source: src/Tools/Testing/ERFT/Features/Analysis/ErftTargetGeometryService.cs:323

6.2 Closest Point Projection

/// <summary>
/// Projects a point to the closest point on a triangulated mesh.
/// Used to find target elevation for each framing endpoint.
/// </summary>
public sealed class ClosestPointService
{
    /// <summary>
    /// Finds the closest point on the target mesh (bottom face of floor/roof)
    /// to the given query point. Returns Z coordinate for elevation.
    /// </summary>
    public Result<double, string> ProjectToSurface(
        Vector3 queryPoint,
        TargetSurfaceGeometry surface)
    {
        // For each triangle in the mesh:
        //   1. Project query point onto triangle plane
        //   2. Clamp to triangle if outside
        //   3. Track closest distance
        // Return Z of closest point
    }
}

Algorithm Notes:

  • For simple single-slope surfaces (common case), this is fast
  • For hip roofs or warped slabs, may need spatial acceleration (octree) for large meshes
  • Initial implementation: brute-force iterate triangles; optimize later if needed

7. Framing Hierarchy Detection

7.1 Overview

The algorithm must determine:

  1. Which beams frame INTO columns (endpoint within tolerance of column top)
  2. Which beams frame INTO other beams (endpoint on another beam's location line)
  3. Processing order: Columns → Beams-to-Columns → Beams-to-Beams (cascading)

7.2 Detection Algorithm

/// <summary>
/// Builds the framing connectivity graph from selected elements.
/// </summary>
public sealed class FramingHierarchyBuilder
{
    private const double ConnectionToleranceFeet = 0.5 / 12.0; // 1/2" tolerance
    
    public ErftFramingGraph BuildGraph(
        IReadOnlyList<ErftColumnItem> columns,
        IReadOnlyList<ErftBeamItem> beams)
    {
        var columnToBeams = new Dictionary<int, List<int>>();
        var beamToBeams = new Dictionary<int, List<int>>();
        
        // Phase 1: Detect beam-to-column connections
        foreach (var beam in beams)
        {
            foreach (var col in columns)
            {
                // Check if beam start or end is within tolerance of column top
                if (IsWithinTolerance(beam.StartPoint, col.TopPoint))
                {
                    beam.StartConnection = BeamEndConnection.ToColumn;
                    beam.StartConnectionElementId = col.ElementId;
                    columnToBeams.GetOrAdd(col.ElementId).Add(beam.ElementId);
                }
                if (IsWithinTolerance(beam.EndPoint, col.TopPoint))
                {
                    beam.EndConnection = BeamEndConnection.ToColumn;
                    beam.EndConnectionElementId = col.ElementId;
                    columnToBeams.GetOrAdd(col.ElementId).Add(beam.ElementId);
                }
            }
        }
        
        // Phase 2: Detect beam-to-beam connections
        foreach (var beam in beams)
        {
            if (beam.StartConnection == BeamEndConnection.Free)
            {
                var supporting = FindSupportingBeam(beam.StartPoint, beams, beam.ElementId);
                if (supporting != null)
                {
                    beam.StartConnection = BeamEndConnection.ToBeam;
                    beam.StartConnectionElementId = supporting.ElementId;
                    beamToBeams.GetOrAdd(supporting.ElementId).Add(beam.ElementId);
                }
            }
            // Same for EndPoint...
        }
        
        // Phase 3: Build topological processing order
        var order = TopologicalSort(columns, beams, columnToBeams, beamToBeams);
        
        return new ErftFramingGraph { ... };
    }
    
    private ErftBeamItem? FindSupportingBeam(Vector3 point, IReadOnlyList<ErftBeamItem> beams, int excludeId)
    {
        // Find beam whose location line is closest to point
        // Return beam where point projects onto line segment within tolerance
    }
}

7.3 Hybrid Beams (Column + Beam)

Beams can have one end framing to a column and the other end framing to another beam. The algorithm handles this naturally:

  • Each endpoint is classified independently
  • Start might be ToColumn, End might be ToBeam
  • Processing order ensures columns are adjusted first, then beams-to-columns, then dependent beams

8. Adjustment Algorithm

8.1 Processing Order

1. Adjust all COLUMNS:
   - Project top point to target surface
   - Compute delta = (target_Z - current_top_Z)
   - Apply rounding tolerance
   - New Top Offset = Original Top Offset + rounded_delta

2. doc.Regenerate() if needed

3. Adjust all BEAMS framing to COLUMNS:
   - For endpoints connected to columns: inherit column's new top elevation
   - For endpoints connected to target surface: project to surface
   - Apply rounding
   - New Start/End Offset = Original + rounded_delta

4. doc.Regenerate() if needed

5. Adjust remaining BEAMS (framing to other beams):
   - Use Revit's equivalent of "shift-drag snap to beam location line"
   - Project endpoint onto supporting beam's (now sloped) location line
   - Compute elevation at intersection point
   - Apply rounding

8.2 Rounding Logic

public static class RoundingHelper
{
    /// <summary>
    /// Rounds a delta value according to tolerance and mode.
    /// </summary>
    public static double ApplyRounding(double deltaFeet, double toleranceFeet, RoundingMode mode)
    {
        if (toleranceFeet <= 0) return deltaFeet;
        
        return mode switch
        {
            RoundingMode.Nearest => Math.Round(deltaFeet / toleranceFeet) * toleranceFeet,
            RoundingMode.Up => Math.Ceiling(deltaFeet / toleranceFeet) * toleranceFeet,
            RoundingMode.Down => Math.Floor(deltaFeet / toleranceFeet) * toleranceFeet,
            _ => deltaFeet
        };
    }
}

8.3 Beam-to-Beam Snap (API Equivalent)

The manual workflow is: hold Shift, drag beam endpoint onto supporting beam's location line until it highlights, then release.

Revit API Equivalent:

/// <summary>
/// Computes the elevation where a point intersects a sloped beam's location line.
/// </summary>
public static double ComputeElevationOnBeamLocationLine(
    Vector3 queryPoint,
    Vector3 beamStart,
    Vector3 beamEnd)
{
    // 1. Project queryPoint onto the 3D line defined by beamStart→beamEnd
    var lineDir = (beamEnd - beamStart).Normalized();
    var t = Vector3.Dot(queryPoint - beamStart, lineDir);
    var closestOnLine = beamStart + lineDir * t;
    
    // 2. Return Z coordinate of the closest point
    return closestOnLine.Z;
}

9. 3D Preview System

9.1 Scene Components

Component Color (Hex) Opacity Description
Target surface #4080C0 0.3 Semi-transparent floor/roof mesh
Column (pending) #00CC00 1.0 Will be adjusted (green)
Column (no change) #808080 0.7 Already correct (gray)
Column (error) #CC0000 1.0 Cannot compute (red)
Beam (pending) #00CC00 1.0 Will be adjusted (green)
Beam (no change) #808080 0.7 Already correct (gray)
Beam (error) #CC0000 1.0 Cannot compute (red)

9.2 Profile Warming

Reuse SGT's pattern for extracting 2D profile loops from structural framing:

  1. Create temp placement of each unique type
  2. Extract end face geometry
  3. Store as MemberProfileLoops
  4. Use for extrusion in preview

Logging contract:

  • Emit one profile-warmup summary line with total/warmed/failed counts.
  • Keep per-type warm progress logs at Trace (not Debug).

Source: src/Tools/Testing/ERFT/Features/Preview/Revit/ErftMemberProfileWarmupService.cs:74
Source: src/Tools/Testing/ERFT/Features/Preview/Revit/ErftMemberProfileWarmupService.cs:154

9.3 Scene Builder

public sealed class ErftPreviewSceneBuilder
{
    public PreviewScene3D Build(
        ErftPlan plan,
        IReadOnlyDictionary<int, MemberProfileLoops> profiles)
    {
        var meshes = new List<MeshData>();
        
        // 1. Build target surface mesh
        BuildTargetMesh(plan.TargetGeometry, meshes);
        
        // 2. Build column meshes (show NEW position after adjustment)
        foreach (var col in plan.Columns)
            BuildColumnMesh(col, profiles, meshes);
        
        // 3. Build beam meshes (show NEW position after adjustment)
        foreach (var beam in plan.Beams)
            BuildBeamMesh(beam, profiles, meshes);
        
        return new PreviewScene3D { Meshes = meshes, ... };
    }
}

Target preview mesh selection policy:

  • Primary role: TargetSurface using analyzed bottom-surface mesh (Vertices + TriangleIndices)
  • Fallback role: TargetElement only when primary surface mesh is unavailable

Source: src/Tools/Testing/ERFT/Features/Preview/Logic/ErftPreviewSceneBuilder.cs:76


10. UI Components

10.1 Main Window Layout

┌─────────────────────────────────────────────────────────────────┐
│ Extreme Roof Framing Tool                              [X]      │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │                                                             │ │
│ │                    3D PREVIEW VIEWPORT                      │ │
│ │                    (HelixToolkit)                           │ │
│ │                                                             │ │
│ └─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Target: [Floor 123 (Linked: Arch Model)] [Re-pick]              │
├─────────────────────────────────────────────────────────────────┤
│ Rounding Tolerance:  [1/4" ▼]  [○ Nearest ● Up ○ Down]          │
├─────────────────────────────────────────────────────────────────┤
│ Summary:                                                        │
│   Columns: 12 pending, 2 no change                              │
│   Beams:   45 pending, 5 no change, 1 error                     │
├─────────────────────────────────────────────────────────────────┤
│                              [Cancel]  [Apply]                  │
└─────────────────────────────────────────────────────────────────┘

10.2 Tolerance Presets

public static readonly TolerancePreset[] Presets = new[]
{
    new TolerancePreset("1/16\"", 1.0 / 192.0),  // 0.0625"
    new TolerancePreset("1/8\"",  1.0 / 96.0),   // 0.125"
    new TolerancePreset("1/4\"",  1.0 / 48.0),   // 0.25" (default)
    new TolerancePreset("1/2\"",  1.0 / 24.0),   // 0.5"
    new TolerancePreset("1\"",    1.0 / 12.0),   // 1.0"
    new TolerancePreset("Custom", -1),           // Triggers custom input
};

10.3 Re-pick Target Button

Clicking "Re-pick" closes the window temporarily, returns to pick mode, then re-opens with new target and re-computed adjustments.

10.4 Comparison Grid Status Columns

Both comparison grids keep Status as the last column with star sizing and a minimum width. This preserves readability while consuming trailing space instead of displaying a blank trailing area.

Source: src/Tools/Testing/ERFT/Shell/UI/Views/ErftWindow.xaml:427
Source: src/Tools/Testing/ERFT/Shell/UI/Views/ErftWindow.xaml:546


11. ExtensibleStorage Schema

11.1 Element Tagging Schema

public static class ErftSchemaDefinition
{
    public const string Name = "DBTools_ERFT";
    public const string Vendor = "DBTools";
    public static readonly Guid Id = new("B2C3D4E5-F6A7-4B8C-9D0E-1F2A3B4C5D6E");
    
    public const string FieldTag = "Tag";               // "ERFT"
    public const string FieldSystemId = "SystemId";     // GUID linking related elements
    public const string FieldRole = "Role";             // "Column" or "Beam"
    public const string FieldOriginalOffset = "OriginalOffset";  // Pre-adjustment value
    public const string FieldTimestamp = "Timestamp";   // ISO 8601
    public const string FieldVersion = "Version";       // Schema version
    
    public const string SchemaVersion = "1";
}

11.2 System Repository

Store full plan in DataStorage element for Edit mode restoration:

public sealed class ErftSystemRepository
{
    private const string SystemSchemaName = "DBTools_ERFT_System";
    
    public void Save(Guid systemId, ErftStoredPlan plan, CancellationToken ct);
    public Result<ErftStoredPlan, string> Load(Guid systemId, CancellationToken ct);
    public void Delete(Guid systemId, CancellationToken ct);
}

12. Contextual Ribbon Integration

12.1 Selection Changed Handler

public sealed class ErftContextualRibbonInjector : IContextualRibbonInjector
{
    public Task InitializeAsync(UIControlledApplication application, CancellationToken ct)
    {
        return _gate.RunAsync(app =>
        {
            EnsureEditEventCreated(); // ExternalEvent.Create in API-safe context
            if (_subscribed) return;
            app.SelectionChanged += OnSelectionChanged;
            _subscribed = true;
            _logger.LogInformation("[ERFT] Contextual ribbon injector subscribed to selection changes.");
            EnsureInjected(app);
            EvaluateAndToggle(app);
        }, ct);
    }

    private async void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
    {
        await _executor.RunAsync(
            () => RefreshFromCurrentContextAsync(CancellationToken.None),
            _logger,
            notifier: null,
            opts: new SafeExecutor.SafeExecuteOptions
            {
                Name = "ERFT Contextual Selection Hook",
                LogStart = false,
                LogCompletion = false,
                ShowCompletionToUser = false,
                SuppressCompletionBanner = true,
                NoBannerOnSuccess = true,
                NotifyKind = SafeExecutor.NotifyKind.None,
                WorkPerformed = false
            },
            ct: CancellationToken.None);
    }
}

Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:66
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:119
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:139 Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:151

Selection-refresh logging must be state-aware:

  • Log contextual evaluation only when (hasErftElements, systemCount, buttonCount) changes.
  • Emit "schema missing" debug line once until schema becomes available again.

Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:212
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:237

12.2 Contextual Tab Detection

Look for:

  • "Modify | Structural Columns"
  • "Modify | Structural Framing"
  • "Modify | Multi-Select" (when mixed selection)
  • Multi-selection title variants containing "Multi" / "Multi-Category"

Inject "Edit ERFT" button when ERFT-tagged elements are selected.

Selection eligibility requires:

  • Category is OST_StructuralColumns or OST_StructuralFraming
  • ERFT schema lookup succeeds
  • Tag == "ERFT"
  • Version == SchemaVersion
  • SystemId is present

Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:207
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:272
Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:320

12.3 Edit Launch Guardrails and Run Scope

  • Edit clicks are rejected with explicit banner feedback when no ERFT system IDs are available, when the ExternalEvent is not initialized, or when ExternalEvent.Raise() does not return Accepted.
  • External-event execution is wrapped in ISafeExecutor with notifier enabled so edit failures are surfaced to the user (not silent).
  • Edit launch hard-fails when stored context/plan data is missing ([SpecFail/Edit/StoredPlanMissing]); no fallback route to new-flow selection is allowed.
  • RunEditFlow(...) creates a per-click InlineUi run scope via AppRuntime.CreateRunScope(...), resolves ErftRunner inside that scope, and clears/disposes the scoped IRevitRunScopeAccessor.Current value in finally.
  • Stored tolerance is strict during reconstruction: non-positive values fail ([SpecFail/Edit/InvalidStoredTolerance]) and are not defaulted.

Source: src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:566
Source: src/Tools/Testing/ERFT/Features/Commands/ErftRunner.cs:95
Source: src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:286


13. Error Handling

13.1 Spec Fail Tags

All errors use traceable spec tags:

Tag Description
[SpecFail/Target/NoGeometry] Cannot extract mesh from target
[SpecFail/Target/NotFloorOrRoof] Selected element is not Floor/Roof
[SpecFail/Source/NoValidElements] No columns or valid beams selected
[SpecFail/Selection/NoColumnConnection] Beams-only source selection has no beam-to-column connection
[SpecFail/Edit/StoredPlanMissing] Contextual edit storage payload missing/corrupt
[SpecFail/Edit/InvalidStoredTolerance] Stored edit tolerance is invalid (<= 0)
[SpecFail/Edit/InvalidStoredRoundingMode] Stored edit rounding mode is invalid/unrecognized
[SpecFail/Projection/NoIntersection] Point cannot project to target surface
[SpecFail/Hierarchy/CycleDetected] Circular beam dependencies
[SpecFail/Storage/SerializationError] JSON serialization failed
[SpecFail/Apply/ParameterReadOnly] Offset parameter is read-only

13.2 ISafeExecutor Wrapping

All entrypoints wrapped:

public Result Execute(ExternalCommandData commandData, ...)
{
    return _safeExecutor.Execute(() => 
    {
        // Command implementation
    });
}

14. Test Strategy

14.1 Test Categories (per test-audit skill)

Legitimate stubs (Revit API externals):

  • Document, Element, FamilyInstance
  • FilteredElementCollector
  • Parameter get/set
  • Transaction

Pure logic tests (no stubs):

  • ClosestPointServiceTests - geometry math
  • FramingHierarchyBuilderTests - graph construction
  • AdjustmentCalculatorTests - delta/rounding math
  • ErftStorageMapperTests - JSON round-trip
  • ErftPreviewSceneBuilderTests - mesh generation

14.2 Planned Test Cases

// ClosestPointServiceTests.cs
[Test] public void ProjectToSurface_SingleTriangle_ReturnsCorrectZ();
[Test] public void ProjectToSurface_PointAboveTriangle_ReturnsTrianglePlaneZ();
[Test] public void ProjectToSurface_PointOutsideTriangle_ReturnsEdgeProjection();
[Test] public void ProjectToSurface_MultipleFaces_ReturnsClosest();

// FramingHierarchyBuilderTests.cs
[Test] public void BuildGraph_ColumnAndBeamTouchingTop_CreatesConnection();
[Test] public void BuildGraph_BeamToBeam_DetectsSecondaryFraming();
[Test] public void BuildGraph_HybridBeam_ClassifiesBothEndpoints();
[Test] public void BuildGraph_NoDependencies_ReturnsEmptyGraph();

// AdjustmentCalculatorTests.cs
[Test] public void ApplyRounding_NearestQuarterInch_RoundsCorrectly();
[Test] public void ApplyRounding_UpMode_AlwaysRoundsUp();
[Test] public void ComputeElevationOnBeamLine_MidpointQuery_InterpolatesZ();

// ErftStorageMapperTests.cs
[Test] public void SerializeDeserialize_RoundTrips();
[Test] public void Deserialize_MissingField_ReturnsFailure();

15. Implementation Phases

Phase 1: Foundation (Estimated: 2-3 days)

Files to create:

  1. DBTools.ERFT.csproj - project file
  2. manifest.yml - tool manifest
  3. ErftToolModule.cs - registration
  4. ErftServiceExtensions.cs - bootstrap
  5. Common/Domain/*.cs - all domain models
  6. Common/Models/ErftSchemaDefinition.cs
  7. Shared/Contracts/*.cs - interfaces
  8. Icon: Assets/erft_icon.png

Verification: Project builds, tool appears in ribbon (does nothing yet)

Phase 2: Selection & Analysis (Estimated: 2-3 days)

Files to create:

  1. Shell/Revit/ErftTargetSelectionFilter.cs
  2. Shell/Revit/ErftSourceSelectionFilter.cs
  3. Features/Commands/ErftNewCommand.cs
  4. Features/Commands/ErftMode.cs
  5. Features/Analysis/Revit/FloorRoofGeometryService.cs
  6. Features/Analysis/Revit/ColumnQueryService.cs
  7. Features/Analysis/Revit/BeamQueryService.cs
  8. Features/Analysis/Logic/TargetSurfaceAnalyzer.cs

Verification: Can pick target and sources, extracts geometry

Phase 3: Hierarchy & Calculation (Estimated: 2-3 days)

Files to create:

  1. Features/Analysis/Logic/FramingHierarchyBuilder.cs
  2. Shared/Geometry/ClosestPointService.cs
  3. Features/Analysis/Logic/AdjustmentCalculator.cs
  4. Common/State/ErftPlanStore.cs

Tests to create:

  1. Tests/ClosestPointServiceTests.cs
  2. Tests/FramingHierarchyBuilderTests.cs
  3. Tests/AdjustmentCalculatorTests.cs

Verification: Computes correct deltas for sample scenarios

Phase 4: UI & Preview (Estimated: 3-4 days)

Files to create:

  1. Shell/UI/Views/ErftWindow.xaml
  2. Shell/UI/Views/ErftWindow.xaml.cs
  3. Shell/UI/ViewModels/ErftWindowViewModel.cs
  4. Features/Preview/Logic/ErftPreviewSceneBuilder.cs
  5. Features/Preview/UI/ErftPreview3DRenderer.cs
  6. Features/Commands/ErftRunner.cs
  7. Shell/Composition/ErftServiceCollectionExtensions.cs

Verification: UI opens, 3D preview renders, tolerance controls work

Phase 5: Apply & Persistence (Estimated: 2-3 days)

Files to create:

  1. Features/Apply/Logic/ErftOrchestrator.cs
  2. Features/Apply/Revit/ErftDomainWriter.cs
  3. Features/Apply/Revit/ErftStorageMapper.cs
  4. Common/Revit/ErftSystemRepository.cs
  5. Common/Domain/ErftStoredPlan.cs

Tests to create:

  1. Tests/ErftStorageMapperTests.cs

Verification: Apply modifies elements, ES data persisted

Phase 6: Edit Mode & Ribbon (Estimated: 1-2 days)

Files to create:

  1. Features/Commands/ErftContextualRibbonInjector.cs

Verification: Edit button appears, loads stored plan, re-applies

Phase 7: Polish & Documentation (Estimated: 1 day)

  • XML doc comments for all public members
  • Edge case handling
  • Final test coverage review
  • Session log

Total Estimated: 13-19 days


Appendix A: Icon Recommendations

The icon should convey "structural framing + sloped surface". Options:

  1. Steel beam with angled line - similar to existing structural icons
  2. Column with sloped roof outline - clearly shows the concept
  3. Grid pattern with slope indicator - abstract but clear

For now, use a placeholder 32x32 PNG with similar styling to existing icons.


Appendix B: XML Documentation Example

/// <summary>
/// Computes the vertical adjustment delta for a column based on closest-point projection
/// to the target floor/roof surface.
/// </summary>
/// <param name="column">The column item containing current position data.</param>
/// <param name="surface">The triangulated target surface geometry.</param>
/// <param name="tolerance">Rounding tolerance in feet.</param>
/// <param name="mode">Rounding direction (nearest, up, or down).</param>
/// <returns>
/// A <see cref="Result{T,E}"/> containing the computed adjustment or an error message
/// if projection fails.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="column"/> or <paramref name="surface"/> is null.
/// </exception>
/// <remarks>
/// The algorithm projects the column's current top point vertically to find the closest
/// point on the target surface's bottom face. The delta is then rounded according to
/// the specified tolerance and mode.
/// </remarks>
public Result<ColumnAdjustment, string> ComputeColumnAdjustment(
    ErftColumnItem column,
    TargetSurfaceGeometry surface,
    double tolerance,
    RoundingMode mode)

Appendix C: Key Decisions Summary

Decision Choice Rationale
Target source Host + Linked Maximum flexibility for coordination workflows
Source scope Host only Can only modify elements in editable document
Slope handling Closest point projection Handles complex geometry (hip roofs, warped slabs)
Beam-to-column tracking Auto-track Reduces manual work after column adjustments
Beam-to-beam connection Location line snap Matches Revit's native shift-drag behavior
Hybrid beams (col+beam) Independent endpoints Natural handling in hierarchy algorithm
Undo strategy Revit native Transaction group provides atomic undo
Rounding UI Presets + custom Balance of convenience and flexibility
Preview style Semi-transparent mesh Clear spatial context with status colors
Relationship detection Auto-detect Reduces user configuration burden
Multi-level Allowed Offset-only adjustments work across levels
Edit mode Full re-target Maximum flexibility for iterative workflows
Column adjustment Offset only Preserves level associations
Modification report Silent apply Reduces friction; Revit audit available