pi-revit 0.2.18 → 0.3.1
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 +42 -3
- package/README.md +130 -35
- package/bin/pi-revit.js +10 -10
- package/extensions/pi-revit/index.ts +89 -11
- package/package.json +4 -2
- 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 +17 -7
- package/src/Revit/BridgeServer.cs +7 -7
- package/src/Revit/ToolRegistry.cs +30 -5
- package/src/Revit/Tools/DocumentGuard.cs +64 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +16 -16
- package/src/Revit/Tools/ExportDocuments.cs +104 -27
- package/src/Revit/Tools/FailureGuard.cs +21 -0
- package/src/Revit/Tools/GetElementDetails.cs +6 -4
- package/src/Revit/Tools/GetElements.cs +10 -20
- package/src/Revit/Tools/GetModelOverview.cs +1 -0
- package/src/Revit/Tools/ManageSelection.cs +26 -17
- package/src/Revit/Tools/SetParameters.cs +10 -8
- package/src/Revit/Tools/ToolSupport.cs +0 -39
- package/workspace/AGENTS.md +40 -9
package/skills/pi-revit/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-revit
|
|
3
|
-
description: Work with the open Autodesk Revit model through the Revit bridge tools (ping, get_model_overview, get_elements, get_element_details, get_element_types, manage_selection, open_view, set_parameters, search_api_docs, execute_csharp, capture_view, export_documents, get_model_health). Use when the user asks about the Revit project, its elements, parameters, selection, or wants to change, script, capture, or export the model.
|
|
3
|
+
description: Work with the open Autodesk Revit model through the Revit bridge tools (ping, get_model_overview, get_elements, get_element_details, get_element_types, manage_selection, open_view, set_parameters, search_api_docs, execute_csharp, capture_view, export_documents, get_model_health) and retrieve saved results with read_revit_result. Use when the user asks about the Revit project, its elements, parameters, selection, or wants to change, script, capture, or export the model.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Revit
|
|
7
7
|
|
|
8
|
-
Work with the live Revit model. The
|
|
8
|
+
Work with the live Revit model. The bridge targets Revit 2025, 2026, and 2027; the 0.3.0 changes were tested live on Revit 2025. Bridge document tools require Revit running with a project open; `ping` and `search_api_docs` work without a document. `read_revit_result` reads an already saved result locally without contacting Revit.
|
|
9
9
|
|
|
10
10
|
## Tool selection
|
|
11
11
|
|
|
@@ -24,30 +24,40 @@ Work with the live Revit model. The tools call a headless bridge add-in inside R
|
|
|
24
24
|
| PNG snapshot of a view (visual QA) | `capture_view` (advanced) |
|
|
25
25
|
| PDF/DWG/PNG/IFC file export | `export_documents` (advanced) |
|
|
26
26
|
| Warnings / model quality audit | `get_model_health` (advanced) |
|
|
27
|
+
| Continue a saved large tool result | `read_revit_result` (local, no Revit call) |
|
|
27
28
|
|
|
28
29
|
Workflow guidance:
|
|
29
30
|
|
|
30
|
-
- Call `get_model_overview`
|
|
31
|
+
- Call `get_model_overview` for the intended open model before changing it. Copy `project.documentId` unchanged into `expected_document_id` for `set_parameters`, `execute_csharp`, `export_documents`, `open_view`, and selection/zoom changes. `manage_selection` also requires it whenever `isolate_in_view: true`, even with action `get`. Pure reads may omit it; a supplied ID is always checked. A matching legacy `expected_document` title alone is insufficient.
|
|
32
|
+
- The ID belongs to one open document in one bridge session. Refresh it after closing/reopening the model or restarting Revit. If the guard rejects a call, activate the intended model and obtain its overview again; do not blindly substitute the currently active model's ID.
|
|
31
33
|
- `get_elements` is the listing/counting primitive (`count_only: true` for bare counts). It returns identity fields only (id, name, category, typeName, levelId); read parameter values with `get_element_details`. Prefer a `category` or `of_class` scope when filtering by a parameter's display name.
|
|
32
34
|
- The selection pipeline is `get_elements` -> ids -> `manage_selection` (action `set`); there is no inline filter on selection.
|
|
33
|
-
- `set_parameters`
|
|
35
|
+
- `set_parameters` handles bulk parameter writes and renames (the `Name` parameter covers levels, views, sheets, types). It can commit a partially successful batch: inspect every failed update, `commitWarnings`, and the reported transaction outcome before describing what changed.
|
|
34
36
|
- Parameter display names are LOCALIZED: in a non-English Revit UI, `Mark`, `Comments`, and every other display name appear under their translated names. When a display-name lookup or `parameter_names` filter finds nothing, or the document may be non-English, use the language-independent `BuiltInParameter` enum name instead (e.g. `ALL_MODEL_MARK` for Mark, `ALL_MODEL_INSTANCE_COMMENTS` for Comments) — `set_parameters`, `get_element_details.parameter_names`, and `get_elements` filter rules all accept them, and `get_element_details` reports each parameter's `builtInParameter` name for discovery.
|
|
35
37
|
- Before writing `execute_csharp` code, verify unfamiliar classes/members with `search_api_docs` (works with no document open; first query builds the index and takes a few seconds). The top match carries its remarks, parameter docs, and returns inline, and every public API enum value is searchable — trust the result over guessing or web search; narrow the query to promote a different match into the top slot.
|
|
36
|
-
- `export_documents`
|
|
38
|
+
- `export_documents` defaults to `Documents\pi-revit\Models\<model title>--<identity hash>\exports`. Use its returned `outputDir` and file paths as authoritative; do not construct a destination from the title or opaque `project.documentId`. Saved paths and cloud/server identities determine the folder; unsaved/unavailable identities use a session fallback. Save As can select a new folder. Pass `output_dir` when the user requests a different destination. Existing title-only folders remain untouched.
|
|
39
|
+
- Large tool payloads return a `result_id`, `file_path`, and continuation instructions. Call `read_revit_result` with that ID and `offset: 0`, then follow `next_offset` until `has_more` is false. Concatenate `text` fragments in order; each fragment is not a standalone JSON result. Offsets count UTF-16 code units. IDs last for the current extension instance; after reload, use the returned absolute file path with `read` while the file exists. Retrieval does not replace the original query's pagination or remove its limits.
|
|
37
40
|
|
|
38
41
|
## execute_csharp playbook
|
|
39
42
|
|
|
40
43
|
- Globals: `doc` (Document), `uidoc` (UIDocument), `uiapp` (UIApplication), and `Dump(value)` to record intermediates into the result's `dumps[]`.
|
|
41
|
-
- The
|
|
44
|
+
- The script runs inside one backend-owned transaction. Do not start another transaction on `doc` (sub-transactions are allowed). The tool checks commit status and attempts rollback on script failure; read its actual outcome instead of assuming rollback succeeded. Result projection can fail after a successful commit and report `returnValueError`. Filesystem and UI effects are separate from model rollback.
|
|
42
45
|
- Scripts must be fully synchronous: `await`/`async` is rejected at compile time; never block on `Task.Result`/`.Wait()`.
|
|
43
46
|
- Return primitives, strings, or anonymous objects/lists; raw Revit API objects are projected to compact shapes (Element -> `{id,name,category,typeName,levelId}`, ElementId -> number, XYZ -> `{x,y,z}`).
|
|
44
47
|
- Lengths are internal units (decimal feet) — convert with `UnitUtils.ConvertToInternalUnits`/`ConvertFromInternalUnits`.
|
|
45
|
-
- Common pitfalls: call `FamilySymbol.Activate()` before `NewFamilyInstance`; use collector-level filtering (`OfCategory`/`OfClass`/`WhereElementIsNotElementType`) and bounded loops
|
|
48
|
+
- Common pitfalls: call `FamilySymbol.Activate()` before `NewFamilyInstance`; use collector-level filtering (`OfCategory`/`OfClass`/`WhereElementIsNotElementType`) and bounded loops. The budget is 120s and Revit cannot be interrupted mid-script. The dialog guard attempts dismissive responses and reports `suppressedDialogs`; it cannot guarantee handling every modal dialog.
|
|
46
49
|
- `capture_view` returns a `filePath` to a temp PNG, never image data — open it with the read tool to actually see it.
|
|
47
50
|
|
|
48
51
|
## Failure modes
|
|
49
52
|
|
|
53
|
+
- **Identity rejected**: no tool action was performed. Verify the intended active model, refresh `project.documentId`, and pass `expected_document_id` unchanged.
|
|
54
|
+
- **Partial UI/export effects**: selection or zoom can already have changed when isolation fails. Export errors can leave incomplete files, and IFC commit warnings are returned. Inspect the reported effects and output paths; model rollback does not remove files or reverse earlier UI actions.
|
|
55
|
+
- **Large-result save failed**: Revit may already have completed the operation. Verify its effects before retrying a write.
|
|
50
56
|
- **Bridge not reachable** ("Revit bridge is not available" / "Could not reach the Revit bridge"): Revit is not running or the add-in did not load. Ask the user to start Revit, then retry `ping`.
|
|
51
57
|
- **HTTP 409 / "No active Revit document is open."** (`hasActiveDocument: false`): Revit is running but no project is open. Ask the user to open a project, then retry. This fails immediately; do not wait or retry blindly.
|
|
52
58
|
- **Timeout** ("Revit did not answer within Ns", 30s default / 120s for execute_csharp, capture_view, export_documents): Revit is busy or showing a modal dialog. An already-started tool still runs to completion in Revit — verify model state (e.g. `get_elements`) before re-issuing a write.
|
|
53
59
|
- **Cancelled**: same caveat — the bridge cannot abort queued or running work, so verify model state before retrying a write tool.
|
|
60
|
+
|
|
61
|
+
## Upgrading from 0.2.x
|
|
62
|
+
|
|
63
|
+
Version 0.3.0 requires exact document IDs for the operations above. Update the Pi package and deploy the matching add-in with Revit closed, then restart Revit and start a fresh Pi session so the new schemas and instructions are loaded. Obtain a fresh overview before writes. `ping` reports an installed/loaded version mismatch. Setup preserves existing workspace `AGENTS.md`; merge these targeting, result-reading, and folder rules into an older workspace's instructions when needed.
|
|
@@ -407,6 +407,7 @@ namespace RevitBridge
|
|
|
407
407
|
? await _queue.RunAsync(uiApp =>
|
|
408
408
|
{
|
|
409
409
|
var document = uiApp.ActiveUIDocument?.Document ?? throw new NoActiveDocumentException();
|
|
410
|
+
RevitBridge.Tools.DocumentGuard.CheckForTool(args, document, tool.Name);
|
|
410
411
|
return tool.Execute(args, new ToolContext(document, uiApp));
|
|
411
412
|
}, TimeSpan.FromMilliseconds(timeoutMs))
|
|
412
413
|
// RequiresDocument = false tools never touch the Revit API, so they
|
|
@@ -435,9 +436,9 @@ namespace RevitBridge
|
|
|
435
436
|
? Math.Clamp(value, 1_000, 600_000)
|
|
436
437
|
: 30_000;
|
|
437
438
|
|
|
438
|
-
/// <summary>
|
|
439
|
-
///
|
|
440
|
-
///
|
|
439
|
+
/// <summary>Complete bounded JSON for model context; the full payload always
|
|
440
|
+
/// remains in details for the extension's saved-result retrieval. ToolOutput
|
|
441
|
+
/// compact text is a display summary, not a substitute for requested data.</summary>
|
|
441
442
|
private static object BuildToolResponse(string toolName, object? output)
|
|
442
443
|
{
|
|
443
444
|
object? payload = output;
|
|
@@ -448,12 +449,11 @@ namespace RevitBridge
|
|
|
448
449
|
compact = toolOutput.CompactText;
|
|
449
450
|
}
|
|
450
451
|
|
|
451
|
-
string text =
|
|
452
|
+
string text = JsonSerializer.Serialize(payload);
|
|
452
453
|
bool truncated = text.Length > MaxContentChars;
|
|
453
454
|
if (truncated)
|
|
454
455
|
{
|
|
455
|
-
|
|
456
|
-
text = text[..Math.Max(0, MaxContentChars - suffix.Length)] + suffix;
|
|
456
|
+
text = $"Result exceeds the {MaxContentChars}-character inline limit. The complete value is in details.payload; the Pi extension saves it locally and provides read_revit_result for bounded retrieval.";
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
return new
|
|
@@ -461,7 +461,7 @@ namespace RevitBridge
|
|
|
461
461
|
success = true,
|
|
462
462
|
toolName,
|
|
463
463
|
content = new[] { new { type = "text", text } },
|
|
464
|
-
details = new { payload, contentTruncated = truncated },
|
|
464
|
+
details = new { payload, summary = compact, contentTruncated = truncated },
|
|
465
465
|
isError = false,
|
|
466
466
|
};
|
|
467
467
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Nodes;
|
|
2
3
|
using Autodesk.Revit.DB;
|
|
3
4
|
using Autodesk.Revit.UI;
|
|
4
5
|
using RevitBridge.Tools;
|
|
@@ -14,9 +15,9 @@ namespace RevitBridge
|
|
|
14
15
|
internal sealed record ToolContext(Document? Document, UIApplication? UIApplication);
|
|
15
16
|
|
|
16
17
|
/// <summary>
|
|
17
|
-
/// Optional tool return shape:
|
|
18
|
-
///
|
|
19
|
-
///
|
|
18
|
+
/// Optional tool return shape: complete structured payload plus a display summary.
|
|
19
|
+
/// The bridge/extension expose complete bounded data or explicit result retrieval to
|
|
20
|
+
/// the model; structured details remain available for rendering and diagnostics.
|
|
20
21
|
/// </summary>
|
|
21
22
|
internal sealed record ToolOutput(object? Payload, string? CompactText = null);
|
|
22
23
|
|
|
@@ -99,12 +100,36 @@ namespace RevitBridge
|
|
|
99
100
|
description = tool.Description,
|
|
100
101
|
category = tool.Write ? "write" : "read",
|
|
101
102
|
tier = tool.Tier,
|
|
102
|
-
parameters = tool
|
|
103
|
+
parameters = DescribeParameters(tool),
|
|
103
104
|
executionMode = "sequential",
|
|
104
105
|
write = tool.Write,
|
|
105
106
|
requiresDocument = tool.RequiresDocument,
|
|
106
107
|
promptSnippet = tool.PromptSnippet,
|
|
107
|
-
promptGuidelines = tool.
|
|
108
|
+
promptGuidelines = tool.RequiresDocument
|
|
109
|
+
? (tool.PromptGuidelines ?? Array.Empty<string>()).Concat(new[]
|
|
110
|
+
{
|
|
111
|
+
$"{tool.Name}: use project.documentId from get_model_overview as expected_document_id to bind the call to that exact open document. It is required for model writes, open_view, and selection changes; legacy expected_document titles alone are insufficient. Refresh after closing/reopening or restarting Revit."
|
|
112
|
+
}).ToArray()
|
|
113
|
+
: tool.PromptGuidelines,
|
|
108
114
|
};
|
|
115
|
+
|
|
116
|
+
private static object DescribeParameters(ITool tool)
|
|
117
|
+
{
|
|
118
|
+
if (!tool.RequiresDocument) return tool.ParametersSchema;
|
|
119
|
+
var schema = JsonSerializer.SerializeToNode(tool.ParametersSchema)!.AsObject();
|
|
120
|
+
var properties = schema["properties"]!.AsObject();
|
|
121
|
+
properties["expected_document_id"] = new JsonObject
|
|
122
|
+
{
|
|
123
|
+
["type"] = "string",
|
|
124
|
+
["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
|
+
};
|
|
126
|
+
if (DocumentGuard.AlwaysRequiresIdentity(tool.Name))
|
|
127
|
+
{
|
|
128
|
+
var required = schema["required"] as JsonArray ?? new JsonArray();
|
|
129
|
+
if (!required.Any(x => x?.GetValue<string>() == "expected_document_id")) required.Add("expected_document_id");
|
|
130
|
+
schema["required"] = required;
|
|
131
|
+
}
|
|
132
|
+
return schema;
|
|
133
|
+
}
|
|
109
134
|
}
|
|
110
135
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
/// <summary>Opaque identity of one open native document in this loaded bridge session.</summary>
|
|
7
|
+
internal static class DocumentGuard
|
|
8
|
+
{
|
|
9
|
+
private sealed record Identity(Document Document, string Value);
|
|
10
|
+
private static readonly string Generation = Guid.NewGuid().ToString("N");
|
|
11
|
+
private static readonly List<Identity> Identities = new();
|
|
12
|
+
|
|
13
|
+
public static string GetIdentity(Document document)
|
|
14
|
+
{
|
|
15
|
+
if (!document.IsValidObject)
|
|
16
|
+
throw new ArgumentException("The target document is closed or invalid. Read get_model_overview again.");
|
|
17
|
+
// Revit can supply multiple managed wrappers for the same native document.
|
|
18
|
+
// Its documented Equals semantics identify that native document; reference identity cannot.
|
|
19
|
+
// All accesses happen on the Revit API thread. Pruning avoids retaining closed documents.
|
|
20
|
+
Identities.RemoveAll(entry => !entry.Document.IsValidObject);
|
|
21
|
+
var existing = Identities.FirstOrDefault(entry => entry.Document.Equals(document));
|
|
22
|
+
if (existing != null) return existing.Value;
|
|
23
|
+
var created = new Identity(document, Generation + ":" + Guid.NewGuid().ToString("N"));
|
|
24
|
+
Identities.Add(created);
|
|
25
|
+
return created.Value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public static bool AlwaysRequiresIdentity(string toolName)
|
|
29
|
+
=> toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
|
|
30
|
+
|
|
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)
|
|
33
|
+
{
|
|
34
|
+
bool required = AlwaysRequiresIdentity(toolName)
|
|
35
|
+
|| (toolName == "manage_selection"
|
|
36
|
+
&& (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
|
|
37
|
+
|| JsonArgs.GetBool(args, "isolate_in_view", false)));
|
|
38
|
+
if (required && !args.TryGetProperty("expected_document_id", out _))
|
|
39
|
+
throw new ArgumentException("expected_document_id is required for this operation. Read project.documentId from get_model_overview for the intended open document and pass it unchanged. A title alone cannot identify a document. No action was performed.");
|
|
40
|
+
CheckExpectedDocument(args, document);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static void CheckExpectedDocument(JsonElement args, Document document)
|
|
44
|
+
{
|
|
45
|
+
if (args.TryGetProperty("expected_document_id", out var id))
|
|
46
|
+
{
|
|
47
|
+
if (id.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(id.GetString()))
|
|
48
|
+
throw new ArgumentException("expected_document_id must be a non-empty identity from get_model_overview. No action was performed.");
|
|
49
|
+
if (!string.Equals(id.GetString(), GetIdentity(document), StringComparison.Ordinal))
|
|
50
|
+
throw new ArgumentException($"The active document '{document.Title}' is not the exact open document this call expected, or that identity is stale after reopening/restarting. No action was performed. Activate the intended document and read get_model_overview again.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Retained as an additional human-readable check, never as a substitute for identity.
|
|
54
|
+
string? expected = JsonArgs.GetString(args, "expected_document");
|
|
55
|
+
if (string.IsNullOrWhiteSpace(expected)) return;
|
|
56
|
+
static string Normalize(string title)
|
|
57
|
+
{
|
|
58
|
+
string value = title.Trim();
|
|
59
|
+
return value.EndsWith(".rvt", StringComparison.OrdinalIgnoreCase) ? value[..^4] : value;
|
|
60
|
+
}
|
|
61
|
+
if (!string.Equals(Normalize(expected), Normalize(document.Title), StringComparison.OrdinalIgnoreCase))
|
|
62
|
+
throw new ArgumentException($"Active document is '{document.Title}' but this call expected title '{expected}'. No action was performed. Read get_model_overview for the intended document.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -59,7 +59,7 @@ namespace RevitBridge.Tools
|
|
|
59
59
|
|
|
60
60
|
public string Name => "execute_csharp";
|
|
61
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
|
|
62
|
+
public string Description => "Compile and run a C# script on the Revit API thread against the open model — the escape hatch for anything without a dedicated tool: element creation, deletion, geometry edits (move/copy/rotate), views, sheets, schedules, tagging, families, links, worksets. Globals: doc (Document), uidoc (UIDocument), uiapp (UIApplication), and Dump(value) to record intermediate values into the result's dumps[]. Default imports: System, System.Linq, System.Collections.Generic, Autodesk.Revit.DB, Autodesk.Revit.UI — add using directives at the top for sub-namespaces (e.g. using Autodesk.Revit.DB.Architecture;). The entire run is wrapped in ONE transaction named 'execute_csharp': committed on success; on failure rollback is attempted and its confirmed status is reported (do not open your own Transaction; sub-transactions are fine). The script's final expression or return statement becomes returnValue; return primitives, strings, or anonymous objects/lists — Revit API values are projected to safe shapes (Element -> {id,name,category,typeName,levelId}, ElementId -> number, XYZ -> {x,y,z}, Parameter -> {name,value,displayValue}; other API objects become strings) with depth and item caps, so never rely on raw API objects round-tripping. Lengths are in internal units (decimal feet) — convert with UnitUtils. Prefer collector-level filtering (FilteredElementCollector .OfCategory/.OfClass/.WhereElementIsNotElementType) and bounded loops: the call budget is 120s and Revit cannot be interrupted mid-script. Scripts must be fully synchronous — await/async is rejected at compile time, and blocking on tasks (Task.Result/.Wait()) can freeze Revit. Modal dialogs raised while running are auto-dismissed and reported in suppressedDialogs — unrecognized dialogs are answered dismissively (Cancel/Close/No) rather than confirmed, so an operation that raises a confirmation prompt may be cancelled; check suppressedDialogs when a result looks incomplete. Verify unfamiliar signatures with search_api_docs first.";
|
|
63
63
|
public bool Write => true;
|
|
64
64
|
|
|
65
65
|
public object ParametersSchema => new
|
|
@@ -119,9 +119,9 @@ namespace RevitBridge.Tools
|
|
|
119
119
|
|
|
120
120
|
using var dialogGuard = new DialogGuard(uiapp);
|
|
121
121
|
using var transaction = new Transaction(doc, "execute_csharp");
|
|
122
|
-
var failureGuard = FailureGuard.Attach(transaction);
|
|
123
122
|
if (transaction.Start() != TransactionStatus.Started)
|
|
124
123
|
throw new InvalidOperationException("Unable to start the execute_csharp transaction.");
|
|
124
|
+
var failureGuard = FailureGuard.Attach(transaction);
|
|
125
125
|
|
|
126
126
|
object? returnValue;
|
|
127
127
|
string? projectionError = null;
|
|
@@ -147,23 +147,24 @@ namespace RevitBridge.Tools
|
|
|
147
147
|
}
|
|
148
148
|
catch (Exception ex)
|
|
149
149
|
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
153
|
-
transaction.RollBack();
|
|
154
|
-
}
|
|
155
|
-
catch
|
|
156
|
-
{
|
|
157
|
-
// Reporting the script failure outranks a rollback hiccup.
|
|
158
|
-
}
|
|
159
|
-
throw new InvalidOperationException(FormatRuntimeError(ex, dialogGuard.Suppressed));
|
|
150
|
+
throw new InvalidOperationException(
|
|
151
|
+
FormatRuntimeError(ex, dialogGuard.Suppressed) + " " + FailureGuard.RollBackAndDescribe(transaction), ex);
|
|
160
152
|
}
|
|
161
153
|
|
|
162
|
-
|
|
154
|
+
try
|
|
155
|
+
{
|
|
156
|
+
var status = transaction.Commit();
|
|
157
|
+
var finalStatus = transaction.GetStatus();
|
|
158
|
+
if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
|
|
159
|
+
throw new InvalidOperationException($"The execute_csharp commit returned {status}; current transaction status is {finalStatus}.");
|
|
160
|
+
}
|
|
161
|
+
catch (Exception ex)
|
|
162
|
+
{
|
|
163
163
|
throw new InvalidOperationException(
|
|
164
|
-
"
|
|
164
|
+
$"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}"
|
|
165
165
|
+ failureGuard.DescribeErrors()
|
|
166
|
-
+ DescribeDialogs(dialogGuard.Suppressed));
|
|
166
|
+
+ DescribeDialogs(dialogGuard.Suppressed), ex);
|
|
167
|
+
}
|
|
167
168
|
|
|
168
169
|
stopwatch.Stop();
|
|
169
170
|
|
|
@@ -240,7 +241,6 @@ namespace RevitBridge.Tools
|
|
|
240
241
|
string line = TryGetScriptLine(ex) is { } scriptLine ? $" at script line {scriptLine}" : string.Empty;
|
|
241
242
|
string inner = ex.InnerException is { } innerEx ? $" Inner: {innerEx.GetType().Name}: {innerEx.Message}" : string.Empty;
|
|
242
243
|
return $"C# script threw {ex.GetType().Name}{line}: {ex.Message}.{inner}"
|
|
243
|
-
+ " The execute_csharp transaction was rolled back; no model changes were saved."
|
|
244
244
|
+ DescribeDialogs(suppressedDialogs);
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
using System.IO;
|
|
2
|
+
using System.Security.Cryptography;
|
|
3
|
+
using System.Text;
|
|
2
4
|
using System.Text.Json;
|
|
3
5
|
using Autodesk.Revit.DB;
|
|
4
6
|
|
|
@@ -19,7 +21,7 @@ namespace RevitBridge.Tools
|
|
|
19
21
|
|
|
20
22
|
public string Name => "export_documents";
|
|
21
23
|
public string Label => "Export Documents";
|
|
22
|
-
public string Description => "Export documents from the open Revit model. format 'pdf'/'dwg'/'png' export the given sheet/view ids to files: pdf combines everything into one file by default (combine=false writes one PDF per sheet/view, named by Revit's naming rule); png renders 2048 px wide; ifc exports the whole model, or just what one given view shows. Files sort themselves per model: with output_dir omitted they land in Documents\\pi-revit\\Models\\<model title>\\exports, derived from the document being exported (pass output_dir only for a different explicit target); file_name_prefix sets the base file name (default: the document title; Revit appends view/sheet suffixes for multi-file exports). Returns the produced file paths with sizes. Find sheet/view ids with get_elements (category 'Sheets' or 'Views') first.";
|
|
24
|
+
public string Description => "Export documents from the open Revit model. format 'pdf'/'dwg'/'png' export the given sheet/view ids to files: pdf combines everything into one file by default (combine=false writes one PDF per sheet/view, named by Revit's naming rule); png renders 2048 px wide; ifc exports the whole model, or just what one given view shows. Files sort themselves per model: with output_dir omitted they land in Documents\\pi-revit\\Models\\<model title>--<identity hash>\\exports, derived from the document being exported (pass output_dir only for a different explicit target); file_name_prefix sets the base file name (default: the document title; Revit appends view/sheet suffixes for multi-file exports). Returns the produced file paths with sizes. Find sheet/view ids with get_elements (category 'Sheets' or 'Views') first.";
|
|
23
25
|
public bool Write => true;
|
|
24
26
|
public string Tier => "advanced";
|
|
25
27
|
|
|
@@ -43,7 +45,7 @@ namespace RevitBridge.Tools
|
|
|
43
45
|
output_dir = new
|
|
44
46
|
{
|
|
45
47
|
type = "string",
|
|
46
|
-
description = "Output directory, created if missing. Default: Documents\\pi-revit\\Models\\<model title>\\exports — files sort under the exported model automatically; omit unless the user names a different target.",
|
|
48
|
+
description = "Output directory, created if missing. Default: Documents\\pi-revit\\Models\\<model title>--<identity hash>\\exports — files sort under the exported model automatically; omit unless the user names a different target.",
|
|
47
49
|
},
|
|
48
50
|
file_name_prefix = new
|
|
49
51
|
{
|
|
@@ -86,17 +88,37 @@ namespace RevitBridge.Tools
|
|
|
86
88
|
// recent (a shared output_dir, another tool writing alongside) does not.
|
|
87
89
|
var before = SnapshotWriteTimes(outputDir);
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
IReadOnlyList<string> commitWarnings = Array.Empty<string>();
|
|
92
|
+
try
|
|
90
93
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
switch (format)
|
|
95
|
+
{
|
|
96
|
+
case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
|
|
97
|
+
case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
|
|
98
|
+
case "png": ExportPng(doc, views, outputDir, baseName); break;
|
|
99
|
+
default: commitWarnings = ExportIfc(doc, views, outputDir, baseName); break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (Exception ex)
|
|
103
|
+
{
|
|
104
|
+
// A failed export may already have written some files. A Revit
|
|
105
|
+
// transaction rollback cannot undo those filesystem changes.
|
|
106
|
+
string observed;
|
|
107
|
+
try
|
|
108
|
+
{
|
|
109
|
+
var changed = FindChangedFiles(outputDir, before).ToList();
|
|
110
|
+
observed = changed.Count == 0
|
|
111
|
+
? "No changed files were observed."
|
|
112
|
+
: $"Observed {changed.Count} new or changed file(s): {string.Join(", ", changed.Take(20))}{(changed.Count > 20 ? " (additional files omitted)" : string.Empty)}.";
|
|
113
|
+
}
|
|
114
|
+
catch (Exception scanError)
|
|
115
|
+
{
|
|
116
|
+
observed = $"Could not inspect remaining files: {scanError.Message}.";
|
|
117
|
+
}
|
|
118
|
+
throw new InvalidOperationException($"{ex.Message} The export failed; files in '{outputDir}' may be incomplete and were not removed. {observed}", ex);
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
var files =
|
|
98
|
-
.Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
|
|
99
|
-
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
121
|
+
var files = FindChangedFiles(outputDir, before)
|
|
100
122
|
.Select(path => new Dictionary<string, object?>
|
|
101
123
|
{
|
|
102
124
|
["path"] = path,
|
|
@@ -108,15 +130,23 @@ namespace RevitBridge.Tools
|
|
|
108
130
|
|
|
109
131
|
string sample = string.Join(", ", files.Take(3).Select(file => Path.GetFileName((string)file["path"]!)));
|
|
110
132
|
string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
|
|
133
|
+
if (commitWarnings.Count > 0)
|
|
134
|
+
compact += $" {commitWarnings.Count} Revit warning(s) auto-dismissed (see commitWarnings).";
|
|
111
135
|
return new ToolOutput(new
|
|
112
136
|
{
|
|
113
137
|
format,
|
|
114
138
|
outputDir,
|
|
115
139
|
fileCount = files.Count,
|
|
116
140
|
files,
|
|
141
|
+
commitWarnings,
|
|
117
142
|
}, compact);
|
|
118
143
|
}
|
|
119
144
|
|
|
145
|
+
private static IEnumerable<string> FindChangedFiles(string directory, IReadOnlyDictionary<string, DateTime> before)
|
|
146
|
+
=> Directory.GetFiles(directory)
|
|
147
|
+
.Where(path => !before.TryGetValue(path, out DateTime writtenBefore) || SafeWriteTime(path) != writtenBefore)
|
|
148
|
+
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
|
|
149
|
+
|
|
120
150
|
/// <summary>path -> last write time for the files already in the output directory.
|
|
121
151
|
/// A file whose time cannot be read is left out, so the export reports it if it
|
|
122
152
|
/// shows up afterwards rather than silently swallowing a produced file.</summary>
|
|
@@ -197,7 +227,7 @@ namespace RevitBridge.Tools
|
|
|
197
227
|
/// <summary>The IFC exporter writes IFC GUID parameters onto exported elements,
|
|
198
228
|
/// so the Revit API requires a transaction around it — owned here, matching
|
|
199
229
|
/// Revit's own behavior of committing those GUIDs on export.</summary>
|
|
200
|
-
private static
|
|
230
|
+
private static IReadOnlyList<string> ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
|
|
201
231
|
{
|
|
202
232
|
var options = new IFCExportOptions();
|
|
203
233
|
if (views.Count == 1)
|
|
@@ -206,18 +236,20 @@ namespace RevitBridge.Tools
|
|
|
206
236
|
using var transaction = new Transaction(doc, "export_documents: ifc");
|
|
207
237
|
if (transaction.Start() != TransactionStatus.Started)
|
|
208
238
|
throw new InvalidOperationException("Unable to start the IFC export transaction.");
|
|
239
|
+
var failureGuard = FailureGuard.Attach(transaction);
|
|
209
240
|
try
|
|
210
241
|
{
|
|
211
242
|
if (!Run(() => doc.Export(outputDir, baseName, options), "IFC"))
|
|
212
243
|
throw new InvalidOperationException("Revit reported a failed IFC export.");
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
var status = transaction.Commit();
|
|
245
|
+
var finalStatus = transaction.GetStatus();
|
|
246
|
+
if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
|
|
247
|
+
throw new InvalidOperationException($"The IFC export commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
|
|
248
|
+
return failureGuard.Warnings;
|
|
215
249
|
}
|
|
216
|
-
catch
|
|
250
|
+
catch (Exception ex)
|
|
217
251
|
{
|
|
218
|
-
|
|
219
|
-
transaction.RollBack();
|
|
220
|
-
throw;
|
|
252
|
+
throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
|
|
221
253
|
}
|
|
222
254
|
}
|
|
223
255
|
|
|
@@ -278,27 +310,72 @@ namespace RevitBridge.Tools
|
|
|
278
310
|
}
|
|
279
311
|
|
|
280
312
|
/// <summary>Per-model folder under the pi-revit workspace:
|
|
281
|
-
/// Documents\pi-revit\Models\<model title>.
|
|
282
|
-
///
|
|
283
|
-
///
|
|
284
|
-
/// the model's identity so same-titled models stay distinguishable.</summary>
|
|
313
|
+
/// Documents\pi-revit\Models\<model title>--<identity hash>.
|
|
314
|
+
/// Existing title-only folders are left untouched; identity markers are
|
|
315
|
+
/// diagnostic records, not the mechanism that separates exports.</summary>
|
|
285
316
|
private static string ModelFolder(Document doc)
|
|
286
317
|
{
|
|
287
318
|
string workspace = Path.Combine(
|
|
288
319
|
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "pi-revit");
|
|
289
|
-
string
|
|
290
|
-
string folder = Path.Combine(workspace, "Models",
|
|
320
|
+
string identity = GetModelIdentity(doc);
|
|
321
|
+
string folder = Path.Combine(workspace, "Models", GetModelFolderName(doc.Title, identity));
|
|
291
322
|
Directory.CreateDirectory(folder);
|
|
292
|
-
TryRecordModelIdentity(folder, doc);
|
|
323
|
+
TryRecordModelIdentity(folder, doc, identity);
|
|
293
324
|
return folder;
|
|
294
325
|
}
|
|
295
326
|
|
|
296
|
-
|
|
327
|
+
internal static string GetModelFolderName(string title, string identity)
|
|
328
|
+
{
|
|
329
|
+
string cleanTitle = SanitizeFileName(string.IsNullOrWhiteSpace(title) ? "untitled" : title);
|
|
330
|
+
// Bound the default path component, leaving room for the identity and exports.
|
|
331
|
+
if (cleanTitle.Length > 80)
|
|
332
|
+
cleanTitle = cleanTitle[..80];
|
|
333
|
+
string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant()[..24];
|
|
334
|
+
return $"{cleanTitle}--{hash}";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Revit can return different managed wrappers for one open native document.
|
|
338
|
+
// Document.Equals/GetHashCode identify that open document, unlike reference
|
|
339
|
+
// equality in ConditionalWeakTable. Closed document entries are pruned below.
|
|
340
|
+
// Tool execution and this dictionary are confined to the Revit API thread.
|
|
341
|
+
private static readonly Dictionary<Document, string> OpenDocumentIdentities = new();
|
|
342
|
+
|
|
343
|
+
internal static string GetModelIdentity(Document doc)
|
|
344
|
+
{
|
|
345
|
+
try
|
|
346
|
+
{
|
|
347
|
+
if (doc.IsModelInCloud)
|
|
348
|
+
{
|
|
349
|
+
var path = doc.GetCloudModelPath();
|
|
350
|
+
return $"cloud:{path.Region.ToUpperInvariant()}:{path.GetProjectGUID():D}:{path.GetModelGUID():D}";
|
|
351
|
+
}
|
|
352
|
+
string pathName = doc.PathName;
|
|
353
|
+
if (!string.IsNullOrWhiteSpace(pathName))
|
|
354
|
+
{
|
|
355
|
+
// Revit file paths are Windows paths; canonicalize separators,
|
|
356
|
+
// relative segments and case. Revit Server identities use RSN URLs.
|
|
357
|
+
if (pathName.StartsWith("RSN://", StringComparison.OrdinalIgnoreCase))
|
|
358
|
+
return "server:" + pathName.Replace('\\', '/').TrimEnd('/').ToUpperInvariant();
|
|
359
|
+
return "file:" + Path.GetFullPath(pathName).Replace('/', '\\').ToUpperInvariant();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch
|
|
363
|
+
{
|
|
364
|
+
// An unavailable identity must not collapse unrelated documents into
|
|
365
|
+
// a shared title-only directory. Use the same safe fallback as unsaved docs.
|
|
366
|
+
}
|
|
367
|
+
foreach (var closed in OpenDocumentIdentities.Keys.Where(key => !key.IsValidObject).ToList())
|
|
368
|
+
OpenDocumentIdentities.Remove(closed);
|
|
369
|
+
if (!OpenDocumentIdentities.TryGetValue(doc, out string? identity))
|
|
370
|
+
OpenDocumentIdentities[doc] = identity = $"session:{Guid.NewGuid():N}";
|
|
371
|
+
return identity;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private static void TryRecordModelIdentity(string folder, Document doc, string identity)
|
|
297
375
|
{
|
|
298
376
|
try
|
|
299
377
|
{
|
|
300
|
-
string
|
|
301
|
-
string line = $"{guid}\t{doc.PathName}";
|
|
378
|
+
string line = $"{identity}\t{doc.PathName}";
|
|
302
379
|
string marker = Path.Combine(folder, "model.txt");
|
|
303
380
|
if (!File.Exists(marker) || !File.ReadAllLines(marker).Contains(line))
|
|
304
381
|
File.AppendAllLines(marker, new[] { line });
|
|
@@ -18,6 +18,8 @@ namespace RevitBridge.Tools
|
|
|
18
18
|
|
|
19
19
|
public static FailureGuard Attach(Transaction transaction)
|
|
20
20
|
{
|
|
21
|
+
if (transaction.GetStatus() != TransactionStatus.Started)
|
|
22
|
+
throw new InvalidOperationException("FailureGuard must be attached after the transaction starts; Start resets failure handling options.");
|
|
21
23
|
var guard = new FailureGuard();
|
|
22
24
|
var options = transaction.GetFailureHandlingOptions();
|
|
23
25
|
options.SetFailuresPreprocessor(guard);
|
|
@@ -26,6 +28,25 @@ namespace RevitBridge.Tools
|
|
|
26
28
|
return guard;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/// <summary>Best-effort cleanup that preserves the original failure and never
|
|
32
|
+
/// claims a rollback unless Revit confirms its final transaction status.</summary>
|
|
33
|
+
public static string RollBackAndDescribe(Transaction transaction)
|
|
34
|
+
{
|
|
35
|
+
try
|
|
36
|
+
{
|
|
37
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
38
|
+
transaction.RollBack();
|
|
39
|
+
var status = transaction.GetStatus();
|
|
40
|
+
return status == TransactionStatus.RolledBack
|
|
41
|
+
? "The transaction was rolled back; no changes from this transaction were saved."
|
|
42
|
+
: $"Transaction status is {status}; rollback is not confirmed. Inspect Revit before retrying.";
|
|
43
|
+
}
|
|
44
|
+
catch (Exception ex)
|
|
45
|
+
{
|
|
46
|
+
return $"Rollback could not be confirmed ({ex.GetType().Name}: {ex.Message}). Inspect Revit before retrying.";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
public FailureProcessingResult PreprocessFailures(FailuresAccessor accessor)
|
|
30
51
|
{
|
|
31
52
|
bool hasError = false;
|
|
@@ -36,7 +36,7 @@ namespace RevitBridge.Tools
|
|
|
36
36
|
properties = new
|
|
37
37
|
{
|
|
38
38
|
parameters = new { type = "boolean", description = "Instance parameter values. Default true." },
|
|
39
|
-
type_parameters = new { type = "boolean", description = "
|
|
39
|
+
type_parameters = new { type = "boolean", description = "Include the element type's parameters, marked isType=true, independently of the instance parameters flag. Default false." },
|
|
40
40
|
location = new { type = "boolean", description = "Location point or curve (coordinates in internal feet). Default false." },
|
|
41
41
|
bounding_box = new { type = "boolean", description = "Model bounding box min/max (internal feet). Default false." },
|
|
42
42
|
materials = new { type = "boolean", description = "Material ids/names with area/volume (internal units). Default false." },
|
|
@@ -72,6 +72,7 @@ namespace RevitBridge.Tools
|
|
|
72
72
|
: default;
|
|
73
73
|
bool withParameters = JsonArgs.GetBool(include, "parameters", true);
|
|
74
74
|
bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
|
|
75
|
+
bool withAnyParameters = withParameters || withTypeParameters;
|
|
75
76
|
bool withLocation = JsonArgs.GetBool(include, "location", false);
|
|
76
77
|
bool withBoundingBox = JsonArgs.GetBool(include, "bounding_box", false);
|
|
77
78
|
bool withMaterials = JsonArgs.GetBool(include, "materials", false);
|
|
@@ -102,10 +103,11 @@ namespace RevitBridge.Tools
|
|
|
102
103
|
|
|
103
104
|
int parameterCount = 0;
|
|
104
105
|
int parameterTotal = 0;
|
|
105
|
-
if (
|
|
106
|
+
if (withAnyParameters)
|
|
106
107
|
{
|
|
107
108
|
var parameters = new List<Dictionary<string, object?>>();
|
|
108
|
-
|
|
109
|
+
if (withParameters)
|
|
110
|
+
parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
109
111
|
if (withTypeParameters && elementType != null)
|
|
110
112
|
parameterTotal += AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
111
113
|
dto["parameters"] = parameters;
|
|
@@ -122,7 +124,7 @@ namespace RevitBridge.Tools
|
|
|
122
124
|
elements.Add(dto);
|
|
123
125
|
// With a name filter active, "0 params" is ambiguous (none matched vs none
|
|
124
126
|
// exist): report matched-of-total so a localization miss is visible.
|
|
125
|
-
string paramSummary = !
|
|
127
|
+
string paramSummary = !withAnyParameters ? string.Empty
|
|
126
128
|
: nameFilter != null ? $", {parameterCount} of {parameterTotal} params matched parameter_names"
|
|
127
129
|
: $", {parameterCount} params";
|
|
128
130
|
compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{paramSummary})");
|