pi-revit 0.4.0 → 0.5.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 (119) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +465 -430
  3. package/README.md +604 -548
  4. package/bin/pi-revit.js +9 -9
  5. package/docs/architecture.md +271 -0
  6. package/docs/evaluation.md +434 -0
  7. package/docs/invariants.json +147 -0
  8. package/extensions/pi-revit/completion-monitor.ts +55 -0
  9. package/extensions/pi-revit/contracts.ts +146 -0
  10. package/extensions/pi-revit/discovery.ts +93 -0
  11. package/extensions/pi-revit/index.ts +342 -255
  12. package/extensions/pi-revit/instance-router.ts +86 -86
  13. package/extensions/pi-revit/platform-prompt.ts +40 -0
  14. package/extensions/pi-revit/scope-monitor.ts +114 -0
  15. package/extensions/pi-revit/script-library.ts +144 -144
  16. package/extensions/pi-revit/tool-catalog.ts +113 -14
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +8 -2
  20. package/scripts/build.ps1 +9 -9
  21. package/scripts/check-sdk.ps1 +66 -66
  22. package/scripts/check-tool-documentation.mjs +287 -0
  23. package/scripts/deploy.ps1 +16 -16
  24. package/scripts/generate-contracts.mjs +80 -0
  25. package/scripts/lib/platform.mjs +226 -0
  26. package/scripts/test-extension.mjs +15 -0
  27. package/skills/pi-revit/SKILL.md +30 -218
  28. package/skills/pi-revit/contracts.generated.json +3524 -0
  29. package/skills/pi-revit/references/execution-rules.md +41 -0
  30. package/skills/pi-revit/references/model-audit-export.md +38 -27
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +37 -26
  33. package/skills/pi-revit/references/tool-index.md +89 -0
  34. package/skills/pi-revit/references/tools/capture_view.md +62 -0
  35. package/skills/pi-revit/references/tools/change_element_types.md +65 -0
  36. package/skills/pi-revit/references/tools/create_tags.md +85 -0
  37. package/skills/pi-revit/references/tools/delete_elements.md +66 -0
  38. package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
  39. package/skills/pi-revit/references/tools/export_documents.md +75 -0
  40. package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
  41. package/skills/pi-revit/references/tools/get_element_details.md +66 -0
  42. package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
  43. package/skills/pi-revit/references/tools/get_element_types.md +67 -0
  44. package/skills/pi-revit/references/tools/get_elements.md +87 -0
  45. package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
  46. package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
  47. package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
  48. package/skills/pi-revit/references/tools/get_model_health.md +53 -0
  49. package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
  50. package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
  51. package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
  52. package/skills/pi-revit/references/tools/get_schedules.md +71 -0
  53. package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
  54. package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
  55. package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
  56. package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
  57. package/skills/pi-revit/references/tools/manage_selection.md +66 -0
  58. package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
  59. package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
  60. package/skills/pi-revit/references/tools/manage_views.md +95 -0
  61. package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
  62. package/skills/pi-revit/references/tools/open_view.md +59 -0
  63. package/skills/pi-revit/references/tools/ping.md +41 -0
  64. package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
  65. package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
  66. package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
  67. package/skills/pi-revit/references/tools/set_parameters.md +75 -0
  68. package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
  69. package/skills/pi-revit/references/tools/transform_elements.md +79 -0
  70. package/skills/pi-revit/references/visual-verification.md +36 -0
  71. package/skills/pi-revit/tool-manifest.json +338 -0
  72. package/src/Revit/BridgeServer.cs +93 -87
  73. package/src/Revit/OperationStore.cs +178 -178
  74. package/src/Revit/ToolRegistry.cs +88 -57
  75. package/src/Revit/Tools/CaptureView.cs +10 -2
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -95
  79. package/src/Revit/Tools/DeleteElements.cs +53 -44
  80. package/src/Revit/Tools/DocumentGuard.cs +74 -64
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -27
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
  85. package/src/Revit/Tools/ExportDocuments.cs +129 -121
  86. package/src/Revit/Tools/GetElementDetails.cs +37 -41
  87. package/src/Revit/Tools/GetElementRelationships.cs +82 -76
  88. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  89. package/src/Revit/Tools/GetElements.cs +75 -86
  90. package/src/Revit/Tools/GetLinkedElements.cs +89 -82
  91. package/src/Revit/Tools/GetLinkedModels.cs +73 -66
  92. package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
  93. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  94. package/src/Revit/Tools/GetModelOverview.cs +185 -158
  95. package/src/Revit/Tools/GetScheduleFields.cs +44 -37
  96. package/src/Revit/Tools/GetSchedules.cs +96 -89
  97. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  98. package/src/Revit/Tools/InheritedState.cs +144 -0
  99. package/src/Revit/Tools/ManageElementSets.cs +114 -106
  100. package/src/Revit/Tools/ManageSchedules.cs +174 -164
  101. package/src/Revit/Tools/ManageSelection.cs +45 -37
  102. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
  103. package/src/Revit/Tools/ManageSheets.cs +72 -63
  104. package/src/Revit/Tools/ManageViews.cs +115 -100
  105. package/src/Revit/Tools/MeasureGeometry.cs +60 -54
  106. package/src/Revit/Tools/ModelChanges.cs +154 -0
  107. package/src/Revit/Tools/ModelEditBatch.cs +105 -102
  108. package/src/Revit/Tools/ModelEditInputs.cs +49 -49
  109. package/src/Revit/Tools/OpenView.cs +9 -2
  110. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  111. package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
  112. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  113. package/src/Revit/Tools/SetParameters.cs +60 -79
  114. package/src/Revit/Tools/SpatialBounds.cs +30 -30
  115. package/src/Revit/Tools/SummarizeElements.cs +94 -87
  116. package/src/Revit/Tools/ToolContract.cs +48 -0
  117. package/src/Revit/Tools/ToolSupport.cs +5 -1
  118. package/src/Revit/Tools/TransformElements.cs +72 -57
  119. package/workspace/AGENTS.md +26 -20
