pi-revit 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +388 -351
  2. package/README.md +426 -22
  3. package/extensions/pi-revit/index.ts +246 -187
  4. package/extensions/pi-revit/instance-router.ts +86 -0
  5. package/extensions/pi-revit/script-library.ts +146 -0
  6. package/extensions/pi-revit/tool-catalog.ts +67 -0
  7. package/package.json +59 -59
  8. package/skills/pi-revit/SKILL.md +196 -32
  9. package/skills/pi-revit/references/model-audit-export.md +29 -0
  10. package/skills/pi-revit/references/room-documentation.md +28 -0
  11. package/src/Revit/BridgeServer.cs +87 -37
  12. package/src/Revit/OperationStore.cs +178 -0
  13. package/src/Revit/ToolRegistry.cs +57 -35
  14. package/src/Revit/Tools/CaptureView.cs +2 -1
  15. package/src/Revit/Tools/ChangeElementTypes.cs +60 -0
  16. package/src/Revit/Tools/CreateTags.cs +95 -0
  17. package/src/Revit/Tools/DeleteElements.cs +44 -0
  18. package/src/Revit/Tools/DocumentGuard.cs +64 -64
  19. package/src/Revit/Tools/ElementQueryScope.cs +27 -0
  20. package/src/Revit/Tools/ExecuteCsharp.cs +45 -35
  21. package/src/Revit/Tools/ExportDocuments.cs +121 -120
  22. package/src/Revit/Tools/FailureGuard.cs +26 -26
  23. package/src/Revit/Tools/GetElementDetails.cs +42 -12
  24. package/src/Revit/Tools/GetElementRelationships.cs +76 -0
  25. package/src/Revit/Tools/GetElements.cs +37 -23
  26. package/src/Revit/Tools/GetLinkedElements.cs +82 -0
  27. package/src/Revit/Tools/GetLinkedModels.cs +66 -0
  28. package/src/Revit/Tools/GetModelCoordinates.cs +49 -0
  29. package/src/Revit/Tools/GetModelOverview.cs +2 -2
  30. package/src/Revit/Tools/GetScheduleFields.cs +37 -0
  31. package/src/Revit/Tools/GetSchedules.cs +89 -0
  32. package/src/Revit/Tools/ManageElementSets.cs +106 -0
  33. package/src/Revit/Tools/ManageSchedules.cs +164 -0
  34. package/src/Revit/Tools/ManageSelection.cs +37 -36
  35. package/src/Revit/Tools/ManageSheetPlacements.cs +97 -0
  36. package/src/Revit/Tools/ManageSheets.cs +63 -0
  37. package/src/Revit/Tools/ManageViews.cs +100 -0
  38. package/src/Revit/Tools/MeasureGeometry.cs +54 -0
  39. package/src/Revit/Tools/ModelEditBatch.cs +102 -0
  40. package/src/Revit/Tools/ModelEditInputs.cs +49 -0
  41. package/src/Revit/Tools/OpenView.cs +2 -1
  42. package/src/Revit/Tools/QuerySpatialElements.cs +63 -0
  43. package/src/Revit/Tools/SetParameters.cs +43 -93
  44. package/src/Revit/Tools/SpatialBounds.cs +30 -0
  45. package/src/Revit/Tools/SummarizeElements.cs +87 -0
  46. package/src/Revit/Tools/ToolSupport.cs +1 -1
  47. package/src/Revit/Tools/TransformElements.cs +58 -0
  48. package/workspace/AGENTS.md +45 -45
@@ -0,0 +1,27 @@
1
+ using System.Text.Json;
2
+ using System.Text.Json.Nodes;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal static class ElementQueryScope
7
+ {
8
+ public static JsonObject Schema()
9
+ {
10
+ var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
11
+ foreach (string key in new[] { "count_only", "fields", "offset", "limit", "parameter_names", "include_type_parameters" })
12
+ schema["properties"]!.AsObject().Remove(key);
13
+ schema["description"] = "Filter the whole matching scope. Query paging and projections are not supported here.";
14
+ schema["additionalProperties"] = false;
15
+ return schema;
16
+ }
17
+
18
+ public static JsonObject Parse(JsonElement args)
19
+ {
20
+ var query = args.TryGetProperty("query", out var input) ? JsonNode.Parse(input.GetRawText()) as JsonObject : new JsonObject();
21
+ if (query == null) throw new ArgumentException("query must be an object.");
22
+ var allowed = Schema()["properties"]!.AsObject();
23
+ foreach (var property in query)
24
+ if (!allowed.ContainsKey(property.Key)) throw new ArgumentException($"query does not support '{property.Key}'. It always uses the whole matching scope.");
25
+ return query;
26
+ }
27
+ }
@@ -20,11 +20,12 @@ namespace RevitBridge.Tools
20
20
  /// </summary>
