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.
@@ -21,7 +21,7 @@ namespace RevitBridge.Tools
21
21
 
22
22
  public string Name => "get_elements";
23
23
  public string Label => "Get Elements";
24
- public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields only (id, name, category, typeName, levelId) — read parameter values with get_element_details. Most filter rules evaluate inside Revit's collector; 'regex' rules (and rules on parameters that cannot be quick-filtered) run as a slower post-collector scan. Prefer combining display-name filter rules with a category or of_class scope: the same display name (e.g. 'Width') can resolve to different parameters per category, which forces the slower per-element scan in unscoped queries. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
24
+ public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields only (id, name, category, typeName, levelId) — read parameter values with get_element_details. Rules with an explicit BuiltInParameter or shared GUID can evaluate inside Revit's collector; display-name rules, regex rules, and unsupported quick filters run as a per-element scan. A display name can identify different parameters even within one category or class. Scope queries to reduce the scan, or use an explicit parameter identity when appropriate. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
25
25
 
26
26
  public object ParametersSchema => new
27
27
  {
@@ -292,8 +292,9 @@ namespace RevitBridge.Tools
292
292
  if (rules.Count == 0)
293
293
  return (null, null, Array.Empty<string>());
294
294
 
295
- // Probe a few in-scope elements so display-name parameters resolve to ids
296
- // and value typing / unit conversion can use the parameter's storage + spec.
295
+ // Probe a few in-scope elements for missing-name diagnostics and to find
296
+ // storage/spec exemplars for explicit parameter identities. A sample must
297
+ // never turn a display name into an assumed globally uniform identity.
297
298
  var probes = new List<Element>(ProbeSize);
298
299
  foreach (Element element in createBaseCollector())
299
300
  {
@@ -325,30 +326,19 @@ namespace RevitBridge.Tools
325
326
  }
326
327
  }
327
328
 
328
- // Display-name promotion is only trustworthy inside one category/class: the
329
- // probe sees just the first ProbeSize elements in collector order, so in an
330
- // unscoped query a unanimous sample can still hide other categories further
331
- // on whose same-named parameter has a different id — and a pinned quick rule
332
- // would silently drop their matches.
333
- bool scoped = !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "category"))
334
- || !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "of_class"));
335
-
336
329
  foreach (var rule in rules)
337
330
  {
338
331
  if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
339
332
  continue; // always post-scan (missing-parameter semantics)
333
+ // Category/class scope and unanimous samples do not establish a
334
+ // uniform parameter identity across later families. Resolve display
335
+ // names per element; only explicit identities may use a quick rule.
336
+ if (rule.BuiltIn is null && rule.SharedGuid is null)
337
+ continue;
340
338
  var found = probes.Select(probe => FindParameter(probe, rule)).Where(parameter => parameter != null).ToList();
341
339
  if (found.Count == 0)
342
340
  continue;
343
- // BuiltInParameter/guid rules address one global parameter id. A plain
344
- // display name can resolve to DIFFERENT ids per category or family
345
- // (e.g. 'Width' -> DOOR_WIDTH vs WINDOW_WIDTH). Promote a display-name
346
- // rule only when the query is scoped AND all probed elements agree on
347
- // the id; otherwise it stays on the (per-element, correct) post-scan path.
348
- bool oneGlobalId = rule.BuiltIn != null || rule.SharedGuid != null
349
- || (scoped && found.All(parameter => parameter!.Id == found[0]!.Id));
350
- if (oneGlobalId)
351
- rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
341
+ rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
352
342
  }
353
343
 
354
344
  var quickRules = rules.Where(rule => rule.QuickRule != null).ToList();
@@ -46,6 +46,7 @@ namespace RevitBridge.Tools
46
46
  var project = new Dictionary<string, object?>
