pi-revit 0.3.1 → 0.4.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +388 -351
  2. package/README.md +426 -22
  3. package/extensions/pi-revit/index.ts +246 -187
  4. package/extensions/pi-revit/instance-router.ts +86 -0
  5. package/extensions/pi-revit/script-library.ts +146 -0
  6. package/extensions/pi-revit/tool-catalog.ts +67 -0
  7. package/package.json +59 -59
  8. package/skills/pi-revit/SKILL.md +196 -32
  9. package/skills/pi-revit/references/model-audit-export.md +29 -0
  10. package/skills/pi-revit/references/room-documentation.md +28 -0
  11. package/src/Revit/BridgeServer.cs +87 -37
  12. package/src/Revit/OperationStore.cs +178 -0
  13. package/src/Revit/ToolRegistry.cs +57 -35
  14. package/src/Revit/Tools/CaptureView.cs +2 -1
  15. package/src/Revit/Tools/ChangeElementTypes.cs +60 -0
  16. package/src/Revit/Tools/CreateTags.cs +95 -0
  17. package/src/Revit/Tools/DeleteElements.cs +44 -0
  18. package/src/Revit/Tools/DocumentGuard.cs +64 -64
  19. package/src/Revit/Tools/ElementQueryScope.cs +27 -0
  20. package/src/Revit/Tools/ExecuteCsharp.cs +45 -35
  21. package/src/Revit/Tools/ExportDocuments.cs +121 -120
  22. package/src/Revit/Tools/FailureGuard.cs +26 -26
  23. package/src/Revit/Tools/GetElementDetails.cs +42 -12
  24. package/src/Revit/Tools/GetElementRelationships.cs +76 -0
  25. package/src/Revit/Tools/GetElements.cs +37 -23
  26. package/src/Revit/Tools/GetLinkedElements.cs +82 -0
  27. package/src/Revit/Tools/GetLinkedModels.cs +66 -0
  28. package/src/Revit/Tools/GetModelCoordinates.cs +49 -0
  29. package/src/Revit/Tools/GetModelOverview.cs +2 -2
  30. package/src/Revit/Tools/GetScheduleFields.cs +37 -0
  31. package/src/Revit/Tools/GetSchedules.cs +89 -0
  32. package/src/Revit/Tools/ManageElementSets.cs +106 -0
  33. package/src/Revit/Tools/ManageSchedules.cs +164 -0
  34. package/src/Revit/Tools/ManageSelection.cs +37 -36
  35. package/src/Revit/Tools/ManageSheetPlacements.cs +97 -0
  36. package/src/Revit/Tools/ManageSheets.cs +63 -0
  37. package/src/Revit/Tools/ManageViews.cs +100 -0
  38. package/src/Revit/Tools/MeasureGeometry.cs +54 -0
  39. package/src/Revit/Tools/ModelEditBatch.cs +102 -0
  40. package/src/Revit/Tools/ModelEditInputs.cs +49 -0
  41. package/src/Revit/Tools/OpenView.cs +2 -1
  42. package/src/Revit/Tools/QuerySpatialElements.cs +63 -0
  43. package/src/Revit/Tools/SetParameters.cs +43 -93
  44. package/src/Revit/Tools/SpatialBounds.cs +30 -0
  45. package/src/Revit/Tools/SummarizeElements.cs +87 -0
  46. package/src/Revit/Tools/ToolSupport.cs +1 -1
  47. package/src/Revit/Tools/TransformElements.cs +58 -0
  48. package/workspace/AGENTS.md +45 -45
@@ -10,7 +10,7 @@ namespace RevitBridge.Tools
10
10
 
11
11
  public string Name => "get_element_details";
12
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.";
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. Use get_elements parameter_names to project a small set of values alongside a query.";
14
14
 
15
15
  public object ParametersSchema => new