@@ -1,164 +1,174 @@
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
- }
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 IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
10
+ public IReadOnlyList<string> Keywords => new[] { "create schedule", "schedule", "add field", "sort", "filter schedule", "quantities", "takeoff" };
11
+ public IReadOnlyList<ToolLimit> Limits => new[]
12
+ {
13
+ new ToolLimit("Calculated (formula) fields", "revit_unsupported", "Revit 2025 API exposes ScheduleFieldType.Formula but no member to author a formula"),
14
+ new ToolLimit("Combined-parameter fields", "api", "ScheduleDefinition.InsertCombinedParameterField"),
15
+ new ToolLimit("Schedule templates, titleblock revision schedules and embedded schedules", "api", "ViewSchedule and ScheduleDefinition members; verify with search_api_docs"),
16
+ new ToolLimit("Placing a schedule on a sheet", "tool", "manage_sheet_placements"),
17
+ };
18
+ public string? Verification => "reread";
19
+ public string Label => "Manage Schedules";
20
+ public string Tier => "advanced";
21
+ public bool Write => true;
22
+ 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.";
23
+ public object ParametersSchema => new
24
+ {
25
+ type = "object", properties = new
26
+ {
27
+ action = new { type = "string", @enum = new[] { "create", "configure" } },
28
+ 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 },
29
+ is_itemized = new { type = "boolean" }, preview = ModelEditInputs.PreviewSchema,
30
+ add_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
31
+ {
32
+ 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" },
33
+ heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
34
+ }, required = new[] { "field_type" } } },
35
+ update_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
36
+ {
37
+ field_id = new { type = "integer", minimum = 0 }, heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
38
+ }, required = new[] { "field_id" } } },
39
+ 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" } } },
40
+ filters = new { type = "array", maxItems = 8, items = new { type = "object", properties = new
41
+ {
42
+ field_id = new { type = "integer", minimum = 0 }, comparison = new { type = "string", @enum = new[] { "equals", "not_equals", "contains", "greater_than", "less_than" } },
43
+ 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." },
44
+ }, required = new[] { "field_id", "comparison", "value_type", "value" } } },
45
+ }, required = new[] { "action" },
46
+ };
47
+
48
+ public object Execute(JsonElement args, ToolContext context)
49
+ {
50
+ var doc = context.Document ?? throw new NoActiveDocumentException();
51
+ string action = JsonArgs.GetString(args, "action") ?? "";
52
+ if (action is not ("create" or "configure")) throw new ArgumentException("action must be create or configure.");
53
+ var additions = Array(args, "add_fields", 50); var updates = Array(args, "update_fields", 50);
54
+ var sorts = Array(args, "sort_fields", 4); var filters = Array(args, "filters", 8);
55
+ return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
56
+ {
57
+ ViewSchedule schedule;
58
+ if (action == "create")
59
+ {
60
+ string category = JsonArgs.GetString(args, "category") ?? throw new ArgumentException("category is required for creation.");
61
+ var categoryId = CategoryResolver.Resolve(doc, category);
62
+ schedule = JsonArgs.GetLong(args, "area_scheme_id") is long area ? ViewSchedule.CreateSchedule(doc, categoryId, new ElementId(area)) : ViewSchedule.CreateSchedule(doc, categoryId);
63
+ if (!args.TryGetProperty("name", out _)) throw new ArgumentException("name is required for creation.");
64
+ }
65
+ else
66
+ {
67
+ if (args.TryGetProperty("category", out _) || args.TryGetProperty("area_scheme_id", out _)) throw new ArgumentException("category and area_scheme_id are creation-only.");
68
+ schedule = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "schedule_id") ?? 0)) as ViewSchedule ?? throw new ArgumentException("schedule_id is not a schedule.");
69
+ }
70
+ if (schedule.IsTemplate || schedule.IsTitleblockRevisionSchedule || schedule.Definition.IsEmbedded) throw new ArgumentException("Use a regular, non-template schedule.");
71
+ if (args.TryGetProperty("name", out var name))
72
+ {
73
+ if (name.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(name.GetString())) throw new ArgumentException("name must be nonempty.");
74
+ ElementNames.Assign(schedule, name.GetString()!);
75
+ }
76
+ var definition = schedule.Definition;
77
+ var added = new List<int>();
78
+ foreach (var input in additions)
79
+ {
80
+ string typeName = JsonArgs.GetString(input, "field_type") ?? "";
81
+ 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.");
82
+ ScheduleField field;
83
+ if (type == ScheduleFieldType.Count && !input.TryGetProperty("parameter_id", out _))
84
+ {
85
+ field = definition.AddField(type);
86
+ }
87
+ else
88
+ {
89
+ long parameterId = JsonArgs.GetLong(input, "parameter_id") ?? throw new ArgumentException("parameter_id is required for this field.");
90
+ 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.");
91
+ field = definition.AddField(eligible);
92
+ }
93
+ ConfigureField(field, input); added.Add(field.FieldId.IntegerValue);
94
+ }
95
+ var seen = new HashSet<int>();
96
+ foreach (var input in updates)
97
+ {
98
+ var id = FieldId(input);
99
+ if (!seen.Add(id.IntegerValue)) throw new ArgumentException("update_fields contains a duplicate field_id.");
100
+ ConfigureField(definition.GetField(id), input);
101
+ }
102
+ if (args.TryGetProperty("is_itemized", out _)) definition.IsItemized = Bool(args, "is_itemized");
103
+ if (args.TryGetProperty("sort_fields", out _))
104
+ definition.SetSortGroupFields(sorts.Select(input => new ScheduleSortGroupField(FieldId(input), Bool(input, "descending") ? ScheduleSortOrder.Descending : ScheduleSortOrder.Ascending) { ShowHeader = Bool(input, "show_header") }).ToList());
105
+ if (args.TryGetProperty("filters", out _)) definition.SetFilters(filters.Select(input => Filter(definition, input)).ToList());
106
+ doc.Regenerate();
107
+ return new() { ["schedule_id"] = schedule.Id.Value, ["unique_id"] = schedule.UniqueId, ["name"] = schedule.Name,
108
+ ["created"] = action == "create", ["id_is_temporary"] = action == "create" && JsonArgs.GetBool(args, "preview", false),
109
+ ["added_field_ids"] = added, ["added_field_ids_are_temporary"] = JsonArgs.GetBool(args, "preview", false),
110
+ ["is_itemized"] = definition.IsItemized, ["field_count"] = definition.GetFieldCount(), ["filter_count"] = definition.GetFilterCount(), ["sort_count"] = definition.GetSortGroupFieldCount() };
111
+ }) }).Payload;
112
+ }
113
+
114
+ private static List<JsonElement> Array(JsonElement args, string name, int max)
115
+ {
116
+ if (!args.TryGetProperty(name, out var value)) return new();
117
+ if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > max) throw new ArgumentException($"{name} must be an array of at most {max} entries.");
118
+ var items = value.EnumerateArray().ToList();
119
+ if (items.Any(item => item.ValueKind != JsonValueKind.Object)) throw new ArgumentException($"{name} entries must be objects.");
120
+ return items;
121
+ }
122
+ private static bool Bool(JsonElement args, string name)
123
+ {
124
+ if (!args.TryGetProperty(name, out var value)) return false;
125
+ if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException($"{name} must be boolean.");
126
+ return value.GetBoolean();
127
+ }
128
+ private static ScheduleFieldId FieldId(JsonElement input)
129
+ {
130
+ long id = JsonArgs.GetLong(input, "field_id") ?? -1;
131
+ return id is >= 0 and <= int.MaxValue ? new ScheduleFieldId((int)id) : throw new ArgumentException("field_id must be a nonnegative schedule-local ID.");
132
+ }
133
+ private static void ConfigureField(ScheduleField field, JsonElement input)
134
+ {
135
+ if (input.TryGetProperty("heading", out var heading)) field.ColumnHeading = heading.ValueKind == JsonValueKind.String ? heading.GetString()! : throw new ArgumentException("heading must be a string.");
136
+ if (input.TryGetProperty("hidden", out _)) field.IsHidden = Bool(input, "hidden");
137
+ if (input.TryGetProperty("width", out _))
138
+ {
139
+ double width = ModelEditInputs.Number(input, "width") * ModelEditInputs.LengthScale(input);
140
+ if (width <= 0) throw new ArgumentException("Column width must be positive.");
141
+ field.GridColumnWidth = width; field.SheetColumnWidth = width;
142
+ }
143
+ }
144
+ private static ScheduleFilter Filter(ScheduleDefinition definition, JsonElement input)
145
+ {
146
+ var id = FieldId(input); var field = definition.GetField(id);
147
+ var comparison = JsonArgs.GetString(input, "comparison") switch
148
+ {
149
+ "equals" => ScheduleFilterType.Equal, "not_equals" => ScheduleFilterType.NotEqual, "contains" => ScheduleFilterType.Contains,
150
+ "greater_than" => ScheduleFilterType.GreaterThan, "less_than" => ScheduleFilterType.LessThan, _ => throw new ArgumentException("Unknown comparison."),
151
+ };
152
+ if (comparison == ScheduleFilterType.Contains ? !definition.CanFilterBySubstring(id) : !definition.CanFilterByValue(id)) throw new ArgumentException("This field does not support that filter operation.");
153
+ if (!input.TryGetProperty("value", out var value)) throw new ArgumentException("Filter value is required.");
154
+ string? type = JsonArgs.GetString(input, "value_type");
155
+ if (type == "string" && value.ValueKind == JsonValueKind.String) return new ScheduleFilter(id, comparison, value.GetString()!);
156
+ if (type == "integer" && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int integer)) return new ScheduleFilter(id, comparison, integer);
157
+ if (type == "element_id" && value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long elementId)) return new ScheduleFilter(id, comparison, new ElementId(elementId));
158
+ if (type == "number")
159
+ {
160
+ double number = ModelEditInputs.Number(input, "value");
161
+ var spec = field.GetSpecTypeId(); string? unit = JsonArgs.GetString(input, "unit");
162
+ if (UnitUtils.IsMeasurableSpec(spec))
163
+ {
164
+ if (string.IsNullOrWhiteSpace(unit)) throw new ArgumentException("Measured numeric filters require an explicit unit.");
165
+ var unitId = UnitResolver.Resolve(unit);
166
+ if (!UnitUtils.IsValidUnit(spec, unitId)) throw new ArgumentException("Filter unit does not match the field's specification.");
167
+ number = UnitUtils.ConvertToInternalUnits(number, unitId);
168
+ }
169
+ else if (unit != null) throw new ArgumentException("This numeric field has no measurable unit.");
170
+ return new ScheduleFilter(id, comparison, number);
171
+ }
172
+ throw new ArgumentException("Filter value does not match value_type.");
173
+ }
174
+ }
@@ -11,8 +11,16 @@ namespace RevitBridge.Tools
11
11
  /// </summary>
