pi-revit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/extensions/pi-revit/index.ts +239 -0
- package/package.json +28 -0
- package/scripts/build.ps1 +37 -0
- package/scripts/deploy.ps1 +99 -0
- package/scripts/revit.ps1 +112 -0
- package/scripts/setup-workspace.ps1 +53 -0
- package/scripts/uninstall.ps1 +103 -0
- package/skills/pi-revit/SKILL.md +50 -0
- package/src/Revit/Application.cs +94 -0
- package/src/Revit/BridgeServer.cs +419 -0
- package/src/Revit/CommandQueue.cs +93 -0
- package/src/Revit/RevitBridge.addin +11 -0
- package/src/Revit/RevitBridge.csproj +48 -0
- package/src/Revit/ToolRegistry.cs +109 -0
- package/src/Revit/Tools/CaptureView.cs +168 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +418 -0
- package/src/Revit/Tools/ExportDocuments.cs +256 -0
- package/src/Revit/Tools/GetElementDetails.cs +318 -0
- package/src/Revit/Tools/GetElementTypes.cs +151 -0
- package/src/Revit/Tools/GetElements.cs +725 -0
- package/src/Revit/Tools/GetModelHealth.cs +191 -0
- package/src/Revit/Tools/GetModelOverview.cs +159 -0
- package/src/Revit/Tools/ManageSelection.cs +220 -0
- package/src/Revit/Tools/SearchApiDocs.cs +610 -0
- package/src/Revit/Tools/SetParameters.cs +382 -0
- package/src/Revit/Tools/ToolSupport.cs +261 -0
- package/workspace/AGENTS.md +23 -0
- package/workspace/pi-revit-global.cmd +31 -0
- package/workspace/pi-revit-here.cmd +5 -0
- package/workspace/project-AGENTS.md +8 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
using System.Globalization;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using Autodesk.Revit.DB;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge.Tools
|
|
6
|
+
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Generic bulk parameter write — and the home for rename (parameter 'Name',
|
|
9
|
+
/// falling back to the Element.Name property when the parameter is read-only,
|
|
10
|
+
/// unit handling). Owns one Transaction("set_parameters") per call: partial success
|
|
11
|
+
/// commits and reports the failed updates; total failure rolls back. Values
|
|
12
|
+
/// are storage-type validated; numeric values convert from the given forge
|
|
13
|
+
/// unit (or the document's display units for the parameter's spec) to
|
|
14
|
+
/// internal units.
|
|
15
|
+
/// </summary>
|
|
16
|
+
internal sealed class SetParameters : ITool
|
|
17
|
+
{
|
|
18
|
+
private const int MaxUpdates = 200;
|
|
19
|
+
|
|
20
|
+
public string Name => "set_parameters";
|
|
21
|
+
public string Label => "Set Parameters";
|
|
22
|
+
public string Description => "Write parameter values on elements — also the home for rename: parameter 'Name' covers levels, views, sheets, types, etc. (falls back to the element's Name property when the Name parameter is read-only). updates apply in ONE transaction: partial success commits and lists the failures; if every update fails the transaction is rolled back and nothing changes. parameter accepts a display name (Comments, Mark, Name), a BuiltInParameter enum name (e.g. ALL_MODEL_MARK), or guid:<GUID> for a shared parameter; type parameters live on the element type, so pass the type's id. Values are validated against the parameter's storage type; numeric values are interpreted in the document's display units for that parameter unless 'unit' (e.g. millimeters, feet, squareMeters) is given.";
|
|
23
|
+
public bool Write => true;
|
|
24
|
+
|
|
25
|
+
public object ParametersSchema => new
|
|
26
|
+
{
|
|
27
|
+
type = "object",
|
|
28
|
+
properties = new
|
|
29
|
+
{
|
|
30
|
+
updates = new
|
|
31
|
+
{
|
|
32
|
+
type = "array",
|
|
33
|
+
description = "Parameter writes to apply in one transaction (1-200 per call).",
|
|
34
|
+
items = new
|
|
35
|
+
{
|
|
36
|
+
type = "object",
|
|
37
|
+
properties = new
|
|
38
|
+
{
|
|
39
|
+
element_id = new { type = "integer", description = "Target element id (use the type id for type parameters)." },
|
|
40
|
+
parameter = new { type = "string", description = "Parameter display name (e.g. Comments, Mark, Name), BuiltInParameter enum name (e.g. ALL_MODEL_MARK), or guid:<GUID> for a shared parameter." },
|
|
41
|
+
value = new { description = "New value: string, number, or boolean (yes/no parameters). Empty string clears a text parameter." },
|
|
42
|
+
unit = new { type = "string", description = "Unit of a numeric value, e.g. millimeters, meters, feet, inches, squareMeters, degrees. Default: the document's display units for that parameter." },
|
|
43
|
+
},
|
|
44
|
+
required = new[] { "element_id", "parameter", "value" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required = new[] { "updates" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
public string? PromptSnippet => "Write element parameter values in bulk, including renames via the 'Name' parameter.";
|
|
52
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
53
|
+
{
|
|
54
|
+
"Use set_parameters for parameter writes and renames (parameter 'Name'); pass 'unit' with numeric values unless the document's display units are intended.",
|
|
55
|
+
"set_parameters commits partial successes — always check the failed list in its result instead of assuming every update applied.",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
59
|
+
{
|
|
60
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
61
|
+
var updates = ParseUpdates(args);
|
|
62
|
+
|
|
63
|
+
var succeeded = new List<Dictionary<string, object?>>();
|
|
64
|
+
var failed = new List<Dictionary<string, object?>>();
|
|
65
|
+
|
|
66
|
+
using var transaction = new Transaction(doc, "set_parameters");
|
|
67
|
+
if (transaction.Start() != TransactionStatus.Started)
|
|
68
|
+
throw new InvalidOperationException("Unable to start the set_parameters transaction.");
|
|
69
|
+
|
|
70
|
+
try
|
|
71
|
+
{
|
|
72
|
+
foreach (var update in updates)
|
|
73
|
+
{
|
|
74
|
+
try
|
|
75
|
+
{
|
|
76
|
+
string? newDisplayValue = Apply(doc, update);
|
|
77
|
+
succeeded.Add(new Dictionary<string, object?>
|
|
78
|
+
{
|
|
79
|
+
["id"] = update.ElementId,
|
|
80
|
+
["parameter"] = update.ParameterInput,
|
|
81
|
+
["newDisplayValue"] = newDisplayValue,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (Exception ex)
|
|
85
|
+
{
|
|
86
|
+
failed.Add(new Dictionary<string, object?>
|
|
87
|
+
{
|
|
88
|
+
["id"] = update.ElementId,
|
|
89
|
+
["parameter"] = update.ParameterInput,
|
|
90
|
+
["reason"] = ex.Message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (succeeded.Count > 0)
|
|
96
|
+
{
|
|
97
|
+
if (transaction.Commit() != TransactionStatus.Committed)
|
|
98
|
+
throw new InvalidOperationException("The set_parameters transaction failed to commit; no changes were saved.");
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
{
|
|
102
|
+
transaction.RollBack();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch
|
|
106
|
+
{
|
|
107
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
108
|
+
transaction.RollBack();
|
|
109
|
+
throw;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
int elementCount = succeeded.Select(row => row["id"]).Distinct().Count();
|
|
113
|
+
string failureSample = failed.Count > 0
|
|
114
|
+
? $" First failure: element {failed[0]["id"]} '{failed[0]["parameter"]}': {failed[0]["reason"]}"
|
|
115
|
+
: string.Empty;
|
|
116
|
+
string compact = failed.Count == 0
|
|
117
|
+
? $"Updated {succeeded.Count} parameter value(s) on {elementCount} element(s)."
|
|
118
|
+
: succeeded.Count == 0
|
|
119
|
+
? $"No updates applied; all {failed.Count} failed — transaction rolled back.{failureSample}"
|
|
120
|
+
: $"Updated {succeeded.Count} parameter value(s) on {elementCount} element(s); {failed.Count} failed.{failureSample}";
|
|
121
|
+
|
|
122
|
+
return new ToolOutput(new
|
|
123
|
+
{
|
|
124
|
+
updated = succeeded.Count,
|
|
125
|
+
committed = succeeded.Count > 0,
|
|
126
|
+
succeeded,
|
|
127
|
+
failed,
|
|
128
|
+
}, compact);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------------------------------------------------- application
|
|
132
|
+
|
|
133
|
+
/// <summary>Applies one update; returns the new display value or throws with the per-update reason.</summary>
|
|
134
|
+
private static string? Apply(Document doc, Update update)
|
|
135
|
+
{
|
|
136
|
+
var element = doc.GetElement(new ElementId(update.ElementId))
|
|
137
|
+
?? throw new InvalidOperationException($"Element {update.ElementId} not found.");
|
|
138
|
+
|
|
139
|
+
var parameter = FindParameter(element, update);
|
|
140
|
+
bool nameTarget = string.Equals(update.ParameterInput, "Name", StringComparison.OrdinalIgnoreCase)
|
|
141
|
+
|| string.Equals(parameter?.Definition?.Name, "Name", StringComparison.OrdinalIgnoreCase);
|
|
142
|
+
|
|
143
|
+
if (parameter is null)
|
|
144
|
+
{
|
|
145
|
+
if (nameTarget)
|
|
146
|
+
return SetElementName(element, update);
|
|
147
|
+
throw new InvalidOperationException($"Parameter '{update.ParameterInput}' not found on element {update.ElementId} ('{element.Name}'). Type parameters live on the element type — pass its id instead.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (parameter.IsReadOnly)
|
|
151
|
+
{
|
|
152
|
+
if (nameTarget)
|
|
153
|
+
return SetElementName(element, update);
|
|
154
|
+
throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' is read-only on element {update.ElementId}.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
bool accepted = parameter.StorageType switch
|
|
158
|
+
{
|
|
159
|
+
StorageType.String => parameter.Set(ValueAsString(update.Value)),
|
|
160
|
+
StorageType.Double => parameter.Set(ToInternalUnits(doc, parameter, RequireDouble(parameter, update.Value), update.Unit)),
|
|
161
|
+
StorageType.Integer => parameter.Set(RequireInt(parameter, update.Value)),
|
|
162
|
+
StorageType.ElementId => parameter.Set(new ElementId(RequireLong(parameter, update.Value))),
|
|
163
|
+
_ => throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' has storage type None and cannot be set."),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (!accepted)
|
|
167
|
+
{
|
|
168
|
+
if (nameTarget)
|
|
169
|
+
return SetElementName(element, update);
|
|
170
|
+
throw new InvalidOperationException($"Revit rejected the value for '{parameter.Definition?.Name}' on element {update.ElementId} (Parameter.Set returned false).");
|
|
171
|
+
}
|
|
172
|
+
return DescribeNewValue(parameter) ?? ValueAsString(update.Value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// <summary>Rename fallback: the Element.Name property setter.</summary>
|
|
176
|
+
private static string SetElementName(Element element, Update update)
|
|
177
|
+
{
|
|
178
|
+
string name = ValueAsString(update.Value);
|
|
179
|
+
if (string.IsNullOrWhiteSpace(name))
|
|
180
|
+
throw new InvalidOperationException("Renaming needs a non-empty string value.");
|
|
181
|
+
element.Name = name;
|
|
182
|
+
return name;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static Parameter? FindParameter(Element element, Update update)
|
|
186
|
+
{
|
|
187
|
+
if (update.BuiltIn is { } builtIn)
|
|
188
|
+
return element.get_Parameter(builtIn);
|
|
189
|
+
if (update.SharedGuid is { } guid)
|
|
190
|
+
return element.get_Parameter(guid);
|
|
191
|
+
|
|
192
|
+
var direct = element.LookupParameter(update.ParameterInput);
|
|
193
|
+
if (direct != null)
|
|
194
|
+
return direct;
|
|
195
|
+
foreach (Parameter parameter in element.Parameters)
|
|
196
|
+
{
|
|
197
|
+
if (string.Equals(parameter.Definition?.Name, update.ParameterInput, StringComparison.OrdinalIgnoreCase))
|
|
198
|
+
return parameter;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static string? DescribeNewValue(Parameter parameter)
|
|
204
|
+
{
|
|
205
|
+
try
|
|
206
|
+
{
|
|
207
|
+
return parameter.StorageType == StorageType.String ? parameter.AsString() : parameter.AsValueString();
|
|
208
|
+
}
|
|
209
|
+
catch
|
|
210
|
+
{
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// <summary>Converts a user-supplied numeric value to internal units using the
|
|
216
|
+
/// explicit unit, or the document's display units for the parameter's spec.</summary>
|
|
217
|
+
private static double ToInternalUnits(Document doc, Parameter parameter, double value, string? unitInput)
|
|
218
|
+
{
|
|
219
|
+
ForgeTypeId? spec = null;
|
|
220
|
+
try
|
|
221
|
+
{
|
|
222
|
+
spec = parameter.Definition?.GetDataType();
|
|
223
|
+
}
|
|
224
|
+
catch
|
|
225
|
+
{
|
|
226
|
+
// Treat as unitless below.
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
bool measurable = false;
|
|
230
|
+
try
|
|
231
|
+
{
|
|
232
|
+
measurable = spec != null && UnitUtils.IsMeasurableSpec(spec);
|
|
233
|
+
}
|
|
234
|
+
catch
|
|
235
|
+
{
|
|
236
|
+
// Treat as unitless below.
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!measurable || spec is null)
|
|
240
|
+
{
|
|
241
|
+
if (!string.IsNullOrWhiteSpace(unitInput))
|
|
242
|
+
throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' is unitless; remove unit '{unitInput}'.");
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ForgeTypeId unit;
|
|
247
|
+
if (!string.IsNullOrWhiteSpace(unitInput))
|
|
248
|
+
{
|
|
249
|
+
unit = UnitResolver.Resolve(unitInput);
|
|
250
|
+
if (!UnitUtils.IsValidUnit(spec, unit))
|
|
251
|
+
throw new InvalidOperationException($"Unit '{unitInput}' does not fit parameter '{parameter.Definition?.Name}' (spec {UnitResolver.ShortName(spec)}); the document displays it in {UnitResolver.ShortName(doc.GetUnits().GetFormatOptions(spec).GetUnitTypeId())}.");
|
|
252
|
+
}
|
|
253
|
+
else
|
|
254
|
+
{
|
|
255
|
+
unit = doc.GetUnits().GetFormatOptions(spec).GetUnitTypeId();
|
|
256
|
+
}
|
|
257
|
+
return UnitUtils.ConvertToInternalUnits(value, unit);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ------------------------------------------------------------ value typing
|
|
261
|
+
|
|
262
|
+
private static double RequireDouble(Parameter parameter, JsonElement value)
|
|
263
|
+
{
|
|
264
|
+
if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out double fromNumber))
|
|
265
|
+
return fromNumber;
|
|
266
|
+
if (value.ValueKind == JsonValueKind.String
|
|
267
|
+
&& double.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double fromString))
|
|
268
|
+
return fromString;
|
|
269
|
+
throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' stores a number; got {value.GetRawText()}.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private static int RequireInt(Parameter parameter, JsonElement value)
|
|
273
|
+
{
|
|
274
|
+
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int fromNumber))
|
|
275
|
+
return fromNumber;
|
|
276
|
+
if (value.ValueKind == JsonValueKind.True)
|
|
277
|
+
return 1;
|
|
278
|
+
if (value.ValueKind == JsonValueKind.False)
|
|
279
|
+
return 0;
|
|
280
|
+
if (value.ValueKind == JsonValueKind.String)
|
|
281
|
+
{
|
|
282
|
+
string text = value.GetString()?.Trim() ?? string.Empty;
|
|
283
|
+
if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int fromString))
|
|
284
|
+
return fromString;
|
|
285
|
+
if (string.Equals(text, "yes", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "true", StringComparison.OrdinalIgnoreCase))
|
|
286
|
+
return 1;
|
|
287
|
+
if (string.Equals(text, "no", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "false", StringComparison.OrdinalIgnoreCase))
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' stores an integer (yes/no parameters take true/false); got {value.GetRawText()}.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private static long RequireLong(Parameter parameter, JsonElement value)
|
|
294
|
+
{
|
|
295
|
+
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long fromNumber))
|
|
296
|
+
return fromNumber;
|
|
297
|
+
if (value.ValueKind == JsonValueKind.String
|
|
298
|
+
&& long.TryParse(value.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out long fromString))
|
|
299
|
+
return fromString;
|
|
300
|
+
throw new InvalidOperationException($"Parameter '{parameter.Definition?.Name}' stores an element id; pass the id as an integer. Got {value.GetRawText()}.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private static string ValueAsString(JsonElement value) => value.ValueKind switch
|
|
304
|
+
{
|
|
305
|
+
JsonValueKind.String => value.GetString() ?? string.Empty,
|
|
306
|
+
JsonValueKind.True => "true",
|
|
307
|
+
JsonValueKind.False => "false",
|
|
308
|
+
JsonValueKind.Null or JsonValueKind.Undefined => string.Empty,
|
|
309
|
+
_ => value.GetRawText(),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------- parsing
|
|
313
|
+
|
|
314
|
+
private sealed class Update
|
|
315
|
+
{
|
|
316
|
+
public required long ElementId { get; init; }
|
|
317
|
+
public required string ParameterInput { get; init; }
|
|
318
|
+
public JsonElement Value { get; init; }
|
|
319
|
+
public string? Unit { get; init; }
|
|
320
|
+
public BuiltInParameter? BuiltIn { get; init; }
|
|
321
|
+
public Guid? SharedGuid { get; init; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private static List<Update> ParseUpdates(JsonElement args)
|
|
325
|
+
{
|
|
326
|
+
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty("updates", out var updatesElement) || updatesElement.ValueKind != JsonValueKind.Array)
|
|
327
|
+
throw new ArgumentException("Missing or invalid required field: updates must be an array of { element_id, parameter, value, unit? } objects.");
|
|
328
|
+
|
|
329
|
+
var updates = new List<Update>(updatesElement.GetArrayLength());
|
|
330
|
+
foreach (var item in updatesElement.EnumerateArray())
|
|
331
|
+
{
|
|
332
|
+
if (item.ValueKind != JsonValueKind.Object)
|
|
333
|
+
throw new ArgumentException("Each update must be an object with element_id, parameter, and value.");
|
|
334
|
+
|
|
335
|
+
long elementId = JsonArgs.GetLong(item, "element_id")
|
|
336
|
+
?? throw new ArgumentException("Each update needs an integer element_id.");
|
|
337
|
+
string parameterInput = (JsonArgs.GetString(item, "parameter") ?? string.Empty).Trim();
|
|
338
|
+
if (parameterInput.Length == 0)
|
|
339
|
+
throw new ArgumentException($"The update for element {elementId} needs a parameter name.");
|
|
340
|
+
if (!item.TryGetProperty("value", out var value))
|
|
341
|
+
throw new ArgumentException($"The update for element {elementId} ('{parameterInput}') needs a value.");
|
|
342
|
+
|
|
343
|
+
BuiltInParameter? builtIn = null;
|
|
344
|
+
Guid? sharedGuid = null;
|
|
345
|
+
if (parameterInput.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
|
|
346
|
+
{
|
|
347
|
+
sharedGuid = Guid.TryParse(parameterInput["guid:".Length..], out var guid)
|
|
348
|
+
? guid
|
|
349
|
+
: throw new ArgumentException($"Invalid shared parameter guid: {parameterInput}");
|
|
350
|
+
}
|
|
351
|
+
else
|
|
352
|
+
{
|
|
353
|
+
string enumName = parameterInput.StartsWith("BuiltInParameter.", StringComparison.OrdinalIgnoreCase)
|
|
354
|
+
? parameterInput["BuiltInParameter.".Length..]
|
|
355
|
+
: parameterInput;
|
|
356
|
+
// BuiltInParameter names are SHOUTY_SNAKE_CASE; require an underscore or
|
|
357
|
+
// all-caps so plain display names like "Comments" never collide.
|
|
358
|
+
bool looksLikeEnumName = enumName.Length > 0 && char.IsLetter(enumName[0]) && !enumName.Contains(' ')
|
|
359
|
+
&& (enumName.Contains('_') || enumName.All(c => !char.IsLetter(c) || char.IsUpper(c)));
|
|
360
|
+
if (looksLikeEnumName && Enum.TryParse<BuiltInParameter>(enumName, true, out var parsed) && parsed != BuiltInParameter.INVALID)
|
|
361
|
+
builtIn = parsed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
updates.Add(new Update
|
|
365
|
+
{
|
|
366
|
+
ElementId = elementId,
|
|
367
|
+
ParameterInput = parameterInput,
|
|
368
|
+
Value = value,
|
|
369
|
+
Unit = JsonArgs.GetString(item, "unit"),
|
|
370
|
+
BuiltIn = builtIn,
|
|
371
|
+
SharedGuid = sharedGuid,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (updates.Count == 0)
|
|
376
|
+
throw new ArgumentException("updates must contain at least one update.");
|
|
377
|
+
if (updates.Count > MaxUpdates)
|
|
378
|
+
throw new ArgumentException($"Too many updates ({updates.Count}); max {MaxUpdates} per call. Apply them in batches.");
|
|
379
|
+
return updates;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# pi-revit workspace
|
|
2
|
+
|
|
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).
|
|
6
|
+
|
|
7
|
+
## Conventions
|
|
8
|
+
|
|
9
|
+
- One subfolder per Revit project under `Projects\`; start Pi from inside the project folder so
|
|
10
|
+
sessions, notes, and outputs stay scoped to that project.
|
|
11
|
+
- Save exports, captured view PNGs, scripts, and analysis results inside the current project
|
|
12
|
+
folder (e.g. an `exports\` subfolder) — not in temp, not in the model's directory.
|
|
13
|
+
- Keep per-project knowledge (naming conventions, known model quirks, decisions) in the project
|
|
14
|
+
folder's own `AGENTS.md` so every session starts informed.
|
|
15
|
+
|
|
16
|
+
## Tool habits
|
|
17
|
+
|
|
18
|
+
- Start unfamiliar models with `get_model_overview`; use `get_elements` for any listing or
|
|
19
|
+
counting; read parameter values with `get_element_details`.
|
|
20
|
+
- Before `execute_csharp`, verify unfamiliar API signatures with `search_api_docs`.
|
|
21
|
+
- Write tools (`set_parameters`, `execute_csharp`) change the real model — state clearly what
|
|
22
|
+
was changed. `set_parameters` commits partial successes: always check its `failed` list.
|
|
23
|
+
A failed `execute_csharp` rolls back entirely and reports the error.
|