16
16
  {
@@ -36,7 +36,7 @@ namespace RevitBridge.Tools
36
36
  properties = new
37
37
  {
38
38
  parameters = new { type = "boolean", description = "Instance parameter values. Default true." },
39
- type_parameters = new { type = "boolean", description = "Include the element type's parameters, marked isType=true, independently of the instance parameters flag. Default false." },
39
+ type_parameters = new { type = "boolean", description = "Include the element type's parameters, marked isType=true, independently of the instance parameters flag. Default false." },
40
40
  location = new { type = "boolean", description = "Location point or curve (coordinates in internal feet). Default false." },
41
41
  bounding_box = new { type = "boolean", description = "Model bounding box min/max (internal feet). Default false." },
42
42
  materials = new { type = "boolean", description = "Material ids/names with area/volume (internal units). Default false." },
@@ -70,9 +70,9 @@ namespace RevitBridge.Tools
70
70
  JsonElement include = args.TryGetProperty("include", out var includeElement) && includeElement.ValueKind == JsonValueKind.Object
71
71
  ? includeElement
72
72
  : default;
73
- bool withParameters = JsonArgs.GetBool(include, "parameters", true);
74
- bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
75
- bool withAnyParameters = withParameters || withTypeParameters;
73
+ bool withParameters = JsonArgs.GetBool(include, "parameters", true);
74
+ bool withTypeParameters = JsonArgs.GetBool(include, "type_parameters", false);
75
+ bool withAnyParameters = withParameters || withTypeParameters;
76
76
  bool withLocation = JsonArgs.GetBool(include, "location", false);
77
77
  bool withBoundingBox = JsonArgs.GetBool(include, "bounding_box", false);
78
78
  bool withMaterials = JsonArgs.GetBool(include, "materials", false);
@@ -103,11 +103,11 @@ namespace RevitBridge.Tools
103
103
 
104
104
  int parameterCount = 0;
105
105
  int parameterTotal = 0;
106
- if (withAnyParameters)
107
- {
108
- var parameters = new List<Dictionary<string, object?>>();
109
- if (withParameters)
110
- parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
106
+ if (withAnyParameters)
107
+ {
108
+ var parameters = new List<Dictionary<string, object?>>();
109
+ if (withParameters)
110
+ parameterTotal += AppendParameters(doc, element, nameFilter, isType: false, parameters);
111
111
  if (withTypeParameters && elementType != null)
112
112
  parameterTotal += AppendParameters(doc, elementType, nameFilter, isType: true, parameters);
113
113
  dto["parameters"] = parameters;
@@ -124,7 +124,7 @@ namespace RevitBridge.Tools
124
124
  elements.Add(dto);
125
125
  // With a name filter active, "0 params" is ambiguous (none matched vs none
126
126
  // exist): report matched-of-total so a localization miss is visible.
127
- string paramSummary = !withAnyParameters ? string.Empty
127
+ string paramSummary = !withAnyParameters ? string.Empty
128
128
  : nameFilter != null ? $", {parameterCount} of {parameterTotal} params matched parameter_names"
129
129
  : $", {parameterCount} params";
130
130
  compactParts.Add($"'{element.Name}' (id {id}, {element.Category?.Name ?? "no category"}{paramSummary})");
@@ -167,7 +167,37 @@ namespace RevitBridge.Tools
167
167
  return total;
168
168
  }
169
169
 
170
- private static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
170
+ internal static IReadOnlyList<object> ProjectParameters(Document doc, Element element, IReadOnlyList<string> names, bool includeType)
171
+ {
172
+ var result = new List<object>();
173
+ void Project(Element source, bool isType)
174
+ {
175
+ foreach (string input in names.Distinct(StringComparer.OrdinalIgnoreCase))
176
+ {
177
+ IEnumerable<Parameter> matches;
178
+ if (input.StartsWith("guid:", StringComparison.OrdinalIgnoreCase))
179
+ {
180
+ if (!Guid.TryParse(input[5..], out var guid)) throw new ArgumentException($"Invalid shared parameter identity: {input}");
181
+ var parameter = source.get_Parameter(guid);
182
+ matches = parameter == null ? Array.Empty<Parameter>() : new[] { parameter };
183
+ }
184
+ else if (Enum.TryParse<BuiltInParameter>(input, true, out var builtIn) && Enum.IsDefined(builtIn) && builtIn != BuiltInParameter.INVALID)
185
+ {
186
+ var parameter = source.get_Parameter(builtIn);
187
+ matches = parameter == null ? Array.Empty<Parameter>() : new[] { parameter };
188
+ }
189
+ else
190
+ matches = source.Parameters.Cast<Parameter>().Where(p => string.Equals(p.Definition?.Name, input, StringComparison.OrdinalIgnoreCase));
191
+ var values = matches.Select(p => DescribeParameter(doc, p, p.Definition?.Name ?? "", isType)).ToArray();
192
+ result.Add(new { requested = input, isType, found = values.Length > 0, ambiguous = values.Length > 1, matches = values });
193
+ }
194
+ }
195
+ Project(element, false);
196
+ if (includeType && doc.GetElement(element.GetTypeId()) is { } type) Project(type, true);
197
+ return result;
198
+ }
199
+
200
+ internal static Dictionary<string, object?> DescribeParameter(Document doc, Parameter parameter, string name, bool isType)
171
201
  {
172
202
  bool hasValue = parameter.HasValue;
173
203
  var row = new Dictionary<string, object?>
@@ -0,0 +1,76 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetElementRelationships : ITool
7
+ {
8
+ public string Name => "get_element_relationships";
9
+ public string Label => "Get Element Relationships";
10
+ public string Tier => "advanced";
11
+ public string Description => "Inspect host-document element relationships: type, level, owning view, host, family parent/subcomponents, group or assembly membership, joined geometry and logical dependents. Dependents are not a complete deletion-impact prediction. Pagination applies independently to each relationship; links are not traversed.";
12
+ private static readonly string[] Kinds = { "type", "level", "owner_view", "host", "parent", "subcomponents", "group", "assembly", "members", "joined", "dependents" };
13
+ public object ParametersSchema => new
14
+ {
15
+ type = "object",
16
+ properties = new
17
+ {
18
+ element_id = new { type = "integer" },
19
+ relationships = new { type = "array", items = new { type = "string", @enum = Kinds } },
20
+ offset = new { type = "integer", minimum = 0 },
21
+ limit = new { type = "integer", minimum = 1, maximum = 200 },
22
+ },
23
+ required = new[] { "element_id" },
24
+ };
25
+
26
+ public object Execute(JsonElement args, ToolContext context)
27
+ {
28
+ var doc = context.Document ?? throw new NoActiveDocumentException();
29
+ var id = JsonArgs.GetLong(args, "element_id") ?? throw new ArgumentException("element_id is required.");
30
+ var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException("element_id does not exist in the active document.");
31
+ var kinds = (IReadOnlyList<string>?)JsonArgs.GetStringArray(args, "relationships") ?? Kinds;
32
+ if (kinds.Any(kind => !Kinds.Contains(kind))) throw new ArgumentException("Unknown relationship kind.");
33
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
34
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 200);
35
+ var result = new Dictionary<string, object?>();
36
+ foreach (var kind in kinds.Distinct())
37
+ {
38
+ IEnumerable<ElementId> ids = kind switch
39
+ {
40
+ "type" => One(element.GetTypeId()),
41
+ "level" => One(element.LevelId),
42
+ "owner_view" => One(element.OwnerViewId),
43
+ "host" => One((element as FamilyInstance)?.Host?.Id),
44
+ "parent" => One((element as FamilyInstance)?.SuperComponent?.Id),
45
+ "subcomponents" => element is FamilyInstance family ? family.GetSubComponentIds() : Array.Empty<ElementId>(),
46
+ "group" => One(element.GroupId),
47
+ "assembly" => One(element.AssemblyInstanceId),
48
+ "members" => element is Group group ? group.GetMemberIds()
49
+ : element is AssemblyInstance assembly ? assembly.GetMemberIds() : Array.Empty<ElementId>(),
50
+ "joined" => Joined(doc, element),
51
+ "dependents" => element.GetDependentElements(null),
52
+ _ => Array.Empty<ElementId>(),
53
+ };
54
+ var all = ids.Where(x => x != ElementId.InvalidElementId).DistinctBy(x => x.Value).OrderBy(x => x.Value).ToList();
55
+ var rows = all.Skip(offset).Take(limit).Select(x =>
56
+ {
57
+ var related = doc.GetElement(x);
58
+ return new { id = x.Value, name = related?.Name, category = related?.Category?.Name, unique_id = related?.UniqueId };
59
+ }).ToArray();
60
+ result[kind] = new { total_count = all.Count, returned_count = rows.Length, offset,
61
+ has_more = offset + rows.Length < all.Count,
62
+ next_offset = offset + rows.Length < all.Count ? (int?)(offset + rows.Length) : null, elements = rows };
63
+ }
64
+ return new { document_id = DocumentGuard.GetIdentity(doc), element_id = id, relationships = result };
65
+ }
66
+
67
+ private static IEnumerable<ElementId> One(ElementId? id)
68
+ => id == null || id == ElementId.InvalidElementId ? Array.Empty<ElementId>() : new[] { id };
69
+
70
+ private static IEnumerable<ElementId> Joined(Document doc, Element element)
71
+ {
72
+ // JoinGeometryUtils rejects family documents; surface that restriction explicitly.
73
+ if (doc.IsFamilyDocument) throw new ArgumentException("joined relationships require a project document. Request other relationship kinds for families.");
74
+ return JoinGeometryUtils.GetJoinedElements(doc, element);
75
+ }
76
+ }
@@ -21,7 +21,7 @@ namespace RevitBridge.Tools
21
21
 
22
22
  public string Name => "get_elements";
23
23
  public string Label => "Get Elements";
24
- public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields only (id, name, category, typeName, levelId) — read parameter values with get_element_details. Rules with an explicit BuiltInParameter or shared GUID can evaluate inside Revit's collector; display-name rules, regex rules, and unsupported quick filters run as a per-element scan. A display name can identify different parameters even within one category or class. Scope queries to reduce the scan, or use an explicit parameter identity when appropriate. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
24
+ public string Description => "Query Revit elements: scope by category (display name like 'Walls' or enum name like 'OST_Walls'), element class, level, type id, or active view; filter by parameter rules; paginate with offset/limit or just count with count_only. Returns identity fields and optional parameter_names projections; include_type_parameters also reads the requested values from each type. Rules with an explicit BuiltInParameter or shared GUID can evaluate inside Revit's collector; display-name rules, regex rules, and unsupported quick filters run as a per-element scan. A display name can identify different parameters even within one category or class. Scope queries to reduce the scan, or use an explicit parameter identity when appropriate. Numeric rule values are interpreted in the document's display units for that parameter unless 'unit' is given.";
25
25
 
26
26
  public object ParametersSchema => new
27
27
  {
@@ -72,12 +72,18 @@ namespace RevitBridge.Tools
72
72
  level = new { type = "string", description = "Only elements hosted on this level (level name or id)." },
73
73
  type_id = new { type = "integer", description = "Only instances of this element type id (see get_element_types)." },
74
74
  in_active_view = new { type = "boolean", description = "Only elements visible in the active view. Default false." },
75
- count_only = new { type = "boolean", description = "Return only total_count, no element rows. Default false." },
75
+ count_only = new { type = "boolean", description = "Return only total_count, no element rows. Default false." },
76
+ parameter_names = new
77
+ {
78
+ type = "array", maxItems = 20, items = new { type = "string" },
79
+ description = "Optional parameter projections for each returned element: display names, BuiltInParameter names or guid:<GUID>. Every matching parameter is reported, including missing/ambiguous matches. Numeric values use internal units with separate display values. Ignored by count_only.",
80
+ },
81
+ include_type_parameters = new { type = "boolean", description = "Also project requested parameters from each element's type. Default false." },
76
82
  fields = new
77
83
  {
78
84
  type = "array",
79
85
  items = new { type = "string", @enum = new[] { "id", "name", "category", "typeName", "levelId" } },
80
- description = "Identity fields per row; default all five. id is always included. Parameter values live in get_element_details.",
86
+ description = "Identity fields per row; default all five. id is always included. Use parameter_names for optional values alongside these fields.",
81
87
  },
82
88
  offset = new { type = "integer", description = "Pagination offset. Default 0." },
83
89
  limit = new { type = "integer", description = "Max rows to return (1-1000). Default 200." },
@@ -89,7 +95,7 @@ namespace RevitBridge.Tools
89
95
  public IReadOnlyList<string>? PromptGuidelines => new[]
90
96
  {
91
97
  "Use get_elements to list or count elements of any category or class (walls, doors, rooms, sheets, views, ...); use the returned ids with the other Revit tools.",
92
- "get_elements returns identity fields only; read parameter values with get_element_details.",
98
+ "get_elements can project up to 20 parameter_names per row; use get_element_details for full element inspection.",
93
99
  };
94
100
 
95
101
  public object? Execute(JsonElement args, ToolContext context)
@@ -103,7 +109,10 @@ namespace RevitBridge.Tools
103
109
  || string.Equals(JsonArgs.GetString(args, "mode"), "count", StringComparison.OrdinalIgnoreCase);
104
110
  int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
105
111
  int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", DefaultLimit), 1, MaxLimit);
106
- var fields = ResolveFields(args);
112
+ var fields = ResolveFields(args);
113
+ var parameterNames = JsonArgs.GetStringArray(args, "parameter_names");
114
+ if (parameterNames?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 parameter identities.");
115
+ bool includeTypeParameters = JsonArgs.GetBool(args, "include_type_parameters", false);
107
116
 
108
117
  ElementId? categoryId = string.IsNullOrWhiteSpace(categoryInput) ? null : CategoryResolver.Resolve(doc, categoryInput);
109
118
  Type? elementClass = string.IsNullOrWhiteSpace(classInput) ? null : ElementClassResolver.Resolve(classInput);
@@ -158,8 +167,13 @@ namespace RevitBridge.Tools
158
167
  {
159
168
  if (postPredicate != null && !postPredicate(element))
160
169
  continue;
161
- if (rows != null && total >= offset && rows.Count < limit)
162
- rows.Add(ElementIdentity.Build(doc, element, fields));
170
+ if (rows != null && total >= offset && rows.Count < limit)
171
+ {
172
+ var row = ElementIdentity.Build(doc, element, fields);
173
+ if (parameterNames is { Count: > 0 })
174
+ row["parameters"] = GetElementDetails.ProjectParameters(doc, element, parameterNames, includeTypeParameters);
175
+ rows.Add(row);
176
+ }
163
177
  total++;
164
178
  }
165
179
 
@@ -292,9 +306,9 @@ namespace RevitBridge.Tools
292
306
  if (rules.Count == 0)
293
307
  return (null, null, Array.Empty<string>());
294
308
 
295
- // Probe a few in-scope elements for missing-name diagnostics and to find
296
- // storage/spec exemplars for explicit parameter identities. A sample must
297
- // never turn a display name into an assumed globally uniform identity.
309
+ // Probe a few in-scope elements for missing-name diagnostics and to find
310
+ // storage/spec exemplars for explicit parameter identities. A sample must
311
+ // never turn a display name into an assumed globally uniform identity.
298
312
  var probes = new List<Element>(ProbeSize);
299
313
  foreach (Element element in createBaseCollector())
300
314
  {
@@ -326,19 +340,19 @@ namespace RevitBridge.Tools
326
340
  }
327
341
  }
328
342
 
329
- foreach (var rule in rules)
330
- {
331
- if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
332
- continue; // always post-scan (missing-parameter semantics)
333
- // Category/class scope and unanimous samples do not establish a
334
- // uniform parameter identity across later families. Resolve display
335
- // names per element; only explicit identities may use a quick rule.
336
- if (rule.BuiltIn is null && rule.SharedGuid is null)
337
- continue;
338
- var found = probes.Select(probe => FindParameter(probe, rule)).Where(parameter => parameter != null).ToList();
339
- if (found.Count == 0)
340
- continue;
341
- rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
343
+ foreach (var rule in rules)
344
+ {
345
+ if (rule.Op is RuleOp.Regex or RuleOp.IsEmpty or RuleOp.IsNotEmpty)
346
+ continue; // always post-scan (missing-parameter semantics)
347
+ // Category/class scope and unanimous samples do not establish a
348
+ // uniform parameter identity across later families. Resolve display
349
+ // names per element; only explicit identities may use a quick rule.
350
+ if (rule.BuiltIn is null && rule.SharedGuid is null)
351
+ continue;
352
+ var found = probes.Select(probe => FindParameter(probe, rule)).Where(parameter => parameter != null).ToList();
353
+ if (found.Count == 0)
354
+ continue;
355
+ rule.QuickRule = TryBuildQuickRule(doc, rule, found[0]!);
342
356
  }
343
357
 
344
358
  var quickRules = rules.Where(rule => rule.QuickRule != null).ToList();
@@ -0,0 +1,82 @@
1
+ using System.Text.Json;
2
+ using System.Text.Json.Nodes;
3
+ using Autodesk.Revit.DB;
4
+
5
+ namespace RevitBridge.Tools;
6
+
7
+ internal sealed class GetLinkedElements : ITool
8
+ {
9
+ public string Name => "get_linked_elements";
10
+ public string Label => "Get Linked Elements";
11
+ public string Tier => "advanced";
12
+ public string Description => "Query one loaded Revit link with get_elements filters and pagination. Use link_instance_id and expected_linked_document_id from get_linked_models. Returned element IDs belong to the linked document and cannot be passed to host write tools. Optional bounds are axis-aligned in host coordinates, in internal feet. Only direct links are supported; active-view scoping is unavailable.";
13
+ public object ParametersSchema
14
+ {
15
+ get
16
+ {
17
+ var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
18
+ var properties = schema["properties"]!.AsObject();
19
+ properties.Remove("in_active_view");
20
+ properties["link_instance_id"] = new JsonObject { ["type"] = "integer" };
21
+ properties["expected_linked_document_id"] = new JsonObject { ["type"] = "string" };
22
+ properties["include_bounds"] = new JsonObject { ["type"] = "boolean" };
23
+ schema["required"] = new JsonArray("link_instance_id", "expected_linked_document_id");
24
+ return schema;
25
+ }
26
+ }
27
+
28
+ public object Execute(JsonElement args, ToolContext context)
29
+ {
30
+ var host = context.Document ?? throw new NoActiveDocumentException();
31
+ var id = JsonArgs.GetLong(args, "link_instance_id") ?? throw new ArgumentException("link_instance_id is required.");
32
+ var link = host.GetElement(new ElementId(id)) as RevitLinkInstance
33
+ ?? throw new ArgumentException("link_instance_id is not a Revit link instance in the active host document.");
34
+ var linked = link.GetLinkDocument() ?? throw new ArgumentException("The link is unloaded. Load it in Revit before querying it.");
35
+ string identity = DocumentGuard.GetIdentity(linked);
36
+ if (!string.Equals(JsonArgs.GetString(args, "expected_linked_document_id"), identity, StringComparison.Ordinal))
37
+ throw new ArgumentException("The linked document identity is missing or stale. Read get_linked_models again.");
38
+ if (JsonArgs.GetBool(args, "in_active_view", false))
39
+ throw new ArgumentException("in_active_view is not supported for linked queries.");
40
+
41
+ var result = (ToolOutput)new GetElements().Execute(args, new ToolContext(linked, null))!;
42
+ var payload = JsonSerializer.SerializeToNode(result.Payload)!.AsObject();
43
+ payload["host_document_id"] = DocumentGuard.GetIdentity(host);
44
+ payload["linked_document_id"] = identity;
45
+ payload["link_instance_id"] = id;
46
+ payload["coordinate_unit"] = "ft";
47
+ if (payload["elements"] is JsonArray elements)
48
+ {
49
+ var transform = link.GetTotalTransform();
50
+ foreach (var row in elements)
51
+ {
52
+ var element = linked.GetElement(new ElementId(row!["id"]!.GetValue<long>()));
53
+ row["unique_id"] = element.UniqueId;
54
+ row["reference"] = new JsonObject
55
+ {
56
+ ["host_document_id"] = DocumentGuard.GetIdentity(host),
57
+ ["link_instance_id"] = id,
58
+ ["linked_document_id"] = identity,
59
+ ["element_id"] = element.Id.Value,
60
+ };
61
+ if (JsonArgs.GetBool(args, "include_bounds", false))
62
+ row["host_bounds"] = JsonSerializer.SerializeToNode(HostBounds(element.get_BoundingBox(null), transform));
63
+ }
64
+ }
65
+ return new ToolOutput(payload, result.CompactText);
66
+ }
67
+
68
+ internal static object? HostBounds(BoundingBoxXYZ? box, Transform placement)
69
+ {
70
+ if (box == null) return null;
71
+ var corners = new List<XYZ>(8);
72
+ foreach (double x in new[] { box.Min.X, box.Max.X })
73
+ foreach (double y in new[] { box.Min.Y, box.Max.Y })
74
+ foreach (double z in new[] { box.Min.Z, box.Max.Z })
75
+ corners.Add(placement.OfPoint(box.Transform.OfPoint(new XYZ(x, y, z))));
76
+ return new
77
+ {
78
+ min = new[] { corners.Min(p => p.X), corners.Min(p => p.Y), corners.Min(p => p.Z) },
79
+ max = new[] { corners.Max(p => p.X), corners.Max(p => p.Y), corners.Max(p => p.Z) },
80
+ };
81
+ }
82
+ }
@@ -0,0 +1,66 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetLinkedModels : ITool
7
+ {
8
+ public string Name => "get_linked_models";
9
+ public string Label => "Get Linked Models";
10
+ public string Tier => "advanced";
11
+ public string Description => "List placed Revit links, including unloaded instances, exact linked document identities and their transforms into host coordinates. Nested links are not traversed. Coordinates and transform origins use internal feet.";
12
+ public object ParametersSchema => new
13
+ {
14
+ type = "object",
15
+ properties = new
16
+ {
17
+ offset = new { type = "integer", minimum = 0 },
18
+ limit = new { type = "integer", minimum = 1, maximum = 1000 },
19
+ },
20
+ required = Array.Empty<string>(),
21
+ };
22
+
23
+ public object Execute(JsonElement args, ToolContext context)
24
+ {
25
+ var doc = context.Document ?? throw new NoActiveDocumentException();
26
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
27
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
28
+ var links = new FilteredElementCollector(doc).OfClass(typeof(RevitLinkInstance))
29
+ .Cast<RevitLinkInstance>().OrderBy(x => x.Id.Value).ToList();
30
+ var rows = links.Skip(offset).Take(limit).Select(link =>
31
+ {
32
+ var linked = link.GetLinkDocument();
33
+ var transform = link.GetTotalTransform();
34
+ return new
35
+ {
36
+ link_instance_id = link.Id.Value,
37
+ unique_id = link.UniqueId,
38
+ name = link.Name,
39
+ type_id = link.GetTypeId().Value,
40
+ loaded = linked != null,
41
+ linked_document_id = linked == null ? null : DocumentGuard.GetIdentity(linked),
42
+ document_title = linked?.Title,
43
+ transform = new
44
+ {
45
+ origin = Coordinates(transform.Origin),
46
+ basis_x = Coordinates(transform.BasisX),
47
+ basis_y = Coordinates(transform.BasisY),
48
+ basis_z = Coordinates(transform.BasisZ),
49
+ },
50
+ };
51
+ }).ToArray();
52
+ return new
53
+ {
54
+ document_id = DocumentGuard.GetIdentity(doc),
55
+ coordinate_unit = "ft",
56
+ total_count = links.Count,
57
+ returned_count = rows.Length,
58
+ offset,
59
+ has_more = offset + rows.Length < links.Count,
60
+ next_offset = offset + rows.Length < links.Count ? (int?)(offset + rows.Length) : null,
61
+ links = rows,
62
+ };
63
+ }
64
+
65
+ internal static double[] Coordinates(XYZ point) => new[] { point.X, point.Y, point.Z };
66
+ }
@@ -0,0 +1,49 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetModelCoordinates : ITool
7
+ {
8
+ public string Name => "get_model_coordinates";
9
+ public string Label => "Get Model Coordinates";
10
+ public string Tier => "advanced";
11
+ public string Description => "Read project/survey base points, active project location and site coordinates. Optionally report shared coordinates for up to 100 points expressed along document internal axes. Required unit applies to input points and every returned length; angles and latitude/longitude are degrees. The active location's Revit GetProjectPosition defines the shared mapping. List project locations with paging. Does not infer a GIS coordinate reference system or modify/acquire coordinates.";
12
+ public object ParametersSchema => new
13
+ {
14
+ type = "object", properties = new
15
+ {
16
+ unit = ModelEditInputs.LengthUnitSchema,
17
+ points = new { type = "array", maxItems = 100, items = ModelEditInputs.VectorSchema("Point along document internal axes, in unit.") },
18
+ offset = new { type = "integer", minimum = 0 }, limit = new { type = "integer", minimum = 1, maximum = 100 },
19
+ }, required = new[] { "unit" },
20
+ };
21
+ public object Execute(JsonElement args, ToolContext context)
22
+ {
23
+ var doc = context.Document ?? throw new NoActiveDocumentException();
24
+ if (doc.IsFamilyDocument) throw new ArgumentException("Project coordinates require a project document.");
25
+ double scale = ModelEditInputs.LengthScale(args);
26
+ var active = doc.ActiveProjectLocation;
27
+ object Position(ProjectPosition position) => new { east_west = position.EastWest / scale, north_south = position.NorthSouth / scale, elevation = position.Elevation / scale, angle_degrees = position.Angle * 180 / Math.PI };
28
+ object? Base(BasePoint? point) => point == null ? null : new { id = point.Id.Value, internal_position = SpatialBounds.Coordinates(point.Position, scale), shared_position = SpatialBounds.Coordinates(point.SharedPosition, scale) };
29
+ var points = new List<object>();
30
+ if (args.TryGetProperty("points", out var values))
31
+ {
32
+ if (values.ValueKind != JsonValueKind.Array || values.GetArrayLength() > 100) throw new ArgumentException("points must be an array of at most 100 XYZ points.");
33
+ foreach (var value in values.EnumerateArray())
34
+ {
35
+ var wrapper = JsonSerializer.SerializeToElement(new { point = value });
36
+ var point = ModelEditInputs.Vector(wrapper, "point").Multiply(scale);
37
+ points.Add(new { internal_position = SpatialBounds.Coordinates(point, scale), shared_position = Position(active.GetProjectPosition(point)) });
38
+ }
39
+ }
40
+ var locations = doc.ProjectLocations.Cast<ProjectLocation>().OrderBy(x => x.Id.Value).ToList();
41
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0)), limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 50), 1, 100);
42
+ var page = locations.Skip(offset).Take(limit).Select(x => new { id = x.Id.Value, name = x.Name, active = x.Id == active.Id, internal_origin_in_shared_coordinates = Position(x.GetProjectPosition(XYZ.Zero)) }).ToArray();
43
+ var site = doc.SiteLocation;
44
+ return new { unit = JsonArgs.GetString(args, "unit"), angle_unit = "degrees", point_input_coordinates = "document_internal", active_project_location = new { id = active.Id.Value, name = active.Name },
45
+ project_base_point = Base(BasePoint.GetProjectBasePoint(doc)), survey_point = Base(BasePoint.GetSurveyPoint(doc)),
46
+ site = new { name = site.Name, latitude_degrees = site.Latitude * 180 / Math.PI, longitude_degrees = site.Longitude * 180 / Math.PI, time_zone = site.TimeZone },
47
+ total_locations = locations.Count, offset, next_offset = offset + page.Length < locations.Count ? (int?)(offset + page.Length) : null, locations = page, points };
48
+ }
49
+ }
@@ -45,8 +45,8 @@ namespace RevitBridge.Tools
45
45
  string? lengthUnit = DisplayUnitName(units, SpecTypeId.Length);
46
46
  var project = new Dictionary<string, object?>
47
47
  {
48
- ["title"] = doc.Title,
49
- ["documentId"] = DocumentGuard.GetIdentity(doc),
48
+ ["title"] = doc.Title,
49
+ ["documentId"] = DocumentGuard.GetIdentity(doc),
50
50
  ["name"] = info?.Name,
51
51
  ["number"] = info?.Number,
52
52
  ["clientName"] = info?.ClientName,
@@ -0,0 +1,37 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetScheduleFields : ITool
7
+ {
8
+ public string Name => "get_schedule_fields";
9
+ public string Label => "Get Schedule Fields";
10
+ public string Tier => "advanced";
11
+ public string Description => "List fields eligible to be added to one existing schedule, with paging and localized-name filtering. A field is identified by the pair parameter_id + field_type; negative built-in parameter IDs are valid. These identities differ from schedule-local field_id values returned by get_schedules. Existing fields are marked included. Count may appear in the returned fields; manage_schedules accepts its discovered pair or field_type Count without a parameter_id. Calculated/combined fields are not exposed here.";
12
+ public object ParametersSchema => new
13
+ {
14
+ type = "object", properties = new
15
+ {
16
+ schedule_id = new { type = "integer", minimum = 1 }, name_filter = new { type = "string" },
17
+ offset = new { type = "integer", minimum = 0 }, limit = new { type = "integer", minimum = 1, maximum = 200 },
18
+ }, required = new[] { "schedule_id" },
19
+ };
20
+ public object Execute(JsonElement args, ToolContext context)
21
+ {
22
+ var doc = context.Document ?? throw new NoActiveDocumentException();
23
+ var schedule = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "schedule_id") ?? 0)) as ViewSchedule ?? throw new ArgumentException("schedule_id is not a schedule.");
24
+ if (schedule.IsTemplate || schedule.IsTitleblockRevisionSchedule) throw new ArgumentException("Use a regular schedule, not a template or titleblock revision schedule.");
25
+ int offset = JsonArgs.GetInt(args, "offset", 0), limit = JsonArgs.GetInt(args, "limit", 100);
26
+ if (offset < 0 || limit is < 1 or > 200) throw new ArgumentException("Invalid paging range.");
27
+ string needle = JsonArgs.GetString(args, "name_filter") ?? "";
28
+ var definition = schedule.Definition;
29
+ var included = definition.GetFieldOrder().Select(id => definition.GetField(id)).Select(f => (f.ParameterId.Value, f.FieldType)).ToHashSet();
30
+ var fields = definition.GetSchedulableFields().Select(f => new
31
+ {
32
+ parameter_id = f.ParameterId.Value, field_type = f.FieldType.ToString(), name = f.GetName(doc), included = included.Contains((f.ParameterId.Value, f.FieldType)),
33
+ }).Where(f => f.name.Contains(needle, StringComparison.OrdinalIgnoreCase)).OrderBy(f => f.parameter_id).ThenBy(f => f.field_type, StringComparer.Ordinal).ToArray();
34
+ var page = fields.Skip(offset).Take(limit).ToArray();
35
+ return new { schedule_id = schedule.Id.Value, total_count = fields.Length, offset, returned_count = page.Length, next_offset = offset + page.Length < fields.Length ? (int?)(offset + page.Length) : null, fields = page };
36
+ }
37
+ }
@@ -0,0 +1,89 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class GetSchedules : ITool
7
+ {
8
+ public string Name => "get_schedules";
9
+ public string Label => "Get Schedules";
10
+ public string Tier => "advanced";
11
+ public string Description => "List schedules, or read one schedule's field definitions and displayed body cells by schedule_id. Cell text uses Revit formatting and may contain group headings/totals; rows are not guaranteed to correspond to individual elements. Row and column pagination are independent. Templates and revision schedules are excluded from the list.";
12
+ public object ParametersSchema => new
13
+ {
14
+ type = "object",
15
+ properties = new
16
+ {
17
+ schedule_id = new { type = "integer" },
18
+ name_filter = new { type = "string" },
19
+ offset = new { type = "integer", minimum = 0, description = "List offset or body row offset." },
20
+ limit = new { type = "integer", minimum = 1, maximum = 200 },
21
+ column_offset = new { type = "integer", minimum = 0 },
22
+ column_limit = new { type = "integer", minimum = 1, maximum = 50 },
23
+ },
24
+ required = Array.Empty<string>(),
25
+ };
26
+
27
+ public object Execute(JsonElement args, ToolContext context)
28
+ {
29
+ var doc = context.Document ?? throw new NoActiveDocumentException();
30
+ int offset = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
31
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 50), 1, 200);
32
+ if (JsonArgs.GetLong(args, "schedule_id") is not { } id)
33
+ {
34
+ string needle = JsonArgs.GetString(args, "name_filter") ?? "";
35
+ var schedules = new FilteredElementCollector(doc).OfClass(typeof(ViewSchedule)).Cast<ViewSchedule>()
36
+ .Where(s => !s.IsTemplate && !s.IsTitleblockRevisionSchedule && s.Name.Contains(needle, StringComparison.OrdinalIgnoreCase))
37
+ .OrderBy(s => s.Id.Value).ToList();
38
+ var rows = schedules.Skip(offset).Take(limit).Select(s => new
39
+ {
40
+ id = s.Id.Value, name = s.Name, unique_id = s.UniqueId,
41
+ category_id = s.Definition.CategoryId.Value,
42
+ is_itemized = s.Definition.IsItemized,
43
+ field_count = s.Definition.GetFieldCount(),
44
+ }).ToArray();
45
+ return new { total_count = schedules.Count, returned_count = rows.Length, offset,
46
+ has_more = offset + rows.Length < schedules.Count,
47
+ next_offset = offset + rows.Length < schedules.Count ? (int?)(offset + rows.Length) : null, schedules = rows };
48
+ }
49
+
50
+ var schedule = doc.GetElement(new ElementId(id)) as ViewSchedule
51
+ ?? throw new ArgumentException("schedule_id is not a schedule in the active document.");
52
+ if (schedule.IsTemplate) throw new ArgumentException("Schedule templates do not contain displayed body rows.");
53
+ using var table = schedule.GetTableData();
54
+ using var body = table.GetSectionData(SectionType.Body);
55
+ int columnOffset = Math.Max(0, JsonArgs.GetInt(args, "column_offset", 0));
56
+ int columnLimit = Math.Clamp(JsonArgs.GetInt(args, "column_limit", 50), 1, 50);
57
+ int columnCount = Math.Min(columnLimit, Math.Max(0, body.NumberOfColumns - columnOffset));
58
+ int rowCount = Math.Min(limit, Math.Max(0, body.NumberOfRows - offset));
59
+ var cells = Enumerable.Range(0, rowCount).Select(row => new
60
+ {
61
+ row_index = body.FirstRowNumber + offset + row,
62
+ cells = Enumerable.Range(0, columnCount).Select(col => schedule.GetCellText(SectionType.Body,
63
+ body.FirstRowNumber + offset + row, body.FirstColumnNumber + columnOffset + col)).ToArray(),
64
+ }).ToArray();
65
+ var fields = schedule.Definition.GetFieldOrder().Select(fieldId =>
66
+ {
67
+ var field = schedule.Definition.GetField(fieldId);
68
+ return new { field_id = fieldId.IntegerValue, parameter_id = field.ParameterId.Value,
69
+ name = field.GetName(), heading = field.ColumnHeading, hidden = field.IsHidden, field_type = field.FieldType.ToString(),
70
+ spec_type_id = field.GetSpecTypeId().TypeId, grid_width_feet = field.GridColumnWidth, sheet_width_feet = field.SheetColumnWidth,
71
+ can_filter_value = schedule.Definition.CanFilterByValue(fieldId), can_filter_substring = schedule.Definition.CanFilterBySubstring(fieldId) };
72
+ }).ToArray();
73
+ return new
74
+ {
75
+ schedule_id = id, name = schedule.Name, is_itemized = schedule.Definition.IsItemized, fields,
76
+ sort_fields = schedule.Definition.GetSortGroupFields().Select(sort => new { field_id = sort.FieldId.IntegerValue, order = sort.SortOrder.ToString(), show_header = sort.ShowHeader }).ToArray(),
77
+ filters = schedule.Definition.GetFilters().Select(filter => new { field_id = filter.FieldId.IntegerValue, comparison = filter.FilterType.ToString(),
78
+ value = filter.IsStringValue ? (object)filter.GetStringValue() : filter.IsDoubleValue ? filter.GetDoubleValue() : filter.IsIntegerValue ? filter.GetIntegerValue() : filter.IsElementIdValue ? filter.GetElementIdValue().Value : null,
79
+ value_unit = filter.IsDoubleValue ? "internal" : null }).ToArray(),
80
+ total_rows = body.NumberOfRows, total_columns = body.NumberOfColumns,
81
+ offset, returned_rows = rowCount,
82
+ next_offset = offset + rowCount < body.NumberOfRows ? (int?)(offset + rowCount) : null,
83
+ column_offset = columnOffset, returned_columns = columnCount,
84
+ first_column_index = body.FirstColumnNumber + columnOffset,
85
+ next_column_offset = columnOffset + columnCount < body.NumberOfColumns ? (int?)(columnOffset + columnCount) : null,
86
+ rows = cells,
87
+ };
88
+ }
89
+ }