12
12
  internal sealed class ManageSelection : ITool
13
13
  {
14
- public string Name => "manage_selection";
15
- public IReadOnlyList<string> Effects => new[] { "ui", "model" };
14
+ public string Name => "manage_selection";
15
+ public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
16
+ public IReadOnlyList<string> Keywords => new[] { "select", "highlight", "zoom", "isolate", "show in revit", "selected elements", "current selection" };
17
+ public IReadOnlyList<ToolLimit> Limits => new[]
18
+ {
19
+ new ToolLimit("Permanent hiding or view visibility changes", "api", "View.HideElements; View.SetCategoryHidden"),
20
+ new ToolLimit("Saved selection sets", "api", "SelectionFilterElement.Create"),
21
+ };
22
+ public string? Verification => "reread";
23
+ public IReadOnlyList<string> Effects => new[] { "ui", "model" };
16
24
  public string Label => "Manage Selection";
17
25
  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.";
18
26
 
@@ -137,25 +145,25 @@ namespace RevitBridge.Tools
137
145
  compactParts.Add($"ids not found: {string.Join(", ", notFound)}");
138
146
  }
139
147
 
140
- if (isolateInView)
141
- {
142
- var view = uiDocument.ActiveGraphicalView;
143
- IReadOnlyList<string> warnings;
144
- try
145
- {
146
- if (view is null)
147
- throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
148
- warnings = ApplyTemporaryIsolate(doc, view, isolateTargets);
149
- }
150
- catch (Exception ex)
151
- {
152
- throw new InvalidOperationException($"Selection action '{action}' completed before temporary isolate failed: {ex.Message}. Selection/zoom changes were not rolled back.", ex);
153
- }
154
- if (warnings.Count > 0)
155
- {
156
- payload["commitWarnings"] = warnings;
157
- compactParts.Add($"{warnings.Count} Revit warning(s) auto-dismissed (see commitWarnings)");
158
- }
148
+ if (isolateInView)
149
+ {
150
+ var view = uiDocument.ActiveGraphicalView;
151
+ IReadOnlyList<string> warnings;
152
+ try
153
+ {
154
+ if (view is null)
155
+ throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
156
+ warnings = ApplyTemporaryIsolate(doc, view, isolateTargets);
157
+ }
158
+ catch (Exception ex)
159
+ {
160
+ throw new InvalidOperationException($"Selection action '{action}' completed before temporary isolate failed: {ex.Message}. Selection/zoom changes were not rolled back.", ex);
161
+ }
162
+ if (warnings.Count > 0)
163
+ {
164
+ payload["commitWarnings"] = warnings;
165
+ compactParts.Add($"{warnings.Count} Revit warning(s) auto-dismissed (see commitWarnings)");
166
+ }
159
167
  payload["viewId"] = view.Id.Value;
160
168
  payload["viewName"] = view.Name;
161
169
  if (isolateTargets.Count == 0)
@@ -204,28 +212,28 @@ namespace RevitBridge.Tools
204
212
  /// only changes it inside an open transaction — so exactly this branch wraps
205
213
  /// a small one while the tool stays Write = false.
206
214
  /// </summary>
207
- private static IReadOnlyList<string> ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
208
- {
209
- using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
210
- if (transaction.Start() != TransactionStatus.Started)
211
- throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
212
- var failureGuard = FailureGuard.Attach(transaction);
215
+ private static IReadOnlyList<string> ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
216
+ {
217
+ using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
218
+ if (transaction.Start() != TransactionStatus.Started)
219
+ throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
220
+ var failureGuard = FailureGuard.Attach(transaction);
213
221
  try
214
222
  {
215
223
  if (elementIds.Count == 0)
216
224
  view.DisableTemporaryViewMode(TemporaryViewMode.TemporaryHideIsolate);
217
225
  else
218
226
  view.IsolateElementsTemporary(elementIds);
219
- var status = transaction.Commit();
220
- var finalStatus = transaction.GetStatus();
221
- if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
222
- throw new InvalidOperationException($"The temporary-isolate commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
223
- return failureGuard.Warnings;
224
- }
225
- catch (Exception ex)
226
- {
227
- throw new InvalidOperationException($"Temporary isolate in view '{view.Name}' failed: {ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
228
- }
227
+ var status = transaction.Commit();
228
+ var finalStatus = transaction.GetStatus();
229
+ if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
230
+ throw new InvalidOperationException($"The temporary-isolate commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
231
+ return failureGuard.Warnings;
232
+ }
233
+ catch (Exception ex)
234
+ {
235
+ throw new InvalidOperationException($"Temporary isolate in view '{view.Name}' failed: {ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
236
+ }
229
237
  }
230
238
  }
231
239
  }