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
- Core Concepts
- User Workflow
- Data Models
- Architecture
- Selection & Validation
- Geometry Analysis
- Framing Hierarchy Detection
- Adjustment Algorithm
- 3D Preview System
- UI Components
- ExtensibleStorage Schema
- Contextual Ribbon Integration
- Error Handling
- Test Strategy
- 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:
- Select the target slope surface (floor/roof)
- Select the framing elements to adjust (columns and beams)
- Configure rounding tolerance (e.g., nearest 1/4")
- Preview the adjustments in 3D with status highlighting
- 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)
- User clicks "ERFT" ribbon button
- Yellow banner: "Select a LINKED floor or roof target (press ESC to switch to HOST target selection)."
- User picks linked floor/roof OR presses ESC to continue to host phase
- Yellow banner (host fallback): "Select a HOST floor or roof target (press ESC to cancel ERFT launch)."
- User picks host floor/roof OR presses ESC to cancel
- Yellow banner: "Select source structural columns and beams to adjust, then click Finish."
- User picks multiple columns/beams, clicks Finish
7b. Concrete filter: exclude any selected members whose Structural Material name matches
\bconcrete\b(case-insensitive). - Validation: Must have at least 1 column OR 1 beam framing to a column
- Progress overlay appears: "Analyzing geometry..."
- 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
- UI window opens with:
- 3D preview showing target surface + framing
- Tolerance controls (presets + custom)
- Round up/down toggle
- Element list with status indicators
- User adjusts tolerance as needed
- Preview updates in real-time
Source:
src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:2214. 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:231Source:src/Tools/Testing/ERFT/Features/Selection/ErftSelectionOrchestrator.cs:313
2.2 Edit Flow (via Contextual Ribbon)
- User selects elements with ERFT ExtensibleStorage data
- Contextual "Edit ERFT" button appears in Modify tab
- User clicks button
- Click handler validates edit preconditions:
- at least one ERFT
SystemIdis available in current selection - contextual
ExternalEventexists ExternalEvent.Raise()returnsAccepted
- at least one ERFT
- External-event handler runs edit orchestration through
ISafeExecutorwith notifier enabled (NotifyKind=Error) - Edit flow loads target context from DataStorage for selected
SystemId - 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 - Edit flow creates
AppRuntime.CreateRunScope(..., RevitRunScopeProfile.InlineUi)and resolvesErftRunnerfrom that run scope - UI opens with previous settings populated
- User can:
- Re-pick target floor/roof
- Adjust tolerances
- Re-apply changes
- Apply overwrites previous offsets (not cumulative)
Source:
src/Tools/Testing/ERFT/Features/Commands/ErftContextualRibbonInjector.cs:566Source: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:
- At least one column must be selected, OR
- At least one beam must frame directly to a column (detected via proximity)
- 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:98Source: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:23Source:src/Tools/Testing/ERFT/Common/Domain/TargetSurfaceGeometry.cs:52Source: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:
- Which beams frame INTO columns (endpoint within tolerance of column top)
- Which beams frame INTO other beams (endpoint on another beam's location line)
- 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 beToBeam - 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:
- Create temp placement of each unique type
- Extract end face geometry
- Store as
MemberProfileLoops - 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:
TargetSurfaceusing analyzed bottom-surface mesh (Vertices+TriangleIndices) - Fallback role:
TargetElementonly 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:139Source: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_StructuralColumnsorOST_StructuralFraming - ERFT schema lookup succeeds
Tag == "ERFT"Version == SchemaVersionSystemIdis 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
ExternalEventis not initialized, or whenExternalEvent.Raise()does not returnAccepted. - External-event execution is wrapped in
ISafeExecutorwith 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-clickInlineUirun scope viaAppRuntime.CreateRunScope(...), resolvesErftRunnerinside that scope, and clears/disposes the scopedIRevitRunScopeAccessor.Currentvalue infinally.- 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 mathFramingHierarchyBuilderTests- graph constructionAdjustmentCalculatorTests- delta/rounding mathErftStorageMapperTests- JSON round-tripErftPreviewSceneBuilderTests- 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:
DBTools.ERFT.csproj- project filemanifest.yml- tool manifestErftToolModule.cs- registrationErftServiceExtensions.cs- bootstrapCommon/Domain/*.cs- all domain modelsCommon/Models/ErftSchemaDefinition.csShared/Contracts/*.cs- interfaces- 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:
Shell/Revit/ErftTargetSelectionFilter.csShell/Revit/ErftSourceSelectionFilter.csFeatures/Commands/ErftNewCommand.csFeatures/Commands/ErftMode.csFeatures/Analysis/Revit/FloorRoofGeometryService.csFeatures/Analysis/Revit/ColumnQueryService.csFeatures/Analysis/Revit/BeamQueryService.csFeatures/Analysis/Logic/TargetSurfaceAnalyzer.cs
Verification: Can pick target and sources, extracts geometry
Phase 3: Hierarchy & Calculation (Estimated: 2-3 days)
Files to create:
Features/Analysis/Logic/FramingHierarchyBuilder.csShared/Geometry/ClosestPointService.csFeatures/Analysis/Logic/AdjustmentCalculator.csCommon/State/ErftPlanStore.cs
Tests to create:
Tests/ClosestPointServiceTests.csTests/FramingHierarchyBuilderTests.csTests/AdjustmentCalculatorTests.cs
Verification: Computes correct deltas for sample scenarios
Phase 4: UI & Preview (Estimated: 3-4 days)
Files to create:
Shell/UI/Views/ErftWindow.xamlShell/UI/Views/ErftWindow.xaml.csShell/UI/ViewModels/ErftWindowViewModel.csFeatures/Preview/Logic/ErftPreviewSceneBuilder.csFeatures/Preview/UI/ErftPreview3DRenderer.csFeatures/Commands/ErftRunner.csShell/Composition/ErftServiceCollectionExtensions.cs
Verification: UI opens, 3D preview renders, tolerance controls work
Phase 5: Apply & Persistence (Estimated: 2-3 days)
Files to create:
Features/Apply/Logic/ErftOrchestrator.csFeatures/Apply/Revit/ErftDomainWriter.csFeatures/Apply/Revit/ErftStorageMapper.csCommon/Revit/ErftSystemRepository.csCommon/Domain/ErftStoredPlan.cs
Tests to create:
Tests/ErftStorageMapperTests.cs
Verification: Apply modifies elements, ES data persisted
Phase 6: Edit Mode & Ribbon (Estimated: 1-2 days)
Files to create:
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:
- Steel beam with angled line - similar to existing structural icons
- Column with sloped roof outline - clearly shows the concept
- 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 |