21
21
  public sealed class ScriptGlobals
22
22
  {
23
- internal ScriptGlobals(Document document, UIDocument uiDocument, UIApplication uiApplication, Action<object?> dump)
23
+ internal ScriptGlobals(Document document, UIDocument uiDocument, UIApplication uiApplication, JsonElement inputValues, Action<object?> dump)
24
24
  {
25
25
  doc = document;
26
26
  uidoc = uiDocument;
27
- uiapp = uiApplication;
27
+ uiapp = uiApplication;
28
+ inputs = inputValues;
28
29
  Dump = dump;
29
30
  }
30
31
 
@@ -35,7 +36,10 @@ namespace RevitBridge.Tools
35
36
  public UIDocument uidoc { get; }
36
37
 
37
38
  /// <summary>The Revit UI application.</summary>
38
- public UIApplication uiapp { get; }
39
+ public UIApplication uiapp { get; }
40
+
41
+ /// <summary>Structured inputs supplied separately from the script source.</summary>
42
+ public JsonElement inputs { get; }
39
43
 
40
44
  /// <summary>Records a value into the result's dumps[] (safely projected at call time).</summary>
41
45
  public Action<object?> Dump { get; }
@@ -59,19 +63,21 @@ namespace RevitBridge.Tools
59
63
 
60
64
  public string Name => "execute_csharp";
61
65
  public string Label => "Execute C#";
62
- public string Description => "Compile and run a C# script on the Revit API thread against the open model — the escape hatch for anything without a dedicated tool: element creation, deletion, geometry edits (move/copy/rotate), views, sheets, schedules, tagging, families, links, worksets. Globals: doc (Document), uidoc (UIDocument), uiapp (UIApplication), and Dump(value) to record intermediate values into the result's dumps[]. Default imports: System, System.Linq, System.Collections.Generic, Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives at the top for sub-namespaces (e.g. using Autodesk.Revit.DB.Architecture;). The entire run is wrapped in ONE transaction named 'execute_csharp': committed on success; on failure rollback is attempted and its confirmed status is reported (do not open your own Transaction; sub-transactions are fine). The script's final expression or return statement becomes returnValue; return primitives, strings, or anonymous objects/lists — Revit API values are projected to safe shapes (Element -> {id,name,category,typeName,levelId}, ElementId -> number, XYZ -> {x,y,z}, Parameter -> {name,value,displayValue}; other API objects become strings) with depth and item caps, so never rely on raw API objects round-tripping. Lengths are in internal units (decimal feet) — convert with UnitUtils. Prefer collector-level filtering (FilteredElementCollector .OfCategory/.OfClass/.WhereElementIsNotElementType) and bounded loops: the call budget is 120s and Revit cannot be interrupted mid-script. Scripts must be fully synchronous — await/async is rejected at compile time, and blocking on tasks (Task.Result/.Wait()) can freeze Revit. Modal dialogs raised while running are auto-dismissed and reported in suppressedDialogs — unrecognized dialogs are answered dismissively (Cancel/Close/No) rather than confirmed, so an operation that raises a confirmation prompt may be cancelled; check suppressedDialogs when a result looks incomplete. Verify unfamiliar signatures with search_api_docs first.";
63
- public bool Write => true;
66
+ public string Description => "Compile and run a C# script on the Revit API thread against the open model — the escape hatch for anything without a dedicated tool: element creation, deletion, geometry edits (move/copy/rotate), views, sheets, schedules, tagging, families, links, worksets. Globals: doc (Document), uidoc (UIDocument), uiapp (UIApplication), inputs (System.Text.Json.JsonElement, defaults to an empty object), and Dump(value) to record intermediate values into the result's dumps[]. Default imports: System, System.Linq, System.Collections.Generic, Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives at the top for sub-namespaces (e.g. using Autodesk.Revit.DB.Architecture;). The entire run is wrapped in ONE transaction named 'execute_csharp': committed on success; on failure rollback is attempted and its confirmed status is reported (do not open your own Transaction; sub-transactions are fine). The script's final expression or return statement becomes returnValue; return primitives, strings, or anonymous objects/lists — Revit API values are projected to safe shapes (Element -> {id,name,category,typeName,levelId}, ElementId -> number, XYZ -> {x,y,z}, Parameter -> {name,value,displayValue}; other API objects become strings) with depth and item caps, so never rely on raw API objects round-tripping. Lengths are in internal units (decimal feet) — convert with UnitUtils. Prefer collector-level filtering (FilteredElementCollector .OfCategory/.OfClass/.WhereElementIsNotElementType) and bounded loops: the call budget is 120s and Revit cannot be interrupted mid-script. Scripts must be fully synchronous — await/async is rejected at compile time, and blocking on tasks (Task.Result/.Wait()) can freeze Revit. Modal dialogs raised while running are auto-dismissed and reported in suppressedDialogs — unrecognized dialogs are answered dismissively (Cancel/Close/No) rather than confirmed, so an operation that raises a confirmation prompt may be cancelled; check suppressedDialogs when a result looks incomplete. Verify unfamiliar signatures with search_api_docs first.";
67
+ public bool Write => true;
68
+ public IReadOnlyList<string> Effects => new[] { "model", "ui", "files", "external" };
64
69
 
65
70
  public object ParametersSchema => new
66
71
  {
67
72
  type = "object",
68
73
  properties = new
69
74
  {
70
- code = new
75
+ code = new
71
76
  {
72
77
  type = "string",
73
- description = "C# script body (top-level statements; using directives allowed at the top). Globals doc/uidoc/uiapp and Dump(value) are in scope. The final expression or a return statement is the result.",
74
- },
78
+ description = "C# script body (top-level statements; using directives allowed at the top). Globals doc/uidoc/uiapp/inputs and Dump(value) are in scope. The final expression or a return statement is the result.",
79
+ },
80
+ inputs = new { type = "object", description = "Optional structured JSON inputs, available as the inputs JsonElement global. Default empty object. Values are never interpolated into source code." },
75
81
  expected_document = new
76
82
  {
77
83
  type = "string",
@@ -96,8 +102,11 @@ namespace RevitBridge.Tools
96
102
  DocumentGuard.CheckExpectedDocument(args, doc);
97
103
 
98
104
  string code = JsonArgs.GetString(args, "code") ?? string.Empty;
99
- if (string.IsNullOrWhiteSpace(code))
100
- throw new ArgumentException("code must be a non-empty C# script.");
105
+ if (string.IsNullOrWhiteSpace(code))
106
+ throw new ArgumentException("code must be a non-empty C# script.");
107
+ var inputValues = args.TryGetProperty("inputs", out var suppliedInputs) ? suppliedInputs.Clone() : JsonSerializer.SerializeToElement(new { });
108
+ if (inputValues.ValueKind != JsonValueKind.Object) throw new ArgumentException("inputs must be a JSON object.");
109
+ if (inputValues.GetRawText().Length > 100000) throw new ArgumentException("inputs must not exceed 100,000 JSON characters.");
101
110
 
102
111
  var stopwatch = Stopwatch.StartNew();
103
112
 
@@ -109,7 +118,7 @@ namespace RevitBridge.Tools
109
118
  RejectAsyncCode(script.GetCompilation());
110
119
 
111
120
  var dumps = new List<object?>();
112
- var globals = new ScriptGlobals(doc, uidoc, uiapp, value =>
121
+ var globals = new ScriptGlobals(doc, uidoc, uiapp, inputValues, value =>
113
122
  {
114
123
  if (dumps.Count < MaxDumps)
115
124
  dumps.Add(Project(value, 0));
@@ -119,9 +128,9 @@ namespace RevitBridge.Tools
119
128
 
120
129
  using var dialogGuard = new DialogGuard(uiapp);
121
130
  using var transaction = new Transaction(doc, "execute_csharp");
122
- if (transaction.Start() != TransactionStatus.Started)
123
- throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
124
- var failureGuard = FailureGuard.Attach(transaction);
131
+ if (transaction.Start() != TransactionStatus.Started)
132
+ throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
133
+ var failureGuard = FailureGuard.Attach(transaction);
125
134
 
126
135
  object? returnValue;
127
136
  string? projectionError = null;
@@ -147,24 +156,24 @@ namespace RevitBridge.Tools
147
156
  }
148
157
  catch (Exception ex)
149
158
  {
150
- throw new InvalidOperationException(
151
- FormatRuntimeError(ex, dialogGuard.Suppressed) + " " + FailureGuard.RollBackAndDescribe(transaction), ex);
152
- }
153
-
154
- try
155
- {
156
- var status = transaction.Commit();
157
- var finalStatus = transaction.GetStatus();
158
- if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
159
- throw new InvalidOperationException($"The execute_csharp commit returned {status}; current transaction status is {finalStatus}.");
160
- }
161
- catch (Exception ex)
162
- {
163
- throw new InvalidOperationException(
164
- $"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}"
165
- + failureGuard.DescribeErrors()
166
- + DescribeDialogs(dialogGuard.Suppressed), ex);
167
- }
159
+ throw new InvalidOperationException(
160
+ FormatRuntimeError(ex, dialogGuard.Suppressed) + " " + FailureGuard.RollBackAndDescribe(transaction), ex);
161
+ }
162
+
163
+ try
164
+ {
165
+ var status = transaction.Commit();
166
+ var finalStatus = transaction.GetStatus();
167
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
168
+ throw new InvalidOperationException($"The execute_csharp commit returned {status}; current transaction status is {finalStatus}.");
169
+ }
170
+ catch (Exception ex)
171
+ {
172
+ throw new InvalidOperationException(
173
+ $"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}"
174
+ + failureGuard.DescribeErrors()
175
+ + DescribeDialogs(dialogGuard.Suppressed), ex);
176
+ }
168
177
 
169
178
  stopwatch.Stop();
170
179
 
@@ -194,7 +203,8 @@ namespace RevitBridge.Tools
194
203
  Assembly.Load("netstandard"),
195
204
  typeof(Enumerable).Assembly, // System.Linq
196
205
  typeof(Regex).Assembly, // System.Text.RegularExpressions
197
- typeof(Console).Assembly, // System.Console
206
+ typeof(Console).Assembly, // System.Console
207
+ typeof(JsonElement).Assembly, // System.Text.Json
198
208
  typeof(Document).Assembly, // RevitAPI
199
209
  typeof(UIApplication).Assembly) // RevitAPIUI
200
210
  .WithImports("System", "System.Linq", "System.Collections.Generic", "Autodesk.Revit.DB", "Autodesk.Revit.UI")
@@ -209,7 +219,7 @@ namespace RevitBridge.Tools
209
219
  });
210
220
  string more = errors.Count > MaxReportedErrors ? $"\n … +{errors.Count - MaxReportedErrors} more error(s)" : string.Empty;
211
221
  return $"C# compilation failed with {errors.Count} error(s):\n{string.Join("\n", lines)}{more}\n"
212
- + "Globals: doc, uidoc, uiapp, Dump(value). Default imports: System, System.Linq, System.Collections.Generic, "
222
+ + "Globals: doc, uidoc, uiapp, inputs (JsonElement), Dump(value). Default imports: System, System.Linq, System.Collections.Generic, "
213
223
  + "Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives for other namespaces.";
214
224
  }
