pi-revit 0.3.0 → 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 +394 -351
- package/README.md +438 -25
- package/bin/pi-revit.js +3 -5
- package/extensions/pi-revit/index.ts +151 -92
- 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 +58 -57
- package/scripts/build.ps1 +9 -3
- package/scripts/check-sdk.ps1 +66 -0
- package/scripts/deploy.ps1 +16 -4
- package/skills/pi-revit/SKILL.md +181 -17
- 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 +79 -29
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +26 -4
- 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 +2 -2
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +23 -13
- package/src/Revit/Tools/ExportDocuments.cs +2 -1
- package/src/Revit/Tools/GetElementDetails.cs +32 -2
- package/src/Revit/Tools/GetElementRelationships.cs +76 -0
- package/src/Revit/Tools/GetElements.cs +21 -7
- 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/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 +2 -1
- 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/TransformElements.cs +58 -0
|
@@ -39,7 +39,8 @@ namespace RevitBridge
|
|
|
39
39
|
private readonly CommandQueue _queue;
|
|
40
40
|
private readonly ToolRegistry _registry;
|
|
41
41
|
private readonly string _revitVersion;
|
|
42
|
-
private readonly Func<bool> _hasOpenDocument;
|
|
42
|
+
private readonly Func<bool> _hasOpenDocument;
|
|
43
|
+
private readonly OperationStore _operations = new();
|
|
43
44
|
|
|
44
45
|
/// <summary>How often the bridge re-checks that bridge.json still points somewhere
|
|
45
46
|
/// live (see EnsureBridgeInfo). Cheap: one small file read per tick.</summary>
|
|
@@ -117,17 +118,23 @@ namespace RevitBridge
|
|
|
117
118
|
|
|
118
119
|
public static string BridgeInfoPath() => Path.Combine(BridgeInfoDirectory(), "bridge.json");
|
|
119
120
|
|
|
120
|
-
private void WriteBridgeInfo()
|
|
121
|
-
{
|
|
122
|
-
Directory.CreateDirectory(BridgeInfoDirectory());
|
|
123
|
-
|
|
121
|
+
private void WriteBridgeInfo()
|
|
122
|
+
{
|
|
123
|
+
Directory.CreateDirectory(BridgeInfoDirectory());
|
|
124
|
+
string json = JsonSerializer.Serialize(new
|
|
124
125
|
{
|
|
125
126
|
baseUrl = $"http://127.0.0.1:{_port}",
|
|
126
127
|
token = _token,
|
|
127
128
|
pid = Environment.ProcessId,
|
|
128
|
-
revitVersion = _revitVersion,
|
|
129
|
+
revitVersion = _revitVersion,
|
|
130
|
+
bridgeId = _operations.Generation,
|
|
131
|
+
supportsOperationTracking = true,
|
|
129
132
|
startedAtUtc = DateTime.UtcNow.ToString("o"),
|
|
130
|
-
}, new JsonSerializerOptions { WriteIndented = true })
|
|
133
|
+
}, new JsonSerializerOptions { WriteIndented = true });
|
|
134
|
+
string instances = Path.Combine(BridgeInfoDirectory(), "instances");
|
|
135
|
+
Directory.CreateDirectory(instances);
|
|
136
|
+
File.WriteAllText(Path.Combine(instances, _operations.Generation + ".json"), json);
|
|
137
|
+
File.WriteAllText(BridgeInfoPath(), json);
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
/// <summary>
|
|
@@ -186,8 +193,9 @@ namespace RevitBridge
|
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
private
|
|
190
|
-
{
|
|
196
|
+
private void DeleteBridgeInfoIfOwned()
|
|
197
|
+
{
|
|
198
|
+
try { File.Delete(Path.Combine(BridgeInfoDirectory(), "instances", _operations.Generation + ".json")); } catch { }
|
|
191
199
|
try
|
|
192
200
|
{
|
|
193
201
|
string path = BridgeInfoPath();
|
|
@@ -337,7 +345,10 @@ namespace RevitBridge
|
|
|
337
345
|
private async Task<(int Status, object Response)> RouteAsync(string method, string path, Dictionary<string, string> query, string body, CancellationToken token)
|
|
338
346
|
{
|
|
339
347
|
if (method == "GET" && path == "/ping")
|
|
340
|
-
return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion });
|
|
348
|
+
return (200, new { ok = true, service = "revit-bridge", revitVersion = _revitVersion, pid = Environment.ProcessId, addinVersion = AddinVersion, bridgeId = _operations.Generation, supportsOperationTracking = true });
|
|
349
|
+
|
|
350
|
+
if (method == "GET" && path.StartsWith("/operations/", StringComparison.Ordinal))
|
|
351
|
+
return (200, _operations.Status(WebUtility.UrlDecode(path["/operations/".Length..])));
|
|
341
352
|
|
|
342
353
|
if (method == "GET" && path == "/tools")
|
|
343
354
|
{
|
|
@@ -378,14 +389,6 @@ namespace RevitBridge
|
|
|
378
389
|
if (tool is null)
|
|
379
390
|
return (404, new { error = true, message = $"Unknown tool: {name}" });
|
|
380
391
|
|
|
381
|
-
// Pre-check before enqueueing: with zero documents open Revit does not
|
|
382
|
-
// pump ExternalEvents, so a queued call would hang instead of failing.
|
|
383
|
-
// The in-queue NoActiveDocumentException below remains as the backstop
|
|
384
|
-
// for a document closing between this check and execution. Tools that
|
|
385
|
-
// never touch the Revit API (RequiresDocument = false) skip the gate.
|
|
386
|
-
if (tool.RequiresDocument && !_hasOpenDocument())
|
|
387
|
-
return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." });
|
|
388
|
-
|
|
389
392
|
JsonElement args;
|
|
390
393
|
try
|
|
391
394
|
{
|
|
@@ -399,34 +402,81 @@ namespace RevitBridge
|
|
|
399
402
|
return (400, new { error = true, message = $"Invalid JSON tool arguments: {ex.Message}" });
|
|
400
403
|
}
|
|
401
404
|
|
|
402
|
-
int timeoutMs = ResolveTimeoutMs(query);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
+
int timeoutMs = ResolveTimeoutMs(query);
|
|
406
|
+
|
|
407
|
+
if (query.TryGetValue("operation_id", out string? operationId))
|
|
408
|
+
{
|
|
409
|
+
try
|
|
410
|
+
{
|
|
411
|
+
var (entry, isNew) = _operations.Reserve(operationId, name, args, TimeSpan.FromMilliseconds(timeoutMs));
|
|
412
|
+
var completion = _operations.Wait(entry);
|
|
413
|
+
if (isNew) _ = CompleteTrackedOperationAsync(entry, tool, args, timeoutMs);
|
|
414
|
+
var reply = await completion;
|
|
415
|
+
return (reply.Status, reply.Response);
|
|
416
|
+
}
|
|
417
|
+
catch (ArgumentException error) { return (409, new { error = true, message = error.Message, operation_id = operationId }); }
|
|
418
|
+
catch (InvalidOperationException error) { return (503, new { error = true, message = error.Message, operation_id = operationId }); }
|
|
419
|
+
}
|
|
420
|
+
var result = await RunToolAsync(tool, args, timeoutMs, null);
|
|
421
|
+
return (result.Status, result.Response);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async Task CompleteTrackedOperationAsync(OperationStore.Entry entry, ITool tool, JsonElement args, int timeoutMs)
|
|
425
|
+
{
|
|
426
|
+
var reply = await RunToolAsync(tool, args, timeoutMs, entry);
|
|
427
|
+
_operations.Complete(entry, new OperationStore.Reply(reply.Status, reply.Response, reply.Outcome));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private async Task<(int Status, object Response, string? Outcome)> RunToolAsync(ITool tool, JsonElement args, int timeoutMs, OperationStore.Entry? operation)
|
|
431
|
+
{
|
|
432
|
+
string name = tool.Name;
|
|
433
|
+
// Cached receipts remain accessible even when the active document closes.
|
|
434
|
+
if (tool.RequiresDocument && !_hasOpenDocument())
|
|
435
|
+
return (409, new { error = true, hasActiveDocument = false, message = "No active Revit document is open." }, null);
|
|
436
|
+
|
|
437
|
+
void StartOperation()
|
|
438
|
+
{
|
|
439
|
+
if (operation != null && !_operations.TryStart(operation)) throw new TimeoutException("Operation expired before execution; no tool action was performed.");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try
|
|
405
443
|
{
|
|
406
444
|
object? output = tool.RequiresDocument
|
|
407
445
|
? await _queue.RunAsync(uiApp =>
|
|
408
|
-
{
|
|
446
|
+
{
|
|
447
|
+
StartOperation();
|
|
409
448
|
var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
|
|
410
|
-
RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name);
|
|
449
|
+
RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name, tool.Write);
|
|
411
450
|
return tool.Execute(args, new ToolContext(document, uiApp));
|
|
412
451
|
}, TimeSpan.FromMilliseconds(timeoutMs))
|
|
413
452
|
// RequiresDocument = false tools never touch the Revit API, so they
|
|
414
453
|
// run right here on the server task instead of the CommandQueue.
|
|
415
|
-
:
|
|
416
|
-
|
|
417
|
-
return (200, BuildToolResponse(name, output));
|
|
454
|
+
: ExecuteWithoutDocument();
|
|
455
|
+
|
|
456
|
+
try { return (200, BuildToolResponse(name, output), null); }
|
|
457
|
+
catch (Exception error)
|
|
458
|
+
{
|
|
459
|
+
return (500, new { error = true, toolName = name, result_unavailable = true,
|
|
460
|
+
message = "The tool finished, but its response could not be constructed. Model or other effects may already have occurred; inspect them before retrying. " + error.Message }, "result_unavailable");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
object? ExecuteWithoutDocument()
|
|
464
|
+
{
|
|
465
|
+
StartOperation();
|
|
466
|
+
return tool.Execute(args, new ToolContext(null, null));
|
|
467
|
+
}
|
|
418
468
|
}
|
|
419
469
|
catch (NoActiveDocumentException ex)
|
|
420
470
|
{
|
|
421
|
-
return (409, new { error = true, hasActiveDocument = false, message = ex.Message });
|
|
471
|
+
return (409, new { error = true, hasActiveDocument = false, message = ex.Message }, null);
|
|
422
472
|
}
|
|
423
473
|
catch (ArgumentException ex)
|
|
424
474
|
{
|
|
425
|
-
return (400, new { error = true, toolName = name, message = ex.Message });
|
|
475
|
+
return (400, new { error = true, toolName = name, message = ex.Message }, null);
|
|
426
476
|
}
|
|
427
477
|
catch (Exception ex)
|
|
428
478
|
{
|
|
429
|
-
return (500, new { error = true, toolName = name, message = ex.Message });
|
|
479
|
+
return (500, new { error = true, toolName = name, message = ex.Message }, null);
|
|
430
480
|
}
|
|
431
481
|
}
|
|
432
482
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
using System.Diagnostics;
|
|
2
|
+
using System.Security.Cryptography;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge;
|
|
6
|
+
|
|
7
|
+
/// <summary>Session-local operation receipts. Expiring a result never permits the same ID to execute again.</summary>
|
|
8
|
+
internal sealed class OperationStore
|
|
9
|
+
{
|
|
10
|
+
internal sealed record Reply(int Status, object Response, string? Outcome = null);
|
|
11
|
+
internal sealed class Entry
|
|
12
|
+
{
|
|
13
|
+
internal required string Id;
|
|
14
|
+
internal required string Tool;
|
|
15
|
+
internal required string Fingerprint;
|
|
16
|
+
internal required long Deadline;
|
|
17
|
+
internal DateTimeOffset Created = DateTimeOffset.UtcNow;
|
|
18
|
+
internal DateTimeOffset? Started;
|
|
19
|
+
internal DateTimeOffset? Finished;
|
|
20
|
+
internal string State = "queued";
|
|
21
|
+
internal TaskCompletionSource<Reply>? Completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
22
|
+
internal Reply? SavedReply;
|
|
23
|
+
internal int SavedBytes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private readonly object _gate = new();
|
|
27
|
+
private readonly Dictionary<string, Entry> _entries = new(StringComparer.Ordinal);
|
|
28
|
+
private readonly Queue<Entry> _results = new();
|
|
29
|
+
private readonly int _maxRecords;
|
|
30
|
+
private readonly int _maxResults;
|
|
31
|
+
private readonly int _maxBytes;
|
|
32
|
+
private int _bytes;
|
|
33
|
+
public string Generation { get; } = Guid.NewGuid().ToString("N");
|
|
34
|
+
|
|
35
|
+
internal OperationStore(int maxRecords = 10000, int maxResults = 128, int maxBytes = 32 * 1024 * 1024)
|
|
36
|
+
{
|
|
37
|
+
_maxRecords = maxRecords; _maxResults = maxResults; _maxBytes = maxBytes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public (Entry Entry, bool IsNew) Reserve(string id, string tool, JsonElement args, TimeSpan timeout)
|
|
41
|
+
{
|
|
42
|
+
id = ValidateId(id);
|
|
43
|
+
string fingerprint = Fingerprint(tool, args);
|
|
44
|
+
lock (_gate)
|
|
45
|
+
{
|
|
46
|
+
if (_entries.TryGetValue(id, out var existing))
|
|
47
|
+
{
|
|
48
|
+
if (existing.Fingerprint != fingerprint) throw new ArgumentException("operation_id was already used with different tool arguments. No new operation was started.");
|
|
49
|
+
return (existing, false);
|
|
50
|
+
}
|
|
51
|
+
if (_entries.Count >= _maxRecords) throw new InvalidOperationException("Operation receipt capacity reached. Restart the bridge before starting new tracked operations; existing receipts remain queryable.");
|
|
52
|
+
var entry = new Entry { Id = id, Tool = tool, Fingerprint = fingerprint,
|
|
53
|
+
Deadline = Stopwatch.GetTimestamp() + (long)(timeout.TotalSeconds * Stopwatch.Frequency) };
|
|
54
|
+
_entries.Add(id, entry);
|
|
55
|
+
return (entry, true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public Task<Reply> Wait(Entry entry)
|
|
60
|
+
{
|
|
61
|
+
lock (_gate)
|
|
62
|
+
{
|
|
63
|
+
ExpireQueued(entry);
|
|
64
|
+
return entry.Completion?.Task ?? Task.FromResult(entry.SavedReply ?? new Reply(409, new
|
|
65
|
+
{
|
|
66
|
+
error = true, operation_id = entry.Id, state = entry.State,
|
|
67
|
+
message = "The operation already finished, but its full result has expired. It was not repeated. Inspect current model state before deciding on a new operation.",
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public bool TryStart(Entry entry)
|
|
73
|
+
{
|
|
74
|
+
lock (_gate)
|
|
75
|
+
{
|
|
76
|
+
ExpireQueued(entry);
|
|
77
|
+
if (entry.State != "queued") return false;
|
|
78
|
+
entry.State = "running"; entry.Started = DateTimeOffset.UtcNow;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public void Complete(Entry entry, Reply reply)
|
|
84
|
+
{
|
|
85
|
+
lock (_gate)
|
|
86
|
+
{
|
|
87
|
+
ExpireQueued(entry);
|
|
88
|
+
if (entry.Completion == null) return;
|
|
89
|
+
Finish(entry, reply.Outcome ?? (reply.Status < 400 ? "succeeded" : "failed"), reply);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public object Status(string id)
|
|
94
|
+
{
|
|
95
|
+
var parts = id.Split(':');
|
|
96
|
+
if (parts.Length == 2 && Guid.TryParse(parts[1], out var uuid)) id = parts[0] + ":" + uuid.ToString("N");
|
|
97
|
+
lock (_gate)
|
|
98
|
+
{
|
|
99
|
+
if (!_entries.TryGetValue(id, out var entry)) return new
|
|
100
|
+
{
|
|
101
|
+
operation_id = id, bridge_id = Generation, state = "unknown",
|
|
102
|
+
message = "No receipt exists in this bridge session. The outcome is unknown; this does not prove that the operation never executed. Check the original bridge and model before retrying.",
|
|
103
|
+
};
|
|
104
|
+
ExpireQueued(entry);
|
|
105
|
+
return new
|
|
106
|
+
{
|
|
107
|
+
operation_id = id, bridge_id = Generation, tool = entry.Tool, state = entry.State,
|
|
108
|
+
created_at = entry.Created, started_at = entry.Started, finished_at = entry.Finished,
|
|
109
|
+
result_available = entry.SavedReply != null, result_http_status = entry.SavedReply?.Status,
|
|
110
|
+
result = entry.SavedReply?.Response,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private void ExpireQueued(Entry entry)
|
|
116
|
+
{
|
|
117
|
+
if (entry.State == "queued" && Stopwatch.GetTimestamp() > entry.Deadline)
|
|
118
|
+
Finish(entry, "expired_before_start", new Reply(408, new { error = true, operation_id = entry.Id,
|
|
119
|
+
message = "Operation expired before it started; no tool action was performed." }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private void Finish(Entry entry, string state, Reply reply)
|
|
123
|
+
{
|
|
124
|
+
var completion = entry.Completion!;
|
|
125
|
+
// Keep a detached JSON value, never API objects or callbacks, in the receipt cache.
|
|
126
|
+
byte[] bytes;
|
|
127
|
+
try { bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response); }
|
|
128
|
+
catch (Exception error)
|
|
129
|
+
{
|
|
130
|
+
state = "result_unavailable";
|
|
131
|
+
reply = new Reply(500, new { error = true, message = "The tool finished but its receipt could not be serialized. Model or other effects may already have occurred. Inspect before retrying. " + error.Message });
|
|
132
|
+
bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response);
|
|
133
|
+
}
|
|
134
|
+
entry.State = state; entry.Finished = DateTimeOffset.UtcNow;
|
|
135
|
+
entry.SavedReply = new Reply(reply.Status, JsonSerializer.Deserialize<JsonElement>(bytes));
|
|
136
|
+
entry.SavedBytes = bytes.Length; _bytes += bytes.Length;
|
|
137
|
+
_results.Enqueue(entry);
|
|
138
|
+
entry.Completion = null;
|
|
139
|
+
while (_results.Count > _maxResults || _bytes > _maxBytes)
|
|
140
|
+
{
|
|
141
|
+
var old = _results.Dequeue();
|
|
142
|
+
_bytes -= old.SavedBytes; old.SavedBytes = 0; old.SavedReply = null;
|
|
143
|
+
}
|
|
144
|
+
completion.TrySetResult(reply);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private string ValidateId(string id)
|
|
148
|
+
{
|
|
149
|
+
string[] parts = id.Split(':');
|
|
150
|
+
if (parts.Length != 2 || parts[0] != Generation || !Guid.TryParse(parts[1], out _))
|
|
151
|
+
throw new ArgumentException("operation_id must contain this bridge session's bridgeId and a UUID. An ID from another bridge/restart cannot be replayed; its outcome must be checked separately.");
|
|
152
|
+
return parts[0] + ":" + Guid.Parse(parts[1]).ToString("N");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private static string Fingerprint(string tool, JsonElement args)
|
|
156
|
+
{
|
|
157
|
+
using var stream = new MemoryStream();
|
|
158
|
+
using (var writer = new Utf8JsonWriter(stream))
|
|
159
|
+
{
|
|
160
|
+
void Write(JsonElement element)
|
|
161
|
+
{
|
|
162
|
+
if (element.ValueKind == JsonValueKind.Object)
|
|
163
|
+
{
|
|
164
|
+
writer.WriteStartObject();
|
|
165
|
+
foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { writer.WritePropertyName(property.Name); Write(property.Value); }
|
|
166
|
+
writer.WriteEndObject();
|
|
167
|
+
}
|
|
168
|
+
else if (element.ValueKind == JsonValueKind.Array)
|
|
169
|
+
{
|
|
170
|
+
writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) Write(item); writer.WriteEndArray();
|
|
171
|
+
}
|
|
172
|
+
else element.WriteTo(writer);
|
|
173
|
+
}
|
|
174
|
+
writer.WriteStartArray(); writer.WriteStringValue(tool); Write(args); writer.WriteEndArray();
|
|
175
|
+
}
|
|
176
|
+
return Convert.ToHexString(SHA256.HashData(stream.ToArray()));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -32,7 +32,10 @@ namespace RevitBridge
|
|
|
32
32
|
object ParametersSchema { get; }
|
|
33
33
|
|
|
34
34
|
/// <summary>True only for tools that mutate the model. Write tools own their transaction.</summary>
|
|
35
|
-
bool Write => false;
|
|
35
|
+
bool Write => false;
|
|
36
|
+
|
|
37
|
+
/// <summary>Potential effects, independent of whether this particular call changes anything.</summary>
|
|
38
|
+
IReadOnlyList<string> Effects => Write ? new[] { "model" } : Array.Empty<string>();
|
|
36
39
|
|
|
37
40
|
/// <summary>
|
|
38
41
|
/// False only for tools that never touch the Revit API. They skip the bridge's
|
|
@@ -76,7 +79,25 @@ namespace RevitBridge
|
|
|
76
79
|
registry.Add(new SetParameters());
|
|
77
80
|
registry.Add(new CaptureView());
|
|
78
81
|
registry.Add(new ExportDocuments());
|
|
79
|
-
registry.Add(new GetModelHealth());
|
|
82
|
+
registry.Add(new GetModelHealth());
|
|
83
|
+
registry.Add(new GetLinkedModels());
|
|
84
|
+
registry.Add(new GetLinkedElements());
|
|
85
|
+
registry.Add(new GetSchedules());
|
|
86
|
+
registry.Add(new GetElementRelationships());
|
|
87
|
+
registry.Add(new SummarizeElements());
|
|
88
|
+
registry.Add(new ManageElementSets());
|
|
89
|
+
registry.Add(new TransformElements());
|
|
90
|
+
registry.Add(new Tools.DeleteElements());
|
|
91
|
+
registry.Add(new ChangeElementTypes());
|
|
92
|
+
registry.Add(new ManageViews());
|
|
93
|
+
registry.Add(new ManageSheets());
|
|
94
|
+
registry.Add(new ManageSheetPlacements());
|
|
95
|
+
registry.Add(new GetScheduleFields());
|
|
96
|
+
registry.Add(new ManageSchedules());
|
|
97
|
+
registry.Add(new CreateTags());
|
|
98
|
+
registry.Add(new QuerySpatialElements());
|
|
99
|
+
registry.Add(new MeasureGeometry());
|
|
100
|
+
registry.Add(new GetModelCoordinates());
|
|
80
101
|
return registry;
|
|
81
102
|
}
|
|
82
103
|
|
|
@@ -102,7 +123,8 @@ namespace RevitBridge
|
|
|
102
123
|
tier = tool.Tier,
|
|
103
124
|
parameters = DescribeParameters(tool),
|
|
104
125
|
executionMode = "sequential",
|
|
105
|
-
write = tool.Write,
|
|
126
|
+
write = tool.Write,
|
|
127
|
+
effects = tool.Effects,
|
|
106
128
|
requiresDocument = tool.RequiresDocument,
|
|
107
129
|
promptSnippet = tool.PromptSnippet,
|
|
108
130
|
promptGuidelines = tool.RequiresDocument
|
|
@@ -123,7 +145,7 @@ namespace RevitBridge
|
|
|
123
145
|
["type"] = "string",
|
|
124
146
|
["description"] = "Exact opaque project.documentId from get_model_overview. Required for document writes and UI mutations; optional for reads. Invalid after close/reopen or bridge restart."
|
|
125
147
|
};
|
|
126
|
-
if (DocumentGuard.AlwaysRequiresIdentity(tool.Name))
|
|
148
|
+
if (tool.Write || DocumentGuard.AlwaysRequiresIdentity(tool.Name))
|
|
127
149
|
{
|
|
128
150
|
var required = schema["required"] as JsonArray ?? new JsonArray();
|
|
129
151
|
if (!required.Any(x => x?.GetValue<string>() == "expected_document_id")) required.Add("expected_document_id");
|
|
@@ -19,7 +19,8 @@ namespace RevitBridge.Tools
|
|
|
19
19
|
/// Long enough that a capture is never pulled out from under a session still reading it.</summary>
|
|
20
20
|
private static readonly TimeSpan CaptureRetention = TimeSpan.FromHours(24);
|
|
21
21
|
|
|
22
|
-
public string Name => "capture_view";
|
|
22
|
+
public string Name => "capture_view";
|
|
23
|
+
public IReadOnlyList<string> Effects => new[] { "files" };
|
|
23
24
|
public string Label => "Capture View";
|
|
24
25
|
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.";
|
|
25
26
|
public string Tier => "advanced";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class ChangeElementTypes : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "change_element_types";
|
|
9
|
+
public string Label => "Change Element Types";
|
|
10
|
+
public string Tier => "advanced";
|
|
11
|
+
public bool Write => true;
|
|
12
|
+
public string Description => "Change the type of 1–200 elements using explicit element_id/type_id pairs. Revit validates each type against the target; invalid or pinned targets are reported per update. Some type changes replace the original element: always use resulting_id/unique_id afterward. Default partial success; atomic=true rolls back all if any update fails. preview=true commit-validates then rolls back, and replacement IDs from previews are temporary. Revit constraints can affect connected/hosted elements; results describe requested targets, not every dependent effect.";
|
|
13
|
+
public object ParametersSchema => new
|
|
14
|
+
{
|
|
15
|
+
type = "object", properties = new
|
|
16
|
+
{
|
|
17
|
+
updates = new { type = "array", minItems = 1, maxItems = 200, items = new
|
|
18
|
+
{
|
|
19
|
+
type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, type_id = new { type = "integer", minimum = 1 } },
|
|
20
|
+
required = new[] { "element_id", "type_id" },
|
|
21
|
+
} },
|
|
22
|
+
preview = ModelEditInputs.PreviewSchema,
|
|
23
|
+
atomic = new { type = "boolean", description = "Roll back the complete batch if any update fails. Default false." },
|
|
24
|
+
}, required = new[] { "updates" },
|
|
25
|
+
};
|
|
26
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
27
|
+
{
|
|
28
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
29
|
+
if (!args.TryGetProperty("updates", out var updates) || updates.ValueKind != JsonValueKind.Array || updates.GetArrayLength() is < 1 or > 200)
|
|
30
|
+
throw new ArgumentException("updates must contain 1–200 element_id/type_id pairs.");
|
|
31
|
+
var seen = new HashSet<long>();
|
|
32
|
+
var steps = new List<ModelEditBatch.Step>();
|
|
33
|
+
foreach (var update in updates.EnumerateArray())
|
|
34
|
+
{
|
|
35
|
+
long elementId = JsonArgs.GetLong(update, "element_id") ?? 0;
|
|
36
|
+
long typeId = JsonArgs.GetLong(update, "type_id") ?? 0;
|
|
37
|
+
if (elementId <= 0 || typeId <= 0 || !seen.Add(elementId)) throw new ArgumentException("Each update needs positive IDs, and element_id must be unique within the batch.");
|
|
38
|
+
steps.Add(new(new() { ["element_id"] = elementId, ["type_id"] = typeId }, () =>
|
|
39
|
+
{
|
|
40
|
+
var element = doc.GetElement(new ElementId(elementId)) ?? throw new ArgumentException($"Element {elementId} not found.");
|
|
41
|
+
if (element.Pinned) throw new ArgumentException("Target is pinned; it was not unpinned.");
|
|
42
|
+
var type = doc.GetElement(new ElementId(typeId)) as ElementType ?? throw new ArgumentException($"Type {typeId} not found.");
|
|
43
|
+
if (!element.IsValidType(type.Id)) throw new ArgumentException($"Type {typeId} is not valid for element {elementId}.");
|
|
44
|
+
var before = ModelEditInputs.Snapshot(element);
|
|
45
|
+
var replacement = element.ChangeTypeId(type.Id);
|
|
46
|
+
doc.Regenerate();
|
|
47
|
+
var resultId = replacement == ElementId.InvalidElementId ? new ElementId(elementId) : replacement;
|
|
48
|
+
var result = doc.GetElement(resultId) ?? throw new InvalidOperationException("Changed element could not be resolved.");
|
|
49
|
+
return new() { ["before"] = before, ["after"] = ModelEditInputs.Snapshot(result),
|
|
50
|
+
["resulting_id"] = result.Id.Value, ["unique_id"] = result.UniqueId,
|
|
51
|
+
["replaced"] = result.Id.Value != elementId,
|
|
52
|
+
["resulting_id_is_temporary"] = result.Id.Value != elementId && JsonArgs.GetBool(args, "preview", false) };
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
var batch = ModelEditBatch.Run(doc, Name, args, steps);
|
|
56
|
+
foreach (var row in batch.Proposed)
|
|
57
|
+
if (row["replaced"] is true) row["resulting_id_is_temporary"] = true;
|
|
58
|
+
return batch.Payload;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
using Autodesk.Revit.DB.Architecture;
|
|
4
|
+
using Autodesk.Revit.DB.Mechanical;
|
|
5
|
+
|
|
6
|
+
namespace RevitBridge.Tools;
|
|
7
|
+
|
|
8
|
+
internal sealed class CreateTags : ITool
|
|
9
|
+
{
|
|
10
|
+
public string Name => "create_tags";
|
|
11
|
+
public string Label => "Create Tags";
|
|
12
|
+
public string Tier => "advanced";
|
|
13
|
+
public bool Write => true;
|
|
14
|
+
public string Description => "Create up to 100 tags for host-document elements in one explicit view using a loaded tag FamilySymbol. kind=element uses IndependentTag; room, space and area use their corresponding spatial tag APIs. Targets contain element_id and head_position [x,y,z] in document internal coordinates with explicit length unit. head_position always means the tag head, including with leader=true. Spatial tags require a compatible plan view and positions at that spatial element's level; orientation applies only to element tags. Templates, perspective views and unlocked 3D views cannot host independent tags. Default partial success; atomic=true rolls back all on any failure. preview=true commit-validates then rolls back; all proposed tag IDs are temporary. Linked targets and face/subelement references are outside this tool.";
|
|
15
|
+
public object ParametersSchema => new
|
|
16
|
+
{
|
|
17
|
+
type = "object", properties = new
|
|
18
|
+
{
|
|
19
|
+
kind = new { type = "string", @enum = new[] { "element", "room", "space", "area" } },
|
|
20
|
+
view_id = new { type = "integer", minimum = 1 }, tag_type_id = new { type = "integer", minimum = 1 }, unit = ModelEditInputs.LengthUnitSchema,
|
|
21
|
+
targets = new { type = "array", minItems = 1, maxItems = 100, items = new { type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, head_position = ModelEditInputs.VectorSchema("Tag head in document internal coordinates, in unit.") }, required = new[] { "element_id", "head_position" } } },
|
|
22
|
+
leader = new { type = "boolean" }, orientation = new { type = "string", @enum = new[] { "horizontal", "vertical" } },
|
|
23
|
+
preview = ModelEditInputs.PreviewSchema, atomic = new { type = "boolean" },
|
|
24
|
+
}, required = new[] { "kind", "view_id", "tag_type_id", "unit", "targets" },
|
|
25
|
+
};
|
|
26
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
27
|
+
{
|
|
28
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
29
|
+
string kind = JsonArgs.GetString(args, "kind") ?? "";
|
|
30
|
+
if (kind is not ("element" or "room" or "space" or "area")) throw new ArgumentException("Unknown tag kind.");
|
|
31
|
+
var view = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "view_id") ?? 0)) as View ?? throw new ArgumentException("view_id is not a view.");
|
|
32
|
+
if (view.IsTemplate || (view is View3D three && (three.IsPerspective || !three.IsLocked))) throw new ArgumentException("Tags require a non-template view; 3D views must be orthographic and locked.");
|
|
33
|
+
if (kind != "element" && view is not ViewPlan) throw new ArgumentException("Spatial tags require a plan view.");
|
|
34
|
+
var type = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "tag_type_id") ?? 0)) as FamilySymbol ?? throw new ArgumentException("tag_type_id must identify a loaded tag FamilySymbol.");
|
|
35
|
+
bool leader = false;
|
|
36
|
+
if (args.TryGetProperty("leader", out var leaderValue))
|
|
37
|
+
{
|
|
38
|
+
if (leaderValue.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException("leader must be boolean.");
|
|
39
|
+
leader = leaderValue.GetBoolean();
|
|
40
|
+
}
|
|
41
|
+
if (kind != "element" && args.TryGetProperty("orientation", out _)) throw new ArgumentException("orientation applies only to element tags.");
|
|
42
|
+
var orientation = (JsonArgs.GetString(args, "orientation") ?? "horizontal") switch
|
|
43
|
+
{
|
|
44
|
+
"horizontal" => TagOrientation.Horizontal, "vertical" => TagOrientation.Vertical, _ => throw new ArgumentException("Invalid orientation."),
|
|
45
|
+
};
|
|
46
|
+
if (!args.TryGetProperty("targets", out var targets) || targets.ValueKind != JsonValueKind.Array || targets.GetArrayLength() is < 1 or > 100) throw new ArgumentException("targets must contain 1–100 tag requests.");
|
|
47
|
+
double scale = ModelEditInputs.LengthScale(args);
|
|
48
|
+
var steps = new List<ModelEditBatch.Step>();
|
|
49
|
+
foreach (var input in targets.EnumerateArray())
|
|
50
|
+
{
|
|
51
|
+
if (input.ValueKind != JsonValueKind.Object) throw new ArgumentException("Each target must be an object.");
|
|
52
|
+
long id = JsonArgs.GetLong(input, "element_id") ?? 0;
|
|
53
|
+
if (id <= 0) throw new ArgumentException("Target element_id must be positive.");
|
|
54
|
+
var point = ModelEditInputs.Vector(input, "head_position").Multiply(scale);
|
|
55
|
+
steps.Add(new(new() { ["element_id"] = id }, () =>
|
|
56
|
+
{
|
|
57
|
+
var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException($"Element {id} not found.");
|
|
58
|
+
if (!type.IsActive) { type.Activate(); doc.Regenerate(); }
|
|
59
|
+
Element tag;
|
|
60
|
+
if (kind == "element")
|
|
61
|
+
{
|
|
62
|
+
var independent = IndependentTag.Create(doc, type.Id, view.Id, new Reference(element), false, orientation, point);
|
|
63
|
+
independent.HasLeader = leader;
|
|
64
|
+
independent.TagHeadPosition = point;
|
|
65
|
+
tag = independent;
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
{
|
|
69
|
+
if (element.Location is not LocationPoint location || view.GenLevel == null || element.LevelId != view.GenLevel.Id)
|
|
70
|
+
throw new ArgumentException("A spatial tag target must be placed on the plan view's level.");
|
|
71
|
+
var anchor = new UV(location.Point.X, location.Point.Y);
|
|
72
|
+
SpatialElementTag spatial = kind switch
|
|
73
|
+
{
|
|
74
|
+
"room" when element is Room => doc.Create.NewRoomTag(new LinkElementId(element.Id), anchor, view.Id),
|
|
75
|
+
"space" when element is Space space => doc.Create.NewSpaceTag(space, anchor, view),
|
|
76
|
+
"area" when element is Area area => doc.Create.NewAreaTag((ViewPlan)view, area, anchor),
|
|
77
|
+
_ => throw new ArgumentException($"Element {id} does not match kind {kind}."),
|
|
78
|
+
};
|
|
79
|
+
if (!spatial.IsValidType(type.Id)) throw new ArgumentException("Tag type does not match the spatial tag category.");
|
|
80
|
+
var replacement = spatial.ChangeTypeId(type.Id);
|
|
81
|
+
if (replacement != ElementId.InvalidElementId) spatial = (SpatialElementTag)doc.GetElement(replacement);
|
|
82
|
+
spatial.HasLeader = leader; spatial.TagHeadPosition = point;
|
|
83
|
+
tag = spatial;
|
|
84
|
+
}
|
|
85
|
+
doc.Regenerate();
|
|
86
|
+
var actual = tag is IndependentTag independentResult ? independentResult.TagHeadPosition : ((SpatialElementTag)tag).TagHeadPosition;
|
|
87
|
+
return new() { ["tag_id"] = tag.Id.Value, ["unique_id"] = tag.UniqueId, ["tag_type_id"] = tag.GetTypeId().Value, ["view_id"] = tag.OwnerViewId.Value,
|
|
88
|
+
["kind"] = kind, ["head_position"] = new[] { actual.X, actual.Y, actual.Z }, ["unit"] = "feet", ["leader"] = leader, ["id_is_temporary"] = false };
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
var batch = ModelEditBatch.Run(doc, Name, args, steps);
|
|
92
|
+
foreach (var row in batch.Proposed) row["id_is_temporary"] = true;
|
|
93
|
+
return batch.Payload;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class DeleteElements : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "delete_elements";
|
|
9
|
+
public string Label => "Delete Elements";
|
|
10
|
+
public string Tier => "advanced";
|
|
11
|
+
public bool Write => true;
|
|
12
|
+
public string Description => "Delete 1–200 explicitly selected elements in one atomic step, returning the full set of IDs returned by Revit's deletion API, including dependents. Use preview=true to inspect that set with commit validation and confirmed rollback. Optional expected_deleted_ids rejects a changed deletion set before commit; pass the full preview set to bind a later deletion to that set. Pinned requested elements are rejected. At most 10,000 deleted IDs are permitted; a larger cascade rolls back. Returned IDs include dependencies removed by Document.Delete, not a complete audit of surviving elements modified by Revit.";
|
|
13
|
+
public object ParametersSchema => new
|
|
14
|
+
{
|
|
15
|
+
type = "object", properties = new
|
|
16
|
+
{
|
|
17
|
+
element_ids = ModelEditInputs.IdsSchema,
|
|
18
|
+
preview = ModelEditInputs.PreviewSchema,
|
|
19
|
+
expected_deleted_ids = new { type = "array", minItems = 1, maxItems = 10000, uniqueItems = true, items = new { type = "integer", minimum = 1 }, description = "Exact full deleted_ids set from a previous preview; mismatches roll back." },
|
|
20
|
+
}, required = new[] { "element_ids" },
|
|
21
|
+
};
|
|
22
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
23
|
+
{
|
|
24
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
25
|
+
var ids = ModelEditInputs.Ids(args);
|
|
26
|
+
List<long>? expected = args.TryGetProperty("expected_deleted_ids", out _) ? JsonArgs.GetLongArray(args, "expected_deleted_ids") : null;
|
|
27
|
+
if (expected != null && (expected.Count is < 1 or > 10000 || expected.Any(x => x <= 0) || expected.Distinct().Count() != expected.Count))
|
|
28
|
+
throw new ArgumentException("expected_deleted_ids must contain 1–10,000 distinct positive IDs.");
|
|
29
|
+
return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(
|
|
30
|
+
new() { ["element_ids"] = ids.Select(x => x.Value).ToArray() }, () =>
|
|
31
|
+
{
|
|
32
|
+
foreach (var id in ids)
|
|
33
|
+
{
|
|
34
|
+
var element = doc.GetElement(id) ?? throw new ArgumentException($"Element {id.Value} not found.");
|
|
35
|
+
if (element.Pinned) throw new ArgumentException($"Element {id.Value} is pinned; no elements were unpinned.");
|
|
36
|
+
}
|
|
37
|
+
var deleted = doc.Delete(ids).Select(id => id.Value).OrderBy(id => id).ToArray();
|
|
38
|
+
if (deleted.Length > 10000) throw new ArgumentException("Deletion exceeds the 10,000-ID cascade limit.");
|
|
39
|
+
if (expected != null && !deleted.ToHashSet().SetEquals(expected)) throw new ArgumentException("The deletion set changed since the preview; deletion was rolled back. Run a fresh preview.");
|
|
40
|
+
return new() { ["deleted_ids"] = deleted, ["deleted_count"] = deleted.Length,
|
|
41
|
+
["dependent_ids"] = deleted.Except(ids.Select(x => x.Value)).ToArray() };
|
|
42
|
+
}) }).Payload;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -29,9 +29,9 @@ internal static class DocumentGuard
|
|
|
29
29
|
=> toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
|
|
30
30
|
|
|
31
31
|
/// <summary>Must run on the Revit API thread immediately before the tool action.</summary>
|
|
32
|
-
public static void CheckForTool(JsonElement args, Document document, string toolName)
|
|
32
|
+
public static void CheckForTool(JsonElement args, Document document, string toolName, bool writes = false)
|
|
33
33
|
{
|
|
34
|
-
bool required = AlwaysRequiresIdentity(toolName)
|
|
34
|
+
bool required = writes || AlwaysRequiresIdentity(toolName)
|
|
35
35
|
|| (toolName == "manage_selection"
|
|
36
36
|
&& (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
|
|
37
37
|
|| JsonArgs.GetBool(args, "isolate_in_view", false)));
|