pi-revit 0.2.11 → 0.2.13
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 +272 -217
- package/README.md +3 -1
- package/bin/pi-revit.js +1 -1
- package/package.json +56 -56
- package/scripts/benchmark-search-docs.py +313 -313
- package/scripts/deploy.ps1 +12 -0
- package/scripts/setup-workspace.ps1 +80 -80
- package/scripts/uninstall.ps1 +146 -146
- package/skills/pi-revit/SKILL.md +2 -1
- package/src/Revit/BridgeServer.cs +67 -0
- package/src/Revit/ToolRegistry.cs +1 -0
- package/src/Revit/Tools/CaptureView.cs +60 -1
- package/src/Revit/Tools/ExecuteCsharp.cs +17 -2
- package/src/Revit/Tools/ExportDocuments.cs +36 -5
- package/src/Revit/Tools/FailureGuard.cs +62 -62
- package/src/Revit/Tools/GetElements.cs +49 -15
- package/src/Revit/Tools/OpenView.cs +103 -0
- package/src/Revit/Tools/ToolSupport.cs +301 -301
- package/workspace/pi-revit-global.cmd +12 -12
|
@@ -1,301 +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
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
rem pi-revit: open Pi in the pi-revit workspace from anywhere. All arguments are
|
|
3
|
-
rem passed straight to pi (e.g. pi-revit -c continues the last session).
|
|
4
|
-
rem Model output needs no foldering decision: the Revit tools file each export
|
|
5
|
-
rem under Models\<model title>\ automatically, keyed by the document it came from.
|
|
6
|
-
rem Installed next to the pi command by scripts\setup-workspace.ps1, which rewrites
|
|
7
|
-
rem the WORKSPACE line below to the chosen -WorkspaceDir.
|
|
8
|
-
|
|
9
|
-
set "WORKSPACE=%USERPROFILE%\Documents\pi-revit"
|
|
10
|
-
|
|
11
|
-
cd /d "%WORKSPACE%"
|
|
12
|
-
pi %*
|
|
1
|
+
@echo off
|
|
2
|
+
rem pi-revit: open Pi in the pi-revit workspace from anywhere. All arguments are
|
|
3
|
+
rem passed straight to pi (e.g. pi-revit -c continues the last session).
|
|
4
|
+
rem Model output needs no foldering decision: the Revit tools file each export
|
|
5
|
+
rem under Models\<model title>\ automatically, keyed by the document it came from.
|
|
6
|
+
rem Installed next to the pi command by scripts\setup-workspace.ps1, which rewrites
|
|
7
|
+
rem the WORKSPACE line below to the chosen -WorkspaceDir.
|
|
8
|
+
|
|
9
|
+
set "WORKSPACE=%USERPROFILE%\Documents\pi-revit"
|
|
10
|
+
|
|
11
|
+
cd /d "%WORKSPACE%"
|
|
12
|
+
pi %*
|