215
225
 
@@ -241,7 +251,7 @@ namespace RevitBridge.Tools
241
251
  string line = TryGetScriptLine(ex) is { } scriptLine ? $" at script line {scriptLine}" : string.Empty;
242
252
  string inner = ex.InnerException is { } innerEx ? $" Inner: {innerEx.GetType().Name}: {innerEx.Message}" : string.Empty;
243
253
  return $"C# script threw {ex.GetType().Name}{line}: {ex.Message}.{inner}"
244
- + DescribeDialogs(suppressedDialogs);
254
+ + DescribeDialogs(suppressedDialogs);
245
255
  }
246
256
 
247
257
  private static string DescribeDialogs(IReadOnlyList<string> suppressed)
@@ -1,6 +1,6 @@
1
- using System.IO;
2
- using System.Security.Cryptography;
3
- using System.Text;
1
+ using System.IO;
2
+ using System.Security.Cryptography;
3
+ using System.Text;
4
4
  using System.Text.Json;
5
5
  using Autodesk.Revit.DB;
6
6
 
@@ -22,7 +22,8 @@ namespace RevitBridge.Tools
22
22
  public string Name => "export_documents";
23
23
  public string Label => "Export Documents";
24
24
  public string Description => "Export documents from the open Revit model. format 'pdf'/'dwg'/'png' export the given sheet/view ids to files: pdf combines everything into one file by default (combine=false writes one PDF per sheet/view, named by Revit's naming rule); png renders 2048 px wide; ifc exports the whole model, or just what one given view shows. Files sort themselves per model: with output_dir omitted they land in Documents\\pi-revit\\Models\\<model title>--<identity hash>\\exports, derived from the document being exported (pass output_dir only for a different explicit target); file_name_prefix sets the base file name (default: the document title; Revit appends view/sheet suffixes for multi-file exports). Returns the produced file paths with sizes. Find sheet/view ids with get_elements (category 'Sheets' or 'Views') first.";
