pi-revit 0.3.0 → 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 +394 -351
  2. package/README.md +438 -25
  3. package/bin/pi-revit.js +3 -5
  4. package/extensions/pi-revit/index.ts +151 -92
  5. package/extensions/pi-revit/instance-router.ts +86 -0
  6. package/extensions/pi-revit/script-library.ts +146 -0
  7. package/extensions/pi-revit/tool-catalog.ts +67 -0
  8. package/package.json +58 -57
  9. package/scripts/build.ps1 +9 -3
  10. package/scripts/check-sdk.ps1 +66 -0
  11. package/scripts/deploy.ps1 +16 -4
  12. package/skills/pi-revit/SKILL.md +181 -17
  13. package/skills/pi-revit/references/model-audit-export.md +29 -0
  14. package/skills/pi-revit/references/room-documentation.md +28 -0
  15. package/src/Revit/BridgeServer.cs +79 -29
  16. package/src/Revit/OperationStore.cs +178 -0
  17. package/src/Revit/ToolRegistry.cs +26 -4
  18. package/src/Revit/Tools/CaptureView.cs +2 -1
  19. package/src/Revit/Tools/ChangeElementTypes.cs +60 -0
  20. package/src/Revit/Tools/CreateTags.cs +95 -0
  21. package/src/Revit/Tools/DeleteElements.cs +44 -0
  22. package/src/Revit/Tools/DocumentGuard.cs +2 -2
  23. package/src/Revit/Tools/ElementQueryScope.cs +27 -0
  24. package/src/Revit/Tools/ExecuteCsharp.cs +23 -13
  25. package/src/Revit/Tools/ExportDocuments.cs +2 -1
  26. package/src/Revit/Tools/GetElementDetails.cs +32 -2
  27. package/src/Revit/Tools/GetElementRelationships.cs +76 -0
  28. package/src/Revit/Tools/GetElements.cs +21 -7
  29. package/src/Revit/Tools/GetLinkedElements.cs +82 -0
  30. package/src/Revit/Tools/GetLinkedModels.cs +66 -0
  31. package/src/Revit/Tools/GetModelCoordinates.cs +49 -0
  32. package/src/Revit/Tools/GetScheduleFields.cs +37 -0
  33. package/src/Revit/Tools/GetSchedules.cs +89 -0
  34. package/src/Revit/Tools/ManageElementSets.cs +106 -0
  35. package/src/Revit/Tools/ManageSchedules.cs +164 -0
  36. package/src/Revit/Tools/ManageSelection.cs +2 -1
  37. package/src/Revit/Tools/ManageSheetPlacements.cs +97 -0
  38. package/src/Revit/Tools/ManageSheets.cs +63 -0
  39. package/src/Revit/Tools/ManageViews.cs +100 -0
  40. package/src/Revit/Tools/MeasureGeometry.cs +54 -0
  41. package/src/Revit/Tools/ModelEditBatch.cs +102 -0
  42. package/src/Revit/Tools/ModelEditInputs.cs +49 -0
  43. package/src/Revit/Tools/OpenView.cs +2 -1
  44. package/src/Revit/Tools/QuerySpatialElements.cs +63 -0
  45. package/src/Revit/Tools/SetParameters.cs +43 -93
  46. package/src/Revit/Tools/SpatialBounds.cs +30 -0
  47. package/src/Revit/Tools/SummarizeElements.cs +87 -0
  48. package/src/Revit/Tools/TransformElements.cs +58 -0
