pi-revit 0.2.11 → 0.2.13
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 +272 -217
- package/README.md +3 -1
- package/bin/pi-revit.js +1 -1
- package/package.json +56 -56
- package/scripts/benchmark-search-docs.py +313 -313
- package/scripts/deploy.ps1 +12 -0
- package/scripts/setup-workspace.ps1 +80 -80
- package/scripts/uninstall.ps1 +146 -146
- package/skills/pi-revit/SKILL.md +2 -1
- package/src/Revit/BridgeServer.cs +67 -0
- package/src/Revit/ToolRegistry.cs +1 -0
- package/src/Revit/Tools/CaptureView.cs +60 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +17 -2
- package/src/Revit/Tools/ExportDocuments.cs +36 -5
- package/src/Revit/Tools/FailureGuard.cs +62 -62
- package/src/Revit/Tools/GetElements.cs +49 -15
- package/src/Revit/Tools/OpenView.cs +103 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -301
- package/workspace/pi-revit-global.cmd +12 -12
|
@@ -15,6 +15,10 @@ namespace RevitBridge.Tools
|
|
|
15
15
|
{
|
|
16
16
|
private const int MaxLongEdgePx = 1568;
|
|
17
17
|
|
|
18
|
+
/// <summary>How long a captured PNG stays on disk before a later capture sweeps it.
|
|
19
|
+
/// Long enough that a capture is never pulled out from under a session still reading it.</summary>
|
|
20
|
+
private static readonly TimeSpan CaptureRetention = TimeSpan.FromHours(24);
|
|
21
|
+
|
|
18
22
|
public string Name => "capture_view";
|
|
19
23
|
public string Label => "Capture View";
|
|
20
24
|
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.";
|
|
@@ -45,7 +49,8 @@ namespace RevitBridge.Tools
|
|
|
45
49
|
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
46
50
|
var view = ResolveView(doc, args);
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
PruneOldCaptures();
|
|
53
|
+
string prefix = Path.Combine(CaptureDirectory(), "revit_view_" + Guid.NewGuid().ToString("N"));
|
|
49
54
|
ExportPng(doc, view, prefix, GuessLandscape(view));
|
|
50
55
|
string filePath = FindExportedFile(prefix);
|
|
51
56
|
var (width, height) = ReadPngSize(filePath);
|
|
@@ -74,6 +79,60 @@ namespace RevitBridge.Tools
|
|
|
74
79
|
}, compact);
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/// <summary>Captures live in one folder this add-in owns, NOT in Path.GetTempPath().
|
|
83
|
+
/// Revit can hand out a fresh per-session temp folder (observed: %LOCALAPPDATA%\Temp\
|
|
84
|
+
/// <guid>\), so a sweep of GetTempPath() only ever sees the current session's own
|
|
85
|
+
/// files while every earlier session's captures accumulate unreachably in sibling
|
|
86
|
+
/// folders. A fixed directory makes the sweep below correct and cheap. Falls back to
|
|
87
|
+
/// the temp path if the folder cannot be created — a capture must never fail over
|
|
88
|
+
/// where it is filed.</summary>
|
|
89
|
+
private static string CaptureDirectory()
|
|
90
|
+
{
|
|
91
|
+
try
|
|
92
|
+
{
|
|
93
|
+
string directory = Path.Combine(
|
|
94
|
+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
95
|
+
"pi-revit",
|
|
96
|
+
"captures");
|
|
97
|
+
Directory.CreateDirectory(directory);
|
|
98
|
+
return directory;
|
|
99
|
+
}
|
|
100
|
+
catch
|
|
101
|
+
{
|
|
102
|
+
return Path.GetTempPath();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// <summary>The PNG must outlive the call — the agent opens it with the read tool
|
|
107
|
+
/// after the result returns — so this run's file is never deleted here. Instead each
|
|
108
|
+
/// capture sweeps the ones left by earlier sessions, which otherwise accumulate
|
|
109
|
+
/// forever. Best-effort: a locked or vanished file is skipped, and a failing sweep
|
|
110
|
+
/// never costs the caller their capture. The wildcard also covers the older
|
|
111
|
+
/// 'revit_view_snapshot_*' naming so pre-existing captures are collected too.</summary>
|
|
112
|
+
private static void PruneOldCaptures()
|
|
113
|
+
{
|
|
114
|
+
try
|
|
115
|
+
{
|
|
116
|
+
DateTime cutoff = DateTime.UtcNow - CaptureRetention;
|
|
117
|
+
foreach (string path in Directory.EnumerateFiles(CaptureDirectory(), "revit_view_*.png"))
|
|
118
|
+
{
|
|
119
|
+
try
|
|
120
|
+
{
|
|
121
|
+
if (File.GetLastWriteTimeUtc(path) < cutoff)
|
|
122
|
+
File.Delete(path);
|
|
123
|
+
}
|
|
124
|
+
catch
|
|
125
|
+
{
|
|
126
|
+
// In use by a reader, or already gone: leave it for the next sweep.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch
|
|
131
|
+
{
|
|
132
|
+
// Capture folder unreadable: cleanup is never worth failing a capture over.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
77
136
|
private static View ResolveView(Document doc, JsonElement args)
|
|
78
137
|
{
|
|
79
138
|
View view;
|
|
@@ -124,13 +124,26 @@ namespace RevitBridge.Tools
|
|
|
124
124
|
throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
|
|
125
125
|
|
|
126
126
|
object? returnValue;
|
|
127
|
+
string? projectionError = null;
|
|
127
128
|
try
|
|
128
129
|
{
|
|
129
130
|
// Runs synchronously on this (the Revit API) thread; await/async was
|
|
130
131
|
// rejected at compile time, so the task completes without suspending.
|
|
131
132
|
var state = script.RunAsync(globals).GetAwaiter().GetResult();
|
|
132
|
-
// Project inside the transaction so values reflect the script-end model
|
|
133
|
-
|
|
133
|
+
// Project inside the transaction so values reflect the script-end model
|
|
134
|
+
// state — but never let projection failure masquerade as script failure:
|
|
135
|
+
// a lazy sequence that faults while being walked (a deleted element, a
|
|
136
|
+
// throwing property) would otherwise roll back a script that fully
|
|
137
|
+
// succeeded. The script's changes stand; only the return value is lost.
|
|
138
|
+
try
|
|
139
|
+
{
|
|
140
|
+
returnValue = Project(state.ReturnValue, 0);
|
|
141
|
+
}
|
|
142
|
+
catch (Exception ex)
|
|
143
|
+
{
|
|
144
|
+
projectionError = $"{ex.GetType().Name}: {ex.Message}";
|
|
145
|
+
returnValue = $"<the script succeeded, but its return value could not be projected: {projectionError}. Return primitives, strings, or anonymous objects/lists, and use Dump(...) for intermediates.>";
|
|
146
|
+
}
|
|
134
147
|
}
|
|
135
148
|
catch (Exception ex)
|
|
136
149
|
{
|
|
@@ -160,6 +173,8 @@ namespace RevitBridge.Tools
|
|
|
160
173
|
["dumps"] = dumps,
|
|
161
174
|
["durationMs"] = stopwatch.ElapsedMilliseconds,
|
|
162
175
|
};
|
|
176
|
+
if (projectionError != null)
|
|
177
|
+
payload["returnValueError"] = projectionError;
|
|
163
178
|
if (dialogGuard.Suppressed.Count > 0)
|
|
164
179
|
payload["suppressedDialogs"] = dialogGuard.Suppressed;
|
|
165
180
|
if (failureGuard.Warnings.Count > 0)
|
|
@@ -79,10 +79,12 @@ namespace RevitBridge.Tools
|
|
|
79
79
|
string baseName = SanitizeFileName(string.IsNullOrWhiteSpace(prefixInput) ? doc.Title : prefixInput.Trim());
|
|
80
80
|
bool combine = JsonArgs.GetBool(args, "combine", true);
|
|
81
81
|
|
|
82
|
-
// Produced files are found by diffing the directory
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// Produced files are found by diffing the directory, because Revit appends its
|
|
83
|
+
// own view/sheet suffixes to multi-file export names. Each file is compared
|
|
84
|
+
// against ITS OWN pre-export write time, so an overwrite of a pre-existing name
|
|
85
|
+
// still counts as produced while an untouched file that merely happens to be
|
|
86
|
+
// recent (a shared output_dir, another tool writing alongside) does not.
|
|
87
|
+
var before = SnapshotWriteTimes(outputDir);
|
|
86
88
|
|
|
87
89
|
switch (format)
|
|
88
90
|
{
|
|
@@ -93,7 +95,7 @@ namespace RevitBridge.Tools
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
var files = Directory.GetFiles(outputDir)
|
|
96
|
-
.Where(path => !before.
|
|
98
|
+
.Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
|
|
97
99
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
98
100
|
.Select(path => new Dictionary<string, object?>
|
|
99
101
|
{
|
|
@@ -115,6 +117,35 @@ namespace RevitBridge.Tools
|
|
|
115
117
|
}, compact);
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/// <summary>path -> last write time for the files already in the output directory.
|
|
121
|
+
/// A file whose time cannot be read is left out, so the export reports it if it
|
|
122
|
+
/// shows up afterwards rather than silently swallowing a produced file.</summary>
|
|
123
|
+
private static Dictionary<string, DateTime> SnapshotWriteTimes(string directory)
|
|
124
|
+
{
|
|
125
|
+
var snapshot = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
|
126
|
+
foreach (string path in Directory.GetFiles(directory))
|
|
127
|
+
{
|
|
128
|
+
DateTime writtenAt = SafeWriteTime(path);
|
|
129
|
+
if (writtenAt != DateTime.MinValue)
|
|
130
|
+
snapshot[path] = writtenAt;
|
|
131
|
+
}
|
|
132
|
+
return snapshot;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// <summary>DateTime.MinValue when the timestamp is unreadable (file locked or gone),
|
|
136
|
+
/// which compares as "changed" — an unknown file is reported, never hidden.</summary>
|
|
137
|
+
private static DateTime SafeWriteTime(string path)
|
|
138
|
+
{
|
|
139
|
+
try
|
|
140
|
+
{
|
|
141
|
+
return File.GetLastWriteTimeUtc(path);
|
|
142
|
+
}
|
|
143
|
+
catch
|
|
144
|
+
{
|
|
145
|
+
return DateTime.MinValue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
// ------------------------------------------------------------- format runs
|
|
119
150
|
|
|
120
151
|
private static void ExportPdf(Document doc, List<View> views, string outputDir, string baseName, bool combine)
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
using Autodesk.Revit.DB;
|
|
2
|
-
|
|
3
|
-
namespace RevitBridge.Tools
|
|
4
|
-
{
|
|
5
|
-
/// <summary>
|
|
6
|
-
/// Deterministic commit-time failure handling for tool-owned transactions. Without a
|
|
7
|
-
/// preprocessor, Revit resolves commit failures interactively: warnings pop the
|
|
8
|
-
/// transient toast (and spam the journal), and error-severity failures block the
|
|
9
|
-
/// Revit API thread behind the modal resolution dialog until a human clicks — past
|
|
10
|
-
/// the bridge's call budget. Attach() deletes warnings (recorded for the tool
|
|
11
|
-
/// result) and turns errors into a rollback, so a headless caller always gets an
|
|
12
|
-
/// answer instead of a hang.
|
|
13
|
-
/// </summary>
|
|
14
|
-
internal sealed class FailureGuard : IFailuresPreprocessor
|
|
15
|
-
{
|
|
16
|
-
public List<string> Warnings { get; } = new();
|
|
17
|
-
public List<string> Errors { get; } = new();
|
|
18
|
-
|
|
19
|
-
public static FailureGuard Attach(Transaction transaction)
|
|
20
|
-
{
|
|
21
|
-
var guard = new FailureGuard();
|
|
22
|
-
var options = transaction.GetFailureHandlingOptions();
|
|
23
|
-
options.SetFailuresPreprocessor(guard);
|
|
24
|
-
options.SetClearAfterRollback(true);
|
|
25
|
-
transaction.SetFailureHandlingOptions(options);
|
|
26
|
-
return guard;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public FailureProcessingResult PreprocessFailures(FailuresAccessor accessor)
|
|
30
|
-
{
|
|
31
|
-
bool hasError = false;
|
|
32
|
-
foreach (FailureMessageAccessor failure in accessor.GetFailureMessages())
|
|
33
|
-
{
|
|
34
|
-
string description;
|
|
35
|
-
try
|
|
36
|
-
{
|
|
37
|
-
description = failure.GetDescriptionText();
|
|
38
|
-
}
|
|
39
|
-
catch
|
|
40
|
-
{
|
|
41
|
-
description = "(failure without description)";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (failure.GetSeverity() == FailureSeverity.Warning)
|
|
45
|
-
{
|
|
46
|
-
Warnings.Add(description);
|
|
47
|
-
accessor.DeleteWarning(failure);
|
|
48
|
-
}
|
|
49
|
-
else
|
|
50
|
-
{
|
|
51
|
-
hasError = true;
|
|
52
|
-
Errors.Add(description);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return hasError ? FailureProcessingResult.ProceedWithRollBack : FailureProcessingResult.Continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// <summary>Formats the recorded errors for a commit-failure message.</summary>
|
|
59
|
-
public string DescribeErrors()
|
|
60
|
-
=> Errors.Count > 0 ? $" Revit failure(s): {string.Join("; ", Errors)}." : string.Empty;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
using Autodesk.Revit.DB;
|
|
2
|
+
|
|
3
|
+
namespace RevitBridge.Tools
|
|
4
|
+
{
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// Deterministic commit-time failure handling for tool-owned transactions. Without a
|
|
7
|
+
/// preprocessor, Revit resolves commit failures interactively: warnings pop the
|
|
8
|
+
/// transient toast (and spam the journal), and error-severity failures block the
|
|
9
|
+
/// Revit API thread behind the modal resolution dialog until a human clicks — past
|
|
10
|
+
/// the bridge's call budget. Attach() deletes warnings (recorded for the tool
|
|
11
|
+
/// result) and turns errors into a rollback, so a headless caller always gets an
|
|
12
|
+
/// answer instead of a hang.
|
|
13
|
+
/// </summary>
|
|
14
|
+
internal sealed class FailureGuard : IFailuresPreprocessor
|
|
15
|
+
{
|
|
16
|
+
public List<string> Warnings { get; } = new();
|
|
17
|
+
public List<string> Errors { get; } = new();
|
|
18
|
+
|
|
19
|
+
public static FailureGuard Attach(Transaction transaction)
|
|
20
|
+
{
|
|
21
|
+
var guard = new FailureGuard();
|
|
22
|
+
var options = transaction.GetFailureHandlingOptions();
|
|
23
|
+
options.SetFailuresPreprocessor(guard);
|
|
24
|
+
options.SetClearAfterRollback(true);
|
|
25
|
+
transaction.SetFailureHandlingOptions(options);
|
|
26
|
+
return guard;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public FailureProcessingResult PreprocessFailures(FailuresAccessor accessor)
|
|
30
|
+
{
|
|
31
|
+
bool hasError = false;
|
|
32
|
+
foreach (FailureMessageAccessor failure in accessor.GetFailureMessages())
|
|
33
|
+
{
|
|
34
|
+
string description;
|
|
35
|
+
try
|
|
36
|
+
{
|
|
37
|
+
description = failure.GetDescriptionText();
|
|
38
|
+
}
|
|
39
|
+
catch
|
|
40
|
+
{
|
|
41
|
+
description = "(failure without description)";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (failure.GetSeverity() == FailureSeverity.Warning)
|
|
45
|
+
{
|
|
46
|
+
Warnings.Add(description);
|
|
47
|
+
accessor.DeleteWarning(failure);
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
{
|
|
51
|
+
hasError = true;
|
|
52
|
+
Errors.Add(description);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return hasError ? FailureProcessingResult.ProceedWithRollBack : FailureProcessingResult.Continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// <summary>Formats the recorded errors for a commit-failure message.</summary>
|
|
59
|
+
public string DescribeErrors()
|
|
60
|
+
=> Errors.Count > 0 ? $" Revit failure(s): {string.Join("; ", Errors)}." : string.Empty;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -136,7 +136,7 @@ namespace RevitBridge.Tools
|
|
|
136
136
|
return collector;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
var (quickFilter, postPredicate) = BuildParameterFilter(doc, args, CreateBaseCollector);
|
|
139
|
+
var (quickFilter, postPredicate, filterWarnings) = BuildParameterFilter(doc, args, CreateBaseCollector);
|
|
140
140
|
|
|
141
141
|
FilteredElementCollector CreateCollector()
|
|
142
142
|
{
|
|
@@ -147,7 +147,7 @@ namespace RevitBridge.Tools
|
|
|
147
147
|
string scope = DescribeScope(categoryInput, classInput, inActiveView);
|
|
148
148
|
|
|
149
149
|
if (countOnly && postPredicate is null)
|
|
150
|
-
return CountResult(scope, CreateCollector().GetElementCount());
|
|
150
|
+
return CountResult(scope, CreateCollector().GetElementCount(), filterWarnings);
|
|
151
151
|
|
|
152
152
|
var rows = countOnly ? null : new List<Dictionary<string, object?>>(Math.Min(limit, 256));
|
|
153
153
|
int total = 0;
|
|
@@ -161,7 +161,7 @@ namespace RevitBridge.Tools
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (rows is null)
|
|
164
|
-
return CountResult(scope, total);
|
|
164
|
+
return CountResult(scope, total, filterWarnings);
|
|
165
165
|
|
|
166
166
|
bool hasMore = offset + rows.Count < total;
|
|
167
167
|
int? nextOffset = hasMore ? offset + rows.Count : null;
|
|
@@ -172,9 +172,10 @@ namespace RevitBridge.Tools
|
|
|
172
172
|
var sample = rows.Take(3).Select(row => $"'{row["name"]}' ({row["id"]})");
|
|
173
173
|
sampleText = $" Sample: {string.Join(", ", sample)}.";
|
|
174
174
|
}
|
|
175
|
-
string compact = hasMore
|
|
175
|
+
string compact = (hasMore
|
|
176
176
|
? $"{scope}: {total} total, returned {rows.Count} at offset {offset}, has_more (next_offset {nextOffset}).{sampleText}"
|
|
177
|
-
: $"{scope}: {total} total, returned {rows.Count} at offset {offset}.{sampleText}"
|
|
177
|
+
: $"{scope}: {total} total, returned {rows.Count} at offset {offset}.{sampleText}")
|
|
178
|
+
+ WarningSuffix(filterWarnings);
|
|
178
179
|
|
|
179
180
|
return new ToolOutput(new
|
|
180
181
|
{
|
|
@@ -184,11 +185,16 @@ namespace RevitBridge.Tools
|
|
|
184
185
|
has_more = hasMore,
|
|
185
186
|
next_offset = nextOffset,
|
|
186
187
|
elements = rows,
|
|
188
|
+
warnings = filterWarnings.Count > 0 ? filterWarnings : null,
|
|
187
189
|
}, compact);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
private static ToolOutput CountResult(string scope, int count)
|
|
191
|
-
=> new(new { total_count = count, count_only = true
|
|
192
|
+
private static ToolOutput CountResult(string scope, int count, IReadOnlyList<string> warnings)
|
|
193
|
+
=> new(new { total_count = count, count_only = true, warnings = warnings.Count > 0 ? warnings : null },
|
|
194
|
+
$"{scope}: {count} elements match." + WarningSuffix(warnings));
|
|
195
|
+
|
|
196
|
+
private static string WarningSuffix(IReadOnlyList<string> warnings)
|
|
197
|
+
=> warnings.Count > 0 ? " WARNING: " + string.Join(" ", warnings) : string.Empty;
|
|
192
198
|
|
|
193
199
|
private static string DescribeScope(string? category, string? className, bool inActiveView)
|
|
194
200
|
{
|
|
@@ -263,11 +269,11 @@ namespace RevitBridge.Tools
|
|
|
263
269
|
public FilterRule? QuickRule { get; set; }
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
private static (ElementFilter? Quick, Func<Element, bool>? Post) BuildParameterFilter(
|
|
272
|
+
private static (ElementFilter? Quick, Func<Element, bool>? Post, IReadOnlyList<string> Warnings) BuildParameterFilter(
|
|
267
273
|
Document doc, JsonElement args, Func<FilteredElementCollector> createBaseCollector)
|
|
268
274
|
{
|
|
269
275
|
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty("filter", out var filterElement) || filterElement.ValueKind != JsonValueKind.Object)
|
|
270
|
-
return (null, null);
|
|
276
|
+
return (null, null, Array.Empty<string>());
|
|
271
277
|
|
|
272
278
|
bool matchAny = string.Equals(JsonArgs.GetString(filterElement, "match"), "any", StringComparison.OrdinalIgnoreCase);
|
|
273
279
|
if (!filterElement.TryGetProperty("rules", out var rulesElement) || rulesElement.ValueKind != JsonValueKind.Array)
|
|
@@ -275,7 +281,7 @@ namespace RevitBridge.Tools
|
|
|
275
281
|
|
|
276
282
|
var rules = rulesElement.EnumerateArray().Select(ParseRule).ToList();
|
|
277
283
|
if (rules.Count == 0)
|
|
278
|
-
return (null, null);
|
|
284
|
+
return (null, null, Array.Empty<string>());
|
|
279
285
|
|
|
280
286
|
// Probe a few in-scope elements so display-name parameters resolve to ids
|
|
281
287
|
// and value typing / unit conversion can use the parameter's storage + spec.
|
|
@@ -287,6 +293,29 @@ namespace RevitBridge.Tools
|
|
|
287
293
|
break;
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
// A display-name rule that no probed element carries is the localized-UI trap:
|
|
297
|
+
// the post-scan then matches nothing and the result reads "0 total" -- which the
|
|
298
|
+
// caller cannot tell apart from a genuine no-match. Typical case: an English
|
|
299
|
+
// display name queried against a non-English UI, where the same parameter
|
|
300
|
+
// carries a translated name.
|
|
301
|
+
// A warning (not an error: the parameter may exist past the probe window,
|
|
302
|
+
// and is_empty legitimately matches missing parameters) makes the zero honest.
|
|
303
|
+
var warnings = new List<string>();
|
|
304
|
+
if (probes.Count > 0)
|
|
305
|
+
{
|
|
306
|
+
foreach (var rule in rules)
|
|
307
|
+
{
|
|
308
|
+
if (rule.BuiltIn != null || rule.SharedGuid != null)
|
|
309
|
+
continue;
|
|
310
|
+
if (probes.All(probe => FindParameter(probe, rule) is null))
|
|
311
|
+
warnings.Add(
|
|
312
|
+
$"Filter parameter '{rule.ParamInput}' was not found on any of the {probes.Count} probed element(s) in scope, "
|
|
313
|
+
+ "so a 0-match result may mean 'unknown parameter name', not 'no matching elements'. Display names are "
|
|
314
|
+
+ "localized in non-English Revit UIs -- prefer the language-independent BuiltInParameter enum name "
|
|
315
|
+
+ "(get_element_details reports it as builtInParameter per parameter).");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
290
319
|
// Display-name promotion is only trustworthy inside one category/class: the
|
|
291
320
|
// probe sees just the first ProbeSize elements in collector order, so in an
|
|
292
321
|
// unscoped query a unanimous sample can still hide other categories further
|
|
@@ -321,10 +350,10 @@ namespace RevitBridge.Tools
|
|
|
321
350
|
// OR with any post-scan rule means everything must be post-scanned: a
|
|
322
351
|
// collector-level OR filter would wrongly exclude post-rule-only matches.
|
|
323
352
|
if (postRules.Count > 0)
|
|
324
|
-
return (null, element => rules.Any(rule => EvaluatePost(doc, element, rule)));
|
|
353
|
+
return (null, element => rules.Any(rule => EvaluatePost(doc, element, rule)), warnings);
|
|
325
354
|
|
|
326
355
|
var filters = quickRules.Select(rule => (ElementFilter)new ElementParameterFilter(rule.QuickRule!)).ToList();
|
|
327
|
-
return (filters.Count == 1 ? filters[0] : new LogicalOrFilter(filters), null);
|
|
356
|
+
return (filters.Count == 1 ? filters[0] : new LogicalOrFilter(filters), null, warnings);
|
|
328
357
|
}
|
|
329
358
|
|
|
330
359
|
ElementFilter? quick = quickRules.Count > 0
|
|
@@ -333,7 +362,7 @@ namespace RevitBridge.Tools
|
|
|
333
362
|
Func<Element, bool>? post = postRules.Count > 0
|
|
334
363
|
? element => postRules.All(rule => EvaluatePost(doc, element, rule))
|
|
335
364
|
: null;
|
|
336
|
-
return (quick, post);
|
|
365
|
+
return (quick, post, warnings);
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
private static Rule ParseRule(JsonElement element)
|
|
@@ -501,6 +530,11 @@ namespace RevitBridge.Tools
|
|
|
501
530
|
}
|
|
502
531
|
}
|
|
503
532
|
|
|
533
|
+
/// <summary>Per-element evaluation for rules the collector cannot quick-filter.
|
|
534
|
+
/// A value that does not fit the parameter's storage type is a caller mistake, and
|
|
535
|
+
/// must fail the same way it does on the quick path (TryBuildQuickRule) — otherwise
|
|
536
|
+
/// the identical query reports a clear error when scoped and a silent zero matches
|
|
537
|
+
/// when unscoped.</summary>
|
|
504
538
|
private static bool EvaluatePost(Document doc, Element element, Rule rule)
|
|
505
539
|
{
|
|
506
540
|
var parameter = FindParameter(element, rule);
|
|
@@ -544,7 +578,7 @@ namespace RevitBridge.Tools
|
|
|
544
578
|
if (rule.Op == RuleOp.Contains)
|
|
545
579
|
return ComparableText(doc, parameter).Contains(ValueAsString(rule.Value), StringComparison.Ordinal);
|
|
546
580
|
if (!TryValueAsDouble(rule.Value, out double raw))
|
|
547
|
-
|
|
581
|
+
throw new ArgumentException($"Filter rule on numeric parameter '{parameter.Definition?.Name}' needs a numeric value (got {rule.Value.GetRawText()}).");
|
|
548
582
|
double target = ToInternalUnits(doc, rule, parameter, raw);
|
|
549
583
|
double actual = parameter.AsDouble();
|
|
550
584
|
return rule.Op switch
|
|
@@ -559,7 +593,7 @@ namespace RevitBridge.Tools
|
|
|
559
593
|
if (rule.Op == RuleOp.Contains)
|
|
560
594
|
return ComparableText(doc, parameter).Contains(ValueAsString(rule.Value), StringComparison.Ordinal);
|
|
561
595
|
if (!TryValueAsInt(rule.Value, out int target))
|
|
562
|
-
|
|
596
|
+
throw new ArgumentException($"Filter rule on integer parameter '{parameter.Definition?.Name}' needs an integer or boolean value.");
|
|
563
597
|
return Compare(parameter.AsInteger().CompareTo(target), rule.Op);
|
|
564
598
|
}
|
|
565
599
|
case StorageType.ElementId:
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Activates a view or sheet in the Revit UI — the API equivalent of double-clicking
|
|
8
|
+
/// it in the Project Browser. Uses UIDocument.RequestViewChange, which is explicitly
|
|
9
|
+
/// permitted from an ExternalEvent callback (where all bridge tools run) as long as no
|
|
10
|
+
/// transaction is open; the tool opens none. The activation is asynchronous by design:
|
|
11
|
+
/// Revit performs it the moment control returns from the bridge call.
|
|
12
|
+
/// </summary>
|
|
13
|
+
internal sealed class OpenView : ITool
|
|
14
|
+
{
|
|
15
|
+
public string Name => "open_view";
|
|
16
|
+
public string Label => "Open View";
|
|
17
|
+
public string Description => "Activate a view or sheet in the Revit UI, like double-clicking it in the Project Browser. Identify the target by view_id (an id from get_elements category Views/Sheets or from view/sheet creation) or by name (exact view name, sheet number like 'A-101', or sheet number - name; case-insensitive). Activation is queued and completes the instant this call returns control to Revit — a capture_view immediately after may still show the previous active view. View templates and internal views cannot be opened.";
|
|
18
|
+
public bool Write => false;
|
|
19
|
+
|
|
20
|
+
public object ParametersSchema => new
|
|
21
|
+
{
|
|
22
|
+
type = "object",
|
|
23
|
+
properties = new
|
|
24
|
+
{
|
|
25
|
+
view_id = new { type = "integer", description = "Id of the view or sheet to activate." },
|
|
26
|
+
name = new { type = "string", description = "Exact view name, sheet number, or sheet 'number - name' (case-insensitive). Used when view_id is absent." },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
public string? PromptSnippet => "Activate a Revit view or sheet in the UI by id or name (like double-clicking it in the Project Browser).";
|
|
31
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
32
|
+
{
|
|
33
|
+
"Use open_view to put a view or sheet on the user's screen (e.g. after creating a sheet); activation completes right after the call, so capture_view in the SAME call batch may still show the old view.",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
37
|
+
{
|
|
38
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
39
|
+
var uiapp = context.UIApplication ?? throw new NoActiveDocumentException();
|
|
40
|
+
var uidoc = uiapp.ActiveUIDocument ?? throw new NoActiveDocumentException();
|
|
41
|
+
|
|
42
|
+
long? viewId = JsonArgs.GetLong(args, "view_id");
|
|
43
|
+
string? name = JsonArgs.GetString(args, "name");
|
|
44
|
+
if (viewId is null && string.IsNullOrWhiteSpace(name))
|
|
45
|
+
throw new ArgumentException("Pass view_id or name to identify the view or sheet to open.");
|
|
46
|
+
|
|
47
|
+
View view = viewId is { } id ? ResolveById(doc, id) : ResolveByName(doc, name!.Trim());
|
|
48
|
+
|
|
49
|
+
try
|
|
50
|
+
{
|
|
51
|
+
uidoc.RequestViewChange(view);
|
|
52
|
+
}
|
|
53
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException ex)
|
|
54
|
+
{
|
|
55
|
+
throw new ArgumentException($"View '{view.Name}' (id {view.Id.Value}) cannot be activated: {ex.Message}");
|
|
56
|
+
}
|
|
57
|
+
catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
|
|
58
|
+
{
|
|
59
|
+
throw new InvalidOperationException(
|
|
60
|
+
$"Revit refused to queue the view change to '{view.Name}': {ex.Message} "
|
|
61
|
+
+ "The document may be mid-edit; retry after the current operation finishes.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
string kind = view is ViewSheet sheet ? $"sheet {sheet.SheetNumber}" : view.ViewType.ToString();
|
|
65
|
+
return new ToolOutput(new
|
|
66
|
+
{
|
|
67
|
+
requestedViewId = view.Id.Value,
|
|
68
|
+
viewName = view.Name,
|
|
69
|
+
viewType = view.ViewType.ToString(),
|
|
70
|
+
}, $"Queued activation of {kind} '{view.Name}' (id {view.Id.Value}); it opens as soon as Revit regains control.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static View ResolveById(Document doc, long id)
|
|
74
|
+
{
|
|
75
|
+
var view = doc.GetElement(new ElementId(id)) as View
|
|
76
|
+
?? throw new ArgumentException($"Element {id} is not a view or sheet (or does not exist). Find view ids with get_elements, category 'Views' or 'Sheets'.");
|
|
77
|
+
if (view.IsTemplate)
|
|
78
|
+
throw new ArgumentException($"View '{view.Name}' (id {id}) is a view template; templates cannot be opened.");
|
|
79
|
+
return view;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static View ResolveByName(Document doc, string name)
|
|
83
|
+
{
|
|
84
|
+
var candidates = new FilteredElementCollector(doc)
|
|
85
|
+
.OfClass(typeof(View))
|
|
86
|
+
.Cast<View>()
|
|
87
|
+
.Where(view => !view.IsTemplate)
|
|
88
|
+
.Where(view =>
|
|
89
|
+
string.Equals(view.Name, name, StringComparison.OrdinalIgnoreCase)
|
|
90
|
+
|| (view is ViewSheet sheet
|
|
91
|
+
&& (string.Equals(sheet.SheetNumber, name, StringComparison.OrdinalIgnoreCase)
|
|
92
|
+
|| string.Equals($"{sheet.SheetNumber} - {sheet.Name}", name, StringComparison.OrdinalIgnoreCase))))
|
|
93
|
+
.ToList();
|
|
94
|
+
|
|
95
|
+
if (candidates.Count == 1)
|
|
96
|
+
return candidates[0];
|
|
97
|
+
if (candidates.Count == 0)
|
|
98
|
+
throw new ArgumentException($"No view or sheet named '{name}'. Find the exact name or sheet number with get_elements, category 'Views' or 'Sheets'.");
|
|
99
|
+
string list = string.Join("; ", candidates.Take(8).Select(view => $"'{view.Name}' (id {view.Id.Value}, {view.ViewType})"));
|
|
100
|
+
throw new ArgumentException($"'{name}' matches {candidates.Count} views: {list}. Pass view_id to disambiguate.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|