pi-revit 0.2.8 → 0.2.9
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 +33 -0
- package/README.md +26 -4
- package/extensions/pi-revit/index.ts +64 -20
- package/package.json +1 -1
- package/skills/pi-revit/SKILL.md +2 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +6 -0
- package/src/Revit/Tools/GetElementDetails.cs +26 -6
- package/src/Revit/Tools/GetElements.cs +12 -5
- package/src/Revit/Tools/SearchApiDocs.cs +6 -5
- package/src/Revit/Tools/SetParameters.cs +6 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -261
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ Format follows [Keep a Changelog](https://keepachangelog.com/); version headers
|
|
|
7
7
|
Every published version gets an entry with **Added** / **Changed** / **Fixed** sections
|
|
8
8
|
describing what the user will notice — not internal refactors.
|
|
9
9
|
|
|
10
|
+
## [0.2.9] - 2026-07-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Self-healing tool discovery: when pi starts before Revit, the extension now keeps
|
|
14
|
+
retrying tool discovery in the background (every 15s) and also re-discovers on a
|
|
15
|
+
successful `ping` — no more sessions stuck with only `ping` registered until a fresh
|
|
16
|
+
pi start. When tools arrive mid-session, `ping`'s result says so.
|
|
17
|
+
- `set_parameters` and `execute_csharp` accept an optional `expected_document` (the model
|
|
18
|
+
title): if the active document differs — e.g. the user switched models mid-session —
|
|
19
|
+
the write fails cleanly instead of landing in the wrong model.
|
|
20
|
+
- `get_element_details.parameter_names` now also matches language-independent
|
|
21
|
+
BuiltInParameter enum names (e.g. `ALL_MODEL_MARK`), so filtering works in non-English
|
|
22
|
+
Revit UIs where display names are localized.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `get_element_details` no longer reports a misleading "0 params" when a
|
|
26
|
+
`parameter_names` filter simply matched nothing — it now reports "N of M params
|
|
27
|
+
matched parameter_names" so localization misses are visible. (This explains the
|
|
28
|
+
earlier "0 params vs 38 params" reports: different filter arguments, not flaky reads.)
|
|
29
|
+
- `get_elements`: a display-name filter rule in an **unscoped** query (no category /
|
|
30
|
+
of_class) is no longer promoted to a pinned collector filter based on a 50-element
|
|
31
|
+
probe — it stays on the per-element post-scan path, so categories beyond the probe
|
|
32
|
+
window can't be silently dropped when the same parameter name maps to different ids.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- SKILL.md: guidance on localized parameter names (prefer BuiltInParameter enum names)
|
|
36
|
+
and on using `expected_document` for long sessions / multiple open models.
|
|
37
|
+
- README: new "Safety model" section stating explicitly what the add-in enforces and
|
|
38
|
+
that write-confirmation UX is a client-side decision.
|
|
39
|
+
|
|
40
|
+
Requires redeploying the Revit add-in (`scripts\deploy.ps1` with Revit closed, then
|
|
41
|
+
restart Revit).
|
|
42
|
+
|
|
10
43
|
## [0.2.8] - 2026-07-21
|
|
11
44
|
|
|
12
45
|
### Fixed
|
package/README.md
CHANGED
|
@@ -29,10 +29,32 @@ headless Revit add-in ← no ribbon, no panels; just a bridge
|
|
|
29
29
|
Revit API ← reads run directly; writes run in one named transaction
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The extension discovers its tools from the bridge at startup (
|
|
33
|
-
tool list always matches what the add-in serves. Everything between Pi and
|
|
34
|
-
local-machine only; note that Pi sends conversation context and tool results to your
|
|
35
|
-
LLM provider, like any Pi session.
|
|
32
|
+
The extension discovers its tools from the bridge at startup (retrying in the background until
|
|
33
|
+
Revit is up), so the tool list always matches what the add-in serves. Everything between Pi and
|
|
34
|
+
Revit is local-machine only; note that Pi sends conversation context and tool results to your
|
|
35
|
+
selected LLM provider, like any Pi session.
|
|
36
|
+
|
|
37
|
+
## Safety model
|
|
38
|
+
|
|
39
|
+
Be deliberate about pointing an LLM at a real project model. The add-in enforces what it can
|
|
40
|
+
enforce mechanically, and is honest about what it cannot:
|
|
41
|
+
|
|
42
|
+
- Every write tool is flagged `write: true` — that flag is the machine-readable signal a client
|
|
43
|
+
can gate on. Whether a write needs human confirmation is a **client-side decision**: the
|
|
44
|
+
add-in cannot know your policy, so confirmation UX belongs in the Pi client/agent layer, not
|
|
45
|
+
here.
|
|
46
|
+
- All writes run in one named transaction: committed on success, rolled back on failure, always
|
|
47
|
+
visible in Revit's undo history. Commit-time warnings are reported back (`commitWarnings`);
|
|
48
|
+
error-severity failures roll back with Revit's failure text.
|
|
49
|
+
- `execute_csharp` is an unrestricted escape hatch by design — scripts have full CLR access.
|
|
50
|
+
Treat it like giving the agent a macro editor, on a model you have saved or can restore.
|
|
51
|
+
- Blocking popups are auto-answered so Revit can never hang behind a dialog; unrecognized
|
|
52
|
+
dialogs get the dismissive answer (Cancel/Close/No), never a blind OK.
|
|
53
|
+
- Writes accept an optional `expected_document` check so a queued write cannot silently land in
|
|
54
|
+
a different model than intended.
|
|
55
|
+
|
|
56
|
+
Practical advice: work on saved models, keep worksharing backups/central protection as usual,
|
|
57
|
+
and review the agent's summary of what changed after any write session.
|
|
36
58
|
|
|
37
59
|
## Requirements
|
|
38
60
|
|
|
@@ -275,7 +275,7 @@ async function announceUpdateOnce(notify: (message: string, level: "info") => vo
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
function registerPing(pi: ExtensionAPI) {
|
|
278
|
+
function registerPing(pi: ExtensionAPI, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">) {
|
|
279
279
|
pi.registerTool({
|
|
280
280
|
name: "ping",
|
|
281
281
|
label: "Ping Revit Bridge",
|
|
@@ -287,18 +287,71 @@ function registerPing(pi: ExtensionAPI) {
|
|
|
287
287
|
async execute(_toolCallId, _params, signal) {
|
|
288
288
|
const payload = await bridgeRequest("/ping", { method: "GET" }, signal, 10_000);
|
|
289
289
|
const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
|
|
290
|
+
// The bridge is alive: if this session started before Revit and only has
|
|
291
|
+
// ping, register the bridge tools now and tell the model they arrived.
|
|
292
|
+
let registrationNote = "";
|
|
293
|
+
if (onBridgeAlive) {
|
|
294
|
+
const state = await onBridgeAlive();
|
|
295
|
+
if (state === "registered")
|
|
296
|
+
registrationNote = "\nNOTE: The Revit bridge tools (get_elements, set_parameters, execute_csharp, ...) were just registered in this session and are available from now on.";
|
|
297
|
+
else if (state === "failed")
|
|
298
|
+
registrationNote = "\nNOTE: Bridge tool discovery failed even though ping succeeded; retry ping or restart pi.";
|
|
299
|
+
}
|
|
290
300
|
return {
|
|
291
|
-
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") }],
|
|
301
|
+
content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote }],
|
|
292
302
|
details: payload,
|
|
293
303
|
};
|
|
294
304
|
},
|
|
295
305
|
});
|
|
296
306
|
}
|
|
297
307
|
|
|
308
|
+
const REDISCOVERY_INTERVAL_MS = 15_000;
|
|
309
|
+
|
|
298
310
|
export default async function revitConnector(pi: ExtensionAPI) {
|
|
311
|
+
// Self-healing discovery: when pi starts before Revit is ready, the initial
|
|
312
|
+
// GET /tools fails and only ping is registered. Rather than requiring a
|
|
313
|
+
// fresh pi start (/reload does not reliably re-run async registration), a
|
|
314
|
+
// background retry keeps probing until the bridge appears, and a successful
|
|
315
|
+
// ping also triggers an immediate attempt.
|
|
316
|
+
let bridgeToolsRegistered = false;
|
|
317
|
+
let discoveryInFlight: Promise<boolean> | null = null;
|
|
318
|
+
|
|
319
|
+
async function discoverAndRegister(): Promise<boolean> {
|
|
320
|
+
if (bridgeToolsRegistered) return true;
|
|
321
|
+
if (discoveryInFlight) return discoveryInFlight;
|
|
322
|
+
discoveryInFlight = (async () => {
|
|
323
|
+
try {
|
|
324
|
+
const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
|
|
325
|
+
tools?: BridgeToolDescriptor[];
|
|
326
|
+
};
|
|
327
|
+
const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
|
|
328
|
+
if (descriptors.length === 0) return false;
|
|
329
|
+
for (const descriptor of descriptors) {
|
|
330
|
+
if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
|
|
331
|
+
if (descriptor.name === "ping") continue;
|
|
332
|
+
registerBridgeTool(pi, descriptor);
|
|
333
|
+
}
|
|
334
|
+
bridgeToolsRegistered = true;
|
|
335
|
+
return true;
|
|
336
|
+
} catch {
|
|
337
|
+
// Bridge down (Revit closed, still starting, stale bridge.json):
|
|
338
|
+
// stay on ping only and try again later.
|
|
339
|
+
return false;
|
|
340
|
+
} finally {
|
|
341
|
+
discoveryInFlight = null;
|
|
342
|
+
}
|
|
343
|
+
})();
|
|
344
|
+
return discoveryInFlight;
|
|
345
|
+
}
|
|
346
|
+
|
|
299
347
|
// ping is hard-coded: it must work (and report clearly) even when the
|
|
300
|
-
// bridge is down, so it is never part of /tools discovery.
|
|
301
|
-
|
|
348
|
+
// bridge is down, so it is never part of /tools discovery. A successful
|
|
349
|
+
// ping doubles as a re-discovery trigger — the natural first call in a
|
|
350
|
+
// session that finds itself without bridge tools.
|
|
351
|
+
registerPing(pi, async () => {
|
|
352
|
+
if (bridgeToolsRegistered) return "ready";
|
|
353
|
+
return (await discoverAndRegister()) ? "registered" : "failed";
|
|
354
|
+
});
|
|
302
355
|
|
|
303
356
|
// Surface an incomplete update (see versionMismatch) once per session, right
|
|
304
357
|
// where the user lands after running `pi update --extensions`. Bridge down at
|
|
@@ -314,21 +367,12 @@ export default async function revitConnector(pi: ExtensionAPI) {
|
|
|
314
367
|
}
|
|
315
368
|
});
|
|
316
369
|
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const payload = (await bridgeRequest("/tools", { method: "GET" }, undefined, DISCOVERY_TIMEOUT_MS)) as {
|
|
320
|
-
tools?: BridgeToolDescriptor[];
|
|
321
|
-
};
|
|
322
|
-
descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
|
|
323
|
-
} catch {
|
|
324
|
-
// Bridge down at startup (Revit closed, stale bridge.json, ...): keep
|
|
325
|
-
// only ping registered and never block pi startup. /reload re-discovers.
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
370
|
+
if (await discoverAndRegister()) return;
|
|
328
371
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
372
|
+
// Never block pi startup on Revit: keep retrying quietly in the background
|
|
373
|
+
// and stop the moment discovery succeeds.
|
|
374
|
+
const timer = setInterval(async () => {
|
|
375
|
+
if (await discoverAndRegister()) clearInterval(timer);
|
|
376
|
+
}, REDISCOVERY_INTERVAL_MS);
|
|
377
|
+
timer.unref?.();
|
|
334
378
|
}
|
package/package.json
CHANGED
package/skills/pi-revit/SKILL.md
CHANGED
|
@@ -29,7 +29,8 @@ Workflow guidance:
|
|
|
29
29
|
- Call `get_model_overview` first when starting work on an unfamiliar model — one call returns project metadata, units, levels, grids, and category counts.
|
|
30
30
|
- `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.
|
|
31
31
|
- The selection pipeline is `get_elements` -> ids -> `manage_selection` (action `set`); there is no inline filter on selection.
|
|
32
|
-
- `set_parameters` is the home for bulk parameter writes AND renames (the `Name` parameter covers levels, views, sheets, types). One transaction per batch; per-element failures are reported.
|
|
32
|
+
- `set_parameters` is the home for bulk parameter writes AND renames (the `Name` parameter covers levels, views, sheets, types). One transaction per batch; per-element failures are reported. Pass `expected_document` (the model title) when several models are open or the session is long — it makes the write fail cleanly instead of landing in a different active document.
|
|
33
|
+
- Parameter display names are LOCALIZED: in a non-English Revit UI, `Mark` is `Kennzeichen` (German), `マーク` (Japanese), etc. 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.
|
|
33
34
|
- 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.
|
|
34
35
|
- `export_documents` files its output under `Documents\pi-revit\Models\<model title>\exports` automatically when `output_dir` is omitted — keyed to the exported document, so it lands right even across many models. Pass `output_dir` only when the user names a different target.
|
|
35
36
|
|
|
@@ -72,6 +72,11 @@ namespace RevitBridge.Tools
|
|
|
72
72
|
type = "string",
|
|
73
73
|
description = "C# script body (top-level statements; using directives allowed at the top). Globals doc/uidoc/uiapp and Dump(value) are in scope. The final expression or a return statement is the result.",
|
|
74
74
|
},
|
|
75
|
+
expected_document = new
|
|
76
|
+
{
|
|
77
|
+
type = "string",
|
|
78
|
+
description = "Optional safety check: title of the document this script is meant for (as reported by get_model_overview). If the active document differs (user switched models), the call fails before running anything.",
|
|
79
|
+
},
|
|
75
80
|
},
|
|
76
81
|
required = new[] { "code" },
|
|
77
82
|
};
|
|
@@ -88,6 +93,7 @@ namespace RevitBridge.Tools
|
|
|
88
93
|
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
89
94
|
var uiapp = context.UIApplication ?? throw new NoActiveDocumentException();
|
|
90
95
|
var uidoc = uiapp.ActiveUIDocument ?? throw new NoActiveDocumentException();
|
|
96
|
+
DocumentGuard.CheckExpectedDocument(args, doc);
|
|
91
97
|
|
|
92
98
|
string code = JsonArgs.GetString(args, "code") ?? string.Empty;
|
|
93
99
|
if (string.IsNullOrWhiteSpace(code))
|
|
@@ -27,7 +27,7 @@ namespace RevitBridge.Tools
|
|
|
27
27
|
{
|
|
28
28
|
type = "array",
|
|
29
29
|
items = new { type = "string" },
|
|
30
|
-
description = "Only return these parameters (case-insensitive exact names). Default: all.",
|
|
30
|
+
description = "Only return these parameters. Matches the display name OR the BuiltInParameter enum name (case-insensitive exact). Display names are localized (e.g. 'Mark' is 'Kennzeichen' in a German UI) — prefer enum names like ALL_MODEL_MARK for language-independent filtering. Default: all.",
|
|
31
31
|
},
|
|
32
32
|
include = new
|
|
33
33
|
{
|
|
@@ -101,12 +101,13 @@ namespace RevitBridge.Tools
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
int parameterCount = 0;
|
|
104
|
+
int parameterTotal = 0;
|
|
104
105
|
if (withParameters)
|
|
105
106
|
{
|
|
106
107
|
var parameters = new List<Dictionary<string, object?>>();
|
|
107
|
-
AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
108
|
+
parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
108
109
|
if (withTypeParameters && elementType != null)
|
|
109
|
-
AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
110
|
+
parameterTotal += AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
110
111
|
dto["parameters"] = parameters;
|
|
111
112
|
parameterCount = parameters.Count;
|
|
112
113
|
}
|
|
@@ -119,7 +120,12 @@ namespace RevitBridge.Tools
|
|
|
119
120
|
dto["materials"] = DescribeMaterials(doc, element);
|
|
120
121
|
|
|
121
122
|
elements.Add(dto);
|
|
122
|
-
|
|
123
|
+
// With a name filter active, "0 params" is ambiguous (none matched vs none
|
|
124
|
+
// exist): report matched-of-total so a localization miss is visible.
|
|
125
|
+
string paramSummary = !withParameters ? string.Empty
|
|
126
|
+
: nameFilter != null ? $", {parameterCount} of {parameterTotal} params matched parameter_names"
|
|
127
|
+
: $", {parameterCount} params";
|
|
128
|
+
compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{paramSummary})");
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
string compact = $"{elements.Count} element(s)"
|
|
@@ -131,18 +137,32 @@ namespace RevitBridge.Tools
|
|
|
131
137
|
return new ToolOutput(new { count = elements.Count, elements, not_found = notFound }, compact);
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
|
|
140
|
+
/// <summary>Appends matching parameter rows and returns how many parameters the
|
|
141
|
+
/// element exposes in total (before the name filter).</summary>
|
|
142
|
+
private static int AppendParameters(Document doc, Element element, HashSet<string>? nameFilter, bool isType, List<Dictionary<string, object?>> target)
|
|
135
143
|
{
|
|
136
144
|
var rows = new List<Dictionary<string, object?>>();
|
|
145
|
+
int total = 0;
|
|
137
146
|
foreach (Parameter parameter in element.Parameters)
|
|
138
147
|
{
|
|
148
|
+
total++;
|
|
139
149
|
string name = parameter.Definition?.Name ?? string.Empty;
|
|
140
150
|
if (nameFilter != null && !nameFilter.Contains(name))
|
|
141
|
-
|
|
151
|
+
{
|
|
152
|
+
// Display names are localized; accept the language-independent
|
|
153
|
+
// BuiltInParameter enum name (e.g. ALL_MODEL_MARK) as an alias.
|
|
154
|
+
string? builtIn = parameter.Definition is InternalDefinition internalDefinition
|
|
155
|
+
&& internalDefinition.BuiltInParameter != BuiltInParameter.INVALID
|
|
156
|
+
? internalDefinition.BuiltInParameter.ToString()
|
|
157
|
+
: null;
|
|
158
|
+
if (builtIn is null || !nameFilter.Contains(builtIn))
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
142
161
|
rows.Add(DescribeParameter(doc, parameter, name, isType));
|
|
143
162
|
}
|
|
144
163
|
rows.Sort((a, b) => string.Compare(a["name"] as string, b["name"] as string, StringComparison.OrdinalIgnoreCase));
|
|
145
164
|
target.AddRange(rows);
|
|
165
|
+
return total;
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
private static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
|
|
@@ -287,6 +287,14 @@ namespace RevitBridge.Tools
|
|
|
287
287
|
break;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Display-name promotion is only trustworthy inside one category/class: the
|
|
291
|
+
// probe sees just the first ProbeSize elements in collector order, so in an
|
|
292
|
+
// unscoped query a unanimous sample can still hide other categories further
|
|
293
|
+
// on whose same-named parameter has a different id — and a pinned quick rule
|
|
294
|
+
// would silently drop their matches.
|
|
295
|
+
bool scoped = !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "category"))
|
|
296
|
+
|| !string.IsNullOrWhiteSpace(JsonArgs.GetString(args, "of_class"));
|
|
297
|
+
|
|
290
298
|
foreach (var rule in rules)
|
|
291
299
|
{
|
|
292
300
|
if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
|
|
@@ -296,12 +304,11 @@ namespace RevitBridge.Tools
|
|
|
296
304
|
continue;
|
|
297
305
|
// BuiltInParameter/guid rules address one global parameter id. A plain
|
|
298
306
|
// display name can resolve to DIFFERENT ids per category or family
|
|
299
|
-
// (e.g. 'Width' -> DOOR_WIDTH vs WINDOW_WIDTH)
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// otherwise the rule stays on the (per-element, correct) post-scan path.
|
|
307
|
+
// (e.g. 'Width' -> DOOR_WIDTH vs WINDOW_WIDTH). Promote a display-name
|
|
308
|
+
// rule only when the query is scoped AND all probed elements agree on
|
|
309
|
+
// the id; otherwise it stays on the (per-element, correct) post-scan path.
|
|
303
310
|
bool oneGlobalId = rule.BuiltIn != null || rule.SharedGuid != null
|
|
304
|
-
|| found.All(parameter => parameter!.Id == found[0]!.Id);
|
|
311
|
+
|| (scoped && found.All(parameter => parameter!.Id == found[0]!.Id));
|
|
305
312
|
if (oneGlobalId)
|
|
306
313
|
rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
|
|
307
314
|
}
|
|
@@ -222,11 +222,12 @@ namespace RevitBridge.Tools
|
|
|
222
222
|
return markdown.ToString();
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
/// <summary>The model
|
|
226
|
-
///
|
|
227
|
-
///
|
|
228
|
-
///
|
|
229
|
-
///
|
|
225
|
+
/// <summary>The model can read details.payload (the cap note above even points it
|
|
226
|
+
/// there), but this markdown is what lands in the strongest-attention position of
|
|
227
|
+
/// the tool result — so the top match carries its remarks, parameter, return, and
|
|
228
|
+
/// exception docs inline (capped for display; the payload keeps full text). Lines
|
|
229
|
+
/// that would blow the markdown budget are dropped individually. When the top match
|
|
230
|
+
/// is one of several same-named overloads, a note says how to target another one.</summary>
|
|
230
231
|
private static void AppendTopMatchDocs(StringBuilder markdown, ApiMember member, int overloadCount)
|
|
231
232
|
{
|
|
232
233
|
var lines = new List<string>(5);
|
|
@@ -44,6 +44,11 @@ namespace RevitBridge.Tools
|
|
|
44
44
|
required = new[] { "element_id", "parameter", "value" },
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
+
expected_document = new
|
|
48
|
+
{
|
|
49
|
+
type = "string",
|
|
50
|
+
description = "Optional safety check: title of the document these writes are meant for (as reported by get_model_overview). If the active document differs (user switched models), the call fails without changing anything.",
|
|
51
|
+
},
|
|
47
52
|
},
|
|
48
53
|
required = new[] { "updates" },
|
|
49
54
|
};
|
|
@@ -58,6 +63,7 @@ namespace RevitBridge.Tools
|
|
|
58
63
|
public object? Execute(JsonElement args, ToolContext context)
|
|
59
64
|
{
|
|
60
65
|
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
66
|
+
DocumentGuard.CheckExpectedDocument(args, doc);
|
|
61
67
|
var updates = ParseUpdates(args);
|
|
62
68
|
|
|
63
69
|
var succeeded = new List<Dictionary<string, object?>>();
|
|
@@ -1,261 +1,301 @@
|
|
|
1
|
-
using System.Text.Json;
|
|
2
|
-
using Autodesk.Revit.DB;
|
|
3
|
-
|
|
4
|
-
namespace RevitBridge.Tools
|
|
5
|
-
{
|
|
6
|
-
/// <summary>
|
|
7
|
-
/// Resolves a user-facing category string (friendly alias, category display name,
|
|
8
|
-
/// or BuiltInCategory enum name like OST_Walls) to a category ElementId usable
|
|
9
|
-
/// with FilteredElementCollector.OfCategoryId. Shared by the query tools.
|
|
10
|
-
/// </summary>
|
|
11
|
-
internal static class CategoryResolver
|
|
12
|
-
{
|
|
13
|
-
private static readonly IReadOnlyDictionary<string, BuiltInCategory> Aliases = new Dictionary<string, BuiltInCategory>(StringComparer.OrdinalIgnoreCase)
|
|
14
|
-
{
|
|
15
|
-
["wall"] = BuiltInCategory.OST_Walls,
|
|
16
|
-
["walls"] = BuiltInCategory.OST_Walls,
|
|
17
|
-
["door"] = BuiltInCategory.OST_Doors,
|
|
18
|
-
["doors"] = BuiltInCategory.OST_Doors,
|
|
19
|
-
["window"] = BuiltInCategory.OST_Windows,
|
|
20
|
-
["windows"] = BuiltInCategory.OST_Windows,
|
|
21
|
-
["floor"] = BuiltInCategory.OST_Floors,
|
|
22
|
-
["floors"] = BuiltInCategory.OST_Floors,
|
|
23
|
-
["room"] = BuiltInCategory.OST_Rooms,
|
|
24
|
-
["rooms"] = BuiltInCategory.OST_Rooms,
|
|
25
|
-
["area"] = BuiltInCategory.OST_Areas,
|
|
26
|
-
["areas"] = BuiltInCategory.OST_Areas,
|
|
27
|
-
["column"] = BuiltInCategory.OST_Columns,
|
|
28
|
-
["columns"] = BuiltInCategory.OST_Columns,
|
|
29
|
-
["roof"] = BuiltInCategory.OST_Roofs,
|
|
30
|
-
["roofs"] = BuiltInCategory.OST_Roofs,
|
|
31
|
-
["stair"] = BuiltInCategory.OST_Stairs,
|
|
32
|
-
["stairs"] = BuiltInCategory.OST_Stairs,
|
|
33
|
-
["ceiling"] = BuiltInCategory.OST_Ceilings,
|
|
34
|
-
["ceilings"] = BuiltInCategory.OST_Ceilings,
|
|
35
|
-
["furniture"] = BuiltInCategory.OST_Furniture,
|
|
36
|
-
["pipe"] = BuiltInCategory.OST_PipeCurves,
|
|
37
|
-
["pipes"] = BuiltInCategory.OST_PipeCurves,
|
|
38
|
-
["duct"] = BuiltInCategory.OST_DuctCurves,
|
|
39
|
-
["ducts"] = BuiltInCategory.OST_DuctCurves,
|
|
40
|
-
["beam"] = BuiltInCategory.OST_StructuralFraming,
|
|
41
|
-
["beams"] = BuiltInCategory.OST_StructuralFraming,
|
|
42
|
-
["structural framing"] = BuiltInCategory.OST_StructuralFraming,
|
|
43
|
-
["generic model"] = BuiltInCategory.OST_GenericModel,
|
|
44
|
-
["generic models"] = BuiltInCategory.OST_GenericModel,
|
|
45
|
-
["level"] = BuiltInCategory.OST_Levels,
|
|
46
|
-
["levels"] = BuiltInCategory.OST_Levels,
|
|
47
|
-
["grid"] = BuiltInCategory.OST_Grids,
|
|
48
|
-
["grids"] = BuiltInCategory.OST_Grids,
|
|
49
|
-
["sheet"] = BuiltInCategory.OST_Sheets,
|
|
50
|
-
["sheets"] = BuiltInCategory.OST_Sheets,
|
|
51
|
-
["view"] = BuiltInCategory.OST_Views,
|
|
52
|
-
["views"] = BuiltInCategory.OST_Views,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
public static ElementId Resolve(Document doc, string input)
|
|
56
|
-
{
|
|
57
|
-
string trimmed = input.Trim();
|
|
58
|
-
if (trimmed.Length == 0)
|
|
59
|
-
throw new ArgumentException("category must be a non-empty string.");
|
|
60
|
-
|
|
61
|
-
if (Aliases.TryGetValue(trimmed, out var alias))
|
|
62
|
-
return new ElementId(alias);
|
|
63
|
-
if (TryParseBuiltInCategory(trimmed, out var builtIn))
|
|
64
|
-
return new ElementId(builtIn);
|
|
65
|
-
if (TryParseBuiltInCategory("OST_" + trimmed.Replace(" ", string.Empty), out builtIn))
|
|
66
|
-
return new ElementId(builtIn);
|
|
67
|
-
|
|
68
|
-
foreach (Category category in doc.Settings.Categories)
|
|
69
|
-
{
|
|
70
|
-
if (string.Equals(category.Name, trimmed, StringComparison.OrdinalIgnoreCase))
|
|
71
|
-
return category.Id;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
throw new ArgumentException($"Unknown category: {input}. Use a BuiltInCategory enum name (e.g. OST_Walls) or a category display name as shown in Revit (e.g. Walls, Doors, Sheets, Views).");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private static bool TryParseBuiltInCategory(string text, out BuiltInCategory category)
|
|
78
|
-
{
|
|
79
|
-
category = BuiltInCategory.INVALID;
|
|
80
|
-
return text.Length > 0
|
|
81
|
-
&& char.IsLetter(text[0])
|
|
82
|
-
&& Enum.TryParse(text, true, out category)
|
|
83
|
-
&& category != BuiltInCategory.INVALID;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/// <summary>
|
|
88
|
-
/// Resolves a Revit API class name ("Wall", "ViewSheet", "WallType", or a full
|
|
89
|
-
/// name like "Autodesk.Revit.DB.Mechanical.Duct") to its Type for OfClass filters.
|
|
90
|
-
/// </summary>
|
|
91
|
-
internal static class ElementClassResolver
|
|
92
|
-
{
|
|
93
|
-
private static readonly Lazy<IReadOnlyDictionary<string, Type>> BySimpleName = new(BuildSimpleNameMap);
|
|
94
|
-
|
|
95
|
-
public static Type Resolve(string input)
|
|
96
|
-
{
|
|
97
|
-
string trimmed = input.Trim();
|
|
98
|
-
var assembly = typeof(Element).Assembly;
|
|
99
|
-
Type? match = trimmed.Contains('.') ? assembly.GetType(trimmed, false, true) : null;
|
|
100
|
-
match ??= assembly.GetType("Autodesk.Revit.DB." + trimmed, false, true);
|
|
101
|
-
if (match is null)
|
|
102
|
-
BySimpleName.Value.TryGetValue(trimmed, out match);
|
|
103
|
-
|
|
104
|
-
if (match is null || !typeof(Element).IsAssignableFrom(match))
|
|
105
|
-
throw new ArgumentException($"Unknown Revit element class: {input}. Use an Element subclass from the Revit API (e.g. Wall, FamilyInstance, ViewSheet, Level, WallType) or a full name like Autodesk.Revit.DB.Mechanical.Duct.");
|
|
106
|
-
if (match == typeof(Element))
|
|
107
|
-
throw new ArgumentException("of_class must be a concrete Element subclass, not Element itself.");
|
|
108
|
-
return match;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private static IReadOnlyDictionary<string, Type> BuildSimpleNameMap()
|
|
112
|
-
{
|
|
113
|
-
Type[] types;
|
|
114
|
-
try
|
|
115
|
-
{
|
|
116
|
-
types = typeof(Element).Assembly.GetTypes();
|
|
117
|
-
}
|
|
118
|
-
catch (System.Reflection.ReflectionTypeLoadException ex)
|
|
119
|
-
{
|
|
120
|
-
types = ex.Types.OfType<Type>().ToArray();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
var map = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
|
124
|
-
foreach (var type in types)
|
|
125
|
-
{
|
|
126
|
-
if (!type.IsPublic || !typeof(Element).IsAssignableFrom(type))
|
|
127
|
-
continue;
|
|
128
|
-
// Prefer the root Autodesk.Revit.DB namespace when simple names collide.
|
|
129
|
-
if (!map.TryGetValue(type.Name, out var existing) || (type.Namespace == "Autodesk.Revit.DB" && existing.Namespace != "Autodesk.Revit.DB"))
|
|
130
|
-
map[type.Name] = type;
|
|
131
|
-
}
|
|
132
|
-
return map;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/// <summary>
|
|
137
|
-
/// Resolves friendly forge unit names ("millimeters", "squareMeters") or full forge
|
|
138
|
-
/// ids ("autodesk.unit.unit:millimeters-1.0.1") to UnitTypeId values, and back.
|
|
139
|
-
/// </summary>
|
|
140
|
-
internal static class UnitResolver
|
|
141
|
-
{
|
|
142
|
-
private static readonly Lazy<IReadOnlyDictionary<string, ForgeTypeId>> ByName = new(Build);
|
|
143
|
-
|
|
144
|
-
public static ForgeTypeId Resolve(string unit)
|
|
145
|
-
{
|
|
146
|
-
string trimmed = unit.Trim();
|
|
147
|
-
if (ByName.Value.TryGetValue(trimmed, out var match))
|
|
148
|
-
return match;
|
|
149
|
-
throw new ArgumentException($"Unknown unit: {unit}. Use a forge unit name like millimeters, meters, feet, inches, squareMeters, cubicFeet, degrees.");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/// <summary>"autodesk.unit.unit:millimeters-1.0.1" -> "millimeters".</summary>
|
|
153
|
-
public static string ShortName(ForgeTypeId unit)
|
|
154
|
-
{
|
|
155
|
-
string id = unit.TypeId ?? string.Empty;
|
|
156
|
-
int colon = id.IndexOf(':');
|
|
157
|
-
string tail = colon >= 0 ? id[(colon + 1)..] : id;
|
|
158
|
-
int dash = tail.IndexOf('-');
|
|
159
|
-
return dash >= 0 ? tail[..dash] : tail;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private static IReadOnlyDictionary<string, ForgeTypeId> Build()
|
|
163
|
-
{
|
|
164
|
-
var map = new Dictionary<string, ForgeTypeId>(StringComparer.OrdinalIgnoreCase);
|
|
165
|
-
foreach (ForgeTypeId unit in UnitUtils.GetAllUnits())
|
|
166
|
-
{
|
|
167
|
-
map[ShortName(unit)] = unit;
|
|
168
|
-
map[unit.TypeId] = unit;
|
|
169
|
-
}
|
|
170
|
-
return map;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/// <summary>Identity-only element row shared by the query tools.</summary>
|
|
175
|
-
internal static class ElementIdentity
|
|
176
|
-
{
|
|
177
|
-
public static readonly IReadOnlyList<string> Fields = new[] { "id", "name", "category", "typeName", "levelId" };
|
|
178
|
-
|
|
179
|
-
public static Dictionary<string, object?> Build(Document doc, Element element, IReadOnlyList<string> fields)
|
|
180
|
-
{
|
|
181
|
-
var row = new Dictionary<string, object?>(fields.Count);
|
|
182
|
-
foreach (string field in fields)
|
|
183
|
-
{
|
|
184
|
-
row[field] = field switch
|
|
185
|
-
{
|
|
186
|
-
"id" => (object?)element.Id.Value,
|
|
187
|
-
"name" => element.Name,
|
|
188
|
-
"category" => element.Category?.Name,
|
|
189
|
-
"typeName" => doc.GetElement(element.GetTypeId())?.Name,
|
|
190
|
-
"levelId" => element.LevelId is { } levelId && levelId != ElementId.InvalidElementId ? levelId.Value : (long?)null,
|
|
191
|
-
_ => null,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
return row;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/// <summary>Small JsonElement argument readers shared by the tools.</summary>
|
|
199
|
-
internal static class JsonArgs
|
|
200
|
-
{
|
|
201
|
-
public static string? GetString(JsonElement args, string name)
|
|
202
|
-
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
|
203
|
-
? value.GetString()
|
|
204
|
-
: null;
|
|
205
|
-
|
|
206
|
-
public static bool GetBool(JsonElement args, string name, bool fallback)
|
|
207
|
-
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
208
|
-
? value.GetBoolean()
|
|
209
|
-
: fallback;
|
|
210
|
-
|
|
211
|
-
public static int GetInt(JsonElement args, string name, int fallback)
|
|
212
|
-
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int parsed)
|
|
213
|
-
? parsed
|
|
214
|
-
: fallback;
|
|
215
|
-
|
|
216
|
-
public static long? GetLong(JsonElement args, string name)
|
|
217
|
-
{
|
|
218
|
-
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value))
|
|
219
|
-
return null;
|
|
220
|
-
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long fromNumber))
|
|
221
|
-
return fromNumber;
|
|
222
|
-
if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out long fromString))
|
|
223
|
-
return fromString;
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
public static List<long> GetLongArray(JsonElement args, string name)
|
|
228
|
-
{
|
|
229
|
-
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
230
|
-
throw new ArgumentException($"Missing or invalid required field: {name} must be an array of element ids.");
|
|
231
|
-
|
|
232
|
-
var result = new List<long>(value.GetArrayLength());
|
|
233
|
-
foreach (var item in value.EnumerateArray())
|
|
234
|
-
{
|
|
235
|
-
if (item.ValueKind == JsonValueKind.Number && item.TryGetInt64(out long fromNumber))
|
|
236
|
-
result.Add(fromNumber);
|
|
237
|
-
else if (item.ValueKind == JsonValueKind.String && long.TryParse(item.GetString(), out long fromString))
|
|
238
|
-
result.Add(fromString);
|
|
239
|
-
else
|
|
240
|
-
throw new ArgumentException($"{name} must contain only integer element ids.");
|
|
241
|
-
}
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
public static List<string>? GetStringArray(JsonElement args, string name)
|
|
246
|
-
{
|
|
247
|
-
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
248
|
-
return null;
|
|
249
|
-
|
|
250
|
-
var result = new List<string>(value.GetArrayLength());
|
|
251
|
-
foreach (var item in value.EnumerateArray())
|
|
252
|
-
{
|
|
253
|
-
string? text = item.ValueKind == JsonValueKind.String ? item.GetString() : null;
|
|
254
|
-
if (string.IsNullOrWhiteSpace(text))
|
|
255
|
-
throw new ArgumentException($"{name} must contain only non-empty strings.");
|
|
256
|
-
result.Add(text.Trim());
|
|
257
|
-
}
|
|
258
|
-
return result;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools
|
|
5
|
+
{
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Resolves a user-facing category string (friendly alias, category display name,
|
|
8
|
+
/// or BuiltInCategory enum name like OST_Walls) to a category ElementId usable
|
|
9
|
+
/// with FilteredElementCollector.OfCategoryId. Shared by the query tools.
|
|
10
|
+
/// </summary>
|
|
11
|
+
internal static class CategoryResolver
|
|
12
|
+
{
|
|
13
|
+
private static readonly IReadOnlyDictionary<string, BuiltInCategory> Aliases = new Dictionary<string, BuiltInCategory>(StringComparer.OrdinalIgnoreCase)
|
|
14
|
+
{
|
|
15
|
+
["wall"] = BuiltInCategory.OST_Walls,
|
|
16
|
+
["walls"] = BuiltInCategory.OST_Walls,
|
|
17
|
+
["door"] = BuiltInCategory.OST_Doors,
|
|
18
|
+
["doors"] = BuiltInCategory.OST_Doors,
|
|
19
|
+
["window"] = BuiltInCategory.OST_Windows,
|
|
20
|
+
["windows"] = BuiltInCategory.OST_Windows,
|
|
21
|
+
["floor"] = BuiltInCategory.OST_Floors,
|
|
22
|
+
["floors"] = BuiltInCategory.OST_Floors,
|
|
23
|
+
["room"] = BuiltInCategory.OST_Rooms,
|
|
24
|
+
["rooms"] = BuiltInCategory.OST_Rooms,
|
|
25
|
+
["area"] = BuiltInCategory.OST_Areas,
|
|
26
|
+
["areas"] = BuiltInCategory.OST_Areas,
|
|
27
|
+
["column"] = BuiltInCategory.OST_Columns,
|
|
28
|
+
["columns"] = BuiltInCategory.OST_Columns,
|
|
29
|
+
["roof"] = BuiltInCategory.OST_Roofs,
|
|
30
|
+
["roofs"] = BuiltInCategory.OST_Roofs,
|
|
31
|
+
["stair"] = BuiltInCategory.OST_Stairs,
|
|
32
|
+
["stairs"] = BuiltInCategory.OST_Stairs,
|
|
33
|
+
["ceiling"] = BuiltInCategory.OST_Ceilings,
|
|
34
|
+
["ceilings"] = BuiltInCategory.OST_Ceilings,
|
|
35
|
+
["furniture"] = BuiltInCategory.OST_Furniture,
|
|
36
|
+
["pipe"] = BuiltInCategory.OST_PipeCurves,
|
|
37
|
+
["pipes"] = BuiltInCategory.OST_PipeCurves,
|
|
38
|
+
["duct"] = BuiltInCategory.OST_DuctCurves,
|
|
39
|
+
["ducts"] = BuiltInCategory.OST_DuctCurves,
|
|
40
|
+
["beam"] = BuiltInCategory.OST_StructuralFraming,
|
|
41
|
+
["beams"] = BuiltInCategory.OST_StructuralFraming,
|
|
42
|
+
["structural framing"] = BuiltInCategory.OST_StructuralFraming,
|
|
43
|
+
["generic model"] = BuiltInCategory.OST_GenericModel,
|
|
44
|
+
["generic models"] = BuiltInCategory.OST_GenericModel,
|
|
45
|
+
["level"] = BuiltInCategory.OST_Levels,
|
|
46
|
+
["levels"] = BuiltInCategory.OST_Levels,
|
|
47
|
+
["grid"] = BuiltInCategory.OST_Grids,
|
|
48
|
+
["grids"] = BuiltInCategory.OST_Grids,
|
|
49
|
+
["sheet"] = BuiltInCategory.OST_Sheets,
|
|
50
|
+
["sheets"] = BuiltInCategory.OST_Sheets,
|
|
51
|
+
["view"] = BuiltInCategory.OST_Views,
|
|
52
|
+
["views"] = BuiltInCategory.OST_Views,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
public static ElementId Resolve(Document doc, string input)
|
|
56
|
+
{
|
|
57
|
+
string trimmed = input.Trim();
|
|
58
|
+
if (trimmed.Length == 0)
|
|
59
|
+
throw new ArgumentException("category must be a non-empty string.");
|
|
60
|
+
|
|
61
|
+
if (Aliases.TryGetValue(trimmed, out var alias))
|
|
62
|
+
return new ElementId(alias);
|
|
63
|
+
if (TryParseBuiltInCategory(trimmed, out var builtIn))
|
|
64
|
+
return new ElementId(builtIn);
|
|
65
|
+
if (TryParseBuiltInCategory("OST_" + trimmed.Replace(" ", string.Empty), out builtIn))
|
|
66
|
+
return new ElementId(builtIn);
|
|
67
|
+
|
|
68
|
+
foreach (Category category in doc.Settings.Categories)
|
|
69
|
+
{
|
|
70
|
+
if (string.Equals(category.Name, trimmed, StringComparison.OrdinalIgnoreCase))
|
|
71
|
+
return category.Id;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new ArgumentException($"Unknown category: {input}. Use a BuiltInCategory enum name (e.g. OST_Walls) or a category display name as shown in Revit (e.g. Walls, Doors, Sheets, Views).");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static bool TryParseBuiltInCategory(string text, out BuiltInCategory category)
|
|
78
|
+
{
|
|
79
|
+
category = BuiltInCategory.INVALID;
|
|
80
|
+
return text.Length > 0
|
|
81
|
+
&& char.IsLetter(text[0])
|
|
82
|
+
&& Enum.TryParse(text, true, out category)
|
|
83
|
+
&& category != BuiltInCategory.INVALID;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// <summary>
|
|
88
|
+
/// Resolves a Revit API class name ("Wall", "ViewSheet", "WallType", or a full
|
|
89
|
+
/// name like "Autodesk.Revit.DB.Mechanical.Duct") to its Type for OfClass filters.
|
|
90
|
+
/// </summary>
|
|
91
|
+
internal static class ElementClassResolver
|
|
92
|
+
{
|
|
93
|
+
private static readonly Lazy<IReadOnlyDictionary<string, Type>> BySimpleName = new(BuildSimpleNameMap);
|
|
94
|
+
|
|
95
|
+
public static Type Resolve(string input)
|
|
96
|
+
{
|
|
97
|
+
string trimmed = input.Trim();
|
|
98
|
+
var assembly = typeof(Element).Assembly;
|
|
99
|
+
Type? match = trimmed.Contains('.') ? assembly.GetType(trimmed, false, true) : null;
|
|
100
|
+
match ??= assembly.GetType("Autodesk.Revit.DB." + trimmed, false, true);
|
|
101
|
+
if (match is null)
|
|
102
|
+
BySimpleName.Value.TryGetValue(trimmed, out match);
|
|
103
|
+
|
|
104
|
+
if (match is null || !typeof(Element).IsAssignableFrom(match))
|
|
105
|
+
throw new ArgumentException($"Unknown Revit element class: {input}. Use an Element subclass from the Revit API (e.g. Wall, FamilyInstance, ViewSheet, Level, WallType) or a full name like Autodesk.Revit.DB.Mechanical.Duct.");
|
|
106
|
+
if (match == typeof(Element))
|
|
107
|
+
throw new ArgumentException("of_class must be a concrete Element subclass, not Element itself.");
|
|
108
|
+
return match;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static IReadOnlyDictionary<string, Type> BuildSimpleNameMap()
|
|
112
|
+
{
|
|
113
|
+
Type[] types;
|
|
114
|
+
try
|
|
115
|
+
{
|
|
116
|
+
types = typeof(Element).Assembly.GetTypes();
|
|
117
|
+
}
|
|
118
|
+
catch (System.Reflection.ReflectionTypeLoadException ex)
|
|
119
|
+
{
|
|
120
|
+
types = ex.Types.OfType<Type>().ToArray();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var map = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
|
124
|
+
foreach (var type in types)
|
|
125
|
+
{
|
|
126
|
+
if (!type.IsPublic || !typeof(Element).IsAssignableFrom(type))
|
|
127
|
+
continue;
|
|
128
|
+
// Prefer the root Autodesk.Revit.DB namespace when simple names collide.
|
|
129
|
+
if (!map.TryGetValue(type.Name, out var existing) || (type.Namespace == "Autodesk.Revit.DB" && existing.Namespace != "Autodesk.Revit.DB"))
|
|
130
|
+
map[type.Name] = type;
|
|
131
|
+
}
|
|
132
|
+
return map;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// <summary>
|
|
137
|
+
/// Resolves friendly forge unit names ("millimeters", "squareMeters") or full forge
|
|
138
|
+
/// ids ("autodesk.unit.unit:millimeters-1.0.1") to UnitTypeId values, and back.
|
|
139
|
+
/// </summary>
|
|
140
|
+
internal static class UnitResolver
|
|
141
|
+
{
|
|
142
|
+
private static readonly Lazy<IReadOnlyDictionary<string, ForgeTypeId>> ByName = new(Build);
|
|
143
|
+
|
|
144
|
+
public static ForgeTypeId Resolve(string unit)
|
|
145
|
+
{
|
|
146
|
+
string trimmed = unit.Trim();
|
|
147
|
+
if (ByName.Value.TryGetValue(trimmed, out var match))
|
|
148
|
+
return match;
|
|
149
|
+
throw new ArgumentException($"Unknown unit: {unit}. Use a forge unit name like millimeters, meters, feet, inches, squareMeters, cubicFeet, degrees.");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// <summary>"autodesk.unit.unit:millimeters-1.0.1" -> "millimeters".</summary>
|
|
153
|
+
public static string ShortName(ForgeTypeId unit)
|
|
154
|
+
{
|
|
155
|
+
string id = unit.TypeId ?? string.Empty;
|
|
156
|
+
int colon = id.IndexOf(':');
|
|
157
|
+
string tail = colon >= 0 ? id[(colon + 1)..] : id;
|
|
158
|
+
int dash = tail.IndexOf('-');
|
|
159
|
+
return dash >= 0 ? tail[..dash] : tail;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static IReadOnlyDictionary<string, ForgeTypeId> Build()
|
|
163
|
+
{
|
|
164
|
+
var map = new Dictionary<string, ForgeTypeId>(StringComparer.OrdinalIgnoreCase);
|
|
165
|
+
foreach (ForgeTypeId unit in UnitUtils.GetAllUnits())
|
|
166
|
+
{
|
|
167
|
+
map[ShortName(unit)] = unit;
|
|
168
|
+
map[unit.TypeId] = unit;
|
|
169
|
+
}
|
|
170
|
+
return map;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// <summary>Identity-only element row shared by the query tools.</summary>
|
|
175
|
+
internal static class ElementIdentity
|
|
176
|
+
{
|
|
177
|
+
public static readonly IReadOnlyList<string> Fields = new[] { "id", "name", "category", "typeName", "levelId" };
|
|
178
|
+
|
|
179
|
+
public static Dictionary<string, object?> Build(Document doc, Element element, IReadOnlyList<string> fields)
|
|
180
|
+
{
|
|
181
|
+
var row = new Dictionary<string, object?>(fields.Count);
|
|
182
|
+
foreach (string field in fields)
|
|
183
|
+
{
|
|
184
|
+
row[field] = field switch
|
|
185
|
+
{
|
|
186
|
+
"id" => (object?)element.Id.Value,
|
|
187
|
+
"name" => element.Name,
|
|
188
|
+
"category" => element.Category?.Name,
|
|
189
|
+
"typeName" => doc.GetElement(element.GetTypeId())?.Name,
|
|
190
|
+
"levelId" => element.LevelId is { } levelId && levelId != ElementId.InvalidElementId ? levelId.Value : (long?)null,
|
|
191
|
+
_ => null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return row;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// <summary>Small JsonElement argument readers shared by the tools.</summary>
|
|
199
|
+
internal static class JsonArgs
|
|
200
|
+
{
|
|
201
|
+
public static string? GetString(JsonElement args, string name)
|
|
202
|
+
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
|
203
|
+
? value.GetString()
|
|
204
|
+
: null;
|
|
205
|
+
|
|
206
|
+
public static bool GetBool(JsonElement args, string name, bool fallback)
|
|
207
|
+
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
208
|
+
? value.GetBoolean()
|
|
209
|
+
: fallback;
|
|
210
|
+
|
|
211
|
+
public static int GetInt(JsonElement args, string name, int fallback)
|
|
212
|
+
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int parsed)
|
|
213
|
+
? parsed
|
|
214
|
+
: fallback;
|
|
215
|
+
|
|
216
|
+
public static long? GetLong(JsonElement args, string name)
|
|
217
|
+
{
|
|
218
|
+
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value))
|
|
219
|
+
return null;
|
|
220
|
+
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long fromNumber))
|
|
221
|
+
return fromNumber;
|
|
222
|
+
if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out long fromString))
|
|
223
|
+
return fromString;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public static List<long> GetLongArray(JsonElement args, string name)
|
|
228
|
+
{
|
|
229
|
+
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
230
|
+
throw new ArgumentException($"Missing or invalid required field: {name} must be an array of element ids.");
|
|
231
|
+
|
|
232
|
+
var result = new List<long>(value.GetArrayLength());
|
|
233
|
+
foreach (var item in value.EnumerateArray())
|
|
234
|
+
{
|
|
235
|
+
if (item.ValueKind == JsonValueKind.Number && item.TryGetInt64(out long fromNumber))
|
|
236
|
+
result.Add(fromNumber);
|
|
237
|
+
else if (item.ValueKind == JsonValueKind.String && long.TryParse(item.GetString(), out long fromString))
|
|
238
|
+
result.Add(fromString);
|
|
239
|
+
else
|
|
240
|
+
throw new ArgumentException($"{name} must contain only integer element ids.");
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public static List<string>? GetStringArray(JsonElement args, string name)
|
|
246
|
+
{
|
|
247
|
+
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
248
|
+
return null;
|
|
249
|
+
|
|
250
|
+
var result = new List<string>(value.GetArrayLength());
|
|
251
|
+
foreach (var item in value.EnumerateArray())
|
|
252
|
+
{
|
|
253
|
+
string? text = item.ValueKind == JsonValueKind.String ? item.GetString() : null;
|
|
254
|
+
if (string.IsNullOrWhiteSpace(text))
|
|
255
|
+
throw new ArgumentException($"{name} must contain only non-empty strings.");
|
|
256
|
+
result.Add(text.Trim());
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
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
|
+
}
|