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,256 @@
|
|
|
1
|
+
using System.IO;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using Autodesk.Revit.DB;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge.Tools
|
|
6
|
+
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Minimal document export: sheets/views to PDF, DWG, or PNG files, or the
|
|
9
|
+
/// model to IFC. Write = true for the filesystem side effects; only the IFC
|
|
10
|
+
/// branch wraps a Revit transaction (the IFC exporter stores IFC GUID
|
|
11
|
+
/// parameters on exported elements), owned here. Produced files are
|
|
12
|
+
/// discovered by diffing the output directory because Revit appends its own
|
|
13
|
+
/// view/sheet suffixes to multi-file export names.
|
|
14
|
+
/// </summary>
|
|
15
|
+
internal sealed class ExportDocuments : ITool
|
|
16
|
+
{
|
|
17
|
+
private const int MaxIds = 100;
|
|
18
|
+
private const int PngPixelWidth = 2048;
|
|
19
|
+
|
|
20
|
+
public string Name => "export_documents";
|
|
21
|
+
public string Label => "Export Documents";
|
|
22
|
+
public string Description => "Export documents from the open Revit model. format 'pdf'/'dwg'/'png' export the given sheet/view ids to files: pdf combines everything into one file by default (combine=false writes one PDF per sheet/view, named by Revit's naming rule); png renders 2048 px wide; ifc exports the whole model, or just what one given view shows. Files land in output_dir (created if missing; default: a fresh folder under the system temp directory); file_name_prefix sets the base file name (default: the document title; Revit appends view/sheet suffixes for multi-file exports). Returns the produced file paths with sizes. Find sheet/view ids with get_elements (category 'Sheets' or 'Views') first.";
|
|
23
|
+
public bool Write => true;
|
|
24
|
+
public string Tier => "advanced";
|
|
25
|
+
|
|
26
|
+
public object ParametersSchema => new
|
|
27
|
+
{
|
|
28
|
+
type = "object",
|
|
29
|
+
properties = new
|
|
30
|
+
{
|
|
31
|
+
format = new
|
|
32
|
+
{
|
|
33
|
+
type = "string",
|
|
34
|
+
@enum = new[] { "pdf", "dwg", "png", "ifc" },
|
|
35
|
+
description = "Export format. pdf/dwg/png need sheet/view ids; ifc exports the model (optionally filtered to one view).",
|
|
36
|
+
},
|
|
37
|
+
ids = new
|
|
38
|
+
{
|
|
39
|
+
type = "array",
|
|
40
|
+
items = new { type = "integer" },
|
|
41
|
+
description = "Sheet/view element ids to export (1-100). Required for pdf/dwg/png; for ifc at most one view id (default: the whole model).",
|
|
42
|
+
},
|
|
43
|
+
output_dir = new
|
|
44
|
+
{
|
|
45
|
+
type = "string",
|
|
46
|
+
description = "Output directory, created if missing. Default: a fresh folder under the system temp directory.",
|
|
47
|
+
},
|
|
48
|
+
file_name_prefix = new
|
|
49
|
+
{
|
|
50
|
+
type = "string",
|
|
51
|
+
description = "Base file name without extension. Default: the document title.",
|
|
52
|
+
},
|
|
53
|
+
combine = new
|
|
54
|
+
{
|
|
55
|
+
type = "boolean",
|
|
56
|
+
description = "pdf only: combine all sheets/views into one PDF. Default true.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required = new[] { "format" },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
public string? PromptSnippet => "Export sheets/views to PDF, DWG, or PNG files, or the model to IFC.";
|
|
63
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
64
|
+
{
|
|
65
|
+
"export_documents needs explicit sheet/view ids for pdf/dwg/png — find them with get_elements (category 'Sheets' or 'Views') first.",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
69
|
+
{
|
|
70
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
71
|
+
|
|
72
|
+
string format = (JsonArgs.GetString(args, "format") ?? string.Empty).Trim().ToLowerInvariant();
|
|
73
|
+
if (format is not ("pdf" or "dwg" or "png" or "ifc"))
|
|
74
|
+
throw new ArgumentException($"Unknown format: {JsonArgs.GetString(args, "format")}. Supported: pdf, dwg, png, ifc.");
|
|
75
|
+
|
|
76
|
+
var views = ResolveViews(doc, args, format);
|
|
77
|
+
string outputDir = ResolveOutputDir(args);
|
|
78
|
+
string? prefixInput = JsonArgs.GetString(args, "file_name_prefix");
|
|
79
|
+
string baseName = SanitizeFileName(string.IsNullOrWhiteSpace(prefixInput) ? doc.Title : prefixInput.Trim());
|
|
80
|
+
bool combine = JsonArgs.GetBool(args, "combine", true);
|
|
81
|
+
|
|
82
|
+
// Produced files are found by diffing the directory; the timestamp check
|
|
83
|
+
// (with clock-skew slack) also catches overwrites of pre-existing names.
|
|
84
|
+
DateTime startedUtc = DateTime.UtcNow.AddSeconds(-2);
|
|
85
|
+
var before = new HashSet<string>(Directory.GetFiles(outputDir), StringComparer.OrdinalIgnoreCase);
|
|
86
|
+
|
|
87
|
+
switch (format)
|
|
88
|
+
{
|
|
89
|
+
case "pdf": ExportPdf(doc, views, outputDir, baseName, combine); break;
|
|
90
|
+
case "dwg": ExportDwg(doc, views, outputDir, baseName); break;
|
|
91
|
+
case "png": ExportPng(doc, views, outputDir, baseName); break;
|
|
92
|
+
default: ExportIfc(doc, views, outputDir, baseName); break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var files = Directory.GetFiles(outputDir)
|
|
96
|
+
.Where(path => !before.Contains(path) || File.GetLastWriteTimeUtc(path) >= startedUtc)
|
|
97
|
+
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
98
|
+
.Select(path => new Dictionary<string, object?>
|
|
99
|
+
{
|
|
100
|
+
["path"] = path,
|
|
101
|
+
["fileSizeBytes"] = new FileInfo(path).Length,
|
|
102
|
+
})
|
|
103
|
+
.ToList();
|
|
104
|
+
if (files.Count == 0)
|
|
105
|
+
throw new InvalidOperationException($"The {format} export finished but produced no files in {outputDir}.");
|
|
106
|
+
|
|
107
|
+
string sample = string.Join(", ", files.Take(3).Select(file => Path.GetFileName((string)file["path"]!)));
|
|
108
|
+
string compact = $"Exported {files.Count} {format.ToUpperInvariant()} file(s) to {outputDir}: {sample}{(files.Count > 3 ? $" (+{files.Count - 3} more)" : string.Empty)}.";
|
|
109
|
+
return new ToolOutput(new
|
|
110
|
+
{
|
|
111
|
+
format,
|
|
112
|
+
outputDir,
|
|
113
|
+
fileCount = files.Count,
|
|
114
|
+
files,
|
|
115
|
+
}, compact);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ------------------------------------------------------------- format runs
|
|
119
|
+
|
|
120
|
+
private static void ExportPdf(Document doc, List<View> views, string outputDir, string baseName, bool combine)
|
|
121
|
+
{
|
|
122
|
+
var options = new PDFExportOptions
|
|
123
|
+
{
|
|
124
|
+
Combine = combine,
|
|
125
|
+
FileName = baseName, // names the combined file; per-view files follow Revit's naming rule
|
|
126
|
+
};
|
|
127
|
+
if (!Run(() => doc.Export(outputDir, views.Select(view => view.Id).ToList(), options), "PDF"))
|
|
128
|
+
throw new InvalidOperationException("Revit reported a failed PDF export.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static void ExportDwg(Document doc, List<View> views, string outputDir, string baseName)
|
|
132
|
+
{
|
|
133
|
+
bool available = true;
|
|
134
|
+
try
|
|
135
|
+
{
|
|
136
|
+
available = OptionalFunctionalityUtils.IsDWGExportAvailable();
|
|
137
|
+
}
|
|
138
|
+
catch
|
|
139
|
+
{
|
|
140
|
+
// Probe unavailable; let the export call itself report any failure.
|
|
141
|
+
}
|
|
142
|
+
if (!available)
|
|
143
|
+
throw new InvalidOperationException("DWG export is not available in this Revit installation.");
|
|
144
|
+
|
|
145
|
+
if (!Run(() => doc.Export(outputDir, baseName, views.Select(view => view.Id).ToList(), new DWGExportOptions()), "DWG"))
|
|
146
|
+
throw new InvalidOperationException("Revit reported a failed DWG export.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private static void ExportPng(Document doc, List<View> views, string outputDir, string baseName)
|
|
150
|
+
{
|
|
151
|
+
var options = new ImageExportOptions
|
|
152
|
+
{
|
|
153
|
+
ExportRange = ExportRange.SetOfViews,
|
|
154
|
+
FilePath = Path.Combine(outputDir, baseName),
|
|
155
|
+
HLRandWFViewsFileType = ImageFileType.PNG,
|
|
156
|
+
ShadowViewsFileType = ImageFileType.PNG,
|
|
157
|
+
ImageResolution = ImageResolution.DPI_150,
|
|
158
|
+
ZoomType = ZoomFitType.FitToPage,
|
|
159
|
+
PixelSize = PngPixelWidth,
|
|
160
|
+
FitDirection = FitDirectionType.Horizontal,
|
|
161
|
+
};
|
|
162
|
+
options.SetViewsAndSheets(views.Select(view => view.Id).ToList());
|
|
163
|
+
Run(() => doc.ExportImage(options), "PNG");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// <summary>The IFC exporter writes IFC GUID parameters onto exported elements,
|
|
167
|
+
/// so the Revit API requires a transaction around it — owned here, matching
|
|
168
|
+
/// Revit's own behavior of committing those GUIDs on export.</summary>
|
|
169
|
+
private static void ExportIfc(Document doc, List<View> views, string outputDir, string baseName)
|
|
170
|
+
{
|
|
171
|
+
var options = new IFCExportOptions();
|
|
172
|
+
if (views.Count == 1)
|
|
173
|
+
options.FilterViewId = views[0].Id;
|
|
174
|
+
|
|
175
|
+
using var transaction = new Transaction(doc, "export_documents: ifc");
|
|
176
|
+
if (transaction.Start() != TransactionStatus.Started)
|
|
177
|
+
throw new InvalidOperationException("Unable to start the IFC export transaction.");
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
if (!Run(() => doc.Export(outputDir, baseName, options), "IFC"))
|
|
181
|
+
throw new InvalidOperationException("Revit reported a failed IFC export.");
|
|
182
|
+
if (transaction.Commit() != TransactionStatus.Committed)
|
|
183
|
+
throw new InvalidOperationException("The IFC export transaction failed to commit.");
|
|
184
|
+
}
|
|
185
|
+
catch
|
|
186
|
+
{
|
|
187
|
+
if (transaction.GetStatus() == TransactionStatus.Started)
|
|
188
|
+
transaction.RollBack();
|
|
189
|
+
throw;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// <summary>Runs one export call, translating Revit API exceptions into plain errors.</summary>
|
|
194
|
+
private static T Run<T>(Func<T> export, string what)
|
|
195
|
+
{
|
|
196
|
+
try
|
|
197
|
+
{
|
|
198
|
+
return export();
|
|
199
|
+
}
|
|
200
|
+
catch (Autodesk.Revit.Exceptions.ApplicationException ex)
|
|
201
|
+
{
|
|
202
|
+
throw new InvalidOperationException($"Revit could not run the {what} export: {ex.Message}");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static void Run(Action export, string what)
|
|
207
|
+
=> Run<object?>(() => { export(); return null; }, what);
|
|
208
|
+
|
|
209
|
+
// ----------------------------------------------------------------- parsing
|
|
210
|
+
|
|
211
|
+
private static List<View> ResolveViews(Document doc, JsonElement args, string format)
|
|
212
|
+
{
|
|
213
|
+
bool present = args.ValueKind == JsonValueKind.Object
|
|
214
|
+
&& args.TryGetProperty("ids", out var idsElement)
|
|
215
|
+
&& idsElement.ValueKind != JsonValueKind.Null;
|
|
216
|
+
var ids = present ? JsonArgs.GetLongArray(args, "ids") : new List<long>();
|
|
217
|
+
|
|
218
|
+
if (ids.Count > MaxIds)
|
|
219
|
+
throw new ArgumentException($"Too many ids ({ids.Count}); max {MaxIds} per call. Export in batches.");
|
|
220
|
+
if (format != "ifc" && ids.Count == 0)
|
|
221
|
+
throw new ArgumentException($"format '{format}' requires ids of sheets or views. List them with get_elements (category 'Sheets' or 'Views').");
|
|
222
|
+
if (format == "ifc" && ids.Count > 1)
|
|
223
|
+
throw new ArgumentException("format 'ifc' takes at most one view id (the export is filtered to what that view shows); omit ids to export the whole model.");
|
|
224
|
+
|
|
225
|
+
var views = new List<View>(ids.Count);
|
|
226
|
+
foreach (long id in ids.Distinct())
|
|
227
|
+
{
|
|
228
|
+
var view = doc.GetElement(new ElementId(id)) as View
|
|
229
|
+
?? throw new ArgumentException($"id {id} is not a view or sheet.");
|
|
230
|
+
if (view.IsTemplate)
|
|
231
|
+
throw new ArgumentException($"id {id} ('{view.Name}') is a view template and cannot be exported.");
|
|
232
|
+
if (format == "png" && view is ViewSchedule)
|
|
233
|
+
throw new ArgumentException($"id {id} ('{view.Name}') is a schedule; schedules cannot be exported as PNG images.");
|
|
234
|
+
views.Add(view);
|
|
235
|
+
}
|
|
236
|
+
return views;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private static string ResolveOutputDir(JsonElement args)
|
|
240
|
+
{
|
|
241
|
+
string? requested = JsonArgs.GetString(args, "output_dir");
|
|
242
|
+
string outputDir = string.IsNullOrWhiteSpace(requested)
|
|
243
|
+
? Path.Combine(Path.GetTempPath(), "revit_export_" + Guid.NewGuid().ToString("N"))
|
|
244
|
+
: Path.GetFullPath(requested.Trim());
|
|
245
|
+
Directory.CreateDirectory(outputDir);
|
|
246
|
+
return outputDir;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static string SanitizeFileName(string name)
|
|
250
|
+
{
|
|
251
|
+
var invalid = Path.GetInvalidFileNameChars();
|
|
252
|
+
string clean = new string(name.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim();
|
|
253
|
+
return clean.Length > 0 ? clean : "export";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
using System.Globalization;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using Autodesk.Revit.DB;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge.Tools
|
|
6
|
+
{
|
|
7
|
+
internal sealed class GetElementDetails : ITool
|
|
8
|
+
{
|
|
9
|
+
private const int MaxIds = 50;
|
|
10
|
+
|
|
11
|
+
public string Name => "get_element_details";
|
|
12
|
+
public string Label => "Get Element Details";
|
|
13
|
+
public string Description => "Inspect one or more elements by id: parameter VALUES (name, internal value, formatted displayValue, storage type, display unit where applicable, read-only/shared flags), plus optional type parameters, location (point/curve), bounding box, and materials. Internal numeric values are Revit internal units (feet-based); displayValue is formatted in the document's display units and 'unit' names that display unit. The single home for reading parameter values — get_elements only returns identity fields.";
|
|
14
|
+
|
|
15
|
+
public object ParametersSchema => new
|
|
16
|
+
{
|
|
17
|
+
type = "object",
|
|
18
|
+
properties = new
|
|
19
|
+
{
|
|
20
|
+
element_ids = new
|
|
21
|
+
{
|
|
22
|
+
type = "array",
|
|
23
|
+
items = new { type = "integer" },
|
|
24
|
+
description = "Element ids to inspect (1-50 per call).",
|
|
25
|
+
},
|
|
26
|
+
parameter_names = new
|
|
27
|
+
{
|
|
28
|
+
type = "array",
|
|
29
|
+
items = new { type = "string" },
|
|
30
|
+
description = "Only return these parameters (case-insensitive exact names). Default: all.",
|
|
31
|
+
},
|
|
32
|
+
include = new
|
|
33
|
+
{
|
|
34
|
+
type = "object",
|
|
35
|
+
description = "Optional sections per element.",
|
|
36
|
+
properties = new
|
|
37
|
+
{
|
|
38
|
+
parameters = new { type = "boolean", description = "Instance parameter values. Default true." },
|
|
39
|
+
type_parameters = new { type = "boolean", description = "Also include the element type's parameters, marked isType=true. Default false." },
|
|
40
|
+
location = new { type = "boolean", description = "Location point or curve (coordinates in internal feet). Default false." },
|
|
41
|
+
bounding_box = new { type = "boolean", description = "Model bounding box min/max (internal feet). Default false." },
|
|
42
|
+
materials = new { type = "boolean", description = "Material ids/names with area/volume (internal units). Default false." },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required = new[] { "element_ids" },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
public string? PromptSnippet => "Read parameter values, location, bounding box, and materials for specific Revit elements.";
|
|
50
|
+
public IReadOnlyList<string>? PromptGuidelines => new[]
|
|
51
|
+
{
|
|
52
|
+
"Use get_element_details (with ids from get_elements or the user's selection) whenever parameter values are needed; numeric values carry both internal value and display value with its unit.",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
public object? Execute(JsonElement args, ToolContext context)
|
|
56
|
+
{
|
|
57
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
58
|
+
|
|
59
|
+
var ids = JsonArgs.GetLongArray(args, "element_ids");
|
|
60
|
+
if (ids.Count == 0)
|
|
61
|
+
throw new ArgumentException("element_ids must contain at least one element id.");
|
|
62
|
+
if (ids.Count > MaxIds)
|
|
63
|
+
throw new ArgumentException($"Too many element_ids ({ids.Count}); max {MaxIds} per call. Inspect ids in batches.");
|
|
64
|
+
|
|
65
|
+
var parameterNames = JsonArgs.GetStringArray(args, "parameter_names");
|
|
66
|
+
HashSet<string>? nameFilter = parameterNames is { Count: > 0 }
|
|
67
|
+
? new HashSet<string>(parameterNames, StringComparer.OrdinalIgnoreCase)
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
JsonElement include = args.TryGetProperty("include", out var includeElement) && includeElement.ValueKind == JsonValueKind.Object
|
|
71
|
+
? includeElement
|
|
72
|
+
: default;
|
|
73
|
+
bool withParameters = JsonArgs.GetBool(include, "parameters", true);
|
|
74
|
+
bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
|
|
75
|
+
bool withLocation = JsonArgs.GetBool(include, "location", false);
|
|
76
|
+
bool withBoundingBox = JsonArgs.GetBool(include, "bounding_box", false);
|
|
77
|
+
bool withMaterials = JsonArgs.GetBool(include, "materials", false);
|
|
78
|
+
|
|
79
|
+
var elements = new List<Dictionary<string, object?>>(ids.Count);
|
|
80
|
+
var notFound = new List<long>();
|
|
81
|
+
var compactParts = new List<string>();
|
|
82
|
+
|
|
83
|
+
foreach (long id in ids)
|
|
84
|
+
{
|
|
85
|
+
var element = doc.GetElement(new ElementId(id));
|
|
86
|
+
if (element is null)
|
|
87
|
+
{
|
|
88
|
+
notFound.Add(id);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var elementType = doc.GetElement(element.GetTypeId());
|
|
93
|
+
var dto = new Dictionary<string, object?>
|
|
94
|
+
{
|
|
95
|
+
["id"] = id,
|
|
96
|
+
["name"] = element.Name,
|
|
97
|
+
["category"] = element.Category?.Name,
|
|
98
|
+
["typeName"] = elementType?.Name,
|
|
99
|
+
["typeId"] = elementType?.Id.Value,
|
|
100
|
+
["levelId"] = element.LevelId is { } levelId && levelId != ElementId.InvalidElementId ? levelId.Value : (long?)null,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
int parameterCount = 0;
|
|
104
|
+
if (withParameters)
|
|
105
|
+
{
|
|
106
|
+
var parameters = new List<Dictionary<string, object?>>();
|
|
107
|
+
AppendParameters(doc, element, nameFilter, isType: false, parameters);
|
|
108
|
+
if (withTypeParameters && elementType != null)
|
|
109
|
+
AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
|
|
110
|
+
dto["parameters"] = parameters;
|
|
111
|
+
parameterCount = parameters.Count;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (withLocation)
|
|
115
|
+
dto["location"] = DescribeLocation(element);
|
|
116
|
+
if (withBoundingBox)
|
|
117
|
+
dto["boundingBox"] = DescribeBoundingBox(element);
|
|
118
|
+
if (withMaterials)
|
|
119
|
+
dto["materials"] = DescribeMaterials(doc, element);
|
|
120
|
+
|
|
121
|
+
elements.Add(dto);
|
|
122
|
+
compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{(withParameters ? $", {parameterCount} params" : string.Empty)})");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
string compact = $"{elements.Count} element(s)"
|
|
126
|
+
+ (compactParts.Count > 0 ? $": {string.Join("; ", compactParts.Take(5))}" : string.Empty)
|
|
127
|
+
+ (compactParts.Count > 5 ? $" (+{compactParts.Count - 5} more)" : string.Empty)
|
|
128
|
+
+ (notFound.Count > 0 ? $". Not found: {string.Join(", ", notFound)}" : string.Empty)
|
|
129
|
+
+ ".";
|
|
130
|
+
|
|
131
|
+
return new ToolOutput(new { count = elements.Count, elements, not_found = notFound }, compact);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static void AppendParameters(Document doc, Element element, HashSet<string>? nameFilter, bool isType, List<Dictionary<string, object?>> target)
|
|
135
|
+
{
|
|
136
|
+
var rows = new List<Dictionary<string, object?>>();
|
|
137
|
+
foreach (Parameter parameter in element.Parameters)
|
|
138
|
+
{
|
|
139
|
+
string name = parameter.Definition?.Name ?? string.Empty;
|
|
140
|
+
if (nameFilter != null && !nameFilter.Contains(name))
|
|
141
|
+
continue;
|
|
142
|
+
rows.Add(DescribeParameter(doc, parameter, name, isType));
|
|
143
|
+
}
|
|
144
|
+
rows.Sort((a, b) => string.Compare(a["name"] as string, b["name"] as string, StringComparison.OrdinalIgnoreCase));
|
|
145
|
+
target.AddRange(rows);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
|
|
149
|
+
{
|
|
150
|
+
bool hasValue = parameter.HasValue;
|
|
151
|
+
var row = new Dictionary<string, object?>
|
|
152
|
+
{
|
|
153
|
+
["name"] = name,
|
|
154
|
+
["storageType"] = parameter.StorageType.ToString(),
|
|
155
|
+
["isType"] = isType,
|
|
156
|
+
["isReadOnly"] = parameter.IsReadOnly,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
switch (parameter.StorageType)
|
|
160
|
+
{
|
|
161
|
+
case StorageType.String:
|
|
162
|
+
{
|
|
163
|
+
string? text = hasValue ? parameter.AsString() : null;
|
|
164
|
+
row["value"] = text;
|
|
165
|
+
row["displayValue"] = text;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case StorageType.Integer:
|
|
169
|
+
row["value"] = hasValue ? parameter.AsInteger() : (int?)null;
|
|
170
|
+
row["displayValue"] = hasValue
|
|
171
|
+
? SafeValueString(parameter) ?? parameter.AsInteger().ToString(CultureInfo.InvariantCulture)
|
|
172
|
+
: null;
|
|
173
|
+
break;
|
|
174
|
+
case StorageType.Double:
|
|
175
|
+
{
|
|
176
|
+
row["value"] = hasValue ? parameter.AsDouble() : (double?)null;
|
|
177
|
+
row["displayValue"] = hasValue ? SafeValueString(parameter) : null;
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
var spec = parameter.Definition?.GetDataType();
|
|
181
|
+
if (spec != null && UnitUtils.IsMeasurableSpec(spec))
|
|
182
|
+
row["unit"] = UnitResolver.ShortName(doc.GetUnits().GetFormatOptions(spec).GetUnitTypeId());
|
|
183
|
+
}
|
|
184
|
+
catch
|
|
185
|
+
{
|
|
186
|
+
// Leave unit out when the spec or format options are unavailable.
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case StorageType.ElementId:
|
|
191
|
+
{
|
|
192
|
+
var referenced = hasValue ? parameter.AsElementId() : null;
|
|
193
|
+
row["value"] = referenced != null && referenced != ElementId.InvalidElementId ? referenced.Value : (long?)null;
|
|
194
|
+
row["displayValue"] = hasValue
|
|
195
|
+
? SafeValueString(parameter) ?? (referenced != null ? doc.GetElement(referenced)?.Name : null)
|
|
196
|
+
: null;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
default:
|
|
200
|
+
row["value"] = null;
|
|
201
|
+
row["displayValue"] = hasValue ? SafeValueString(parameter) : null;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (parameter.IsShared)
|
|
206
|
+
{
|
|
207
|
+
row["isShared"] = true;
|
|
208
|
+
try
|
|
209
|
+
{
|
|
210
|
+
row["guid"] = parameter.GUID.ToString();
|
|
211
|
+
}
|
|
212
|
+
catch
|
|
213
|
+
{
|
|
214
|
+
// GUID can throw for malformed shared parameters; skip it.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (parameter.Definition is InternalDefinition internalDefinition && internalDefinition.BuiltInParameter != BuiltInParameter.INVALID)
|
|
218
|
+
row["builtInParameter"] = internalDefinition.BuiltInParameter.ToString();
|
|
219
|
+
|
|
220
|
+
return row;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private static string? SafeValueString(Parameter parameter)
|
|
224
|
+
{
|
|
225
|
+
try
|
|
226
|
+
{
|
|
227
|
+
return parameter.AsValueString();
|
|
228
|
+
}
|
|
229
|
+
catch
|
|
230
|
+
{
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static object? DescribeLocation(Element element)
|
|
236
|
+
{
|
|
237
|
+
switch (element.Location)
|
|
238
|
+
{
|
|
239
|
+
case LocationPoint point:
|
|
240
|
+
{
|
|
241
|
+
var dto = new Dictionary<string, object?> { ["kind"] = "point", ["point"] = Point(point.Point) };
|
|
242
|
+
try
|
|
243
|
+
{
|
|
244
|
+
dto["rotation"] = point.Rotation;
|
|
245
|
+
}
|
|
246
|
+
catch
|
|
247
|
+
{
|
|
248
|
+
// Not all point-located elements expose a rotation.
|
|
249
|
+
}
|
|
250
|
+
return dto;
|
|
251
|
+
}
|
|
252
|
+
case LocationCurve curve:
|
|
253
|
+
{
|
|
254
|
+
var c = curve.Curve;
|
|
255
|
+
var dto = new Dictionary<string, object?> { ["kind"] = "curve", ["isLine"] = c is Line };
|
|
256
|
+
if (c.IsBound)
|
|
257
|
+
{
|
|
258
|
+
dto["start"] = Point(c.GetEndPoint(0));
|
|
259
|
+
dto["end"] = Point(c.GetEndPoint(1));
|
|
260
|
+
dto["length"] = c.Length;
|
|
261
|
+
}
|
|
262
|
+
return dto;
|
|
263
|
+
}
|
|
264
|
+
default:
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static object? DescribeBoundingBox(Element element)
|
|
270
|
+
{
|
|
271
|
+
var box = element.get_BoundingBox(null);
|
|
272
|
+
return box is null ? null : new { min = Point(box.Min), max = Point(box.Max) };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static List<Dictionary<string, object?>> DescribeMaterials(Document doc, Element element)
|
|
276
|
+
{
|
|
277
|
+
var result = new List<Dictionary<string, object?>>();
|
|
278
|
+
ICollection<ElementId> materialIds;
|
|
279
|
+
try
|
|
280
|
+
{
|
|
281
|
+
materialIds = element.GetMaterialIds(false);
|
|
282
|
+
}
|
|
283
|
+
catch
|
|
284
|
+
{
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
foreach (var materialId in materialIds)
|
|
289
|
+
{
|
|
290
|
+
var row = new Dictionary<string, object?>
|
|
291
|
+
{
|
|
292
|
+
["id"] = materialId.Value,
|
|
293
|
+
["name"] = doc.GetElement(materialId)?.Name,
|
|
294
|
+
};
|
|
295
|
+
try
|
|
296
|
+
{
|
|
297
|
+
row["area"] = element.GetMaterialArea(materialId, false);
|
|
298
|
+
}
|
|
299
|
+
catch
|
|
300
|
+
{
|
|
301
|
+
// Area is undefined for some element kinds.
|
|
302
|
+
}
|
|
303
|
+
try
|
|
304
|
+
{
|
|
305
|
+
row["volume"] = element.GetMaterialVolume(materialId);
|
|
306
|
+
}
|
|
307
|
+
catch
|
|
308
|
+
{
|
|
309
|
+
// Volume is undefined for some element kinds.
|
|
310
|
+
}
|
|
311
|
+
result.Add(row);
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private static object Point(XYZ point) => new { x = point.X, y = point.Y, z = point.Z };
|
|
317
|
+
}
|
|
318
|
+
}
|