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.
- package/CHANGELOG.md +388 -351
- package/README.md +426 -22
- package/extensions/pi-revit/index.ts +246 -187
- package/extensions/pi-revit/instance-router.ts +86 -0
- package/extensions/pi-revit/script-library.ts +146 -0
- package/extensions/pi-revit/tool-catalog.ts +67 -0
- package/package.json +59 -59
- package/skills/pi-revit/SKILL.md +196 -32
- package/skills/pi-revit/references/model-audit-export.md +29 -0
- package/skills/pi-revit/references/room-documentation.md +28 -0
- package/src/Revit/BridgeServer.cs +87 -37
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +57 -35
- package/src/Revit/Tools/CaptureView.cs +2 -1
- package/src/Revit/Tools/ChangeElementTypes.cs +60 -0
- package/src/Revit/Tools/CreateTags.cs +95 -0
- package/src/Revit/Tools/DeleteElements.cs +44 -0
- package/src/Revit/Tools/DocumentGuard.cs +64 -64
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +45 -35
- package/src/Revit/Tools/ExportDocuments.cs +121 -120
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +42 -12
- package/src/Revit/Tools/GetElementRelationships.cs +76 -0
- package/src/Revit/Tools/GetElements.cs +37 -23
- package/src/Revit/Tools/GetLinkedElements.cs +82 -0
- package/src/Revit/Tools/GetLinkedModels.cs +66 -0
- package/src/Revit/Tools/GetModelCoordinates.cs +49 -0
- package/src/Revit/Tools/GetModelOverview.cs +2 -2
- package/src/Revit/Tools/GetScheduleFields.cs +37 -0
- package/src/Revit/Tools/GetSchedules.cs +89 -0
- package/src/Revit/Tools/ManageElementSets.cs +106 -0
- package/src/Revit/Tools/ManageSchedules.cs +164 -0
- package/src/Revit/Tools/ManageSelection.cs +37 -36
- package/src/Revit/Tools/ManageSheetPlacements.cs +97 -0
- package/src/Revit/Tools/ManageSheets.cs +63 -0
- package/src/Revit/Tools/ManageViews.cs +100 -0
- package/src/Revit/Tools/MeasureGeometry.cs +54 -0
- package/src/Revit/Tools/ModelEditBatch.cs +102 -0
- package/src/Revit/Tools/ModelEditInputs.cs +49 -0
- package/src/Revit/Tools/OpenView.cs +2 -1
- package/src/Revit/Tools/QuerySpatialElements.cs +63 -0
- package/src/Revit/Tools/SetParameters.cs +43 -93
- package/src/Revit/Tools/SpatialBounds.cs +30 -0
- package/src/Revit/Tools/SummarizeElements.cs +87 -0
- package/src/Revit/Tools/ToolSupport.cs +1 -1
- package/src/Revit/Tools/TransformElements.cs +58 -0
- 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\<model title>--<identity hash>.
|
|
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\<model title>--<identity hash>.
|
|
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
|
{
|