25
- public bool Write => true;
25
+ public bool Write => true;
26
+ public IReadOnlyList<string> Effects => new[] { "model", "files" };
26
27
  public string Tier => "advanced";
27
28
 
28
29
  public object ParametersSchema => new
@@ -88,37 +89,37 @@ namespace RevitBridge.Tools
88
89
  // recent (a shared output_dir, another tool writing alongside) does not.
89
90
  var before = SnapshotWriteTimes(outputDir);
90
91
 
91
- IReadOnlyList<string> commitWarnings = Array.Empty<string>();
92
- try
93
- {
94
- switch (format)
95
- {
96
- case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
97
- case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
98
- case "png": ExportPng(doc, views, outputDir, baseName); break;
99
- default: commitWarnings = ExportIfc(doc, views, outputDir, baseName); break;
100
- }
101
- }
102
- catch (Exception ex)
103
- {
104
- // A failed export may already have written some files. A Revit
105
- // transaction rollback cannot undo those filesystem changes.
106
- string observed;
107
- try
108
- {
109
- var changed = FindChangedFiles(outputDir, before).ToList();
110
- observed = changed.Count == 0
111
- ? "No changed files were observed."
112
- : $"Observed {changed.Count} new or changed file(s): {string.Join(", ", changed.Take(20))}{(changed.Count > 20 ? " (additional files omitted)" : string.Empty)}.";
113
- }
114
- catch (Exception scanError)
115
- {
116
- observed = $"Could not inspect remaining files: {scanError.Message}.";
117
- }
118
- throw new InvalidOperationException($"{ex.Message} The export failed; files in '{outputDir}' may be incomplete and were not removed. {observed}", ex);
119
- }
120
-
121
- var files = FindChangedFiles(outputDir, before)
92
+ IReadOnlyList<string> commitWarnings = Array.Empty<string>();
93
+ try
94
+ {
95
+ switch (format)
96
+ {
97
+ case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
98
+ case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
99
+ case "png": ExportPng(doc, views, outputDir, baseName); break;
100
+ default: commitWarnings = ExportIfc(doc, views, outputDir, baseName); break;
101
+ }
102
+ }
103
+ catch (Exception ex)
104
+ {
105
+ // A failed export may already have written some files. A Revit
106
+ // transaction rollback cannot undo those filesystem changes.
107
+ string observed;
108
+ try
109
+ {
110
+ var changed = FindChangedFiles(outputDir, before).ToList();
111
+ observed = changed.Count == 0
112
+ ? "No changed files were observed."
113
+ : $"Observed {changed.Count} new or changed file(s): {string.Join(", ", changed.Take(20))}{(changed.Count > 20 ? " (additional files omitted)" : string.Empty)}.";
114
+ }
115
+ catch (Exception scanError)
116
+ {
117
+ observed = $"Could not inspect remaining files: {scanError.Message}.";
118
+ }
119
+ throw new InvalidOperationException($"{ex.Message} The export failed; files in '{outputDir}' may be incomplete and were not removed. {observed}", ex);
120
+ }
121
+
122
+ var files = FindChangedFiles(outputDir, before)
122
123
  .Select(path => new Dictionary<string, object?>
123
124
  {
124
125
  ["path"] = path,
@@ -129,23 +130,23 @@ namespace RevitBridge.Tools
129
130
  throw new InvalidOperationException($"The {format} export finished but produced no files in {outputDir}.");
130
131
 
131
132
  string sample = string.Join(", ", files.Take(3).Select(file => Path.GetFileName((string)file["path"]!)));
132
- string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
133
- if (commitWarnings.Count > 0)
134
- compact += $" {commitWarnings.Count} Revit warning(s) auto-dismissed (see commitWarnings).";
133
+ string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
134
+ if (commitWarnings.Count > 0)
135
+ compact += $" {commitWarnings.Count} Revit warning(s) auto-dismissed (see commitWarnings).";
135
136
  return new ToolOutput(new
136
137
  {
137
138
  format,
138
139
  outputDir,
139
140
  fileCount = files.Count,
140
- files,
141
- commitWarnings,
142
- }, compact);
143
- }
144
-
145
- private static IEnumerable<string> FindChangedFiles(string directory, IReadOnlyDictionary<string, DateTime> before)
146
- => Directory.GetFiles(directory)
147
- .Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
148
- .OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
141
+ files,
142
+ commitWarnings,
143
+ }, compact);
144
+ }
145
+
146
+ private static IEnumerable<string> FindChangedFiles(string directory, IReadOnlyDictionary<string, DateTime> before)
147
+ => Directory.GetFiles(directory)
148
+ .Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
149
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
149
150
 
