pi-revit 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/extensions/pi-revit/index.ts +239 -0
- package/package.json +28 -0
- package/scripts/build.ps1 +37 -0
- package/scripts/deploy.ps1 +99 -0
- package/scripts/revit.ps1 +112 -0
- package/scripts/setup-workspace.ps1 +53 -0
- package/scripts/uninstall.ps1 +103 -0
- package/skills/pi-revit/SKILL.md +50 -0
- package/src/Revit/Application.cs +94 -0
- package/src/Revit/BridgeServer.cs +419 -0
- package/src/Revit/CommandQueue.cs +93 -0
- package/src/Revit/RevitBridge.addin +11 -0
- package/src/Revit/RevitBridge.csproj +48 -0
- package/src/Revit/ToolRegistry.cs +109 -0
- package/src/Revit/Tools/CaptureView.cs +168 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +418 -0
- package/src/Revit/Tools/ExportDocuments.cs +256 -0
- package/src/Revit/Tools/GetElementDetails.cs +318 -0
- package/src/Revit/Tools/GetElementTypes.cs +151 -0
- package/src/Revit/Tools/GetElements.cs +725 -0
- package/src/Revit/Tools/GetModelHealth.cs +191 -0
- package/src/Revit/Tools/GetModelOverview.cs +159 -0
- package/src/Revit/Tools/ManageSelection.cs +220 -0
- package/src/Revit/Tools/SearchApiDocs.cs +610 -0
- package/src/Revit/Tools/SetParameters.cs +382 -0
- package/src/Revit/Tools/ToolSupport.cs +261 -0
- package/workspace/AGENTS.md +23 -0
- package/workspace/pi-revit-global.cmd +31 -0
- package/workspace/pi-revit-here.cmd +5 -0
- package/workspace/project-AGENTS.md +8 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
using System.IO;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using Autodesk.Revit.DB;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge.Tools
|
|
6
|
+
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Exports a PNG of a view to a temp file for the agent to open with the read
|
|
9
|
+
/// tool: the response carries only the file path and image metadata,
|
|
10
|
+
/// never base64 image data. The long edge is clamped to 1568 px; the fit
|
|
11
|
+
/// direction comes from the view outline and is verified against the produced
|
|
12
|
+
/// PNG header, re-exporting once along the long edge if the guess was wrong.
|
|
13
|
+
/// </summary>
|
|
14
|
+
internal sealed class CaptureView : ITool
|
|
15
|
+
{
|
|
16
|
+
private const int MaxLongEdgePx = 1568;
|
|
17
|
+
|
|
18
|
+
public string Name => "capture_view";
|
|
19
|
+
public string Label => "Capture View";
|
|
20
|
+
public string Description => "Export a PNG snapshot of a Revit view to a temporary file and return its path — the response contains NO image data; open the returned filePath with the read tool to actually see the image. Defaults to the active view; pass view_id for any other graphical view or sheet (find ids with get_elements, category 'Views' or 'Sheets'). The long image edge is capped at 1568 px. Schedules and view templates cannot be captured.";
|
|
21
|
+
public string Tier => "advanced";
|
|
22
|
+
|
|
23
|
+
public object ParametersSchema => new
|
|
24
|
+
{
|
|
25
|
+
type = "object",
|
|
26
|
+
properties = new
|
|
27
|
+
{
|
|
28
|
+
view_id = new
|
|
29
|
+
{
|
|
30
|
+
type = "integer",
|
|
31
|
+
description = "Element id of the view or sheet to capture. Default: the active view.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required = Array.Empty<string>(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
public string? PromptSnippet => "Export a PNG of a Revit view to a temp file (open it with the read tool).";
|
|
38
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
39
|
+
{
|
|
40
|
+
"capture_view returns a filePath and never image data — open that file with the read tool to actually see the view.",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
44
|
+
{
|
|
45
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
46
|
+
var view = ResolveView(doc, args);
|
|
47
|
+
|
|
48
|
+
string prefix = Path.Combine(Path.GetTempPath(), "revit_view_" + Guid.NewGuid().ToString("N"));
|
|
49
|
+
ExportPng(doc, view, prefix, GuessLandscape(view));
|
|
50
|
+
string filePath = FindExportedFile(prefix);
|
|
51
|
+
var (width, height) = ReadPngSize(filePath);
|
|
52
|
+
|
|
53
|
+
// The outline-based aspect guess can be wrong (crop boxes, title blocks);
|
|
54
|
+
// the PNG header is authoritative. One re-export with the fit direction
|
|
55
|
+
// on the long edge guarantees the <= 1568 px clamp.
|
|
56
|
+
if (width > 0 && Math.Max(width, height) > MaxLongEdgePx)
|
|
57
|
+
{
|
|
58
|
+
File.Delete(filePath);
|
|
59
|
+
ExportPng(doc, view, prefix, horizontal: width >= height);
|
|
60
|
+
filePath = FindExportedFile(prefix);
|
|
61
|
+
(width, height) = ReadPngSize(filePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
long fileSizeBytes = new FileInfo(filePath).Length;
|
|
65
|
+
string compact = $"Captured view '{view.Name}' (id {view.Id.Value}) to {filePath} ({width}x{height} px, {Math.Max(1, fileSizeBytes / 1024)} KB). Open filePath with the read tool to see it.";
|
|
66
|
+
return new ToolOutput(new
|
|
67
|
+
{
|
|
68
|
+
filePath,
|
|
69
|
+
viewId = view.Id.Value,
|
|
70
|
+
viewName = view.Name,
|
|
71
|
+
width,
|
|
72
|
+
height,
|
|
73
|
+
fileSizeBytes,
|
|
74
|
+
}, compact);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static View ResolveView(Document doc, JsonElement args)
|
|
78
|
+
{
|
|
79
|
+
View view;
|
|
80
|
+
if (JsonArgs.GetLong(args, "view_id") is { } viewId)
|
|
81
|
+
{
|
|
82
|
+
view = doc.GetElement(new ElementId(viewId)) as View
|
|
83
|
+
?? throw new ArgumentException($"view_id {viewId} is not a view. Find views with get_elements (category 'Views' or 'Sheets').");
|
|
84
|
+
if (view.IsTemplate)
|
|
85
|
+
throw new ArgumentException($"View '{view.Name}' ({viewId}) is a view template and cannot be captured.");
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
{
|
|
89
|
+
view = doc.ActiveView
|
|
90
|
+
?? throw new ArgumentException("No active view to capture; pass view_id.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (view is ViewSchedule)
|
|
94
|
+
throw new ArgumentException($"View '{view.Name}' is a schedule; schedules cannot be exported as images. Capture a graphical view or sheet, or query the data with get_elements instead.");
|
|
95
|
+
return view;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private static void ExportPng(Document doc, View view, string prefix, bool horizontal)
|
|
99
|
+
{
|
|
100
|
+
var options = new ImageExportOptions
|
|
101
|
+
{
|
|
102
|
+
ExportRange = ExportRange.SetOfViews,
|
|
103
|
+
FilePath = prefix,
|
|
104
|
+
HLRandWFViewsFileType = ImageFileType.PNG,
|
|
105
|
+
ShadowViewsFileType = ImageFileType.PNG,
|
|
106
|
+
ImageResolution = ImageResolution.DPI_150,
|
|
107
|
+
ZoomType = ZoomFitType.FitToPage,
|
|
108
|
+
PixelSize = MaxLongEdgePx,
|
|
109
|
+
FitDirection = horizontal ? FitDirectionType.Horizontal : FitDirectionType.Vertical,
|
|
110
|
+
};
|
|
111
|
+
options.SetViewsAndSheets(new List<ElementId> { view.Id });
|
|
112
|
+
|
|
113
|
+
try
|
|
114
|
+
{
|
|
115
|
+
doc.ExportImage(options);
|
|
116
|
+
}
|
|
117
|
+
catch (Autodesk.Revit.Exceptions.ApplicationException ex)
|
|
118
|
+
{
|
|
119
|
+
throw new InvalidOperationException($"Revit could not export view '{view.Name}' as an image: {ex.Message}");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// <summary>Initial fit direction from the view outline; verified against the produced PNG.</summary>
|
|
124
|
+
private static bool GuessLandscape(View view)
|
|
125
|
+
{
|
|
126
|
+
try
|
|
127
|
+
{
|
|
128
|
+
var outline = view.Outline;
|
|
129
|
+
return outline.Max.U - outline.Min.U >= outline.Max.V - outline.Min.V;
|
|
130
|
+
}
|
|
131
|
+
catch
|
|
132
|
+
{
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// <summary>ExportRange.SetOfViews appends " - [view type] - [view name]" to the
|
|
138
|
+
/// file path prefix, so the produced name is discovered by globbing the prefix.</summary>
|
|
139
|
+
private static string FindExportedFile(string prefix)
|
|
140
|
+
{
|
|
141
|
+
string directory = Path.GetDirectoryName(prefix)!;
|
|
142
|
+
var matches = Directory.GetFiles(directory, Path.GetFileName(prefix) + "*.png");
|
|
143
|
+
return matches.Length > 0
|
|
144
|
+
? matches[0]
|
|
145
|
+
: throw new InvalidOperationException("Revit reported a successful export but no PNG file was produced.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// <summary>Reads width/height from the PNG IHDR chunk (offsets 16/20, big-endian).</summary>
|
|
149
|
+
private static (int Width, int Height) ReadPngSize(string filePath)
|
|
150
|
+
{
|
|
151
|
+
try
|
|
152
|
+
{
|
|
153
|
+
using var stream = File.OpenRead(filePath);
|
|
154
|
+
Span<byte> header = stackalloc byte[24];
|
|
155
|
+
stream.ReadExactly(header);
|
|
156
|
+
if (header[12] != (byte)'I' || header[13] != (byte)'H' || header[14] != (byte)'D' || header[15] != (byte)'R')
|
|
157
|
+
return (0, 0);
|
|
158
|
+
int width = (header[16] << 24) | (header[17] << 16) | (header[18] << 8) | header[19];
|
|
159
|
+
int height = (header[20] << 24) | (header[21] << 16) | (header[22] << 8) | header[23];
|
|
160
|
+
return (width, height);
|
|
161
|
+
}
|
|
162
|
+
catch
|
|
163
|
+
{
|
|
164
|
+
return (0, 0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
using System.Collections;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.Reflection;
|
|
4
|
+
using System.Text.Json;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
6
|
+
using Autodesk.Revit.DB;
|
|
7
|
+
using Autodesk.Revit.UI;
|
|
8
|
+
using Autodesk.Revit.UI.Events;
|
|
9
|
+
using Microsoft.CodeAnalysis;
|
|
10
|
+
using Microsoft.CodeAnalysis.CSharp;
|
|
11
|
+
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
|
12
|
+
using Microsoft.CodeAnalysis.Scripting;
|
|
13
|
+
|
|
14
|
+
namespace RevitBridge.Tools
|
|
15
|
+
{
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// Globals visible to execute_csharp scripts: doc, uidoc, uiapp, and the Dump helper.
|
|
18
|
+
/// Public because the dynamically compiled script assembly must bind to it; the
|
|
19
|
+
/// lowercase member names are the script-facing contract.
|
|
20
|
+
/// </summary>
|
|
21
|
+
public sealed class ScriptGlobals
|
|
22
|
+
{
|
|
23
|
+
internal ScriptGlobals(Document document, UIDocument uiDocument, UIApplication uiApplication, Action<object?> dump)
|
|
24
|
+
{
|
|
25
|
+
doc = document;
|
|
26
|
+
uidoc = uiDocument;
|
|
27
|
+
uiapp = uiApplication;
|
|
28
|
+
Dump = dump;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// <summary>The active document.</summary>
|
|
32
|
+
public Document doc { get; }
|
|
33
|
+
|
|
34
|
+
/// <summary>The active UI document (selection, active view).</summary>
|
|
35
|
+
public UIDocument uidoc { get; }
|
|
36
|
+
|
|
37
|
+
/// <summary>The Revit UI application.</summary>
|
|
38
|
+
public UIApplication uiapp { get; }
|
|
39
|
+
|
|
40
|
+
/// <summary>Records a value into the result's dumps[] (safely projected at call time).</summary>
|
|
41
|
+
public Action<object?> Dump { get; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// <summary>
|
|
45
|
+
/// Unrestricted C# escape hatch: compiles the script with Roslyn, runs it on the
|
|
46
|
+
/// Revit API thread inside ONE backend-owned Transaction("execute_csharp") — commit on
|
|
47
|
+
/// success, rollback on any exception. Results are projected through a depth-limited
|
|
48
|
+
/// serializer that never emits raw Revit API objects. A DialogBoxShowing guard
|
|
49
|
+
/// auto-dismisses modal dialogs so a script cannot hang the Revit thread behind a popup.
|
|
50
|
+
/// </summary>
|
|
51
|
+
internal sealed class ExecuteCsharp : ITool
|
|
52
|
+
{
|
|
53
|
+
private const int MaxDumps = 200;
|
|
54
|
+
private const int MaxDepth = 6;
|
|
55
|
+
private const int MaxItems = 100;
|
|
56
|
+
private const int MaxProperties = 40;
|
|
57
|
+
private const int MaxStringChars = 4_000;
|
|
58
|
+
private const int MaxReportedErrors = 20;
|
|
59
|
+
|
|
60
|
+
public string Name => "execute_csharp";
|
|
61
|
+
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, rolled back on any exception, so a failed script never changes the model (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. Verify unfamiliar signatures with search_api_docs first.";
|
|
63
|
+
public bool Write => true;
|
|
64
|
+
|
|
65
|
+
public object ParametersSchema => new
|
|
66
|
+
{
|
|
67
|
+
type = "object",
|
|
68
|
+
properties = new
|
|
69
|
+
{
|
|
70
|
+
code = new
|
|
71
|
+
{
|
|
72
|
+
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
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required = new[] { "code" },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
public string? PromptSnippet => "Run a C# script against the Revit API inside one transaction (globals doc/uidoc/uiapp, Dump helper) — the escape hatch for operations without a dedicated tool.";
|
|
80
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
81
|
+
{
|
|
82
|
+
"Use execute_csharp for Revit operations no dedicated tool covers (creation, deletion, geometry edits, views/sheets, tagging, ...); check exact signatures with search_api_docs before writing the script.",
|
|
83
|
+
"execute_csharp scripts must be fully synchronous (await/async is rejected; never block on Task.Result/.Wait()) and should return primitives or anonymous objects/lists, using Dump(...) for intermediates — raw Revit objects are projected to compact summaries; keep loops bounded and filter at the collector level.",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
87
|
+
{
|
|
88
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
89
|
+
var uiapp = context.UIApplication ?? throw new NoActiveDocumentException();
|
|
90
|
+
var uidoc = uiapp.ActiveUIDocument ?? throw new NoActiveDocumentException();
|
|
91
|
+
|
|
92
|
+
string code = JsonArgs.GetString(args, "code") ?? string.Empty;
|
|
93
|
+
if (string.IsNullOrWhiteSpace(code))
|
|
94
|
+
throw new ArgumentException("code must be a non-empty C# script.");
|
|
95
|
+
|
|
96
|
+
var stopwatch = Stopwatch.StartNew();
|
|
97
|
+
|
|
98
|
+
// Compile before opening the transaction: compile errors must not touch the model.
|
|
99
|
+
var script = CSharpScript.Create<object>(code, Options.Value, typeof(ScriptGlobals));
|
|
100
|
+
var errors = script.Compile().Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
|
|
101
|
+
if (errors.Count > 0)
|
|
102
|
+
throw new ArgumentException(FormatCompileErrors(errors));
|
|
103
|
+
RejectAsyncCode(script.GetCompilation());
|
|
104
|
+
|
|
105
|
+
var dumps = new List<object?>();
|
|
106
|
+
var globals = new ScriptGlobals(doc, uidoc, uiapp, value =>
|
|
107
|
+
{
|
|
108
|
+
if (dumps.Count < MaxDumps)
|
|
109
|
+
dumps.Add(Project(value, 0));
|
|
110
|
+
else if (dumps.Count == MaxDumps)
|
|
111
|
+
dumps.Add($"… Dump cap reached ({MaxDumps}); further values were ignored.");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
using var dialogGuard = new DialogGuard(uiapp);
|
|
115
|
+
using var transaction = new Transaction(doc, "execute_csharp");
|
|
116
|
+
if (transaction.Start() != TransactionStatus.Started)
|
|
117
|
+
throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
|
|
118
|
+
|
|
119
|
+
object? returnValue;
|
|
120
|
+
try
|
|
121
|
+
{
|
|
122
|
+
// Runs synchronously on this (the Revit API) thread; await/async was
|
|
123
|
+
// rejected at compile time, so the task completes without suspending.
|
|
124
|
+
var state = script.RunAsync(globals).GetAwaiter().GetResult();
|
|
125
|
+
// Project inside the transaction so values reflect the script-end model state.
|
|
126
|
+
returnValue = Project(state.ReturnValue, 0);
|
|
127
|
+
}
|
|
128
|
+
catch (Exception ex)
|
|
129
|
+
{
|
|
130
|
+
try
|
|
131
|
+
{
|
|
132
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
133
|
+
transaction.RollBack();
|
|
134
|
+
}
|
|
135
|
+
catch
|
|
136
|
+
{
|
|
137
|
+
// Reporting the script failure outranks a rollback hiccup.
|
|
138
|
+
}
|
|
139
|
+
throw new InvalidOperationException(FormatRuntimeError(ex, dialogGuard.Suppressed));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (transaction.Commit() != TransactionStatus.Committed)
|
|
143
|
+
throw new InvalidOperationException(
|
|
144
|
+
"Revit rolled back the execute_csharp transaction during commit (failure processing rejected the changes); no model changes were saved."
|
|
145
|
+
+ DescribeDialogs(dialogGuard.Suppressed));
|
|
146
|
+
|
|
147
|
+
stopwatch.Stop();
|
|
148
|
+
|
|
149
|
+
var payload = new Dictionary<string, object?>
|
|
150
|
+
{
|
|
151
|
+
["returnValue"] = returnValue,
|
|
152
|
+
["dumps"] = dumps,
|
|
153
|
+
["durationMs"] = stopwatch.ElapsedMilliseconds,
|
|
154
|
+
};
|
|
155
|
+
if (dialogGuard.Suppressed.Count > 0)
|
|
156
|
+
payload["suppressedDialogs"] = dialogGuard.Suppressed;
|
|
157
|
+
return payload;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -------------------------------------------------------------- scripting
|
|
161
|
+
|
|
162
|
+
/// <summary>Compile options: core BCL + LINQ + Revit API references; the five
|
|
163
|
+
/// default imports; debug info so runtime stack traces carry script line numbers.</summary>
|
|
164
|
+
private static readonly Lazy<ScriptOptions> Options = new(() => ScriptOptions.Default
|
|
165
|
+
.WithReferences(
|
|
166
|
+
typeof(object).Assembly, // System.Private.CoreLib
|
|
167
|
+
Assembly.Load("System.Runtime"),
|
|
168
|
+
Assembly.Load("netstandard"),
|
|
169
|
+
typeof(Enumerable).Assembly, // System.Linq
|
|
170
|
+
typeof(Regex).Assembly, // System.Text.RegularExpressions
|
|
171
|
+
typeof(Console).Assembly, // System.Console
|
|
172
|
+
typeof(Document).Assembly, // RevitAPI
|
|
173
|
+
typeof(UIApplication).Assembly) // RevitAPIUI
|
|
174
|
+
.WithImports("System", "System.Linq", "System.Collections.Generic", "Autodesk.Revit.DB", "Autodesk.Revit.UI")
|
|
175
|
+
.WithEmitDebugInformation(true));
|
|
176
|
+
|
|
177
|
+
private static string FormatCompileErrors(IReadOnlyList<Diagnostic> errors)
|
|
178
|
+
{
|
|
179
|
+
var lines = errors.Take(MaxReportedErrors).Select(diagnostic =>
|
|
180
|
+
{
|
|
181
|
+
var position = diagnostic.Location.GetLineSpan().StartLinePosition;
|
|
182
|
+
return $" line {position.Line + 1}, col {position.Character + 1}: {diagnostic.Id} {diagnostic.GetMessage()}";
|
|
183
|
+
});
|
|
184
|
+
string more = errors.Count > MaxReportedErrors ? $"\n … +{errors.Count - MaxReportedErrors} more error(s)" : string.Empty;
|
|
185
|
+
return $"C# compilation failed with {errors.Count} error(s):\n{string.Join("\n", lines)}{more}\n"
|
|
186
|
+
+ "Globals: doc, uidoc, uiapp, Dump(value). Default imports: System, System.Linq, System.Collections.Generic, "
|
|
187
|
+
+ "Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives for other namespaces.";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// <summary>Rejects await/async at compile time. The script runs synchronously on the
|
|
191
|
+
/// Revit API thread (RunAsync + GetResult): an await would either deadlock Revit
|
|
192
|
+
/// forever (the dispatcher is blocked by GetResult, so a captured-context continuation
|
|
193
|
+
/// can never run) or resume the rest of the script on a thread-pool thread inside the
|
|
194
|
+
/// open transaction. Deterministic syntax-tree guard at the execution boundary, not
|
|
195
|
+
/// source-text sniffing.</summary>
|
|
196
|
+
private static void RejectAsyncCode(Compilation compilation)
|
|
197
|
+
{
|
|
198
|
+
foreach (var tree in compilation.SyntaxTrees)
|
|
199
|
+
{
|
|
200
|
+
foreach (var token in tree.GetRoot().DescendantTokens())
|
|
201
|
+
{
|
|
202
|
+
if (!token.IsKind(SyntaxKind.AwaitKeyword) && !token.IsKind(SyntaxKind.AsyncKeyword))
|
|
203
|
+
continue;
|
|
204
|
+
var position = token.GetLocation().GetLineSpan().StartLinePosition;
|
|
205
|
+
throw new ArgumentException(
|
|
206
|
+
$"'{token.ValueText}' at line {position.Line + 1}, col {position.Character + 1}: "
|
|
207
|
+
+ "scripts run synchronously on the Revit API thread — remove await/async "
|
|
208
|
+
+ "(and never block on Task.Result/.Wait()); write fully synchronous code.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private static string FormatRuntimeError(Exception ex, IReadOnlyList<string> suppressedDialogs)
|
|
214
|
+
{
|
|
215
|
+
string line = TryGetScriptLine(ex) is { } scriptLine ? $" at script line {scriptLine}" : string.Empty;
|
|
216
|
+
string inner = ex.InnerException is { } innerEx ? $" Inner: {innerEx.GetType().Name}: {innerEx.Message}" : string.Empty;
|
|
217
|
+
return $"C# script threw {ex.GetType().Name}{line}: {ex.Message}.{inner}"
|
|
218
|
+
+ " The execute_csharp transaction was rolled back; no model changes were saved."
|
|
219
|
+
+ DescribeDialogs(suppressedDialogs);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static string DescribeDialogs(IReadOnlyList<string> suppressed)
|
|
223
|
+
=> suppressed.Count > 0 ? $" Auto-dismissed dialog(s) during the run: {string.Join(", ", suppressed)}." : string.Empty;
|
|
224
|
+
|
|
225
|
+
/// <summary>Script frames carry sequence points (EmitDebugInformation): pull the
|
|
226
|
+
/// first ":line N" out of the stack trace.</summary>
|
|
227
|
+
private static int? TryGetScriptLine(Exception ex)
|
|
228
|
+
{
|
|
229
|
+
string? stackTrace = ex.StackTrace;
|
|
230
|
+
if (stackTrace is null)
|
|
231
|
+
return null;
|
|
232
|
+
var match = Regex.Match(stackTrace, @":line (\d+)");
|
|
233
|
+
return match.Success && int.TryParse(match.Groups[1].Value, out int line) ? line : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// <summary>Auto-dismisses any modal Revit dialog raised while the script runs, so a
|
|
237
|
+
/// popup cannot hang the Revit thread; dismissed dialog ids are reported.</summary>
|
|
238
|
+
private sealed class DialogGuard : IDisposable
|
|
239
|
+
{
|
|
240
|
+
private readonly UIApplication _uiapp;
|
|
241
|
+
public List<string> Suppressed { get; } = new();
|
|
242
|
+
|
|
243
|
+
public DialogGuard(UIApplication uiapp)
|
|
244
|
+
{
|
|
245
|
+
_uiapp = uiapp;
|
|
246
|
+
_uiapp.DialogBoxShowing += OnDialogBoxShowing;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private void OnDialogBoxShowing(object? sender, DialogBoxShowingEventArgs e)
|
|
250
|
+
{
|
|
251
|
+
Suppressed.Add(string.IsNullOrEmpty(e.DialogId) ? e.GetType().Name : e.DialogId);
|
|
252
|
+
try
|
|
253
|
+
{
|
|
254
|
+
e.OverrideResult(1); // IDOK
|
|
255
|
+
}
|
|
256
|
+
catch
|
|
257
|
+
{
|
|
258
|
+
// Some dialogs reject overrides; never let the guard itself throw.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public void Dispose()
|
|
263
|
+
{
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
_uiapp.DialogBoxShowing -= OnDialogBoxShowing;
|
|
267
|
+
}
|
|
268
|
+
catch
|
|
269
|
+
{
|
|
270
|
+
// Unsubscribe must never mask the tool result.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ------------------------------------------------------------- projection
|
|
276
|
+
|
|
277
|
+
/// <summary>Safe serializer: depth-limited, item-capped, and never emits raw Revit
|
|
278
|
+
/// API objects — Element/ElementId/XYZ/Parameter/BoundingBoxXYZ/Category get compact
|
|
279
|
+
/// shapes, any other Autodesk type falls back to ToString().</summary>
|
|
280
|
+
private static object? Project(object? value, int depth)
|
|
281
|
+
{
|
|
282
|
+
if (value is null)
|
|
283
|
+
return null;
|
|
284
|
+
|
|
285
|
+
switch (value)
|
|
286
|
+
{
|
|
287
|
+
case string text:
|
|
288
|
+
return text.Length <= MaxStringChars ? text : text[..MaxStringChars] + "… [truncated]";
|
|
289
|
+
case bool or int or long or short or byte or sbyte or uint or ulong or ushort or decimal:
|
|
290
|
+
return value;
|
|
291
|
+
case double number:
|
|
292
|
+
return double.IsFinite(number) ? number : number.ToString();
|
|
293
|
+
case float number:
|
|
294
|
+
return float.IsFinite(number) ? (object)number : number.ToString();
|
|
295
|
+
case char or Guid or DateTime or DateTimeOffset or TimeSpan or Uri:
|
|
296
|
+
return value.ToString();
|
|
297
|
+
case Enum enumValue:
|
|
298
|
+
return enumValue.ToString();
|
|
299
|
+
case JsonElement json:
|
|
300
|
+
return json.Clone();
|
|
301
|
+
case ElementId id:
|
|
302
|
+
return id.Value;
|
|
303
|
+
case XYZ point:
|
|
304
|
+
return new Dictionary<string, object?> { ["x"] = point.X, ["y"] = point.Y, ["z"] = point.Z };
|
|
305
|
+
case UV uv:
|
|
306
|
+
return new Dictionary<string, object?> { ["u"] = uv.U, ["v"] = uv.V };
|
|
307
|
+
case BoundingBoxXYZ box:
|
|
308
|
+
return new Dictionary<string, object?>
|
|
309
|
+
{
|
|
310
|
+
["min"] = Safe(() => Project(box.Min, depth + 1)),
|
|
311
|
+
["max"] = Safe(() => Project(box.Max, depth + 1)),
|
|
312
|
+
};
|
|
313
|
+
case Element element:
|
|
314
|
+
return ProjectElement(element);
|
|
315
|
+
case Parameter parameter:
|
|
316
|
+
return ProjectParameter(parameter);
|
|
317
|
+
case Category category:
|
|
318
|
+
return new Dictionary<string, object?>
|
|
319
|
+
{
|
|
320
|
+
["id"] = Safe(() => category.Id.Value),
|
|
321
|
+
["name"] = Safe(() => category.Name),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (depth >= MaxDepth)
|
|
326
|
+
return Safe(value.ToString);
|
|
327
|
+
|
|
328
|
+
if (value is IDictionary dictionary)
|
|
329
|
+
{
|
|
330
|
+
var result = new Dictionary<string, object?>();
|
|
331
|
+
foreach (DictionaryEntry entry in dictionary)
|
|
332
|
+
{
|
|
333
|
+
if (result.Count >= MaxItems)
|
|
334
|
+
{
|
|
335
|
+
result["…"] = $"truncated at {MaxItems} entries";
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
result[entry.Key?.ToString() ?? "null"] = Project(entry.Value, depth + 1);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (value is IEnumerable enumerable)
|
|
344
|
+
{
|
|
345
|
+
var items = new List<object?>();
|
|
346
|
+
foreach (object? item in enumerable)
|
|
347
|
+
{
|
|
348
|
+
if (items.Count >= MaxItems)
|
|
349
|
+
{
|
|
350
|
+
items.Add($"… truncated at {MaxItems} items");
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
items.Add(Project(item, depth + 1));
|
|
354
|
+
}
|
|
355
|
+
return items;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
var type = value.GetType();
|
|
359
|
+
// Any other Revit/Autodesk API object (Curve, Material asset, ...) has no safe
|
|
360
|
+
// JSON shape: string fallback instead of reflecting into native handles.
|
|
361
|
+
if (type.Namespace?.StartsWith("Autodesk.", StringComparison.Ordinal) == true)
|
|
362
|
+
return Safe(value.ToString);
|
|
363
|
+
if (value is Task)
|
|
364
|
+
return "<Task — return awaited values, not tasks>";
|
|
365
|
+
|
|
366
|
+
// Plain CLR object (anonymous types, records, user classes): public properties.
|
|
367
|
+
var projected = new Dictionary<string, object?>();
|
|
368
|
+
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
369
|
+
{
|
|
370
|
+
if (projected.Count >= MaxProperties)
|
|
371
|
+
{
|
|
372
|
+
projected["…"] = $"truncated at {MaxProperties} properties";
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
if (property.GetIndexParameters().Length > 0)
|
|
376
|
+
continue;
|
|
377
|
+
projected[property.Name] = Safe(() => Project(property.GetValue(value), depth + 1));
|
|
378
|
+
}
|
|
379
|
+
return projected.Count > 0 ? projected : Safe(value.ToString);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private static object ProjectElement(Element element) => new Dictionary<string, object?>
|
|
383
|
+
{
|
|
384
|
+
["id"] = Safe(() => element.Id.Value),
|
|
385
|
+
["name"] = Safe(() => element.Name),
|
|
386
|
+
["category"] = Safe(() => element.Category?.Name),
|
|
387
|
+
["typeName"] = Safe(() => element.Document.GetElement(element.GetTypeId())?.Name),
|
|
388
|
+
["levelId"] = Safe(() => element.LevelId != ElementId.InvalidElementId ? element.LevelId.Value : (object?)null),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
private static object ProjectParameter(Parameter parameter) => new Dictionary<string, object?>
|
|
392
|
+
{
|
|
393
|
+
["name"] = Safe(() => parameter.Definition?.Name),
|
|
394
|
+
["storageType"] = Safe(() => parameter.StorageType.ToString()),
|
|
395
|
+
["value"] = Safe(() => parameter.StorageType switch
|
|
396
|
+
{
|
|
397
|
+
StorageType.String => parameter.AsString(),
|
|
398
|
+
StorageType.Double => parameter.AsDouble(),
|
|
399
|
+
StorageType.Integer => parameter.AsInteger(),
|
|
400
|
+
StorageType.ElementId => (object?)parameter.AsElementId().Value,
|
|
401
|
+
_ => null,
|
|
402
|
+
}),
|
|
403
|
+
["displayValue"] = Safe(() => parameter.AsValueString()),
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
private static object? Safe(Func<object?> producer)
|
|
407
|
+
{
|
|
408
|
+
try
|
|
409
|
+
{
|
|
410
|
+
return producer();
|
|
411
|
+
}
|
|
412
|
+
catch (Exception ex)
|
|
413
|
+
{
|
|
414
|
+
return $"<error: {ex.Message}>";
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|