@@ -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
+ }
@@ -0,0 +1,106 @@
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 ManageElementSets : ITool
8
+ {
9
+ private sealed record Member(long Id, string UniqueId);
10
+ private sealed record Snapshot(string Id, string DocumentId, string Label, DateTimeOffset Expires, IReadOnlyList<Member> Members);
11
+ private static readonly Dictionary<string, Snapshot> Sets = new(StringComparer.Ordinal);
12
+ private const int MaxElements = 10000;
13
+ private const int MaxSets = 32;
14
+
15
+ public string Name => "manage_element_sets";
16
+ public string Label => "Manage Element Sets";
17
+ public string Tier => "advanced";
18
+ public IReadOnlyList<string> Effects => new[] { "session" };
19
+ public string Description => "Create, list, read or forget reusable element sets without changing the model or selection. A set captures matching host-element identities (up to 10,000), expires after 30 minutes, and is valid only for the exact open document and bridge session. Read pages report live values, deleted members and identity mismatches; membership is a snapshot and filters are not re-evaluated. At most 32 sets are retained. Use read IDs with ordinary tools after checking missing members.";
20
+ public object ParametersSchema => new
21
+ {
22
+ type = "object",
23
+ properties = new
24
+ {
25
+ action = new { type = "string", @enum = new[] { "create", "list", "read", "forget" } },
26
+ query = ElementQueryScope.Schema(),
27
+ label = new { type = "string", maxLength = 160 },
28
+ set_id = new { type = "string" },
29
+ offset = new { type = "integer", minimum = 0 },
30
+ limit = new { type = "integer", minimum = 1, maximum = 1000 },
31
+ parameter_names = new { type = "array", maxItems = 20, items = new { type = "string" } },
32
+ include_type_parameters = new { type = "boolean" },
33
+ },
34
+ required = new[] { "action" },
35
+ };
36
+
37
+ public object Execute(JsonElement args, ToolContext context)
38
+ {
39
+ var doc = context.Document ?? throw new NoActiveDocumentException();
40
+ string documentId = DocumentGuard.GetIdentity(doc);
41
+ var now = DateTimeOffset.UtcNow;
42
+ foreach (var expired in Sets.Where(pair => pair.Value.Expires <= now).Select(pair => pair.Key).ToArray()) Sets.Remove(expired);
43
+ string action = JsonArgs.GetString(args, "action") ?? "";
44
+ if (action == "list") return new
45
+ {
46
+ sets = Sets.Values.Where(set => set.DocumentId == documentId).OrderBy(set => set.Expires)
47
+ .Select(set => new { set_id = set.Id, label = set.Label, total_count = set.Members.Count, expires_at = set.Expires }).ToArray(),
48
+ };
49
+ if (action == "create")
50
+ {
51
+ if (Sets.Count >= MaxSets) throw new ArgumentException("Element-set capacity reached. Forget an existing set or wait for expiry.");
52
+ string label = JsonArgs.GetString(args, "label") ?? "Element set";
53
+ if (label.Length > 160) throw new ArgumentException("label must be at most 160 characters.");
54
+ var query = ElementQueryScope.Parse(args);
55
+ query["fields"] = new JsonArray("id"); query["count_only"] = true;
56
+ JsonElement Query() => JsonSerializer.SerializeToElement(((ToolOutput)new GetElements().Execute(JsonSerializer.SerializeToElement(query), context)!).Payload);
57
+ var count = Query();
58
+ int total = count.GetProperty("total_count").GetInt32();
59
+ if (total > MaxElements) throw new ArgumentException($"Query matches {total} elements; narrow the scope to at most {MaxElements}.");
60
+ query["count_only"] = false; query["limit"] = 1000;
61
+ var members = new List<Member>(total);
62
+ for (int offset = 0; offset < total; offset += 1000)
63
+ {
64
+ query["offset"] = offset;
65
+ foreach (var row in Query().GetProperty("elements").EnumerateArray())
66
+ {
67
+ var element = doc.GetElement(new ElementId(row.GetProperty("id").GetInt64()));
68
+ members.Add(new Member(element.Id.Value, element.UniqueId));
69
+ }
70
+ }
71
+ var snapshot = new Snapshot(Guid.NewGuid().ToString("N"), documentId, label, now.AddMinutes(30), members);
72
+ Sets.Add(snapshot.Id, snapshot);
73
+ return new { set_id = snapshot.Id, document_id = documentId, label, total_count = total, expires_at = snapshot.Expires,
74
+ warnings = count.TryGetProperty("warnings", out var warnings) ? (object)warnings.Clone() : null };
75
+ }
76
+ if (action is not ("read" or "forget")) throw new ArgumentException("Unknown action; use create, list, read or forget.");
77
+ string setId = JsonArgs.GetString(args, "set_id") ?? "";
78
+ if (!Sets.TryGetValue(setId, out var saved)) throw new ArgumentException("Unknown or expired set_id. Create a fresh set; IDs do not survive a bridge restart.");
79
+ if (saved.DocumentId != documentId) throw new ArgumentException("This element set belongs to a different open document. Activate its original document or create a fresh set.");
80
+ if (action == "forget") { Sets.Remove(setId); return new { set_id = setId, forgotten = true }; }
81
+ int start = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
82
+ int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
83
+ var names = JsonArgs.GetStringArray(args, "parameter_names");
84
+ if (names?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 identities.");
85
+ bool includeType = JsonArgs.GetBool(args, "include_type_parameters", false);
86
+ var page = saved.Members.Skip(start).Take(limit).ToArray();
87
+ var rows = new List<object>();
88
+ var missing = new List<object>();
89
+ foreach (var member in page)
90
+ {
91
+ var element = doc.GetElement(new ElementId(member.Id));
92
+ if (element == null || element.UniqueId != member.UniqueId)
93
+ {
94
+ missing.Add(new { id = member.Id, unique_id = member.UniqueId, reason = element == null ? "deleted" : "identity_changed" });
95
+ continue;
96
+ }
97
+ var row = ElementIdentity.Build(doc, element, ElementIdentity.Fields);
98
+ row["unique_id"] = member.UniqueId;
99
+ if (names is { Count: > 0 }) row["parameters"] = GetElementDetails.ProjectParameters(doc, element, names, includeType);
100
+ rows.Add(row);
101
+ }
102
+ return new { set_id = setId, document_id = documentId, snapshot_count = saved.Members.Count, expires_at = saved.Expires,
103
+ offset = start, visited_count = page.Length, returned_count = rows.Count,
104
+ next_offset = start + page.Length < saved.Members.Count ? (int?)(start + page.Length) : null, elements = rows, missing };
105
+ }
106
+ }
@@ -0,0 +1,164 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class ManageSchedules : ITool
7
+ {
8
+ public string Name => "manage_schedules";
9
+ public string Label => "Manage Schedules";
10
+ public string Tier => "advanced";
11
+ public bool Write => true;
12
+ public string Description => "Create or configure a regular schedule in one atomic step. Create requires category and name; optional area_scheme_id supports area schedules. Configure requires schedule_id. Discover add_fields using get_schedule_fields and pass the returned parameter_id + field_type pair, including Count when listed. Count also supports field_type Count without a parameter_id. update_fields, sort_fields and filters use schedule-local field_id from get_schedules. Supplied sort_fields/filters replace their entire lists; empty arrays clear them. Numeric measured filter values require a compatible unit; returned numeric filter values use internal units. Column widths require explicit length units and apply to both grid and sheet. preview=true commit-validates then rolls back; created schedule/field IDs in previews are temporary. Revision schedules, templates, embedded schedules and calculated/combined-field authoring are outside this tool.";
13
+ public object ParametersSchema => new
14
+ {
15
+ type = "object", properties = new
16
+ {
17
+ action = new { type = "string", @enum = new[] { "create", "configure" } },
18
+ schedule_id = new { type = "integer", minimum = 1 }, category = new { type = "string" }, name = new { type = "string", minLength = 1 }, area_scheme_id = new { type = "integer", minimum = 1 },
19
+ is_itemized = new { type = "boolean" }, preview = ModelEditInputs.PreviewSchema,
20
+ add_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
21
+ {
22
+ parameter_id = new { type = "integer", description = "Use the ID returned by get_schedule_fields, including negative built-in IDs. Optional for Count; when supplied, the pair must be eligible for the schedule." }, field_type = new { type = "string" },
23
+ heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
24
+ }, required = new[] { "field_type" } } },
25
+ update_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
26
+ {
27
+ field_id = new { type = "integer", minimum = 0 }, heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
28
+ }, required = new[] { "field_id" } } },
29
+ sort_fields = new { type = "array", maxItems = 4, items = new { type = "object", properties = new { field_id = new { type = "integer", minimum = 0 }, descending = new { type = "boolean" }, show_header = new { type = "boolean" } }, required = new[] { "field_id" } } },
30
+ filters = new { type = "array", maxItems = 8, items = new { type = "object", properties = new
31
+ {
32
+ field_id = new { type = "integer", minimum = 0 }, comparison = new { type = "string", @enum = new[] { "equals", "not_equals", "contains", "greater_than", "less_than" } },
33
+ value_type = new { type = "string", @enum = new[] { "string", "number", "integer", "element_id" } }, value = new { description = "Filter value matching value_type." }, unit = new { type = "string", description = "Required for measured numbers; compatible forge unit name, e.g. meters, squareMeters, degrees." },
34
+ }, required = new[] { "field_id", "comparison", "value_type", "value" } } },
35
+ }, required = new[] { "action" },
36
+ };
37
+
38
+ public object Execute(JsonElement args, ToolContext context)
39
+ {
40
+ var doc = context.Document ?? throw new NoActiveDocumentException();
41
+ string action = JsonArgs.GetString(args, "action") ?? "";
42
+ if (action is not ("create" or "configure")) throw new ArgumentException("action must be create or configure.");
43
+ var additions = Array(args, "add_fields", 50); var updates = Array(args, "update_fields", 50);
44
+ var sorts = Array(args, "sort_fields", 4); var filters = Array(args, "filters", 8);
45
+ return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
46
+ {
47
+ ViewSchedule schedule;
48
+ if (action == "create")
49
+ {
50
+ string category = JsonArgs.GetString(args, "category") ?? throw new ArgumentException("category is required for creation.");
51
+ var categoryId = CategoryResolver.Resolve(doc, category);
52
+ schedule = JsonArgs.GetLong(args, "area_scheme_id") is long area ? ViewSchedule.CreateSchedule(doc, categoryId, new ElementId(area)) : ViewSchedule.CreateSchedule(doc, categoryId);
53
+ if (!args.TryGetProperty("name", out _)) throw new ArgumentException("name is required for creation.");
54
+ }
55
+ else
56
+ {
57
+ if (args.TryGetProperty("category", out _) || args.TryGetProperty("area_scheme_id", out _)) throw new ArgumentException("category and area_scheme_id are creation-only.");
58
+ schedule = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "schedule_id") ?? 0)) as ViewSchedule ?? throw new ArgumentException("schedule_id is not a schedule.");
59
+ }
60
+ if (schedule.IsTemplate || schedule.IsTitleblockRevisionSchedule || schedule.Definition.IsEmbedded) throw new ArgumentException("Use a regular, non-template schedule.");
61
+ if (args.TryGetProperty("name", out var name))
62
+ {
63
+ if (name.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(name.GetString())) throw new ArgumentException("name must be nonempty.");
64
+ schedule.Name = name.GetString()!;
65
+ }
66
+ var definition = schedule.Definition;
67
+ var added = new List<int>();
68
+ foreach (var input in additions)
69
+ {
70
+ string typeName = JsonArgs.GetString(input, "field_type") ?? "";
71
+ if (typeName.Length == 0 || !char.IsLetter(typeName[0]) || !Enum.TryParse<ScheduleFieldType>(typeName, false, out var type) || !Enum.IsDefined(type)) throw new ArgumentException("Use a field_type from get_schedule_fields, or Count.");
72
+ ScheduleField field;
73
+ if (type == ScheduleFieldType.Count && !input.TryGetProperty("parameter_id", out _))
74
+ {
75
+ field = definition.AddField(type);
76
+ }
77
+ else
78
+ {
79
+ long parameterId = JsonArgs.GetLong(input, "parameter_id") ?? throw new ArgumentException("parameter_id is required for this field.");
80
+ var eligible = definition.GetSchedulableFields().FirstOrDefault(f => f.ParameterId.Value == parameterId && f.FieldType == type) ?? throw new ArgumentException("That parameter_id/field_type pair is not eligible for this schedule.");
81
+ field = definition.AddField(eligible);
82
+ }
83
+ ConfigureField(field, input); added.Add(field.FieldId.IntegerValue);
84
+ }
85
+ var seen = new HashSet<int>();
86
+ foreach (var input in updates)
87
+ {
88
+ var id = FieldId(input);
89
+ if (!seen.Add(id.IntegerValue)) throw new ArgumentException("update_fields contains a duplicate field_id.");
90
+ ConfigureField(definition.GetField(id), input);
91
+ }
92
+ if (args.TryGetProperty("is_itemized", out _)) definition.IsItemized = Bool(args, "is_itemized");
93
+ if (args.TryGetProperty("sort_fields", out _))
94
+ definition.SetSortGroupFields(sorts.Select(input => new ScheduleSortGroupField(FieldId(input), Bool(input, "descending") ? ScheduleSortOrder.Descending : ScheduleSortOrder.Ascending) { ShowHeader = Bool(input, "show_header") }).ToList());
95
+ if (args.TryGetProperty("filters", out _)) definition.SetFilters(filters.Select(input => Filter(definition, input)).ToList());
96
+ doc.Regenerate();
97
+ return new() { ["schedule_id"] = schedule.Id.Value, ["unique_id"] = schedule.UniqueId, ["name"] = schedule.Name,
98
+ ["created"] = action == "create", ["id_is_temporary"] = action == "create" && JsonArgs.GetBool(args, "preview", false),
99
+ ["added_field_ids"] = added, ["added_field_ids_are_temporary"] = JsonArgs.GetBool(args, "preview", false),
100
+ ["is_itemized"] = definition.IsItemized, ["field_count"] = definition.GetFieldCount(), ["filter_count"] = definition.GetFilterCount(), ["sort_count"] = definition.GetSortGroupFieldCount() };
101
+ }) }).Payload;
102
+ }
103
+
104
+ private static List<JsonElement> Array(JsonElement args, string name, int max)
105
+ {
106
+ if (!args.TryGetProperty(name, out var value)) return new();
107
+ if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > max) throw new ArgumentException($"{name} must be an array of at most {max} entries.");
108
+ var items = value.EnumerateArray().ToList();
109
+ if (items.Any(item => item.ValueKind != JsonValueKind.Object)) throw new ArgumentException($"{name} entries must be objects.");
110
+ return items;
111
+ }
112
+ private static bool Bool(JsonElement args, string name)
113
+ {
114
+ if (!args.TryGetProperty(name, out var value)) return false;
115
+ if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException($"{name} must be boolean.");
116
+ return value.GetBoolean();
117
+ }
118
+ private static ScheduleFieldId FieldId(JsonElement input)
119
+ {
120
+ long id = JsonArgs.GetLong(input, "field_id") ?? -1;
121
+ return id is >= 0 and <= int.MaxValue ? new ScheduleFieldId((int)id) : throw new ArgumentException("field_id must be a nonnegative schedule-local ID.");
122
+ }
123
+ private static void ConfigureField(ScheduleField field, JsonElement input)
124
+ {
125
+ if (input.TryGetProperty("heading", out var heading)) field.ColumnHeading = heading.ValueKind == JsonValueKind.String ? heading.GetString()! : throw new ArgumentException("heading must be a string.");
126
+ if (input.TryGetProperty("hidden", out _)) field.IsHidden = Bool(input, "hidden");
127
+ if (input.TryGetProperty("width", out _))
128
+ {
129
+ double width = ModelEditInputs.Number(input, "width") * ModelEditInputs.LengthScale(input);
130
+ if (width <= 0) throw new ArgumentException("Column width must be positive.");
131
+ field.GridColumnWidth = width; field.SheetColumnWidth = width;
132
+ }
133
+ }
134
+ private static ScheduleFilter Filter(ScheduleDefinition definition, JsonElement input)
135
+ {
136
+ var id = FieldId(input); var field = definition.GetField(id);
137
+ var comparison = JsonArgs.GetString(input, "comparison") switch
138
+ {
139
+ "equals" => ScheduleFilterType.Equal, "not_equals" => ScheduleFilterType.NotEqual, "contains" => ScheduleFilterType.Contains,
140
+ "greater_than" => ScheduleFilterType.GreaterThan, "less_than" => ScheduleFilterType.LessThan, _ => throw new ArgumentException("Unknown comparison."),
141
+ };
142
+ if (comparison == ScheduleFilterType.Contains ? !definition.CanFilterBySubstring(id) : !definition.CanFilterByValue(id)) throw new ArgumentException("This field does not support that filter operation.");
143
+ if (!input.TryGetProperty("value", out var value)) throw new ArgumentException("Filter value is required.");
144
+ string? type = JsonArgs.GetString(input, "value_type");
145
+ if (type == "string" && value.ValueKind == JsonValueKind.String) return new ScheduleFilter(id, comparison, value.GetString()!);
146
+ if (type == "integer" && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int integer)) return new ScheduleFilter(id, comparison, integer);
147
+ if (type == "element_id" && value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long elementId)) return new ScheduleFilter(id, comparison, new ElementId(elementId));
148
+ if (type == "number")
149
+ {
150
+ double number = ModelEditInputs.Number(input, "value");
151
+ var spec = field.GetSpecTypeId(); string? unit = JsonArgs.GetString(input, "unit");
152
+ if (UnitUtils.IsMeasurableSpec(spec))
153
+ {
154
+ if (string.IsNullOrWhiteSpace(unit)) throw new ArgumentException("Measured numeric filters require an explicit unit.");
155
+ var unitId = UnitResolver.Resolve(unit);
156
+ if (!UnitUtils.IsValidUnit(spec, unitId)) throw new ArgumentException("Filter unit does not match the field's specification.");
157
+ number = UnitUtils.ConvertToInternalUnits(number, unitId);
158
+ }
159
+ else if (unit != null) throw new ArgumentException("This numeric field has no measurable unit.");
160
+ return new ScheduleFilter(id, comparison, number);
161
+ }
162
+ throw new ArgumentException("Filter value does not match value_type.");
163
+ }
164
+ }
@@ -11,7 +11,8 @@ namespace RevitBridge.Tools
11
11
  /// </summary>