47
47
  {
48
48
  ["title"] = doc.Title,
49
+ ["documentId"] = DocumentGuard.GetIdentity(doc),
49
50
  ["name"] = info?.Name,
50
51
  ["number"] = info?.Number,
51
52
  ["clientName"] = info?.ClientName,
@@ -138,9 +138,23 @@ namespace RevitBridge.Tools
138
138
 
139
139
  if (isolateInView)
140
140
  {
141
- var view = uiDocument.ActiveGraphicalView
142
- ?? throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
143
- ApplyTemporaryIsolate(doc, view, isolateTargets);
141
+ var view = uiDocument.ActiveGraphicalView;
142
+ IReadOnlyList<string> warnings;
143
+ try
144
+ {
145
+ if (view is null)
146
+ throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
147
+ warnings = ApplyTemporaryIsolate(doc, view, isolateTargets);
148
+ }
149
+ catch (Exception ex)
150
+ {
151
+ throw new InvalidOperationException($"Selection action '{action}' completed before temporary isolate failed: {ex.Message}. Selection/zoom changes were not rolled back.", ex);
152
+ }
153
+ if (warnings.Count > 0)
154
+ {
155
+ payload["commitWarnings"] = warnings;
156
+ compactParts.Add($"{warnings.Count} Revit warning(s) auto-dismissed (see commitWarnings)");
157
+ }
144
158
  payload["viewId"] = view.Id.Value;
145
159
  payload["viewName"] = view.Name;
146
160
  if (isolateTargets.Count == 0)
@@ -189,32 +203,27 @@ namespace RevitBridge.Tools
189
203
  /// only changes it inside an open transaction — so exactly this branch wraps
190
204
  /// a small one while the tool stays Write = false.
191
205
  /// </summary>
192
- private static void ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
206
+ private static IReadOnlyList<string> ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
193
207
  {
194
208
  using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
195
- var failureGuard = FailureGuard.Attach(transaction);
196
209
  if (transaction.Start() != TransactionStatus.Started)
197
210
  throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
211
+ var failureGuard = FailureGuard.Attach(transaction);
198
212
  try
199
213
  {
200
214
  if (elementIds.Count == 0)
201
215
  view.DisableTemporaryViewMode(TemporaryViewMode.TemporaryHideIsolate);
202
216
  else
203
217
  view.IsolateElementsTemporary(elementIds);
204
- if (transaction.Commit() != TransactionStatus.Committed)
205
- throw new InvalidOperationException("The temporary-isolate transaction failed to commit." + failureGuard.DescribeErrors());
206
- }
207
- catch (Autodesk.Revit.Exceptions.InvalidOperationException ex)
208
- {
209
- if (transaction.GetStatus() == TransactionStatus.Started)
210
- transaction.RollBack();
211
- throw new ArgumentException($"The active view '{view.Name}' does not support temporary isolate: {ex.Message}");
218
+ var status = transaction.Commit();
219
+ var finalStatus = transaction.GetStatus();
220
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
221
+ throw new InvalidOperationException($"The temporary-isolate commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
222
+ return failureGuard.Warnings;
212
223
  }
213
- catch
224
+ catch (Exception ex)
214
225
  {
215
- if (transaction.GetStatus() == TransactionStatus.Started)
216
- transaction.RollBack();
217
- throw;
226
+ throw new InvalidOperationException($"Temporary isolate in view '{view.Name}' failed: {ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
218
227
  }
219
228
  }
220
229
  }
@@ -70,9 +70,9 @@ namespace RevitBridge.Tools
70
70
  var failed = new List<Dictionary<string, object?>>();
71
71
 
72
72
  using var transaction = new Transaction(doc, "set_parameters");
73
- var failureGuard = FailureGuard.Attach(transaction);
74
73
  if (transaction.Start() != TransactionStatus.Started)
75
74
  throw new InvalidOperationException("Unable to start the set_parameters transaction.");
75
+ var failureGuard = FailureGuard.Attach(transaction);
76
76
 
77
77
  try
78
78
  {
@@ -101,21 +101,23 @@ namespace RevitBridge.Tools
101
101
 
102
102
  if (succeeded.Count > 0)
103
103
  {
104
- if (transaction.Commit() != TransactionStatus.Committed)
104
+ var status = transaction.Commit();
105
+ var finalStatus = transaction.GetStatus();
106
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
105
107
  throw new InvalidOperationException(
106
- "Revit rejected the set_parameters commit and the transaction was rolled back; no changes were saved."
108
+ $"The set_parameters commit returned {status}; current transaction status is {finalStatus}."
107
109
  + failureGuard.DescribeErrors());
108
110
  }
109
111
  else
110
112
  {
111
- transaction.RollBack();
113
+ var status = transaction.RollBack();
114
+ if (status != TransactionStatus.RolledBack || transaction.GetStatus() != TransactionStatus.RolledBack)
115
+ throw new InvalidOperationException($"The set_parameters rollback returned {status}.");
112
116
  }
113
117
  }
114
- catch
118
+ catch (Exception ex)
115
119
  {
116
- if (transaction.GetStatus() == TransactionStatus.Started)
117
- transaction.RollBack();
118
- throw;
120
+ throw new InvalidOperationException($"{ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
119
121
  }
120
122
 
121
123
  int elementCount = succeeded.Select(row => row["id"]).Distinct().Count();
@@ -259,43 +259,4 @@ namespace RevitBridge.Tools
259
259
  }
260
260
  }
261
261
 
262
- /// <summary>
263
- /// Optional wrong-document protection for write tools. A write queued while the
264
- /// user switches models would otherwise land in whichever document is active when
265
- /// the queued call runs -- possibly silently, since low element ids resolve in
266
- /// most documents. Tools pass the caller's optional expected_document through
267
- /// here before touching the model.
268
- /// </summary>
269
- internal static class DocumentGuard
270
- {
271
- /// <summary>Throws when expected_document is provided and does not match the
272
- /// active document's title (case-insensitive; the .rvt extension and a
273
- /// detached suffix are tolerated). No-op when the argument is absent.</summary>
274
- public static void CheckExpectedDocument(JsonElement args, Document doc)
275
- {
276
- string? expected = JsonArgs.GetString(args, "expected_document");
277
- if (string.IsNullOrWhiteSpace(expected))
278
- return;
279
- string actual = doc.Title;
280
- if (Matches(expected, actual))
281
- return;
282
- throw new ArgumentException(
283
- $"Active document is '{actual}' but this call expected '{expected}'. Nothing was changed. "
284
- + "The user switched models (or several are open); re-read the target model (get_model_overview) and retry against the right one.");
285
- }
286
-
287
- private static bool Matches(string expected, string actual)
288
- {
289
- static string Normalize(string value)
290
- {
291
- string v = value.Trim();
292
- if (v.EndsWith(".rvt", StringComparison.OrdinalIgnoreCase))
293
- v = v[..^4];
294
- if (v.EndsWith("_detached", StringComparison.OrdinalIgnoreCase))
295
- v = v[..^"_detached".Length];
296
- return v;
297
- }
298
- return string.Equals(Normalize(expected), Normalize(actual), StringComparison.OrdinalIgnoreCase);
299
- }
300
- }
301
262
  }
@@ -1,8 +1,10 @@
1
1
  # pi-revit workspace
2
2
 
3
3
  This folder tree is the working area for Pi + Revit sessions. The pi-revit tools talk to the
4
- Revit bridge add-in; Revit 2025, 2026, or 2027 must be running with a project open
5
- (only `ping` and `search_api_docs` work without a document).
4
+ Revit bridge add-in, targeting Revit 2025, 2026, and 2027. The 0.3.0 changes were
5
+ tested live on Revit 2025. Bridge document tools require a project open; `ping` and
6
+ `search_api_docs` work without a document. `read_revit_result` reads saved local
7
+ results without contacting Revit.
6
8
 
7
9
  ## File rules — where every file goes
8
10
 
@@ -14,20 +16,26 @@ Documents\pi-revit\
14
16
  ├─ AGENTS.md <- this file
15
17
  ├─ pi-revit.cmd <- double-click launcher
16
18
  └─ Models\
17
- └─ <model title>\ <- created automatically by the tools, one folder per Revit model
18
- ├─ model.txt <- the model's identity (GUID + file path), written by the add-in
19
+ └─ <model title>--<identity hash>\ <- selected automatically for the exported document
20
+ ├─ model.txt <- document identity/path marker written by the add-in
19
21
  ├─ exports\ <- export_documents output (its default)
20
22
  ├─ captures\ <- view snapshots worth keeping
21
23
  └─ scripts\ <- generated scripts and analysis for that model
22
24
  ```
23
25
 
24
26
  1. **Let exports sort themselves**: call `export_documents` without `output_dir` — files land
25
- in `Models\<model title>\exports` automatically, keyed to the exported document. Pass
27
+ in an identity-derived model folder automatically. Treat the returned `outputDir`
28
+ and file paths as authoritative; do not derive the hash from the model title or
29
+ opaque `project.documentId`. Save As can change the destination. Existing title-only
30
+ folders remain untouched. Pass
26
31
  `output_dir` only when the user names a different target.
27
32
  2. **Anything else you produce about a model goes into that model's folder**: view captures the
28
- user wants to keep in `Models\<model title>\captures` (`capture_view` writes to temp — copy
29
- the PNG over; the model title comes from `get_model_overview`), scripts and analysis in
30
- `Models\<model title>\scripts`. Create these subfolders on first use.
33
+ user wants to keep in that verified model folder's `captures` subfolder
34
+ (`capture_view` writes to temp), and scripts and analysis in its `scripts`
35
+ subfolder. For a default export, the model folder is the parent of returned
36
+ `outputDir`. If no folder has been established, identify it from existing model
37
+ markers or choose an explicit destination for the task; do not trigger an
38
+ unnecessary export or guess a title-only folder. Create subfolders on first use.
31
39
  3. **Never create files loose in the workspace root.** The root holds `AGENTS.md`,
32
40
  `pi-revit.cmd`, `Models\`, and Pi's own session data — nothing else, ever.
33
41
 
@@ -35,7 +43,30 @@ Documents\pi-revit\
35
43
 
36
44
  - Start unfamiliar models with `get_model_overview`; use `get_elements` for any listing or
37
45
  counting; read parameter values with `get_element_details`.
46
+ - Copy the intended model's `project.documentId` unchanged into `expected_document_id`
47
+ for `set_parameters`, `execute_csharp`, `export_documents`, `open_view`, and
48
+ selection/zoom changes. Any `manage_selection` call with `isolate_in_view: true`
49
+ also requires it, including action `get`. Pure reads may omit it; supplied IDs
50
+ are checked. Legacy `expected_document` titles alone do not satisfy the guard.
51
+ After a rejection, verify the intended model before refreshing its ID. Closing
52
+ and reopening a model or restarting Revit invalidates earlier IDs.
38
53
  - Before `execute_csharp`, verify unfamiliar API signatures with `search_api_docs`.
39
54
  - Write tools (`set_parameters`, `execute_csharp`) change the real model — state clearly what
40
55
  was changed. `set_parameters` commits partial successes: always check its `failed` list.
41
- A failed `execute_csharp` rolls back entirely and reports the error.
56
+ Inspect `commitWarnings` and the actual transaction outcome. A failed
57
+ `execute_csharp` attempts rollback; do not assume rollback succeeded. A
58
+ `returnValueError` can follow a committed edit. Earlier UI actions and created
59
+ files can persist after failure. Read reported effects before retrying.
60
+ - When a result returns `result_id`, use `read_revit_result` from offset zero and
61
+ follow `next_offset` until `has_more` is false. Join its `text` fragments in order;
62
+ offsets count UTF-16 code units. The returned absolute file path remains a
63
+ fallback for `read` after extension reload while the file exists. Tool query
64
+ pagination and limits still apply separately.
65
+
66
+ ## Upgrade notes
67
+
68
+ After installing 0.3.0, deploy the matching add-in with Revit closed, restart Revit,
69
+ and start a fresh Pi session to load the new tool schemas. Obtain a new overview
70
+ before changing a document. Setup preserves existing `AGENTS.md`; older workspaces
71
+ need these exact-ID, saved-result, and identity-derived folder rules merged into
72
+ their existing instructions.