150
151
  /// <summary>path -> last write time for the files already in the output directory.
151
152
  /// A file whose time cannot be read is left out, so the export reports it if it
@@ -227,29 +228,29 @@ namespace RevitBridge.Tools
227
228
  /// <summary>The IFC exporter writes IFC GUID parameters onto exported elements,
228
229
  /// so the Revit API requires a transaction around it — owned here, matching
229
230
  /// Revit's own behavior of committing those GUIDs on export.</summary>
230
- private static IReadOnlyList<string> ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
231
+ private static IReadOnlyList<string> ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
231
232
  {
232
233
  var options = new IFCExportOptions();
233
234
  if (views.Count == 1)
234
235
  options.FilterViewId = views[0].Id;
235
236
 
236
237
  using var transaction = new Transaction(doc, "export_documents: ifc");
237
- if (transaction.Start() != TransactionStatus.Started)
238
- throw new InvalidOperationException("Unable to start the IFC export transaction.");
239
- var failureGuard = FailureGuard.Attach(transaction);
238
+ if (transaction.Start() != TransactionStatus.Started)
239
+ throw new InvalidOperationException("Unable to start the IFC export transaction.");
240
+ var failureGuard = FailureGuard.Attach(transaction);
240
241
  try
241
242
  {
242
243
  if (!Run(() => doc.Export(outputDir, baseName, options), "IFC"))
243
244
  throw new InvalidOperationException("Revit reported a failed IFC export.");
244
- var status = transaction.Commit();
245
- var finalStatus = transaction.GetStatus();
246
- if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
247
- throw new InvalidOperationException($"The IFC export commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
248
- return failureGuard.Warnings;
249
- }
250
- catch (Exception ex)
251
- {
252
- throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
245
+ var status = transaction.Commit();
246
+ var finalStatus = transaction.GetStatus();
247
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
248
+ throw new InvalidOperationException($"The IFC export commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
249
+ return failureGuard.Warnings;
250
+ }
251
+ catch (Exception ex)
252
+ {
253
+ throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
253
254
  }
254
255
  }
255
256
 
@@ -310,72 +311,72 @@ namespace RevitBridge.Tools
310
311
  }
311
312
 
312
313
  /// <summary>Per-model folder under the pi-revit workspace:
313
- /// Documents\pi-revit\Models\&lt;model title&gt;--&lt;identity hash&gt;.
314
- /// Existing title-only folders are left untouched; identity markers are
315
- /// diagnostic records, not the mechanism that separates exports.</summary>
316
- private static string ModelFolder(Document doc)
314
+ /// Documents\pi-revit\Models\&lt;model title&gt;--&lt;identity hash&gt;.
315
+ /// Existing title-only folders are left untouched; identity markers are
316
+ /// diagnostic records, not the mechanism that separates exports.</summary>
317
+ private static string ModelFolder(Document doc)
317
318
  {
318
319
  string workspace = Path.Combine(
319
320
  Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "pi-revit");
320
- string identity = GetModelIdentity(doc);
321
- string folder = Path.Combine(workspace, "Models", GetModelFolderName(doc.Title, identity));
322
- Directory.CreateDirectory(folder);
323
- TryRecordModelIdentity(folder, doc, identity);
324
- return folder;
325
- }
326
-
327
- internal static string GetModelFolderName(string title, string identity)
328
- {
329
- string cleanTitle = SanitizeFileName(string.IsNullOrWhiteSpace(title) ? "untitled" : title);
330
- // Bound the default path component, leaving room for the identity and exports.
331
- if (cleanTitle.Length > 80)
332
- cleanTitle = cleanTitle[..80];
333
- string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant()[..24];
334
- return $"{cleanTitle}--{hash}";
335
- }
336
-
337
- // Revit can return different managed wrappers for one open native document.
338
- // Document.Equals/GetHashCode identify that open document, unlike reference
339
- // equality in ConditionalWeakTable. Closed document entries are pruned below.
340
- // Tool execution and this dictionary are confined to the Revit API thread.
341
- private static readonly Dictionary<Document, string> OpenDocumentIdentities = new();
342
-
343
- internal static string GetModelIdentity(Document doc)
344
- {
345
- try
346
- {
347
- if (doc.IsModelInCloud)
348
- {
349
- var path = doc.GetCloudModelPath();
350
- return $"cloud:{path.Region.ToUpperInvariant()}:{path.GetProjectGUID():D}:{path.GetModelGUID():D}";
351
- }
352
- string pathName = doc.PathName;
353
- if (!string.IsNullOrWhiteSpace(pathName))
354
- {
355
- // Revit file paths are Windows paths; canonicalize separators,
356
- // relative segments and case. Revit Server identities use RSN URLs.
357
- if (pathName.StartsWith("RSN://", StringComparison.OrdinalIgnoreCase))
358
- return "server:" + pathName.Replace('\\', '/').TrimEnd('/').ToUpperInvariant();
359
- return "file:" + Path.GetFullPath(pathName).Replace('/', '\\').ToUpperInvariant();
360
- }
361
- }
362
- catch
363
- {
364
- // An unavailable identity must not collapse unrelated documents into
365
- // a shared title-only directory. Use the same safe fallback as unsaved docs.
366
- }
367
- foreach (var closed in OpenDocumentIdentities.Keys.Where(key => !key.IsValidObject).ToList())
368
- OpenDocumentIdentities.Remove(closed);
369
- if (!OpenDocumentIdentities.TryGetValue(doc, out string? identity))
370
- OpenDocumentIdentities[doc] = identity = $"session:{Guid.NewGuid():N}";
371
- return identity;
372
- }
373
-
374
- private static void TryRecordModelIdentity(string folder, Document doc, string identity)
321
+ string identity = GetModelIdentity(doc);
322
+ string folder = Path.Combine(workspace, "Models", GetModelFolderName(doc.Title, identity));
323
+ Directory.CreateDirectory(folder);
324
+ TryRecordModelIdentity(folder, doc, identity);
325
+ return folder;
326
+ }
327
+
328
+ internal static string GetModelFolderName(string title, string identity)
329
+ {
330
+ string cleanTitle = SanitizeFileName(string.IsNullOrWhiteSpace(title) ? "untitled" : title);
331
+ // Bound the default path component, leaving room for the identity and exports.
332
+ if (cleanTitle.Length > 80)
333
+ cleanTitle = cleanTitle[..80];
334
+ string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant()[..24];
335
+ return $"{cleanTitle}--{hash}";
336
+ }
337
+
338
+ // Revit can return different managed wrappers for one open native document.
339
+ // Document.Equals/GetHashCode identify that open document, unlike reference
340
+ // equality in ConditionalWeakTable. Closed document entries are pruned below.
341
+ // Tool execution and this dictionary are confined to the Revit API thread.
342
+ private static readonly Dictionary<Document, string> OpenDocumentIdentities = new();
343
+
344
+ internal static string GetModelIdentity(Document doc)
345
+ {
346
+ try
347
+ {
348
+ if (doc.IsModelInCloud)
349
+ {
350
+ var path = doc.GetCloudModelPath();
351
+ return $"cloud:{path.Region.ToUpperInvariant()}:{path.GetProjectGUID():D}:{path.GetModelGUID():D}";
352
+ }
353
+ string pathName = doc.PathName;
354
+ if (!string.IsNullOrWhiteSpace(pathName))
355
+ {
356
+ // Revit file paths are Windows paths; canonicalize separators,
357
+ // relative segments and case. Revit Server identities use RSN URLs.
358
+ if (pathName.StartsWith("RSN://", StringComparison.OrdinalIgnoreCase))
359
+ return "server:" + pathName.Replace('\\', '/').TrimEnd('/').ToUpperInvariant();
360
+ return "file:" + Path.GetFullPath(pathName).Replace('/', '\\').ToUpperInvariant();
361
+ }
362
+ }
363
+ catch
364
+ {
365
+ // An unavailable identity must not collapse unrelated documents into
366
+ // a shared title-only directory. Use the same safe fallback as unsaved docs.
367
+ }
368
+ foreach (var closed in OpenDocumentIdentities.Keys.Where(key => !key.IsValidObject).ToList())
369
+ OpenDocumentIdentities.Remove(closed);
370
+ if (!OpenDocumentIdentities.TryGetValue(doc, out string? identity))
371
+ OpenDocumentIdentities[doc] = identity = $"session:{Guid.NewGuid():N}";
372
+ return identity;
373
+ }
374
+
375
+ private static void TryRecordModelIdentity(string folder, Document doc, string identity)
375
376
  {
376
377
  try
377
378
  {
378
- string line = $"{identity}\t{doc.PathName}";
379
+ string line = $"{identity}\t{doc.PathName}";
379
380
  string marker = Path.Combine(folder, "model.txt");
380
381
  if (!File.Exists(marker) || !File.ReadAllLines(marker).Contains(line))
381
382
  File.AppendAllLines(marker, new[] { line });
@@ -16,36 +16,36 @@ namespace RevitBridge.Tools
16
16
  public List<string> Warnings { get; } = new();
17
17
  public List<string> Errors { get; } = new();
18
18
 
19
- public static FailureGuard Attach(Transaction transaction)
20
- {
21
- if (transaction.GetStatus() != TransactionStatus.Started)
22
- throw new InvalidOperationException("FailureGuard must be attached after the transaction starts; Start resets failure handling options.");
23
- var guard = new FailureGuard();
19
+ public static FailureGuard Attach(Transaction transaction)
20
+ {
21
+ if (transaction.GetStatus() != TransactionStatus.Started)
22
+ throw new InvalidOperationException("FailureGuard must be attached after the transaction starts; Start resets failure handling options.");
23
+ var guard = new FailureGuard();
24
24
  var options = transaction.GetFailureHandlingOptions();
25
25
  options.SetFailuresPreprocessor(guard);
26
26
  options.SetClearAfterRollback(true);
27
27
  transaction.SetFailureHandlingOptions(options);
28
- return guard;
29
- }
30
-
31
- /// <summary>Best-effort cleanup that preserves the original failure and never
32
- /// claims a rollback unless Revit confirms its final transaction status.</summary>
33
- public static string RollBackAndDescribe(Transaction transaction)
34
- {
35
- try
36
- {
37
- if (transaction.GetStatus() == TransactionStatus.Started)
38
- transaction.RollBack();
39
- var status = transaction.GetStatus();
40
- return status == TransactionStatus.RolledBack
41
- ? "The transaction was rolled back; no changes from this transaction were saved."
42
- : $"Transaction status is {status}; rollback is not confirmed. Inspect Revit before retrying.";
43
- }
44
- catch (Exception ex)
45
- {
46
- return $"Rollback could not be confirmed ({ex.GetType().Name}: {ex.Message}). Inspect Revit before retrying.";
47
- }
48
- }
28
+ return guard;
29
+ }
30
+
31
+ /// <summary>Best-effort cleanup that preserves the original failure and never
32
+ /// claims a rollback unless Revit confirms its final transaction status.</summary>
33
+ public static string RollBackAndDescribe(Transaction transaction)
34
+ {
35
+ try
36
+ {
37
+ if (transaction.GetStatus() == TransactionStatus.Started)
38
+ transaction.RollBack();
39
+ var status = transaction.GetStatus();
40
+ return status == TransactionStatus.RolledBack
41
+ ? "The transaction was rolled back; no changes from this transaction were saved."
42
+ : $"Transaction status is {status}; rollback is not confirmed. Inspect Revit before retrying.";
43
+ }
44
+ catch (Exception ex)
45
+ {
46
+ return $"Rollback could not be confirmed ({ex.GetType().Name}: {ex.Message}). Inspect Revit before retrying.";
47
+ }
48
+ }
49
49
 
50
50
  public FailureProcessingResult PreprocessFailures(FailuresAccessor accessor)
51
51
  {