12
12
  internal sealed class ManageSelection : ITool
13
13
  {
14
- public string Name => "manage_selection";
14
+ public string Name => "manage_selection";
15
+ public IReadOnlyList<string> Effects => new[] { "ui", "model" };
15
16
  public string Label => "Manage Selection";
16
17
  public string Description => "Manage the Revit selection and view focus. action 'get' returns the currently selected elements (id, name, category, typeName, levelId); 'set'/'add'/'remove' change the selection with element_ids; 'clear' empties it; 'zoom' zooms open views to element_ids (default: the current selection). isolate_in_view=true additionally applies Revit's Temporary Hide/Isolate to the affected elements in the active view — temporary view state, not a model change (Revit needs a brief internal transaction to toggle it); with an empty target set (action 'clear', or 'get' with nothing selected) it resets the temporary isolate instead. Selection and zoom never modify model data.";
17
18
 
@@ -0,0 +1,97 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class ManageSheetPlacements : ITool
7
+ {
8
+ public string Name => "manage_sheet_placements";
9
+ public string Label => "Manage Sheet Placements";
10
+ public string Tier => "advanced";
11
+ public bool Write => true;
12
+ public string Description => "List, place or move viewports and schedule instances on a sheet. Positions are paper-space sheet coordinates with explicit length units, [x,y,0]; never multiply by view scale. For viewports position is the box center excluding its label; for schedules it is the insertion point. Place requires sheet_id/view_id, move requires placement_id. Viewports may rotate none/clockwise/counterclockwise; schedule rotation is not supported. preview=true commit-validates then rolls back; new placement IDs are temporary. List includes both viewports and schedules with paging. Exact document identity is required for every action.";
13
+ public object ParametersSchema => new
14
+ {
15
+ type = "object", properties = new
16
+ {
17
+ action = new { type = "string", @enum = new[] { "list", "place", "move" } },
18
+ sheet_id = new { type = "integer", minimum = 1 }, view_id = new { type = "integer", minimum = 1 }, placement_id = new { type = "integer", minimum = 1 },
19
+ position = ModelEditInputs.VectorSchema("Sheet paper-space [x,y,0] in unit."), unit = ModelEditInputs.LengthUnitSchema,
20
+ rotation = new { type = "string", @enum = new[] { "none", "clockwise", "counterclockwise" } },
21
+ offset = new { type = "integer", minimum = 0 }, limit = new { type = "integer", minimum = 1, maximum = 200 }, preview = ModelEditInputs.PreviewSchema,
22
+ }, required = new[] { "action" },
23
+ };
24
+ public object Execute(JsonElement args, ToolContext context)
25
+ {
26
+ var doc = context.Document ?? throw new NoActiveDocumentException();
27
+ string action = JsonArgs.GetString(args, "action") ?? "";
28
+ ElementId Id(string name) => new(JsonArgs.GetLong(args, name) is long id && id > 0 ? id : throw new ArgumentException($"{name} must be a positive ID."));
29
+ if (action == "list")
30
+ {
31
+ var sheet = doc.GetElement(Id("sheet_id")) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
32
+ var items = new FilteredElementCollector(doc).OwnedByView(sheet.Id).WhereElementIsNotElementType()
33
+ .Where(e => e is Viewport or ScheduleSheetInstance).OrderBy(e => e.Id.Value).ToArray();
34
+ int offset = JsonArgs.GetInt(args, "offset", 0), limit = JsonArgs.GetInt(args, "limit", 100);
35
+ if (offset < 0 || limit is < 1 or > 200) throw new ArgumentException("Invalid paging range.");
36
+ var page = items.Skip(offset).Take(limit).Select(Describe).ToArray();
37
+ return new { sheet_id = sheet.Id.Value, total_count = items.Length, offset, returned_count = page.Length, next_offset = offset + page.Length < items.Length ? (int?)(offset + page.Length) : null, placements = page, coordinate_system = "sheet_paper", unit = "feet" };
38
+ }
39
+ if (action is not ("place" or "move")) throw new ArgumentException("action must be list, place or move.");
40
+ var point = ModelEditInputs.Vector(args, "position").Multiply(ModelEditInputs.LengthScale(args));
41
+ if (Math.Abs(point.Z) > 1e-9) throw new ArgumentException("Sheet position Z must be zero.");
42
+ var rotation = JsonArgs.GetString(args, "rotation") switch
43
+ {
44
+ null => (ViewportRotation?)null, "none" => ViewportRotation.None, "clockwise" => ViewportRotation.Clockwise,
45
+ "counterclockwise" => ViewportRotation.Counterclockwise, _ => throw new ArgumentException("Invalid rotation."),
46
+ };
47
+ return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
48
+ {
49
+ Element placement;
50
+ object? before = null;
51
+ if (action == "place")
52
+ {
53
+ var sheet = doc.GetElement(Id("sheet_id")) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
54
+ if (sheet.IsPlaceholder) throw new ArgumentException("Cannot place content on a placeholder sheet.");
55
+ var view = doc.GetElement(Id("view_id")) as View ?? throw new ArgumentException("view_id is not a view or schedule.");
56
+ if (view is ViewSchedule schedule)
57
+ {
58
+ if (rotation != null) throw new ArgumentException("Schedules do not support viewport rotation.");
59
+ placement = ScheduleSheetInstance.Create(doc, sheet.Id, schedule.Id, point);
60
+ }
61
+ else
62
+ {
63
+ if (!Viewport.CanAddViewToSheet(doc, sheet.Id, view.Id)) throw new ArgumentException("This view cannot be added to that sheet; it may already be placed.");
64
+ placement = Viewport.Create(doc, sheet.Id, view.Id, point);
65
+ }
66
+ }
67
+ else
68
+ {
69
+ placement = doc.GetElement(Id("placement_id")) ?? throw new ArgumentException("placement_id was not found.");
70
+ if (args.TryGetProperty("sheet_id", out _) && placement.OwnerViewId != Id("sheet_id")) throw new ArgumentException("The placement does not belong to the expected sheet_id.");
71
+ if (placement.Pinned) throw new ArgumentException("Placement is pinned; it was not unpinned.");
72
+ before = Describe(placement);
73
+ if (placement is Viewport viewport) viewport.SetBoxCenter(point);
74
+ else if (placement is ScheduleSheetInstance schedule)
75
+ {
76
+ if (rotation != null) throw new ArgumentException("Schedules do not support viewport rotation.");
77
+ schedule.Point = point;
78
+ }
79
+ else throw new ArgumentException("placement_id must identify a viewport or schedule instance.");
80
+ }
81
+ if (rotation != null && placement is Viewport target) target.Rotation = rotation.Value;
82
+ doc.Regenerate();
83
+ if (placement is Viewport positioned) { positioned.SetBoxCenter(point); doc.Regenerate(); }
84
+ return new() { ["before"] = before, ["placement"] = Describe(placement), ["created"] = action == "place", ["id_is_temporary"] = action == "place" && JsonArgs.GetBool(args, "preview", false) };
85
+ }) }).Payload;
86
+ }
87
+ private static object Describe(Element element)
88
+ {
89
+ static double[] Point(XYZ p) => new[] { p.X, p.Y, p.Z };
90
+ return element switch
91
+ {
92
+ Viewport v => new { id = v.Id.Value, unique_id = v.UniqueId, kind = "viewport", sheet_id = v.SheetId.Value, view_id = v.ViewId.Value, position = Point(v.GetBoxCenter()), position_kind = "box_center_excluding_label", rotation = v.Rotation.ToString(), unit = "feet" } as object,
93
+ ScheduleSheetInstance s => new { id = s.Id.Value, unique_id = s.UniqueId, kind = "schedule", sheet_id = s.OwnerViewId.Value, view_id = s.ScheduleId.Value, position = Point(s.Point), position_kind = "insertion_point", unit = "feet" },
94
+ _ => throw new ArgumentException("Element is not a sheet placement."),
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,63 @@
1
+ using System.Text.Json;
2
+ using Autodesk.Revit.DB;
3
+
4
+ namespace RevitBridge.Tools;
5
+
6
+ internal sealed class ManageSheets : ITool
7
+ {
8
+ public string Name => "manage_sheets";
9
+ public string Label => "Manage Sheets";
10
+ public string Tier => "advanced";
11
+ public bool Write => true;
12
+ public string Description => "Create or rename/renumber a drawing sheet. Creation requires name and number; optional titleblock_type_id chooses a loaded titleblock FamilySymbol, otherwise creates a sheet without a titleblock. Update requires sheet_id and accepts name/number. Revit validates unique sheet numbers and valid text; sheet names may repeat; the call rolls back on failure. preview=true commit-validates and rolls back, and new preview sheet IDs are temporary. Use manage_sheet_placements for views/schedules, get_elements for querying sheets, and delete_elements for removal.";
13
+ public object ParametersSchema => new
14
+ {
15
+ type = "object", properties = new
16
+ {
17
+ action = new { type = "string", @enum = new[] { "create", "update" } },
18
+ sheet_id = new { type = "integer", minimum = 1 }, name = new { type = "string", minLength = 1 }, number = new { type = "string", minLength = 1 },
19
+ titleblock_type_id = new { type = "integer", minimum = 1, description = "Creation only; omitted means no titleblock." }, preview = ModelEditInputs.PreviewSchema,
20
+ }, required = new[] { "action" },
21
+ };
22
+ public object Execute(JsonElement args, ToolContext context)
23
+ {
24
+ var doc = context.Document ?? throw new NoActiveDocumentException();
25
+ string action = JsonArgs.GetString(args, "action") ?? "";
26
+ if (action is not ("create" or "update")) throw new ArgumentException("action must be create or update.");
27
+ string? ReadText(string key)
28
+ {
29
+ if (!args.TryGetProperty(key, out var value)) return null;
30
+ if (value.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(value.GetString())) throw new ArgumentException($"{key} must be a nonempty string.");
31
+ return value.GetString();
32
+ }
33
+ string? name = ReadText("name"), number = ReadText("number");
34
+ if (action == "create" && (name == null || number == null)) throw new ArgumentException("Sheet creation requires name and number.");
35
+ if (action == "update" && args.TryGetProperty("titleblock_type_id", out _)) throw new ArgumentException("titleblock_type_id is only supported when creating a sheet; use change_element_types on an existing titleblock instance.");
36
+ return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
37
+ {
38
+ ViewSheet sheet;
39
+ object? before = null;
40
+ if (action == "create")
41
+ {
42
+ var typeId = ElementId.InvalidElementId;
43
+ if (args.TryGetProperty("titleblock_type_id", out _))
44
+ {
45
+ typeId = new ElementId(JsonArgs.GetLong(args, "titleblock_type_id") ?? 0);
46
+ var type = doc.GetElement(typeId) as FamilySymbol;
47
+ if (type?.Category?.Id.Value != (long)BuiltInCategory.OST_TitleBlocks) throw new ArgumentException("titleblock_type_id must identify a titleblock FamilySymbol.");
48
+ }
49
+ sheet = ViewSheet.Create(doc, typeId);
50
+ }
51
+ else
52
+ {
53
+ sheet = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "sheet_id") ?? 0)) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
54
+ before = Describe(sheet);
55
+ }
56
+ if (number != null) sheet.SheetNumber = number;
57
+ if (name != null) sheet.Name = name;
58
+ doc.Regenerate();
59
+ return new() { ["before"] = before, ["sheet"] = Describe(sheet), ["created"] = action == "create", ["id_is_temporary"] = action == "create" && JsonArgs.GetBool(args, "preview", false) };
60
+ }) }).Payload;
61
+ }
62
+ private static object Describe(ViewSheet sheet) => new { id = sheet.Id.Value, unique_id = sheet.UniqueId, name = sheet.Name, number = sheet.SheetNumber, placeholder = sheet.IsPlaceholder };
63
+ }