diff --git a/Directory.Build.props b/Directory.Build.props
index 21fdecd..a09d7fb 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
- 0.9.0
+ 0.10.0
mahach666
Copyright (c) 2026 IOExtend
IfcIO
@@ -24,7 +24,7 @@
https://github.com/IOExtend/IfcIO
https://github.com/IOExtend/IfcIO
git
- ifc;ifc2x3;ifc4;step;iso-10303-21;bim;cad;brep;tessellation;glb;gltf;obj;ifczip;bcf;bcfzip;csg;mep;ids;ifcpatch;ifcselector;ifccsv;clash;diff;viewer;webgl;html-viewer;grids;measure;clash-grouping;buildingsmart
+ ifc;ifc2x3;ifc4;step;iso-10303-21;bim;cad;brep;tessellation;glb;gltf;obj;ifczip;bcf;bcfzip;sequence;4d;cost;5d;zones;csg;mep;ids;ifcpatch;ifcselector;ifccsv;clash;diff;viewer;webgl;html-viewer;grids;measure;clash-grouping;buildingsmart
README.md
icon.png
diff --git a/README.md b/README.md
index d43fa49..2afe730 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,23 @@ Built clean-room under the [IOExtend](https://github.com/IOExtend) organization
- Full revision comparison between two IFC models detecting added, deleted, and modified elements (3D geometry displacement, property set deltas, quantity variations, and storey re-assignments) conforming to `ifcdiff`. Direct export to JSON and BCF 2.1 archives.
- **Standalone 3D WebGL HTML Viewer (`IfcHtmlViewerExporter`)**:
- Export any IFC model to an interactive, self-contained single-file 3D HTML viewer (`.html`) with zero external scripts, CDNs, or server dependencies.
- - Includes orbit camera controls, live multi-axis section clipping planes (Cut X, Cut Y, Cut Z), spatial tree navigation, element property inspector, interactive 2-point 3D measurement tool, and 2D canvas overlay drawing of architectural grid axes and bubble labels.
+ - Includes orbit camera controls, live multi-axis section clipping planes (Cut X, Cut Y, Cut Z), spatial tree navigation, element property inspector, interactive 2-point 3D measurement tool, 2D canvas overlay drawing of architectural grid axes and bubble labels, and interactive 4D timeline simulation playback.
+- **4D Construction Sequencing & Temporal Simulation (`IfcWorkSchedule`, `IfcTask`, `IfcTaskTime`, `IFCRELSEQUENCE`)**:
+ - Full support for construction work schedules, WBS hierarchies, task durations, milestones, and predecessor/successor relationships (`FinishToStart`, `StartToStart`, `FinishToFinish`, `StartToFinish`).
+ - Temporal 4D simulation engine: query active tasks, completed tasks, overall progress ratio, and element states (`Planned`, `InProgress`, `Completed`, `NotStarted`) at any given simulation date.
+ - Interactive 4D timeline playback in the standalone HTML viewer (`ifcio view`).
+- **5D Cost Planning & Budgeting (`IfcCostSchedule`, `IfcCostItem`, `IfcCostValue`, `IFCAPPLIEDVALUE`)**:
+ - Hierarchical Cost Breakdown Structure (CBS), unit rates, quantities, and automatic cost rollup across tree nodes.
+ - Elements and physical quantities linked to cost items via `IFCRELASSIGNSTOCONTROL`.
+ - CLI command `ifcio cost` with CSV export.
+- **Spatial Zones (`IfcZone`, `IFCZONE`)**:
+ - Grouping architectural spaces and rooms into thermal, fire safety, acoustic, security, and ventilation zones.
+- **Bi-directional BCF 2.1 Archive Container (`BcfArchive`)**:
+ - Full in-memory or stream-based container for reading, inspecting, modifying, and writing buildingSMART BCF 2.1 zip archives (`.bcfzip`).
+ - High-level queries by element GUID (`FindTopicsByElement`), status (`FindTopicsByStatus`), and snapshot image attachments.
+ - CLI command `ifcio bcf` for inspection and topic listing.
+- **High-Level IfcOpenShell-Compatible API Facade (`IfcApi`)**:
+ - Pythonic, clean API: `IfcApi.Sequence`, `IfcApi.Cost`, `IfcApi.Spatial`, and `IfcApi.Root` for rapid automation.
- **Zero Dependencies**: Pure C# targeting `.NET 8` and `.NET 10`.
---
@@ -470,6 +486,69 @@ model.SaveHtmlViewer("BuildingModel.html", new IfcHtmlViewerOptions
});
```
+### 4D Construction Sequencing & Simulation
+
+```csharp
+using IfcIO.Model.Sequence;
+
+// Create 4D schedule with tasks, WBS, and element assignments
+var schedule = model.AddWorkSchedule("Tower Schedule");
+var task = schedule.AddTask("Construct Walls", "WBS-1.1", new DateTime(2026, 2, 1), new DateTime(2026, 3, 1));
+task.AssignElement(wall);
+
+// Query temporal state during construction
+var activeTasks = schedule.GetActiveTasksAt(new DateTime(2026, 2, 15));
+var state = schedule.GetElementStateAt(wall, new DateTime(2026, 2, 15)); // => IfcElementSimulationState.InProgress
+```
+
+### 5D Cost Planning & CBS Budgeting
+
+```csharp
+using IfcIO.Model.Cost;
+
+// Create 5D cost schedule with CBS items, unit rates, and quantities
+var budget = model.AddCostSchedule("Master Budget", "USD", IfcCostScheduleType.Budget);
+var masonryItem = budget.AddCostItem("Wall Masonry", "04.20.00", unitRate: 150.0, quantity: 200.0, unitBasis: "m2");
+masonryItem.AssignElement(wall);
+
+Console.WriteLine($"Total Item Cost: {masonryItem.TotalCost} USD"); // => 30000 USD
+Console.WriteLine($"Grand Budget: {budget.GetTotalCost()} USD");
+```
+
+### High-Level IfcOpenShell-Compatible API Facade (`IfcApi`)
+
+```csharp
+using IfcIO.Api;
+
+var model = IfcApi.Root.CreateProject("Commercial Office", schema: IfcIO.Schema.IfcSchema.Ifc4);
+var storey = IfcApi.Root.CreateStorey(model, "Ground Floor", 0.0);
+var wall = IfcApi.Root.CreateWall(model, storey, "Exterior Wall");
+
+// High-level 4D and 5D workflows
+var ws = IfcApi.Sequence.AddWorkSchedule(model, "Main Schedule");
+var task = IfcApi.Sequence.AddTask(model, ws, "Wall Framing", new DateTime(2026, 1, 10), new DateTime(2026, 2, 10));
+IfcApi.Sequence.AssignProcess(task, wall);
+
+var cs = IfcApi.Cost.AddCostSchedule(model, "Base Budget");
+var ci = IfcApi.Cost.AddCostItem(cs, "Framing Labor", unitRate: 75.0, quantity: 120.0);
+IfcApi.Cost.AssignCostControl(ci, wall);
+```
+
+### buildingSMART BCF 2.1 Archive Container (`BcfArchive`)
+
+```csharp
+using IfcIO.Bcf;
+
+// Read, query, and modify BCF zip archives directly
+using var bcf = BcfArchive.Open("Issues.bcfzip");
+var activeTopics = bcf.FindTopicsByStatus("Active");
+var wallIssues = bcf.FindTopicsByElement(wall.GlobalId);
+
+// Add topic and export updated archive
+bcf.AddTopic("Verify concrete cover thickness", "Architectural Review", priority: "High");
+bcf.Save("Issues_Updated.bcfzip");
+```
+
---
## Global CLI Tool (`ifcio`)
@@ -486,6 +565,15 @@ dotnet tool install --global IfcIO.Cli
# Inspect model metadata, storeys, spaces, systems, and element distribution
ifcio info Building.ifc --report --spaces --systems --export-grids Grids.xml
+# 4D construction schedule temporal simulation and CSV export
+ifcio schedule Building.ifc --sim-date 2026-06-01 --export-csv ScheduleReport.csv
+
+# 5D cost breakdown and budget reporting
+ifcio cost Building.ifc --export-csv CostBreakdown.csv
+
+# Inspect BCF 2.1 archive topics and viewpoints
+ifcio bcf Issues.bcfzip
+
# Query elements matching IfcOpenShell-style selector
ifcio query Building.ifc "IfcWall[LoadBearing=TRUE]"
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 5e0874a..b1f2fbb 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -83,6 +83,62 @@ IfcGlbExporter.Export(model, "Building.glb");
IfcObjExporter.Export(model, "Building.obj");
```
+### 4D Construction Scheduling & Timeline Simulation
+
+```csharp
+using IfcIO;
+using IfcIO.Model.Sequence;
+
+var model = IfcModel.Open("Project.ifc");
+
+// Access work schedules and tasks
+var schedule = model.WorkSchedules.FirstOrDefault();
+if (schedule is not null)
+{
+ Console.WriteLine($"Schedule: {schedule.Name}");
+ DateTime simDate = DateTime.UtcNow;
+
+ // Temporal 4D simulation
+ var activeTasks = schedule.GetActiveTasksAt(simDate);
+ double progress = schedule.GetProgressAt(simDate);
+ Console.WriteLine($"Progress at {simDate:d}: {progress:P1} ({activeTasks.Count} active tasks)");
+}
+```
+
+### 5D Cost Planning & CBS Budgeting
+
+```csharp
+using IfcIO;
+using IfcIO.Model.Cost;
+
+var model = IfcModel.Open("Project.ifc");
+
+// Inspect 5D cost schedules
+foreach (var costSchedule in model.CostSchedules)
+{
+ Console.WriteLine($"Cost Plan: {costSchedule.Name} - Grand Total: {costSchedule.GetTotalCost():N2} {costSchedule.Currency}");
+ foreach (var item in costSchedule.AllCostItems)
+ {
+ Console.WriteLine($" - [{item.CostCode}] {item.Name}: {item.TotalCost:N2} {costSchedule.Currency}");
+ }
+}
+```
+
+### BCF 2.1 Archive Container
+
+```csharp
+using IfcIO.Bcf;
+
+// Open a BCF archive directly from disk or stream
+using var archive = BcfArchive.Open("Issues.bcfzip");
+Console.WriteLine($"Found {archive.Topics.Count} coordination topic(s):");
+
+foreach (var topic in archive.Topics)
+{
+ Console.WriteLine($"[{topic.TopicStatus}] {topic.Title} (Priority: {topic.Priority})");
+}
+```
+
---
## CLI Usage
@@ -93,6 +149,15 @@ Run standard operations from your terminal:
# Display summary info, schema version, and element counts
ifcio info Building.ifc
+# Run 4D construction schedule simulation at a specific date
+ifcio schedule Building.ifc --sim-date 2026-06-01 --export-csv ScheduleReport.csv
+
+# Inspect 5D cost budgets and export tabular breakdown
+ifcio cost Building.ifc --export-csv CostBreakdown.csv
+
+# Inspect BCF coordination topics
+ifcio bcf Issues.bcfzip
+
# Convert IFC to binary glTF 2.0 and Wavefront OBJ
ifcio export Building.ifc --glb scene.glb --obj scene.obj
diff --git a/docs/public-api.md b/docs/public-api.md
index ddb1076..52cb423 100644
--- a/docs/public-api.md
+++ b/docs/public-api.md
@@ -42,3 +42,27 @@ The `IfcIO` package is the supported surface listed here.
| `IfcDiffEngine` | Revision comparison engine detecting added, deleted, and modified IFC elements and geometry shifts. |
| `IfcValidator` | Structural STEP syntax and buildingSMART schema conformance validator. |
| `IfcBcfExporter` | buildingSMART BIM Collaboration Format (BCF 2.1 `.bcfzip`) issue and viewpoint generator. |
+| `BcfArchive` | Bidirectional BCF 2.1 archive container for reading, querying, editing, and saving `.bcfzip` files. |
+
+## 4D Construction Sequencing & Temporal Simulation
+
+| Type | Description |
+|---|---|
+| `IfcWorkSchedule` | Construction schedule container managing WBS hierarchies, baseline dates, temporal simulation, and progress tracking. |
+| `IfcTask` | Individual construction activity or milestone with duration, planned/actual dates, and predecessor/successor links. |
+| `IfcTaskTime` | Time entity conforming to `IFCTASKTIME` capturing early/late dates, durations, completion ratio, and milestones. |
+
+## 5D Cost Planning & CBS Budgeting
+
+| Type | Description |
+|---|---|
+| `IfcCostSchedule` | 5D budget, tender, estimate, or cost plan conforming to `IFCCOSTSCHEDULE` with currency and rollups. |
+| `IfcCostItem` | Cost breakdown structure (CBS) node with unit rates, quantities, linked elements, and sub-items. |
+| `IfcCostValue` | Applied monetary value breakdown conforming to `IFCAPPLIEDVALUE`. |
+
+## High-Level IfcOpenShell-Compatible API Facade
+
+| Type | Description |
+|---|---|
+| `IfcApi` | Unified top-level facade conforming to IfcOpenShell high-level API conventions (`IfcApi.Sequence`, `IfcApi.Cost`, `IfcApi.Spatial`, `IfcApi.Root`). |
+| `IfcZone` | Functional spatial zone grouping spaces into thermal, fire safety, acoustic, or ventilation zones (`IFCZONE`). |
diff --git a/src/IfcIO.Cli/Program.cs b/src/IfcIO.Cli/Program.cs
index fa59e12..d98616e 100644
--- a/src/IfcIO.Cli/Program.cs
+++ b/src/IfcIO.Cli/Program.cs
@@ -43,6 +43,9 @@ public static int Main(string[] args)
"import-csv" => ExecuteImportCsv(cmdArgs),
"clash" => ExecuteClash(cmdArgs),
"diff" => ExecuteDiff(cmdArgs),
+ "schedule" or "4d" => ExecuteSchedule(cmdArgs),
+ "cost" or "5d" => ExecuteCost(cmdArgs),
+ "bcf" => ExecuteBcf(cmdArgs),
"view" or "html" => ExecuteView(cmdArgs),
_ when File.Exists(args[0]) => ExecuteDefault(args),
_ => PrintUnknownCommand(command)
@@ -76,6 +79,9 @@ private static void PrintUsage()
import-csv -o Update property sets from a CSV spreadsheet
clash [fileB] Run 3D geometric clash detection (cross-model or self-clash)
diff Compare two IFC model revisions (added, deleted, modified)
+ schedule Inspect 4D construction schedule, simulation progress, and export CSV
+ cost Inspect 5D cost schedule, budgets, takeoffs, and export CSV
+ bcf Inspect buildingSMART BCF 2.1 zip archive topics, viewpoints, comments
view [-o out.html] Export standalone 3D WebGL HTML viewer (.html)
Clash & Diff Options:
@@ -1104,4 +1110,208 @@ private static int ExecuteView(string[] args)
Console.ResetColor();
return 0;
}
+
+ private static int ExecuteSchedule(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Console.Error.WriteLine("Error: Missing IFC file path. Usage: ifcio schedule [--sim-date ] [--export-csv ]");
+ return 1;
+ }
+
+ string inputPath = args[0];
+ if (!File.Exists(inputPath))
+ {
+ Console.Error.WriteLine($"Error: File '{inputPath}' not found.");
+ return 1;
+ }
+
+ var model = IfcModel.Open(inputPath);
+ if (model.WorkSchedules.Count == 0)
+ {
+ Console.WriteLine($"Model '{Path.GetFileName(inputPath)}' contains no 4D construction work schedules.");
+ return 0;
+ }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"4D Construction Work Schedules: {model.WorkSchedules.Count} schedule(s)");
+ Console.ResetColor();
+
+ string? simDateStr = GetOptionValue(args, "--sim-date");
+ DateTime? simDate = null;
+ if (!string.IsNullOrEmpty(simDateStr) && DateTime.TryParse(simDateStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDate))
+ {
+ simDate = parsedDate;
+ }
+
+ foreach (var ws in model.WorkSchedules)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" Schedule: {ws.Name} (GlobalId: {ws.GlobalId})");
+ Console.ResetColor();
+ if (ws.StartTime.HasValue || ws.FinishTime.HasValue)
+ {
+ Console.WriteLine($" Planned: {ws.StartTime:yyyy-MM-dd} -> {ws.FinishTime:yyyy-MM-dd}");
+ }
+ Console.WriteLine($" Total tasks: {ws.AllTasks.Count()}");
+
+ if (simDate.HasValue)
+ {
+ double prog = ws.GetProgressAt(simDate.Value);
+ var active = ws.GetActiveTasksAt(simDate.Value);
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($" Simulation Progress at {simDate.Value:yyyy-MM-dd}: {prog:F1}% (Active tasks: {active.Count})");
+ Console.ResetColor();
+ foreach (var at in active)
+ {
+ Console.WriteLine($" * [{at.Identification ?? "WBS"}] {at.Name} ({at.TaskType}) Elements: {at.AssignedElements.Count}");
+ }
+ }
+ else
+ {
+ foreach (var task in ws.AllTasks)
+ {
+ var start = task.Time?.ActualStartDate ?? task.Time?.PlannedStartDate;
+ var finish = task.Time?.ActualFinishDate ?? task.Time?.PlannedFinishDate;
+ string dates = (start.HasValue && finish.HasValue) ? $"[{start:yyyy-MM-dd} -> {finish:yyyy-MM-dd}]" : "";
+ Console.WriteLine($" - [{task.Identification ?? "Task"}] {task.Name,-30} {dates,-26} Elements: {task.AssignedElements.Count}");
+ }
+ }
+ }
+
+ string? exportCsvPath = GetOptionValue(args, "--export-csv");
+ if (!string.IsNullOrEmpty(exportCsvPath))
+ {
+ using var writer = new StreamWriter(exportCsvPath, false, System.Text.Encoding.UTF8);
+ writer.WriteLine("Schedule;TaskName;WBS;TaskType;StartDate;FinishDate;AssignedElements");
+ foreach (var ws in model.WorkSchedules)
+ {
+ foreach (var t in ws.AllTasks)
+ {
+ var start = t.Time?.ActualStartDate ?? t.Time?.PlannedStartDate;
+ var finish = t.Time?.ActualFinishDate ?? t.Time?.PlannedFinishDate;
+ writer.WriteLine($"{ws.Name};{t.Name};{t.Identification};{t.TaskType};{start:yyyy-MM-dd};{finish:yyyy-MM-dd};{t.AssignedElements.Count}");
+ }
+ }
+ Console.WriteLine($"Schedule exported to CSV: '{exportCsvPath}'");
+ }
+
+ return 0;
+ }
+
+ private static int ExecuteCost(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Console.Error.WriteLine("Error: Missing IFC file path. Usage: ifcio cost [--export-csv ]");
+ return 1;
+ }
+
+ string inputPath = args[0];
+ if (!File.Exists(inputPath))
+ {
+ Console.Error.WriteLine($"Error: File '{inputPath}' not found.");
+ return 1;
+ }
+
+ var model = IfcModel.Open(inputPath);
+ if (model.CostSchedules.Count == 0)
+ {
+ Console.WriteLine($"Model '{Path.GetFileName(inputPath)}' contains no 5D cost schedules.");
+ return 0;
+ }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"5D Cost Schedules & Budgets: {model.CostSchedules.Count} schedule(s)");
+ Console.WriteLine($"Grand Total Cost: {model.CalculateTotalCost():N2}");
+ Console.ResetColor();
+
+ foreach (var cs in model.CostSchedules)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" Schedule: {cs.Name} ({cs.ScheduleType}) - Currency: {cs.Currency}");
+ Console.WriteLine($" Total Cost: {cs.GetTotalCost():N2} {cs.Currency}");
+ Console.ResetColor();
+
+ foreach (var item in cs.AllCostItems)
+ {
+ string rateStr = item.UnitRate.HasValue ? $"Rate: {item.UnitRate.Value:N2}/{item.UnitBasis ?? "unit"}" : "";
+ string qtyStr = item.Quantity.HasValue ? $"Qty: {item.Quantity.Value:N2}" : "";
+ Console.WriteLine($" - [{item.CostCode ?? "CBS"}] {item.Name,-28} Cost: {item.TotalCost,12:N2} {cs.Currency} | {rateStr} {qtyStr} | Elements: {item.AssignedElements.Count}");
+ }
+ }
+
+ string? exportCsvPath = GetOptionValue(args, "--export-csv");
+ if (!string.IsNullOrEmpty(exportCsvPath))
+ {
+ using var writer = new StreamWriter(exportCsvPath, false, System.Text.Encoding.UTF8);
+ writer.WriteLine("Schedule;CostCode;Name;UnitRate;Quantity;UnitBasis;TotalCost;Currency;AssignedElements");
+ foreach (var cs in model.CostSchedules)
+ {
+ foreach (var item in cs.AllCostItems)
+ {
+ writer.WriteLine($"{cs.Name};{item.CostCode};{item.Name};{item.UnitRate?.ToString(CultureInfo.InvariantCulture)};{item.Quantity?.ToString(CultureInfo.InvariantCulture)};{item.UnitBasis};{item.TotalCost.ToString(CultureInfo.InvariantCulture)};{cs.Currency};{item.AssignedElements.Count}");
+ }
+ }
+ Console.WriteLine($"Cost schedule exported to CSV: '{exportCsvPath}'");
+ }
+
+ return 0;
+ }
+
+ private static int ExecuteBcf(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ Console.Error.WriteLine("Error: Missing BCF command. Usage: ifcio bcf ");
+ return 1;
+ }
+
+ string subCmd = args[0].ToLowerInvariant();
+ string bcfPath = (subCmd is "read" or "info" && args.Length > 1) ? args[1] : args[0];
+
+ if (!File.Exists(bcfPath))
+ {
+ Console.Error.WriteLine($"Error: BCF archive '{bcfPath}' not found.");
+ return 1;
+ }
+
+ var archive = Bcf.BcfArchive.Open(bcfPath);
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("================================================================================");
+ Console.WriteLine($" buildingSMART BCF Archive: {Path.GetFileName(bcfPath)}");
+ Console.WriteLine("================================================================================");
+ Console.ResetColor();
+
+ Console.WriteLine($" Total Topics: {archive.Topics.Count}");
+ foreach (var topic in archive.Topics)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($" Topic: [{topic.TopicStatus}] {topic.Title} ({topic.TopicType})");
+ Console.ResetColor();
+ Console.WriteLine($" Guid: {topic.TopicGuid}");
+ Console.WriteLine($" Priority: {topic.Priority}");
+ Console.WriteLine($" Author: {topic.CreationAuthor} on {topic.CreationDate:yyyy-MM-dd HH:mm}");
+ if (!string.IsNullOrEmpty(topic.AssignedTo))
+ {
+ Console.WriteLine($" Assigned To: {topic.AssignedTo}");
+ }
+ if (!string.IsNullOrEmpty(topic.Description))
+ {
+ Console.WriteLine($" Description: {topic.Description}");
+ }
+ Console.WriteLine($" Viewpoints: {topic.Viewpoints.Count}");
+ Console.WriteLine($" Comments: {topic.Comments.Count}");
+
+ foreach (var comment in topic.Comments)
+ {
+ Console.WriteLine($" * [{comment.Date:yyyy-MM-dd HH:mm}] {comment.Author}: {comment.Comment}");
+ }
+ }
+
+ return 0;
+ }
}
diff --git a/src/IfcIO/Api/IfcApi.cs b/src/IfcIO/Api/IfcApi.cs
new file mode 100644
index 0000000..997229b
--- /dev/null
+++ b/src/IfcIO/Api/IfcApi.cs
@@ -0,0 +1,206 @@
+using IfcIO.Geometry;
+using IfcIO.Model;
+using IfcIO.Model.Cost;
+using IfcIO.Model.Sequence;
+using IfcIO.Schema;
+
+namespace IfcIO.Api;
+
+///
+/// High-level API facade inspired by IfcOpenShell API (ifcopenshell.api), providing granular,
+/// domain-driven operations for construction scheduling (4D), cost planning (5D), spatial hierarchy, and BIM modeling.
+///
+public static class IfcApi
+{
+ /// 4D Construction sequencing and work schedule operations.
+ public static class Sequence
+ {
+ /// Creates and adds a new work schedule to the model.
+ public static IfcWorkSchedule AddWorkSchedule(IfcModel model, string name, string? description = null)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+ return model.AddWorkSchedule(name, description);
+ }
+
+ /// Creates and adds a task to a work schedule.
+ public static IfcTask AddTask(
+ IfcWorkSchedule schedule,
+ string name,
+ string? identification = null,
+ DateTime? start = null,
+ DateTime? finish = null,
+ IfcTaskType taskType = IfcTaskType.Construction)
+ {
+ ArgumentNullException.ThrowIfNull(schedule);
+ return schedule.AddTask(name, identification, start, finish, taskType);
+ }
+
+ /// Assigns one or more building elements to a construction or demolition task.
+ public static void AssignProcess(IfcTask task, params IfcElement[] elements)
+ {
+ ArgumentNullException.ThrowIfNull(task);
+ ArgumentNullException.ThrowIfNull(elements);
+ task.AssignElements(elements);
+ }
+
+ /// Creates a sequence dependency link between a predecessor and successor task.
+ public static IfcTaskRelationship AddSequence(
+ IfcTask predecessor,
+ IfcTask successor,
+ IfcSequenceType sequenceType = IfcSequenceType.FinishToStart,
+ TimeSpan? lag = null)
+ {
+ ArgumentNullException.ThrowIfNull(predecessor);
+ ArgumentNullException.ThrowIfNull(successor);
+ return predecessor.AddSuccessor(successor, sequenceType, lag);
+ }
+ }
+
+ /// 5D Cost planning and budget estimation operations.
+ public static class Cost
+ {
+ /// Creates and adds a cost schedule to the model.
+ public static IfcCostSchedule AddCostSchedule(
+ IfcModel model,
+ string name,
+ string currency = "USD",
+ IfcCostScheduleType type = IfcCostScheduleType.CostPlan)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+ return model.AddCostSchedule(name, currency, type);
+ }
+
+ /// Creates and adds a cost item to a cost schedule.
+ public static IfcCostItem AddCostItem(
+ IfcCostSchedule schedule,
+ string name,
+ string? costCode = null,
+ double? unitRate = null,
+ double? quantity = null,
+ string? unitBasis = null)
+ {
+ ArgumentNullException.ThrowIfNull(schedule);
+ return schedule.AddCostItem(name, costCode, unitRate, quantity, unitBasis);
+ }
+
+ /// Assigns one or more building elements to a cost item.
+ public static void AssignCostControl(IfcCostItem costItem, params IfcElement[] elements)
+ {
+ ArgumentNullException.ThrowIfNull(costItem);
+ ArgumentNullException.ThrowIfNull(elements);
+ foreach (var el in elements) costItem.AssignElement(el);
+ }
+
+ /// Assigns a physical quantity to a cost item for automatic rate calculations.
+ public static void AssignQuantity(IfcCostItem costItem, IfcPhysicalQuantity quantity)
+ {
+ ArgumentNullException.ThrowIfNull(costItem);
+ ArgumentNullException.ThrowIfNull(quantity);
+ costItem.AssignQuantity(quantity);
+ }
+ }
+
+ /// Spatial organization, storeys, and zone operations.
+ public static class Spatial
+ {
+ /// Creates and adds a spatial zone grouping one or more spaces.
+ public static IfcZone AddZone(IfcModel model, string name, string? zoneType = null, params IfcSpace[] spaces)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+ return model.AddZone(name, zoneType, spaces);
+ }
+
+ /// Assigns building elements to a building storey.
+ public static void AssignContainer(IfcBuildingStorey storey, params IfcElement[] elements)
+ {
+ ArgumentNullException.ThrowIfNull(storey);
+ ArgumentNullException.ThrowIfNull(elements);
+ foreach (var el in elements)
+ {
+ if (!storey.Elements.Contains(el))
+ {
+ storey.Elements.Add(el);
+ }
+ }
+ }
+ }
+
+ /// Root model and building element factory operations.
+ public static class Root
+ {
+ /// Initializes a new IFC project model.
+ public static IfcModel CreateProject(string projectName, IfcSchema schema = IfcSchema.Ifc4) =>
+ IfcModel.Create(projectName, schema);
+
+ /// Creates and registers a building storey.
+ public static IfcBuildingStorey CreateStorey(IfcModel model, string name, double elevation = 0.0)
+ {
+ ArgumentNullException.ThrowIfNull(model);
+ return model.AddStorey(name, elevation);
+ }
+
+ /// Creates and adds an IFCWALL element to a storey.
+ public static IfcElement CreateWall(IfcBuildingStorey storey, string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ ArgumentNullException.ThrowIfNull(storey);
+ var wall = new IfcElement
+ {
+ Id = storey.Elements.Count + 1,
+ Name = name,
+ IfcType = "IFCWALL",
+ Mesh = mesh,
+ Material = material
+ };
+ storey.Elements.Add(wall);
+ return wall;
+ }
+
+ /// Creates and adds an IFCSLAB element to a storey.
+ public static IfcElement CreateSlab(IfcBuildingStorey storey, string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ ArgumentNullException.ThrowIfNull(storey);
+ var slab = new IfcElement
+ {
+ Id = storey.Elements.Count + 1,
+ Name = name,
+ IfcType = "IFCSLAB",
+ Mesh = mesh,
+ Material = material
+ };
+ storey.Elements.Add(slab);
+ return slab;
+ }
+
+ /// Creates and adds an IFCCOLUMN element to a storey.
+ public static IfcElement CreateColumn(IfcBuildingStorey storey, string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ ArgumentNullException.ThrowIfNull(storey);
+ var column = new IfcElement
+ {
+ Id = storey.Elements.Count + 1,
+ Name = name,
+ IfcType = "IFCCOLUMN",
+ Mesh = mesh,
+ Material = material
+ };
+ storey.Elements.Add(column);
+ return column;
+ }
+
+ /// Creates and adds an IFCBEAM element to a storey.
+ public static IfcElement CreateBeam(IfcBuildingStorey storey, string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ ArgumentNullException.ThrowIfNull(storey);
+ var beam = new IfcElement
+ {
+ Id = storey.Elements.Count + 1,
+ Name = name,
+ IfcType = "IFCBEAM",
+ Mesh = mesh,
+ Material = material
+ };
+ storey.Elements.Add(beam);
+ return beam;
+ }
+ }
+}
diff --git a/src/IfcIO/Bcf/BcfArchive.cs b/src/IfcIO/Bcf/BcfArchive.cs
new file mode 100644
index 0000000..53b83fa
--- /dev/null
+++ b/src/IfcIO/Bcf/BcfArchive.cs
@@ -0,0 +1,74 @@
+namespace IfcIO.Bcf;
+
+///
+/// A managed buildingSMART BIM Collaboration Format (BCF 2.1) archive container holding issues, viewpoints, and comments.
+///
+public sealed class BcfArchive
+{
+ /// The collection of coordination topics contained in this archive.
+ public List Topics { get; } = [];
+
+ /// Optional project name or document title associated with this issue archive.
+ public string? ProjectName { get; set; }
+
+ public BcfArchive() { }
+
+ public BcfArchive(string? projectName = null)
+ {
+ ProjectName = projectName;
+ }
+
+ /// Opens and parses a BCF 2.1 (.bcfzip) file from disk.
+ public static BcfArchive Open(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+ using var fs = File.OpenRead(path);
+ return Open(fs);
+ }
+
+ /// Opens and parses a BCF 2.1 (.bcfzip) zip archive from a stream.
+ public static BcfArchive Open(Stream stream)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ var topics = BcfZipExporter.Read(stream);
+ var archive = new BcfArchive();
+ archive.Topics.AddRange(topics);
+ return archive;
+ }
+
+ /// Saves all topics into a BCF 2.1 (.bcfzip) file on disk.
+ public void Save(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+ BcfZipExporter.Save(path, Topics, ProjectName);
+ }
+
+ /// Saves all topics into a stream as a BCF 2.1 (.bcfzip) zip archive.
+ public void Save(Stream stream)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ BcfZipExporter.Write(stream, Topics, ProjectName);
+ }
+
+ /// Creates and adds a new topic to the archive.
+ public BcfTopic AddTopic(string title, string? description = null, string topicType = "Issue", string priority = "Normal")
+ {
+ var topic = new BcfTopic(title, description, topicType, priority);
+ Topics.Add(topic);
+ return topic;
+ }
+
+ /// Finds all topics that reference or select a given building element by its IFC GlobalId.
+ public IReadOnlyList FindTopicsByElement(string ifcGuid)
+ {
+ ArgumentNullException.ThrowIfNull(ifcGuid);
+ return Topics.Where(t => t.Viewpoints.Any(v => v.SelectedElementGuids.Contains(ifcGuid, StringComparer.OrdinalIgnoreCase))).ToList();
+ }
+
+ /// Finds all topics matching a given status (e.g. "Open", "Resolved", "Closed").
+ public IReadOnlyList FindTopicsByStatus(string status)
+ {
+ ArgumentNullException.ThrowIfNull(status);
+ return Topics.Where(t => string.Equals(t.TopicStatus, status, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+}
diff --git a/src/IfcIO/Bcf/BcfTypes.cs b/src/IfcIO/Bcf/BcfTypes.cs
index 7d85237..b3da873 100644
--- a/src/IfcIO/Bcf/BcfTypes.cs
+++ b/src/IfcIO/Bcf/BcfTypes.cs
@@ -23,8 +23,12 @@ public sealed class BcfViewpoint
public Geometry.IfcVector3? CameraDirection { get; set; }
public Geometry.IfcVector3? CameraUpVector { get; set; }
public double? FieldOfView { get; set; }
+ public string? SnapshotFileName { get; set; }
+ public byte[]? SnapshotData { get; set; }
+ public bool DefaultVisibility { get; set; } = true;
public List SelectedElementGuids { get; } = [];
public List VisibleElementGuids { get; } = [];
+ public List HiddenElementGuids { get; } = [];
}
///
diff --git a/src/IfcIO/Bcf/BcfZipExporter.cs b/src/IfcIO/Bcf/BcfZipExporter.cs
index 466aed8..432254f 100644
--- a/src/IfcIO/Bcf/BcfZipExporter.cs
+++ b/src/IfcIO/Bcf/BcfZipExporter.cs
@@ -82,6 +82,16 @@ public static void Write(Stream stream, IEnumerable topics, string? pr
{
WriteViewpointXml(xml, vp);
}
+
+ // Write snapshot if provided
+ if (vp.SnapshotData is not null && vp.SnapshotData.Length > 0)
+ {
+ string snapFileName = vIdx == 0 ? "snapshot.png" : $"snapshot_{vp.ViewpointGuid:D}.png";
+ vp.SnapshotFileName = snapFileName;
+ var snapEntry = archive.CreateEntry($"{topicFolder}/{snapFileName}", CompressionLevel.Optimal);
+ using var snapStream = snapEntry.Open();
+ snapStream.Write(vp.SnapshotData);
+ }
}
}
}
@@ -183,6 +193,18 @@ public static List Read(Stream stream)
}
}
+ // Read snapshots
+ var snapEntry = archive.GetEntry($"{folder}/snapshot.png") ??
+ archive.Entries.FirstOrDefault(e => e.FullName.StartsWith($"{folder}/snapshot", StringComparison.OrdinalIgnoreCase) && e.FullName.EndsWith(".png", StringComparison.OrdinalIgnoreCase));
+ if (snapEntry is not null && topic.Viewpoints.Count > 0)
+ {
+ using var snapStream = snapEntry.Open();
+ using var ms = new MemoryStream();
+ snapStream.CopyTo(ms);
+ topic.Viewpoints[0].SnapshotData = ms.ToArray();
+ topic.Viewpoints[0].SnapshotFileName = Path.GetFileName(snapEntry.FullName);
+ }
+
result.Add(topic);
}
@@ -222,6 +244,10 @@ private static void WriteMarkupXml(XmlWriter xml, BcfTopic topic, string project
xml.WriteStartElement("Viewpoints");
xml.WriteAttributeString("Guid", vp.ViewpointGuid.ToString("D"));
xml.WriteElementString("Viewpoint", vpFileName);
+ if (!string.IsNullOrEmpty(vp.SnapshotFileName))
+ {
+ xml.WriteElementString("Snapshot", vp.SnapshotFileName);
+ }
xml.WriteEndElement();
}
@@ -285,17 +311,39 @@ private static void WriteViewpointXml(XmlWriter xml, BcfViewpoint vp)
xml.WriteEndElement(); // PerspectiveCamera
}
- if (vp.SelectedElementGuids.Count > 0)
+ if (vp.SelectedElementGuids.Count > 0 || vp.HiddenElementGuids.Count > 0 || vp.VisibleElementGuids.Count > 0 || !vp.DefaultVisibility)
{
xml.WriteStartElement("Components");
- xml.WriteStartElement("Selection");
- foreach (var guid in vp.SelectedElementGuids)
+ if (!vp.DefaultVisibility || vp.HiddenElementGuids.Count > 0 || vp.VisibleElementGuids.Count > 0)
{
- xml.WriteStartElement("Component");
- xml.WriteAttributeString("IfcGuid", guid);
- xml.WriteEndElement();
+ xml.WriteStartElement("Visibility");
+ xml.WriteAttributeString("DefaultVisibility", vp.DefaultVisibility ? "true" : "false");
+ var exceptions = vp.DefaultVisibility ? vp.HiddenElementGuids : vp.VisibleElementGuids;
+ if (exceptions.Count > 0)
+ {
+ xml.WriteStartElement("Exceptions");
+ foreach (var guid in exceptions)
+ {
+ xml.WriteStartElement("Component");
+ xml.WriteAttributeString("IfcGuid", guid);
+ xml.WriteEndElement();
+ }
+ xml.WriteEndElement(); // Exceptions
+ }
+ xml.WriteEndElement(); // Visibility
+ }
+
+ if (vp.SelectedElementGuids.Count > 0)
+ {
+ xml.WriteStartElement("Selection");
+ foreach (var guid in vp.SelectedElementGuids)
+ {
+ xml.WriteStartElement("Component");
+ xml.WriteAttributeString("IfcGuid", guid);
+ xml.WriteEndElement();
+ }
+ xml.WriteEndElement(); // Selection
}
- xml.WriteEndElement(); // Selection
xml.WriteEndElement(); // Components
}
@@ -351,7 +399,30 @@ private static void WriteViewpointXml(XmlWriter xml, BcfViewpoint vp)
}
}
- foreach (var c in root.FindDescendants("Component"))
+ var vis = root.FindDescendants("Visibility").FirstOrDefault();
+ if (vis is not null)
+ {
+ string? defVis = (string?)vis.Attribute("DefaultVisibility");
+ if (bool.TryParse(defVis, out bool dv))
+ {
+ vp.DefaultVisibility = dv;
+ }
+ var ex = vis.FindChild("Exceptions");
+ if (ex is not null)
+ {
+ foreach (var c in ex.FindDescendants("Component"))
+ {
+ string? g = (string?)c.Attribute("IfcGuid");
+ if (!string.IsNullOrEmpty(g))
+ {
+ if (vp.DefaultVisibility) vp.HiddenElementGuids.Add(g);
+ else vp.VisibleElementGuids.Add(g);
+ }
+ }
+ }
+ }
+
+ foreach (var c in root.FindDescendants("Selection").SelectMany(s => s.FindDescendants("Component")))
{
string? ifcGuid = (string?)c.Attribute("IfcGuid");
if (!string.IsNullOrEmpty(ifcGuid))
diff --git a/src/IfcIO/Core/StepWriter.cs b/src/IfcIO/Core/StepWriter.cs
index 65934af..767ebe7 100644
--- a/src/IfcIO/Core/StepWriter.cs
+++ b/src/IfcIO/Core/StepWriter.cs
@@ -81,6 +81,10 @@ public void WriteTo(
public const string Null = "$";
+ public static string Number(int v) => v.ToString(CultureInfo.InvariantCulture);
+
+ public static string Number(int? v) => v.HasValue ? v.Value.ToString(CultureInfo.InvariantCulture) : "$";
+
public static string Str(string? s) => s is null ? "$" : $"'{Escape(s)}'";
public static string Enum(string v) => $".{v}.";
diff --git a/src/IfcIO/Exporters/IfcHtmlViewerExporter.cs b/src/IfcIO/Exporters/IfcHtmlViewerExporter.cs
index 28b00c8..f51dcba 100644
--- a/src/IfcIO/Exporters/IfcHtmlViewerExporter.cs
+++ b/src/IfcIO/Exporters/IfcHtmlViewerExporter.cs
@@ -18,6 +18,7 @@ public sealed class IfcHtmlViewerOptions
public bool IncludeSectioning { get; set; } = true;
public bool IncludeMeasure { get; set; } = true;
public bool IncludeGrids { get; set; } = true;
+ public bool IncludeTimeLiner { get; set; } = true;
public Grids.IfcGridSystem? GridSystem { get; set; }
public int MaxMeshVertices { get; set; } = 500000;
}
@@ -1036,6 +1037,39 @@ private static string BuildJsonPayload(IfcModel model, IfcHtmlViewerOptions opti
writer.WriteEndArray();
}
+ // 4D Work Schedules
+ if (options.IncludeTimeLiner && model.WorkSchedules.Count > 0)
+ {
+ writer.WriteStartArray("workSchedules");
+ foreach (var ws in model.WorkSchedules)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("name", ws.Name);
+ writer.WriteStartArray("tasks");
+ foreach (var task in ws.AllTasks)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("name", task.Name);
+ if (!string.IsNullOrEmpty(task.Identification)) writer.WriteString("wbs", task.Identification);
+ writer.WriteString("type", task.TaskType.ToString());
+ var start = task.Time?.ActualStartDate ?? task.Time?.PlannedStartDate;
+ var finish = task.Time?.ActualFinishDate ?? task.Time?.PlannedFinishDate;
+ if (start.HasValue) writer.WriteString("start", start.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
+ if (finish.HasValue) writer.WriteString("finish", finish.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
+ writer.WriteStartArray("elementIds");
+ foreach (var el in task.AssignedElements)
+ {
+ writer.WriteNumberValue(el.Id);
+ }
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ }
+
writer.WriteEndObject();
writer.Flush();
diff --git a/src/IfcIO/IfcModel.cs b/src/IfcIO/IfcModel.cs
index b73d93e..d274d8b 100644
--- a/src/IfcIO/IfcModel.cs
+++ b/src/IfcIO/IfcModel.cs
@@ -39,6 +39,9 @@ public sealed class IfcModel
public List TypeProducts { get; } = [];
public List Groups { get; } = [];
public List Systems { get; } = [];
+ public List Zones { get; } = [];
+ public List WorkSchedules { get; } = [];
+ public List CostSchedules { get; } = [];
/// Spatial grid system (axes and levels) associated with the IFC model.
public Grids.IfcGridSystem? GridSystem { get; set; }
@@ -357,6 +360,74 @@ public IfcGroup AddGroup(string name, string? description = null, IEnumerable
+ /// Creates and registers a spatial zone grouping spaces sharing thermal, fire, or acoustic classifications.
+ /// Conforms to buildingSMART standard IFCZONE.
+ ///
+ public IfcZone AddZone(string name, string? zoneType = null, IEnumerable? spaces = null)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ var zone = new IfcZone(name, zoneType);
+ if (spaces is not null)
+ {
+ foreach (var s in spaces) zone.AddSpace(s);
+ }
+ Zones.Add(zone);
+ return zone;
+ }
+
+ ///
+ /// Creates and registers a 4D construction work schedule.
+ /// Conforms to buildingSMART standard IFCWORKSCHEDULE.
+ ///
+ public Model.Sequence.IfcWorkSchedule AddWorkSchedule(string name, string? description = null)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ var schedule = new Model.Sequence.IfcWorkSchedule(name, description);
+ WorkSchedules.Add(schedule);
+ return schedule;
+ }
+
+ ///
+ /// Creates and registers a 5D cost schedule / budget plan.
+ /// Conforms to buildingSMART standard IFCCOSTSCHEDULE.
+ ///
+ public Model.Cost.IfcCostSchedule AddCostSchedule(
+ string name,
+ string currency = "USD",
+ Model.Cost.IfcCostScheduleType type = Model.Cost.IfcCostScheduleType.CostPlan)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ var schedule = new Model.Cost.IfcCostSchedule(name, currency, type);
+ CostSchedules.Add(schedule);
+ return schedule;
+ }
+
+ /// Calculates the grand total cost across all cost schedules in the model.
+ public double CalculateTotalCost() => CostSchedules.Sum(s => s.GetTotalCost());
+
+ /// Gets the 4D construction simulation state of an element at the specified date using the active work schedule.
+ public Model.Sequence.IfcElementSimulationState GetElementSimulationStateAt(IfcElement element, DateTime date)
+ {
+ ArgumentNullException.ThrowIfNull(element);
+ if (WorkSchedules.Count == 0) return Model.Sequence.IfcElementSimulationState.Constructed;
+ return WorkSchedules[0].GetElementStateAt(element, date);
+ }
+
+ /// Gets the overall 4D construction schedule progress (0.0 to 100.0) at the specified date.
+ public double GetSimulationProgressAt(DateTime date)
+ {
+ if (WorkSchedules.Count == 0) return 100.0;
+ return WorkSchedules[0].GetProgressAt(date);
+ }
+
+ /// Imports a BCF 2.1 archive (.bcfzip) file from disk.
+ public static List ImportBcf(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+ return Bcf.BcfArchive.Open(path).Topics;
+ }
+
///
/// Adds an element defined by a 3D solid clipped by an infinite plane (Constructive Solid Geometry / Boolean clipping).
/// Conforms to buildingSMART IFCBOOLEANCLIPPINGRESULT.
@@ -1025,6 +1096,233 @@ public void Save(TextWriter writer, string fileName = "model.ifc")
}
}
+ // Zones (IFCZONE)
+ foreach (var zone in Zones)
+ {
+ string zoneGuid = zone.GlobalId ?? IfcGuid.NewGuid();
+ int zoneId = w.Add("IFCZONE",
+ $"{StepWriter.Str(zoneGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(zone.Name)},{StepWriter.Str(zone.Description)},{StepWriter.Str(zone.ZoneType)}");
+
+ var spaceMemberIds = new List();
+ foreach (var space in zone.Spaces)
+ {
+ if (elementEntityMap.TryGetValue(space, out int sId))
+ {
+ spaceMemberIds.Add(sId);
+ }
+ }
+
+ if (spaceMemberIds.Count > 0)
+ {
+ w.Add("IFCRELASSIGNSTOGROUP",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(spaceMemberIds.Select(StepWriter.Ref))},$,{StepWriter.Ref(zoneId)}");
+ }
+ }
+
+ // 4D Construction Work Schedules (IFCWORKSCHEDULE, IFCTASK, IFCTASKTIME, IFCRELASSIGNSTOPROCESS, IFCRELSEQUENCE)
+ var taskEntityMap = new Dictionary();
+ foreach (var ws in WorkSchedules)
+ {
+ string wsGuid = ws.GlobalId ?? IfcGuid.NewGuid();
+ string wsStartStr = ws.StartTime.HasValue ? StepWriter.Str(ws.StartTime.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ string wsFinStr = ws.FinishTime.HasValue ? StepWriter.Str(ws.FinishTime.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ int wsId = (Schema == IfcSchema.Ifc4)
+ ? w.Add("IFCWORKSCHEDULE", $"{StepWriter.Str(wsGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(ws.Name)},{StepWriter.Str(ws.Description)},$,{StepWriter.Str(ws.CreationDate.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture))},$,$,$,$,{wsStartStr},{wsFinStr}")
+ : w.Add("IFCWORKSCHEDULE", $"{StepWriter.Str(wsGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(ws.Name)},{StepWriter.Str(ws.Description)},$,$,{StepWriter.Str(ws.CreationDate.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture))},$,$,$,{wsStartStr},{wsFinStr}");
+
+ var scheduleTaskIds = new List();
+ foreach (var task in ws.AllTasks)
+ {
+ string tGuid = task.GlobalId ?? IfcGuid.NewGuid();
+ string tTypeStr = $".{task.TaskType.ToString().ToUpperInvariant()}.";
+ bool isMilestone = task.Time?.IsMilestone == true;
+ string isMileStr = isMilestone ? ".T." : ".F.";
+
+ int? timeId = null;
+ if (task.Time is not null)
+ {
+ string pStart = task.Time.PlannedStartDate.HasValue ? StepWriter.Str(task.Time.PlannedStartDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ string pFin = task.Time.PlannedFinishDate.HasValue ? StepWriter.Str(task.Time.PlannedFinishDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ string aStart = task.Time.ActualStartDate.HasValue ? StepWriter.Str(task.Time.ActualStartDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ string aFin = task.Time.ActualFinishDate.HasValue ? StepWriter.Str(task.Time.ActualFinishDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)) : "$";
+ string compStr = task.Time.Completion.HasValue ? StepWriter.Real(task.Time.Completion.Value) : "$";
+
+ if (Schema == IfcSchema.Ifc4)
+ {
+ timeId = w.Add("IFCTASKTIME",
+ $"$,$,.NOTDEFINED.,$,$,$,$,$,$,$,{pStart},{pFin},{aStart},{aFin},{compStr},$");
+ }
+ else
+ {
+ timeId = w.Add("IFCSCHEDULETIMECONTROL",
+ $"$,$,$,$,$,$,$,$,$,$,{aStart},{aFin},{pStart},{pFin},{compStr},$");
+ }
+ }
+
+ string timeRef = timeId.HasValue ? StepWriter.Ref(timeId.Value) : "$";
+ string prioStr = task.Priority.HasValue ? StepWriter.Number(task.Priority.Value) : "$";
+ int taskId = (Schema == IfcSchema.Ifc4)
+ ? w.Add("IFCTASK", $"{StepWriter.Str(tGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(task.Name)},{StepWriter.Str(task.Description)},$,{StepWriter.Str(task.Identification)},{StepWriter.Str(task.Status)},$,{isMileStr},{prioStr},{timeRef},{tTypeStr}")
+ : w.Add("IFCTASK", $"{StepWriter.Str(tGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(task.Name)},{StepWriter.Str(task.Description)},$,{StepWriter.Str(task.Identification)},{StepWriter.Str(task.Status)},$,{isMileStr},{prioStr}");
+
+ taskEntityMap[task] = taskId;
+ scheduleTaskIds.Add(taskId);
+
+ // Assign elements via IFCRELASSIGNSTOPROCESS
+ var assignedElemIds = new List();
+ foreach (var el in task.AssignedElements)
+ {
+ if (elementEntityMap.TryGetValue(el, out int elId))
+ {
+ assignedElemIds.Add(elId);
+ }
+ }
+ if (assignedElemIds.Count > 0)
+ {
+ w.Add("IFCRELASSIGNSTOPROCESS",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(assignedElemIds.Select(StepWriter.Ref))},$,{StepWriter.Ref(taskId)},$");
+ }
+ }
+
+ if (scheduleTaskIds.Count > 0)
+ {
+ w.Add("IFCRELASSIGNSTOCONTROL",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(scheduleTaskIds.Select(StepWriter.Ref))},$,{StepWriter.Ref(wsId)}");
+ }
+ }
+
+ // Sequence relationships (IFCRELSEQUENCE)
+ foreach (var ws in WorkSchedules)
+ {
+ foreach (var task in ws.AllTasks)
+ {
+ if (!taskEntityMap.TryGetValue(task, out int relProcId)) continue;
+
+ foreach (var succ in task.Successors)
+ {
+ if (taskEntityMap.TryGetValue(succ.RelatedProcess, out int succId))
+ {
+ string seqEnum = succ.SequenceType switch
+ {
+ Model.Sequence.IfcSequenceType.StartToStart => ".START_START.",
+ Model.Sequence.IfcSequenceType.StartToFinish => ".START_FINISH.",
+ Model.Sequence.IfcSequenceType.FinishToFinish => ".FINISH_FINISH.",
+ _ => ".FINISH_START."
+ };
+ w.Add("IFCRELSEQUENCE",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(succ.Description)},$,{StepWriter.Ref(relProcId)},{StepWriter.Ref(succId)},$,{seqEnum}");
+ }
+ }
+ }
+ }
+
+ // 5D Cost Schedules & Cost Items (IFCCOSTSCHEDULE, IFCCOSTITEM, IFCAPPLIEDVALUE, IFCRELASSIGNSTOCONTROL, IFCRELNESTING)
+ foreach (var cs in CostSchedules)
+ {
+ string csGuid = cs.GlobalId ?? IfcGuid.NewGuid();
+ string csTypeEnum = $".{cs.ScheduleType.ToString().ToUpperInvariant()}.";
+ int csId = (Schema == IfcSchema.Ifc4)
+ ? w.Add("IFCCOSTSCHEDULE", $"{StepWriter.Str(csGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(cs.Name)},{StepWriter.Str(cs.Description)},{StepWriter.Str(cs.Currency)},$,$,$,{StepWriter.Str(cs.Status)},$,{StepWriter.Str(cs.UpdateDate.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture))},$,{csTypeEnum}")
+ : w.Add("IFCCOSTSCHEDULE", $"{StepWriter.Str(csGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(cs.Name)},{StepWriter.Str(cs.Description)},{StepWriter.Str(cs.Currency)},$,$,$,{StepWriter.Str(cs.Status)},$,{StepWriter.Str(cs.UpdateDate.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture))},$,{csTypeEnum}");
+
+ var csItemIds = new List();
+ var itemEntityMap = new Dictionary();
+
+ foreach (var item in cs.AllCostItems)
+ {
+ string ciGuid = item.GlobalId ?? IfcGuid.NewGuid();
+
+ // Cost values (IFCAPPLIEDVALUE)
+ var appliedValIds = new List();
+ if (item.CostValues.Count > 0)
+ {
+ foreach (var cv in item.CostValues)
+ {
+ int avId = w.Add("IFCAPPLIEDVALUE",
+ $"{StepWriter.Str(cv.Name)},$,IFCMONETARYMEASURE({StepWriter.Real(cv.Amount)}),$,$,$,{StepWriter.Str(cv.Category ?? cv.Currency)},$,$,$");
+ appliedValIds.Add(avId);
+ }
+ }
+ if (item.UnitRate.HasValue)
+ {
+ int avId = w.Add("IFCAPPLIEDVALUE",
+ $"{StepWriter.Str("UnitRate")},{StepWriter.Str(item.UnitBasis)},IFCMONETARYMEASURE({StepWriter.Real(item.UnitRate.Value)}),$,$,$,'UnitRate',$,$,$");
+ appliedValIds.Add(avId);
+ }
+
+ // Quantities (IFCQUANTITY*)
+ var qtyValIds = new List();
+ if (item.Quantity.HasValue)
+ {
+ string qtyType = item.UnitBasis?.ToLowerInvariant() switch
+ {
+ "m2" or "sqm" => "IFCQUANTITYAREA",
+ "m3" or "cum" => "IFCQUANTITYVOLUME",
+ "m" or "lm" => "IFCQUANTITYLENGTH",
+ _ => "IFCQUANTITYCOUNT"
+ };
+ int qId = w.Add(qtyType, $"{StepWriter.Str("Quantity")},{StepWriter.Str(item.UnitBasis)},$,{StepWriter.Real(item.Quantity.Value)},$");
+ qtyValIds.Add(qId);
+ }
+
+ string costValuesArg = appliedValIds.Count > 0 ? StepWriter.List(appliedValIds.Select(StepWriter.Ref)) : "$";
+ string quantitiesArg = qtyValIds.Count > 0 ? StepWriter.List(qtyValIds.Select(StepWriter.Ref)) : "$";
+
+ int ciId = (Schema == IfcSchema.Ifc4)
+ ? w.Add("IFCCOSTITEM", $"{StepWriter.Str(ciGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(item.Name)},{StepWriter.Str(item.Description)},$,{StepWriter.Str(item.CostCode)},{costValuesArg},{quantitiesArg}")
+ : w.Add("IFCCOSTITEM", $"{StepWriter.Str(ciGuid)},{StepWriter.Ref(ownerHistoryId)},{StepWriter.Str(item.Name)},{StepWriter.Str(item.Description)},$,{StepWriter.Str(item.CostCode)}");
+
+ itemEntityMap[item] = ciId;
+
+ if (cs.RootCostItems.Contains(item))
+ {
+ csItemIds.Add(ciId);
+ }
+
+ if (Schema == IfcSchema.Ifc2x3 && (appliedValIds.Count > 0 || qtyValIds.Count > 0))
+ {
+ var allValRefs = appliedValIds.Concat(qtyValIds).ToList();
+ w.Add("IFCRELASSIGNSTOCONTROL",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(allValRefs.Select(StepWriter.Ref))},$,{StepWriter.Ref(ciId)}");
+ }
+
+ // Assigned elements
+ var assignedElemIds = new List();
+ foreach (var el in item.AssignedElements)
+ {
+ if (elementEntityMap.TryGetValue(el, out int elId))
+ {
+ assignedElemIds.Add(elId);
+ }
+ }
+ if (assignedElemIds.Count > 0)
+ {
+ w.Add("IFCRELASSIGNSTOCONTROL",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(assignedElemIds.Select(StepWriter.Ref))},$,{StepWriter.Ref(ciId)}");
+ }
+ }
+
+ // Sub-cost items via IFCRELNESTING
+ foreach (var item in cs.AllCostItems)
+ {
+ if (item.SubCostItems.Count > 0 && itemEntityMap.TryGetValue(item, out int parentCiId))
+ {
+ var subCiIds = item.SubCostItems.Where(s => itemEntityMap.ContainsKey(s)).Select(s => itemEntityMap[s]).ToList();
+ if (subCiIds.Count > 0)
+ {
+ w.Add("IFCRELNESTING",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.Ref(parentCiId)},{StepWriter.List(subCiIds.Select(StepWriter.Ref))}");
+ }
+ }
+ }
+
+ if (csItemIds.Count > 0)
+ {
+ w.Add("IFCRELASSIGNSTOCONTROL",
+ $"{StepWriter.Str(IfcGuid.NewGuid())},{StepWriter.Ref(ownerHistoryId)},$,$,{StepWriter.List(csItemIds.Select(StepWriter.Ref))},$,{StepWriter.Ref(csId)}");
+ }
+ }
+
// 9. Material Layers & Profiles
var materialEntityMap = new Dictionary();
foreach (var mat in Materials)
@@ -1292,6 +1590,14 @@ public static IfcModel Open(string path)
return Open(fileReader, Path.GetFileNameWithoutExtension(path));
}
+ /// Parses an existing IFC STEP physical file or stream into a structured model.
+ public static IfcModel Open(Stream stream, string? modelName = null)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 65536, leaveOpen: true);
+ return Open(reader, modelName);
+ }
+
/// Parses an existing IFC STEP stream into a structured model with storeys, elements, properties, and meshes.
public static IfcModel Open(TextReader reader, string? modelName = null)
{
@@ -1724,6 +2030,22 @@ openElem is IfcOpeningElement opening &&
groupMap[grpEnt.Id] = grp;
}
+ foreach (var zoneEnt in step.OfType("IFCZONE"))
+ {
+ string zoneGuid = zoneEnt.GetString(0) ?? IfcGuid.NewGuid();
+ string zName = zoneEnt.GetString(2) ?? "Zone";
+ string? zDesc = zoneEnt.GetString(3);
+ string? zType = zoneEnt.GetString(4);
+
+ var zone = new IfcZone(zName, zType)
+ {
+ GlobalId = zoneGuid,
+ Description = zDesc,
+ };
+ model.Zones.Add(zone);
+ groupMap[zoneEnt.Id] = zone;
+ }
+
foreach (var relGroup in step.OfType("IFCRELASSIGNSTOGROUP"))
{
int? groupId = relGroup.GetRef(6);
@@ -1732,9 +2054,296 @@ openElem is IfcOpeningElement opening &&
var memberIds = relGroup.GetRefList(4);
foreach (int mId in memberIds)
{
- if (allElementsMap.TryGetValue(mId, out var elem) && !targetGroup.Elements.Contains(elem))
+ if (allElementsMap.TryGetValue(mId, out var elem))
+ {
+ if (targetGroup is IfcZone z && elem is IfcSpace sp)
+ {
+ z.AddSpace(sp);
+ }
+ else if (!targetGroup.Elements.Contains(elem))
+ {
+ targetGroup.Elements.Add(elem);
+ }
+ }
+ }
+ }
+ }
+
+ // 9b. Parse 4D Construction Work Schedules (IFCWORKSCHEDULE, IFCTASK, IFCTASKTIME, IFCSCHEDULETIMECONTROL, IFCRELASSIGNSTOPROCESS, IFCRELSEQUENCE)
+ var taskMap = new Dictionary();
+ foreach (var tEnt in step.OfType("IFCTASK"))
+ {
+ string tGuid = tEnt.GetString(0) ?? IfcGuid.NewGuid();
+ string tName = tEnt.GetString(2) ?? "Task";
+ string? tDesc = tEnt.GetString(3);
+ string? tIdent = tEnt.GetString(5);
+ string? tStatus = tEnt.GetString(6);
+ int? priority = tEnt.GetInt(9);
+
+ var task = new Model.Sequence.IfcTask(tName, tIdent)
+ {
+ GlobalId = tGuid,
+ Description = tDesc,
+ Status = tStatus,
+ Priority = priority
+ };
+
+ var timeRef = tEnt.GetRef(10);
+ if (timeRef.HasValue)
+ {
+ var timeEnt = step.Find(timeRef.Value);
+ if (timeEnt is not null)
+ {
+ task.Time = new Model.Sequence.IfcTaskTime();
+ if (DateTime.TryParse(timeEnt.GetString(10), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var pStart))
+ task.Time.PlannedStartDate = pStart;
+ if (DateTime.TryParse(timeEnt.GetString(11), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var pFin))
+ task.Time.PlannedFinishDate = pFin;
+ if (DateTime.TryParse(timeEnt.GetString(12), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var aStart))
+ task.Time.ActualStartDate = aStart;
+ if (DateTime.TryParse(timeEnt.GetString(13), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var aFin))
+ task.Time.ActualFinishDate = aFin;
+ task.Time.Completion = timeEnt.GetReal(14);
+ }
+ }
+
+ taskMap[tEnt.Id] = task;
+ }
+
+ foreach (var wsEnt in step.OfType("IFCWORKSCHEDULE"))
+ {
+ string wsGuid = wsEnt.GetString(0) ?? IfcGuid.NewGuid();
+ string wsName = wsEnt.GetString(2) ?? "Schedule";
+ string? wsDesc = wsEnt.GetString(3);
+ var ws = new Model.Sequence.IfcWorkSchedule(wsName, wsDesc)
+ {
+ GlobalId = wsGuid
+ };
+ if (DateTime.TryParse(wsEnt.GetString(10), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var wsStart))
+ ws.StartTime = wsStart;
+ if (DateTime.TryParse(wsEnt.GetString(11), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var wsFin))
+ ws.FinishTime = wsFin;
+
+ model.WorkSchedules.Add(ws);
+ }
+
+ // Connect tasks to elements via IFCRELASSIGNSTOPROCESS
+ foreach (var relProc in step.OfType("IFCRELASSIGNSTOPROCESS"))
+ {
+ var procRef = relProc.GetRef(6);
+ if (procRef.HasValue && taskMap.TryGetValue(procRef.Value, out var task))
+ {
+ foreach (var elId in relProc.GetRefList(4))
+ {
+ if (allElementsMap.TryGetValue(elId, out var el))
+ {
+ task.AssignElement(el);
+ }
+ }
+ }
+ }
+
+ // Connect sequence dependencies via IFCRELSEQUENCE
+ foreach (var relSeq in step.OfType("IFCRELSEQUENCE"))
+ {
+ var relProcRef = relSeq.GetRef(4);
+ var relSuccRef = relSeq.GetRef(5);
+ if (relProcRef.HasValue && relSuccRef.HasValue &&
+ taskMap.TryGetValue(relProcRef.Value, out var pred) &&
+ taskMap.TryGetValue(relSuccRef.Value, out var succ))
+ {
+ string? seqTypeStr = relSeq.GetString(7);
+ var seqType = seqTypeStr?.Contains("START_START", StringComparison.OrdinalIgnoreCase) == true ? Model.Sequence.IfcSequenceType.StartToStart :
+ seqTypeStr?.Contains("START_FINISH", StringComparison.OrdinalIgnoreCase) == true ? Model.Sequence.IfcSequenceType.StartToFinish :
+ seqTypeStr?.Contains("FINISH_FINISH", StringComparison.OrdinalIgnoreCase) == true ? Model.Sequence.IfcSequenceType.FinishToFinish :
+ Model.Sequence.IfcSequenceType.FinishToStart;
+ pred.AddSuccessor(succ, seqType);
+ }
+ }
+
+ // 9c. Parse 5D Cost Schedules & Cost Items (IFCCOSTSCHEDULE, IFCCOSTITEM, IFCAPPLIEDVALUE, IFCRELASSIGNSTOCONTROL, IFCRELNESTING)
+ var costItemMap = new Dictionary();
+ foreach (var ciEnt in step.OfType("IFCCOSTITEM"))
+ {
+ string ciGuid = ciEnt.GetString(0) ?? IfcGuid.NewGuid();
+ string ciName = ciEnt.GetString(2) ?? "Cost Item";
+ string? ciDesc = ciEnt.GetString(3);
+ string? ciCode = ciEnt.GetString(5);
+
+ var ci = new Model.Cost.IfcCostItem(ciName, ciCode)
+ {
+ GlobalId = ciGuid,
+ Description = ciDesc
+ };
+
+ // Direct references in IFC4 (argument 6: CostValues, argument 7: CostQuantities)
+ foreach (int vRef in ciEnt.GetRefList(6))
+ {
+ var vEnt = step.Find(vRef);
+ if (vEnt is not null && vEnt.Type.Equals("IFCAPPLIEDVALUE", StringComparison.OrdinalIgnoreCase))
+ {
+ string? vName = vEnt.GetString(0);
+ string? vDesc = vEnt.GetString(1);
+ string? rawAmount = vEnt.GetRaw(2);
+ string cleanAmount = ExtractNominalValue(rawAmount);
+ string? vCat = vEnt.GetString(6);
+
+ if (double.TryParse(cleanAmount, NumberStyles.Float, CultureInfo.InvariantCulture, out double amt))
{
- targetGroup.Elements.Add(elem);
+ if (string.Equals(vName, "UnitRate", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(vCat, "UnitRate", StringComparison.OrdinalIgnoreCase))
+ {
+ ci.UnitRate = amt;
+ if (!string.IsNullOrEmpty(vDesc)) ci.UnitBasis = vDesc;
+ }
+ else
+ {
+ ci.AddCostValue(amt, "USD", vCat, vName);
+ }
+ }
+ }
+ }
+
+ foreach (int qRef in ciEnt.GetRefList(7))
+ {
+ var qEnt = step.Find(qRef);
+ if (qEnt is not null && qEnt.Type.StartsWith("IFCQUANTITY", StringComparison.OrdinalIgnoreCase))
+ {
+ double? qVal = qEnt.GetReal(3);
+ if (qVal.HasValue)
+ {
+ ci.Quantity = qVal.Value;
+ string? qDesc = qEnt.GetString(1);
+ if (!string.IsNullOrEmpty(qDesc)) ci.UnitBasis = qDesc;
+ }
+ }
+ }
+
+ costItemMap[ciEnt.Id] = ci;
+ }
+
+ foreach (var csEnt in step.OfType("IFCCOSTSCHEDULE"))
+ {
+ string csGuid = csEnt.GetString(0) ?? IfcGuid.NewGuid();
+ string csName = csEnt.GetString(2) ?? "Cost Schedule";
+ string? csDesc = csEnt.GetString(3);
+ string? csCurrency = csEnt.GetString(4);
+ var cs = new Model.Cost.IfcCostSchedule(csName, string.IsNullOrWhiteSpace(csCurrency) ? "USD" : csCurrency)
+ {
+ GlobalId = csGuid,
+ Description = csDesc
+ };
+
+ string? typeRaw = csEnt.GetRaw(12) ?? csEnt.GetRaw(6);
+ if (!string.IsNullOrWhiteSpace(typeRaw))
+ {
+ string cleanType = typeRaw.Trim('.').Replace("_", "");
+ if (Enum.TryParse(cleanType, true, out var parsedType))
+ {
+ cs.ScheduleType = parsedType;
+ }
+ }
+
+ model.CostSchedules.Add(cs);
+ }
+
+ // Connect sub-items via IFCRELNESTING
+ foreach (var relNest in step.OfType("IFCRELNESTING"))
+ {
+ int? parentRef = relNest.GetRef(4);
+ if (parentRef.HasValue && costItemMap.TryGetValue(parentRef.Value, out var parentItem))
+ {
+ foreach (int childRef in relNest.GetRefList(5))
+ {
+ if (costItemMap.TryGetValue(childRef, out var childItem) && !parentItem.SubCostItems.Contains(childItem))
+ {
+ parentItem.SubCostItems.Add(childItem);
+ }
+ }
+ }
+ }
+
+ // Connect control assignments via IFCRELASSIGNSTOCONTROL
+ foreach (var relCtrl in step.OfType("IFCRELASSIGNSTOCONTROL"))
+ {
+ var ctrlRef = relCtrl.GetRef(6);
+ if (!ctrlRef.HasValue) continue;
+
+ string? ctrlGuid = step.Find(ctrlRef.Value)?.GetString(0);
+
+ // Connect to work schedule
+ var ws = model.WorkSchedules.Find(s => s.GlobalId == ctrlGuid);
+ if (ws is not null)
+ {
+ foreach (var rId in relCtrl.GetRefList(4))
+ {
+ if (taskMap.TryGetValue(rId, out var task) && !ws.RootTasks.Contains(task))
+ {
+ ws.RootTasks.Add(task);
+ }
+ }
+ continue;
+ }
+
+ // Connect to cost schedule
+ var cs = model.CostSchedules.Find(s => s.GlobalId == ctrlGuid);
+ if (cs is not null)
+ {
+ foreach (var rId in relCtrl.GetRefList(4))
+ {
+ if (costItemMap.TryGetValue(rId, out var item) && !cs.RootCostItems.Contains(item))
+ {
+ cs.RootCostItems.Add(item);
+ }
+ }
+ continue;
+ }
+
+ // Connect elements or applied values/quantities to cost item
+ if (costItemMap.TryGetValue(ctrlRef.Value, out var costItem))
+ {
+ foreach (var rId in relCtrl.GetRefList(4))
+ {
+ if (allElementsMap.TryGetValue(rId, out var el))
+ {
+ costItem.AssignElement(el);
+ }
+ else
+ {
+ var relatedEnt = step.Find(rId);
+ if (relatedEnt is not null)
+ {
+ if (relatedEnt.Type.Equals("IFCAPPLIEDVALUE", StringComparison.OrdinalIgnoreCase))
+ {
+ string? vName = relatedEnt.GetString(0);
+ string? vDesc = relatedEnt.GetString(1);
+ string cleanAmount = ExtractNominalValue(relatedEnt.GetRaw(2));
+ string? vCat = relatedEnt.GetString(6);
+ if (double.TryParse(cleanAmount, NumberStyles.Float, CultureInfo.InvariantCulture, out double amt))
+ {
+ if (string.Equals(vName, "UnitRate", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(vCat, "UnitRate", StringComparison.OrdinalIgnoreCase))
+ {
+ costItem.UnitRate = amt;
+ if (!string.IsNullOrEmpty(vDesc)) costItem.UnitBasis = vDesc;
+ }
+ else
+ {
+ costItem.AddCostValue(amt, "USD", vCat, vName);
+ }
+ }
+ }
+ else if (relatedEnt.Type.StartsWith("IFCQUANTITY", StringComparison.OrdinalIgnoreCase))
+ {
+ double? qVal = relatedEnt.GetReal(3);
+ if (qVal.HasValue)
+ {
+ costItem.Quantity = qVal.Value;
+ string? qDesc = relatedEnt.GetString(1);
+ if (!string.IsNullOrEmpty(qDesc)) costItem.UnitBasis = qDesc;
+ }
+ }
+ }
}
}
}
diff --git a/src/IfcIO/Model/Cost/IfcCostTypes.cs b/src/IfcIO/Model/Cost/IfcCostTypes.cs
new file mode 100644
index 0000000..d51b687
--- /dev/null
+++ b/src/IfcIO/Model/Cost/IfcCostTypes.cs
@@ -0,0 +1,221 @@
+using System.Globalization;
+
+namespace IfcIO.Model.Cost;
+
+///
+/// Predefined classification for cost schedules conforming to buildingSMART standard IFCCOSTSCHEDULETYPEENUM.
+///
+public enum IfcCostScheduleType
+{
+ Budget,
+ CostPlan,
+ Estimate,
+ Tender,
+ Actual,
+ Valuation,
+ Pricing,
+ UserDefined,
+ NotDefined
+}
+
+///
+/// A monetary or rate value associated with a cost item.
+/// Conforms to buildingSMART standard IFCAPPLIEDVALUE.
+///
+public sealed class IfcCostValue
+{
+ public string? Name { get; set; }
+
+ /// Cost category (e.g. Material, Labor, Equipment, Subcontractor, Overhead, Profit).
+ public string? Category { get; set; }
+
+ /// The numeric cost amount.
+ public double Amount { get; set; }
+
+ /// Currency code (e.g. USD, EUR, GBP, JPY).
+ public string Currency { get; set; } = "USD";
+
+ /// Measurement unit basis (e.g. "m", "m2", "m3", "kg", "hour", "unit").
+ public string? UnitBasis { get; set; }
+
+ public IfcCostValue() { }
+
+ public IfcCostValue(double amount, string currency = "USD", string? category = null, string? name = null)
+ {
+ Amount = amount;
+ Currency = currency;
+ Category = category;
+ Name = name;
+ }
+}
+
+///
+/// An individual cost line item or cost breakdown structure node within a cost schedule.
+/// Conforms to buildingSMART standard IFCCOSTITEM.
+///
+public sealed class IfcCostItem
+{
+ public string GlobalId { get; set; } = Core.IfcGuid.NewGuid();
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+
+ /// Cost Breakdown Structure (CBS) code or MasterFormat / UniFormat code (e.g. "03.30.00").
+ public string? CostCode { get; set; }
+
+ /// Unit rate per quantity unit (e.g. $150 per m3 of concrete).
+ public double? UnitRate { get; set; }
+
+ /// Total quantified volume, area, length, or count.
+ public double? Quantity { get; set; }
+
+ /// Measurement unit string (e.g. "m3", "m2", "m", "pcs", "kg").
+ public string? UnitBasis { get; set; }
+
+ /// Discrete applied values and cost components (Labor, Material, etc.).
+ public List CostValues { get; } = [];
+
+ /// Hierarchical sub-cost items subordinate to this cost node.
+ public List SubCostItems { get; } = [];
+
+ /// Building elements assigned to this cost item via IFCRELASSIGNSTOCONTROL.
+ public List AssignedElements { get; } = [];
+
+ /// Physical quantities linked to this cost item for automated rate-based takeoffs.
+ public List AssignedQuantities { get; } = [];
+
+ public IfcCostItem() { }
+
+ public IfcCostItem(string name, string? costCode = null, double? unitRate = null, double? quantity = null, string? unitBasis = null)
+ {
+ Name = name;
+ CostCode = costCode;
+ UnitRate = unitRate;
+ Quantity = quantity;
+ UnitBasis = unitBasis;
+ }
+
+ ///
+ /// Gets the calculated total cost for this item, either explicitly via cost values, computed from UnitRate * Quantity, or aggregated from sub-items.
+ ///
+ public double TotalCost
+ {
+ get
+ {
+ if (SubCostItems.Count > 0)
+ {
+ return SubCostItems.Sum(item => item.TotalCost);
+ }
+
+ if (CostValues.Count > 0)
+ {
+ return CostValues.Sum(v => v.Amount);
+ }
+
+ if (UnitRate.HasValue && Quantity.HasValue)
+ {
+ return UnitRate.Value * Quantity.Value;
+ }
+
+ return 0.0;
+ }
+ }
+
+ /// Adds a nested sub-cost item to this node.
+ public IfcCostItem AddSubItem(string name, string? costCode = null, double? unitRate = null, double? quantity = null, string? unitBasis = null)
+ {
+ var subItem = new IfcCostItem(name, costCode, unitRate, quantity, unitBasis);
+ SubCostItems.Add(subItem);
+ return subItem;
+ }
+
+ /// Adds a discrete cost value breakdown component.
+ public void AddCostValue(double amount, string currency = "USD", string? category = null, string? name = null)
+ {
+ CostValues.Add(new IfcCostValue(amount, currency, category, name));
+ }
+
+ /// Assigns an element to this cost item.
+ public void AssignElement(IfcElement element)
+ {
+ ArgumentNullException.ThrowIfNull(element);
+ if (!AssignedElements.Contains(element))
+ {
+ AssignedElements.Add(element);
+ }
+ }
+
+ /// Assigns a physical quantity to this cost item.
+ public void AssignQuantity(IfcPhysicalQuantity quantity)
+ {
+ ArgumentNullException.ThrowIfNull(quantity);
+ if (!AssignedQuantities.Contains(quantity))
+ {
+ AssignedQuantities.Add(quantity);
+ }
+ }
+}
+
+///
+/// A collection of cost items representing a budget, estimate, or cost plan.
+/// Conforms to buildingSMART standard IFCCOSTSCHEDULE.
+///
+public sealed class IfcCostSchedule
+{
+ public string GlobalId { get; set; } = Core.IfcGuid.NewGuid();
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+ public IfcCostScheduleType ScheduleType { get; set; } = IfcCostScheduleType.CostPlan;
+ public string? Status { get; set; }
+ public string Currency { get; set; } = "USD";
+ public DateTime UpdateDate { get; set; } = DateTime.UtcNow;
+
+ /// Top-level root cost items in the Cost Breakdown Structure (CBS).
+ public List RootCostItems { get; } = [];
+
+ public IfcCostSchedule() { }
+
+ public IfcCostSchedule(string name, string currency = "USD", IfcCostScheduleType type = IfcCostScheduleType.CostPlan)
+ {
+ Name = name;
+ Currency = currency;
+ ScheduleType = type;
+ }
+
+ /// Enumerates all cost items across the hierarchical CBS tree.
+ public IEnumerable AllCostItems
+ {
+ get
+ {
+ var stack = new Stack(RootCostItems);
+ while (stack.Count > 0)
+ {
+ var current = stack.Pop();
+ yield return current;
+ for (int i = current.SubCostItems.Count - 1; i >= 0; i--)
+ {
+ stack.Push(current.SubCostItems[i]);
+ }
+ }
+ }
+ }
+
+ /// Adds a top-level cost item to the schedule.
+ public IfcCostItem AddCostItem(string name, string? costCode = null, double? unitRate = null, double? quantity = null, string? unitBasis = null)
+ {
+ var item = new IfcCostItem(name, costCode, unitRate, quantity, unitBasis);
+ RootCostItems.Add(item);
+ return item;
+ }
+
+ /// Calculates the grand total cost of all root cost items in the schedule.
+ public double GetTotalCost() => RootCostItems.Sum(item => item.TotalCost);
+
+ /// Finds a cost item by its CBS cost code or name.
+ public IfcCostItem? FindItem(string codeOrName)
+ {
+ ArgumentNullException.ThrowIfNull(codeOrName);
+ return AllCostItems.FirstOrDefault(item =>
+ string.Equals(item.CostCode, codeOrName, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(item.Name, codeOrName, StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/src/IfcIO/Model/IfcModelTypes.cs b/src/IfcIO/Model/IfcModelTypes.cs
index d64c10d..c5c27a8 100644
--- a/src/IfcIO/Model/IfcModelTypes.cs
+++ b/src/IfcIO/Model/IfcModelTypes.cs
@@ -225,6 +225,34 @@ public sealed class IfcBuildingStorey
public string? GlobalId { get; set; }
public List Elements { get; } = [];
public List Spaces { get; } = [];
+
+ public IfcElement AddWall(string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ var wall = new IfcElement { Id = Elements.Count + 1, Name = name, IfcType = "IFCWALL", Mesh = mesh, Material = material };
+ Elements.Add(wall);
+ return wall;
+ }
+
+ public IfcElement AddSlab(string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ var slab = new IfcElement { Id = Elements.Count + 1, Name = name, IfcType = "IFCSLAB", Mesh = mesh, Material = material };
+ Elements.Add(slab);
+ return slab;
+ }
+
+ public IfcElement AddColumn(string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ var col = new IfcElement { Id = Elements.Count + 1, Name = name, IfcType = "IFCCOLUMN", Mesh = mesh, Material = material };
+ Elements.Add(col);
+ return col;
+ }
+
+ public IfcElement AddBeam(string name, IfcMesh? mesh = null, IfcMaterial? material = null)
+ {
+ var beam = new IfcElement { Id = Elements.Count + 1, Name = name, IfcType = "IFCBEAM", Mesh = mesh, Material = material };
+ Elements.Add(beam);
+ return beam;
+ }
}
///
@@ -427,3 +455,29 @@ public IfcSystem(string name, string systemType = "HVAC") : base(name)
SystemType = systemType;
}
}
+
+///
+/// A spatial group of spaces sharing common thermal, acoustic, fire safety, or security properties.
+/// Conforms to buildingSMART standard IFCZONE.
+///
+public sealed class IfcZone : IfcGroup
+{
+ public string? ZoneType { get; set; }
+ public List Spaces { get; } = [];
+
+ public IfcZone() { }
+
+ public IfcZone(string name, string? zoneType = null) : base(name)
+ {
+ ZoneType = zoneType;
+ }
+
+ public void AddSpace(IfcSpace space)
+ {
+ ArgumentNullException.ThrowIfNull(space);
+ if (!Spaces.Contains(space))
+ {
+ Spaces.Add(space);
+ }
+ }
+}
diff --git a/src/IfcIO/Model/Sequence/IfcSequenceTypes.cs b/src/IfcIO/Model/Sequence/IfcSequenceTypes.cs
new file mode 100644
index 0000000..245befe
--- /dev/null
+++ b/src/IfcIO/Model/Sequence/IfcSequenceTypes.cs
@@ -0,0 +1,358 @@
+using System.Globalization;
+
+namespace IfcIO.Model.Sequence;
+
+///
+/// Predefined classification for construction schedule tasks conforming to buildingSMART standard IFCTASKTYPEENUM.
+///
+public enum IfcTaskType
+{
+ Construction,
+ Demolition,
+ Installation,
+ Maintenance,
+ Move,
+ Operation,
+ Renovation,
+ Safety,
+ UserDefined,
+ NotDefined
+}
+
+///
+/// Predefined sequence dependency relationship between construction tasks conforming to buildingSMART IFCRELSEQUENCE.
+///
+public enum IfcSequenceType
+{
+ StartToStart,
+ StartToFinish,
+ FinishToStart,
+ FinishToFinish,
+ UserDefined,
+ NotDefined
+}
+
+///
+/// Temporal state of a building element during 4D construction schedule simulation.
+///
+public enum IfcElementSimulationState
+{
+ /// Element construction has not yet started.
+ Unstarted,
+
+ /// Element is actively being constructed/installed at the target simulation date.
+ InProgress,
+
+ /// Element construction is fully complete.
+ Constructed,
+
+ /// Element was demolished as part of demolition tasks.
+ Demolished
+}
+
+///
+/// Captures planned and actual scheduling time attributes for an IFC task.
+/// Conforms to buildingSMART IFCTASKTIME (IFC4) and IFCSCHEDULETIMECONTROL (IFC2x3).
+///
+public sealed class IfcTaskTime
+{
+ public string? Name { get; set; }
+ public TimeSpan? Duration { get; set; }
+ public string? ScheduleDuration { get; set; }
+ public DateTime? EarlyStartDate { get; set; }
+ public DateTime? EarlyFinishDate { get; set; }
+ public DateTime? LateStartDate { get; set; }
+ public DateTime? LateFinishDate { get; set; }
+ public DateTime? PlannedStartDate { get; set; }
+ public DateTime? PlannedFinishDate { get; set; }
+ public DateTime? ActualStartDate { get; set; }
+ public DateTime? ActualFinishDate { get; set; }
+
+ /// Task completion ratio ranging from 0.0 (unstarted) to 1.0 (completed).
+ public double? Completion { get; set; }
+
+ /// True if this task represents a zero-duration project milestone.
+ public bool IsMilestone => Duration.HasValue && Duration.Value == TimeSpan.Zero;
+
+ public IfcTaskTime() { }
+
+ public IfcTaskTime(DateTime? plannedStart, DateTime? plannedFinish, double? completion = null)
+ {
+ PlannedStartDate = plannedStart;
+ PlannedFinishDate = plannedFinish;
+ if (plannedStart.HasValue && plannedFinish.HasValue && plannedFinish.Value >= plannedStart.Value)
+ {
+ Duration = plannedFinish.Value - plannedStart.Value;
+ }
+ Completion = completion;
+ }
+}
+
+///
+/// Dependency sequence connection between two tasks conforming to buildingSMART IFCRELSEQUENCE.
+///
+public sealed class IfcTaskRelationship
+{
+ public required IfcTask RelatingProcess { get; init; }
+ public required IfcTask RelatedProcess { get; init; }
+ public IfcSequenceType SequenceType { get; set; } = IfcSequenceType.FinishToStart;
+ public TimeSpan? TimeLag { get; set; }
+ public string? Description { get; set; }
+}
+
+///
+/// A scheduled construction or operational task within a work schedule conforming to buildingSMART IFCTASK.
+///
+public sealed class IfcTask
+{
+ public string GlobalId { get; set; } = Core.IfcGuid.NewGuid();
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+
+ /// Work Breakdown Structure (WBS) identification code (e.g. "1.2.3").
+ public string? Identification { get; set; }
+
+ public string? Status { get; set; }
+ public int? Priority { get; set; }
+ public IfcTaskType TaskType { get; set; } = IfcTaskType.Construction;
+ public IfcTaskTime? Time { get; set; }
+
+ /// Hierarchical subtasks subordinate to this task.
+ public List SubTasks { get; } = [];
+
+ /// Building elements assigned to be constructed or demolished by this task.
+ public List AssignedElements { get; } = [];
+
+ /// Predecessor sequence dependencies leading into this task.
+ public List Predecessors { get; } = [];
+
+ /// Successor sequence dependencies leading out of this task.
+ public List Successors { get; } = [];
+
+ public IfcTask() { }
+
+ public IfcTask(string name, string? identification = null, IfcTaskType taskType = IfcTaskType.Construction)
+ {
+ Name = name;
+ Identification = identification;
+ TaskType = taskType;
+ }
+
+ /// Adds a nested subtask under this task.
+ public IfcTask AddSubTask(string name, string? identification = null, DateTime? start = null, DateTime? finish = null)
+ {
+ var subTask = new IfcTask(name, identification, TaskType)
+ {
+ Time = (start.HasValue || finish.HasValue) ? new IfcTaskTime(start, finish) : null
+ };
+ SubTasks.Add(subTask);
+ return subTask;
+ }
+
+ /// Assigns a physical building element to this task.
+ public void AssignElement(IfcElement element)
+ {
+ ArgumentNullException.ThrowIfNull(element);
+ if (!AssignedElements.Contains(element))
+ {
+ AssignedElements.Add(element);
+ }
+ }
+
+ /// Assigns multiple building elements to this task.
+ public void AssignElements(IEnumerable elements)
+ {
+ ArgumentNullException.ThrowIfNull(elements);
+ foreach (var el in elements)
+ {
+ AssignElement(el);
+ }
+ }
+
+ /// Links a successor task with an optional sequence type and lag.
+ public IfcTaskRelationship AddSuccessor(IfcTask successor, IfcSequenceType sequenceType = IfcSequenceType.FinishToStart, TimeSpan? lag = null)
+ {
+ ArgumentNullException.ThrowIfNull(successor);
+ var rel = new IfcTaskRelationship
+ {
+ RelatingProcess = this,
+ RelatedProcess = successor,
+ SequenceType = sequenceType,
+ TimeLag = lag
+ };
+ Successors.Add(rel);
+ successor.Predecessors.Add(rel);
+ return rel;
+ }
+}
+
+///
+/// A collection of scheduled tasks representing a construction or operational schedule.
+/// Conforms to buildingSMART standard IFCWORKSCHEDULE.
+///
+public sealed class IfcWorkSchedule
+{
+ public string GlobalId { get; set; } = Core.IfcGuid.NewGuid();
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+ public DateTime CreationDate { get; set; } = DateTime.UtcNow;
+ public DateTime? StartTime { get; set; }
+ public DateTime? FinishTime { get; set; }
+ public TimeSpan? TotalFloat { get; set; }
+
+ /// Root-level tasks in the schedule Work Breakdown Structure (WBS).
+ public List RootTasks { get; } = [];
+
+ public IfcWorkSchedule() { }
+
+ public IfcWorkSchedule(string name, string? description = null)
+ {
+ Name = name;
+ Description = description;
+ }
+
+ /// Enumerates all tasks in the schedule across the hierarchical WBS tree.
+ public IEnumerable AllTasks
+ {
+ get
+ {
+ var stack = new Stack(RootTasks);
+ while (stack.Count > 0)
+ {
+ var current = stack.Pop();
+ yield return current;
+ for (int i = current.SubTasks.Count - 1; i >= 0; i--)
+ {
+ stack.Push(current.SubTasks[i]);
+ }
+ }
+ }
+ }
+
+ /// Adds a new root task to the schedule.
+ public IfcTask AddTask(string name, string? identification = null, DateTime? start = null, DateTime? finish = null, IfcTaskType taskType = IfcTaskType.Construction)
+ {
+ var task = new IfcTask(name, identification, taskType)
+ {
+ Time = (start.HasValue || finish.HasValue) ? new IfcTaskTime(start, finish) : null
+ };
+ RootTasks.Add(task);
+ return task;
+ }
+
+ /// Finds a task by its WBS identification code or task name.
+ public IfcTask? FindTask(string identificationOrName)
+ {
+ ArgumentNullException.ThrowIfNull(identificationOrName);
+ return AllTasks.FirstOrDefault(t =>
+ string.Equals(t.Identification, identificationOrName, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(t.Name, identificationOrName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ ///
+ /// Gets all active tasks at the specified target simulation date.
+ ///
+ public IReadOnlyList GetActiveTasksAt(DateTime date)
+ {
+ var result = new List();
+ foreach (var task in AllTasks)
+ {
+ var start = task.Time?.ActualStartDate ?? task.Time?.PlannedStartDate;
+ var finish = task.Time?.ActualFinishDate ?? task.Time?.PlannedFinishDate;
+
+ if (start.HasValue && finish.HasValue && date >= start.Value && date <= finish.Value)
+ {
+ result.Add(task);
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Determines the temporal 4D construction state of an element at the specified date.
+ ///
+ public IfcElementSimulationState GetElementStateAt(IfcElement element, DateTime date)
+ {
+ ArgumentNullException.ThrowIfNull(element);
+
+ // Find all tasks assigned to this element
+ var assignedTasks = AllTasks.Where(t => t.AssignedElements.Contains(element)).ToList();
+ if (assignedTasks.Count == 0)
+ {
+ return IfcElementSimulationState.Constructed;
+ }
+
+ bool anyActive = false;
+ bool allFinished = true;
+ bool anyDemolished = false;
+
+ foreach (var task in assignedTasks)
+ {
+ var start = task.Time?.ActualStartDate ?? task.Time?.PlannedStartDate;
+ var finish = task.Time?.ActualFinishDate ?? task.Time?.PlannedFinishDate;
+
+ if (!start.HasValue || !finish.HasValue)
+ {
+ continue;
+ }
+
+ if (date < start.Value)
+ {
+ allFinished = false;
+ }
+ else if (date >= start.Value && date <= finish.Value)
+ {
+ anyActive = true;
+ allFinished = false;
+ if (task.TaskType == IfcTaskType.Demolition)
+ {
+ anyDemolished = true;
+ }
+ }
+ else if (date > finish.Value)
+ {
+ if (task.TaskType == IfcTaskType.Demolition)
+ {
+ anyDemolished = true;
+ }
+ }
+ }
+
+ if (anyDemolished) return IfcElementSimulationState.Demolished;
+ if (anyActive) return IfcElementSimulationState.InProgress;
+ if (allFinished) return IfcElementSimulationState.Constructed;
+
+ return IfcElementSimulationState.Unstarted;
+ }
+
+ ///
+ /// Calculates the overall schedule progress percentage (0.0 to 100.0) at the specified date.
+ ///
+ public double GetProgressAt(DateTime date)
+ {
+ var tasksWithDates = AllTasks.Where(t =>
+ (t.Time?.ActualStartDate ?? t.Time?.PlannedStartDate).HasValue &&
+ (t.Time?.ActualFinishDate ?? t.Time?.PlannedFinishDate).HasValue).ToList();
+
+ if (tasksWithDates.Count == 0) return 100.0;
+
+ double completedWeight = 0;
+ foreach (var task in tasksWithDates)
+ {
+ var start = (task.Time?.ActualStartDate ?? task.Time?.PlannedStartDate)!.Value;
+ var finish = (task.Time?.ActualFinishDate ?? task.Time?.PlannedFinishDate)!.Value;
+
+ if (date >= finish)
+ {
+ completedWeight += 1.0;
+ }
+ else if (date > start && finish > start)
+ {
+ double fraction = (date - start).TotalSeconds / (finish - start).TotalSeconds;
+ completedWeight += Math.Clamp(fraction, 0.0, 1.0);
+ }
+ }
+
+ return Math.Round((completedWeight / tasksWithDates.Count) * 100.0, 2);
+ }
+}
diff --git a/tests/IfcIO.Tests/BcfArchiveTests.cs b/tests/IfcIO.Tests/BcfArchiveTests.cs
new file mode 100644
index 0000000..861fbe2
--- /dev/null
+++ b/tests/IfcIO.Tests/BcfArchiveTests.cs
@@ -0,0 +1,65 @@
+using System.Text;
+using IfcIO.Bcf;
+using IfcIO.Geometry;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class BcfArchiveTests
+{
+ [Fact]
+ public void BcfArchive_RoundtripWithSnapshots_PreservesTopicsAndViewpoints()
+ {
+ var archive = new BcfArchive("MEP Coordination");
+
+ var topic = archive.AddTopic("Pipe and Beam Interference", "Main chilled water pipe clashes with structural beam", "Clash", "Critical");
+ topic.CreationAuthor = "BIM Manager";
+ topic.AssignedTo = "HVAC Lead";
+ topic.AddComment("Please lower the pipe run by 150mm.", "Structure Lead");
+
+ var vp = new BcfViewpoint
+ {
+ CameraPosition = new IfcVector3(10.0, 5.0, 3.0),
+ CameraDirection = new IfcVector3(0.0, 1.0, 0.0),
+ CameraUpVector = new IfcVector3(0.0, 0.0, 1.0),
+ FieldOfView = 60.0,
+ SnapshotData = Encoding.UTF8.GetBytes("FAKE_PNG_BINARY_BYTES_FOR_TEST")
+ };
+ vp.SelectedElementGuids.Add("3$v9a8K2n1_abcDEF0123");
+ vp.HiddenElementGuids.Add("1$a2b3c4d5_xyzUVW9876");
+ topic.Viewpoints.Add(vp);
+
+ using var ms = new MemoryStream();
+ archive.Save(ms);
+ ms.Position = 0;
+
+ var loaded = BcfArchive.Open(ms);
+ Assert.Single(loaded.Topics);
+ var loadedTopic = loaded.Topics[0];
+
+ Assert.Equal("Pipe and Beam Interference", loadedTopic.Title);
+ Assert.Equal("Critical", loadedTopic.Priority);
+ Assert.Equal("BIM Manager", loadedTopic.CreationAuthor);
+ Assert.Equal("HVAC Lead", loadedTopic.AssignedTo);
+ Assert.Single(loadedTopic.Comments);
+ Assert.Equal("Please lower the pipe run by 150mm.", loadedTopic.Comments[0].Comment);
+
+ Assert.Single(loadedTopic.Viewpoints);
+ var loadedVp = loadedTopic.Viewpoints[0];
+ Assert.NotNull(loadedVp.CameraPosition);
+ Assert.Equal(10.0, loadedVp.CameraPosition.Value.X);
+ Assert.Single(loadedVp.SelectedElementGuids);
+ Assert.Equal("3$v9a8K2n1_abcDEF0123", loadedVp.SelectedElementGuids[0]);
+ Assert.Single(loadedVp.HiddenElementGuids);
+ Assert.Equal("1$a2b3c4d5_xyzUVW9876", loadedVp.HiddenElementGuids[0]);
+ Assert.NotNull(loadedVp.SnapshotData);
+ Assert.Equal("FAKE_PNG_BINARY_BYTES_FOR_TEST", Encoding.UTF8.GetString(loadedVp.SnapshotData));
+
+ // Test archive search helpers
+ var foundByElem = loaded.FindTopicsByElement("3$v9a8K2n1_abcDEF0123");
+ Assert.Single(foundByElem);
+
+ var foundByStatus = loaded.FindTopicsByStatus("Open");
+ Assert.Single(foundByStatus);
+ }
+}
diff --git a/tests/IfcIO.Tests/CliExtendedTests.cs b/tests/IfcIO.Tests/CliExtendedTests.cs
new file mode 100644
index 0000000..3d4af07
--- /dev/null
+++ b/tests/IfcIO.Tests/CliExtendedTests.cs
@@ -0,0 +1,97 @@
+using System.Text;
+using IfcIO.Bcf;
+using IfcIO.Cli;
+using IfcIO.Model;
+using IfcIO.Model.Cost;
+using IfcIO.Model.Sequence;
+using IfcIO.Schema;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class CliExtendedTests
+{
+ private static string CreateModelWithScheduleAndCost()
+ {
+ var model = IfcModel.Create("Scheduled Tower", IfcSchema.Ifc4);
+ var storey = model.AddStorey("L1", 0.0);
+ var wall = storey.AddWall("Exterior Wall");
+
+ var schedule = model.AddWorkSchedule("Tower Schedule");
+ schedule.StartTime = new DateTime(2026, 1, 1);
+ schedule.FinishTime = new DateTime(2026, 6, 30);
+ var task = schedule.AddTask("Construct Walls", "WBS-1.1", new DateTime(2026, 2, 1), new DateTime(2026, 3, 1));
+ task.AssignElement(wall);
+
+ var costSchedule = model.AddCostSchedule("Master Budget", "USD", IfcCostScheduleType.Budget);
+ var costItem = costSchedule.AddCostItem("Wall Masonry", "04.20.00", unitRate: 150.0, quantity: 200.0, unitBasis: "m2");
+ costItem.AssignElement(wall);
+
+ string path = Path.Combine(Path.GetTempPath(), $"scheduled_{Guid.NewGuid():N}.ifc");
+ model.Save(path);
+ return path;
+ }
+
+ [Fact]
+ public void Cli_Schedule_ExecutesSuccessfullyAndExportsCsv()
+ {
+ string ifcPath = CreateModelWithScheduleAndCost();
+ string csvPath = Path.Combine(Path.GetTempPath(), $"schedule_{Guid.NewGuid():N}.csv");
+ try
+ {
+ int code = Program.Main(["schedule", ifcPath, "--sim-date", "2026-02-15", "--export-csv", csvPath]);
+ Assert.Equal(0, code);
+ Assert.True(File.Exists(csvPath));
+ string csvContent = File.ReadAllText(csvPath);
+ Assert.Contains("Tower Schedule", csvContent);
+ Assert.Contains("Construct Walls", csvContent);
+ }
+ finally
+ {
+ if (File.Exists(ifcPath)) File.Delete(ifcPath);
+ if (File.Exists(csvPath)) File.Delete(csvPath);
+ }
+ }
+
+ [Fact]
+ public void Cli_Cost_ExecutesSuccessfullyAndExportsCsv()
+ {
+ string ifcPath = CreateModelWithScheduleAndCost();
+ string csvPath = Path.Combine(Path.GetTempPath(), $"cost_{Guid.NewGuid():N}.csv");
+ try
+ {
+ int code = Program.Main(["cost", ifcPath, "--export-csv", csvPath]);
+ Assert.Equal(0, code);
+ Assert.True(File.Exists(csvPath));
+ string csvContent = File.ReadAllText(csvPath);
+ Assert.Contains("Master Budget", csvContent);
+ Assert.Contains("Wall Masonry", csvContent);
+ Assert.Contains("30000", csvContent); // 150 * 200
+ }
+ finally
+ {
+ if (File.Exists(ifcPath)) File.Delete(ifcPath);
+ if (File.Exists(csvPath)) File.Delete(csvPath);
+ }
+ }
+
+ [Fact]
+ public void Cli_Bcf_Read_ExecutesSuccessfully()
+ {
+ string bcfPath = Path.Combine(Path.GetTempPath(), $"issues_{Guid.NewGuid():N}.bcfzip");
+ try
+ {
+ var archive = new BcfArchive("Coordination Check");
+ var topic = archive.AddTopic("Clash at Column C3", "MEP duct interferes with structural column", "Clash", "High");
+ topic.AddComment("Reviewing reroute options.", "Coordinator");
+ archive.Save(bcfPath);
+
+ int code = Program.Main(["bcf", "read", bcfPath]);
+ Assert.Equal(0, code);
+ }
+ finally
+ {
+ if (File.Exists(bcfPath)) File.Delete(bcfPath);
+ }
+ }
+}
diff --git a/tests/IfcIO.Tests/CostTests.cs b/tests/IfcIO.Tests/CostTests.cs
new file mode 100644
index 0000000..a309156
--- /dev/null
+++ b/tests/IfcIO.Tests/CostTests.cs
@@ -0,0 +1,70 @@
+using IfcIO.Model;
+using IfcIO.Model.Cost;
+using IfcIO.Schema;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class CostTests
+{
+ [Fact]
+ public void CostSchedule_CanCalculateRollupCosts()
+ {
+ var schedule = new IfcCostSchedule("Tower Cost Plan", "USD", IfcCostScheduleType.Budget);
+
+ var concreteWork = schedule.AddCostItem("Concrete Superstructure", "03.00.00");
+ var slabItem = concreteWork.AddSubItem("Cast-in-Place Floor Slabs", "03.31.00", unitRate: 250.0, quantity: 120.0, unitBasis: "m3");
+ var columnItem = concreteWork.AddSubItem("Structural Columns", "03.32.00", unitRate: 400.0, quantity: 35.0, unitBasis: "m3");
+
+ Assert.Equal(30000.0, slabItem.TotalCost); // 250 * 120
+ Assert.Equal(14000.0, columnItem.TotalCost); // 400 * 35
+ Assert.Equal(44000.0, concreteWork.TotalCost);
+ Assert.Equal(44000.0, schedule.GetTotalCost());
+ }
+
+ [Fact]
+ public void CostItem_CanAssignElementsAndCostValues()
+ {
+ var item = new IfcCostItem("Site Earthwork", "02.30.00");
+ item.AddCostValue(15000.0, "USD", "Labor");
+ item.AddCostValue(25000.0, "USD", "Equipment");
+ item.AddCostValue(5000.0, "USD", "Material");
+
+ var slab = new IfcElement { Id = 1, Name = "Foundation Slab" };
+ item.AssignElement(slab);
+
+ Assert.Equal(45000.0, item.TotalCost);
+ Assert.Single(item.AssignedElements);
+ Assert.Equal("Foundation Slab", item.AssignedElements[0].Name);
+ }
+
+ [Theory]
+ [InlineData(IfcSchema.Ifc4)]
+ [InlineData(IfcSchema.Ifc2x3)]
+ public void CostSchedule_StepRoundtrip_PreservesCostItems(IfcSchema schema)
+ {
+ var model = IfcModel.Create("Budget Project", schema);
+ var storey = model.AddStorey("Storey 1", 0.0);
+ var slab = storey.AddSlab("Ground Slab");
+
+ var costSchedule = model.AddCostSchedule("Master Budget", "USD", IfcCostScheduleType.Budget);
+ var costItem = costSchedule.AddCostItem("Slab Package", "03.30.00", unitRate: 300.0, quantity: 50.0, unitBasis: "m3");
+ costItem.AssignElement(slab);
+
+ using var ms = new MemoryStream();
+ model.Save(ms);
+ ms.Position = 0;
+
+ var loaded = IfcModel.Open(ms);
+ Assert.Single(loaded.CostSchedules);
+ var loadedCs = loaded.CostSchedules[0];
+ Assert.Equal("Master Budget", loadedCs.Name);
+ Assert.Single(loadedCs.RootCostItems);
+
+ var loadedItem = loadedCs.RootCostItems[0];
+ Assert.Equal("Slab Package", loadedItem.Name);
+ Assert.Equal("03.30.00", loadedItem.CostCode);
+ Assert.Single(loadedItem.AssignedElements);
+ Assert.Equal(slab.Name, loadedItem.AssignedElements[0].Name);
+ }
+}
diff --git a/tests/IfcIO.Tests/IfcApiTests.cs b/tests/IfcIO.Tests/IfcApiTests.cs
new file mode 100644
index 0000000..d30ae80
--- /dev/null
+++ b/tests/IfcIO.Tests/IfcApiTests.cs
@@ -0,0 +1,52 @@
+using IfcIO.Api;
+using IfcIO.Model.Cost;
+using IfcIO.Model.Sequence;
+using IfcIO.Schema;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class IfcApiTests
+{
+ [Fact]
+ public void IfcApi_CanBuildFullBimModelWithSequenceAndCost()
+ {
+ // 1. Root & Spatial
+ var model = IfcApi.Root.CreateProject("API Modern Campus", IfcSchema.Ifc4);
+ var storey1 = IfcApi.Root.CreateStorey(model, "Ground Level", 0.0);
+
+ var wall1 = IfcApi.Root.CreateWall(storey1, "Perimeter Wall 1");
+ var slab1 = IfcApi.Root.CreateSlab(storey1, "Ground Slab");
+ var col1 = IfcApi.Root.CreateColumn(storey1, "Corner Column");
+ var beam1 = IfcApi.Root.CreateBeam(storey1, "Transfer Beam");
+
+ Assert.Equal(4, storey1.Elements.Count);
+
+ // 2. Spatial Zones
+ var space = model.AddSpace("Lobby", "101");
+ var zone = IfcApi.Spatial.AddZone(model, "Ground Thermal Zone", "Thermal", space);
+ Assert.Single(zone.Spaces);
+
+ // 3. 4D Sequencing
+ var schedule = IfcApi.Sequence.AddWorkSchedule(model, "Master Construction Schedule");
+ var taskSlab = IfcApi.Sequence.AddTask(schedule, "Pour Slab", "1.1", new DateTime(2026, 1, 1), new DateTime(2026, 1, 10));
+ var taskWalls = IfcApi.Sequence.AddTask(schedule, "Erect Walls", "1.2", new DateTime(2026, 1, 11), new DateTime(2026, 1, 20));
+
+ IfcApi.Sequence.AssignProcess(taskSlab, slab1);
+ IfcApi.Sequence.AssignProcess(taskWalls, wall1, col1, beam1);
+ IfcApi.Sequence.AddSequence(taskSlab, taskWalls, IfcSequenceType.FinishToStart);
+
+ Assert.Single(taskSlab.AssignedElements);
+ Assert.Equal(3, taskWalls.AssignedElements.Count);
+ Assert.Single(taskSlab.Successors);
+
+ // 4. 5D Cost Planning
+ var costPlan = IfcApi.Cost.AddCostSchedule(model, "Baseline Cost Plan", "USD", IfcCostScheduleType.Budget);
+ var itemConcrete = IfcApi.Cost.AddCostItem(costPlan, "Concrete Structures", "03.00.00", unitRate: 350.0, quantity: 80.0, unitBasis: "m3");
+ IfcApi.Cost.AssignCostControl(itemConcrete, slab1, wall1, col1, beam1);
+
+ Assert.Equal(28000.0, itemConcrete.TotalCost);
+ Assert.Equal(28000.0, costPlan.GetTotalCost());
+ Assert.Equal(28000.0, model.CalculateTotalCost());
+ }
+}
diff --git a/tests/IfcIO.Tests/SequenceTests.cs b/tests/IfcIO.Tests/SequenceTests.cs
new file mode 100644
index 0000000..f438850
--- /dev/null
+++ b/tests/IfcIO.Tests/SequenceTests.cs
@@ -0,0 +1,115 @@
+using IfcIO.Core;
+using IfcIO.Geometry;
+using IfcIO.Model;
+using IfcIO.Model.Sequence;
+using IfcIO.Schema;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class SequenceTests
+{
+ [Fact]
+ public void WorkSchedule_CanCreateHierarchyAndSequences()
+ {
+ var schedule = new IfcWorkSchedule("Construction Master Schedule");
+
+ var phase1 = schedule.AddTask("Substructure", "1.0", new DateTime(2026, 1, 1), new DateTime(2026, 3, 31));
+ var taskExcavation = phase1.AddSubTask("Bulk Excavation", "1.1", new DateTime(2026, 1, 1), new DateTime(2026, 1, 31));
+ var taskFoundations = phase1.AddSubTask("Reinforced Footings", "1.2", new DateTime(2026, 2, 1), new DateTime(2026, 3, 15));
+
+ var rel = taskExcavation.AddSuccessor(taskFoundations, IfcSequenceType.FinishToStart, TimeSpan.FromDays(1));
+
+ Assert.Single(schedule.RootTasks);
+ Assert.Equal(3, schedule.AllTasks.Count());
+ Assert.Equal("1.0", phase1.Identification);
+ Assert.Equal("1.1", taskExcavation.Identification);
+ Assert.Equal("1.2", taskFoundations.Identification);
+ Assert.Single(taskExcavation.Successors);
+ Assert.Single(taskFoundations.Predecessors);
+ Assert.Equal(IfcSequenceType.FinishToStart, rel.SequenceType);
+ Assert.Equal(TimeSpan.FromDays(1), rel.TimeLag);
+ }
+
+ [Fact]
+ public void WorkSchedule_SimulationState_EvaluatesCorrectly()
+ {
+ var schedule = new IfcWorkSchedule("Simulation Schedule");
+ var wallElement = new IfcElement { Id = 101, Name = "Bearing Wall 1" };
+
+ var task = schedule.AddTask(
+ "Erect Bearing Wall",
+ "WBS-1",
+ new DateTime(2026, 6, 1),
+ new DateTime(2026, 6, 10),
+ IfcTaskType.Construction);
+ task.AssignElement(wallElement);
+
+ // Before start date
+ var stateBefore = schedule.GetElementStateAt(wallElement, new DateTime(2026, 5, 20));
+ Assert.Equal(IfcElementSimulationState.Unstarted, stateBefore);
+
+ // During construction
+ var stateDuring = schedule.GetElementStateAt(wallElement, new DateTime(2026, 6, 5));
+ Assert.Equal(IfcElementSimulationState.InProgress, stateDuring);
+
+ // After completion
+ var stateAfter = schedule.GetElementStateAt(wallElement, new DateTime(2026, 6, 15));
+ Assert.Equal(IfcElementSimulationState.Constructed, stateAfter);
+
+ // Active tasks
+ var active = schedule.GetActiveTasksAt(new DateTime(2026, 6, 5));
+ Assert.Single(active);
+ Assert.Equal(task, active[0]);
+
+ // Progress percentage
+ double progBefore = schedule.GetProgressAt(new DateTime(2026, 5, 1));
+ double progMid = schedule.GetProgressAt(new DateTime(2026, 6, 5)); // halfway
+ double progAfter = schedule.GetProgressAt(new DateTime(2026, 7, 1));
+
+ Assert.Equal(0.0, progBefore);
+ Assert.InRange(progMid, 40.0, 60.0);
+ Assert.Equal(100.0, progAfter);
+ }
+
+ [Theory]
+ [InlineData(IfcSchema.Ifc4)]
+ [InlineData(IfcSchema.Ifc2x3)]
+ public void WorkSchedule_StepRoundtrip_PreservesScheduleAndTasks(IfcSchema schema)
+ {
+ var model = IfcModel.Create("Hospital Project", schema);
+ var storey = model.AddStorey("Level 1", 0.0);
+ var wall = storey.AddWall("Exterior Wall East");
+
+ var schedule = model.AddWorkSchedule("Hospital Schedule", "Main Construction Plan");
+ schedule.StartTime = new DateTime(2026, 1, 1);
+ schedule.FinishTime = new DateTime(2026, 12, 31);
+
+ var taskA = schedule.AddTask("Foundation Work", "1.0", new DateTime(2026, 1, 1), new DateTime(2026, 3, 31));
+ var taskB = schedule.AddTask("Wall Framing", "2.0", new DateTime(2026, 4, 1), new DateTime(2026, 6, 30));
+ taskB.AssignElement(wall);
+ taskA.AddSuccessor(taskB, IfcSequenceType.FinishToStart);
+
+ using var ms = new MemoryStream();
+ model.Save(ms);
+ ms.Position = 0;
+
+ var loaded = IfcModel.Open(ms);
+ Assert.Single(loaded.WorkSchedules);
+ var loadedSchedule = loaded.WorkSchedules[0];
+ Assert.Equal("Hospital Schedule", loadedSchedule.Name);
+ Assert.Equal(2, loadedSchedule.RootTasks.Count);
+
+ var loadedTaskA = loadedSchedule.FindTask("1.0");
+ var loadedTaskB = loadedSchedule.FindTask("2.0");
+
+ Assert.NotNull(loadedTaskA);
+ Assert.NotNull(loadedTaskB);
+ Assert.Equal("Foundation Work", loadedTaskA.Name);
+ Assert.Equal("Wall Framing", loadedTaskB.Name);
+ Assert.Single(loadedTaskB.AssignedElements);
+ Assert.Equal(wall.Name, loadedTaskB.AssignedElements[0].Name);
+ Assert.Single(loadedTaskA.Successors);
+ Assert.Equal(loadedTaskB, loadedTaskA.Successors[0].RelatedProcess);
+ }
+}
diff --git a/tests/IfcIO.Tests/SpatialZoneTests.cs b/tests/IfcIO.Tests/SpatialZoneTests.cs
new file mode 100644
index 0000000..148abf9
--- /dev/null
+++ b/tests/IfcIO.Tests/SpatialZoneTests.cs
@@ -0,0 +1,50 @@
+using IfcIO.Model;
+using IfcIO.Schema;
+using Xunit;
+
+namespace IfcIO.Tests;
+
+public class SpatialZoneTests
+{
+ [Fact]
+ public void Zone_CanAddSpacesAndManageProperties()
+ {
+ var model = IfcModel.Create("Residential Complex");
+ var space1 = model.AddSpace("Living Room", "101");
+ var space2 = model.AddSpace("Kitchen", "102");
+
+ var zone = model.AddZone("Thermal Zone 1", "ThermalZone", [space1, space2]);
+
+ Assert.Equal("Thermal Zone 1", zone.Name);
+ Assert.Equal("ThermalZone", zone.ZoneType);
+ Assert.Equal(2, zone.Spaces.Count);
+ Assert.Contains(space1, zone.Spaces);
+ Assert.Contains(space2, zone.Spaces);
+ }
+
+ [Theory]
+ [InlineData(IfcSchema.Ifc4)]
+ [InlineData(IfcSchema.Ifc2x3)]
+ public void Zone_StepRoundtrip_PreservesZonesAndSpaces(IfcSchema schema)
+ {
+ var model = IfcModel.Create("Office Tower", schema);
+ var spaceA = model.AddSpace("Server Room", "SR-01");
+ var spaceB = model.AddSpace("Electrical Room", "ER-01");
+
+ var fireZone = model.AddZone("Fire Compartment A", "FireSafety", [spaceA, spaceB]);
+
+ using var ms = new MemoryStream();
+ model.Save(ms);
+ ms.Position = 0;
+
+ var loaded = IfcModel.Open(ms);
+ Assert.Single(loaded.Zones);
+
+ var loadedZone = loaded.Zones[0];
+ Assert.Equal("Fire Compartment A", loadedZone.Name);
+ Assert.Equal("FireSafety", loadedZone.ZoneType);
+ Assert.Equal(2, loadedZone.Spaces.Count);
+ Assert.Contains(loadedZone.Spaces, s => s.Name == "Server Room");
+ Assert.Contains(loadedZone.Spaces, s => s.Name == "Electrical Room");
+ }
+}